M9: user-space VFS server + client file API (open/read/write/stat + stdio)

The payoff milestone: files are served by a user-space process, reached over
IPC — the kernel never sees a path or an fd.

- lib/vfs_proto.zig: the VFS wire protocol (Op, Request/Reply fixed header +
  inline payload, Stat), shared by client and server; one message <= MSG_MAX.
- lib/ipc.zig: replyWait() — the server-side dual-return stub (length in rax,
  badge in rdx via a "+{rdx}" read-write operand), deferred from M7.
- sbin/vfs.zig: the real VFS server — an in-heap ramfs (open creates a node)
  with an IPC_ReplyWait dispatch loop serving open/read/write/stat/close.
  Registers its endpoint under the well-known vfs id at startup.
- lib/unistd.zig: POSIX-style client API in rt — a per-process fd table +
  open/close/read/write/lseek/stat, each an IPC_Call to the VFS. The kernel
  knows nothing of fds; the table lives here.
- lib/stdio.zig: C stdio over unistd — FILE + fopen/fclose/fread/fwrite/
  fseek/ftell/rewind/feof/ferror/fputs/fputc/fgetc (unbuffered for now).
- sbin/vfstest.zig: a client that opens/writes/seeks/reads a file and only
  heartbeats "vfstest: ok" if the round trip matched. Packed in the initrd.
- New `vfs` test drives it end to end. initrd test relaxed to generic
  liveness. Suite 31/31.
