diff --git a/system/services/volume-manager/partition.zig b/system/services/volume-manager/partition.zig index d29113c..f9eca22 100644 --- a/system/services/volume-manager/partition.zig +++ b/system/services/volume-manager/partition.zig @@ -172,39 +172,41 @@ fn setLabelFromUtf16(id: *Identity, name_bytes: []const u8) void { 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 { +/// Append every valid GPT volume to `out` (up to `out.len`), returning the count +/// (0 if LBA 1 is not a valid GPT header). 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 documented for MBR, extended to untrusted GPT +/// metadata. The entry-array CRC is deferred (correctness-only; the range check +/// carries safety). +fn gptAllVolumes(reader: SectorReader, device_blocks: u64, out: []Volume) usize { 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; + if (!reader.read(1, &header)) return 0; + if (!std.mem.eql(u8, header[0..8], gpt_signature)) return 0; 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); 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; + if (crc32(check[0..header_size]) != stored_crc) return 0; 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; + if (entry_size != 128 and entry_size != 256 and entry_size != 512) return 0; + if (entry_lba == 0 or entry_lba >= device_blocks) return 0; const scan = @min(num_entries, gpt_entry_scan_maximum); var sector_buf: [sector_bytes]u8 = undefined; var loaded: u64 = std.math.maxInt(u64); + var count: usize = 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 lba = entry_lba + abs / sector_bytes; const off = @as(usize, @intCast(abs % sector_bytes)); if (lba != loaded) { - if (!reader.read(lba, §or_buf)) return null; + if (!reader.read(lba, §or_buf)) break; // return what we have loaded = lba; } 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; 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 }; + 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 @@ -259,18 +262,22 @@ fn fatIdentity(reader: SectorReader, start_lba: u64) ?Identity { 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 { +/// Append every volume on the device `reader` addresses, whose whole-device size +/// is `device_blocks`, to `out` (up to `out.len`), returning the count. A GPT +/// disk (protective MBR) is enumerated by GPT, authoritatively — a zero count is +/// final. Otherwise every fitting MBR entry is a volume; a boot signature with no +/// partition entries is a bare FAT spanning the whole device. Each volume's +/// [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; - if (!reader.read(0, &block0)) return null; - if (!hasBootSignature(&block0)) return null; - if (isProtectiveMbr(&block0)) return gptFirstVolume(reader, device_blocks); + if (!reader.read(0, &block0)) return 0; + if (!hasBootSignature(&block0)) return 0; + if (isProtectiveMbr(&block0)) return gptAllVolumes(reader, device_blocks, out); + var count: usize = 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 kind = entry[4]; 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 // 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 = 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; } - // 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) }; + if (count == 0 and out.len > 0) { + // No partition entries: a bare FAT spanning the device. + 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. @@ -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(@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); +}