diff --git a/build.zig b/build.zig index 0c50026..156c1c8 100644 --- a/build.zig +++ b/build.zig @@ -122,6 +122,7 @@ fn driverArtifact(comptime package: []const u8, comptime artifact: []const u8) S /// (docs/build-packages-plan.md). const production_ship = [_]ShipRow{ service("fat"), + service("exfat"), service("display"), service("display-demo"), service("device-manager"), @@ -422,10 +423,6 @@ pub fn build(b: *std.Build) void { "system/boot-handoff.zig", "system/abi.zig", "system/initial-ramdisk.zig", // v2 path-named entries: find/basename/magic - // The exFAT engine (S4), std-only, before its package exists — the - // engine imports on-disk.zig, so this one entry runs both files' tests. - // Moves into the `exfat` package's own test step once that lands (step 4). - "system/services/exfat/engine.zig", }) |root| { const mod_tests = b.addTest(.{ .root_module = b.createModule(.{ @@ -451,6 +448,7 @@ pub fn build(b: *std.Build) void { csv_library, xkeyboard_config_library, b.dependency("fat", .{}), + b.dependency("exfat", .{}), b.dependency("volume-manager", .{}), b.dependency("display", .{}), b.dependency("ps2-bus", .{}), diff --git a/build.zig.zon b/build.zig.zon index 2b2a5c9..d02ada3 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -46,6 +46,7 @@ .@"pci-bus" = .{ .path = "system/drivers/pci-bus" }, .init = .{ .path = "system/services/init" }, .fat = .{ .path = "system/services/fat" }, + .exfat = .{ .path = "system/services/exfat" }, .display = .{ .path = "system/services/display" }, .@"display-demo" = .{ .path = "system/services/display-demo" }, .@"device-manager" = .{ .path = "system/services/device-manager" }, diff --git a/system/services/exfat/build.zig b/system/services/exfat/build.zig new file mode 100644 index 0000000..9aaa0ed --- /dev/null +++ b/system/services/exfat/build.zig @@ -0,0 +1,34 @@ +//! The exfat service 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", + .root_source_file = b.path("exfat.zig"), + .imports = &.{ + "block", "channel", "envelope", "file-system-harness", + "ipc", "logging", "memory", "process", + "time", "volume-manager-protocol", + }, + }); + b.installArtifact(exe); + + // Standalone `zig build test`; the root aggregate depends on this step. + const test_step = b.step("test", "Run the exfat unit tests"); + for ([_][]const u8{ + "on-disk.zig", // exFAT on-disk struct sizes + geometry + checksums + "engine.zig", // exFAT read/write over a RAM-backed image + }) |test_root| { + const unit_tests = b.addTest(.{ + .root_module = b.createModule(.{ + .root_source_file = b.path(test_root), + .target = b.resolveTargetQuery(.{}), + }), + }); + test_step.dependOn(&b.addRunArtifact(unit_tests).step); + } +} diff --git a/system/services/exfat/build.zig.zon b/system/services/exfat/build.zig.zon new file mode 100644 index 0000000..aeb752b --- /dev/null +++ b/system/services/exfat/build.zig.zon @@ -0,0 +1,16 @@ +.{ + .name = .exfat, + .version = "0.0.0", + .fingerprint = 0x5eafdf02d20f93dd, // 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" }, + .device = .{ .path = "../../../library/device" }, + .protocol = .{ .path = "../../../library/protocol" }, + }, + .paths = .{""}, +} diff --git a/system/services/exfat/exfat.zig b/system/services/exfat/exfat.zig new file mode 100644 index 0000000..22a6b92 --- /dev/null +++ b/system/services/exfat/exfat.zig @@ -0,0 +1,200 @@ +//! system/services/exfat — the exFAT filesystem service. Like fat.zig, this is +//! only the format-specific half: it finds its block device, sets up the DMA +//! bounce buffer, mounts the exFAT engine on it, and hands the mounted volume to +//! the shared filesystem harness (library/kernel/file-system-harness), which owns +//! everything else — vfs serving, the open-node table, mount registration, the +//! exit sweep, durable-on-close. The engine (engine.zig) is the pure, +//! host-testable format code; on-disk.zig its byte layout. +//! +//! This service is a near-clone of fat.zig: the second engine reuses the harness +//! wholesale, which is the reuse the storage architecture promised +//! (docs/file-system-development/storage-architecture.md). The block data path +//! never crosses IPC: a DMA bounce buffer is handed to the block driver by +//! physical address, and the engine copies sectors in and out. + +const std = @import("std"); +const channel = @import("channel"); +const volume_manager_protocol = @import("volume-manager-protocol"); +const ipc = @import("ipc"); +const process = @import("process"); +const block = @import("block"); +const memory = @import("memory"); +const logging = @import("logging"); +const time = @import("time"); +const engine = @import("engine.zig"); +const envelope = @import("envelope"); +const harness = @import("file-system-harness"); + +/// The serving harness, specialized for the exFAT engine. One volume per process. +const Harness = harness.Server(engine.FileSystem); + +// The engine's BlockDevice, backed by the `.block` driver plus a DMA bounce +// buffer the driver reads/writes by physical address. +const IpcBlock = struct { + device: block.Device, + bounce: memory.DmaRegion, // engine.max_transfer_sectors * 512 bytes + + fn readBlocks(context: *anyopaque, lba: u64, count: u32, buffer: []u8) bool { + const self: *IpcBlock = @ptrCast(@alignCast(context)); + if (count == 0 or count > engine.max_transfer_sectors) return false; + const len = count * 512; + if (!self.device.read(lba, count, self.bounce.physical)) return false; + const source: [*]const u8 = @ptrFromInt(self.bounce.virtual); + @memcpy(buffer[0..len], source[0..len]); + return true; + } + fn writeBlocks(context: *anyopaque, lba: u64, count: u32, buffer: []const u8) bool { + const self: *IpcBlock = @ptrCast(@alignCast(context)); + if (count == 0 or count > engine.max_transfer_sectors) return false; + const len = count * 512; + const destination: [*]u8 = @ptrFromInt(self.bounce.virtual); + @memcpy(destination[0..len], buffer[0..len]); + if (!self.device.write(lba, count, self.bounce.physical)) return false; + device_dirty = true; // a block reached the device; a close will flush it + return true; + } +}; + +var ipc_block: IpcBlock = undefined; +// Set whenever a block is written, cleared when the device cache is flushed on a +// file close — so writes are committed to stable media before a power-off. +var device_dirty: bool = false; +var filesystem: engine.FileSystem = undefined; +/// The volume this exFAT process serves, its id given as argv[1] by the volume +/// manager that spawned it. The startup hello names it so the manager returns the +/// right volume's channel. +var my_volume_id: u64 = 0; + +/// The volume's own mount path, handed in as argv[2] by the volume manager: the +/// volume's content id-path (e.g. /volumes/exfat-12345678). Defaults to +/// /volumes/exfat only for a bare launch with no argument; the manager always +/// passes it. The slice points into the entry block, valid for the process life. +var volume_mount_prefix: []const u8 = "/volumes/exfat"; + +/// The mounts this volume installs: its own root, plus — only if it is the boot +/// volume (it resolves /system/configuration) — the two FHS rewrites, so the +/// logger's /system/logs stays decoupled from which volume backs it. Boot-volume +/// detection is by content, so it works no matter which volume carries /system. +/// bound: mounts one volume installs (its root + the two boot rewrites) +/// decided-by: ours +/// protects: the mount_specs array +/// at-limit: truncate - unreachable today (fixed at 3); more configured mounts +/// would need this raised, a deliberate change +/// observed-by: a mount silently missing from the harness's mount log +const maximum_mounts_per_volume = 4; +var mount_specs: [maximum_mounts_per_volume]harness.MountSpec = undefined; + +/// Get this volume's block channel from the volume manager (establishment by +/// lineage — `block` is not a registry name). The manager spawned this process, +/// confined it to its partition, and answers the hello with the channel; the +/// channel is range-confined to this process's badge. Null until the manager has +/// the volume ready — this retries. +fn acquireVolume() ?block.Device { + var attempts: u32 = 0; + const vm = while (attempts < 500) : (attempts += 1) { + if (channel.openEndpoint("volume-manager")) |handle| break handle; + time.sleepMillis(20); + } else return null; + + attempts = 0; + while (attempts < 500) : (attempts += 1) { + var packet: [volume_manager_protocol.message_maximum]u8 = undefined; + const framed = volume_manager_protocol.Protocol.encodeRequest(.hello, my_volume_id, .{}, &.{}, &packet) orelse return null; + var reply: [volume_manager_protocol.message_maximum]u8 = undefined; + const answered = ipc.callCap(vm, framed, &reply, null) catch return null; + const status = envelope.statusOf(reply[0..answered.len]) orelse return null; + if (status.status != 0) { + if (answered.cap) |stray| _ = ipc.close(stray); + _ = logging.write("/system/services/exfat: volume manager refused the hello\n"); + return null; + } + if (answered.cap) |bus| return .{ .endpoint = bus }; + // Acked with no channel: the volume is not ready yet — retry. + time.sleepMillis(20); + } + return null; +} + +/// Durable-on-close: commit the device write cache if any block reached it since +/// the last flush. The harness calls this on every close; the dirty check keeps +/// it cheap. +fn flushIfDirty() void { + if (device_dirty) { + _ = ipc_block.device.flush(); + device_dirty = false; + } +} + +/// exFAT bring-up: find the block device, set up DMA, mount the engine, and hand +/// the volume to the harness — or null to retry on the harness's timer. +fn exfatBringUp(endpoint: ipc.Handle) ?Harness.Volume { + _ = endpoint; + const device = acquireVolume() orelse return null; + const geometry = device.geometry() orelse { + _ = logging.write("/system/services/exfat: block geometry unavailable\n"); + return null; + }; + // Shareable so the buffer's capability can be attached down the chain, making + // its physical addresses reachable under an enforcing IOMMU. No-op otherwise. + const bounce = memory.dmaAlloc(engine.max_transfer_sectors * 512, memory.dma_coherent | memory.dma_shareable) orelse return null; + if (bounce.handle) |handle| { + // Attach, detach, and attach again: the round trip exercises BOTH verbs of + // the DMA-window lifecycle through the whole chain on every boot. + if (!device.attach(handle)) { + _ = logging.write("/system/services/exfat: could not attach the DMA bounce buffer\n"); + return null; + } + if (!device.detach(handle)) { + _ = logging.write("/system/services/exfat: could not detach the DMA bounce buffer\n"); + return null; + } + if (!device.attach(handle)) { + _ = logging.write("/system/services/exfat: could not re-attach the DMA bounce buffer\n"); + return null; + } + _ = ipc.close(handle); // the binding holds its own reference now + } + ipc_block = .{ .device = device, .bounce = bounce }; + + const block_device = engine.BlockDevice{ + .context = &ipc_block, + .block_size = geometry.block_size, + .block_count = geometry.block_count, + .readBlocksFn = IpcBlock.readBlocks, + .writeBlocksFn = IpcBlock.writeBlocks, + }; + filesystem = engine.FileSystem.mount(block_device) orelse { + _ = logging.write("/system/services/exfat: not an exFAT filesystem\n"); + return null; + }; + std.log.info("mounted exFAT ({d} clusters, {d} sectors/cluster, serial 0x{x})", .{ filesystem.geometry.cluster_count, filesystem.geometry.sectors_per_cluster, filesystem.geometry.volume_serial_number }); + + // The volume mounts at its id-path (argv[2]). The boot/system volume — the one + // carrying the /system tree — additionally installs the two FHS rewrites, by + // CONTENT: it resolves /system/configuration on its own media. A data volume + // mounts only at its id-path and never shadows the running system. + mount_specs[0] = .{ .prefix = volume_mount_prefix }; + var mount_count: usize = 1; + if (filesystem.resolve("/system/configuration") != null) { + std.log.info("volume {d} carries the system tree; backing /system/configuration and /system/logs", .{my_volume_id}); + mount_specs[1] = .{ .prefix = "/system/configuration", .rewrite = "/system/configuration" }; + mount_specs[2] = .{ .prefix = "/system/logs", .rewrite = "/system/logs" }; + mount_count = 3; + } else { + std.log.info("volume {d} is a data volume; mounted at {s}", .{ my_volume_id, volume_mount_prefix }); + } + return .{ .engine = &filesystem, .mounts = mount_specs[0..mount_count], .flush = flushIfDirty }; +} + +pub fn main(init: process.Init) void { + // The volume manager spawns this process with its volume id as argv[1] and the + // volume's mount path (its id-path) as argv[2]. + if (init.arguments.get(1)) |id| { + my_volume_id = std.fmt.parseInt(u64, id, 10) catch 0; + } + if (init.arguments.get(2)) |prefix| { + volume_mount_prefix = prefix; + } + _ = logging.write("/system/services/exfat: starting, waiting for a block device\n"); + Harness.run(.{ .bringUp = exfatBringUp }); +}