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).
394 lines
18 KiB
Zig
394 lines
18 KiB
Zig
//! Partition-table parsing, the policy the storage architecture places above the
|
|
//! block driver and below the filesystem (docs/file-system-development/
|
|
//! storage-architecture.md): read the medium, decide what block sub-ranges are
|
|
//! volumes, and read each volume's content identity. The block DRIVER never does
|
|
//! this — it clamps ranges it is told about; this is what tells it the numbers.
|
|
//!
|
|
//! Reads happen through a `SectorReader` (not one preloaded block-0 slice) so the
|
|
//! parser can reach GPT metadata at LBA 1, the entry array beyond it, and each
|
|
//! partition's VBR on demand. The identity it returns is a tagged `Identity`: the
|
|
//! `key` is the id (the mount path is derived from it — a stable, unique,
|
|
//! content-derived handle), and `label` is display metadata (the FAT volume label
|
|
//! or the GPT partition name), never part of the id. Today's rung is MBR/bare-FAT;
|
|
//! GPT (rung 1) and the FAT serial (rung 3) slot in without changing the shape.
|
|
|
|
const std = @import("std");
|
|
|
|
/// A single 512-byte sector's worth of bytes. The parser assumes 512-byte
|
|
/// logical sectors (4Kn media is a separate concern, noted in the plan).
|
|
pub const sector_bytes = 512;
|
|
|
|
/// 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
|
|
/// `key` namespace so a FAT serial and an MBR signature that happen to share bits
|
|
/// stay distinct, and it drives how the mount path is rendered from the id.
|
|
pub const Rung = enum(u8) {
|
|
gpt_guid = 1,
|
|
filesystem_uuid = 2, // reserved: no non-FAT engine reads a superblock UUID yet
|
|
fat_serial = 3,
|
|
mbr_index = 4,
|
|
anonymous = 5,
|
|
};
|
|
|
|
/// A volume's content identity. `key` is the ID — the stable, unique handle the
|
|
/// mount path is derived from and the mount map keys on. `label` is DISPLAY
|
|
/// metadata (FAT volume label / GPT partition name), exposed to a UI but never
|
|
/// part of the path; two volumes with the same label but different keys are
|
|
/// different volumes. Derived from the medium, never from a port.
|
|
pub const Identity = struct {
|
|
rung: Rung,
|
|
key: u128 = 0,
|
|
label: [label_maximum]u8 = [_]u8{0} ** label_maximum,
|
|
label_len: u8 = 0,
|
|
|
|
pub fn labelSlice(self: *const Identity) []const u8 {
|
|
return self.label[0..self.label_len];
|
|
}
|
|
|
|
/// Identity equality is the ID (rung + key) only — the label is display
|
|
/// metadata and does not enter it. Same rung + same key means the same
|
|
/// volume (the dd-cloned-media case the duplicate policy is for).
|
|
pub fn eql(a: Identity, b: Identity) bool {
|
|
return a.rung == b.rung and a.key == b.key;
|
|
}
|
|
};
|
|
|
|
/// One volume the parser found on the device: the block sub-range it occupies
|
|
/// and its content identity.
|
|
pub const Volume = struct {
|
|
base_lba: u64,
|
|
block_count: u64,
|
|
identity: Identity,
|
|
};
|
|
|
|
/// Read sectors on demand. `context` + `readFn` mirror the FAT engine's
|
|
/// `BlockDevice` vtable; `readFn` returns false past the end of the device or on
|
|
/// an I/O error, which the parser treats as "no volume".
|
|
pub const SectorReader = struct {
|
|
context: *anyopaque,
|
|
readFn: *const fn (context: *anyopaque, lba: u64, buffer: *[sector_bytes]u8) bool,
|
|
|
|
pub fn read(self: SectorReader, lba: u64, buffer: *[sector_bytes]u8) bool {
|
|
return self.readFn(self.context, lba, buffer);
|
|
}
|
|
};
|
|
|
|
/// The MBR disk signature (offset 440, 4 bytes LE) — a 32-bit id written at
|
|
/// partition time. Weak (dd-cloned disks share it) but on the medium, and the
|
|
/// last rung of the identity ladder; the fuller rungs (GPT GUID, FAT serial)
|
|
/// take precedence when present.
|
|
fn diskSignature(block0: []const u8) u32 {
|
|
if (block0.len < 444) return 0;
|
|
return std.mem.readInt(u32, block0[440..444], .little);
|
|
}
|
|
|
|
/// The rung-4 identity of the volume at partition `index`: the disk signature
|
|
/// paired with the index, so two partitions of one disk stay distinct. For a
|
|
/// bare FAT (no table) the index is 0. Carries no label.
|
|
fn mbrIdentity(block0: []const u8, index: u8) Identity {
|
|
return .{ .rung = .mbr_index, .key = (@as(u128, diskSignature(block0)) << 8) | index };
|
|
}
|
|
|
|
/// Whether a block looks like a boot sector / partition table (the 0x55AA boot
|
|
/// signature). A bare FAT also carries it, so the caller distinguishes by whether
|
|
/// any partition entry is non-empty.
|
|
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. 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];
|
|
const kind = entry[4];
|
|
const start = std.mem.readInt(u32, entry[8..12], .little);
|
|
const size = std.mem.readInt(u32, entry[12..16], .little);
|
|
if (kind == 0 or start == 0 or size == 0) continue;
|
|
// These bytes come off an untrusted removable medium. A partition that
|
|
// does not fit inside the device is not a partition — skip it. This is
|
|
// where the driver's confinement-safety invariant is established: the
|
|
// clamp's overflow-safety rests on base + count staying inside the
|
|
// 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) };
|
|
}
|
|
// No partition entries: a bare FAT spanning the device.
|
|
return .{ .base_lba = 0, .block_count = device_blocks, .identity = mbrIdentity(&block0, 0) };
|
|
}
|
|
|
|
/// A read-only RAM disk over a byte slice of sectors, for the host tests.
|
|
const RamDisk = struct {
|
|
sectors: []const u8,
|
|
|
|
fn readFn(context: *anyopaque, lba: u64, buffer: *[sector_bytes]u8) bool {
|
|
const self: *const RamDisk = @ptrCast(@alignCast(context));
|
|
const off = lba * sector_bytes;
|
|
if (off + sector_bytes > self.sectors.len) return false;
|
|
@memcpy(buffer, self.sectors[off..][0..sector_bytes]);
|
|
return true;
|
|
}
|
|
|
|
fn reader(self: *const RamDisk) SectorReader {
|
|
return .{ .context = @constCast(self), .readFn = readFn };
|
|
}
|
|
};
|
|
|
|
test "an MBR with one partition yields its range and a distinct identity" {
|
|
var block0 = [_]u8{0} ** 512;
|
|
block0[510] = 0x55;
|
|
block0[511] = 0xAA;
|
|
std.mem.writeInt(u32, block0[440..444], 0xDEADBEEF, .little);
|
|
// partition 0: type 0x0c (FAT32 LBA), start 2048, size 100000
|
|
block0[446 + 4] = 0x0c;
|
|
std.mem.writeInt(u32, block0[446 + 8 ..][0..4], 2048, .little);
|
|
std.mem.writeInt(u32, block0[446 + 12 ..][0..4], 100000, .little);
|
|
const disk = RamDisk{ .sectors = &block0 };
|
|
const v = firstVolume(disk.reader(), 200000).?;
|
|
try std.testing.expectEqual(@as(u64, 2048), v.base_lba);
|
|
try std.testing.expectEqual(@as(u64, 100000), v.block_count);
|
|
try std.testing.expectEqual(Rung.mbr_index, v.identity.rung);
|
|
try std.testing.expectEqual((@as(u128, 0xDEADBEEF) << 8) | 0, v.identity.key);
|
|
}
|
|
|
|
test "a boot signature with no partitions is a bare FAT over the whole device" {
|
|
var block0 = [_]u8{0} ** 512;
|
|
block0[510] = 0x55;
|
|
block0[511] = 0xAA;
|
|
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(@as(u64, 65536), v.block_count);
|
|
}
|
|
|
|
test "no boot signature is no volume" {
|
|
const block0 = [_]u8{0} ** 512;
|
|
const disk = RamDisk{ .sectors = &block0 };
|
|
try std.testing.expect(firstVolume(disk.reader(), 65536) == null);
|
|
}
|
|
|
|
test "a partition that runs past the device is skipped, not trusted" {
|
|
var block0 = [_]u8{0} ** 512;
|
|
block0[510] = 0x55;
|
|
block0[511] = 0xAA;
|
|
// partition 0: start 0xFFFFFF00, size 0x400 — far past a 200000-block device.
|
|
block0[446 + 4] = 0x0c;
|
|
std.mem.writeInt(u32, block0[446 + 8 ..][0..4], 0xFFFFFF00, .little);
|
|
std.mem.writeInt(u32, block0[446 + 12 ..][0..4], 0x400, .little);
|
|
// partition 1: start 2048, size 1000 — fits.
|
|
block0[462 + 4] = 0x0c;
|
|
std.mem.writeInt(u32, block0[462 + 8 ..][0..4], 2048, .little);
|
|
std.mem.writeInt(u32, block0[462 + 12 ..][0..4], 1000, .little);
|
|
const disk = RamDisk{ .sectors = &block0 };
|
|
const v = firstVolume(disk.reader(), 200000).?;
|
|
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);
|
|
}
|