Claude Code keeps per-project memory at <home>/.claude/projects/<key>/,
where <key> is the project's absolute path with every separator and the
drive colon turned into a dash. That makes it both rename-fragile and
machine-specific, and this project has hit each of those once:
- D:\DEV\Schunix -> D:\DEV\SchOS changed the key, and 43 memories were
invisible until moved across by hand.
- Development happens from Windows and from Linux, and each looks in a
different place for the same checkout.
Committing .claude/memory/ fixes both -- git carries it across renames
and between hosts -- and `import` puts it wherever the local machine
expects. .claude/settings.local.json stays ignored: those are per-machine
permissions.
The colon maps to a dash rather than being dropped. Deleting it gives
D-DEV-SchOS, which matches nothing and silently finds zero memories; that
was the first attempt. On Git Bash the key comes from `cygpath -w`,
because Claude Code is a Windows program and sees D:\DEV\SchOS, not
/d/DEV/SchOS.
194 lines
9.9 KiB
Markdown
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/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/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.
|