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.
This commit is contained in:
Daniel Samson
2026-08-09 23:22:04 +01:00
parent 020e31bc8f
commit 48ab12e262
2 changed files with 85 additions and 5 deletions
+79 -2
View File
@@ -213,6 +213,36 @@ fn gptFirstVolume(reader: SectorReader, device_blocks: u64) ?Volume {
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
@@ -237,10 +267,10 @@ pub fn firstVolume(reader: SectorReader, 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 = mbrIdentity(&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 = mbrIdentity(&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.
@@ -391,3 +421,50 @@ test "a protective MBR with a broken GPT header is not a volume" {
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);
}
+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