//! system/services/vfs — the user-space VFS server. Shipped in the initial_ramdisk, spawned as a //! ring-3 process, and reached by every other process through IPC (the `runtime` //! file API marshals open/read/write/stat/close into calls to this server's //! endpoint, published under the well-known `vfs` service id). //! //! Two namespaces meet here (M5): //! - a small in-memory **ramfs** — opening a bare name creates it — enough to //! prove the round trip and to back the existing tests; //! - **mounted filesystems**: a mount table maps an absolute path prefix (e.g. //! `/mnt/usb`) to a backend server's endpoint. An open of a path under a mount //! is *forwarded* to that backend (which speaks this same protocol), and every //! later read/write/status/readdir/close on the resulting handle is relayed to //! it. The VFS is the router; a filesystem (FAT) is the backend. //! //! A path routes through a mount only when it is absolute and lies under a mount //! prefix; bare names always resolve in the flat ramfs — the backward-compat //! contract the `vfs` / `vfs-client-death` tests rely on. const std = @import("std"); const runtime = @import("runtime"); const protocol = runtime.vfs_protocol; const path = @import("path.zig"); const ipc = runtime.ipc; const Node = struct { used: bool = false, name: [24]u8 = undefined, name_len: usize = 0, data: [512]u8 = undefined, size: usize = 0, }; const OpenFile = struct { used: bool = false, // For a local handle: an index into `nodes`. For a forwarding handle: the // node id the backend returned. (usize == u64 here, so it holds either.) node: usize = 0, // Non-null for a handle that forwards to a mounted backend. backend: ?ipc.Handle = null, // The client (task id — an IPC badge is one) that opened this handle. What // release-on-death sweeps by: a service must never depend on its clients // cleaning up after themselves (docs/process-lifecycle.md). owner: u32 = 0, }; // One mounted filesystem: an absolute path prefix and the backend endpoint that // serves everything under it. const Mount = struct { used: bool = false, prefix: [64]u8 = undefined, prefix_len: usize = 0, backend: ipc.Handle = 0, }; var nodes = [_]Node{.{}} ** 8; var opens = [_]OpenFile{.{}} ** 16; var mounts = [_]Mount{.{}} ** 8; fn findNode(name: []const u8) ?usize { for (&nodes, 0..) |*n, i| { if (n.used and std.mem.eql(u8, n.name[0..n.name_len], name)) return i; } return null; } fn createNode(name: []const u8) ?usize { for (&nodes, 0..) |*n, i| { if (!n.used) { const l = @min(name.len, n.name.len); @memcpy(n.name[0..l], name[0..l]); n.* = .{ .used = true, .name = n.name, .name_len = l, .size = 0 }; return i; } } return null; } fn openAt(id: u64) ?*OpenFile { if (id >= opens.len) return null; const o = &opens[@intCast(id)]; return if (o.used) o else null; } /// The mount whose prefix most specifically contains `name`, and the path /// relative to it. Only absolute paths route; bare names never match. const MountMatch = struct { backend: ipc.Handle, relative: []const u8 }; fn longestMount(name: []const u8) ?MountMatch { if (!path.isAbsolute(name)) return null; var best: ?MountMatch = null; var best_len: usize = 0; for (&mounts) |*m| { if (!m.used) continue; const prefix = m.prefix[0..m.prefix_len]; if (path.underMount(name, prefix)) |relative| { if (best == null or prefix.len >= best_len) { best_len = prefix.len; best = .{ .backend = m.backend, .relative = relative }; } } } return best; } /// Serialise a reply header + payload into `out`; returns the total length. fn writeReply(out: []u8, reply: protocol.Reply, payload: []const u8) usize { @memcpy(out[0..protocol.reply_size], std.mem.asBytes(&reply)); const n = @min(payload.len, out.len - protocol.reply_size); @memcpy(out[protocol.reply_size..][0..n], payload[0..n]); return protocol.reply_size + n; } fn fail(out: []u8) usize { return writeReply(out, .{ .status = -1 }, &.{}); } /// Format one whole log line and emit it in a single `debug_write`, so lines from /// concurrent processes can never land in the middle of it. fn writeLine(comptime fmt: []const u8, arguments: anytype) void { var line: [96]u8 = undefined; _ = runtime.system.write(std.fmt.bufPrint(&line, fmt, arguments) catch return); } // --- mount routing ---------------------------------------------------------- /// Forward an open under a mount to its backend and, on success, allocate a local /// forwarding handle that remembers the backend's node id. fn forwardOpen(out: []u8, backend: ipc.Handle, relative: []const u8, flags: u32, sender: u32) usize { const request = protocol.Request{ .operation = .open, .node = 0, .offset = 0, .len = @intCast(relative.len), .flags = flags }; var message: [protocol.message_maximum]u8 = undefined; @memcpy(message[0..protocol.request_size], std.mem.asBytes(&request)); const rel = relative[0..@min(relative.len, protocol.maximum_payload)]; @memcpy(message[protocol.request_size..][0..rel.len], rel); var reply: [protocol.message_maximum]u8 = undefined; const n = ipc.call(backend, message[0 .. protocol.request_size + rel.len], &reply) catch return fail(out); if (n < protocol.reply_size) return fail(out); const backend_reply = std.mem.bytesToValue(protocol.Reply, reply[0..protocol.reply_size]); if (backend_reply.status != 0) return writeReply(out, .{ .status = backend_reply.status }, &.{}); for (&opens, 0..) |*o, i| { if (!o.used) { o.* = .{ .used = true, .node = @intCast(backend_reply.node), .backend = backend, .owner = sender }; return writeReply(out, .{ .status = 0, .node = i }, &.{}); } } return fail(out); } /// Relay a read/write/status/readdir/close on a forwarding handle to the backend /// (the node already rewritten to the backend's id) and copy its reply out. fn forwardRequest(out: []u8, backend: ipc.Handle, request: protocol.Request, payload: []const u8) usize { var message: [protocol.message_maximum]u8 = undefined; @memcpy(message[0..protocol.request_size], std.mem.asBytes(&request)); const plen = @min(payload.len, protocol.maximum_payload); @memcpy(message[protocol.request_size..][0..plen], payload[0..plen]); var reply: [protocol.message_maximum]u8 = undefined; const n = ipc.call(backend, message[0 .. protocol.request_size + plen], &reply) catch return fail(out); const copy = @min(n, out.len); @memcpy(out[0..copy], reply[0..copy]); return copy; } /// Forward a path-based operation (mkdir, unlink) under a mount to its backend and /// relay the reply. No handle is created — these operate by path and return only a /// status. fn forwardPath(out: []u8, backend: ipc.Handle, operation: protocol.Operation, relative: []const u8) usize { const request = protocol.Request{ .operation = operation, .node = 0, .offset = 0, .len = @intCast(relative.len), .flags = 0 }; var message: [protocol.message_maximum]u8 = undefined; @memcpy(message[0..protocol.request_size], std.mem.asBytes(&request)); const rel = relative[0..@min(relative.len, protocol.maximum_payload)]; @memcpy(message[protocol.request_size..][0..rel.len], rel); var reply: [protocol.message_maximum]u8 = undefined; const n = ipc.call(backend, message[0 .. protocol.request_size + rel.len], &reply) catch return fail(out); const copy = @min(n, out.len); @memcpy(out[0..copy], reply[0..copy]); return copy; } /// Forward a rename to its backend: the payload is the mount-relative old path, a /// 0x00 separator, then the mount-relative new path. Relays the backend's reply. fn forwardRename(out: []u8, backend: ipc.Handle, old_relative: []const u8, new_relative: []const u8) usize { const total = old_relative.len + 1 + new_relative.len; var message: [protocol.message_maximum]u8 = undefined; if (protocol.request_size + total > message.len) return fail(out); const request = protocol.Request{ .operation = .rename, .node = 0, .offset = 0, .len = @intCast(total), .flags = 0 }; @memcpy(message[0..protocol.request_size], std.mem.asBytes(&request)); var p = protocol.request_size; @memcpy(message[p..][0..old_relative.len], old_relative); p += old_relative.len; message[p] = 0; p += 1; @memcpy(message[p..][0..new_relative.len], new_relative); p += new_relative.len; var reply: [protocol.message_maximum]u8 = undefined; const n = ipc.call(backend, message[0..p], &reply) catch return fail(out); const copy = @min(n, out.len); @memcpy(out[0..copy], reply[0..copy]); return copy; } /// Best-effort close of a backend node (used when a dead client's forwarding /// handles are swept — the backend must not leak the vfs's opens). fn forwardClose(backend: ipc.Handle, backend_node: u64) void { const request = protocol.Request{ .operation = .close, .node = backend_node, .offset = 0, .len = 0, .flags = 0 }; var reply: [protocol.message_maximum]u8 = undefined; _ = ipc.call(backend, std.mem.asBytes(&request), &reply) catch {}; } fn doMount(out: []u8, prefix: []const u8, backend: ipc.Handle) usize { for (&mounts) |*m| { if (m.used and std.mem.eql(u8, m.prefix[0..m.prefix_len], prefix)) { m.backend = backend; writeLine("/system/services/vfs: remounted {s}\n", .{prefix}); return writeReply(out, .{ .status = 0 }, &.{}); } } for (&mounts) |*m| { if (!m.used) { const l = @min(prefix.len, m.prefix.len); m.used = true; @memcpy(m.prefix[0..l], prefix[0..l]); m.prefix_len = l; m.backend = backend; writeLine("/system/services/vfs: mounted {s}\n", .{prefix[0..l]}); return writeReply(out, .{ .status = 0 }, &.{}); } } return fail(out); } fn doUnmount(out: []u8, prefix: []const u8) usize { for (&mounts) |*m| { if (m.used and std.mem.eql(u8, m.prefix[0..m.prefix_len], prefix)) { m.used = false; writeLine("/system/services/vfs: unmounted {s}\n", .{prefix}); return writeReply(out, .{ .status = 0 }, &.{}); } } return fail(out); } /// Release every open handle `client` held — called on that client's published /// exit event. Forwarding handles also tell their backend to release; local /// nodes (the ramfs files) stay, since ramfs contents outlive their writers. fn releaseClientHandles(client: u32) void { var released: u32 = 0; for (&opens) |*o| { if (o.used and o.owner == client) { if (o.backend) |backend| forwardClose(backend, o.node); o.used = false; released += 1; } } if (released != 0) writeLine("/system/services/vfs: released {d} handle(s) for dead client {d}\n", .{ released, client }); } /// Handle one request from `sender`; write the reply into `out`, return its length. fn handle(message: []const u8, out: []u8, sender: u32, capability: ?ipc.Handle) usize { if (message.len < protocol.request_size) return fail(out); const request = std.mem.bytesToValue(protocol.Request, message[0..protocol.request_size]); const payload = message[protocol.request_size..]; switch (request.operation) { .mount => { const prefix = payload[0..@min(payload.len, request.len)]; const backend = capability orelse return fail(out); return doMount(out, prefix, backend); }, .unmount => { const prefix = payload[0..@min(payload.len, request.len)]; return doUnmount(out, prefix); }, .open => { const name = payload[0..@min(payload.len, request.len)]; if (longestMount(name)) |m| return forwardOpen(out, m.backend, m.relative, request.flags, sender); // An absolute path with no matching mount is simply not found — only // bare names live in the flat ramfs. (Else /mnt/usb would be silently // created as a flat file when its filesystem is not yet mounted.) if (path.isAbsolute(name)) return fail(out); const ni = findNode(name) orelse createNode(name) orelse return fail(out); for (&opens, 0..) |*o, i| { if (!o.used) { o.* = .{ .used = true, .node = ni, .backend = null, .owner = sender }; return writeReply(out, .{ .status = 0, .node = i }, &.{}); } } return fail(out); }, .read => { const of = openAt(request.node) orelse return fail(out); if (of.backend) |backend| { var forwarded = request; forwarded.node = of.node; return forwardRequest(out, backend, forwarded, payload); } const nd = &nodes[@intCast(of.node)]; const off: usize = @intCast(request.offset); if (off >= nd.size) return writeReply(out, .{ .status = 0, .len = 0 }, &.{}); // EOF const n = @min(@min(nd.size - off, request.len), protocol.maximum_payload); return writeReply(out, .{ .status = 0, .len = @intCast(n) }, nd.data[off .. off + n]); }, .write => { const of = openAt(request.node) orelse return fail(out); if (of.backend) |backend| { var forwarded = request; forwarded.node = of.node; return forwardRequest(out, backend, forwarded, payload); } const nd = &nodes[@intCast(of.node)]; const off: usize = @intCast(request.offset); if (off > nd.data.len) return fail(out); const n = @min(@min(payload.len, request.len), nd.data.len - off); @memcpy(nd.data[off .. off + n], payload[0..n]); if (off + n > nd.size) nd.size = off + n; return writeReply(out, .{ .status = 0, .len = @intCast(n) }, &.{}); }, .status => { const of = openAt(request.node) orelse return fail(out); if (of.backend) |backend| { var forwarded = request; forwarded.node = of.node; return forwardRequest(out, backend, forwarded, payload); } const st = protocol.FileStatus{ .size = nodes[@intCast(of.node)].size, .kind = @intFromEnum(protocol.NodeKind.regular) }; return writeReply(out, .{ .status = 0, .len = @sizeOf(protocol.FileStatus) }, std.mem.asBytes(&st)); }, .readdir => { const of = openAt(request.node) orelse return fail(out); if (of.backend) |backend| { var forwarded = request; forwarded.node = of.node; return forwardRequest(out, backend, forwarded, payload); } // The flat ramfs has no directories: report EOF. return writeReply(out, .{ .status = 0, .len = 0 }, &.{}); }, .close => { const of = openAt(request.node); if (of) |o| { if (o.backend) |backend| forwardClose(backend, o.node); o.used = false; } return writeReply(out, .{ .status = 0 }, &.{}); }, .mkdir, .unlink => { const name = payload[0..@min(payload.len, request.len)]; if (longestMount(name)) |m| return forwardPath(out, m.backend, request.operation, m.relative); // Only a mounted backend has real directories; the flat ramfs cannot // create or remove them (and a bare-name path is not a mount target). return fail(out); }, .rename => { const both = payload[0..@min(payload.len, request.len)]; const sep = std.mem.indexOfScalar(u8, both, 0) orelse return fail(out); const old_path = both[0..sep]; const new_path = both[sep + 1 ..]; const mo = longestMount(old_path) orelse return fail(out); const mn = longestMount(new_path) orelse return fail(out); // Both paths must live under the same mount — cross-filesystem rename is // not supported. if (mo.backend != mn.backend) return fail(out); return forwardRename(out, mo.backend, mo.relative, mn.relative); }, } } /// Startup, under the harness: subscribe to the published exit events — when a /// client dies holding open handles, the exit notification is how the VFS learns /// to release them (docs/process-lifecycle.md). fn initialise(endpoint: ipc.Handle) bool { if (!runtime.process.subscribeExits(endpoint)) { _ = runtime.system.write("/system/services/vfs: exit subscription failed\n"); } _ = runtime.system.write("/system/services/vfs: ready\n"); return true; } /// A non-signal notification: the only kind the VFS subscribes to is exit events. fn onNotification(badge: u64) void { if (badge & ipc.notify_exit_bit != 0) { releaseClientHandles(@intCast(badge & ~(ipc.notify_badge_bit | ipc.notify_exit_bit))); } } pub fn main() void { // The harness owns the loop: requests dispatch to handle(), exit events to // onNotification(), ping and terminate are answered for free — this service // gained the whole lifecycle contract by deleting its hand-rolled loop. runtime.service.run(protocol.message_maximum, .{ .service = .vfs, .init = initialise, .on_message = handle, .on_notification = onNotification, }); }