diff options
Diffstat (limited to 'src/fs.zig')
| -rw-r--r-- | src/fs.zig | 124 |
1 files changed, 103 insertions, 21 deletions
@@ -65,6 +65,9 @@ pub const WriteError = error{ }; pub fn writeFile(path: []const u8, bytes: []const u8) WriteError!void { + // No core here to answer from, so the only safe answer is no answer: an + // open(2) into our own mount is the call that never returns. + if (isOwnMount(path)) return error.OpenFailed; var pathbuf: [4096:0]u8 = undefined; if (path.len >= pathbuf.len) return error.PathTooLong; if (std.mem.indexOfScalar(u8, path, 0) != null) return error.OpenFailed; @@ -671,6 +674,9 @@ pub fn resolve(p: ?*pardes.Pardes, word: []const u8, cwd: []const u8, out: *[409 } if (std.mem.eql(u8, joined, "/virtual")) return resolveVirtual(p, "/", out); if (std.mem.startsWith(u8, joined, "/virtual/")) return resolveVirtual(p, joined[8..], out); + // This editor's own tree, seen through a mount: answer from the tree + // instead of walking out into the view and back in. + if (ownMountSuffix(joined)) |inner| return resolveVirtual(p, inner, out); if (resolveOs(joined, out)) |found| return found; if (resolveVirtual(p, joined, out)) |found| return found; if (resolveVirtual(p, word, out)) |found| return found; @@ -682,7 +688,9 @@ pub fn resolve(p: ?*pardes.Pardes, word: []const u8, cwd: []const u8, out: *[409 } /// The name this session is posted under, taken from its socket path. -var own_name_buf: [64]u8 = undefined; +/// As wide as a name a listener will accept, so there is no session whose +/// name is too long to recognise and therefore too long to protect. +var own_name_buf: [108]u8 = undefined; var own_name_len: usize = 0; pub fn noteOwnSocket(socket_path: []const u8) void { @@ -697,35 +705,91 @@ pub fn noteOwnSocket(socket_path: []const u8) void { own_name_len = name.len; } -/// Is this path inside this editor's OWN 9P tree, as a mount presents it? +/// What this path names inside this editor's OWN 9P tree, if it does: +/// `/mnt/9p/pardes/<me>/pane/3/body` -> `pane/3/body`, and the mount root +/// itself -> `/`. /// -/// Touching one from inside the editor deadlocks the session outright: the -/// realpath, the stat and the read all leave through the mount and come back -/// as 9P requests that only the editor's loop can answer, while that loop is -/// blocked making them. The filesystem then stops answering anybody. So the -/// answer has to come from the NAME, before any syscall -- the syscall is the -/// thing that never returns. Look at `/n/self/...` instead, which the editor -/// serves from memory without leaving the process. -pub fn isOwnMount(path: []const u8) bool { - if (own_name_len == 0) return false; +/// A mount is a VIEW of a tree this editor already holds. Going out through +/// the view to reach it deadlocks the session outright -- the realpath, the +/// stat and the read all leave through the mount and come back as 9P requests +/// only this editor's loop can answer, while that loop is blocked making them, +/// and the filesystem then stops answering anybody. So the view is recognised +/// by NAME, before any syscall (the syscall is the thing that never returns), +/// and the request is served from the tree directly. Same answer, no round +/// trip, and a session can drive itself through its own 9P namespace. +pub fn ownMountSuffix(path: []const u8) ?[]const u8 { + if (own_name_len == 0 or path.len == 0 or path[0] != '/') return null; + // Whole components, and the registry's own layout: a session is posted at + // `<runtime>/9p/pardes/<name>` and mounts group under `/mnt/9p/pardes/`. + // Matching a bare `/pardes/<name>` anywhere in the string would claim + // `~/src/pardes/<name>/README` -- an ordinary directory that happens to + // read like a mount -- and serve the tree's README over the real file. var buf: [own_name_buf.len + 16]u8 = undefined; - const needle = std.fmt.bufPrint(&buf, "/pardes/{s}/", .{own_name_buf[0..own_name_len]}) catch return false; - return std.mem.indexOf(u8, path, needle) != null; + const stem = std.fmt.bufPrint(&buf, "/9p/pardes/{s}", .{own_name_buf[0..own_name_len]}) catch return null; + var at: usize = 0; + while (std.mem.indexOfPos(u8, path, at, stem)) |found| : (at = found + 1) { + const rest = path[found + stem.len ..]; + if (rest.len != 0 and rest[0] != '/') continue; // a longer name that merely starts the same + if (rest.len <= 1) return "/"; + return rest[1..]; + } + return null; +} + +pub fn isOwnMount(path: []const u8) bool { + return ownMountSuffix(path) != null; } -test "a path inside this session's own mount is refused before any syscall" { +test "a path inside this session's own mount is answered from the tree, not through the mount" { noteOwnSocket("/run/user/1000/pardes-9p-demo.sock"); defer own_name_len = 0; - try std.testing.expect(isOwnMount("/mnt/9p/pardes/demo/index")); - try std.testing.expect(isOwnMount("/mnt/9p/pardes/demo/pane/new")); - try std.testing.expect(!isOwnMount("/mnt/9p/pardes/other/index")); - try std.testing.expect(!isOwnMount("/home/goblin/src/pardes/demo.zig")); + // What the path names inside the tree, with the mount prefix taken off. + try std.testing.expectEqualStrings("index", ownMountSuffix("/mnt/9p/pardes/demo/index").?); + try std.testing.expectEqualStrings("pane/new", ownMountSuffix("/mnt/9p/pardes/demo/pane/new").?); + try std.testing.expectEqualStrings("/", ownMountSuffix("/mnt/9p/pardes/demo").?); + try std.testing.expectEqualStrings("/", ownMountSuffix("/mnt/9p/pardes/demo/").?); + // The registry posts the session there too, so that spelling counts. + try std.testing.expectEqualStrings("index", ownMountSuffix("/run/user/1000/9p/pardes/demo/index").?); + // Another session's mount is somebody else's to answer, and a name that + // merely STARTS with ours is a different name. + try std.testing.expect(ownMountSuffix("/mnt/9p/pardes/other/index") == null); + try std.testing.expect(ownMountSuffix("/mnt/9p/pardes/demo2/index") == null); + // An ordinary directory that reads like a mount is an ordinary directory. + // Serving the tree here would hand back the tree's README for the file on + // disk, and -- before `write` learned the same trick -- write the tree's + // bytes over it. + try std.testing.expect(ownMountSuffix("/home/goblin/src/pardes/demo/README") == null); + try std.testing.expect(ownMountSuffix("/home/goblin/src/pardes/demo.zig") == null); + // Relative paths never name a mount: they are resolved against a cwd first. + try std.testing.expect(ownMountSuffix("mnt/9p/pardes/demo/index") == null); + + // The syscall is the thing that never returns, so it is never made. var out: [4096]u8 = undefined; try std.testing.expect(resolveOs("/mnt/9p/pardes/demo/index", &out) == null); - // A session with no listener has no name, so nothing is refused. + // A session with no listener has no name, so nothing is redirected. noteOwnSocket("/tmp/not-a-pardes-socket"); - try std.testing.expect(!isOwnMount("/mnt/9p/pardes/demo/index")); + try std.testing.expect(ownMountSuffix("/mnt/9p/pardes/demo/index") == null); +} + +test "reading this session's own mount returns what the tree holds" { + const p = try pardes.Pardes.init(std.testing.allocator, .{ .tty_only = true, .cols = 80, .rows = 24 }); + defer p.deinit(); + noteOwnSocket("/run/user/1000/pardes-9p-selfread.sock"); + defer own_name_len = 0; + + const direct = try read(p, "/n/self/index"); + defer p.gpa.free(direct); + const mounted = try read(p, "/mnt/9p/pardes/selfread/index"); + defer p.gpa.free(mounted); + try std.testing.expectEqualStrings(direct, mounted); + try std.testing.expect(direct.len > 0); + + // A write goes to the same tree the read came from, not out through the + // mount: `/index` is 0400 there, and an OS write would instead try to + // create a file under a directory that does not exist. + try std.testing.expectError(error.ReadOnlyFilesystem, write(p, "/mnt/9p/pardes/selfread/index", "nope\n")); + try std.testing.expectError(error.ReadOnlyFilesystem, write(p, "/n/self/index", "nope\n")); } pub fn resolveOs(path: []const u8, out: *[4096]u8) ?Resolved { @@ -754,7 +818,13 @@ pub fn read(p: *pardes.Pardes, path: []const u8) ![]u8 { } pub fn readLimit(p: *pardes.Pardes, path: []const u8, max_bytes: usize) ![]u8 { - if (isOwnMount(path)) return error.OwnMount; + // The same view, reached by a reader that never went through `resolve`. + // The rewritten path cannot contain the mount stem, so this recurses once. + if (ownMountSuffix(path)) |inner| { + var self_buf: [4096]u8 = undefined; + const internal = std.fmt.bufPrint(&self_buf, "/n/self/{s}", .{inner}) catch return error.FileTooLarge; + return readLimit(p, internal, max_bytes); + } const limit = @min(max_bytes, limits.max_file_bytes); if (std.mem.eql(u8, std.mem.trimEnd(u8, path, "/"), "/n")) { var out: std.Io.Writer.Allocating = .init(p.gpa); @@ -871,6 +941,14 @@ fn readNode(p: *pardes.Pardes, initial_node: u64, path: []const u8, limit: usize } pub fn write(p: *pardes.Pardes, path: []const u8, bytes: []const u8) !void { + // The same view `readLimit` answers from the tree, so a write lands where + // the matching read came from -- and, just as importantly, never becomes + // an open(2) through a mount this loop is the one that answers. + if (ownMountSuffix(path)) |inner| { + var self_buf: [4096]u8 = undefined; + const internal = std.fmt.bufPrint(&self_buf, "/n/self/{s}", .{inner}) catch return error.PathTooLong; + return write(p, internal, bytes); + } if (std.mem.eql(u8, std.mem.trimEnd(u8, path, "/"), "/n")) return error.IsDirectory; var self_path: ?[]const u8 = null; if (std.mem.eql(u8, path, "/virtual")) self_path = ""; @@ -1233,6 +1311,10 @@ test "Restore prefers default directory then falls back to original path" { fn readFileLimit(gpa: std.mem.Allocator, path: []const u8, limit: usize) ![]u8 { if (std.mem.indexOfScalar(u8, path, 0) != null) return error.OpenFailed; + // Same reason as `writeFile`: no core to answer from here, and the open is + // the call that never returns. Callers holding one use `readLimit`, which + // serves the tree instead. + if (isOwnMount(path)) return error.FileNotFound; if (!platform_has_fs or std.mem.startsWith(u8, path, "/virtual/")) { const archive_path = if (std.mem.startsWith(u8, path, "/virtual/")) path[9..] else path; var normalized_buf: [4096]u8 = undefined; |
