src/os/server_os_macos.zig
Ref: Size: 11.1 KiB History
//! Darwin arm of `server_os`. Spellings only; the contract is in the root.
//! Four operations use a different MECHANISM rather than a different
//! spelling, because the Linux one does not exist here, and each is
//! recorded in docs/decisions.md (2026-09-03, "the daemon's Darwin arm"):
//! `closeFrom` walks the fd table because there is no close_range,
//! `anonFd` is an unlinked mkstemp file because there is no memfd,
//! `sendNoSigNoWait` sets SO_NOSIGPIPE on the socket because there is no
//! MSG_NOSIGNAL and sets O_NONBLOCK on the fd because MSG_DONTWAIT does not
//! reach xnu's wait for buffer space, and `peerCred` takes two calls because
//! LOCAL_PEERCRED answers no pid.
const std = @import("std");
const root = @import("server_os.zig");
const c = @cImport({
@cInclude("util.h"); // forkpty
@cInclude("sys/ioctl.h");
@cInclude("sys/socket.h");
@cInclude("sys/un.h"); // LOCAL_PEERPID
@cInclude("unistd.h"); // getpeereid, getdtablesize
@cInclude("stdlib.h"); // mkstemp
});
pub fn getpid() std.posix.pid_t {
return std.c.getpid();
}
pub fn peerCred(fd: std.posix.socket_t) ?root.PeerCred {
// Two calls where Linux has one: Darwin's LOCAL_PEERCRED answers a
// `struct xucred` with no pid in it, so the uid comes from getpeereid
// and the pid from a socket option of its own.
var uid: c.uid_t = undefined;
var gid: c.gid_t = undefined;
if (c.getpeereid(fd, &uid, &gid) != 0) return null;
var pid: c.pid_t = 0;
var len: c.socklen_t = @sizeOf(c.pid_t);
// SOL_LOCAL is 0 on Darwin; LOCAL_PEERPID answers the peer's pid for a
// unix socket the way SO_PEERCRED's pid field does on Linux. The root
// rejects a non-positive pid, so a kernel that will not name the peer
// reads as "will not say" there rather than as a pid of 0 here.
if (c.getsockopt(fd, 0, c.LOCAL_PEERPID, &pid, &len) != 0) return null;
return .{ .uid = @intCast(uid), .pid = @intCast(pid) };
}
pub fn sendNoSigNoWait(fd: std.posix.socket_t, bytes: []const u8) std.posix.SendError!usize {
// Darwin has no MSG_NOSIGNAL: the "do not raise SIGPIPE" bit is a
// property of the SOCKET, not of the send. Set per call rather than once
// at accept, because every fd that reaches this operation must have it
// and there is no one place every such fd is created — an accepted
// client, a socketpair end, an adopted fd across an upgrade. The option
// is idempotent and costs one setsockopt on a path that is already a
// syscall.
//
// A REFUSED set is the interesting case and must not fall through to
// send. Measured 2026-09-03 on macOS 26: Darwin's `sosetopt` rejects
// every socket option with EINVAL once a socket is shut down in both
// directions, which is exactly the state a hung-up peer leaves behind —
// so the one send that would raise the signal is also the one send the
// flag cannot be set for. EINVAL here is therefore not an argument
// complaint (the level, name, value and length are all fixed above);
// it is the kernel saying the peer is gone, which is what `send` would
// have answered had it not signalled first. Every other setsockopt
// failure — a bad fd, not a socket — describes a socket that cannot
// raise SIGPIPE either, so those fall through and let `send` name them.
const on: c_int = 1;
const rc = c.setsockopt(fd, c.SOL_SOCKET, c.SO_NOSIGPIPE, &on, @sizeOf(c_int));
if (rc != 0 and std.posix.errno(rc) == .INVAL) return error.BrokenPipe;
// The NoWait half is the fd's, not the send's. MSG_DONTWAIT exists on
// Darwin and the name suggests it covers this, but xnu only consults it
// when it takes the socket buffer lock. The wait for buffer SPACE, a
// little further into sosend, tests the socket's own SS_NBIO bit — which
// is O_NONBLOCK on the file descriptor and nothing the flags argument can
// reach. So a send with MSG_DONTWAIT on a blocking fd whose peer has
// stopped reading sleeps in the kernel until the peer drains, which is
// exactly the stall this operation promises the daemon it will never
// take. Linux honours the flag and needs none of this.
//
// Make it true rather than assume it: read the flags and add O_NONBLOCK
// when it is missing. It is a no-op for every fd the daemon owns
// (`Server.setNonblocking` sets it on each accepted client, and a `Sink`
// socket fd only ever comes from that accept), so the cost is one fcntl
// on a path that already makes two syscalls, and the fds that are not the
// daemon's — a socketpair a test or a future caller hands in — get the
// guarantee the name makes instead of a hang. The change is sticky, which
// is correct: an fd this operation may be called on must never block.
// An fd fcntl refuses is a bad fd or not a socket, and `send` names that
// better than a swallowed fcntl error would.
if (std.posix.fcntl(fd, std.posix.F.GETFL, 0)) |fl| {
const nb: u32 = @bitCast(std.posix.O{ .NONBLOCK = true });
if (fl & nb == 0) _ = std.posix.fcntl(fd, std.posix.F.SETFL, fl | nb) catch {};
} else |_| {}
return std.posix.sendto(fd, bytes, std.posix.MSG.DONTWAIT, null, 0) catch |err| switch (err) {
// Darwin's THIRD spelling of "the peer is gone", and the one that
// aborts the process instead of being an error. `std.posix.send` maps
// ENOTCONN to `unreachable`, because for a local fd it can only mean
// the caller passed something unconnected; Darwin also uses it for the
// FAR end, for a peer that has begun closing but not finished. A
// moment earlier the socket still takes SO_NOSIGPIPE, a moment later
// it answers EPIPE, and in between it answers this.
//
// Found by READING this arm, not by a trace out of it: the abort was
// caught on 2026-09-04 in the CLIENT arm, out of the probe that asks
// whether an ssh agent is listening (`client_os_macos.sendNoSig` says
// so, and quotes it). This arm reaches `send` the same way, so it had
// the same hole — and on the daemon's only pump, where an abort takes
// every session with it rather than one probe. It is a race and does
// not reproduce on demand: measured against a peer that had closed
// and settled, all three shapes — accepted then closed, closed with
// no delay, never accepted — answer EPIPE on both systems. `sendto`
// RETURNS the error where `send` unwraps it, which is why this arm
// goes through it directly and answers the contract's BrokenPipe.
error.SocketNotConnected => return error.BrokenPipe,
// Only a `sendto` carrying an ADDRESS can raise these, and this one
// passes null. `std.posix.send` calls them unreachable for the same
// reason.
error.AddressFamilyNotSupported,
error.SymLinkLoop,
error.NameTooLong,
error.FileNotFound,
error.NotDir,
error.NetworkUnreachable,
error.AddressNotAvailable,
error.UnreachableAddress,
=> unreachable,
else => |e| return e,
};
}
pub fn sockType(fd: std.posix.fd_t) error{NotASocket}!u32 {
var t: c_int = undefined;
var len: c.socklen_t = @sizeOf(c_int);
if (c.getsockopt(fd, c.SOL_SOCKET, c.SO_TYPE, &t, &len) != 0) return error.NotASocket;
return @intCast(t);
}
pub fn forkPty(ws: root.Winsize) error{ForkPtyFailed}!root.ForkedPty {
var master: c_int = undefined;
var cws: c.struct_winsize = .{ .ws_row = ws.row, .ws_col = ws.col, .ws_xpixel = 0, .ws_ypixel = 0 };
const pid = c.forkpty(&master, null, null, &cws);
if (pid < 0) return error.ForkPtyFailed;
return .{ .pid = pid, .master = master };
}
pub fn exitNow(code: u8) noreturn {
// `_exit(2)` and not `exit(3)`: the root's doc says why — atexit and the
// stdio flush would write the parent's pending bytes a second time.
std.c._exit(code);
}
pub fn forkDetached(
exe: [*:0]const u8,
argv: [*:null]const ?[*:0]const u8,
stdin_fd: std.posix.fd_t,
out_fd: std.posix.fd_t,
) error{ForkFailed}!std.posix.pid_t {
const pid = std.posix.fork() catch return error.ForkFailed;
if (pid != 0) return pid;
_ = std.c.setsid();
std.posix.dup2(stdin_fd, std.posix.STDIN_FILENO) catch exitNow(127);
std.posix.dup2(out_fd, std.posix.STDOUT_FILENO) catch exitNow(127);
std.posix.dup2(out_fd, std.posix.STDERR_FILENO) catch exitNow(127);
std.posix.execveZ(exe, argv, std.c.environ) catch exitNow(127);
unreachable;
}
pub fn closeFrom(first: std.posix.fd_t) void {
// No close_range on Darwin: one close per slot up to the table size,
// which is a few hundred cheap EBADFs once per session start. Between
// fork and exec, so nothing else is opening fds underneath the walk.
// `getdtablesize` is the soft RLIMIT_NOFILE, which is also the ceiling
// on any fd this process could be holding, so the walk cannot miss one.
var fd: std.posix.fd_t = first;
const top: std.posix.fd_t = c.getdtablesize();
while (fd < top) : (fd += 1) _ = std.c.close(fd);
}
pub fn ptyMode(master: std.posix.fd_t) std.posix.TermiosGetError!root.PtyMode {
// Measured 2026-09-03: Darwin's master answers tcgetattr for the slave's
// line discipline, so this is the Linux shape and not a reopen by name.
const t = try std.posix.tcgetattr(master);
return .{ .icanon = t.lflag.ICANON, .echo = t.lflag.ECHO };
}
pub fn ptyFgPgid(master: std.posix.fd_t) error{IoctlFailed}!std.posix.pid_t {
var pgid: c.pid_t = 0;
if (c.ioctl(master, c.TIOCGPGRP, &pgid) < 0) return error.IoctlFailed;
return @intCast(pgid);
}
pub fn setWinsize(master: std.posix.fd_t, ws: root.Winsize) error{IoctlFailed}!void {
var cws: c.struct_winsize = .{ .ws_row = ws.row, .ws_col = ws.col, .ws_xpixel = 0, .ws_ypixel = 0 };
if (c.ioctl(master, c.TIOCSWINSZ, &cws) < 0) return error.IoctlFailed;
}
pub fn anonFd(name: [*:0]const u8) error{CarrierFailed}!std.posix.fd_t {
// No memfd on Darwin. A 0600 file this uid creates and unlinks before
// anyone could open it by name is private by mode where memfd is
// private by having no name; the window is the two calls below, on an
// empty file. /tmp rather than the runtime dir because this file
// imports nothing of ours and must not learn the socket directory.
var tmpl: [128]u8 = undefined;
const t = std.fmt.bufPrintZ(&tmpl, "/tmp/mux-{s}-XXXXXX", .{std.mem.span(name)}) catch return error.CarrierFailed;
const fd = c.mkstemp(t.ptr);
if (fd < 0) return error.CarrierFailed;
errdefer std.posix.close(fd);
// mkstemp fills the XXXXXX in place, so the name to unlink is `t` as it
// reads now and not the template that was printed into it.
std.posix.unlink(t) catch return error.CarrierFailed;
// mkstemp opens O_CLOEXEC on modern Darwin; the candidate must inherit
// the carrier across `mux d upgrade`'s exec, so the flag comes back off.
const flags = std.posix.fcntl(fd, std.posix.F.GETFD, 0) catch return error.CarrierFailed;
_ = std.posix.fcntl(fd, std.posix.F.SETFD, flags & ~@as(usize, std.posix.FD_CLOEXEC)) catch
return error.CarrierFailed;
return fd;
}