From 704af30412063c5efd61d8d54f843bf7fb750811 Mon Sep 17 00:00:00 2001 From: Gabriel Schneider Date: Wed, 30 Sep 2026 10:13:10 -0300 Subject: A socket path too long for sun_path leaves no listener and the editor's turn untouched: the GUI no longer calls into a freed one at startup listen started the editor's turn and pointed its answer_held and wake_parked hooks at the new Listener, then, when socketPath found the path too long for sun_path (108 bytes), freed the Listener with gpa.destroy while the hooks stayed set; the GUI's next rest (waitInput) called answerHeld on freed memory and died with SIGSEGV. The lapis bench's runs crashed for 12-character names in an 82-byte runtime directory (a 110-byte path) and not for shorter ones. The path is checked before anything is made; a unit test asks listen for a name too long and finds the turn's hooks clear and the turn not started. Co-Authored-By: Claude Opus 5.5 --- src/9p_io.zig | 45 ++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 40 insertions(+), 5 deletions(-) (limited to 'src') diff --git a/src/9p_io.zig b/src/9p_io.zig index 78ff5d59..d1b2099c 100644 --- a/src/9p_io.zig +++ b/src/9p_io.zig @@ -851,6 +851,16 @@ pub fn listen(io: std.Io, gpa: std.mem.Allocator, core: *pardes.Pardes, named: [ return null; }; if (!ensureSocketDir(dir)) return null; + const entry_name = if (named.len != 0) named else fallback; + // Checked before anything is made: the turn's hooks point at the + // listener from here on, and a path too long for sun_path used to free + // it with them still set, so the editor's next rest called into freed + // memory. + var path_check: [sun_path_len]u8 = undefined; + if (socketPath(&path_check, dir, entry_name) == null) { + log.warn("no socket for {s}: the path is too long for a unix socket", .{entry_name}); + return null; + } const l = gpa.create(Listener) catch return null; l.* = .{ .io = io, .core = core }; pardes.turn.start(io); @@ -863,11 +873,7 @@ pub fn listen(io: std.Io, gpa: std.mem.Allocator, core: *pardes.Pardes, named: [ .handler = .{ .ctx = l, .serve = Listener.onServe, .opened = Listener.onOpened, .refused = Listener.onRefused }, .greet_timeout_ms = Listener.greet_deadline_ms, }); - const entry_name = if (named.len != 0) named else fallback; - const p = socketPath(&l.path_buf, dir, entry_name) orelse { - gpa.destroy(l); - return null; - }; + const p = socketPath(&l.path_buf, dir, entry_name).?; _ = l.runner.listen(.{ .unix = p }, max_conns) catch |err| retry: { 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)) { @@ -1261,6 +1267,35 @@ test "TCP addresses are numeric and normalize mapped IPv4" { extern "c" fn mkdtemp(template: [*:0]u8) ?[*:0]u8; extern "c" fn rmdir(path: [*:0]const u8) c_int; +test "a socket path too long for sun_path is no listener, and leaves the editor's turn as it was" { + if (comptime !supported) return error.SkipZigTest; + const p = try pardes.Pardes.init(testing.allocator, .{ .tty_only = true }); + defer p.deinit(); + var directory: [64:0]u8 = undefined; + _ = try std.fmt.bufPrintSentinel(&directory, "/tmp/pardes-long-XXXXXX", .{}, 0); + if (mkdtemp(&directory) == null) return error.TempDirectoryFailed; + const dir = std.mem.span(@as([*:0]const u8, &directory)); + defer _ = rmdir(&directory); + // A name that fits: the directory is one a listener takes. + var fits: [sun_path_len]u8 = undefined; + try testing.expect(socketPath(&fits, dir, "short") != null); + const old = libc.getenv("XDG_RUNTIME_DIR"); + var old_buf: [4096]u8 = undefined; + const old_copy = if (old) |o| try std.fmt.bufPrintSentinel(&old_buf, "{s}", .{std.mem.span(o)}, 0) else null; + var z: [4096]u8 = undefined; + _ = setenv("XDG_RUNTIME_DIR", try std.fmt.bufPrintSentinel(&z, "{s}", .{dir}, 0), 1); + defer if (old_copy) |o| { + _ = setenv("XDG_RUNTIME_DIR", o, 1); + } else { + _ = unsetenv("XDG_RUNTIME_DIR"); + }; + const long = "n" ** 120; + try testing.expect(listen(testing.io, testing.allocator, p, long, "", null, null) == null); + try testing.expect(pardes.turn.answer_held == null); + try testing.expect(pardes.turn.wake_parked == null); + try testing.expect(pardes.turn.io == null); +} + test "a listening editor posts itself into the 9P registry and unposts on stop" { if (comptime !supported) return error.SkipZigTest; const gpa = testing.allocator; -- cgit v1.3