From 52f3e3615f77f690739c0a3c30ff400a2223ed34 Mon Sep 17 00:00:00 2001 From: Gabriel Schneider Date: Wed, 30 Sep 2026 15:14:00 -0300 Subject: The 9P socket appears already listening: it is listened on under a name of its own and renamed into place A client that waits for the socket file and then connects, as fs.py's session helper and scripts do, was sometimes refused under load. The listener bound the socket at its final name, so the file existed a moment before listen(2), and a connect in that window got ECONNREFUSED. Now it listens under `.`, is chmodded, then renamed over the final name, so that name only ever names a listening socket. A final name held by a live listener is still refused, and a name with no room for the suffix is bound in place as before. The registry test checks the socket is listening where it appears and that the temporary name is gone. Co-Authored-By: Claude Opus 5.5 --- src/9p_io.zig | 29 ++++++++++++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) (limited to 'src/9p_io.zig') diff --git a/src/9p_io.zig b/src/9p_io.zig index 0eaa2d7e..e475416e 100644 --- a/src/9p_io.zig +++ b/src/9p_io.zig @@ -874,7 +874,29 @@ pub fn listen(io: std.Io, gpa: std.mem.Allocator, core: *pardes.Pardes, named: [ .greet_timeout_ms = Listener.greet_deadline_ms, }); const p = socketPath(&l.path_buf, dir, entry_name).?; - _ = l.runner.listen(.{ .unix = p }, max_conns) catch |err| retry: { + // Listened on under a name of its own, then renamed into place: a client + // that waits for the socket to appear finds it listening already. Bound + // at its own name, it was there a moment before listen(2), and a connect + // then was refused. (No room for the longer name: bound in place.) + var tmp_buf: [sun_path_len]u8 = undefined; + if (std.fmt.bufPrintSentinel(&tmp_buf, "{s}.{d}", .{ p, libc.getpid() }, 0) catch null) |tmp| { + const existing = @import("fs.zig").statPath(io, p, .{ .follow_symlinks = false }) catch null; + if (existing != null and (existing.?.kind != .unix_domain_socket or alive(p))) { + log.warn("something is already listening on {s}", .{p}); + l.deinit(gpa); + return null; + } + _ = libc.unlink(tmp); + _ = l.runner.listen(.{ .unix = tmp }, max_conns) catch { + l.deinit(gpa); + return null; + }; + if (libc.chmod(tmp, 0o600) != 0 or libc.rename(tmp, p) != 0) { + _ = libc.unlink(tmp); + l.deinit(gpa); + return null; + } + } else _ = 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)) { log.warn("something is already listening on {s}", .{p}); @@ -1338,6 +1360,11 @@ test "a listening editor posts itself into the 9P registry and unposts on stop" // The socket stays exactly where pardes has always bound it: // adopting the registry moves nothing, it only advertises. try testing.expect(statNoFollow(sock) != null); + // It appeared already listening, renamed into place from a name of + // its own, which is gone. + try testing.expect(alive(sock)); + var tmp_buf: [sun_path_len:0]u8 = undefined; + try testing.expect(statNoFollow(try std.fmt.bufPrintSentinel(&tmp_buf, "{s}.{d}", .{ sock, libc.getpid() }, 0)) == null); // And the registry holds a symlink to it one directory down, so // several editors group under /mnt/9p/pardes/ instead of // crowding the registry root — the layout zmx posts its -- cgit v1.3