# Finishing the storage stack: the S1–S5 plan *2026-08-09. Continues the volume-manager track (V0–V5, on main) from "one FAT volume" to "any filesystem, any number of volumes, identified by content, remounting where they belong, surviving a driver crash." Executes the settled design in [storage-architecture.md](file-system-development/storage-architecture.md) and [storage-design-rationale.md](file-system-development/storage-design-rationale.md). Track discipline as always: work on main; one commit per coherent step with `git commit -F` (no `-m`, no co-author trailer); every new test shown to FAIL against the old behavior; one QEMU suite at a time; CSV is configuration read by the volume manager (the policy), never itself policy; bounds discipline (`tools/check-bounds.py` gate); adversarial boundary review at each phase.* ## Where V0–V4 left it The volume manager probes ONE storage device, parses its MBR (rung-4 identity = `(diskSignature<<8)|index`), spawns ONE FAT service confined to that partition's badge-scoped block range, supervises it, and unmounts it when the device is pulled. `partition.firstVolume` returns the FIRST partition; `openAnyStorage` adopts the FIRST device; `var volume: ?Volume` and `volume_id = 1` are singular; `filesystem_binary` and fat's mount prefixes are hardcoded; `medium_changed` is published by the driver but consumed by no one; remount-on-replug is bench-pending. ## The five phases and how they depend ``` S1 identity ladder ──► S2 mount map ──► S3 multi-volume ──► S4 exFAT (needs S2+S3) S5 removal robustness ── independent; single-volume ── may land any time ``` - **S1** grows the identity read off the medium (GPT GUID, FAT serial+label). It comes FIRST because a volume's mount name is now its identity (below), and the friendly form of that name is the FAT label / GPT name S1 parses. - **S2** moves the last policy out of hardcode into `volumes.csv` + `filesystems.csv`, and names each volume by its identity — no port-name. - **S3** generalizes to N volumes across N devices. - **S4** adds exFAT — a COMPLETE second engine that proves the V1 harness extraction. Needs S2 (to route by signature) and S3 (to run a second volume). - **S5** closes the removal-lifecycle gaps. Independent of the rest; single-volume. Recommended build order is S1 → S2 → S3 → S4 → S5. S5 may be pulled earlier. --- ## S1 — the identity ladder **Goal.** Grow `system/services/volume-manager/partition.zig` from the single rung-4 identity into a ladder that reads the richest available content identity: GPT partition GUID (rung 1, 128-bit), FAT volume serial + label (rung 3), MBR signature + index (rung 4, kept), bare-FAT (kept, enriched to its serial). The `u64` identity becomes a small tagged struct `Identity{ rung, key: u128, label, has_label }`. Because GPT metadata is at LBA 1 and the entry array beyond it, and the FAT serial is in each partition's VBR, `firstVolume` stops taking one preloaded block-0 slice and takes a `SectorReader` (context + read-one-sector fn, mirroring the engine's `BlockDevice` vtable) — host-testable against a RAM-disk reader exactly as the four existing `partition.zig` tests are. **Key touchpoints.** `partition.zig` (the `Rung`/`Identity`/`SectorReader` types; `gptFirstVolume`; `fatIdentity`; `firstVolume` control flow — GPT authoritative, else MBR walk skipping type-0xEE, else bare-FAT, each preferring the FAT serial over the disk signature); `volume-manager.zig` (`Volume.identity` type; a `ProbeReader` over the existing 512-byte bounce; the probe log prints `identity.key`); `build.zig` (add `b.dependency("volume-manager", .{})` to the package-test aggregation loop so the host fixtures run under root `zig build test`). Declared bound `gpt_entry_scan_maximum = 128` with the full bounds block; `sector_bytes`/`fat_label_bytes` named consts. **Steps (commits).** (1) The SectorReader/Identity flag-day — pure refactor, no new behavior, all existing tests green. (2) GPT parsing (rung 1) — protective-MBR + `EFI PART` signature + header CRC-32 + per-entry overflow-safe range validation (the confinement-safety invariant the driver's clamp rests on, extended to GPT). (3) FAT serial + label (rung 3), preferred over the disk signature; `Identity.eql`. (4) Test wiring, `check-bounds.py`, full suite, docs, memory. **Discrimination.** Host: a GPT disk yields `rung==.gpt_guid` + the exact GUID key (old code walks the 0xEE protective entry as an ordinary partition); a GPT entry past the device is skipped, an out-of-device-only GPT returns null (the security-boundary guard); an invalid GPT header is not a volume; a bare FAT reports its real serial `0x12345678` not the rung-4 pseudo-signature; an MBR+FAT partition prefers the serial over the disk signature. On-image: the `volume-probe` QEMU regex tightens to `volume 0x0*12345678` — the boot image's real FAT32 serial reaches the running log. **Top risks.** Adversarial GPT input from untrusted media (huge entry counts, bogus offsets, overflowing ranges) — mitigated by header CRC + size bounds + `gpt_entry_scan_maximum` + per-entry overflow-safe validation under boundary review. The `std.hash.crc` symbol in Zig 0.16 is unverified (fallback: a ~15-line reflected CRC-32, poly `0xEDB88320`, used by both parser and fixtures so they never drift onto magic constants). --- ## S2 — the mount map: volumes.csv + filesystems.csv **Goal.** Move the last two pieces of storage policy out of hardcode into configuration read by the volume manager. `filesystems.csv` (content signature → filesystem binary) so the VM picks the binary from the probed signature; `volumes.csv` (identity → mount prefix, danos's fstab) as the explicit override for a volume the user wants at a fixed path. **The default mount name is the volume's own identity, never a port or role name**: `/volumes/