Architecture — the layers and where the boundaries go
Five layers. The test of a boundary is whether a layer can be host-tested without the one below it, and the memory-manager rewrite is the evidence that this is worth insisting on: everything in that subsystem which could not be host-tested is where the defects were.
B4 files vibeos_fs_ops_t: FAT, ext2, NTFS, ISO9660, exFAT
B3 volumes partitions, mounting, which filesystem is on what
B2 block cache one cache, shared, the only place a sector is held
B1 block devices a request, a completion, and a reason when it fails
B0 drivers virtio-blk, AHCI, and whatever comes next
Today B4 is split across two directories, B2 is bypassed by the only filesystem that runs, and B1 is four function pointers with three shapes.
B0 — drivers
A driver registers a table and nothing else:
typedef struct vibeos_blk_driver {
const char *name;
uint32_t sector_bytes; /* 512 today; not assumed to be */
uint64_t sectors; /* how big the device is */
int (*submit)(void *ctx, vibeos_blk_request_t *req);
void *ctx;
} vibeos_blk_driver_t;
One entry point rather than three. read, read_many and write today are
the same operation with different shapes, which is why write has no
write_many — nobody noticed the asymmetry because nothing writes.
Registration, not weak symbols. The PE/COFF lesson is written down in CLAUDE.md: a weak definition in a different object from its caller links on Linux and fails on Windows. Every extension point here is a table.
A driver never blocks forever. Every wait is bounded and the bound is a counter, because virtio-net taught this project that a lock around something that already waited turns one stuck transmit into every core parked behind it.
B1 — block devices
The request is a value, not a set of arguments:
typedef struct vibeos_blk_request {
uint32_t device;
uint64_t lba;
uint32_t sectors;
void *buf;
int write;
vibeos_blk_result_t result; /* filled by the driver */
} vibeos_blk_request_t;
Two things this buys, and both are answers to defects this project has had.
A reason, not a zero. vibeos_blk_result_t distinguishes not issued,
device absent, timed out, medium error, short transfer and ok. Today
a caller cannot tell a disk that is missing from a disk that is broken, and the
FAT chain reader once returned an end-of-chain marker for a failed table read —
the same value a healthy last cluster returns.
Multiple devices. There is one global device today. A machine with a disk
and a CD, or a disk and a swap partition on another spindle, cannot be
described. device is an index into a small table.
B2 — the block cache
One cache. Every sector read by any filesystem passes through it, including FAT. That is the single largest structural change in this plan and it is early, because "two structures that hold the same fact" is the shape of four separate defects in this codebase — most recently a page cache that returned the frames of a different file because two cores placed entries at once.
It has its own lock, supplied by registration, for the reason CLAUDE.md gives after the fourth time: a layer with mutable statics and more than one possible caller locks itself.
B3 — volumes
Three things that are usually confused with each other, and this plan keeps them apart because each fails differently.
Reading a partition table
MBR and GPT. Exists as kernel/fs/partition.c, host-tested, never run. A
partition is a device to everything above: {device, first_sector, sectors},
which is the same shape B1 already takes and the same shape mm's
vibeos_swap_area_t already takes. That is not a coincidence worth losing — a
swap partition is a volume, and B3 is what fills that descriptor in.
GPT before MBR, and both. A machine that only understands MBR cannot read a disk any modern tool produces, and one that only understands GPT cannot read the boot media this project ships. The protective MBR on a GPT disk is exactly the trap: it parses as a valid MBR describing one huge partition, so a reader that tries MBR first and stops on success gets a plausible wrong answer rather than an error.
Probing what is on a volume
Given a volume, which filesystem is it? Each vibeos_fs_ops_t gains a probe
that reads its own superblock and says yes or no, and the volume layer asks
each in turn.
Probe order is a correctness question, not a preference. exFAT's boot sector is a FAT boot sector with different fields; a FAT probe that only checks the jump instruction and the signature says yes to an exFAT volume and then mounts it wrong. Each probe must identify its own filesystem positively and the order must be recorded with the reason.
Mounting more than one thing
There is one mount today, g_rootfs, a global. That is why the plan can say
"only FAT runs": there is nowhere to put a second filesystem even if it worked.
A mount table with paths — a small fixed array, since this kernel does not
allocate on these paths — and a lookup that resolves a path to (mount, tail).
Everything above stops taking a vibeos_fsmount_t * and starts taking a path.
Writing a partition table
Creating, deleting and formatting. This is the only part of B3 that can destroy data that was not this machine's to lose, and it is treated accordingly: every write to a table is refused unless the caller passes the current table's checksum, so a stale view cannot overwrite a newer one. See the phase.
B4 — files
vibeos_fs_ops_t, unchanged in shape. What changes is that every filesystem
reaches the disk the same way, and that the read contract stops conflating two
numbers:
/* Bytes delivered into buf. Never more than len. */
long vibeos_fs_read_at(mnt, node, offset, buf, len);
/* How long the file is. A different question, and asking it must not
* require a buffer that could hold the answer. */
int vibeos_fs_size(mnt, node, uint64_t *out_size);
That separation is what unblocks the memory manager: execve can then read a
header window while knowing the file's real length, and the 6 MiB of staging
buffers become a header window each.
Where the journal is not
It is not one of the five layers, and putting it in kernel/fs/ — where it is
today — says it is a filesystem concern. It is not.
A journal answers one question: do these writes happen together or not at all? That is a transaction, and transactions are orthogonal to both files and blocks. ext2 wants one; FAT does not have one; and the partition-table writer in I4c wants exactly the same thing when it puts the backup table down before the primary. Two customers in two different layers is the test of whether something belongs in a module of its own, and this passes it.
Its failure mode is different from everything else here too. The rest of this stack fails by returning the wrong data, which a test can see immediately. A journal fails by not being replayed, which is invisible until the power goes out — so its verification is "interrupt the write at every point and check what comes back", a different kind of harness from anything else in this plan.
So: kernel/txn/journal.c, above B2, used by whoever needs it.
The one thing that cannot wait
A journal controls the order in which writes reach the medium, and a cache that reorders around a barrier makes it worthless. That contract is between the journal and B2, and it has to exist before write-back is designed — not after.
/* Everything submitted before this reaches the medium before anything
* submitted after it. Not a flush: a flush empties the cache, a barrier
* only orders it, and a journal that used a flush would write the whole
* cache out on every transaction. */
int vibeos_blk_barrier(uint32_t device);
If I4 lands a write-back policy with no barrier in it, adding the journal later means rewriting the write path a second time. That is the whole reason this section is here rather than in the phase that builds the journal.
Where the mm page cache fits
It stays where it is, above B4, keyed by file identity rather than by sector. Decision D6 of the mm plan already settled that the two caches are not merged, and this plan does not reopen it — but it does make the reason cleaner: B2 caches sectors of a device, the mm cache holds pages of a file, and after this refactor the second sits on top of the first instead of beside it.