6 Commits
Author SHA1 Message Date
Daniel Samson ea8ccf65d0 volume-manager: cover the GPT non-128 entry-size offset path (S1 review)
The S1 adversarial boundary review found the off = (i*entry_size) % 512
arithmetic tested only for 128-byte entries. Add a test with 256-byte entries
and the sole valid entry at index 1 (offset 256), exercising the non-zero-offset
path. No code change — the parser was already correct (off is always a multiple
of entry_size >= 128, so off + 128 <= 512); this closes the coverage gap.
2026-08-09 23:37:52 +01:00
Daniel Samson aca3d5855a volume-manager: S1 close-out — partition fixtures in the root aggregate; docs (S1)
Add volume-manager to build.zig's root package-test loop, so `zig build test`
runs the partition parser's nine host tests and a fixture added there can never
be silently skipped. Flip the identity-ladder build status in
storage-design-rationale.md: rungs 1 (GPT partition GUID) and 3 (FAT serial +
label) are built, joining rung 4; rung 2 (filesystem UUID) waits on a non-FAT
engine; the volumes.csv map and the id-derived mount path land with S2.
2026-08-09 23:26:12 +01:00
Daniel Samson 5637d0e5fc device-manager: the entries-per-reply test asserts 7 (the current shape), not 10
A pre-existing stale assertion, surfaced by wiring volume-manager into the root
test aggregate (its partition fixtures now run under `zig build test`).
entries_per_reply is computed as (packet_maximum 256 - prefix 16) / sizeof
ChildEntry 32 = 7; the test asserted 10, the value the old count-header layout
carried. No behavior change — the enumerate producer and consumers already page
by the real capacity; only the test documented an obsolete number.
2026-08-09 23:26:12 +01:00
Daniel Samson 48ab12e262 volume-manager: the FAT volume serial + label is the identity (rung 3) (S1)
Rung 3, stronger than the MBR disk signature. fatIdentity reads the VBR at the
partition start — 0x55AA plus a 0x28/0x29 extended boot signature; FAT32 iff
fat_size_16 == 0; BS_VolID and BS_VolLab at the FAT12/16 vs FAT32 EBR offsets,
cross-checked against fat/on-disk.zig. firstVolume now prefers it over
mbrIdentity in both the MBR-entry path and the bare-FAT fallback, keeping the
rung-4 id when the VBR is not an extended FAT. The serial becomes the identity
key (the id); the label becomes the display name. Two host tests — a bare FAT32
reports its serial + label; an MBR FAT partition prefers the serial while a
non-FAT partition keeps rung 4 — both FAIL with the preference neutralized (2/9)
and pass with it (9/9). On-image witness: the volume-probe QEMU regex tightens to
the boot image's real serial 0x12345678, which before rung 3 was the ~0x0
pseudo-signature read from VBR offset 440.
2026-08-09 23:22:04 +01:00
Daniel Samson 020e31bc8f volume-manager: GPT parsing — the partition GUID is the id, the name is the label (S1)
Rung 1 of the identity ladder. A protective MBR (a type-0xEE entry) routes
probing to the GPT, authoritatively: gptFirstVolume verifies the LBA-1 header's
'EFI PART' signature and a header CRC-32 (inline reflected poly 0xEDB88320,
shared with the fixtures so parser and tests never drift onto a magic constant),
then walks the entry array — bounded by the declared gpt_entry_scan_maximum —
for the first entry with a non-zero type GUID and an overflow-safe in-device
range. That range check is the confinement-safety guard the driver's clamp
rests on, the invariant firstVolume already enforces for MBR, extended to
untrusted GPT metadata. The unique partition GUID becomes the identity key (the
id / mount-path handle); the 36-char partition name becomes the display label.
Three host tests (GUID-as-id; entry-past-device skipped and an all-out-of-range
table is null; a broken header/CRC is not a volume) — all three FAIL with the
GPT branch neutralized (3/7) and pass with it (7/7). Entry-array CRC deferred
(correctness-only; the range check carries the safety property).
2026-08-09 23:15:23 +01:00
Daniel Samson c81120ef0f volume-manager: partition parser takes a SectorReader; identity is a tagged Identity (S1)
The identity ladder's flag-day — no behavior change. partition.firstVolume stops
taking one preloaded block-0 slice and takes a SectorReader (a read-one-sector
fn), so it can reach GPT metadata at LBA 1 and each partition's VBR on demand
(the next commits). The u64 identity becomes Identity{rung,key,label}: key is the
id (the mount path derives from it), label is display metadata (empty at rung 4).
Identity equality is id-only (rung+key) — the label never enters it. Only rung-4
(MBR sig+index / bare-FAT index 0) is produced, byte-identical to before; the
four host tests port to a RAM-disk reader, and fat-mount/volume-probe/
volume-removal stay green.
2026-08-09 23:06:23 +01:00
6 changed files with 440 additions and 51 deletions
+1
View File
@@ -442,6 +442,7 @@ pub fn build(b: *std.Build) void {
csv_library,
xkeyboard_config_library,
b.dependency("fat", .{}),
b.dependency("volume-manager", .{}),
b.dependency("display", .{}),
b.dependency("ps2-bus", .{}),
b.dependency("usb-hid", .{}),
@@ -215,16 +215,16 @@ matrix-proven shape; genuinely open.
broke whenever a drive changed ports or enumeration order; `UUID=` entries
exist because device-path identity failed. danos skips that era: the mount
map (`volumes.csv` — configuration, read by the volume manager) keys on
**content identity, never port or discovery order**. Build status: only
rung 4 (MBR signature + partition index) is implemented today; the fuller
rungs and the `volumes.csv` map itself land with the identity ladder, so
today a single volume mounts at the fixed `/volumes/usb` and its recorded
identity is not yet consulted to pick a path. The target ladder the prober
reads off the medium, strongest first:
1. GPT partition GUID — 128-bit, unique, stable for the volume's life *(planned)*;
**content identity, never port or discovery order**. Build status: rungs 1,
3, and 4 (GPT partition GUID, FAT serial + label, MBR signature + index) are
implemented (S1); rung 2 waits on a non-FAT engine. The `volumes.csv` map and
the id-derived mount path land with S2, so today a single volume still mounts
at the fixed `/volumes/usb` and its recorded identity is not yet consulted to
pick a path. The ladder the prober reads off the medium, strongest first:
1. GPT partition GUID — 128-bit, unique, stable for the volume's life — **built (S1)**;
2. filesystem UUID (ext-family and most modern formats, in the superblock) *(planned)*;
3. FAT volume serial + label — 32 bits, weak (dd-cloned sticks share it)
but what real sticks carry *(planned)*;
but what real sticks carry — **built (S1)**;
4. MBR disk signature + partition index — **built**; a bare FAT with no
table takes index 0 over the whole device;
5. nothing — an anonymous volume: generated mount name, no persistence *(planned)*.
@@ -212,8 +212,9 @@ test "a tree report fits the push floor with the header folded in" {
try std.testing.expectEqual(@as(usize, 48), @sizeOf(ChildAdded));
try std.testing.expectEqual(envelope.post_maximum, Protocol.event_maximum);
try std.testing.expect(Protocol.event_maximum <= envelope.post_maximum);
// Ten records per enumerate reply — what the old count-header layout carried.
try std.testing.expectEqual(@as(usize, 10), entries_per_reply);
// Seven records per enumerate reply: (packet_maximum 256 - prefix 16) / 32.
// (The old count-header layout carried ten; this asserts the current shape.)
try std.testing.expectEqual(@as(usize, 7), entries_per_reply);
}
test "the verb and event numbering, and the device id in the header" {
+407 -30
View File
@@ -1,54 +1,258 @@
//! 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
//! storage-architecture.md): read the medium, 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.
//! Reads happen through a `SectorReader` (not one preloaded block-0 slice) so the
//! parser can reach GPT metadata at LBA 1, the entry array beyond it, and each
//! partition's VBR on demand. The identity it returns is a tagged `Identity`: the
//! `key` is the id (the mount path is derived from it — a stable, unique,
//! content-derived handle), and `label` is display metadata (the FAT volume label
//! or the GPT partition name), never part of the id. Today's rung is MBR/bare-FAT;
//! GPT (rung 1) and the FAT serial (rung 3) slot in without changing the shape.
const std = @import("std");
/// A single 512-byte sector's worth of bytes. The parser assumes 512-byte
/// logical sectors (4Kn media is a separate concern, noted in the plan).
pub const sector_bytes = 512;
/// bound: bytes of a volume's display label the parser records (a GPT partition
/// name is 36 UTF-16 units; a FAT volume label is 11 bytes; 36 covers both)
/// decided-by: hardware
/// protects: the Identity.label buffer
/// at-limit: degrade - a longer name is truncated to this many ASCII bytes
/// observed-by: a volume whose displayed label is clipped
pub const label_maximum = 36;
/// Which rung of the identity ladder produced this identity. The rung tags the
/// `key` namespace so a FAT serial and an MBR signature that happen to share bits
/// stay distinct, and it drives how the mount path is rendered from the id.
pub const Rung = enum(u8) {
gpt_guid = 1,
filesystem_uuid = 2, // reserved: no non-FAT engine reads a superblock UUID yet
fat_serial = 3,
mbr_index = 4,
anonymous = 5,
};
/// A volume's content identity. `key` is the ID — the stable, unique handle the
/// mount path is derived from and the mount map keys on. `label` is DISPLAY
/// metadata (FAT volume label / GPT partition name), exposed to a UI but never
/// part of the path; two volumes with the same label but different keys are
/// different volumes. Derived from the medium, never from a port.
pub const Identity = struct {
rung: Rung,
key: u128 = 0,
label: [label_maximum]u8 = [_]u8{0} ** label_maximum,
label_len: u8 = 0,
pub fn labelSlice(self: *const Identity) []const u8 {
return self.label[0..self.label_len];
}
/// Identity equality is the ID (rung + key) only — the label is display
/// metadata and does not enter it. Same rung + same key means the same
/// volume (the dd-cloned-media case the duplicate policy is for).
pub fn eql(a: Identity, b: Identity) bool {
return a.rung == b.rung and a.key == b.key;
}
};
/// 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.
/// and its content identity.
pub const Volume = struct {
base_lba: u64,
block_count: u64,
identity: u64,
identity: Identity,
};
/// Read sectors on demand. `context` + `readFn` mirror the FAT engine's
/// `BlockDevice` vtable; `readFn` returns false past the end of the device or on
/// an I/O error, which the parser treats as "no volume".
pub const SectorReader = struct {
context: *anyopaque,
readFn: *const fn (context: *anyopaque, lba: u64, buffer: *[sector_bytes]u8) bool,
pub fn read(self: SectorReader, lba: u64, buffer: *[sector_bytes]u8) bool {
return self.readFn(self.context, lba, buffer);
}
};
/// 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.
/// last rung of the identity ladder; the fuller rungs (GPT GUID, FAT serial)
/// take precedence when present.
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
/// The rung-4 identity of the volume at partition `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;
/// bare FAT (no table) the index is 0. Carries no label.
fn mbrIdentity(block0: []const u8, index: u8) Identity {
return .{ .rung = .mbr_index, .key = (@as(u128, 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.
/// Whether a block looks like a boot sector / 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;
/// GPT header signature at LBA 1.
const gpt_signature = "EFI PART";
/// bound: GPT partition entries scanned before the prober gives up
/// decided-by: ours
/// protects: the entry-array scan loop from an untrusted num_partition_entries
/// at-limit: degrade - stop scanning; a device whose usable entry sits past the
/// cap is treated as having no GPT volume (real tables carry <=128 entries)
/// observed-by: the gpt-entry-past-device host test
const gpt_entry_scan_maximum = 128;
/// Reflected CRC-32 (polynomial 0xEDB88320) — the ISO-HDLC variant GPT uses for
/// its header checksum. Inlined so the parser and the host fixtures compute it
/// the same way and never drift onto a magic constant.
fn crc32(bytes: []const u8) u32 {
var c: u32 = 0xFFFFFFFF;
for (bytes) |b| {
c ^= b;
var k: u8 = 0;
while (k < 8) : (k += 1) {
c = if (c & 1 != 0) (c >> 1) ^ 0xEDB88320 else c >> 1;
}
}
return c ^ 0xFFFFFFFF;
}
/// A GPT disk carries a protective MBR: a boot-signed block 0 with a partition
/// entry of type 0xEE. Its presence routes probing to the GPT (authoritative).
fn isProtectiveMbr(block0: []const u8) bool {
if (!hasBootSignature(block0)) return false;
var index: usize = 0;
while (index < 4) : (index += 1) {
if (block0[446 + index * 16 + 4] == 0xEE) return true;
}
return false;
}
/// Copy the GPT partition name (36 UTF-16LE units, the 72 bytes at entry+56)
/// into the identity's display label as ASCII, dropping non-ASCII units.
fn setLabelFromUtf16(id: *Identity, name_bytes: []const u8) void {
var out: usize = 0;
var i: usize = 0;
while (i + 1 < name_bytes.len and out < label_maximum) : (i += 2) {
const unit = std.mem.readInt(u16, name_bytes[i..][0..2], .little);
if (unit == 0) break;
if (unit < 0x80) {
id.label[out] = @intCast(unit);
out += 1;
}
}
id.label_len = @intCast(out);
}
/// The first GPT volume, or null if LBA 1 is not a valid GPT header or no entry
/// validates. The header CRC-32 and the per-entry overflow-safe range check are
/// the confinement-safety guards the driver's clamp rests on — the invariant
/// firstVolume documents for MBR, extended to untrusted GPT metadata. The
/// entry-array CRC is deferred (correctness-only; the range check carries safety).
fn gptFirstVolume(reader: SectorReader, device_blocks: u64) ?Volume {
var header: [sector_bytes]u8 = undefined;
if (!reader.read(1, &header)) return null;
if (!std.mem.eql(u8, header[0..8], gpt_signature)) return null;
const header_size = std.mem.readInt(u32, header[12..16], .little);
if (header_size < 92 or header_size > sector_bytes) return null;
const stored_crc = std.mem.readInt(u32, header[16..20], .little);
var check: [sector_bytes]u8 = undefined;
@memcpy(check[0..header_size], header[0..header_size]);
@memset(check[16..20], 0);
if (crc32(check[0..header_size]) != stored_crc) return null;
const entry_lba = std.mem.readInt(u64, header[72..80], .little);
const num_entries = std.mem.readInt(u32, header[80..84], .little);
const entry_size = std.mem.readInt(u32, header[84..88], .little);
if (entry_size != 128 and entry_size != 256 and entry_size != 512) return null;
if (entry_lba == 0 or entry_lba >= device_blocks) return null;
const scan = @min(num_entries, gpt_entry_scan_maximum);
var sector_buf: [sector_bytes]u8 = undefined;
var loaded: u64 = std.math.maxInt(u64);
var i: u32 = 0;
while (i < scan) : (i += 1) {
const abs = @as(u64, i) * entry_size;
const lba = entry_lba + abs / sector_bytes;
const off = @as(usize, @intCast(abs % sector_bytes));
if (lba != loaded) {
if (!reader.read(lba, &sector_buf)) return null;
loaded = lba;
}
const entry = sector_buf[off..][0..128]; // the fields we read live in the first 128 bytes
var type_nonzero = false;
for (entry[0..16]) |b| {
if (b != 0) {
type_nonzero = true;
break;
}
}
if (!type_nonzero) continue;
const start = std.mem.readInt(u64, entry[32..40], .little);
const end = std.mem.readInt(u64, entry[40..48], .little); // inclusive last LBA
// Untrusted range from removable media: overflow-safe validation. Reject a
// partition that starts at 0, is reversed, or ends outside the device; only
// then is start + count <= device_blocks guaranteed for the driver's clamp.
if (start == 0 or end < start or end >= device_blocks) continue;
var id = Identity{ .rung = .gpt_guid, .key = std.mem.readInt(u128, entry[16..32], .little) };
setLabelFromUtf16(&id, entry[56..128]);
return .{ .base_lba = start, .block_count = end - start + 1, .identity = id };
}
return null;
}
/// Trim trailing spaces (FAT labels are space-padded) and copy into the display
/// label, clamped to label_maximum.
fn setFatLabel(id: *Identity, label: []const u8) void {
var end: usize = label.len;
while (end > 0 and label[end - 1] == ' ') : (end -= 1) {}
const n = @min(end, label_maximum);
@memcpy(id.label[0..n], label[0..n]);
id.label_len = @intCast(n);
}
/// The FAT volume serial (BS_VolID) + label (BS_VolLab) read from the VBR at
/// `start_lba` — rung 3, stronger than the MBR disk signature. Null if the
/// sector is not an extended FAT boot record (no 0x55AA, or no 0x28/0x29
/// extended boot signature). FAT32 is distinguished by fat_size_16 == 0; the
/// serial and label live at different EBR offsets for FAT12/16 vs FAT32 (the
/// offsets are cross-checked against system/services/fat/on-disk.zig).
fn fatIdentity(reader: SectorReader, start_lba: u64) ?Identity {
var vbr: [sector_bytes]u8 = undefined;
if (!reader.read(start_lba, &vbr)) return null;
if (vbr[510] != 0x55 or vbr[511] != 0xAA) return null;
const is_fat32 = std.mem.readInt(u16, vbr[22..24], .little) == 0;
const sig_off: usize = if (is_fat32) 66 else 38;
if (vbr[sig_off] != 0x28 and vbr[sig_off] != 0x29) return null;
const id_off: usize = if (is_fat32) 67 else 39;
const label_off: usize = if (is_fat32) 71 else 43;
var id = Identity{ .rung = .fat_serial, .key = std.mem.readInt(u32, vbr[id_off..][0..4], .little) };
setFatLabel(&id, vbr[label_off..][0..11]);
return id;
}
/// The first volume on the device `reader` addresses, whose whole-device size is
/// `device_blocks`, or null if none is found. A GPT disk (protective MBR) is
/// handled by GPT, authoritatively — its null is final. Otherwise 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 device.
pub fn firstVolume(reader: SectorReader, device_blocks: u64) ?Volume {
var block0: [sector_bytes]u8 = undefined;
if (!reader.read(0, &block0)) return null;
if (!hasBootSignature(&block0)) return null;
if (isProtectiveMbr(&block0)) return gptFirstVolume(reader, device_blocks);
var index: u8 = 0;
while (index < 4) : (index += 1) {
const entry = block0[446 + @as(usize, index) * 16 ..][0..16];
@@ -63,12 +267,29 @@ pub fn firstVolume(block0: []const u8, device_blocks: u64) ?Volume {
// 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) };
return .{ .base_lba = start, .block_count = size, .identity = fatIdentity(reader, start) orelse mbrIdentity(&block0, index) };
}
// No partition entries: a bare FAT spanning the device.
return .{ .base_lba = 0, .block_count = device_blocks, .identity = identityOf(block0, 0) };
return .{ .base_lba = 0, .block_count = device_blocks, .identity = fatIdentity(reader, 0) orelse mbrIdentity(&block0, 0) };
}
/// A read-only RAM disk over a byte slice of sectors, for the host tests.
const RamDisk = struct {
sectors: []const u8,
fn readFn(context: *anyopaque, lba: u64, buffer: *[sector_bytes]u8) bool {
const self: *const RamDisk = @ptrCast(@alignCast(context));
const off = lba * sector_bytes;
if (off + sector_bytes > self.sectors.len) return false;
@memcpy(buffer, self.sectors[off..][0..sector_bytes]);
return true;
}
fn reader(self: *const RamDisk) SectorReader {
return .{ .context = @constCast(self), .readFn = readFn };
}
};
test "an MBR with one partition yields its range and a distinct identity" {
var block0 = [_]u8{0} ** 512;
block0[510] = 0x55;
@@ -78,24 +299,28 @@ test "an MBR with one partition yields its range and a distinct identity" {
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).?;
const disk = RamDisk{ .sectors = &block0 };
const v = firstVolume(disk.reader(), 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);
try std.testing.expectEqual(Rung.mbr_index, v.identity.rung);
try std.testing.expectEqual((@as(u128, 0xDEADBEEF) << 8) | 0, v.identity.key);
}
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).?;
const disk = RamDisk{ .sectors = &block0 };
const v = firstVolume(disk.reader(), 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);
const disk = RamDisk{ .sectors = &block0 };
try std.testing.expect(firstVolume(disk.reader(), 65536) == null);
}
test "a partition that runs past the device is skipped, not trusted" {
@@ -110,7 +335,159 @@ test "a partition that runs past the device is skipped, not trusted" {
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).?;
const disk = RamDisk{ .sectors = &block0 };
const v = firstVolume(disk.reader(), 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);
}
/// A single 128-byte GPT partition entry for the tests.
fn gptEntry(type_nonzero: bool, unique_guid: u128, start: u64, end: u64) [128]u8 {
var e = [_]u8{0} ** 128;
if (type_nonzero) e[0] = 0x01; // any non-zero byte makes the type GUID non-zero
std.mem.writeInt(u128, e[16..32], unique_guid, .little);
std.mem.writeInt(u64, e[32..40], start, .little);
std.mem.writeInt(u64, e[40..48], end, .little);
return e;
}
/// Lay out a disk with `entry_size`-spaced GPT entries: protective MBR (LBA 0),
/// GPT header with a correct CRC (LBA 1), the entry array (LBA 2+).
fn buildGptDiskSized(disk: []u8, entries: []const [128]u8, entry_size: u32) void {
@memset(disk, 0);
disk[510] = 0x55;
disk[511] = 0xAA;
disk[446 + 4] = 0xEE; // protective entry type
std.mem.writeInt(u32, disk[446 + 8 ..][0..4], 1, .little);
std.mem.writeInt(u32, disk[446 + 12 ..][0..4], 0xFFFFFFFF, .little);
const h = disk[sector_bytes..][0..sector_bytes];
@memcpy(h[0..8], gpt_signature);
std.mem.writeInt(u32, h[12..16], 92, .little); // header_size
std.mem.writeInt(u64, h[72..80], 2, .little); // partition_entry_lba
std.mem.writeInt(u32, h[80..84], @intCast(entries.len), .little);
std.mem.writeInt(u32, h[84..88], entry_size, .little); // size_of_partition_entry
@memset(h[16..20], 0);
std.mem.writeInt(u32, h[16..20], crc32(h[0..92]), .little);
const step: usize = @intCast(entry_size);
var i: usize = 0;
while (i < entries.len) : (i += 1) {
const abs = 2 * sector_bytes + i * step;
@memcpy(disk[abs..][0..128], &entries[i]);
}
}
/// The common 128-byte-entry case.
fn buildGptDisk(disk: []u8, entries: []const [128]u8) void {
buildGptDiskSized(disk, entries, 128);
}
test "a GPT disk yields the partition GUID as the identity id" {
var disk = [_]u8{0} ** (4 * sector_bytes);
const guid: u128 = 0x112233445566778899AABBCCDDEEFF00;
const entries = [_][128]u8{gptEntry(true, guid, 2048, 4095)};
buildGptDisk(&disk, &entries);
const rd = RamDisk{ .sectors = &disk };
const v = firstVolume(rd.reader(), 200000).?;
try std.testing.expectEqual(@as(u64, 2048), v.base_lba);
try std.testing.expectEqual(@as(u64, 2048), v.block_count); // 4095 - 2048 + 1
try std.testing.expectEqual(Rung.gpt_guid, v.identity.rung);
try std.testing.expectEqual(guid, v.identity.key);
}
test "a GPT entry past the device is skipped; an all-out-of-range table is no volume" {
var disk = [_]u8{0} ** (4 * sector_bytes);
const entries = [_][128]u8{
gptEntry(true, 0xAAA, 2048, 999999), // ends past a 200000-block device
gptEntry(true, 0xBBB, 4096, 8191), // fits
};
buildGptDisk(&disk, &entries);
const rd = RamDisk{ .sectors = &disk };
const v = firstVolume(rd.reader(), 200000).?;
try std.testing.expectEqual(@as(u64, 4096), v.base_lba); // the fitting one, not the overflowing one
try std.testing.expectEqual(@as(u128, 0xBBB), v.identity.key);
var solo_disk = [_]u8{0} ** (4 * sector_bytes);
const solo = [_][128]u8{gptEntry(true, 0xAAA, 2048, 999999)};
buildGptDisk(&solo_disk, &solo);
const rd2 = RamDisk{ .sectors = &solo_disk };
try std.testing.expect(firstVolume(rd2.reader(), 200000) == null);
}
test "a protective MBR with a broken GPT header is not a volume" {
var disk = [_]u8{0} ** (4 * sector_bytes);
const entries = [_][128]u8{gptEntry(true, 0xCCC, 2048, 4095)};
buildGptDisk(&disk, &entries);
disk[sector_bytes] = 'X'; // wreck the 'EFI PART' signature
const rd = RamDisk{ .sectors = &disk };
try std.testing.expect(firstVolume(rd.reader(), 200000) == null);
var bad_crc = [_]u8{0} ** (4 * sector_bytes);
buildGptDisk(&bad_crc, &entries);
bad_crc[sector_bytes + 16] ^= 0xFF; // corrupt a header-CRC byte
const rd2 = RamDisk{ .sectors = &bad_crc };
try std.testing.expect(firstVolume(rd2.reader(), 200000) == null);
}
test "a bare FAT32 reports its volume serial and label as the identity" {
var block0 = [_]u8{0} ** 512;
block0[510] = 0x55;
block0[511] = 0xAA;
std.mem.writeInt(u16, block0[22..24], 0, .little); // fat_size_16 == 0 → FAT32
block0[66] = 0x29; // FAT32 extended boot signature
std.mem.writeInt(u32, block0[67..71], 0x12345678, .little); // BS_VolID
@memcpy(block0[71..82], "DANOS "); // BS_VolLab, space-padded to 11
const disk = RamDisk{ .sectors = &block0 };
const v = firstVolume(disk.reader(), 65536).?;
try std.testing.expectEqual(@as(u64, 0), v.base_lba);
try std.testing.expectEqual(Rung.fat_serial, v.identity.rung);
try std.testing.expectEqual(@as(u128, 0x12345678), v.identity.key);
try std.testing.expectEqualStrings("DANOS", v.identity.labelSlice());
}
test "an MBR FAT partition prefers the volume serial; a non-FAT partition keeps rung 4" {
var disk = [_]u8{0} ** (3 * 512);
disk[510] = 0x55;
disk[511] = 0xAA;
std.mem.writeInt(u32, disk[440..444], 0xDEADBEEF, .little);
disk[446 + 4] = 0x0c; // FAT32-LBA partition
std.mem.writeInt(u32, disk[446 + 8 ..][0..4], 1, .little); // start LBA 1
std.mem.writeInt(u32, disk[446 + 12 ..][0..4], 2, .little); // size 2
const vbr = disk[512..][0..512]; // a FAT16 VBR at the partition start
vbr[510] = 0x55;
vbr[511] = 0xAA;
std.mem.writeInt(u16, vbr[22..24], 0x0080, .little); // fat_size_16 != 0 → FAT16
vbr[38] = 0x29; // FAT12/16 extended boot signature
std.mem.writeInt(u32, vbr[39..43], 0xCAFEBABE, .little);
@memcpy(vbr[43..54], "MYVOL ");
const rd = RamDisk{ .sectors = &disk };
const v = firstVolume(rd.reader(), 200000).?;
try std.testing.expectEqual(@as(u64, 1), v.base_lba);
try std.testing.expectEqual(Rung.fat_serial, v.identity.rung);
try std.testing.expectEqual(@as(u128, 0xCAFEBABE), v.identity.key);
try std.testing.expectEqualStrings("MYVOL", v.identity.labelSlice());
// A partition whose VBR is not an extended FAT falls back to the rung-4 id.
var plain = [_]u8{0} ** (3 * 512);
@memcpy(plain[0..512], disk[0..512]); // same MBR; LBA 1 left blank
const rd2 = RamDisk{ .sectors = &plain };
const v2 = firstVolume(rd2.reader(), 200000).?;
try std.testing.expectEqual(Rung.mbr_index, v2.identity.rung);
try std.testing.expectEqual((@as(u128, 0xDEADBEEF) << 8) | 0, v2.identity.key);
}
test "GPT with 256-byte entries reads the non-128 offset arithmetic correctly" {
// With entry_size 256, entry 1 lands at offset 256 of the same sector (LBA 2).
// Put the only valid entry at index 1 so the off = (i*entry_size) % 512 path
// (256, not 0) is exercised — the sharp edge the 128-byte tests never hit.
var disk = [_]u8{0} ** (5 * sector_bytes);
const entries = [_][128]u8{
gptEntry(false, 0, 0, 0), // index 0: unused (type GUID zero)
gptEntry(true, 0xF00D, 4096, 8191), // index 1: at offset 256
};
buildGptDiskSized(&disk, &entries, 256);
const rd = RamDisk{ .sectors = &disk };
const v = firstVolume(rd.reader(), 200000).?;
try std.testing.expectEqual(@as(u64, 4096), v.base_lba);
try std.testing.expectEqual(Rung.gpt_guid, v.identity.rung);
try std.testing.expectEqual(@as(u128, 0xF00D), v.identity.key);
}
@@ -40,7 +40,7 @@ const Volume = struct {
storage_device_id: u64, // the device-manager id this volume's provider serves
base_lba: u64,
block_count: u64,
identity: u64,
identity: partition.Identity,
id: u64,
filesystem_pid: u32 = 0,
};
@@ -169,7 +169,7 @@ fn spawnFilesystem(v: *Volume) void {
}
v.filesystem_pid = pid;
fs_spawn_ns = time.clock();
std.log.info("volume 0x{x} -> {s} (pid {d}), lba {d}, {d} blocks", .{ v.identity, filesystem_binary, pid, v.base_lba, v.block_count });
std.log.info("volume 0x{x} -> {s} (pid {d}), lba {d}, {d} blocks", .{ v.identity.key, filesystem_binary, pid, v.base_lba, v.block_count });
}
/// Schedule a fat restart after backoff; the poll loop performs it once due.
@@ -204,12 +204,19 @@ fn bringUpVolume() void {
_ = ipc.close(device.endpoint);
return;
};
if (!device.read(0, 1, bounce.physical)) {
_ = ipc.close(device.endpoint);
return;
}
const sector: [*]const u8 = @ptrFromInt(bounce.virtual);
const found = partition.firstVolume(sector[0..512], geometry.block_count) orelse {
const ProbeReader = struct {
device: block.Device,
fn readSector(context: *anyopaque, lba: u64, buffer: *[partition.sector_bytes]u8) bool {
const self: *@This() = @ptrCast(@alignCast(context));
if (!self.device.read(lba, 1, bounce.physical)) return false;
const src: [*]const u8 = @ptrFromInt(bounce.virtual);
@memcpy(buffer, src[0..partition.sector_bytes]);
return true;
}
};
var probe = ProbeReader{ .device = device };
const reader = partition.SectorReader{ .context = &probe, .readFn = ProbeReader.readSector };
const found = partition.firstVolume(reader, geometry.block_count) orelse {
if (!logged_no_volume) {
_ = logging.write("volume-manager: storage present but no recognizable volume\n");
logged_no_volume = true;
+6 -3
View File
@@ -795,9 +795,12 @@ CASES = [
"smp": 4,
"timeout": 150,
# The volume manager probes the partition table, then confines a filesystem
# to the volume and hands it over — one log line naming the volume's range,
# its identity, and the filesystem it spawned for it.
"expect": r"volume-manager: volume 0x[0-9a-f]+ -> \S+ \(pid \d+\), lba \d+, \d+ blocks"
# to the volume and hands it over. Since S1 rung 3 the identity is the boot
# image's real FAT32 serial (0x12345678, from make-fat-image.py) — before
# rung 3 it was the rung-4 pseudo-signature read from VBR offset 440 (~0x0),
# so this tightened value is an on-image witness that the enriched identity
# reaches the running log, not just the host tests.
"expect": r"volume-manager: volume 0x0*12345678 -> \S+ \(pid \d+\), lba \d+, \d+ blocks"
r"[\s\S]*volume-manager: handed volume \d+ to pid \d+",
"fail": r"DANOS-TEST-RESULT: FAIL"},
# Phase 2b: mkdir/unlink through the mount. Reuses the fat-mount build — the