summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorGabriel Schneider <[email protected]>2026-09-29 13:36:33 -0300
committerGabriel Schneider <[email protected]>2026-10-01 00:12:16 -0300
commitfea3f2c5abf14aada5d9187b82985e51eb5b7a98 (patch)
tree6dd3c54c8aaa1ad5941a74200b18da12e512b7fb /src
parentbe5f559939fc11cbfba95b4a7d518816bec8677a (diff)
downloadpardes-fea3f2c5abf14aada5d9187b82985e51eb5b7a98.tar.gz
pardes-fea3f2c5abf14aada5d9187b82985e51eb5b7a98.zip
A path with a part over 255 bytes is refused, never a panic
std's statFile takes the kernel's ENAMETOOLONG for a bug (errnoBug), so a name, look, DumpDir or Save path with a part over 255 bytes panicked the editor (exec.kindOf via writeName, recentKeeps, dumpFailed). Every non-test statFile now goes through fs.statPath, which refuses such a name as NameTooLong first; fs.py drives long, looping, not-a-directory and not-ours paths through name, look, DumpDir, Dump and Save. Co-Authored-By: Claude Opus 5.5 <[email protected]>
Diffstat (limited to 'src')
-rw-r--r--src/9p_io.zig2
-rw-r--r--src/exec.zig4
-rw-r--r--src/file_watch.zig4
-rw-r--r--src/fs.zig17
-rw-r--r--src/host_io.zig6
5 files changed, 24 insertions, 9 deletions
diff --git a/src/9p_io.zig b/src/9p_io.zig
index 96f91bc1..560c4ed6 100644
--- a/src/9p_io.zig
+++ b/src/9p_io.zig
@@ -865,7 +865,7 @@ pub fn listen(io: std.Io, gpa: std.mem.Allocator, core: *pardes.Pardes, named: [
return null;
};
_ = l.runner.listen(.{ .unix = p }, max_conns) catch |err| retry: {
- const existing = std.Io.Dir.cwd().statFile(io, p, .{ .follow_symlinks = false }) catch null;
+ const existing = @import("fs.zig").statPath(io, p, .{ .follow_symlinks = false }) catch null;
if (err != error.AddressInUse or existing == null or existing.?.kind != .unix_domain_socket or alive(p)) {
log.warn("something is already listening on {s}", .{p});
l.deinit(gpa);
diff --git a/src/exec.zig b/src/exec.zig
index 88c0705d..4785e7a2 100644
--- a/src/exec.zig
+++ b/src/exec.zig
@@ -391,7 +391,7 @@ pub fn isDirectory(path: []const u8) bool {
const z = std.fmt.bufPrintSentinel(&buf, "{s}", .{path}, 0) catch return false;
pardes.turn.yield();
defer pardes.turn.back();
- const stat = std.Io.Dir.cwd().statFile(std.Io.Threaded.global_single_threaded.io(), z, .{}) catch return false;
+ const stat = @import("fs.zig").statPath(std.Io.Threaded.global_single_threaded.io(), z, .{}) catch return false;
return stat.kind == .directory;
}
@@ -407,7 +407,7 @@ pub fn kindOf(path: []const u8) ?std.Io.File.Kind {
const z = std.fmt.bufPrintSentinel(&buf, "{s}", .{path}, 0) catch return null;
pardes.turn.yield();
defer pardes.turn.back();
- const stat = std.Io.Dir.cwd().statFile(std.Io.Threaded.global_single_threaded.io(), z, .{}) catch return null;
+ const stat = @import("fs.zig").statPath(std.Io.Threaded.global_single_threaded.io(), z, .{}) catch return null;
return stat.kind;
}
diff --git a/src/file_watch.zig b/src/file_watch.zig
index 8bd9de8b..81f4ba87 100644
--- a/src/file_watch.zig
+++ b/src/file_watch.zig
@@ -397,7 +397,7 @@ fn watchPath(
// stat and a lookup out there: the turn goes out with them.
pardes.turn.yield();
defer pardes.turn.back();
- const stat = std.Io.Dir.cwd().statFile(std.Io.Threaded.global_single_threaded.io(), watched_path, .{}) catch null;
+ const stat = @import("fs.zig").statPath(std.Io.Threaded.global_single_threaded.io(), watched_path, .{}) catch null;
const dir = if (stat != null and stat.?.kind == .directory) watched_path else std.fs.path.dirname(watched_path) orelse ".";
var dir_buf: [4096:0]u8 = undefined;
if (dir.len >= dir_buf.len) return;
@@ -631,7 +631,7 @@ pub fn identify(io: std.Io, path: []const u8) !Identity {
const native = filesystem.localPath(path) orelse return error.NonLocalPath;
pardes.turn.yield();
defer pardes.turn.back();
- const stat = try std.Io.Dir.cwd().statFile(io, native, .{});
+ const stat = try @import("fs.zig").statPath(io, native, .{});
if (stat.kind != .file) return error.NotFile;
return .{
.inode = stat.inode,
diff --git a/src/fs.zig b/src/fs.zig
index 5fd1fd4e..753a4446 100644
--- a/src/fs.zig
+++ b/src/fs.zig
@@ -159,7 +159,7 @@ pub fn osHandle(p: *pardes.Pardes, req: Req) Reply {
const stat = stat: {
pardes.turn.yield();
defer pardes.turn.back();
- break :stat std.Io.Dir.cwd().statFile(io, path, .{}) catch return Reply.fail(req.tag, E.NOENT);
+ break :stat statPath(io, path, .{}) catch return Reply.fail(req.tag, E.NOENT);
};
const attr: Reply.Attr = .{ .name = if (req.node == os_root) "os" else std.fs.path.basename(path), .node = req.node, .dir = stat.kind == .directory, .size = stat.size, .mode = if (stat.kind == .directory) 0o755 else 0o644, .mtime = std.math.cast(u32, stat.mtime.toSeconds()) orelse 0 };
switch (req.op) {
@@ -622,6 +622,21 @@ pub fn isVirtual(path: []const u8) bool {
std.mem.eql(u8, path, "/n") or std.mem.startsWith(u8, path, "/n/");
}
+/// std's `statFile`, but a name with a part over 255 bytes is NameTooLong:
+/// std's own takes the kernel's ENAMETOOLONG for that for a bug and panics
+/// (Io.Threaded dirStatFileLinux), and such a name comes from anyone who
+/// writes one to `name`, `look` or DumpDir.
+pub fn statPath(io: std.Io, path: []const u8, options: std.Io.Dir.StatFileOptions) !std.Io.File.Stat {
+ var parts = std.mem.tokenizeScalar(u8, path, '/');
+ while (parts.next()) |part| if (part.len > 255) return error.NameTooLong;
+ return std.Io.Dir.cwd().statFile(io, path, options);
+}
+
+test "a path with a part over 255 bytes is NameTooLong, never a panic" {
+ const long = "/tmp/" ++ "x" ** 300 ++ "/f";
+ try std.testing.expectError(error.NameTooLong, statPath(std.testing.io, long, .{}));
+}
+
pub fn localPath(path: []const u8) ?[]const u8 {
if (std.mem.eql(u8, path, "/n/os")) return "/";
if (std.mem.startsWith(u8, path, "/n/os/")) return path[5..];
diff --git a/src/host_io.zig b/src/host_io.zig
index 8bbb0985..9d4b27c2 100644
--- a/src/host_io.zig
+++ b/src/host_io.zig
@@ -1048,7 +1048,7 @@ pub const Shell = struct {
var buf: [2048]u8 = undefined;
try std.testing.expectEqualStrings(bash_rc, readSmall(a_bash, &buf) orelse return error.ReadFailed);
try std.testing.expectEqualStrings(fish_rc, readSmall(a.fishPath().?, &buf) orelse return error.ReadFailed);
- const stat = try std.Io.Dir.cwd().statFile(std.testing.io, a_bash, .{});
+ const stat = try @import("fs.zig").statPath(std.testing.io, a_bash, .{});
try std.testing.expectEqual(0, stat.permissions.toMode() & 0o077);
var kept: [rc_path_capacity:0]u8 = @splat(0);
@memcpy(kept[0..a_bash.len], a_bash);
@@ -1202,7 +1202,7 @@ pub fn forkShell(
// A shell's directory may be inside a mount this editor serves.
pardes.turn.yield();
defer pardes.turn.back();
- const stat = try std.Io.Dir.cwd().statFile(std.Io.Threaded.global_single_threaded.io(), path, .{});
+ const stat = try @import("fs.zig").statPath(std.Io.Threaded.global_single_threaded.io(), path, .{});
if (stat.kind != .directory) return error.NotDir;
break :dir path;
};
@@ -1440,7 +1440,7 @@ pub fn shellCwd(pid: libc.pid_t, buf: []u8) ?[]const u8 {
// no directory to be named by, so the name it had stays, and a
// restart works there once it is back.
if (std.mem.endsWith(u8, link, " (deleted)")) {
- _ = std.Io.Dir.cwd().statFile(std.Io.Threaded.global_single_threaded.io(), link, .{}) catch return null;
+ _ = @import("fs.zig").statPath(std.Io.Threaded.global_single_threaded.io(), link, .{}) catch return null;
}
return link;
},