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:
@@ -213,6 +213,36 @@ fn gptFirstVolume(reader: SectorReader, device_blocks: u64) ?Volume {
|
|||||||
return null;
|
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
|
/// 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
|
/// `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
|
/// 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
|
// device (usb-storage.zig resolveTransfer), which only holds because the
|
||||||
// range handed down is validated here. The subtraction cannot overflow.
|
// range handed down is validated here. The subtraction cannot overflow.
|
||||||
if (start > device_blocks or device_blocks - start < size) continue;
|
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.
|
// 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.
|
/// 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 };
|
const rd2 = RamDisk{ .sectors = &bad_crc };
|
||||||
try std.testing.expect(firstVolume(rd2.reader(), 200000) == null);
|
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
@@ -795,9 +795,12 @@ CASES = [
|
|||||||
"smp": 4,
|
"smp": 4,
|
||||||
"timeout": 150,
|
"timeout": 150,
|
||||||
# The volume manager probes the partition table, then confines a filesystem
|
# 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,
|
# to the volume and hands it over. Since S1 rung 3 the identity is the boot
|
||||||
# its identity, and the filesystem it spawned for it.
|
# image's real FAT32 serial (0x12345678, from make-fat-image.py) — before
|
||||||
"expect": r"volume-manager: volume 0x[0-9a-f]+ -> \S+ \(pid \d+\), lba \d+, \d+ blocks"
|
# 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+",
|
r"[\s\S]*volume-manager: handed volume \d+ to pid \d+",
|
||||||
"fail": r"DANOS-TEST-RESULT: FAIL"},
|
"fail": r"DANOS-TEST-RESULT: FAIL"},
|
||||||
# Phase 2b: mkdir/unlink through the mount. Reuses the fat-mount build — the
|
# Phase 2b: mkdir/unlink through the mount. Reuses the fat-mount build — the
|
||||||
|
|||||||
Reference in New Issue
Block a user