vfs: the root moves into the kernel — resolve + redirect cutover

runtime.fs now routes every path through fs_resolve: kernel-served
/system nodes are read via fs_node (tokens, no open state); everything
under a userspace mount goes straight to the owning backend's endpoint
with the kernel-rewritten mount-relative path — one syscall of naming,
then the unchanged vfs-protocol rendezvous, public API untouched. mkdir/
unlink/rename resolve-then-forward (rename checks both paths land on
the SAME backend); mount is the fs_mount syscall.

The fat server mounts twice — /mnt/usb from the volume root and /var
from its /var subtree — so the logger now writes the FHS path
/var/log/<boot-stamp>/... and swapping the persistent medium later
touches only fat's two mount calls. With clients holding fat's node ids
directly, fat records each handle's owner, checks it, and sweeps a dead
client's handles via the published exit events (the old router's
pattern, now where the state actually lives).

The userspace vfs server and its router die; ServiceId.vfs=1 stays
reserved-retired; protocol.zig moves to system/vfs-protocol.zig (the
wire contract is backend-only now). vfs-test becomes the ring-3 proof
of the kernel VFS (own-binary ELF magic through /system, read-only
refusals, listing); vfs-client-death becomes the fat sweep test over
the full storage chain, with a ring-scanning check (the last-write
buffer is too racy under a chattering tree).
This commit is contained in:
Daniel Samson
2026-07-21 16:27:06 +01:00
parent 7c5645fe48
commit e186858315
15 changed files with 379 additions and 590 deletions
+39 -15
View File
@@ -101,18 +101,42 @@ fn initialise(endpoint: runtime.ipc.Handle) bool {
};
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 VFS namespace at /mnt/usb (retry while the VFS
// comes up). From here the VFS routes /mnt/usb/... to this server.
var tries: u32 = 0;
while (tries < 100) : (tries += 1) {
if (runtime.fs.mount(mount_point, endpoint)) {
std.log.info("mounted {s}", .{mount_point});
return true;
}
runtime.system.sleep(50);
// 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).
_ = runtime.process.subscribeExits(endpoint);
// Mount ourselves into the kernel VFS at /mnt/usb — and serve /var from the
// volume's /var subtree, so FHS paths (the logger's /var/log) stay decoupled
// from which volume carries them. A mount is one syscall now; no retry
// needed (the kernel's table exists before any service).
if (runtime.fs.mount(mount_point, endpoint)) {
std.log.info("mounted {s}", .{mount_point});
} else {
_ = runtime.system.write("/system/services/fat: could not mount /mnt/usb\n");
}
_ = runtime.system.write("/system/services/fat: could not mount into the VFS\n");
return true; // still serve directly, even if the namespace mount didn't take
if (runtime.fs.mountRewritten("/var", endpoint, "/var")) {
std.log.info("mounted /var", .{});
} else {
_ = runtime.system.write("/system/services/fat: could not mount /var\n");
}
return true;
}
/// 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 = runtime.ipc.Received{ .len = 0, .badge = badge, .cap = null };
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 };
@@ -127,7 +151,7 @@ fn splitParent(path: []const u8) ParentLeaf {
};
}
fn handleOpen(out: []u8, path: []const u8, flags: u32) usize {
fn handleOpen(out: []u8, path: []const u8, flags: u32, sender: u32) usize {
var node = filesystem.resolve(path);
if (node == null and flags & protocol.create != 0) {
const split = splitParent(path);
@@ -141,13 +165,12 @@ fn handleOpen(out: []u8, path: []const u8, flags: u32) usize {
filesystem.truncate(&resolved);
}
const index = allocOpen() orelse return fail(out);
open_nodes[index] = .{ .used = true, .node = resolved };
open_nodes[index] = .{ .used = true, .node = resolved, .owner = sender };
return writeReply(out, .{ .status = 0, .node = index }, &.{});
}
fn onMessage(message: []const u8, out: []u8, sender: u32, capability: ?runtime.ipc.Handle) usize {
_ = capability;
_ = sender;
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..];
@@ -157,7 +180,7 @@ fn onMessage(message: []const u8, out: []u8, sender: u32, capability: ?runtime.i
filesystem.current_time_epoch = runtime.system.wallClock();
switch (request.operation) {
.open => return handleOpen(out, payload[0..@min(payload.len, request.len)], request.flags),
.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: [protocol.maximum_payload]u8 = undefined;
@@ -237,5 +260,6 @@ pub fn main() void {
.service = .fat,
.init = initialise,
.on_message = onMessage,
.on_notification = onNotification,
});
}