diff --git a/system/services/volume-manager/partition.zig b/system/services/volume-manager/partition.zig index f631e48..b03a9a6 100644 --- a/system/services/volume-manager/partition.zig +++ b/system/services/volume-manager/partition.zig @@ -1,54 +1,112 @@ //! 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 block 0, decide what block sub-ranges are +//! 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. //! -//! Today: MBR (the four-entry table at offset 446) plus the bare-FAT case (a boot -//! sector right at LBA 0). GPT is the next entry in the identity ladder and slots -//! in here without touching anything above or below. +//! 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; + +/// 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. +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 a content identity stable for the volume's life (the mount map keys on -/// it; the boot volume is recorded by it). `identity` is derived from the medium, -/// never from a port — a moved drive keeps it. +/// and its content identity. pub const Volume = struct { base_lba: u64, block_count: u64, - identity: 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 -/// simplest rung of the identity ladder; the fuller rungs (GPT partition GUID, -/// FAT volume serial) refine `identityOf` without changing the shape. +/// 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 identity of the volume at partition index `index`: the disk signature +/// 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. -fn identityOf(block0: []const u8, index: u8) u64 { - return (@as(u64, diskSignature(block0)) << 8) | index; +/// 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 block 0 looks like a partition table (the 0x55AA boot signature). A -/// bare FAT also carries it, so the caller distinguishes by whether any partition -/// entry is non-empty. +/// 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; } -/// The first volume on a device whose block 0 is `block0` and 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. -pub fn firstVolume(block0: []const u8, device_blocks: u64) ?Volume { - if (!hasBootSignature(block0)) 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. +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; var index: u8 = 0; while (index < 4) : (index += 1) { const entry = block0[446 + @as(usize, index) * 16 ..][0..16]; @@ -63,12 +121,29 @@ pub fn firstVolume(block0: []const u8, 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 = identityOf(block0, index) }; + 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 = identityOf(block0, 0) }; + 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; @@ -78,24 +153,28 @@ test "an MBR with one partition yields its range and a distinct identity" { 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 v = firstVolume(&block0, 200000).?; + 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((@as(u64, 0xDEADBEEF) << 8) | 0, v.identity); + 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 v = firstVolume(&block0, 65536).?; + 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; - try std.testing.expect(firstVolume(&block0, 65536) == null); + 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" { @@ -110,7 +189,8 @@ test "a partition that runs past the device is skipped, not trusted" { 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 v = firstVolume(&block0, 200000).?; + 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); } diff --git a/system/services/volume-manager/volume-manager.zig b/system/services/volume-manager/volume-manager.zig index 6fe5851..f9e6ea6 100644 --- a/system/services/volume-manager/volume-manager.zig +++ b/system/services/volume-manager/volume-manager.zig @@ -40,7 +40,7 @@ const Volume = struct { storage_device_id: u64, // the device-manager id this volume's provider serves base_lba: u64, block_count: u64, - identity: u64, + identity: partition.Identity, id: u64, filesystem_pid: u32 = 0, }; @@ -169,7 +169,7 @@ fn spawnFilesystem(v: *Volume) void { } v.filesystem_pid = pid; fs_spawn_ns = time.clock(); - std.log.info("volume 0x{x} -> {s} (pid {d}), lba {d}, {d} blocks", .{ v.identity, filesystem_binary, pid, v.base_lba, v.block_count }); + std.log.info("volume 0x{x} -> {s} (pid {d}), lba {d}, {d} blocks", .{ v.identity.key, filesystem_binary, pid, v.base_lba, v.block_count }); } /// Schedule a fat restart after backoff; the poll loop performs it once due. @@ -204,12 +204,19 @@ fn bringUpVolume() void { _ = ipc.close(device.endpoint); return; }; - if (!device.read(0, 1, bounce.physical)) { - _ = ipc.close(device.endpoint); - return; - } - const sector: [*]const u8 = @ptrFromInt(bounce.virtual); - const found = partition.firstVolume(sector[0..512], geometry.block_count) orelse { + const ProbeReader = struct { + device: block.Device, + fn readSector(context: *anyopaque, lba: u64, buffer: *[partition.sector_bytes]u8) bool { + const self: *@This() = @ptrCast(@alignCast(context)); + if (!self.device.read(lba, 1, bounce.physical)) return false; + const src: [*]const u8 = @ptrFromInt(bounce.virtual); + @memcpy(buffer, src[0..partition.sector_bytes]); + return true; + } + }; + var probe = ProbeReader{ .device = device }; + const reader = partition.SectorReader{ .context = &probe, .readFn = ProbeReader.readSector }; + const found = partition.firstVolume(reader, geometry.block_count) orelse { if (!logged_no_volume) { _ = logging.write("volume-manager: storage present but no recognizable volume\n"); logged_no_volume = true;