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.
117 lines
5.6 KiB
Zig
117 lines
5.6 KiB
Zig
//! Partition-table parsing, the policy the storage architecture places above the
|
|
//! block driver and below the filesystem (docs/file-system-development/
|
|
//! storage-architecture.md): read block 0, decide what block sub-ranges are
|
|
//! volumes, and read each volume's content identity. The block DRIVER never does
|
|
//! this — it clamps ranges it is told about; this is what tells it the numbers.
|
|
//!
|
|
//! Today: MBR (the four-entry table at offset 446) plus the bare-FAT case (a boot
|
|
//! sector right at LBA 0). GPT is the next entry in the identity ladder and slots
|
|
//! in here without touching anything above or below.
|
|
|
|
const std = @import("std");
|
|
|
|
/// One volume the parser found on the device: the block sub-range it occupies
|
|
/// and a content identity stable for the volume's life (the mount map keys on
|
|
/// it; the boot volume is recorded by it). `identity` is derived from the medium,
|
|
/// never from a port — a moved drive keeps it.
|
|
pub const Volume = struct {
|
|
base_lba: u64,
|
|
block_count: u64,
|
|
identity: u64,
|
|
};
|
|
|
|
/// The MBR disk signature (offset 440, 4 bytes LE) — a 32-bit id written at
|
|
/// partition time. Weak (dd-cloned disks share it) but on the medium, and the
|
|
/// simplest rung of the identity ladder; the fuller rungs (GPT partition GUID,
|
|
/// FAT volume serial) refine `identityOf` without changing the shape.
|
|
fn diskSignature(block0: []const u8) u32 {
|
|
if (block0.len < 444) return 0;
|
|
return std.mem.readInt(u32, block0[440..444], .little);
|
|
}
|
|
|
|
/// The identity of the volume at partition index `index`: the disk signature
|
|
/// paired with the index, so two partitions of one disk stay distinct. For a
|
|
/// bare FAT (no table) the index is 0.
|
|
fn identityOf(block0: []const u8, index: u8) u64 {
|
|
return (@as(u64, diskSignature(block0)) << 8) | index;
|
|
}
|
|
|
|
/// Whether block 0 looks like a partition table (the 0x55AA boot signature). A
|
|
/// bare FAT also carries it, so the caller distinguishes by whether any partition
|
|
/// entry is non-empty.
|
|
fn hasBootSignature(block0: []const u8) bool {
|
|
return block0.len >= 512 and block0[510] == 0x55 and block0[511] == 0xAA;
|
|
}
|
|
|
|
/// The first volume on a device whose block 0 is `block0` and whose whole-device
|
|
/// size is `device_blocks`, or null if none is found. An MBR with a non-empty
|
|
/// entry yields that partition's [start, size); otherwise a boot signature with
|
|
/// no partitions is treated as a bare FAT spanning the whole device.
|
|
pub fn firstVolume(block0: []const u8, device_blocks: u64) ?Volume {
|
|
if (!hasBootSignature(block0)) return null;
|
|
var index: u8 = 0;
|
|
while (index < 4) : (index += 1) {
|
|
const entry = block0[446 + @as(usize, index) * 16 ..][0..16];
|
|
const kind = entry[4];
|
|
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.
|
|
return .{ .base_lba = 0, .block_count = device_blocks, .identity = identityOf(block0, 0) };
|
|
}
|
|
|
|
test "an MBR with one partition yields its range and a distinct identity" {
|
|
var block0 = [_]u8{0} ** 512;
|
|
block0[510] = 0x55;
|
|
block0[511] = 0xAA;
|
|
std.mem.writeInt(u32, block0[440..444], 0xDEADBEEF, .little);
|
|
// partition 0: type 0x0c (FAT32 LBA), start 2048, size 100000
|
|
block0[446 + 4] = 0x0c;
|
|
std.mem.writeInt(u32, block0[446 + 8 ..][0..4], 2048, .little);
|
|
std.mem.writeInt(u32, block0[446 + 12 ..][0..4], 100000, .little);
|
|
const v = firstVolume(&block0, 200000).?;
|
|
try std.testing.expectEqual(@as(u64, 2048), v.base_lba);
|
|
try std.testing.expectEqual(@as(u64, 100000), v.block_count);
|
|
try std.testing.expectEqual((@as(u64, 0xDEADBEEF) << 8) | 0, v.identity);
|
|
}
|
|
|
|
test "a boot signature with no partitions is a bare FAT over the whole device" {
|
|
var block0 = [_]u8{0} ** 512;
|
|
block0[510] = 0x55;
|
|
block0[511] = 0xAA;
|
|
const v = firstVolume(&block0, 65536).?;
|
|
try std.testing.expectEqual(@as(u64, 0), v.base_lba);
|
|
try std.testing.expectEqual(@as(u64, 65536), v.block_count);
|
|
}
|
|
|
|
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);
|
|
}
|