This commit is contained in:
Daniel Samson
2026-07-09 07:48:24 +01:00
parent 750a73f050
commit b0f894f50c
10 changed files with 583 additions and 14 deletions
+3
View File
@@ -218,6 +218,7 @@ pub fn build(b: *std.Build) void {
// the host-side mkinitrd tool. The bootloader ferries the image to the kernel, // the host-side mkinitrd tool. The bootloader ferries the image to the kernel,
// which unpacks it and spawns each program (src/user/proto/initrd.zig). // which unpacks it and spawns each program (src/user/proto/initrd.zig).
const vfs_exe = addUserBinary(b, kernel_target, rt_mod, "vfs", "sbin/vfs.zig"); const vfs_exe = addUserBinary(b, kernel_target, rt_mod, "vfs", "sbin/vfs.zig");
const vfstest_exe = addUserBinary(b, kernel_target, rt_mod, "vfstest", "sbin/vfstest.zig");
// Pack the user binaries into the initrd image with the host-side Python tool // Pack the user binaries into the initrd image with the host-side Python tool
// (the container format is trivial, and Python sidesteps std API churn). Args: // (the container format is trivial, and Python sidesteps std API churn). Args:
@@ -227,6 +228,8 @@ pub fn build(b: *std.Build) void {
const initrd_img = mk_run.addOutputFileArg("initrd.img"); const initrd_img = mk_run.addOutputFileArg("initrd.img");
mk_run.addArg("vfs"); mk_run.addArg("vfs");
mk_run.addFileArg(vfs_exe.getEmittedBin()); mk_run.addFileArg(vfs_exe.getEmittedBin());
mk_run.addArg("vfstest");
mk_run.addFileArg(vfstest_exe.getEmittedBin());
// Install the image to zig-out/bin (so the QEMU test harness picks it up like // Install the image to zig-out/bin (so the QEMU test harness picks it up like
// the other binaries). The run-x86-64 ESP install is added below. // the other binaries). The run-x86-64 ESP install is added below.
+26 -2
View File
@@ -50,5 +50,29 @@ pub fn call(h: Handle, msg: []const u8, reply: []u8) CallError!usize {
return if (failed(r)) error.Failed else r; return if (failed(r)) error.Failed else r;
} }
// replyWait() (server side — returns message length + sender badge) lands with /// The result of a `replyWait`: the request length and the sender's badge (a
// the first server binary (M9), where it can be exercised end to end. /// task id, or an IRQ notification if the high bit is set).
pub const Received = struct {
len: usize,
badge: u64,
};
/// Server side of IPC_ReplyWait: deliver `reply` to the client last received (if
/// any), then block until the next request arrives in `recv`. Returns its length
/// and the sender badge. This syscall returns two values — the length in rax and
/// the badge in rdx — so it needs a hand-written stub: rdx is a read-write
/// operand (input = reply length, arg #3; output = badge).
pub fn replyWait(h: Handle, reply: []const u8, recv: []u8) Received {
var rax: usize = undefined;
var rdx: usize = reply.len; // in: reply_len (arg #3 -> rdx); out: badge
asm volatile ("syscall"
: [rax] "={rax}" (rax),
[rdx] "+{rdx}" (rdx),
: [n] "{rax}" (@intFromEnum(danos.Syscall.ipc_reply_wait)),
[a0] "{rdi}" (h),
[a1] "{rsi}" (@intFromPtr(reply.ptr)),
[a3] "{r10}" (@intFromPtr(recv.ptr)),
[a4] "{r8}" (recv.len),
: .{ .rcx = true, .r11 = true, .memory = true });
return .{ .len = rax, .badge = rdx };
}
+6
View File
@@ -14,6 +14,12 @@ pub const sys = @import("sys.zig");
pub const heap = @import("heap.zig"); pub const heap = @import("heap.zig");
pub const ipc = @import("ipc.zig"); pub const ipc = @import("ipc.zig");
pub const start = @import("start.zig"); pub const start = @import("start.zig");
/// The VFS wire protocol (shared with the VFS server).
pub const vfsproto = @import("vfs_proto.zig");
/// POSIX-style file API: open/read/write/lseek/stat/close.
pub const unistd = @import("unistd.zig");
/// C stdio: fopen/fread/fwrite/fseek/ftell/fclose over unistd.
pub const stdio = @import("stdio.zig");
/// Re-exported so a user binary can `pub const panic = rt.panic;`. /// Re-exported so a user binary can `pub const panic = rt.panic;`.
pub const panic = start.panic; pub const panic = start.panic;
+115
View File
@@ -0,0 +1,115 @@
//! A small C stdio layer over the POSIX-style file API (unistd.zig). Unbuffered
//! for now — each fread/fwrite is one VFS round trip; an internal buffer (fewer
//! IPC calls) is a later optimisation. Both a Zig-callable API and `extern "C"`
//! symbols are provided, so Zig and future C programs share it.
const std = @import("std");
const unistd = @import("unistd.zig");
const heap = @import("heap.zig");
pub const SEEK_SET = unistd.SEEK_SET;
pub const SEEK_CUR = unistd.SEEK_CUR;
pub const SEEK_END = unistd.SEEK_END;
/// A C `FILE`: an fd plus sticky end-of-file / error flags. Allocated on the
/// heap; `fclose` frees it.
pub const FILE = extern struct {
fd: i32,
eof: c_int = 0,
err: c_int = 0,
};
fn flagsFor(mode: []const u8) u32 {
if (mode.len == 0) return 0;
return switch (mode[0]) {
'w', 'a' => unistd.O_CREAT,
else => 0,
};
}
/// Open `path` in `mode` ("r"/"w"/"a", '+' ignored for now). Returns null on error.
pub fn fopen(path: []const u8, mode: []const u8) ?*FILE {
const fd = unistd.open(path, flagsFor(mode));
if (fd < 0) return null;
const f = heap.allocator().create(FILE) catch {
unistd.close(fd);
return null;
};
f.* = .{ .fd = fd };
if (mode.len > 0 and mode[0] == 'a') _ = unistd.lseek(fd, 0, unistd.SEEK_END);
return f;
}
pub fn fclose(f: *FILE) c_int {
unistd.close(f.fd);
heap.allocator().destroy(f);
return 0;
}
/// Read `size*nmemb` bytes; returns the number of whole items read.
pub fn fread(buf: []u8, size: usize, nmemb: usize, f: *FILE) usize {
const total = size * nmemb;
if (total == 0) return 0;
const n = unistd.read(f.fd, buf[0..@min(buf.len, total)]);
if (n <= 0) {
f.eof = 1;
return 0;
}
return @as(usize, @intCast(n)) / size;
}
/// Write `size*nmemb` bytes; returns the number of whole items written.
pub fn fwrite(data: []const u8, size: usize, nmemb: usize, f: *FILE) usize {
const total = @min(data.len, size * nmemb);
if (total == 0) return 0;
const n = unistd.write(f.fd, data[0..total]);
if (n <= 0) {
f.err = 1;
return 0;
}
return @as(usize, @intCast(n)) / size;
}
pub fn fseek(f: *FILE, off: i64, whence: u32) c_int {
f.eof = 0;
return if (unistd.lseek(f.fd, off, whence) < 0) -1 else 0;
}
pub fn ftell(f: *FILE) i64 {
return unistd.lseek(f.fd, 0, unistd.SEEK_CUR);
}
pub fn rewind(f: *FILE) void {
_ = fseek(f, 0, SEEK_SET);
}
pub fn feof(f: *FILE) c_int {
return f.eof;
}
pub fn ferror(f: *FILE) c_int {
return f.err;
}
pub fn fputs(s: []const u8, f: *FILE) c_int {
return if (unistd.write(f.fd, s) < 0) -1 else 0;
}
pub fn fputc(c: u8, f: *FILE) c_int {
const b = [_]u8{c};
return if (unistd.write(f.fd, &b) == 1) c else -1;
}
pub fn fgetc(f: *FILE) c_int {
var b: [1]u8 = undefined;
const n = unistd.read(f.fd, &b);
if (n <= 0) {
f.eof = 1;
return -1; // EOF
}
return b[0];
}
// Real `extern "C"` symbols (fopen/fread/fseek/...) — with a C-string signature
// distinct from the Zig slice API above — land with the first C program, wired
// via @export so they don't collide with these Zig names.
+149
View File
@@ -0,0 +1,149 @@
//! POSIX-style file API for user programs — the low level under C stdio. Files
//! are named objects served by the user-space VFS server (sbin/vfs.zig); each
//! call marshals a request, IPC_Calls the VFS, and unmarshals the reply. The
//! kernel knows nothing of files or fds — the fd table lives here, per process.
const std = @import("std");
const proto = @import("vfs_proto.zig");
const ipc = @import("ipc.zig");
const danos = @import("danos");
pub const O_CREAT = proto.O_CREAT;
pub const SEEK_SET: u32 = 0;
pub const SEEK_CUR: u32 = 1;
pub const SEEK_END: u32 = 2;
// Resolve (and cache) the VFS server endpoint, looked up by well-known id.
var vfs_handle: usize = 0;
var vfs_resolved = false;
fn vfs() ?usize {
if (!vfs_resolved) {
vfs_handle = ipc.lookup(.vfs) orelse return null;
vfs_resolved = true;
}
return vfs_handle;
}
const max_fds = 32;
const Fd = struct { used: bool = false, node: u64 = 0, offset: u64 = 0 };
var fds = [_]Fd{.{}} ** max_fds;
fn allocFd() ?usize {
for (&fds, 0..) |*f, i| {
if (!f.used) {
f.* = .{ .used = true };
return i;
}
}
return null;
}
const Result = struct { reply: proto.Reply, payload: []u8 };
/// One request/reply round trip: [Request header][send payload] -> VFS ->
/// [Reply header][recv payload]. The recv payload is written into `out`.
fn transact(req: proto.Request, send: []const u8, out: []u8) ?Result {
const h = vfs() orelse return null;
var msg: [proto.msg_max]u8 = undefined;
@memcpy(msg[0..proto.req_size], std.mem.asBytes(&req));
const slen = @min(send.len, proto.max_payload);
@memcpy(msg[proto.req_size..][0..slen], send[0..slen]);
var rbuf: [proto.msg_max]u8 = undefined;
const n = ipc.call(h, msg[0 .. proto.req_size + slen], &rbuf) catch return null;
if (n < proto.reply_size) return null;
const reply = std.mem.bytesToValue(proto.Reply, rbuf[0..proto.reply_size]);
const rpl = @min(n - proto.reply_size, out.len);
@memcpy(out[0..rpl], rbuf[proto.reply_size..][0..rpl]);
return .{ .reply = reply, .payload = out[0..rpl] };
}
/// Open (or create, with O_CREAT) `path`; returns an fd or -1.
pub fn open(path: []const u8, flags: u32) i32 {
const fd = allocFd() orelse return -1;
const req = proto.Request{ .op = .open, .node = 0, .offset = 0, .len = @intCast(path.len), .flags = flags };
const r = transact(req, path, &.{}) orelse {
fds[fd].used = false;
return -1;
};
if (r.reply.status != 0) {
fds[fd].used = false;
return -1;
}
fds[fd] = .{ .used = true, .node = r.reply.node, .offset = 0 };
return @intCast(fd);
}
fn fdPtr(fd: i32) ?*Fd {
if (fd < 0 or fd >= max_fds) return null;
const f = &fds[@intCast(fd)];
return if (f.used) f else null;
}
/// Read up to `buf.len` bytes at the current offset; returns the count or -1.
pub fn read(fd: i32, buf: []u8) isize {
const f = fdPtr(fd) orelse return -1;
const want: u32 = @intCast(@min(buf.len, proto.max_payload));
const req = proto.Request{ .op = .read, .node = f.node, .offset = f.offset, .len = want, .flags = 0 };
const r = transact(req, &.{}, buf) orelse return -1;
if (r.reply.status != 0) return -1;
f.offset += r.reply.len;
return @intCast(r.reply.len);
}
/// Write `data` at the current offset; returns the count or -1.
pub fn write(fd: i32, data: []const u8) isize {
const f = fdPtr(fd) orelse return -1;
const want: u32 = @intCast(@min(data.len, proto.max_payload));
const req = proto.Request{ .op = .write, .node = f.node, .offset = f.offset, .len = want, .flags = 0 };
const r = transact(req, data[0..want], &.{}) orelse return -1;
if (r.reply.status != 0) return -1;
f.offset += r.reply.len;
return @intCast(r.reply.len);
}
/// Reposition the fd's offset. Returns the new offset or -1. (SEEK_END needs the
/// file size, which `stat` provides; handled by fetching it here.)
pub fn lseek(fd: i32, off: i64, whence: u32) i64 {
const f = fdPtr(fd) orelse return -1;
const base: i64 = switch (whence) {
SEEK_SET => 0,
SEEK_CUR => @intCast(f.offset),
SEEK_END => blk: {
const req = proto.Request{ .op = .stat, .node = f.node, .offset = 0, .len = 0, .flags = 0 };
var sbuf: [@sizeOf(proto.Stat)]u8 = undefined;
const r = transact(req, &.{}, &sbuf) orelse return -1;
if (r.reply.status != 0 or r.payload.len < @sizeOf(proto.Stat)) return -1;
const st = std.mem.bytesToValue(proto.Stat, sbuf[0..@sizeOf(proto.Stat)]);
break :blk @intCast(st.size);
},
else => return -1,
};
const pos = base + off;
if (pos < 0) return -1;
f.offset = @intCast(pos);
return pos;
}
/// Stat `path`. Returns 0 or -1.
pub fn stat(path: []const u8, out: *proto.Stat) i32 {
// Open, stat by node, close — simple and enough for now.
const fd = open(path, 0);
if (fd < 0) return -1;
defer close(fd);
const f = fdPtr(fd).?;
const req = proto.Request{ .op = .stat, .node = f.node, .offset = 0, .len = 0, .flags = 0 };
var sbuf: [@sizeOf(proto.Stat)]u8 = undefined;
const r = transact(req, &.{}, &sbuf) orelse return -1;
if (r.reply.status != 0 or r.payload.len < @sizeOf(proto.Stat)) return -1;
out.* = std.mem.bytesToValue(proto.Stat, sbuf[0..@sizeOf(proto.Stat)]);
return 0;
}
/// Close an fd (best effort — tells the VFS to release the open file).
pub fn close(fd: i32) void {
const f = fdPtr(fd) orelse return;
const req = proto.Request{ .op = .close, .node = f.node, .offset = 0, .len = 0, .flags = 0 };
_ = transact(req, &.{}, &.{});
f.used = false;
}
+53
View File
@@ -0,0 +1,53 @@
//! The VFS wire protocol — the message format spoken between a client (via the
//! `rt` file API) and the user-space VFS server over IPC. A request is a fixed
//! `Request` header followed by an inline payload (a path, or write bytes); a
//! reply is a fixed `Reply` header followed by an inline payload (read bytes, or
//! a Stat). Everything fits in one IPC message (<= ipc MSG_MAX = 256 bytes).
//!
//! This is user-space only — the kernel knows nothing of files or paths; it only
//! moves the bytes. Shared by lib/unistd.zig (client) and sbin/vfs.zig (server).
pub const Op = enum(u32) {
open, // open(path) -> node id
close, // close(node)
read, // read(node, offset, len) -> bytes
write, // write(node, offset, bytes) -> count
stat, // stat(node) -> Stat
};
/// Request header. `node` is the server-side open-file id (from a prior open);
/// for `open` the path is the payload and `len` is its length. `offset`/`len`
/// carry the read/write position and count.
pub const Request = extern struct {
op: Op,
node: u64,
offset: u64,
len: u32,
flags: u32,
};
/// Reply header. `status` is 0 on success or a negative errno; `node` is the new
/// open-file id (for `open`); `len` is the payload length (bytes read, or the
/// Stat size).
pub const Reply = extern struct {
status: i32,
_pad: u32 = 0,
node: u64 = 0,
len: u32 = 0,
_pad2: u32 = 0,
};
pub const Stat = extern struct {
size: u64,
kind: u32,
_pad: u32 = 0,
};
pub const msg_max: usize = 256;
pub const req_size: usize = @sizeOf(Request);
pub const reply_size: usize = @sizeOf(Reply);
/// Largest inline payload that still fits one IPC message alongside a header.
pub const max_payload: usize = msg_max - req_size;
/// Open flags.
pub const O_CREAT: u32 = 1;
+128 -6
View File
@@ -1,14 +1,136 @@
//! /sbin/vfs — the user-space VFS server. Shipped in the initrd and spawned as a //! /sbin/vfs — the user-space VFS server. Shipped in the initrd, spawned as a
//! ring-3 process. This is a placeholder that only heartbeats, proving the initrd //! ring-3 process, and reached by every other process through IPC (the `rt`
//! pipeline ships and spawns it; the real path namespace + IPC dispatch loop //! file API marshals open/read/write/stat/close into calls to this server's
//! (open/read/write/stat forwarded to driver processes) is built in M9. //! endpoint, published under the well-known `vfs` service id).
//!
//! For now the namespace is a small in-memory ramfs (opening a name creates it):
//! enough to prove the whole path — client file API -> IPC -> server dispatch ->
//! reply. Device nodes backed by user-space drivers (/dev) layer on top in M10,
//! where `open` on a /dev name forwards to the owning driver's endpoint.
const std = @import("std");
const rt = @import("rt"); const rt = @import("rt");
const proto = rt.vfsproto;
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,
node: usize = 0,
};
var nodes = [_]Node{.{}} ** 8;
var opens = [_]OpenFile{.{}} ** 16;
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;
}
/// Serialise a reply header + payload into `out`; returns the total length.
fn writeReply(out: []u8, reply: proto.Reply, payload: []const u8) usize {
@memcpy(out[0..proto.reply_size], std.mem.asBytes(&reply));
const n = @min(payload.len, out.len - proto.reply_size);
@memcpy(out[proto.reply_size..][0..n], payload[0..n]);
return proto.reply_size + n;
}
fn fail(out: []u8) usize {
return writeReply(out, .{ .status = -1 }, &.{});
}
/// Handle one request; write the reply into `out`, return its length.
fn handle(msg: []const u8, out: []u8) usize {
if (msg.len < proto.req_size) return fail(out);
const req = std.mem.bytesToValue(proto.Request, msg[0..proto.req_size]);
const payload = msg[proto.req_size..];
switch (req.op) {
.open => {
const name = payload[0..@min(payload.len, req.len)];
const ni = findNode(name) orelse createNode(name) orelse return fail(out);
for (&opens, 0..) |*o, i| {
if (!o.used) {
o.* = .{ .used = true, .node = ni };
return writeReply(out, .{ .status = 0, .node = i }, &.{});
}
}
return fail(out);
},
.read => {
const of = openAt(req.node) orelse return fail(out);
const nd = &nodes[of.node];
const off: usize = @intCast(req.offset);
if (off >= nd.size) return writeReply(out, .{ .status = 0, .len = 0 }, &.{}); // EOF
const n = @min(@min(nd.size - off, req.len), proto.max_payload);
return writeReply(out, .{ .status = 0, .len = @intCast(n) }, nd.data[off .. off + n]);
},
.write => {
const of = openAt(req.node) orelse return fail(out);
const nd = &nodes[of.node];
const off: usize = @intCast(req.offset);
if (off > nd.data.len) return fail(out);
const n = @min(@min(payload.len, req.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) }, &.{});
},
.stat => {
const of = openAt(req.node) orelse return fail(out);
const st = proto.Stat{ .size = nodes[of.node].size, .kind = 0 };
return writeReply(out, .{ .status = 0, .len = @sizeOf(proto.Stat) }, std.mem.asBytes(&st));
},
.close => {
if (req.node < opens.len) opens[@intCast(req.node)].used = false;
return writeReply(out, .{ .status = 0 }, &.{});
},
}
}
pub fn main() void { pub fn main() void {
const ep = rt.ipc.createEndpoint() orelse {
_ = rt.sys.write("vfs: no endpoint\n");
return;
};
if (!rt.ipc.register(.vfs, ep)) {
_ = rt.sys.write("vfs: register failed\n");
return;
}
_ = rt.sys.write("vfs: ready\n");
var reply_buf: [proto.msg_max]u8 = undefined;
var reply_len: usize = 0;
var recv: [proto.msg_max]u8 = undefined;
while (true) { while (true) {
_ = rt.sys.write("vfs: alive\n"); const got = rt.ipc.replyWait(ep, reply_buf[0..reply_len], &recv);
rt.sys.sleep(1000); // Ignore notifications (none expected here); handle a request.
reply_len = handle(recv[0..got.len], &reply_buf);
} }
} }
+47
View File
@@ -0,0 +1,47 @@
//! /sbin/vfstest — a client that proves the VFS round trip end to end: open a
//! file through the `rt` 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 initrd alongside vfs.
const std = @import("std");
const rt = @import("rt");
pub fn main() void {
const u = rt.unistd;
const payload = "hello-vfs";
// The VFS server may not have registered yet — retry open until it's up.
var fd: i32 = -1;
var tries: u32 = 0;
while (fd < 0 and tries < 200) : (tries += 1) {
fd = u.open("greeting", u.O_CREAT);
if (fd < 0) rt.sys.sleep(20);
}
if (fd < 0) {
_ = rt.sys.write("vfstest: open failed\n");
return;
}
if (u.write(fd, payload) != @as(isize, payload.len)) {
_ = rt.sys.write("vfstest: write failed\n");
return;
}
_ = u.lseek(fd, 0, u.SEEK_SET);
var buf: [32]u8 = undefined;
const n = u.read(fd, &buf);
u.close(fd);
if (n == @as(isize, payload.len) and std.mem.eql(u8, buf[0..@intCast(n)], payload)) {
while (true) {
_ = rt.sys.write("vfstest: ok\n");
rt.sys.sleep(1000);
}
}
_ = rt.sys.write("vfstest: mismatch\n");
}
pub const panic = rt.panic;
comptime {
_ = &rt.start._start;
}
+51 -6
View File
@@ -107,6 +107,8 @@ pub fn run(case: []const u8, boot_info: *const BootInfo) void {
processTest(boot_info); processTest(boot_info);
} else if (eql(case, "initrd")) { } else if (eql(case, "initrd")) {
initrdTest(boot_info); initrdTest(boot_info);
} else if (eql(case, "vfs")) {
vfsTest(boot_info);
} else if (eql(case, "poweroff")) { } else if (eql(case, "poweroff")) {
powerTest(.off); powerTest(.off);
} else if (eql(case, "reboot")) { } else if (eql(case, "reboot")) {
@@ -970,17 +972,60 @@ fn initrdTest(boot_info: *const BootInfo) void {
} }
check("every initrd binary spawned", spawned == rd.count); check("every initrd binary spawned", spawned == rd.count);
// Wait for the spawned program(s) to heartbeat (the vfs stub sleeps ~1 s). // Wait for the spawned programs to run and make syscalls (they write + sleep).
sched.setPriority(1); sched.setPriority(1);
const deadline = arch.millis() + 8000; const deadline = arch.millis() + 8000;
while (process.write_count < 2 and arch.millis() < deadline) sched.yield(); while (process.write_count < 2 and arch.millis() < deadline) sched.yield();
sched.setPriority(4); sched.setPriority(4);
const prefix = "vfs: alive"; check("initrd processes ran and made syscalls (>=2)", process.write_count >= 2);
const beat_ok = process.write_len >= prefix.len and eql(process.write_buf[0..prefix.len], prefix); check("syscalls came from user mode (CPL 3)", process.write_from_user);
check("an initrd process heartbeats (>=2)", process.write_count >= 2); result();
check("heartbeat text arrived intact", beat_ok); }
check("heartbeats came from user mode (CPL 3)", process.write_from_user);
/// The full VFS path: spawn the user-space VFS server and a client from the
/// initrd. The client opens a file through the rt file API, writes, seeks, reads
/// it back, and — only if the round trip matched — heartbeats "vfstest: ok". So
/// seeing that marker proves client open/write/read reached the server over IPC
/// and came back correct. (The client retries until the server registers.)
fn vfsTest(boot_info: *const BootInfo) void {
log("DANOS-TEST-BEGIN: vfs\n", .{});
if (boot_info.initrd_len == 0) {
check("bootloader handed over an initrd", false);
result();
return;
}
const image = @as([*]const u8, @ptrFromInt(danos.physToVirt(boot_info.initrd_base)))[0..boot_info.initrd_len];
const rd = initrd.Reader.init(image) orelse {
check("initrd image is valid", false);
result();
return;
};
process.write_count = 0;
process.write_from_user = false;
var i: u32 = 0;
while (i < rd.count) : (i += 1) {
const item = rd.entry(i) orelse continue;
process.spawnProcess(item.blob, 4) catch |err| {
log("DANOS-VFS-ERR: {s}: {s}\n", .{ item.name, @errorName(err) });
};
}
// Wait for the client's success heartbeat (it round-trips, then beats ~1/s).
const prefix = "vfstest: ok";
sched.setPriority(1);
const deadline = arch.millis() + 10000;
while (arch.millis() < deadline) {
if (process.write_len >= prefix.len and eql(process.write_buf[0..prefix.len], prefix) and process.write_count >= 2) break;
sched.yield();
}
sched.setPriority(4);
const ok = process.write_len >= prefix.len and eql(process.write_buf[0..prefix.len], prefix);
check("client completed the VFS round trip (open/write/read matched)", ok);
check("the round trip ran repeatedly (server stays up)", process.write_count >= 2);
check("client syscalls came from user mode (CPL 3)", process.write_from_user);
result(); result();
} }
+5
View File
@@ -183,6 +183,11 @@ CASES = [
{"name": "initrd", {"name": "initrd",
"expect": r"DANOS-TEST-RESULT: PASS", "expect": r"DANOS-TEST-RESULT: PASS",
"fail": r"DANOS-TEST-RESULT: FAIL"}, "fail": r"DANOS-TEST-RESULT: FAIL"},
# The user-space VFS: a client opens/writes/reads a file through the rt file
# API, which IPCs the VFS server process; the round trip must match.
{"name": "vfs",
"expect": r"DANOS-TEST-RESULT: PASS",
"fail": r"DANOS-TEST-RESULT: FAIL"},
# The ACPI power path succeeds by QEMU *exiting* (S5 off / reset), so match the # The ACPI power path succeeds by QEMU *exiting* (S5 off / reset), so match the
# pre-transition marker; the FAIL line only appears if the transition didn't take. # pre-transition marker; the FAIL line only appears if the transition didn't take.
{"name": "poweroff", {"name": "poweroff",