From 48ab12e262c0a3f7df91e661523dacd9702154a9 Mon Sep 17 00:00:00 2001 From: Daniel Samson <12231216+daniel-samson@users.noreply.github.com> Date: Sun, 9 Aug 2026 23:22:04 +0100 Subject: [PATCH] volume-manager: the FAT volume serial + label is the identity (rung 3) (S1) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- system/services/volume-manager/partition.zig | 81 +++++++++++++++++++- test/qemu_test.py | 9 ++- 2 files changed, 85 insertions(+), 5 deletions(-) diff --git a/system/services/volume-manager/partition.zig b/system/services/volume-manager/partition.zig index 6680726..6a72a68 100644 --- a/system/services/volume-manager/partition.zig +++ b/system/services/volume-manager/partition.zig @@ -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); +} diff --git a/test/qemu_test.py b/test/qemu_test.py index 1808bb9..dcabd19 100644 --- a/test/qemu_test.py +++ b/test/qemu_test.py @@ -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