//! system/services/fat — the FAT filesystem server. Spawned as a boot service, it //! opens the block device (a USB stick via usb-storage) under `.block`, mounts the //! FAT filesystem on it (the pure engine in engine.zig), and mounts itself into //! the VFS at /volumes/usb. From then on the VFS forwards every open/read/write/ //! status/readdir/close under /volumes/usb to this server, which serves the same //! vfs-protocol as a backend — turning block reads into file reads. //! //! 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 of //! it. const std = @import("std"); const ipc = @import("ipc"); const process = @import("process"); const service = @import("service"); const time = @import("time"); const block = @import("block"); const file_system = @import("file-system"); const memory = @import("memory"); const logging = @import("logging"); const engine = @import("engine.zig"); const on_disk = @import("on-disk.zig"); const vfs_protocol = @import("vfs-protocol"); const mount_point = "/volumes/usb"; // 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; // Open handles the VFS holds against this backend: each maps a node id to a // resolved engine node. const OpenNode = struct { used: bool = false, node: engine.Node = undefined, owner: u32 = 0 }; var open_nodes = [_]OpenNode{.{}} ** 32; fn allocOpen() ?usize { for (&open_nodes, 0..) |*o, i| { if (!o.used) return i; } return null; } fn openAt(id: u64) ?*OpenNode { if (id >= open_nodes.len) return null; const o = &open_nodes[@intCast(id)]; return if (o.used) o else null; } fn writeReply(out: []u8, reply: vfs_protocol.Reply, payload: []const u8) usize { @memcpy(out[0..vfs_protocol.reply_size], std.mem.asBytes(&reply)); const n = @min(payload.len, out.len - vfs_protocol.reply_size); @memcpy(out[vfs_protocol.reply_size..][0..n], payload[0..n]); return vfs_protocol.reply_size + n; } fn fail(out: []u8) usize { return writeReply(out, .{ .status = -1 }, &.{}); } /// How often to look for a block device while none is mounted. Storage arriving /// is EVENT-shaped (the usb chain registering, possibly after a driver restart), /// but the registry has no subscription — a slow poll from our own harness loop /// keeps the service responsive (ping, terminate) while it waits, and keeps it /// alive to catch storage that appears LATE (a restarted usb-storage after a /// transient failure — the resilience half of docs/logging.md's storage story). const mount_retry_ms = 500; var mounted = false; var service_endpoint: ipc.Handle = 0; fn initialise(endpoint: ipc.Handle) bool { service_endpoint = endpoint; _ = logging.write("/system/services/fat: starting, waiting for a block device\n"); // With the router in the kernel, clients hold OUR node ids directly; sweep // a dead client's open handles via the published exit events (the pattern // the old userspace router used for its own table). _ = process.subscribeExits(endpoint); tryBringUp(); if (!mounted) _ = time.timerOnce(endpoint, mount_retry_ms); return true; // serve regardless: requests fail politely until storage mounts } /// One storage bring-up attempt: block device -> FAT mount -> VFS mounts. Sets /// `mounted` on success; a failure leaves everything untouched for the next tick. fn tryBringUp() void { if (mounted) return; const device = block.tryOpen() orelse return; const geometry = device.geometry() orelse { _ = logging.write("/system/services/fat: block geometry unavailable\n"); return; }; // Shareable so the buffer's capability can be attached down the chain (block server // -> controller), making its physical addresses reachable by the device under an // enforcing IOMMU. No-op binding otherwise. const bounce = memory.dmaAlloc(engine.max_transfer_sectors * 512, memory.dma_coherent | memory.dma_shareable) orelse return; if (bounce.handle) |handle| { if (!device.attach(handle)) { _ = logging.write("/system/services/fat: could not attach the DMA bounce buffer\n"); return; } _ = 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/fat: not a FAT filesystem\n"); return; }; std.log.info("mounted FAT ({s}, {d} clusters, partition lba {d})", .{ @tagName(filesystem.geometry.fat_type), filesystem.geometry.cluster_count, filesystem.base_lba }); // Mount ourselves into the kernel VFS at /volumes/usb — and serve // /system/configuration and /system/logs from the volume's identically-named // subtrees (the boot volume is hierarchy-shaped, so rewrite == prefix), so // hierarchy paths (the logger's /system/logs) stay decoupled from which // volume carries them. if (file_system.mount(mount_point, endpointForMount())) { std.log.info("mounted {s}", .{mount_point}); } else { _ = logging.write("/system/services/fat: could not mount /volumes/usb\n"); } if (file_system.mountRewritten("/system/configuration", endpointForMount(), "/system/configuration")) { std.log.info("mounted /system/configuration", .{}); } else { _ = logging.write("/system/services/fat: could not mount /system/configuration\n"); } if (file_system.mountRewritten("/system/logs", endpointForMount(), "/system/logs")) { std.log.info("mounted /system/logs", .{}); } else { _ = logging.write("/system/services/fat: could not mount /system/logs\n"); } mounted = true; } fn endpointForMount() ipc.Handle { return service_endpoint; } /// A subscribed process-exit event: release every open handle the dead client /// held, so a crashed reader can't pin table slots (or, later, locks). fn onNotification(badge: u64) void { const got = ipc.Received{ .len = 0, .badge = badge, .cap = null }; if (got.isTimer()) { tryBringUp(); if (!mounted) _ = time.timerOnce(service_endpoint, mount_retry_ms); return; } if (!got.isChildExit()) return; const dead = got.childProcessId(); var released: u32 = 0; for (&open_nodes) |*o| { if (o.used and o.owner == dead) { o.* = .{}; released += 1; } } if (released != 0) std.log.info("released {d} handle(s) for dead client {d}", .{ released, dead }); } const ParentLeaf = struct { parent: []const u8, leaf: []const u8 }; // Split a path into its parent directory and final component: "/a/b" -> ("/a", // "b"); "/b" -> ("/", "b"); "b" -> ("/", "b"). fn splitParent(path: []const u8) ParentLeaf { const slash = std.mem.lastIndexOfScalar(u8, path, '/'); return .{ .parent = if (slash) |s| (if (s == 0) "/" else path[0..s]) else "/", .leaf = if (slash) |s| path[s + 1 ..] else path, }; } fn handleOpen(out: []u8, path: []const u8, flags: u32, sender: u32) usize { var node = filesystem.resolve(path); if (node == null and flags & vfs_protocol.create != 0) { const split = splitParent(path); const parent = filesystem.resolve(split.parent) orelse return fail(out); node = filesystem.createFile(parent, split.leaf); } var resolved = node orelse return fail(out); // O_TRUNC: replace an existing file's contents rather than overwriting in place // (frees the old chain, so a shorter rewrite leaves no stale tail). if (flags & vfs_protocol.truncate != 0 and !resolved.is_directory) { filesystem.truncate(&resolved); } const index = allocOpen() orelse return fail(out); open_nodes[index] = .{ .used = true, .node = resolved, .owner = sender }; return writeReply(out, .{ .status = 0, .node = index }, &.{}); } /// The vfs protocol has no operation that takes a capability, so `arrived` is /// never claimed here — which, under the harness's ownership rule, means the /// loop closes whatever a caller attached. That is the point of the rule: this /// callback used to discard a `?ipc.Handle` and every request carrying one — a /// legal thing for any client to do — spent a slot of the VFS server's /// thirty-two until it could accept no capability at all. fn onMessage(message: []const u8, out: []u8, sender: u32, arrived: *ipc.Arrival) usize { _ = arrived; if (!mounted) return fail(out); // storage not up (yet): fail politely, clients retry if (message.len < vfs_protocol.request_size) return fail(out); const request = std.mem.bytesToValue(vfs_protocol.Request, message[0..vfs_protocol.request_size]); const payload = message[vfs_protocol.request_size..]; // Stamp create/write with the current wall-clock time (mtime). Cheap, and it // keeps the engine pure (it takes the time as data, not a syscall). filesystem.current_time_epoch = time.wallClock(); switch (request.operation) { .open => return handleOpen(out, payload[0..@min(payload.len, request.len)], request.flags, sender), .read => { const o = openAt(request.node) orelse return fail(out); var buffer: [vfs_protocol.maximum_payload]u8 = undefined; const want = @min(@as(usize, request.len), buffer.len); const n = filesystem.readFile(o.node, @intCast(request.offset), buffer[0..want]); return writeReply(out, .{ .status = 0, .len = @intCast(n) }, buffer[0..n]); }, .write => { const o = openAt(request.node) orelse return fail(out); const data = payload[0..@min(payload.len, request.len)]; const n = filesystem.writeFile(&o.node, @intCast(request.offset), data); return writeReply(out, .{ .status = 0, .len = @intCast(n) }, &.{}); }, .status => { const o = openAt(request.node) orelse return fail(out); const kind: vfs_protocol.NodeKind = if (o.node.is_directory) .directory else .regular; const status = vfs_protocol.FileStatus{ .size = o.node.size, .kind = @intFromEnum(kind), .mtime = o.node.mtime }; return writeReply(out, .{ .status = 0, .len = @sizeOf(vfs_protocol.FileStatus) }, std.mem.asBytes(&status)); }, .readdir => { const o = openAt(request.node) orelse return fail(out); if (!o.node.is_directory) return writeReply(out, .{ .status = 0, .len = 0 }, &.{}); const listing = filesystem.listEntry(o.node, @intCast(request.offset)) orelse return writeReply(out, .{ .status = 0, .len = 0 }, &.{}); const kind: vfs_protocol.NodeKind = if (listing.is_directory) .directory else .regular; const header = vfs_protocol.DirectoryEntry{ .kind = @intFromEnum(kind), .name_len = @intCast(listing.name_len), .size = listing.size }; var buffer: [vfs_protocol.maximum_payload]u8 = undefined; @memcpy(buffer[0..vfs_protocol.directory_entry_size], std.mem.asBytes(&header)); const nlen = @min(listing.name_len, buffer.len - vfs_protocol.directory_entry_size); @memcpy(buffer[vfs_protocol.directory_entry_size..][0..nlen], listing.name_buffer[0..nlen]); const total = vfs_protocol.directory_entry_size + nlen; return writeReply(out, .{ .status = 0, .len = @intCast(total) }, buffer[0..total]); }, .close => { if (openAt(request.node)) |o| o.used = false; // Durable-on-close: if any block reached the device since the last // flush, commit its cache to stable media now (best-effort). This is // what makes init's shutdown log flush survive a real power-off, and is // the right default for removable media the user may unplug. if (device_dirty) { _ = ipc_block.device.flush(); device_dirty = false; } return writeReply(out, .{ .status = 0 }, &.{}); }, .mkdir => { const path = payload[0..@min(payload.len, request.len)]; if (filesystem.resolve(path) != null) return fail(out); // already exists — no duplicate entries const split = splitParent(path); const parent = filesystem.resolve(split.parent) orelse return fail(out); if (filesystem.createDirectory(parent, split.leaf) == null) return fail(out); return writeReply(out, .{ .status = 0 }, &.{}); }, .unlink => { const split = splitParent(payload[0..@min(payload.len, request.len)]); const parent = filesystem.resolve(split.parent) orelse return fail(out); if (!filesystem.removeFile(parent, split.leaf)) return fail(out); return writeReply(out, .{ .status = 0 }, &.{}); }, .rename => { const both = payload[0..@min(payload.len, request.len)]; const sep = std.mem.indexOfScalar(u8, both, 0) orelse return fail(out); const old_split = splitParent(both[0..sep]); const new_split = splitParent(both[sep + 1 ..]); // Same-directory rename only. if (!std.mem.eql(u8, old_split.parent, new_split.parent)) return fail(out); const parent = filesystem.resolve(old_split.parent) orelse return fail(out); if (!filesystem.rename(parent, old_split.leaf, new_split.leaf)) return fail(out); return writeReply(out, .{ .status = 0 }, &.{}); }, // A backend is never itself a mount target. // Router verbs, and the registry's claim verb: a file backend answers // none of them (docs/os-development/protocol-namespace.md — only init // implements `bind`). .mount, .unmount, .bind => return fail(out), } } pub fn main() void { service.run(vfs_protocol.message_maximum, .{ .service = "vfs", .init = initialise, .on_message = onMessage, .on_notification = onNotification, }); }