diff --git a/build.zig b/build.zig index a7c4a80..05a9087 100644 --- a/build.zig +++ b/build.zig @@ -218,6 +218,7 @@ pub fn build(b: *std.Build) void { // 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). 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 // (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"); mk_run.addArg("vfs"); 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 // the other binaries). The run-x86-64 ESP install is added below. diff --git a/lib/ipc.zig b/lib/ipc.zig index 0262cbd..0f1545c 100644 --- a/lib/ipc.zig +++ b/lib/ipc.zig @@ -50,5 +50,29 @@ pub fn call(h: Handle, msg: []const u8, reply: []u8) CallError!usize { return if (failed(r)) error.Failed else r; } -// replyWait() (server side — returns message length + sender badge) lands with -// the first server binary (M9), where it can be exercised end to end. +/// The result of a `replyWait`: the request length and the sender's badge (a +/// 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 }; +} diff --git a/lib/rt.zig b/lib/rt.zig index d6644c9..2f36bcf 100644 --- a/lib/rt.zig +++ b/lib/rt.zig @@ -14,6 +14,12 @@ pub const sys = @import("sys.zig"); pub const heap = @import("heap.zig"); pub const ipc = @import("ipc.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;`. pub const panic = start.panic; diff --git a/lib/stdio.zig b/lib/stdio.zig new file mode 100644 index 0000000..8d8155a --- /dev/null +++ b/lib/stdio.zig @@ -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. diff --git a/lib/unistd.zig b/lib/unistd.zig new file mode 100644 index 0000000..6e162c2 --- /dev/null +++ b/lib/unistd.zig @@ -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; +} diff --git a/lib/vfs_proto.zig b/lib/vfs_proto.zig new file mode 100644 index 0000000..8ba33de --- /dev/null +++ b/lib/vfs_proto.zig @@ -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; diff --git a/sbin/vfs.zig b/sbin/vfs.zig index a5db7d5..9222dca 100644 --- a/sbin/vfs.zig +++ b/sbin/vfs.zig @@ -1,14 +1,136 @@ -//! /sbin/vfs — the user-space VFS server. Shipped in the initrd and spawned as a -//! ring-3 process. This is a placeholder that only heartbeats, proving the initrd -//! pipeline ships and spawns it; the real path namespace + IPC dispatch loop -//! (open/read/write/stat forwarded to driver processes) is built in M9. +//! /sbin/vfs — the user-space VFS server. Shipped in the initrd, spawned as a +//! ring-3 process, and reached by every other process through IPC (the `rt` +//! file API marshals open/read/write/stat/close into calls to this server's +//! 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 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 { + 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) { - _ = rt.sys.write("vfs: alive\n"); - rt.sys.sleep(1000); + const got = rt.ipc.replyWait(ep, reply_buf[0..reply_len], &recv); + // Ignore notifications (none expected here); handle a request. + reply_len = handle(recv[0..got.len], &reply_buf); } } diff --git a/sbin/vfstest.zig b/sbin/vfstest.zig new file mode 100644 index 0000000..b8da8ed --- /dev/null +++ b/sbin/vfstest.zig @@ -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; +} diff --git a/src/kernel/tests.zig b/src/kernel/tests.zig index 176d124..8a8e4fa 100644 --- a/src/kernel/tests.zig +++ b/src/kernel/tests.zig @@ -107,6 +107,8 @@ pub fn run(case: []const u8, boot_info: *const BootInfo) void { processTest(boot_info); } else if (eql(case, "initrd")) { initrdTest(boot_info); + } else if (eql(case, "vfs")) { + vfsTest(boot_info); } else if (eql(case, "poweroff")) { powerTest(.off); } else if (eql(case, "reboot")) { @@ -970,17 +972,60 @@ fn initrdTest(boot_info: *const BootInfo) void { } 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); const deadline = arch.millis() + 8000; while (process.write_count < 2 and arch.millis() < deadline) sched.yield(); sched.setPriority(4); - const prefix = "vfs: alive"; - const beat_ok = process.write_len >= prefix.len and eql(process.write_buf[0..prefix.len], prefix); - check("an initrd process heartbeats (>=2)", process.write_count >= 2); - check("heartbeat text arrived intact", beat_ok); - check("heartbeats came from user mode (CPL 3)", process.write_from_user); + check("initrd processes ran and made syscalls (>=2)", process.write_count >= 2); + check("syscalls came from user mode (CPL 3)", process.write_from_user); + result(); +} + +/// 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(); } diff --git a/test/qemu_test.py b/test/qemu_test.py index e44286b..e8b8bf0 100644 --- a/test/qemu_test.py +++ b/test/qemu_test.py @@ -183,6 +183,11 @@ CASES = [ {"name": "initrd", "expect": r"DANOS-TEST-RESULT: PASS", "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 # pre-transition marker; the FAIL line only appears if the transition didn't take. {"name": "poweroff",