From 2c2745e9e5339e2908876db978f5f48d19ea2a8e Mon Sep 17 00:00:00 2001 From: Daniel Samson <12231216+daniel-samson@users.noreply.github.com> Date: Mon, 10 Aug 2026 03:28:08 +0100 Subject: [PATCH] volume-manager: recognize exFAT and route it by content (S4 step 6) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit partition.zig gains an exFAT VBR recognizer: the "EXFAT " name (where a FAT BPB keeps its OEM string, so the two never collide) plus the 0x55AA signature, with VolumeSerialNumber (offset 100) as a new exfat_serial identity rung. A `recognize` helper tries exFAT, then FAT, then the MBR disk-signature fallback, and sets each volume's FilesystemKind — so allVolumes/firstVolume and the GPT path all tag a volume with the engine its content needs. volume-map renders exfat- as the id-path, and filesystems.csv adds the exfat -> /system/services/exfat row: an exFAT stick now spawns the exFAT service, at its own content id-path. Host tests: a bare exFAT volume recognized with its serial; a FAT VBR still recognized as fat (the discrimination); the exfat- id render. build + zig build test + bounds green. --- system/configuration/filesystems.csv | 1 + system/services/volume-manager/partition.zig | 75 +++++++++++++++++-- system/services/volume-manager/volume-map.zig | 2 + 3 files changed, 70 insertions(+), 8 deletions(-) diff --git a/system/configuration/filesystems.csv b/system/configuration/filesystems.csv index 81dea8b..9170968 100644 --- a/system/configuration/filesystems.csv +++ b/system/configuration/filesystems.csv @@ -5,3 +5,4 @@ # # signature, binary fat, /system/services/fat +exfat, /system/services/exfat diff --git a/system/services/volume-manager/partition.zig b/system/services/volume-manager/partition.zig index f9eca22..fc07615 100644 --- a/system/services/volume-manager/partition.zig +++ b/system/services/volume-manager/partition.zig @@ -31,10 +31,11 @@ pub const label_maximum = 36; /// 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 + filesystem_uuid = 2, // reserved: no engine reads a superblock UUID yet fat_serial = 3, mbr_index = 4, anonymous = 5, + exfat_serial = 6, // exFAT's VolumeSerialNumber — content-strong like fat_serial }; /// A volume's content identity. `key` is the ID — the stable, unique handle the @@ -61,15 +62,16 @@ pub const Identity = struct { }; /// Which filesystem a volume's content is — the key `filesystems.csv` maps to a -/// service binary. Today only FAT is recognized (S4 adds exFAT with a real VBR -/// recognizer); until then every probed volume is `.fat`, matching the volume -/// manager's historical hand-off of everything to the FAT service. +/// service binary. FAT and exFAT are recognized by their VBRs; content that is +/// neither falls back to `.fat`, the volume manager's historical hand-off. pub const FilesystemKind = enum { fat, + exfat, unknown, pub fn fromToken(token: []const u8) FilesystemKind { if (std.mem.eql(u8, token, "fat")) return .fat; + if (std.mem.eql(u8, token, "exfat")) return .exfat; return .unknown; } }; @@ -226,7 +228,7 @@ fn gptAllVolumes(reader: SectorReader, device_blocks: u64, out: []Volume) usize 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]); - out[count] = .{ .base_lba = start, .block_count = end - start + 1, .identity = id }; + out[count] = .{ .base_lba = start, .block_count = end - start + 1, .identity = id, .signature = signatureAt(reader, start) }; count += 1; } return count; @@ -262,6 +264,36 @@ fn fatIdentity(reader: SectorReader, start_lba: u64) ?Identity { return id; } +/// The exFAT VolumeSerialNumber (offset 100) read from the Main Boot Sector at +/// `start_lba` — its content identity, rung `exfat_serial`. Null unless the sector +/// is an exFAT VBR (the "EXFAT " name at offset 3 + the 0x55AA signature; the +/// name is where a FAT BPB keeps its OEM string, so the two never collide). The +/// label lives in a root-directory entry, not the VBR, so it is left empty here. +fn exfatIdentity(reader: SectorReader, start_lba: u64) ?Identity { + var vbr: [sector_bytes]u8 = undefined; + if (!reader.read(start_lba, &vbr)) return null; + if (vbr[510] != 0x55 or vbr[511] != 0xAA) return null; + if (!std.mem.eql(u8, vbr[3..11], "EXFAT ")) return null; + return .{ .rung = .exfat_serial, .key = std.mem.readInt(u32, vbr[100..104], .little) }; +} + +const Recognized = struct { identity: Identity, signature: FilesystemKind }; + +/// Recognize the filesystem at `start_lba` by its VBR: exFAT first (its serial and +/// the `.exfat` signature), else FAT (its serial), else unknown content that keeps +/// the MBR disk-signature identity and the historical `.fat` hand-off. +fn recognize(reader: SectorReader, start_lba: u64, block0: *const [sector_bytes]u8, index: u8) Recognized { + if (exfatIdentity(reader, start_lba)) |id| return .{ .identity = id, .signature = .exfat }; + if (fatIdentity(reader, start_lba)) |id| return .{ .identity = id, .signature = .fat }; + return .{ .identity = mbrIdentity(block0, index), .signature = .fat }; +} + +/// The filesystem signature at `start_lba` when the identity is decided elsewhere +/// (a GPT partition keeps its GUID identity but still needs its content's kind). +fn signatureAt(reader: SectorReader, start_lba: u64) FilesystemKind { + return if (exfatIdentity(reader, start_lba) != null) .exfat else .fat; +} + /// 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 @@ -290,12 +322,14 @@ pub fn allVolumes(reader: SectorReader, device_blocks: u64, out: []Volume) usize // 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; - out[count] = .{ .base_lba = start, .block_count = size, .identity = fatIdentity(reader, start) orelse mbrIdentity(&block0, index) }; + const found = recognize(reader, start, &block0, index); + out[count] = .{ .base_lba = start, .block_count = size, .identity = found.identity, .signature = found.signature }; count += 1; } 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) }; + // No partition entries: a bare FAT or exFAT spanning the device. + const found = recognize(reader, 0, &block0, 0); + out[0] = .{ .base_lba = 0, .block_count = device_blocks, .identity = found.identity, .signature = found.signature }; return 1; } return count; @@ -360,6 +394,31 @@ test "no boot signature is no volume" { const disk = RamDisk{ .sectors = &block0 }; try std.testing.expect(firstVolume(disk.reader(), 65536) == null); } +test "a bare exFAT volume is recognized by its VBR, with its serial as the id" { + var block0 = [_]u8{0} ** 512; + @memcpy(block0[3..11], "EXFAT "); + block0[510] = 0x55; + block0[511] = 0xAA; + std.mem.writeInt(u32, block0[100..104], 0xDA7A0001, .little); // VolumeSerialNumber + const disk = RamDisk{ .sectors = &block0 }; + const v = firstVolume(disk.reader(), 65536).?; + try std.testing.expectEqual(FilesystemKind.exfat, v.signature); + try std.testing.expectEqual(Rung.exfat_serial, v.identity.rung); + try std.testing.expectEqual(@as(u128, 0xDA7A0001), v.identity.key); +} +test "a FAT VBR is recognized as fat, not exfat — the signatures never collide" { + var block0 = [_]u8{0} ** 512; + @memcpy(block0[3..11], "MSWIN4.1"); // a FAT OEM name, not "EXFAT " + block0[510] = 0x55; + block0[511] = 0xAA; + std.mem.writeInt(u16, block0[22..24], 16, .little); // fat_size_16 != 0 -> FAT16 shape + block0[38] = 0x29; // extended boot signature + std.mem.writeInt(u32, block0[39..43], 0x12345678, .little); // volume id + const disk = RamDisk{ .sectors = &block0 }; + const v = firstVolume(disk.reader(), 65536).?; + try std.testing.expectEqual(FilesystemKind.fat, v.signature); + try std.testing.expectEqual(Rung.fat_serial, v.identity.rung); +} test "a partition that runs past the device is skipped, not trusted" { var block0 = [_]u8{0} ** 512; diff --git a/system/services/volume-manager/volume-map.zig b/system/services/volume-manager/volume-map.zig index 42cecc5..aee9323 100644 --- a/system/services/volume-manager/volume-map.zig +++ b/system/services/volume-manager/volume-map.zig @@ -42,6 +42,7 @@ pub fn idString(identity: partition.Identity, buf: []u8) []const u8 { .gpt_guid => std.fmt.bufPrint(buf, "gpt-{x:0>32}", .{identity.key}) catch "", .filesystem_uuid => std.fmt.bufPrint(buf, "uuid-{x:0>32}", .{identity.key}) catch "", .fat_serial => std.fmt.bufPrint(buf, "fat-{x:0>8}", .{@as(u32, @truncate(identity.key))}) catch "", + .exfat_serial => std.fmt.bufPrint(buf, "exfat-{x:0>8}", .{@as(u32, @truncate(identity.key))}) catch "", .mbr_index => std.fmt.bufPrint(buf, "mbr-{x}-{d}", .{ @as(u32, @truncate(identity.key >> 8)), @as(u8, @truncate(identity.key & 0xff)), @@ -105,6 +106,7 @@ const test_override_slots = 4; test "idString renders each rung's id token" { var buf: [id_maximum]u8 = undefined; try testing.expectEqualStrings("fat-12345678", idString(.{ .rung = .fat_serial, .key = 0x12345678 }, &buf)); + try testing.expectEqualStrings("exfat-da7a0001", idString(.{ .rung = .exfat_serial, .key = 0xDA7A0001 }, &buf)); try testing.expectEqualStrings("mbr-deadbeef-1", idString(.{ .rung = .mbr_index, .key = (@as(u128, 0xDEADBEEF) << 8) | 1 }, &buf)); const guid: u128 = 0x00112233445566778899AABBCCDDEEFF; try testing.expectEqualStrings("gpt-00112233445566778899aabbccddeeff", idString(.{ .rung = .gpt_guid, .key = guid }, &buf));