From 3e9f8805f293f622bb885cf849b5ce47dc062ad1 Mon Sep 17 00:00:00 2001 From: Gabriel Schneider Date: Sat, 19 Sep 2026 23:55:47 -0300 Subject: 9ns: --name and /mnt/9p/ mounts, qid.path as inode number, interrupts as Tflush - --name NAME (default derived from the transport: socket basename, tcp-IP-PORT, spawned command, fdN) mounts at /mnt/9p/; --mount still overrides. ensureMountpoint walks down and creates missing components, shadowing the deepest unwritable ancestor. NINE_MOUNT is the only exported variable. - The inode number reported to the kernel is the 9P qid.path for every node, root included; a server handing qid.path 1 to a file (Pardes /self) no longer collides with the root. - FUSE_INTERRUPT for the request in flight becomes Tflush; a blocked read returns EINTR when the server answers the flush, chunked transfers return short counts, other requests arriving meanwhile are stashed and served next. Servers ignoring Tflush still block until they answer. - 9ns-test now covers nine/bridge/fuse; new adv_bridge_interrupt suite (28); 9ns-itest grows to 88 checks. Co-Authored-By: Claude Fable 5.1 --- 9ns/src/ns.zig | 83 +++++++++++++++++++++++++++++++++++++++++++++++----------- 1 file changed, 67 insertions(+), 16 deletions(-) (limited to '9ns/src/ns.zig') diff --git a/9ns/src/ns.zig b/9ns/src/ns.zig index 6da2c7f..b4342e5 100644 --- a/9ns/src/ns.zig +++ b/9ns/src/ns.zig @@ -198,25 +198,47 @@ fn buildArgv(gpa: Allocator, argv: []const []const u8) ![:null]?[*:0]const u8 { /// Make sure `path` is a directory, inside the *current* mount namespace: /// /// * already a directory → done; -/// * else `mkdir`; on `EACCES`/`EPERM`/`EROFS` shadow the parent directory -/// with a tmpfs that re-exposes every existing entry (bind mounts for -/// directories and files, recreated symlinks) and `mkdir` inside it; +/// * else find the deepest existing ancestor and `mkdir` the missing +/// components under it one by one (`mkdir -p`); the first of them failing +/// with `EACCES`/`EPERM`/`EROFS` (the normal case for `/mnt/9p/` as +/// a plain user) means **shadow that ancestor**: mount a `tmpfs` over it +/// that re-exposes every existing entry (bind mounts for directories and +/// files, recreated symlinks), then create the missing components inside; /// * anything else fails with the errno and a hint. /// +/// So `/mnt/9p/x` on a host without `/mnt/9p` shadows `/mnt` and creates +/// `9p/x`; with a root-owned `/mnt/9p` it shadows `/mnt/9p`; inside a 9ns +/// namespace, where `/mnt/9p` is ours, it just creates `x`. `/` and `/proc` +/// are never shadowed, nor a directory with more than `max_shadow_entries`. +/// /// Every failure prints `9ns: : E` to stderr before /// returning. Meant to be called in the child of `spawn` (or from a /// throwaway namespace: `unshare -Urm`). pub fn ensureMountpoint(gpa: Allocator, path: [:0]const u8) !void { - if (fileType(linux.AT.FDCWD, path, false)) |ft| { + if (try existingKind(path)) |ft| { if (ft == .dir) return; std.debug.print("9ns: mountpoint {s}: exists but is not a directory\n", .{path}); return error.Mountpoint; } - if (fileType(linux.AT.FDCWD, path, true) == .symlink) { - std.debug.print("9ns: mountpoint {s}: dangling symlink\n", .{path}); - return error.Mountpoint; + // Deepest existing ancestor: walk up until something is there. + var base: []const u8 = path; + while (true) { + base = std.fs.path.dirname(base) orelse "/"; + var base_buf: [path_max]u8 = undefined; + const base_z = std.fmt.bufPrintZ(&base_buf, "{s}", .{base}) catch { + std.debug.print("9ns: mountpoint {s}: path too long\n", .{path}); + return error.Mountpoint; + }; + const kind = try existingKind(base_z) orelse continue; + if (kind != .dir) { + std.debug.print("9ns: mountpoint {s}: {s} is not a directory\n", .{ path, base }); + return error.Mountpoint; + } + break; } - const mk = linux.errno(linux.mkdirat(linux.AT.FDCWD, path, 0o755)); + // Missing components, deepest ancestor first. + const missing = path[base.len..]; + const mk = mkdirComponents(path, base.len, missing); switch (mk) { .SUCCESS => return, .ACCES, .PERM, .ROFS => {}, @@ -225,21 +247,20 @@ pub fn ensureMountpoint(gpa: Allocator, path: [:0]const u8) !void { return error.Mountpoint; }, } - const parent = std.fs.path.dirname(path) orelse "/"; - if (std.mem.eql(u8, parent, "/") or isSameDirectory(parent, "/")) { + if (std.mem.eql(u8, base, "/") or isSameDirectory(base, "/")) { std.debug.print("9ns: mkdir {s}: E{t}; refusing to shadow / (pass --mount an existing directory)\n", .{ path, mk }); return error.Mountpoint; } // The shadow rebuilds entries from /proc/self/fd//; a tmpfs // over /proc (or a subtree of it) would take that away from itself. - if (std.mem.eql(u8, parent, "/proc") or std.mem.startsWith(u8, parent, "/proc/")) { - std.debug.print("9ns: mkdir {s}: E{t}; refusing to shadow {s} (pass --mount an existing directory)\n", .{ path, mk, parent }); + if (std.mem.eql(u8, base, "/proc") or std.mem.startsWith(u8, base, "/proc/")) { + std.debug.print("9ns: mkdir {s}: E{t}; refusing to shadow {s} (pass --mount an existing directory)\n", .{ path, mk, base }); return error.Mountpoint; } - const parent_z = try gpa.dupeZ(u8, parent); - defer gpa.free(parent_z); - try shadowDirectory(gpa, parent_z); - switch (linux.errno(linux.mkdirat(linux.AT.FDCWD, path, 0o755))) { + const base_z = try gpa.dupeZ(u8, base); + defer gpa.free(base_z); + try shadowDirectory(gpa, base_z); + switch (mkdirComponents(path, base.len, missing)) { .SUCCESS => {}, else => |e| { std.debug.print("9ns: mkdir {s} (in shadow tmpfs): E{t}\n", .{ path, e }); @@ -248,6 +269,36 @@ pub fn ensureMountpoint(gpa: Allocator, path: [:0]const u8) !void { } } +/// What `path` is, following symlinks: null when nothing is there; an +/// error (reported) for a dangling symlink. +fn existingKind(path: [*:0]const u8) !?FileType { + if (fileType(linux.AT.FDCWD, path, false)) |ft| return ft; + if (fileType(linux.AT.FDCWD, path, true) == .symlink) { + std.debug.print("9ns: mountpoint {s}: dangling symlink\n", .{std.mem.span(path)}); + return error.Mountpoint; + } + return null; +} + +/// `mkdir` each component of `missing` (which is `path[base_len..]`, so +/// it starts with '/') under the existing prefix `path[0..base_len]`, in +/// order. Returns the errno of the first failure (`.SUCCESS` when all were +/// created); `EEXIST` on a component is fine (another process, or a retry). +fn mkdirComponents(path: []const u8, base_len: usize, missing: []const u8) E { + var buf: [path_max]u8 = undefined; + var end: usize = base_len; + var it = std.mem.tokenizeScalar(u8, missing, '/'); + while (it.next()) |comp| { + end += 1 + comp.len; + const prefix = std.fmt.bufPrintZ(&buf, "{s}", .{path[0..end]}) catch return .NAMETOOLONG; + switch (linux.errno(linux.mkdirat(linux.AT.FDCWD, prefix, 0o755))) { + .SUCCESS, .EXIST => {}, + else => |e| return e, + } + } + return .SUCCESS; +} + const FileType = enum { dir, symlink, other }; /// True when both paths resolve (following symlinks, including magic ones -- cgit v1.3