diff --git a/build.zig b/build.zig index 156c1c8..41c7926 100644 --- a/build.zig +++ b/build.zig @@ -333,6 +333,7 @@ pub fn build(b: *std.Build) void { if (test_case != null) for ([_][]const u8{ "vfs-test", // the user-space VFS round-trip client "fat-test", + "exfat-test", // the exFAT mount round-trip client (S4) "badge-scope-test", // the guessable-id probe: a second process names the first's node and layer "shared-memory-server", "shared-memory-client", diff --git a/build.zig.zon b/build.zig.zon index d02ada3..a65500f 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -65,6 +65,7 @@ .@"virtio-gpu" = .{ .path = "system/drivers/virtio-gpu" }, .@"vfs-test" = .{ .path = "test/system/services/vfs-test", .lazy = true }, .@"fat-test" = .{ .path = "test/system/services/fat-test", .lazy = true }, + .@"exfat-test" = .{ .path = "test/system/services/exfat-test", .lazy = true }, .@"badge-scope-test" = .{ .path = "test/system/services/badge-scope-test", .lazy = true }, .@"shared-memory-server" = .{ .path = "test/system/services/shared-memory-server", .lazy = true }, .@"shared-memory-client" = .{ .path = "test/system/services/shared-memory-client", .lazy = true }, diff --git a/system/services/exfat/engine.zig b/system/services/exfat/engine.zig index d7108aa..48043ed 100644 --- a/system/services/exfat/engine.zig +++ b/system/services/exfat/engine.zig @@ -1276,3 +1276,21 @@ test "rename keeps the file's contents under the new name" { _ = fs.readFile(renamed, 0, &readback); try std.testing.expectEqualSlices(u8, &payload, &readback); } + +test "the exFAT engine rejects a FAT volume (mutual exclusion at mount)" { + const allocator = std.testing.allocator; + const bytes = try allocator.alloc(u8, 128 * sector_size); + defer allocator.free(bytes); + @memset(bytes, 0); + // A FAT-shaped boot sector: an OEM name and a non-zero bytes-per-sector where + // exFAT keeps MustBeZero zero, plus the 0x55AA signature. + @memcpy(bytes[3..11], "MSWIN4.1"); + std.mem.writeInt(u16, bytes[11..13], 512, .little); // FAT bytes_per_sector = exFAT MustBeZero + bytes[510] = 0x55; + bytes[511] = 0xAA; + var disk = RamDisk{ .bytes = bytes }; + try std.testing.expect(FileSystem.mount(disk.device()) == null); + // Control: it mounts its own. + formatExfat(bytes); + try std.testing.expect(FileSystem.mount(disk.device()) != null); +} diff --git a/system/services/fat/engine.zig b/system/services/fat/engine.zig index de1723c..b1ad462 100644 --- a/system/services/fat/engine.zig +++ b/system/services/fat/engine.zig @@ -1657,3 +1657,21 @@ test "short-name checksum matches the reference vector" { const c = FileSystem.shortChecksum("REDAME TXT".*); try std.testing.expect(a != c); } + +test "the FAT engine rejects an exFAT volume (mutual exclusion at mount)" { + const allocator = std.testing.allocator; + const bytes = try allocator.alloc(u8, 5000 * sector_size); + defer allocator.free(bytes); + @memset(bytes, 0); + // An exFAT boot sector: the "EXFAT " name and 0x55AA, but MustBeZero (offset + // 11, where a FAT BPB keeps bytes-per-sector) stays zero — so this engine's + // geometryOf reads a zero bytes-per-sector and rejects it. + @memcpy(bytes[3..11], "EXFAT "); + bytes[on_disk.boot_signature_offset] = 0x55; + bytes[on_disk.boot_signature_offset + 1] = 0xAA; + var disk = RamDisk{ .bytes = bytes }; + try std.testing.expect(FileSystem.mount(disk.device()) == null); + // Control: a real FAT16 mounts. + formatFat16(bytes); + try std.testing.expect(FileSystem.mount(disk.device()) != null); +} diff --git a/test/system/services/exfat-test/build.zig b/test/system/services/exfat-test/build.zig new file mode 100644 index 0000000..06aeaa5 --- /dev/null +++ b/test/system/services/exfat-test/build.zig @@ -0,0 +1,15 @@ +//! The exfat-test test fixture as a binary package (docs/build-packages-plan.md): +//! this file names the binary and EXACTLY the modules its source imports — +//! build-support resolves each name from the domains this zon declares. + +const std = @import("std"); +const build_support = @import("build-support"); + +pub fn build(b: *std.Build) void { + const exe = build_support.userBinary(b, .{ + .name = "exfat-test", + .root_source_file = b.path("exfat-test.zig"), + .imports = &.{ "file-system", "logging", "process", "time" }, + }); + b.installArtifact(exe); +} diff --git a/test/system/services/exfat-test/build.zig.zon b/test/system/services/exfat-test/build.zig.zon new file mode 100644 index 0000000..ea6f0de --- /dev/null +++ b/test/system/services/exfat-test/build.zig.zon @@ -0,0 +1,14 @@ +.{ + .name = .exfat_test, + .version = "0.0.0", + .fingerprint = 0x77b19e3f7ee43ce3, // Changing this has security and trust implications. + .minimum_zig_version = "0.16.0", + .dependencies = .{ + // build-support supplies the shared recipe; kernel is implicit in + // every binary (the root shim + link script live there). The rest + // are exactly the homes of this binary's declared imports. + .@"build-support" = .{ .path = "../../../../build-support" }, + .kernel = .{ .path = "../../../../library/kernel" }, + }, + .paths = .{""}, +} diff --git a/test/system/services/exfat-test/exfat-test.zig b/test/system/services/exfat-test/exfat-test.zig new file mode 100644 index 0000000..5f779ac --- /dev/null +++ b/test/system/services/exfat-test/exfat-test.zig @@ -0,0 +1,88 @@ +//! test/system/services/exfat-test — a client that proves the exFAT mount end to +//! end: it waits for the exfat server to mount the volume at /volumes/exfat- +//! e0fa0001, reads the seeded HELLO.TXT off it, and exercises mkdir / write / +//! read / rename / remove through the VFS (which routes the id-path to the exfat +//! backend). Shipped in the initial_ramdisk; the `exfat-volume` QEMU case spawns +//! it alongside init with an exFAT data device attached beside the FAT boot volume. + +const std = @import("std"); +const fs = @import("file-system"); +const process = @import("process"); +const time = @import("time"); +const logging = @import("logging"); + +// The exFAT data device's content id-path — its VolumeSerialNumber is 0xE0FA0001 +// (the `exfat-volume` case passes --serial E0FA0001 to make-exfat-image.py). +const mount = "/volumes/exfat-e0fa0001"; + +fn writeLine(comptime fmt: []const u8, arguments: anytype) void { + var line: [128]u8 = undefined; + _ = logging.write(std.fmt.bufPrint(&line, fmt, arguments) catch return); +} + +pub fn main(init: process.Init) void { + _ = init; + + // Wait for the exfat server to bring up the USB storage chain and mount. + var opened: ?fs.Directory = null; + var tries: u32 = 0; + while (opened == null and tries < 1400) : (tries += 1) { + opened = fs.openDirectory(mount); + if (opened == null) time.sleepMillis(50); + } + var dir = opened orelse { + _ = logging.write("exfat-test: " ++ mount ++ " never became available\n"); + return; + }; + var count: u32 = 0; + var entry: fs.Entry = .{}; + while (dir.next(&entry)) { + writeLine("exfat-test: entry '{s}' size={d}\n", .{ entry.name(), entry.size }); + count += 1; + if (count > 32) break; + } + dir.close(); + + // Read the seeded HELLO.TXT (make-exfat-image.py writes "exfat hello danos\n"). + var read_ok = false; + if (fs.open(mount ++ "/HELLO.TXT", .{})) |opened_file| { + var file = opened_file; + var buf: [32]u8 = undefined; + const n = file.read(&buf) orelse 0; + file.close(); + read_ok = std.mem.startsWith(u8, buf[0..n], "exfat hello danos"); + } + if (read_ok) _ = logging.write("exfat-test: read HELLO.TXT ok\n"); + + // Mutation through the mount: mkdir, create + write, rename, read back, remove + // — proof the write path reaches the engine over a real device. + var mut_ok = false; + if (fs.makeDirectory(mount ++ "/TESTDIR")) { + var wrote = false; + if (fs.open(mount ++ "/TESTDIR/W.TXT", .{ .create = true, .truncate = true })) |created| { + var f = created; + wrote = (f.writeAll("exfat-mutation-ok") orelse 0) == "exfat-mutation-ok".len; + f.close(); + } + const renamed = fs.rename(mount ++ "/TESTDIR/W.TXT", mount ++ "/TESTDIR/R.TXT"); + var readback = false; + if (fs.open(mount ++ "/TESTDIR/R.TXT", .{})) |reopened| { + var f = reopened; + var buf: [32]u8 = undefined; + const got = f.read(&buf) orelse 0; + f.close(); + readback = std.mem.eql(u8, buf[0..got], "exfat-mutation-ok"); + } + const removed = fs.remove(mount ++ "/TESTDIR/R.TXT"); + mut_ok = wrote and renamed and readback and removed; + } + if (mut_ok) _ = logging.write("exfat-test: mutations ok\n"); + + if (read_ok and mut_ok) { + while (true) { + _ = logging.write("exfat-test: ok\n"); + time.sleepMillis(1000); + } + } + writeLine("exfat-test: FAILED (read={} mutations={})\n", .{ read_ok, mut_ok }); +}