c3a7ad58
fix: the Darwin arms answer ENOTCONN as a gone peer, not a panic
a73x 2026-09-04 10:16
Commit message
src/os/client_os_macos.zig
| Old | New | ||
|---|---|---|---|
| @@ -58,7 +58,38 @@ pub fn sendNoSig(fd: std.posix.socket_t, bytes: []const u8) std.posix.SendError! | |||
| 58 | if (rc != 0 and std.posix.errno(rc) == .INVAL) return error.BrokenPipe; | 58 | if (rc != 0 and std.posix.errno(rc) == .INVAL) return error.BrokenPipe; |
| 59 | // Flags 0, not DONTWAIT: this side BLOCKS. The root's doc says why — | 59 | // Flags 0, not DONTWAIT: this side BLOCKS. The root's doc says why — |
| 60 | // the agent probe writes five bytes and then polls for the answer. | 60 | // the agent probe writes five bytes and then polls for the answer. |
| 61 | return std.posix.send(fd, bytes, 0); | 61 | return std.posix.sendto(fd, bytes, 0, null, 0) catch |err| switch (err) { |
| 62 | // Darwin's THIRD spelling of "the peer is gone", and the one that | ||
| 63 | // aborts the process instead of being an error. `std.posix.send` maps | ||
| 64 | // ENOTCONN to `unreachable`, because for a local fd it can only mean | ||
| 65 | // the caller passed something unconnected; Darwin also uses it for the | ||
| 66 | // FAR end, for a peer that has begun closing but not finished. A | ||
| 67 | // moment earlier the socket still takes SO_NOSIGPIPE, a moment later | ||
| 68 | // it answers EPIPE, and in between it answers this. | ||
| 69 | // | ||
| 70 | // Seen once in a Mac `make check` on 2026-09-04, as `attempt to unwrap | ||
| 71 | // error: SocketNotConnected` out of this call, in the probe that asks | ||
| 72 | // whether an ssh agent is listening. It is a race and does not | ||
| 73 | // reproduce on demand: measured against a peer that had closed and | ||
| 74 | // settled, all three shapes — accepted then closed, closed with no | ||
| 75 | // delay, never accepted — answer EPIPE on both systems. `sendto` | ||
| 76 | // RETURNS the error where `send` unwraps it, which is why this arm | ||
| 77 | // goes through it directly and answers the contract's BrokenPipe. | ||
| 78 | error.SocketNotConnected => return error.BrokenPipe, | ||
| 79 | // Only a `sendto` carrying an ADDRESS can raise these, and this one | ||
| 80 | // passes null. `std.posix.send` calls them unreachable for the same | ||
| 81 | // reason. | ||
| 82 | error.AddressFamilyNotSupported, | ||
| 83 | error.SymLinkLoop, | ||
| 84 | error.NameTooLong, | ||
| 85 | error.FileNotFound, | ||
| 86 | error.NotDir, | ||
| 87 | error.NetworkUnreachable, | ||
| 88 | error.AddressNotAvailable, | ||
| 89 | error.UnreachableAddress, | ||
| 90 | => unreachable, | ||
| 91 | else => |e| return e, | ||
| 92 | }; | ||
| 62 | } | 93 | } |
| 63 | 94 | ||
| 64 | /// sysctl KERN_PROC_PID: the kernel's own record of the process, the Darwin | 95 | /// sysctl KERN_PROC_PID: the kernel's own record of the process, the Darwin |
src/os/server_os_macos.zig
| Old | New | ||
|---|---|---|---|
| @@ -89,7 +89,41 @@ pub fn sendNoSigNoWait(fd: std.posix.socket_t, bytes: []const u8) std.posix.Send | |||
| 89 | if (fl & nb == 0) _ = std.posix.fcntl(fd, std.posix.F.SETFL, fl | nb) catch {}; | 89 | if (fl & nb == 0) _ = std.posix.fcntl(fd, std.posix.F.SETFL, fl | nb) catch {}; |
| 90 | } else |_| {} | 90 | } else |_| {} |
| 91 | 91 | ||
| 92 | return std.posix.send(fd, bytes, std.posix.MSG.DONTWAIT); | 92 | return std.posix.sendto(fd, bytes, std.posix.MSG.DONTWAIT, null, 0) catch |err| switch (err) { |
| 93 | // Darwin's THIRD spelling of "the peer is gone", and the one that | ||
| 94 | // aborts the process instead of being an error. `std.posix.send` maps | ||
| 95 | // ENOTCONN to `unreachable`, because for a local fd it can only mean | ||
| 96 | // the caller passed something unconnected; Darwin also uses it for the | ||
| 97 | // FAR end, for a peer that has begun closing but not finished. A | ||
| 98 | // moment earlier the socket still takes SO_NOSIGPIPE, a moment later | ||
| 99 | // it answers EPIPE, and in between it answers this. | ||
| 100 | // | ||
| 101 | // Found by READING this arm, not by a trace out of it: the abort was | ||
| 102 | // caught on 2026-09-04 in the CLIENT arm, out of the probe that asks | ||
| 103 | // whether an ssh agent is listening (`client_os_macos.sendNoSig` says | ||
| 104 | // so, and quotes it). This arm reaches `send` the same way, so it had | ||
| 105 | // the same hole — and on the daemon's only pump, where an abort takes | ||
| 106 | // every session with it rather than one probe. It is a race and does | ||
| 107 | // not reproduce on demand: measured against a peer that had closed | ||
| 108 | // and settled, all three shapes — accepted then closed, closed with | ||
| 109 | // no delay, never accepted — answer EPIPE on both systems. `sendto` | ||
| 110 | // RETURNS the error where `send` unwraps it, which is why this arm | ||
| 111 | // goes through it directly and answers the contract's BrokenPipe. | ||
| 112 | error.SocketNotConnected => return error.BrokenPipe, | ||
| 113 | // Only a `sendto` carrying an ADDRESS can raise these, and this one | ||
| 114 | // passes null. `std.posix.send` calls them unreachable for the same | ||
| 115 | // reason. | ||
| 116 | error.AddressFamilyNotSupported, | ||
| 117 | error.SymLinkLoop, | ||
| 118 | error.NameTooLong, | ||
| 119 | error.FileNotFound, | ||
| 120 | error.NotDir, | ||
| 121 | error.NetworkUnreachable, | ||
| 122 | error.AddressNotAvailable, | ||
| 123 | error.UnreachableAddress, | ||
| 124 | => unreachable, | ||
| 125 | else => |e| return e, | ||
| 126 | }; | ||
| 93 | } | 127 | } |
| 94 | 128 | ||
| 95 | pub fn sockType(fd: std.posix.fd_t) error{NotASocket}!u32 { | 129 | pub fn sockType(fd: std.posix.fd_t) error{NotASocket}!u32 { |