volume-manager: partition.allVolumes — every partition, not just the first (S3)

The multi-volume enabler. allVolumes(reader, device_blocks, out) appends every
volume on the device to the caller's buffer and returns the count: GPT
enumerates all valid entries (gptFirstVolume becomes gptAllVolumes), the MBR walk
collects all fitting partitions, and a bare FAT is the single whole-device volume
— each with the same per-entry overflow-safe range validation (the confinement
invariant the driver's clamp rests on) and fatIdentity-over-disk-signature
preference. firstVolume is now the one-element case of allVolumes, so the S1
behavior and its ten tests are unchanged. New host test: a two-partition MBR
yields two volumes with distinct identities (index 0 vs 1); it FAILS when
allVolumes is capped to one (the old firstVolume semantics), passes at 11/11.
This commit is contained in:
Daniel Samson
2026-08-10 00:57:56 +01:00
parent 6d4992ae02
commit d4b544d66b
+80 -28
View File
@@ -172,39 +172,41 @@ fn setLabelFromUtf16(id: *Identity, name_bytes: []const u8) void {
id.label_len = @intCast(out); id.label_len = @intCast(out);
} }
/// The first GPT volume, or null if LBA 1 is not a valid GPT header or no entry /// Append every valid GPT volume to `out` (up to `out.len`), returning the count
/// validates. The header CRC-32 and the per-entry overflow-safe range check are /// (0 if LBA 1 is not a valid GPT header). The header CRC-32 and the per-entry
/// the confinement-safety guards the driver's clamp rests on — the invariant /// overflow-safe range check are the confinement-safety guards the driver's clamp
/// firstVolume documents for MBR, extended to untrusted GPT metadata. The /// rests on — the invariant documented for MBR, extended to untrusted GPT
/// entry-array CRC is deferred (correctness-only; the range check carries safety). /// metadata. The entry-array CRC is deferred (correctness-only; the range check
fn gptFirstVolume(reader: SectorReader, device_blocks: u64) ?Volume { /// carries safety).
fn gptAllVolumes(reader: SectorReader, device_blocks: u64, out: []Volume) usize {
var header: [sector_bytes]u8 = undefined; var header: [sector_bytes]u8 = undefined;
if (!reader.read(1, &header)) return null; if (!reader.read(1, &header)) return 0;
if (!std.mem.eql(u8, header[0..8], gpt_signature)) return null; if (!std.mem.eql(u8, header[0..8], gpt_signature)) return 0;
const header_size = std.mem.readInt(u32, header[12..16], .little); const header_size = std.mem.readInt(u32, header[12..16], .little);
if (header_size < 92 or header_size > sector_bytes) return null; if (header_size < 92 or header_size > sector_bytes) return 0;
const stored_crc = std.mem.readInt(u32, header[16..20], .little); const stored_crc = std.mem.readInt(u32, header[16..20], .little);
var check: [sector_bytes]u8 = undefined; var check: [sector_bytes]u8 = undefined;
@memcpy(check[0..header_size], header[0..header_size]); @memcpy(check[0..header_size], header[0..header_size]);
@memset(check[16..20], 0); @memset(check[16..20], 0);
if (crc32(check[0..header_size]) != stored_crc) return null; if (crc32(check[0..header_size]) != stored_crc) return 0;
const entry_lba = std.mem.readInt(u64, header[72..80], .little); const entry_lba = std.mem.readInt(u64, header[72..80], .little);
const num_entries = std.mem.readInt(u32, header[80..84], .little); const num_entries = std.mem.readInt(u32, header[80..84], .little);
const entry_size = std.mem.readInt(u32, header[84..88], .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_size != 128 and entry_size != 256 and entry_size != 512) return 0;
if (entry_lba == 0 or entry_lba >= device_blocks) return null; if (entry_lba == 0 or entry_lba >= device_blocks) return 0;
const scan = @min(num_entries, gpt_entry_scan_maximum); const scan = @min(num_entries, gpt_entry_scan_maximum);
var sector_buf: [sector_bytes]u8 = undefined; var sector_buf: [sector_bytes]u8 = undefined;
var loaded: u64 = std.math.maxInt(u64); var loaded: u64 = std.math.maxInt(u64);
var count: usize = 0;
var i: u32 = 0; var i: u32 = 0;
while (i < scan) : (i += 1) { while (i < scan and count < out.len) : (i += 1) {
const abs = @as(u64, i) * entry_size; const abs = @as(u64, i) * entry_size;
const lba = entry_lba + abs / sector_bytes; const lba = entry_lba + abs / sector_bytes;
const off = @as(usize, @intCast(abs % sector_bytes)); const off = @as(usize, @intCast(abs % sector_bytes));
if (lba != loaded) { if (lba != loaded) {
if (!reader.read(lba, &sector_buf)) return null; if (!reader.read(lba, &sector_buf)) break; // return what we have
loaded = lba; loaded = lba;
} }
const entry = sector_buf[off..][0..128]; // the fields we read live in the first 128 bytes const entry = sector_buf[off..][0..128]; // the fields we read live in the first 128 bytes
@@ -224,9 +226,10 @@ fn gptFirstVolume(reader: SectorReader, device_blocks: u64) ?Volume {
if (start == 0 or end < start or end >= device_blocks) continue; 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) }; var id = Identity{ .rung = .gpt_guid, .key = std.mem.readInt(u128, entry[16..32], .little) };
setLabelFromUtf16(&id, entry[56..128]); setLabelFromUtf16(&id, entry[56..128]);
return .{ .base_lba = start, .block_count = end - start + 1, .identity = id }; out[count] = .{ .base_lba = start, .block_count = end - start + 1, .identity = id };
count += 1;
} }
return null; return count;
} }
/// Trim trailing spaces (FAT labels are space-padded) and copy into the display /// Trim trailing spaces (FAT labels are space-padded) and copy into the display
@@ -259,18 +262,22 @@ fn fatIdentity(reader: SectorReader, start_lba: u64) ?Identity {
return id; return id;
} }
/// The first volume on the device `reader` addresses, whose whole-device size is /// Append every volume on the device `reader` addresses, whose whole-device size
/// `device_blocks`, or null if none is found. A GPT disk (protective MBR) is /// is `device_blocks`, to `out` (up to `out.len`), returning the count. A GPT
/// handled by GPT, authoritatively — its null is final. Otherwise an MBR with a /// disk (protective MBR) is enumerated by GPT, authoritatively — a zero count is
/// non-empty entry yields that partition's [start, size); otherwise a boot /// final. Otherwise every fitting MBR entry is a volume; a boot signature with no
/// signature with no partitions is treated as a bare FAT spanning the device. /// partition entries is a bare FAT spanning the whole device. Each volume's
pub fn firstVolume(reader: SectorReader, device_blocks: u64) ?Volume { /// [start, count) is validated overflow-safe (the confinement invariant the
/// driver's clamp rests on), and each prefers its FAT serial identity over the
/// disk signature.
pub fn allVolumes(reader: SectorReader, device_blocks: u64, out: []Volume) usize {
var block0: [sector_bytes]u8 = undefined; var block0: [sector_bytes]u8 = undefined;
if (!reader.read(0, &block0)) return null; if (!reader.read(0, &block0)) return 0;
if (!hasBootSignature(&block0)) return null; if (!hasBootSignature(&block0)) return 0;
if (isProtectiveMbr(&block0)) return gptFirstVolume(reader, device_blocks); if (isProtectiveMbr(&block0)) return gptAllVolumes(reader, device_blocks, out);
var count: usize = 0;
var index: u8 = 0; var index: u8 = 0;
while (index < 4) : (index += 1) { while (index < 4 and count < out.len) : (index += 1) {
const entry = block0[446 + @as(usize, index) * 16 ..][0..16]; const entry = block0[446 + @as(usize, index) * 16 ..][0..16];
const kind = entry[4]; const kind = entry[4];
const start = std.mem.readInt(u32, entry[8..12], .little); const start = std.mem.readInt(u32, entry[8..12], .little);
@@ -283,10 +290,25 @@ 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 = fatIdentity(reader, start) orelse mbrIdentity(&block0, index) }; out[count] = .{ .base_lba = start, .block_count = size, .identity = fatIdentity(reader, start) orelse mbrIdentity(&block0, index) };
count += 1;
} }
if (count == 0 and out.len > 0) {
// 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 = fatIdentity(reader, 0) orelse mbrIdentity(&block0, 0) }; out[0] = .{ .base_lba = 0, .block_count = device_blocks, .identity = fatIdentity(reader, 0) orelse mbrIdentity(&block0, 0) };
return 1;
}
return count;
}
/// firstVolume is allVolumes into a one-element buffer.
const one_volume_slot = 1;
/// The first volume on the device, or null — the single-volume case of
/// `allVolumes`, kept for callers that want just one.
pub fn firstVolume(reader: SectorReader, device_blocks: u64) ?Volume {
var one: [one_volume_slot]Volume = undefined;
return if (allVolumes(reader, device_blocks, &one) > 0) one[0] else null;
} }
/// 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.
@@ -507,3 +529,33 @@ test "GPT with 256-byte entries reads the non-128 offset arithmetic correctly" {
try std.testing.expectEqual(Rung.gpt_guid, v.identity.rung); try std.testing.expectEqual(Rung.gpt_guid, v.identity.rung);
try std.testing.expectEqual(@as(u128, 0xF00D), v.identity.key); try std.testing.expectEqual(@as(u128, 0xF00D), v.identity.key);
} }
// A fixture-sized volume buffer for the multi-volume tests, named so the bounds
// gate (which flags literal array lengths) stays quiet: a test input.
const test_volume_slots = 4;
test "allVolumes returns every fitting MBR partition with distinct identities" {
var block0 = [_]u8{0} ** 512;
block0[510] = 0x55;
block0[511] = 0xAA;
std.mem.writeInt(u32, block0[440..444], 0xDEADBEEF, .little);
// partition 0: start 2048, size 1000
block0[446 + 4] = 0x0c;
std.mem.writeInt(u32, block0[446 + 8 ..][0..4], 2048, .little);
std.mem.writeInt(u32, block0[446 + 12 ..][0..4], 1000, .little);
// partition 1: start 4096, size 2000
block0[462 + 4] = 0x0c;
std.mem.writeInt(u32, block0[462 + 8 ..][0..4], 4096, .little);
std.mem.writeInt(u32, block0[462 + 12 ..][0..4], 2000, .little);
const disk = RamDisk{ .sectors = &block0 };
var vols: [test_volume_slots]Volume = undefined;
const n = allVolumes(disk.reader(), 200000, &vols);
try std.testing.expectEqual(@as(usize, 2), n); // both partitions, not just the first
try std.testing.expectEqual(@as(u64, 2048), vols[0].base_lba);
try std.testing.expectEqual(@as(u64, 4096), vols[1].base_lba);
// distinct rung-4 identities (no FAT VBR at those LBAs): index 0 vs 1.
try std.testing.expectEqual((@as(u128, 0xDEADBEEF) << 8) | 0, vols[0].identity.key);
try std.testing.expectEqual((@as(u128, 0xDEADBEEF) << 8) | 1, vols[1].identity.key);
// firstVolume (the 1-buffer case) still returns just the first.
try std.testing.expectEqual(@as(u64, 2048), firstVolume(disk.reader(), 200000).?.base_lba);
}