volume-manager: harden the probe and supervision from the V3 review

Five confirmed defects from the boundary review:

1. (security) The VM never checked a partition fit inside the device, so a
   crafted MBR could hand the driver a range whose base+lba wraps past a u32
   — panicking usb-storage in a loop, and at multi-volume overlapping a
   neighbour. This is the exact invariant the clamp's overflow-safety rests
   on. partition.firstVolume now skips any entry that runs past the device
   (host-tested), establishing the invariant where the untrusted bytes are
   first read.
2. (leak) The probe re-acquired a fresh block channel on every 500 ms retry,
   leaking a handle each time on a medium-absent device. The channel is now
   acquired once and kept.
3. (wedge) A failed spawn or defineRange stranded the volume with no retry;
   both now arm a backoff restart.
4. (loop) fat respawn had no exit-reason gate, no backoff, no crash-loop cap
   — a faulting filesystem respawned in a zero-delay loop, and a clean exit
   was resurrected. Supervision now mirrors the device manager: a clean exit
   is not restarted, a fault backs off, three fast deaths give up.
5. (removable) A device that parsed to no volume was terminal; it now keeps
   polling so an inserted medium is picked up — the removal-lifecycle trigger.

Known limitation (noted, not fixed here): if the VM itself crashes and init
restarts it, the orphaned fat keeps serving vfs while the new VM spawns a
second fat whose bind is refused — the same "manager restart re-learns the
world" gap the device manager also defers. The old fat keeps storage working.

Neutral: partition unit tests + fat-mount, volume-probe, block-range, logger
all green.
This commit is contained in:
Daniel Samson
2026-08-09 18:50:01 +01:00
parent a67a7015bf
commit 7c6ed2ca09
2 changed files with 97 additions and 14 deletions
@@ -56,6 +56,13 @@ pub fn firstVolume(block0: []const u8, device_blocks: u64) ?Volume {
const start = std.mem.readInt(u32, entry[8..12], .little);
const size = std.mem.readInt(u32, entry[12..16], .little);
if (kind == 0 or start == 0 or size == 0) continue;
// These bytes come off an untrusted removable medium. A partition that
// does not fit inside the device is not a partition — skip it. This is
// where the driver's confinement-safety invariant is established: the
// clamp's overflow-safety rests on base + count staying inside the
// device (usb-storage.zig resolveTransfer), which only holds because the
// range handed down is validated here. The subtraction cannot overflow.
if (start > device_blocks or device_blocks - start < size) continue;
return .{ .base_lba = start, .block_count = size, .identity = identityOf(block0, index) };
}
// No partition entries: a bare FAT spanning the device.
@@ -90,3 +97,20 @@ test "no boot signature is no volume" {
const block0 = [_]u8{0} ** 512;
try std.testing.expect(firstVolume(&block0, 65536) == null);
}
test "a partition that runs past the device is skipped, not trusted" {
var block0 = [_]u8{0} ** 512;
block0[510] = 0x55;
block0[511] = 0xAA;
// partition 0: start 0xFFFFFF00, size 0x400 — far past a 200000-block device.
block0[446 + 4] = 0x0c;
std.mem.writeInt(u32, block0[446 + 8 ..][0..4], 0xFFFFFF00, .little);
std.mem.writeInt(u32, block0[446 + 12 ..][0..4], 0x400, .little);
// partition 1: start 2048, size 1000 — fits.
block0[462 + 4] = 0x0c;
std.mem.writeInt(u32, block0[462 + 8 ..][0..4], 2048, .little);
std.mem.writeInt(u32, block0[462 + 12 ..][0..4], 1000, .little);
const v = firstVolume(&block0, 200000).?;
try std.testing.expectEqual(@as(u64, 2048), v.base_lba); // the fitting one, not the overflowing one
try std.testing.expectEqual(@as(u64, 1000), v.block_count);
}