From 020e31bc8ffd603e05be8d18d496e5fee3f01de6 Mon Sep 17 00:00:00 2001 From: Daniel Samson <12231216+daniel-samson@users.noreply.github.com> Date: Sun, 9 Aug 2026 23:15:23 +0100 Subject: [PATCH] =?UTF-8?q?volume-manager:=20GPT=20parsing=20=E2=80=94=20t?= =?UTF-8?q?he=20partition=20GUID=20is=20the=20id,=20the=20name=20is=20the?= =?UTF-8?q?=20label=20(S1)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Rung 1 of the identity ladder. A protective MBR (a type-0xEE entry) routes probing to the GPT, authoritatively: gptFirstVolume verifies the LBA-1 header's 'EFI PART' signature and a header CRC-32 (inline reflected poly 0xEDB88320, shared with the fixtures so parser and tests never drift onto a magic constant), then walks the entry array — bounded by the declared gpt_entry_scan_maximum — for the first entry with a non-zero type GUID and an overflow-safe in-device range. That range check is the confinement-safety guard the driver's clamp rests on, the invariant firstVolume already enforces for MBR, extended to untrusted GPT metadata. The unique partition GUID becomes the identity key (the id / mount-path handle); the 36-char partition name becomes the display label. Three host tests (GUID-as-id; entry-past-device skipped and an all-out-of-range table is null; a broken header/CRC is not a volume) — all three FAIL with the GPT branch neutralized (3/7) and pass with it (7/7). Entry-array CRC deferred (correctness-only; the range check carries the safety property). --- system/services/volume-manager/partition.zig | 207 ++++++++++++++++++- 1 file changed, 202 insertions(+), 5 deletions(-) diff --git a/system/services/volume-manager/partition.zig b/system/services/volume-manager/partition.zig index b03a9a6..6680726 100644 --- a/system/services/volume-manager/partition.zig +++ b/system/services/volume-manager/partition.zig @@ -18,8 +18,12 @@ const std = @import("std"); /// logical sectors (4Kn media is a separate concern, noted in the plan). pub const sector_bytes = 512; -/// The longest display label the parser records: a GPT partition name is 36 -/// UTF-16 units; a FAT volume label is 11 bytes; 36 ASCII bytes covers both. +/// bound: bytes of a volume's display label the parser records (a GPT partition +/// name is 36 UTF-16 units; a FAT volume label is 11 bytes; 36 covers both) +/// decided-by: hardware +/// protects: the Identity.label buffer +/// at-limit: degrade - a longer name is truncated to this many ASCII bytes +/// observed-by: a volume whose displayed label is clipped pub const label_maximum = 36; /// Which rung of the identity ladder produced this identity. The rung tags the @@ -99,14 +103,126 @@ fn hasBootSignature(block0: []const u8) bool { return block0.len >= 512 and block0[510] == 0x55 and block0[511] == 0xAA; } +/// GPT header signature at LBA 1. +const gpt_signature = "EFI PART"; + +/// bound: GPT partition entries scanned before the prober gives up +/// decided-by: ours +/// protects: the entry-array scan loop from an untrusted num_partition_entries +/// at-limit: degrade - stop scanning; a device whose usable entry sits past the +/// cap is treated as having no GPT volume (real tables carry <=128 entries) +/// observed-by: the gpt-entry-past-device host test +const gpt_entry_scan_maximum = 128; + +/// Reflected CRC-32 (polynomial 0xEDB88320) — the ISO-HDLC variant GPT uses for +/// its header checksum. Inlined so the parser and the host fixtures compute it +/// the same way and never drift onto a magic constant. +fn crc32(bytes: []const u8) u32 { + var c: u32 = 0xFFFFFFFF; + for (bytes) |b| { + c ^= b; + var k: u8 = 0; + while (k < 8) : (k += 1) { + c = if (c & 1 != 0) (c >> 1) ^ 0xEDB88320 else c >> 1; + } + } + return c ^ 0xFFFFFFFF; +} + +/// A GPT disk carries a protective MBR: a boot-signed block 0 with a partition +/// entry of type 0xEE. Its presence routes probing to the GPT (authoritative). +fn isProtectiveMbr(block0: []const u8) bool { + if (!hasBootSignature(block0)) return false; + var index: usize = 0; + while (index < 4) : (index += 1) { + if (block0[446 + index * 16 + 4] == 0xEE) return true; + } + return false; +} + +/// Copy the GPT partition name (36 UTF-16LE units, the 72 bytes at entry+56) +/// into the identity's display label as ASCII, dropping non-ASCII units. +fn setLabelFromUtf16(id: *Identity, name_bytes: []const u8) void { + var out: usize = 0; + var i: usize = 0; + while (i + 1 < name_bytes.len and out < label_maximum) : (i += 2) { + const unit = std.mem.readInt(u16, name_bytes[i..][0..2], .little); + if (unit == 0) break; + if (unit < 0x80) { + id.label[out] = @intCast(unit); + out += 1; + } + } + 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 { + 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; + const header_size = std.mem.readInt(u32, header[12..16], .little); + if (header_size < 92 or header_size > sector_bytes) return null; + 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; + + 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; + + const scan = @min(num_entries, gpt_entry_scan_maximum); + var sector_buf: [sector_bytes]u8 = undefined; + var loaded: u64 = std.math.maxInt(u64); + var i: u32 = 0; + while (i < scan) : (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; + loaded = lba; + } + const entry = sector_buf[off..][0..128]; // the fields we read live in the first 128 bytes + var type_nonzero = false; + for (entry[0..16]) |b| { + if (b != 0) { + type_nonzero = true; + break; + } + } + if (!type_nonzero) continue; + const start = std.mem.readInt(u64, entry[32..40], .little); + const end = std.mem.readInt(u64, entry[40..48], .little); // inclusive last LBA + // Untrusted range from removable media: overflow-safe validation. Reject a + // partition that starts at 0, is reversed, or ends outside the device; only + // then is start + count <= device_blocks guaranteed for the driver's clamp. + 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 }; + } + return null; +} + /// The first volume on the device `reader` addresses, whose whole-device size is -/// `device_blocks`, or null if none is found. 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 whole device. +/// `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 { 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); var index: u8 = 0; while (index < 4) : (index += 1) { const entry = block0[446 + @as(usize, index) * 16 ..][0..16]; @@ -194,3 +310,84 @@ test "a partition that runs past the device is skipped, not trusted" { try std.testing.expectEqual(@as(u64, 2048), v.base_lba); // the fitting one, not the overflowing one try std.testing.expectEqual(@as(u64, 1000), v.block_count); } + +/// A single 128-byte GPT partition entry for the tests. +fn gptEntry(type_nonzero: bool, unique_guid: u128, start: u64, end: u64) [128]u8 { + var e = [_]u8{0} ** 128; + if (type_nonzero) e[0] = 0x01; // any non-zero byte makes the type GUID non-zero + std.mem.writeInt(u128, e[16..32], unique_guid, .little); + std.mem.writeInt(u64, e[32..40], start, .little); + std.mem.writeInt(u64, e[40..48], end, .little); + return e; +} + +/// Lay out a 4-sector disk: protective MBR (LBA 0), GPT header with a correct +/// CRC (LBA 1), and the entry array (LBA 2). +fn buildGptDisk(disk: []u8, entries: []const [128]u8) void { + @memset(disk, 0); + disk[510] = 0x55; + disk[511] = 0xAA; + disk[446 + 4] = 0xEE; // protective entry type + std.mem.writeInt(u32, disk[446 + 8 ..][0..4], 1, .little); + std.mem.writeInt(u32, disk[446 + 12 ..][0..4], 0xFFFFFFFF, .little); + const h = disk[sector_bytes..][0..sector_bytes]; + @memcpy(h[0..8], gpt_signature); + std.mem.writeInt(u32, h[12..16], 92, .little); // header_size + std.mem.writeInt(u64, h[72..80], 2, .little); // partition_entry_lba + std.mem.writeInt(u32, h[80..84], @intCast(entries.len), .little); + std.mem.writeInt(u32, h[84..88], 128, .little); // size_of_partition_entry + @memset(h[16..20], 0); + std.mem.writeInt(u32, h[16..20], crc32(h[0..92]), .little); + const ea = disk[2 * sector_bytes ..][0..sector_bytes]; + var i: usize = 0; + while (i < entries.len and i < 4) : (i += 1) { + @memcpy(ea[i * 128 ..][0..128], &entries[i]); + } +} + +test "a GPT disk yields the partition GUID as the identity id" { + var disk = [_]u8{0} ** (4 * sector_bytes); + const guid: u128 = 0x112233445566778899AABBCCDDEEFF00; + const entries = [_][128]u8{gptEntry(true, guid, 2048, 4095)}; + buildGptDisk(&disk, &entries); + const rd = RamDisk{ .sectors = &disk }; + const v = firstVolume(rd.reader(), 200000).?; + try std.testing.expectEqual(@as(u64, 2048), v.base_lba); + try std.testing.expectEqual(@as(u64, 2048), v.block_count); // 4095 - 2048 + 1 + try std.testing.expectEqual(Rung.gpt_guid, v.identity.rung); + try std.testing.expectEqual(guid, v.identity.key); +} + +test "a GPT entry past the device is skipped; an all-out-of-range table is no volume" { + var disk = [_]u8{0} ** (4 * sector_bytes); + const entries = [_][128]u8{ + gptEntry(true, 0xAAA, 2048, 999999), // ends past a 200000-block device + gptEntry(true, 0xBBB, 4096, 8191), // fits + }; + buildGptDisk(&disk, &entries); + const rd = RamDisk{ .sectors = &disk }; + const v = firstVolume(rd.reader(), 200000).?; + try std.testing.expectEqual(@as(u64, 4096), v.base_lba); // the fitting one, not the overflowing one + try std.testing.expectEqual(@as(u128, 0xBBB), v.identity.key); + + var solo_disk = [_]u8{0} ** (4 * sector_bytes); + const solo = [_][128]u8{gptEntry(true, 0xAAA, 2048, 999999)}; + buildGptDisk(&solo_disk, &solo); + const rd2 = RamDisk{ .sectors = &solo_disk }; + try std.testing.expect(firstVolume(rd2.reader(), 200000) == null); +} + +test "a protective MBR with a broken GPT header is not a volume" { + var disk = [_]u8{0} ** (4 * sector_bytes); + const entries = [_][128]u8{gptEntry(true, 0xCCC, 2048, 4095)}; + buildGptDisk(&disk, &entries); + disk[sector_bytes] = 'X'; // wreck the 'EFI PART' signature + const rd = RamDisk{ .sectors = &disk }; + try std.testing.expect(firstVolume(rd.reader(), 200000) == null); + + var bad_crc = [_]u8{0} ** (4 * sector_bytes); + buildGptDisk(&bad_crc, &entries); + bad_crc[sector_bytes + 16] ^= 0xFF; // corrupt a header-CRC byte + const rd2 = RamDisk{ .sectors = &bad_crc }; + try std.testing.expect(firstVolume(rd2.reader(), 200000) == null); +}