a73x

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;
}