SchOS/.claude/memory/schfs-phase3-writes.md
Nexoniarz a1c965da1e include: one folder per subsystem, and includes that name it
Sixty-five headers sat side by side in include/, so `#include "irq.h"`
said nothing about where irq.h was or what it belonged to, and errno.h,
stat.h and fcntl.h at the top shadowed the system's for any host tool
built with -I./include. They are in folders now -- arch/ core/ fs/
dev/ gfx/ net/ uapi/ (and ldls/ for the Linux driver layer) -- and
every include names the folder, as Linux's do: "core/proc.h".

Done mechanically and checked the hard way: the kernel built from this
tree disassembles identically to the one built from the tree before
it. The full image boots, and userland is the same set of programs.
Paths in docs and comments follow; docs/guides/layout.md has a table
of what is where, and tools/fix-includes.py rewrites old-style
includes from the folders in the tree, for code written before this.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S1giNvrHTrPMPw7V6x1kQ4
2026-10-23 15:05:12 +02:00

194 lines
9.9 KiB
Markdown

---
name: schfs-phase3-writes
description: "SchFS phase 3 is done — full write API, in-RAM allocation map derived from the tree; the only gap left is VFS integration"
metadata:
node_type: memory
type: project
originSessionId: a665e14b-86bd-4b41-a228-51377f3e9225
modified: 2026-08-03T13:59:08.496Z
---
**Phase 3 done (2026-08-03), including VFS integration.** SchFS is a complete,
mountable, read-write filesystem. Follows [[schfs-phase2-mount]].
```
mkdir /mnt && mount -t schfs /dev/hdb /mnt
```
then ordinary `ls`/`cat`/`>`/`>>`/`mkdir`/`cp`/`rm`/`rmdir` all work on it.
**The VFS now has a real dispatch layer**: `struct fs_ops` in `include/fs/file.h`,
one table per filesystem, chosen by `iget()` from the device number and hung off
the inode. v1's implementations kept their bodies and are named `v1_*`; SchFS's
live in `kernel/fs/schfs/vfs.c`. **Adding a filesystem is a new table, not
another branch in fifteen functions.** `getdents64` is an fs_op for the same
reason — v1's `struct dirent` (16-bit inum, 14-byte name) cannot represent a
SchFS entry, so each filesystem emits the record stream itself.
Mounting is a substitution during path resolution (`mount_add`/`mount_cross` in
`fs.c`), recorded nowhere on disk. `..` at a mounted root has to be redirected
out explicitly, or the mount becomes a subtree you can enter and never leave.
**Two design decisions that contradict the older design doc — the doc is now
right, older notes are not:**
- *Free space is NOT tracked as tree keys.* Allocating from a tree-resident map
mutates the tree, which allocates, which changes the map. Instead the map is
**derived** at mount by walking the tree (`kernel/fs/schfs/space.c`) and lives
only in RAM. It cannot disagree with the filesystem because it is computed
from it. This is what caps a mounted volume at 64 GiB (`SCHFS_SPACE_PAGES`).
- *Data blocks carry a generation too* (`schfs_extent.gen`, 64-bit). Same rule
as tree nodes: a run stamped with the group being built may be overwritten in
place. Without it every 100-byte write copied a 4 KB block and split an
extent.
**Testing.** `make schfs-check` now runs three suites — add `schfs-objtest`
(`tools/schfs_objtest.c`, memory-backed, covers files/dirs/space accounting) to
the two that existed. `sh tools/mutate.sh` injects nine bugs; all nine must be
caught.
**The two tests that actually earn their keep**, both found by mutation testing
after everything else passed:
- Filling and emptying the volume *three times on one mount* and checking each
round stores the same amount. A leaked bitmap heals on remount, so a
remount-based check cannot see it; only spending the space again can.
- Overwriting a block **after a commit**. Within one transaction group both the
tree and the data rewrite in place, so a test that never commits never runs
the copy-on-write path at all.
`struct schfs_vol` is **not copyable** — `tree.dev` points at its own `dev`
field. Open into the storage it will live in.
## Migrating root off the xv6 legacy FS (2026-08-09)
Nexoniarz: kill the legacy FS, keep SchFS ("SchFS is goat"). Agreed — and it is
a **migration, not a rewrite**: SchFS is 4,204 lines across 7 files against
`kernel/fs/fs.c`'s 1,830, and it already builds images, mounts, and dispatches
through `struct fs_ops`. Every xv6 legacy problem (DIRSIZ 14, the 16-byte
dirent, the HTree byte layout, `ushort inum`) lives below a filesystem that has
already been replaced but never promoted.
**The one line holding xv6 in charge:**
kernel/fs/fs.c:1642: ip = (*spath == '/') ? iget(ROOTDEV, ROOTINO) : idup(start);
**Blocker 1, now cleared: `mkschfs` would not build on Windows.** It needed
`sys/sysmacros.h` (major/minor) and `lstat`/`readlink`/`S_ISLNK`, none of which
mingw has. Stubbed under `__MINGW32__` — honest here because Windows has no
mknod and `find sysroot -type l` returns nothing, so neither device nodes nor
symlinks can appear in the tree being imaged. **If symlinks are ever staged**
(procfs wants `/proc/<pid>/exe` to be one) this host can no longer build a
faithful image and the stub must become a real reparse-point read.
`make build/schfs.img` now works: 586 of 8000 blocks, 2344 KB, generation 2.
**Blocker 2, confirmed and not started:** booting with schfs.img as the root
disk panics `fsinit: invalid file system` — v1 `fsinit()` probes ROOTDEV for a
v1 superblock and halts. So the order is:
1. `fsinit`/`namei` reach the root through `fs_ops` instead of
`iget(ROOTDEV, ROOTINO)`.
2. A `ramroot`-equivalent SchFS image, so root can be swapped without a disk.
3. Point `ROOTDEV` at the SchFS volume; keep v1 mountable until it is proven.
4. Delete `fs.c`.
**Do this with a fresh session.** Swapping the root filesystem is the change
most able to leave an unbootable image, and step 1 wants room to boot, fail and
retry.
### Sizing step 1 (2026-08-09) -- it is small
`ops_for(dev)` in `kernel/fs/fs.c:278` already dispatches the whole VFS:
if (dev == SCHFS_DEV) return &schfs_ops;
if (dev == PROCFS_DEV) return &procfs_ops;
return &v1_ops; // legacy, only because it is the default
`struct fs_ops` (include/fs/file.h:35) is a complete vtable -- iload, istore,
ifree, read, write, truncate, dirops -- and **SchFS implements all of it**. So
`fs.c` is no longer "the filesystem"; it is the fallback branch. Killing it is
three edits, not a rewrite:
1. `fsinit()` stops requiring a v1 superblock on ROOTDEV; call `schfs_kmount()`.
2. `ROOTDEV` becomes `SCHFS_DEV` so `ops_for` returns `schfs_ops` for `/`.
3. `namei`'s `iget(ROOTDEV, ROOTINO)` uses `schfs_kroot()` for the root inode.
Then `v1_ops` has no caller and `fs.c` can go. Keep a build flag or a second
disk to boot v1 until SchFS has carried `/` through a full bootcheck run.
Nexoniarz calls the legacy one "ShitFS v1" -- worth knowing that "SchFS v1" in
older notes means the *xv6-derived* filesystem, not early SchFS.
### The recycle bin, removed 2026-08-09
`sys_delete` (syscall 51, NO_LINUX) set an on-disk inode flag `showmode = 0`,
and `fileread`/`filewrite` then refused every operation with a printf. A
"deleted" file still existed, still had its name, still held its blocks, and
answered read() with -1 -- and since nothing was freed, deleting filled the disk
faster. Both gates removed; the syscall entry and the on-disk field stay only
because pulling them touches the inode layout, and both die with fs.c.
### SchFS is the root filesystem -- default since 2026-08-09
`make` builds it, `make qemu` boots it, `tools/bootcheck.ps1` tests it. There is
no opt-in switch any more: `schfsroot`, `-DSCHFS_ROOT` and the `-SchFS` /
`-SchFSRoot` bootcheck switches are all gone. schunix.img depends on
`build/schfs.img`, and a kernel handed anything else on device 1 panics in
forkret().
Full suite green -- base, -Tty, -Ps, -Sock, -Fb, -Usb, -Evdev.
Three edits carried `/` over:
1. `kernel/proc/proc.c` forkret: `schfs_kmount(ROOTDEV)` + `schfs_kmounted()`
check, replacing `fsinit(ROOTDEV)`. Mounted there and not in main() because
mounting reads blocks, bread() sleeps, and sleeping needs a process context.
`schfs_kmount` returns **void** -- it reports failure by leaving the volume
unmounted, because elsewhere it is a probe.
2. `kernel/fs/fs.c` namei: root is `iget(SCHFS_DEV, schfs_kroot())`, so
`ops_for()` routes `/` through `schfs_ops`. SCHFS_DEV is 64 -- a synthetic
number for ops dispatch, not the disk the volume lives on.
3. `kernel/fs/log.c`: `begin_op`/`end_op` return immediately, the rest is
`#if 0`. The v1 log made multi-block v1 updates atomic; SchFS gets that from
CoW. They stay as no-ops because sys_fs.c calls them in dozens of places.
**The bug that cost the most, and would cost it again:** `sinit()` sets pid 1's
cwd with `namei("/")` in `main()`, *before* forkret mounts anything. The old v1
root survived that because `iget(ROOTDEV, ROOTINO)` is two compile-time
constants that touch no disk -- the inode fills in lazily at the first ilock.
SchFS reads its root object id off the uberblock, so pid 1 got a cwd built from
an unmounted volume. Absolute paths never consult cwd, so `exec("/bin/init")`
worked and everything looked healthy; the first *relative* lookup is init's
`open("console")`, which failed, so init had no fd 0/1/2, so every shell it
spawned exited at once and init respawned it forever. **Silent boot, no
`init: ready`, indistinguishable from a hang.** Fixed by re-pointing
`myproc()->cwd` after the mount.
Also: the kernel execs `/bin/init` (`lib/initcode.S`), not `/init`. Probing the
wrong path made lookup look broken when it was not.
**Still to do -- the v1 format code is compiled but unreachable from `/`.**
`fs.c` cannot simply be deleted: iget/ilock/iput/namei/readi, the whole VFS
layer, live in it too. Only the v1 on-disk format goes -- fsinit, the
superblock, bmap/balloc, `v1_ops`, and the third-party HTree/extent code -- plus
log.c, tools/mkfs.c and build/fs.img. The MIT attribution in NOTICE stays
regardless ([[schunix-licensing]]).
fs.img is still built: the -Usb suite uses it as the USB stick.
### Two harness bugs found doing this, both of which faked OS failures
- **bootcheck slept a flat 4s between commands.** `ls /bin | wc -l` overran it,
so the next command was typed while the shell was busy -- the tty echoed it,
no reader took it, and it landed concatenated onto the following one. The
transcript read `cat /proc/pcicat /proc/schfs` and three PCI checks failed,
looking exactly like a broken PCI driver. Now it waits for the prompt by
polling the logfile QEMU is writing. (It still must not read QEMU's stdout --
that deadlocks, see the script header.)
- **The USB stick was `$hdb`**, so the root volume was attached twice, once as
a disk and once over USB. That failed the storage checks *and* the xHCI
interrupt check. The stick is pinned to `build/fs.img` now and -Usb is green.
**The general lesson, twice over: a red check is not evidence until the harness
is ruled out.** Both of these looked like driver bugs.