From fea3f2c5abf14aada5d9187b82985e51eb5b7a98 Mon Sep 17 00:00:00 2001 From: Gabriel Schneider Date: Tue, 29 Sep 2026 13:36:33 -0300 Subject: 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 --- src/9p_io.zig | 2 +- src/exec.zig | 4 ++-- src/file_watch.zig | 4 ++-- src/fs.zig | 17 ++++++++++++++++- src/host_io.zig | 6 +++--- 5 files changed, 24 insertions(+), 9 deletions(-) (limited to 'src') 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; }, -- cgit v1.3