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
+3 -6
View File
@@ -364,7 +364,7 @@ pub fn build(b: *std.Build) void {
// is the first "protocol module" (see docs/driver-model.md); usb/block will // is the first "protocol module" (see docs/driver-model.md); usb/block will
// expose theirs the same way. // expose theirs the same way.
const vfs_protocol_module = b.addModule("vfs-protocol", .{ const vfs_protocol_module = b.addModule("vfs-protocol", .{
.root_source_file = b.path("system/services/vfs/protocol.zig"), .root_source_file = b.path("system/vfs-protocol.zig"),
}); });
// The input wire protocol: the input service's public interface, exposed as its own // The input wire protocol: the input service's public interface, exposed as its own
@@ -511,8 +511,7 @@ pub fn build(b: *std.Build) void {
// Each is built by the same user-binary recipe and laid out at its FHS path on // Each is built by the same user-binary recipe and laid out at its FHS path on
// the boot volume (see `bundled` below). The EFI loader walks the tree at boot // the boot volume (see `bundled` below). The EFI loader walks the tree at boot
// and hands the kernel an in-RAM initial_ramdisk of it (system/initial-ramdisk.zig). // and hands the kernel an in-RAM initial_ramdisk of it (system/initial-ramdisk.zig).
const vfs_exe = addUserBinary(b, kernel_target, runtime_module, mmio_module, xkeyboard_config_module, acpi_ids_module, "vfs", "system/services/vfs/vfs.zig"); const vfstest_exe = addUserBinary(b, kernel_target, runtime_module, mmio_module, xkeyboard_config_module, acpi_ids_module, "vfs-test", "system/services/vfs-test/vfs-test.zig");
const vfstest_exe = addUserBinary(b, kernel_target, runtime_module, mmio_module, xkeyboard_config_module, acpi_ids_module, "vfs-test", "system/services/vfs/vfs-test.zig");
const ps2_bus_exe = addUserBinary(b, kernel_target, runtime_module, mmio_module, xkeyboard_config_module, acpi_ids_module, "ps2-bus", "system/drivers/ps2-bus/ps2-bus.zig"); const ps2_bus_exe = addUserBinary(b, kernel_target, runtime_module, mmio_module, xkeyboard_config_module, acpi_ids_module, "ps2-bus", "system/drivers/ps2-bus/ps2-bus.zig");
const ps2_keyboard_exe = addUserBinary(b, kernel_target, runtime_module, mmio_module, xkeyboard_config_module, acpi_ids_module, "ps2-keyboard", "system/drivers/ps2-bus/keyboard.zig"); const ps2_keyboard_exe = addUserBinary(b, kernel_target, runtime_module, mmio_module, xkeyboard_config_module, acpi_ids_module, "ps2-keyboard", "system/drivers/ps2-bus/keyboard.zig");
const ps2_mouse_exe = addUserBinary(b, kernel_target, runtime_module, mmio_module, xkeyboard_config_module, acpi_ids_module, "ps2-mouse", "system/drivers/ps2-bus/mouse.zig"); const ps2_mouse_exe = addUserBinary(b, kernel_target, runtime_module, mmio_module, xkeyboard_config_module, acpi_ids_module, "ps2-mouse", "system/drivers/ps2-bus/mouse.zig");
@@ -594,7 +593,6 @@ pub fn build(b: *std.Build) void {
// are these paths with a leading slash. // are these paths with a leading slash.
const bundled = [_]BundledBinary{ const bundled = [_]BundledBinary{
.{ .path = "system/services/init", .binary = init_exe.getEmittedBin() }, .{ .path = "system/services/init", .binary = init_exe.getEmittedBin() },
.{ .path = "system/services/vfs", .binary = vfs_exe.getEmittedBin() },
.{ .path = "system/services/fat", .binary = fat_exe.getEmittedBin() }, .{ .path = "system/services/fat", .binary = fat_exe.getEmittedBin() },
.{ .path = "system/services/display", .binary = display_exe.getEmittedBin() }, .{ .path = "system/services/display", .binary = display_exe.getEmittedBin() },
.{ .path = "system/services/display-demo", .binary = display_demo_exe.getEmittedBin() }, .{ .path = "system/services/display-demo", .binary = display_demo_exe.getEmittedBin() },
@@ -888,8 +886,7 @@ pub fn build(b: *std.Build) void {
"system/drivers/usb-hid/hid-report.zig", // HID boot-report keyboard/mouse decode "system/drivers/usb-hid/hid-report.zig", // HID boot-report keyboard/mouse decode
"system/drivers/usb-storage/bulk-only-transport.zig", // CBW/CSW wrapper sizes "system/drivers/usb-storage/bulk-only-transport.zig", // CBW/CSW wrapper sizes
"system/drivers/usb-storage/scsi.zig", // SCSI CDB encodings (big-endian) "system/drivers/usb-storage/scsi.zig", // SCSI CDB encodings (big-endian)
"system/services/vfs/path.zig", // mount-prefix path matching "system/vfs-protocol.zig", // NodeKind / DirectoryEntry sizes + op values
"system/services/vfs/protocol.zig", // NodeKind / DirectoryEntry sizes + op values
"system/services/fat/on-disk.zig", // FAT on-disk struct sizes + type detection "system/services/fat/on-disk.zig", // FAT on-disk struct sizes + type detection
"system/services/fat/engine.zig", // FAT read/write over a RAM-backed image "system/services/fat/engine.zig", // FAT read/write over a RAM-backed image
"system/services/display/compositor.zig", // Rect math + fill/composite/blit-tile "system/services/display/compositor.zig", // Rect math + fill/composite/blit-tile
+109 -48
View File
@@ -1,5 +1,5 @@
//! runtime.fs — the danos-native file API. A program opens, reads, writes, and //! runtime.fs — the danos-native file API. A program opens, reads, writes, and
//! lists files served by the user-space VFS (system/services/vfs), each call //! lists files through the kernel VFS root (resolve + redirect), each call
//! marshalling a vfs-protocol request over IPC. This is the danos-native layer //! marshalling a vfs-protocol request over IPC. This is the danos-native layer
//! danos programs use directly; it is also where the file operations that later //! danos programs use directly; it is also where the file operations that later
//! become `std.os.danos` are staged (see docs/zig-self-hosting.md). It replaces //! become `std.os.danos` are staged (see docs/zig-self-hosting.md). It replaces
@@ -12,6 +12,7 @@
const std = @import("std"); const std = @import("std");
const ipc = @import("ipc.zig"); const ipc = @import("ipc.zig");
const system = @import("system.zig");
const protocol = @import("vfs-protocol"); const protocol = @import("vfs-protocol");
/// The kind of a filesystem node — re-exported so a caller need not import the /// The kind of a filesystem node — re-exported so a caller need not import the
@@ -60,23 +61,37 @@ pub const OpenOptions = struct {
} }
}; };
// The VFS server endpoint, looked up once by well-known id and cached. // The route to a path: the kernel resolves (fs_resolve) and either serves the
var vfs_handle: ipc.Handle = 0; // node itself (the initrd at /system — a permanent token) or redirects us to
var vfs_resolved = false; // the owning filesystem backend's endpoint, to which we speak the vfs-protocol
fn vfs() ?ipc.Handle { // rendezvous directly with the rewritten mount-relative path.
if (!vfs_resolved) { const Route = union(enum) {
vfs_handle = ipc.lookup(.vfs) orelse return null; kernel: u64,
vfs_resolved = true; backend: struct { handle: ipc.Handle, path: [224]u8, path_len: usize },
fn backendPath(self: *const Route) []const u8 {
return self.backend.path[0..self.backend.path_len];
}
};
fn resolve(path: []const u8, flags: usize) ?Route {
var out: [224]u8 = undefined;
const route = system.fsResolve(path, flags, &out) orelse return null;
switch (route) {
.kernel => |token| return .{ .kernel = token },
.backend => |b| {
var r: Route = .{ .backend = .{ .handle = b.handle, .path = undefined, .path_len = b.path_len } };
@memcpy(r.backend.path[0..b.path_len], out[0..b.path_len]);
return r;
},
} }
return vfs_handle;
} }
const Result = struct { reply: protocol.Reply, payload: []u8 }; const Result = struct { reply: protocol.Reply, payload: []u8 };
// One request/reply round trip: [Request header][send payload] -> VFS -> // One request/reply round trip: [Request header][send payload] -> backend ->
// [Reply header][receive payload]. The receive payload lands in `out`. // [Reply header][receive payload]. The receive payload lands in `out`.
fn transact(request: protocol.Request, send: []const u8, out: []u8) ?Result { fn transact(h: ipc.Handle, request: protocol.Request, send: []const u8, out: []u8) ?Result {
const h = vfs() orelse return null;
var message: [protocol.message_maximum]u8 = undefined; var message: [protocol.message_maximum]u8 = undefined;
@memcpy(message[0..protocol.request_size], std.mem.asBytes(&request)); @memcpy(message[0..protocol.request_size], std.mem.asBytes(&request));
const slen = @min(send.len, protocol.maximum_payload); const slen = @min(send.len, protocol.maximum_payload);
@@ -95,13 +110,21 @@ fn transact(request: protocol.Request, send: []const u8, out: []u8) ?Result {
pub const File = struct { pub const File = struct {
node: u64, node: u64,
offset: u64 = 0, offset: u64 = 0,
/// The owning backend's endpoint, or null for a kernel-served node (the
/// read-only /system tree), whose `node` is a permanent fs_node token.
backend: ?ipc.Handle = null,
/// Read up to `buffer.len` bytes at the current offset; returns the count, or /// Read up to `buffer.len` bytes at the current offset; returns the count, or
/// null on error. /// null on error.
pub fn read(self: *File, buffer: []u8) ?usize { pub fn read(self: *File, buffer: []u8) ?usize {
const h = self.backend orelse {
const n = system.fsNodeRead(self.node, self.offset, buffer) orelse return null;
self.offset += n;
return n;
};
const want: u32 = @intCast(@min(buffer.len, protocol.maximum_payload)); const want: u32 = @intCast(@min(buffer.len, protocol.maximum_payload));
const request = protocol.Request{ .operation = .read, .node = self.node, .offset = self.offset, .len = want, .flags = 0 }; const request = protocol.Request{ .operation = .read, .node = self.node, .offset = self.offset, .len = want, .flags = 0 };
const r = transact(request, &.{}, buffer) orelse return null; const r = transact(h, request, &.{}, buffer) orelse return null;
if (r.reply.status != 0) return null; if (r.reply.status != 0) return null;
self.offset += r.reply.len; self.offset += r.reply.len;
return r.reply.len; return r.reply.len;
@@ -109,11 +132,13 @@ pub const File = struct {
/// Write `data` at the current offset; returns the count written. A single /// Write `data` at the current offset; returns the count written. A single
/// call is capped at the VFS payload size, so the return may be short — use /// call is capped at the VFS payload size, so the return may be short — use
/// `writeAll` to write the whole slice. Null on error. /// `writeAll` to write the whole slice. Null on error (kernel-served nodes
/// are read-only).
pub fn write(self: *File, data: []const u8) ?usize { pub fn write(self: *File, data: []const u8) ?usize {
const h = self.backend orelse return null;
const want: u32 = @intCast(@min(data.len, protocol.maximum_payload)); const want: u32 = @intCast(@min(data.len, protocol.maximum_payload));
const request = protocol.Request{ .operation = .write, .node = self.node, .offset = self.offset, .len = want, .flags = 0 }; const request = protocol.Request{ .operation = .write, .node = self.node, .offset = self.offset, .len = want, .flags = 0 };
const r = transact(request, data[0..want], &.{}) orelse return null; const r = transact(h, request, data[0..want], &.{}) orelse return null;
if (r.reply.status != 0) return null; if (r.reply.status != 0) return null;
self.offset += r.reply.len; self.offset += r.reply.len;
return r.reply.len; return r.reply.len;
@@ -138,27 +163,40 @@ pub const File = struct {
/// This file's metadata. /// This file's metadata.
pub fn attributes(self: *File) ?Attributes { pub fn attributes(self: *File) ?Attributes {
const h = self.backend orelse {
const a = system.fsNodeStatus(self.node) orelse return null;
return .{ .size = a.size, .kind = if (a.kind == system.file_kind_directory) .directory else .regular, .mtime = a.mtime };
};
const request = protocol.Request{ .operation = .status, .node = self.node, .offset = 0, .len = 0, .flags = 0 }; const request = protocol.Request{ .operation = .status, .node = self.node, .offset = 0, .len = 0, .flags = 0 };
var buffer: [@sizeOf(protocol.FileStatus)]u8 = undefined; var buffer: [@sizeOf(protocol.FileStatus)]u8 = undefined;
const r = transact(request, &.{}, &buffer) orelse return null; const r = transact(h, request, &.{}, &buffer) orelse return null;
if (r.reply.status != 0 or r.payload.len < @sizeOf(protocol.FileStatus)) return null; if (r.reply.status != 0 or r.payload.len < @sizeOf(protocol.FileStatus)) return null;
const status = std.mem.bytesToValue(protocol.FileStatus, buffer[0..@sizeOf(protocol.FileStatus)]); const status = std.mem.bytesToValue(protocol.FileStatus, buffer[0..@sizeOf(protocol.FileStatus)]);
return .{ .size = status.size, .kind = kindFromWire(status.kind), .mtime = status.mtime }; return .{ .size = status.size, .kind = kindFromWire(status.kind), .mtime = status.mtime };
} }
/// Release the VFS's open handle for this file. /// Release the backend's open handle for this file. Kernel-served node
/// tokens are permanent — nothing to release.
pub fn close(self: *File) void { pub fn close(self: *File) void {
const h = self.backend orelse return;
const request = protocol.Request{ .operation = .close, .node = self.node, .offset = 0, .len = 0, .flags = 0 }; const request = protocol.Request{ .operation = .close, .node = self.node, .offset = 0, .len = 0, .flags = 0 };
_ = transact(request, &.{}, &.{}); _ = transact(h, request, &.{}, &.{});
} }
}; };
/// Open (or create, with `.create`) `path`. Returns the open file, or null. /// Open (or create, with `.create`) `path`. Returns the open file, or null.
pub fn open(path: []const u8, options: OpenOptions) ?File { pub fn open(path: []const u8, options: OpenOptions) ?File {
const request = protocol.Request{ .operation = .open, .node = 0, .offset = 0, .len = @intCast(path.len), .flags = options.wireFlags() }; const route = resolve(path, options.wireFlags()) orelse return null;
const r = transact(request, path, &.{}) orelse return null; switch (route) {
.kernel => |token| return .{ .node = token, .backend = null },
.backend => |b| {
const relative = route.backendPath();
const request = protocol.Request{ .operation = .open, .node = 0, .offset = 0, .len = @intCast(relative.len), .flags = options.wireFlags() };
const r = transact(b.handle, request, relative, &.{}) orelse return null;
if (r.reply.status != 0) return null; if (r.reply.status != 0) return null;
return .{ .node = r.reply.node }; return .{ .node = r.reply.node, .backend = b.handle };
},
}
} }
/// A path's metadata without keeping it open (open -> status -> close). /// A path's metadata without keeping it open (open -> status -> close).
@@ -190,13 +228,27 @@ pub const Entry = struct {
pub const Directory = struct { pub const Directory = struct {
node: u64, node: u64,
cursor: u64 = 0, cursor: u64 = 0,
backend: ?ipc.Handle = null,
/// Fill `entry` with the next directory entry; false at end of directory or /// Fill `entry` with the next directory entry; false at end of directory or
/// on error. /// on error.
pub fn next(self: *Directory, entry: *Entry) bool { pub fn next(self: *Directory, entry: *Entry) bool {
const h = self.backend orelse {
var buffer: [@sizeOf(system.DirectoryEntryHeader) + 64]u8 = undefined;
const n = system.fsNodeReaddir(self.node, self.cursor, &buffer) orelse return false;
if (n < @sizeOf(system.DirectoryEntryHeader)) return false; // end
const header = std.mem.bytesToValue(system.DirectoryEntryHeader, buffer[0..@sizeOf(system.DirectoryEntryHeader)]);
entry.kind = if (header.kind == system.file_kind_directory) .directory else .regular;
entry.size = header.size;
const nlen = @min(@as(usize, header.name_len), entry.name_buffer.len);
@memcpy(entry.name_buffer[0..nlen], buffer[@sizeOf(system.DirectoryEntryHeader)..][0..nlen]);
entry.name_len = nlen;
self.cursor += 1;
return true;
};
const request = protocol.Request{ .operation = .readdir, .node = self.node, .offset = self.cursor, .len = 0, .flags = 0 }; const request = protocol.Request{ .operation = .readdir, .node = self.node, .offset = self.cursor, .len = 0, .flags = 0 };
var buffer: [protocol.message_maximum]u8 = undefined; var buffer: [protocol.message_maximum]u8 = undefined;
const r = transact(request, &.{}, &buffer) orelse return false; const r = transact(h, request, &.{}, &buffer) orelse return false;
if (r.reply.status != 0 or r.reply.len == 0) return false; // error or EOF if (r.reply.status != 0 or r.reply.len == 0) return false; // error or EOF
if (r.payload.len < protocol.directory_entry_size) return false; if (r.payload.len < protocol.directory_entry_size) return false;
const header = std.mem.bytesToValue(protocol.DirectoryEntry, r.payload[0..protocol.directory_entry_size]); const header = std.mem.bytesToValue(protocol.DirectoryEntry, r.payload[0..protocol.directory_entry_size]);
@@ -210,9 +262,9 @@ pub const Directory = struct {
return true; return true;
} }
/// Release the VFS's open handle for this directory. /// Release the backend's open handle for this directory.
pub fn close(self: *Directory) void { pub fn close(self: *Directory) void {
var f = File{ .node = self.node }; var f = File{ .node = self.node, .backend = self.backend };
f.close(); f.close();
} }
}; };
@@ -220,13 +272,18 @@ pub const Directory = struct {
/// Open `path` as a directory for listing. Returns null if it isn't one / on error. /// Open `path` as a directory for listing. Returns null if it isn't one / on error.
pub fn openDirectory(path: []const u8) ?Directory { pub fn openDirectory(path: []const u8) ?Directory {
const file = open(path, .{ .directory = true }) orelse return null; const file = open(path, .{ .directory = true }) orelse return null;
return .{ .node = file.node }; return .{ .node = file.node, .backend = file.backend };
} }
// A path-based request that returns only a status (mkdir, unlink). // A path-based request that returns only a status (mkdir, unlink). Kernel-served
// paths (the read-only /system) refuse mutation by construction: the resolve
// must land on a backend.
fn pathOperation(operation: protocol.Operation, path: []const u8) bool { fn pathOperation(operation: protocol.Operation, path: []const u8) bool {
const request = protocol.Request{ .operation = operation, .node = 0, .offset = 0, .len = @intCast(path.len), .flags = 0 }; const route = resolve(path, 0) orelse return false;
const r = transact(request, path, &.{}) orelse return false; if (route != .backend) return false;
const relative = route.backendPath();
const request = protocol.Request{ .operation = operation, .node = 0, .offset = 0, .len = @intCast(relative.len), .flags = 0 };
const r = transact(route.backend.handle, request, relative, &.{}) orelse return false;
return r.reply.status == 0; return r.reply.status == 0;
} }
@@ -261,33 +318,37 @@ pub fn remove(path: []const u8) bool {
return pathOperation(.unlink, path); return pathOperation(.unlink, path);
} }
/// Rename `old_path` to `new_path`. Both must be in the same directory (same- /// Rename `old_path` to `new_path`. Both must resolve to the SAME filesystem
/// directory, 8.3-name rename only for now). Returns true on success. /// backend (same-directory, 8.3-name rename only for now). Returns true on
/// success.
pub fn rename(old_path: []const u8, new_path: []const u8) bool { pub fn rename(old_path: []const u8, new_path: []const u8) bool {
const total = old_path.len + 1 + new_path.len; const old_route = resolve(old_path, 0) orelse return false;
const new_route = resolve(new_path, 0) orelse return false;
if (old_route != .backend or new_route != .backend) return false;
if (old_route.backend.handle != new_route.backend.handle) return false; // cross-filesystem
const old_relative = old_route.backendPath();
const new_relative = new_route.backendPath();
const total = old_relative.len + 1 + new_relative.len;
if (total > protocol.maximum_payload) return false; if (total > protocol.maximum_payload) return false;
var payload: [protocol.maximum_payload]u8 = undefined; var payload: [protocol.maximum_payload]u8 = undefined;
@memcpy(payload[0..old_path.len], old_path); @memcpy(payload[0..old_relative.len], old_relative);
payload[old_path.len] = 0; payload[old_relative.len] = 0;
@memcpy(payload[old_path.len + 1 ..][0..new_path.len], new_path); @memcpy(payload[old_relative.len + 1 ..][0..new_relative.len], new_relative);
const request = protocol.Request{ .operation = .rename, .node = 0, .offset = 0, .len = @intCast(total), .flags = 0 }; const request = protocol.Request{ .operation = .rename, .node = 0, .offset = 0, .len = @intCast(total), .flags = 0 };
const r = transact(request, payload[0..total], &.{}) orelse return false; const r = transact(old_route.backend.handle, request, payload[0..total], &.{}) orelse return false;
return r.reply.status == 0; return r.reply.status == 0;
} }
/// Mount a filesystem backend (its server endpoint) at absolute path `target`; /// Mount a filesystem backend (its server endpoint) at absolute path `target`;
/// the VFS then routes everything under `target` to that backend. This is the one /// the kernel VFS then routes everything under `target` to that backend.
/// call that hands the VFS a capability (the backend endpoint). Returns true on /// Possession of the endpoint handle is the capability. Returns true on success.
/// success.
pub fn mount(target: []const u8, backend: ipc.Handle) bool { pub fn mount(target: []const u8, backend: ipc.Handle) bool {
const h = vfs() orelse return false; return system.fsMount(target, backend, "");
const request = protocol.Request{ .operation = .mount, .node = 0, .offset = 0, .len = @intCast(target.len), .flags = 0 }; }
var message: [protocol.message_maximum]u8 = undefined;
@memcpy(message[0..protocol.request_size], std.mem.asBytes(&request)); /// As `mount`, with a backend-side rewrite prefix: a path under `target` reaches
const tlen = @min(target.len, protocol.maximum_payload); /// the backend as `rewrite` + the mount-relative tail. How one volume serves two
@memcpy(message[protocol.request_size..][0..tlen], target[0..tlen]); /// mounts ("/mnt/usb" from its root, "/var" from its /var subtree).
var rbuf: [protocol.message_maximum]u8 = undefined; pub fn mountRewritten(target: []const u8, backend: ipc.Handle, rewrite: []const u8) bool {
const result = ipc.callCap(h, message[0 .. protocol.request_size + tlen], &rbuf, backend) catch return false; return system.fsMount(target, backend, rewrite);
if (result.len < protocol.reply_size) return false;
return std.mem.bytesToValue(protocol.Reply, rbuf[0..protocol.reply_size]).status == 0;
} }
+70
View File
@@ -32,6 +32,10 @@ pub const klog_record_magic = abi.klog_record_magic;
pub const klog_flag_truncated = abi.klog_flag_truncated; pub const klog_flag_truncated = abi.klog_flag_truncated;
pub const klog_maximum_message = abi.klog_maximum_message; pub const klog_maximum_message = abi.klog_maximum_message;
pub const maximum_process_name = abi.maximum_process_name; pub const maximum_process_name = abi.maximum_process_name;
pub const FileAttributes = abi.FileAttributes;
pub const DirectoryEntryHeader = abi.DirectoryEntryHeader;
pub const file_kind_regular = abi.file_kind_regular;
pub const file_kind_directory = abi.file_kind_directory;
/// Write raw bytes to the kernel log (bring-up/panic diagnostics; ordinary /// Write raw bytes to the kernel log (bring-up/panic diagnostics; ordinary
/// output goes through std.log -> writeRecord). The kernel stamps the record /// output goes through std.log -> writeRecord). The kernel stamps the record
@@ -100,6 +104,72 @@ pub fn klogStatus() ?KlogStatus {
return status; return status;
} }
/// Where fs_resolve routed a path: served by the kernel (a permanent node
/// token for fs_node) or by a userspace filesystem backend (an endpoint handle
/// plus the rewritten mount-relative path, returned in the caller's buffer).
pub const FsRoute = union(enum) {
kernel: u64,
backend: struct { handle: usize, path_len: usize },
};
/// Route `path` through the kernel VFS. For a backend route the rewritten
/// mount-relative path lands in `out` (behind a kernel-written length prefix,
/// already stripped here: out[0..path_len] is the path).
pub fn fsResolve(path: []const u8, flags: usize, out: []u8) ?FsRoute {
var rax: usize = undefined;
var rdx: usize = flags; // in: flags (arg #3); out: node token / backend handle
asm volatile ("syscall"
: [rax] "={rax}" (rax),
[rdx] "+{rdx}" (rdx),
: [n] "{rax}" (@intFromEnum(abi.SystemCall.fs_resolve)),
[a0] "{rdi}" (@intFromPtr(path.ptr)),
[a1] "{rsi}" (path.len),
[a3] "{r10}" (@intFromPtr(out.ptr)),
[a4] "{r8}" (out.len),
: .{ .rcx = true, .r11 = true, .memory = true });
if (@as(isize, @bitCast(rax)) < 0) return null;
if (rax == abi.fs_route_kernel) return .{ .kernel = rdx };
if (rax != abi.fs_route_backend) return null;
const path_len = @as(usize, out[0]) | (@as(usize, out[1]) << 8);
if (path_len + 2 > out.len) return null;
std.mem.copyForwards(u8, out[0..path_len], out[2..][0..path_len]);
return .{ .backend = .{ .handle = rdx, .path_len = path_len } };
}
/// Read `out.len` bytes of a kernel-served node at `offset` (fs_node read).
pub fn fsNodeRead(node_token: u64, offset: u64, out: []u8) ?usize {
const r = sc.systemCall5(.fs_node, abi.fs_node_read, node_token, offset, @intFromPtr(out.ptr), out.len);
if (@as(isize, @bitCast(r)) < 0) return null;
return r;
}
/// A kernel-served node's metadata (fs_node status).
pub fn fsNodeStatus(node_token: u64) ?abi.FileAttributes {
var attributes: abi.FileAttributes = undefined;
const r = sc.systemCall5(.fs_node, abi.fs_node_status, node_token, 0, @intFromPtr(&attributes), @sizeOf(abi.FileAttributes));
if (@as(isize, @bitCast(r)) < 0) return null;
return attributes;
}
/// The `cursor`th child of a kernel-served directory (fs_node readdir): fills
/// `out` with [DirectoryEntryHeader][name]; returns total bytes (0 = end).
pub fn fsNodeReaddir(node_token: u64, cursor: u64, out: []u8) ?usize {
const r = sc.systemCall5(.fs_node, abi.fs_node_readdir, node_token, cursor, @intFromPtr(out.ptr), out.len);
if (@as(isize, @bitCast(r)) < 0) return null;
return r;
}
/// Mount a userspace filesystem's endpoint at `prefix`, with an optional
/// backend-side `rewrite` prefix ("" = none). Possession of the endpoint
/// handle is the capability.
pub fn fsMount(prefix: []const u8, backend: usize, rewrite: []const u8) bool {
return sc.systemCall5(.fs_mount, @intFromPtr(prefix.ptr), prefix.len, backend, @intFromPtr(rewrite.ptr), rewrite.len) == 0;
}
pub fn fsUnmount(prefix: []const u8) bool {
return sc.systemCall2(.fs_unmount, @intFromPtr(prefix.ptr), prefix.len) == 0;
}
/// End the process. Never returns. /// End the process. Never returns.
pub fn exit(code: usize) noreturn { pub fn exit(code: usize) noreturn {
_ = sc.systemCall1(.exit, code); _ = sc.systemCall1(.exit, code);
+1 -1
View File
@@ -282,7 +282,7 @@ pub const KlogStatus = extern struct {
/// ipc_register/ipc_lookup). Small integers, so no string interning is needed /// ipc_register/ipc_lookup). Small integers, so no string interning is needed
/// during bring-up. The VFS server registers under `vfs`; clients look it up. /// during bring-up. The VFS server registers under `vfs`; clients look it up.
pub const ServiceId = enum(u32) { pub const ServiceId = enum(u32) {
vfs = 1, vfs = 1, // RETIRED: the router moved into the kernel (fs_resolve); the slot stays reserved
input = 2, input = 2,
ps2_bus = 3, // the 8042 owner; child device drivers attach here for raw bytes ps2_bus = 3, // the 8042 owner; child device drivers attach here for raw bytes
device_manager = 4, // the tree, the matcher, the supervisor (docs/device-manager.md) device_manager = 4, // the tree, the matcher, the supervisor (docs/device-manager.md)
+8 -5
View File
@@ -1309,8 +1309,7 @@ fn systemKlogStatus(state: *architecture.CpuState) void {
/// through the kernel mount table (docs/vfs-protocol.md). Kernel-served -> /// through the kernel mount table (docs/vfs-protocol.md). Kernel-served ->
/// rax=fs_route_kernel, rdx=node token. Backend-served -> rax=fs_route_backend, /// rax=fs_route_kernel, rdx=node token. Backend-served -> rax=fs_route_backend,
/// rdx=an endpoint handle in the caller's table (deduplicated), and the /// rdx=an endpoint handle in the caller's table (deduplicated), and the
/// rewritten mount-relative path copied to `out` with its length in the third /// rewritten mount-relative path copied into `out` behind a u16 length prefix. Fails for unknown paths, create-intent on /system, or an
/// result register. Fails for unknown paths, create-intent on /system, or an
/// undersized out buffer. /// undersized out buffer.
fn systemFsResolve(state: *architecture.CpuState) void { fn systemFsResolve(state: *architecture.CpuState) void {
const path_ptr = architecture.systemCallArg(state, 0); const path_ptr = architecture.systemCallArg(state, 0);
@@ -1331,14 +1330,18 @@ fn systemFsResolve(state: *architecture.CpuState) void {
architecture.setSystemCallResult2(state, node_token); architecture.setSystemCallResult2(state, node_token);
}, },
.backend => |*backend| { .backend => |*backend| {
if (backend.path_len > out_cap) return fail(state); // The rewritten path goes back in the out buffer behind a u16
// length prefix (a third result register would collide with r8's
// argument role in the userspace stub).
if (backend.path_len + 2 > out_cap) return fail(state);
const handle = ipc.installHandleDeduped(t, backend.endpoint); const handle = ipc.installHandleDeduped(t, backend.endpoint);
if (handle < 0) return fail(state); if (handle < 0) return fail(state);
const destination: [*]u8 = @ptrFromInt(out_ptr); const destination: [*]u8 = @ptrFromInt(out_ptr);
@memcpy(destination[0..backend.path_len], backend.path[0..backend.path_len]); destination[0] = @intCast(backend.path_len & 0xFF);
destination[1] = @intCast(backend.path_len >> 8);
@memcpy(destination[2..][0..backend.path_len], backend.path[0..backend.path_len]);
architecture.setSystemCallResult(state, abi.fs_route_backend); architecture.setSystemCallResult(state, abi.fs_route_backend);
architecture.setSystemCallResult2(state, @intCast(handle)); architecture.setSystemCallResult2(state, @intCast(handle));
architecture.setSystemCallResult3(state, backend.path_len);
}, },
.not_found => fail(state), .not_found => fail(state),
} }
+49 -16
View File
@@ -265,6 +265,31 @@ fn bufferHas(needle: []const u8) bool {
return std.mem.indexOf(u8, process.write_buffer[0..process.write_len], needle) != null; return std.mem.indexOf(u8, process.write_buffer[0..process.write_len], needle) != null;
} }
/// Substring search across the whole retained log ring (record payloads are
/// contiguous in the stream, so a one-line needle always matches if present).
/// Unlike `bufferHas` (the single LAST write), this survives busy-tree chatter.
fn ringHas(needle: []const u8) bool {
var chunk: [1024]u8 = undefined;
var overlap: [128]u8 = undefined;
var overlap_len: usize = 0;
var offset = kernel_log.status().tail;
while (true) {
const n = kernel_log.readAt(offset, &chunk) orelse return false;
if (n == 0) return false;
offset += n;
// Search the previous tail glued to this chunk, then the chunk itself.
if (overlap_len != 0) {
var glued: [1152]u8 = undefined;
@memcpy(glued[0..overlap_len], overlap[0..overlap_len]);
const m = @min(n, glued.len - overlap_len);
@memcpy(glued[overlap_len..][0..m], chunk[0..m]);
if (std.mem.indexOf(u8, glued[0 .. overlap_len + m], needle) != null) return true;
} else if (std.mem.indexOf(u8, chunk[0..n], needle) != null) return true;
overlap_len = @min(n, @min(overlap.len, needle.len));
@memcpy(overlap[0..overlap_len], chunk[n - overlap_len ..][0..overlap_len]);
}
}
/// How many `pci_device` functions the devices broker currently holds. A durable /// How many `pci_device` functions the devices broker currently holds. A durable
/// snapshot, unlike a `bufferHas` poll of the single-latest write_buffer line, so a /// snapshot, unlike a `bufferHas` poll of the single-latest write_buffer line, so a
/// test can wait on it without racing transient log output. `scratch` is /// test can wait on it without racing transient log output. `scratch` is
@@ -2120,11 +2145,11 @@ fn claimReleaseTest(boot_information: *const BootInformation) void {
result(); result();
} }
/// M17.3: the published exit events, proven by their first subscriber. The VFS /// M17.3: the published exit events, proven by a stateful server. The fat
/// subscribes at startup; a client opens a file and parks holding the handle; /// server subscribes at startup; a client opens a file on the volume and parks
/// the kill posts the exit event to the VFS's endpoint; the VFS releases the /// holding the handle; the kill posts the exit event to fat's endpoint; fat
/// dead client's handle and says so — the service-side mirror of iron rule 1 /// releases the dead client's handle and says so — the service-side mirror of
/// (a service must never depend on clients cleaning up after themselves). /// iron rule 1 (a service must never depend on clients cleaning up).
fn vfsClientDeathTest(boot_information: *const BootInformation) void { fn vfsClientDeathTest(boot_information: *const BootInformation) void {
log("DANOS-TEST-BEGIN: vfs-client-death\n", .{}); log("DANOS-TEST-BEGIN: vfs-client-death\n", .{});
if (boot_information.initial_ramdisk_len == 0) { if (boot_information.initial_ramdisk_len == 0) {
@@ -2140,7 +2165,11 @@ fn vfsClientDeathTest(boot_information: *const BootInformation) void {
}; };
process.write_count = 0; process.write_count = 0;
check("vfs spawned", spawnNamed(rd, "vfs")); // The full tree: the storage chain must come up for /mnt/usb to exist —
// the fat server (not a router) now owns client file state and its sweep.
process.setInitialRamdisk(image);
const init_ok = if (process.spawnBundled("/system/services/init")) true else |_| false;
check("init spawned (boots the storage chain)", init_ok);
const me = scheduler.currentId(); const me = scheduler.currentId();
const endpoint = ipcsync.createIpcEndpoint() orelse { const endpoint = ipcsync.createIpcEndpoint() orelse {
@@ -2158,16 +2187,18 @@ fn vfsClientDeathTest(boot_information: *const BootInformation) void {
} }
check("parked client spawned (supervised)", client != 0); check("parked client spawned (supervised)", client != 0);
// Its heartbeat is the fence: once it beats, the handle is open. // Its heartbeat is the fence: once it beats, the handle is open. The park
// waits out the whole USB->block->fat chain, so give it room; with the full
// tree chattering, the ring (not the last-write buffer) is the evidence.
const parked = "vfstest: parked"; const parked = "vfstest: parked";
scheduler.setPriority(1); scheduler.setPriority(1);
var deadline = architecture.millis() + 10000; var deadline = architecture.millis() + 30000;
while (architecture.millis() < deadline) { while (architecture.millis() < deadline) {
if (bufferHas(parked)) break; if (ringHas(parked)) break;
scheduler.yield(); scheduler.yield();
} }
scheduler.setPriority(4); scheduler.setPriority(4);
check("client parked holding an open handle", bufferHas(parked)); check("client parked holding an open handle", ringHas(parked));
check("the kill is accepted", process.killProcess(me, client) == 0); check("the kill is accepted", process.killProcess(me, client) == 0);
var badge: u64 = 0; var badge: u64 = 0;
@@ -2175,16 +2206,17 @@ fn vfsClientDeathTest(boot_information: *const BootInformation) void {
_ = ipcsync.replyWait(endpoint, 0, 0, 0, 0, abi.no_cap, &badge, &received_cap); _ = ipcsync.replyWait(endpoint, 0, 0, 0, 0, abi.no_cap, &badge, &received_cap);
check("the exit notification arrived", badge == abi.notify_badge_bit | abi.notify_exit_bit | client); check("the exit notification arrived", badge == abi.notify_badge_bit | abi.notify_exit_bit | client);
// The VFS heard the same published event; its release line is the proof. // The fat server heard the same published event; its release line in the
// ring is the proof.
const released = "released 1 handle(s) for dead client"; const released = "released 1 handle(s) for dead client";
scheduler.setPriority(1); scheduler.setPriority(1);
deadline = architecture.millis() + 10000; deadline = architecture.millis() + 10000;
while (architecture.millis() < deadline) { while (architecture.millis() < deadline) {
if (bufferHas(released)) break; if (ringHas(released)) break;
scheduler.yield(); scheduler.yield();
} }
scheduler.setPriority(4); scheduler.setPriority(4);
check("the VFS released the dead client's handle", bufferHas(released)); check("the fat server released the dead client's handle", ringHas(released));
result(); result();
} }
@@ -2683,9 +2715,10 @@ fn vfsTest(boot_information: *const BootInformation) void {
process.write_count = 0; process.write_count = 0;
process.write_from_user = false; process.write_from_user = false;
// Spawn just the server and its client (other initial_ramdisk binaries would write to // Seed the kernel VFS (/system) — the router the client exercises.
// the shared evidence buffer and confuse the marker check). process.setInitialRamdisk(image);
_ = spawnNamed(rd, "vfs"); // Spawn just the client: the kernel itself is the VFS root it exercises
// (resolve + fs_node over /system through the plain runtime.fs API).
_ = spawnNamed(rd, "vfs-test"); _ = spawnNamed(rd, "vfs-test");
// Wait for the client's success heartbeat (it round-trips, then beats ~1/s). // Wait for the client's success heartbeat (it round-trips, then beats ~1/s).
+35 -11
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 }); 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 // With the router in the kernel, clients hold OUR node ids directly; sweep
// comes up). From here the VFS routes /mnt/usb/... to this server. // a dead client's open handles via the published exit events (the pattern
var tries: u32 = 0; // the old userspace router used for its own table).
while (tries < 100) : (tries += 1) { _ = 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)) { if (runtime.fs.mount(mount_point, endpoint)) {
std.log.info("mounted {s}", .{mount_point}); std.log.info("mounted {s}", .{mount_point});
} else {
_ = runtime.system.write("/system/services/fat: could not mount /mnt/usb\n");
}
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; 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;
} }
runtime.system.sleep(50);
} }
_ = runtime.system.write("/system/services/fat: could not mount into the VFS\n"); if (released != 0) std.log.info("released {d} handle(s) for dead client {d}", .{ released, dead });
return true; // still serve directly, even if the namespace mount didn't take
} }
const ParentLeaf = struct { parent: []const u8, leaf: []const u8 }; 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); var node = filesystem.resolve(path);
if (node == null and flags & protocol.create != 0) { if (node == null and flags & protocol.create != 0) {
const split = splitParent(path); const split = splitParent(path);
@@ -141,13 +165,12 @@ fn handleOpen(out: []u8, path: []const u8, flags: u32) usize {
filesystem.truncate(&resolved); filesystem.truncate(&resolved);
} }
const index = allocOpen() orelse return fail(out); 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 }, &.{}); return writeReply(out, .{ .status = 0, .node = index }, &.{});
} }
fn onMessage(message: []const u8, out: []u8, sender: u32, capability: ?runtime.ipc.Handle) usize { fn onMessage(message: []const u8, out: []u8, sender: u32, capability: ?runtime.ipc.Handle) usize {
_ = capability; _ = capability;
_ = sender;
if (message.len < protocol.request_size) return fail(out); if (message.len < protocol.request_size) return fail(out);
const request = std.mem.bytesToValue(protocol.Request, message[0..protocol.request_size]); const request = std.mem.bytesToValue(protocol.Request, message[0..protocol.request_size]);
const payload = message[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(); filesystem.current_time_epoch = runtime.system.wallClock();
switch (request.operation) { 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 => { .read => {
const o = openAt(request.node) orelse return fail(out); const o = openAt(request.node) orelse return fail(out);
var buffer: [protocol.maximum_payload]u8 = undefined; var buffer: [protocol.maximum_payload]u8 = undefined;
@@ -237,5 +260,6 @@ pub fn main() void {
.service = .fat, .service = .fat,
.init = initialise, .init = initialise,
.on_message = onMessage, .on_message = onMessage,
.on_notification = onNotification,
}); });
} }
-1
View File
@@ -28,7 +28,6 @@ const build_options = @import("build_options");
/// future init reads this from a manifest under /system/services instead of a /// future init reads this from a manifest under /system/services instead of a
/// hardcoded list.) /// hardcoded list.)
const boot_services = [_][]const u8{ const boot_services = [_][]const u8{
"/system/services/vfs",
"/system/services/input", "/system/services/input",
"/system/services/device-manager", "/system/services/device-manager",
"/system/services/fat", "/system/services/fat",
+8 -6
View File
@@ -35,9 +35,11 @@ const runtime = @import("runtime");
const system = runtime.system; const system = runtime.system;
const fs = runtime.fs; const fs = runtime.fs;
/// Where log trees live. Flips to "/var/log" when the kernel VFS routes /var /// Where log trees live: the FHS path. The kernel VFS routes /var to whatever
/// to the flash volume (M-G); today the FAT service mounts at /mnt/usb only. /// volume the fat server mounted there (today: the /var subtree of the USB
const base = "/mnt/usb/var/log"; /// flash volume) — swapping the persistent medium later touches fat's two
/// mount calls, never this constant.
const base = "/var/log";
/// Drain cadence and the quiet period after which files are closed (flushed). /// Drain cadence and the quiet period after which files are closed (flushed).
const tick_ms = 250; const tick_ms = 250;
@@ -119,9 +121,9 @@ fn onTerminate() void {
fn tick() void { fn tick() void {
if (!storage_ready) { if (!storage_ready) {
// Probe the mount; the ring buffers until it appears. `exists` on the // makePath doubles as the readiness probe: while /var is unmounted the
// mount root is the documented readiness check. // resolve fails fast (no storage round trip) and the ring buffers; the
if (!fs.exists("/mnt/usb")) return; // first success creates the whole per-boot tree.
if (!fs.makePath(boot_directory[0..boot_directory_len])) return; if (!fs.makePath(boot_directory[0..boot_directory_len])) return;
storage_ready = true; storage_ready = true;
if (!announced) { if (!announced) {
+89
View File
@@ -0,0 +1,89 @@
//! /system/tests/vfs-test — a ring-3 client that proves the kernel VFS end to
//! end through the plain `runtime.fs` API: resolve its OWN binary under the
//! kernel-served /system mount, check its metadata, read its ELF magic, and
//! list /system/services. On success it heartbeats "vfstest: ok" so the kernel
//! test can observe it; on failure it reports what went wrong.
//!
//! The "park" role (the fat-client-death test): open a file on the FAT volume,
//! then hold the handle forever without closing — the kill and the fat
//! server's release-on-death sweep are the point.
const std = @import("std");
const runtime = @import("runtime");
const fs = runtime.fs;
pub fn main(init: runtime.process.Init) void {
if (init.arguments.count > 1) {
park();
return;
}
// Our own binary, resolved through the kernel mount table.
const self_path = "/system/tests/vfs-test";
var file = fs.open(self_path, .{}) orelse {
_ = runtime.system.write("vfstest: open of own binary failed\n");
return;
};
defer file.close();
const attributes = file.attributes() orelse {
_ = runtime.system.write("vfstest: attributes failed\n");
return;
};
if (attributes.kind != .regular or attributes.size == 0) {
_ = runtime.system.write("vfstest: bad attributes\n");
return;
}
var header: [4]u8 = undefined;
const n = file.read(&header) orelse 0;
if (n != 4 or header[0] != 0x7f or header[1] != 'E' or header[2] != 'L' or header[3] != 'F') {
_ = runtime.system.write("vfstest: ELF magic mismatch\n");
return;
}
// The write refusal: /system is read-only by construction.
if (file.write("x") != null or fs.open("/system/tests/new-file", .{ .create = true }) != null) {
_ = runtime.system.write("vfstest: /system accepted a write\n");
return;
}
// Listing: /system/services contains init.
var saw_init = false;
if (fs.openDirectory("/system/services")) |listing| {
var directory = listing;
defer directory.close();
var entry: fs.Entry = .{};
while (directory.next(&entry)) {
if (std.mem.eql(u8, entry.name(), "init")) saw_init = true;
}
}
if (!saw_init) {
_ = runtime.system.write("vfstest: /system/services listing missed init\n");
return;
}
while (true) {
_ = runtime.system.write("vfstest: ok\n");
runtime.system.sleep(1000);
}
}
fn park() void {
// The storage chain (usb -> block -> fat -> mounts) takes a few seconds;
// retry until the volume appears.
var parked: ?fs.File = null;
var tries: u32 = 0;
while (parked == null and tries < 1000) : (tries += 1) {
parked = fs.open("/mnt/usb/parked", .{ .create = true });
if (parked == null) runtime.system.sleep(20);
}
if (parked == null) {
_ = runtime.system.write("vfstest: park open failed\n");
return;
}
while (true) {
_ = runtime.system.write("vfstest: parked\n");
runtime.system.sleep(500);
}
}
-39
View File
@@ -1,39 +0,0 @@
//! Pure path utilities for the VFS mount router — no IPC, no state, so they are
//! host-testable in isolation. The router uses these to decide whether an opened
//! path lies under a mount point and, if so, what it looks like relative to that
//! mount.
const std = @import("std");
/// If `path` lies under `mount_prefix` — equal to it, or the prefix followed by a
/// path separator — return the path relative to the mount ("/" for an exact
/// match, otherwise the tail beginning with '/'). Returns null when `path` is not
/// under the mount, so a prefix like "/mnt/usb" never captures "/mnt/usbextra".
pub fn underMount(path: []const u8, mount_prefix: []const u8) ?[]const u8 {
if (path.len < mount_prefix.len) return null;
if (!std.mem.eql(u8, path[0..mount_prefix.len], mount_prefix)) return null;
if (path.len == mount_prefix.len) return "/";
if (path[mount_prefix.len] != '/') return null;
return path[mount_prefix.len..];
}
/// Whether `path` is absolute (rooted at '/'). Bare names — what the flat ramfs
/// uses — are relative and never route through a mount.
pub fn isAbsolute(path: []const u8) bool {
return path.len > 0 and path[0] == '/';
}
test "underMount matches only at path boundaries" {
try std.testing.expectEqualStrings("/", underMount("/mnt/usb", "/mnt/usb").?);
try std.testing.expectEqualStrings("/system/kernel", underMount("/mnt/usb/system/kernel", "/mnt/usb").?);
try std.testing.expect(underMount("/mnt/usbextra", "/mnt/usb") == null); // not a boundary
try std.testing.expect(underMount("/mnt", "/mnt/usb") == null); // shorter than the prefix
try std.testing.expect(underMount("/other", "/mnt/usb") == null);
try std.testing.expect(underMount("greeting", "/mnt/usb") == null); // a bare name
}
test "isAbsolute distinguishes paths from bare names" {
try std.testing.expect(isAbsolute("/mnt/usb"));
try std.testing.expect(!isAbsolute("greeting"));
try std.testing.expect(!isAbsolute(""));
}
-62
View File
@@ -1,62 +0,0 @@
//! /system/services/vfs/vfs-test — a client that proves the VFS round trip end to end: open a
//! file through the `runtime.fs` file API, write to it, seek back, read it, and compare.
//! On success it heartbeats "vfstest: ok" so the kernel test can observe it;
//! on failure it reports what went wrong. Shipped in the initial_ramdisk alongside vfs.
const std = @import("std");
const runtime = @import("runtime");
const fs = runtime.fs;
pub fn main(init: runtime.process.Init) void {
const payload = "hello-vfs";
// The "park" role (the vfs-client-death test): open a file, then hold the
// handle forever without closing — the kill and the VFS's release-on-death
// are the point.
if (init.arguments.count > 1) {
var parked: ?fs.File = null;
var tries: u32 = 0;
while (parked == null and tries < 200) : (tries += 1) {
parked = fs.open("parked", .{ .create = true });
if (parked == null) runtime.system.sleep(20);
}
if (parked == null) {
_ = runtime.system.write("vfstest: park open failed\n");
return;
}
while (true) {
_ = runtime.system.write("vfstest: parked\n");
runtime.system.sleep(500);
}
}
// The VFS server may not have registered yet — retry open until it's up.
var opened: ?fs.File = null;
var tries: u32 = 0;
while (opened == null and tries < 200) : (tries += 1) {
opened = fs.open("greeting", .{ .create = true });
if (opened == null) runtime.system.sleep(20);
}
var greeting = opened orelse {
_ = runtime.system.write("vfstest: open failed\n");
return;
};
if ((greeting.write(payload) orelse 0) != payload.len) {
_ = runtime.system.write("vfstest: write failed\n");
return;
}
greeting.seekTo(0);
var buffer: [32]u8 = undefined;
const n = greeting.read(&buffer) orelse 0;
greeting.close();
if (n == payload.len and std.mem.eql(u8, buffer[0..n], payload)) {
while (true) {
_ = runtime.system.write("vfstest: ok\n");
runtime.system.sleep(1000);
}
}
_ = runtime.system.write("vfstest: mismatch\n");
}
-389
View File
@@ -1,389 +0,0 @@
//! 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 }, &.{});
}
// --- 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;
std.log.info("remounted {s}", .{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;
std.log.info("mounted {s}", .{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;
std.log.info("unmounted {s}", .{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) std.log.info("released {d} handle(s) for dead client {d}", .{ 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,
});
}
+2 -1
View File
@@ -427,6 +427,7 @@ CASES = [
# open handle, and the VFS releases it (process-lifecycle.md "Who learns of a death"). # open handle, and the VFS releases it (process-lifecycle.md "Who learns of a death").
{"name": "vfs-client-death", {"name": "vfs-client-death",
"smp": 4, "smp": 4,
"timeout": 90, # the park client waits out the whole USB->block->fat chain
"expect": r"DANOS-TEST-RESULT: PASS", "expect": r"DANOS-TEST-RESULT: PASS",
"fail": r"DANOS-TEST-RESULT: FAIL"}, "fail": r"DANOS-TEST-RESULT: FAIL"},
# M17.4: signals over IPC — ping, reload, terminate (clean exit), the one-shot # M17.4: signals over IPC — ping, reload, terminate (clean exit), the one-shot
@@ -562,7 +563,7 @@ CASES = [
"smp": 4, "smp": 4,
"timeout": 150, "timeout": 150,
"qmp_after": {"delay": 8, "command": "system_powerdown"}, "qmp_after": {"delay": 8, "command": "system_powerdown"},
"expect": r"logger: logging to /mnt/usb/var/log/\d{4}-\d{2}-\d{2}T\d{6}Z[\s\S]*" "expect": r"logger: logging to /var/log/\d{4}-\d{2}-\d{2}T\d{6}Z[\s\S]*"
r"init: shutting down[\s\S]*" r"init: shutting down[\s\S]*"
r"logger: flushed through sequence \d+[\s\S]*" r"logger: flushed through sequence \d+[\s\S]*"
r"power: entering S5", r"power: entering S5",