summaryrefslogtreecommitdiff
path: root/src/fs.zig
diff options
context:
space:
mode:
Diffstat (limited to 'src/fs.zig')
-rw-r--r--src/fs.zig124
1 files changed, 103 insertions, 21 deletions
diff --git a/src/fs.zig b/src/fs.zig
index 5c75eb72..9200236d 100644
--- a/src/fs.zig
+++ b/src/fs.zig
@@ -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;