a73x

src/os/client_os_macos.zig

Ref:   Size: 7.1 KiB   History

//! Darwin arm of `client_os`. Spellings only; the contract is in the root.
//! Three operations use a different MECHANISM rather than a different
//! spelling, because the Linux one does not exist here, and each has its
//! twin in `server_os_macos` (docs/decisions.md, 2026-09-03, "the daemon's
//! Darwin arm"): `peerCred` takes two calls because LOCAL_PEERCRED answers
//! no pid, `sendNoSig` sets SO_NOSIGPIPE on the socket because there is no
//! MSG_NOSIGNAL, and `parentOf` asks sysctl because there is no /proc.
const std = @import("std");
const root = @import("client_os.zig");
const c = @cImport({
    @cInclude("util.h"); // openpty (test-only through the root)
    @cInclude("sys/ioctl.h");
    @cInclude("sys/socket.h");
    @cInclude("sys/un.h"); // LOCAL_PEERPID
    @cInclude("sys/sysctl.h"); // kinfo_proc for parentOf
    @cInclude("unistd.h"); // getpeereid
});

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. SOL_LOCAL is 0.
    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);
    if (c.getsockopt(fd, 0, c.LOCAL_PEERPID, &pid, &len) != 0) return null;
    // A pid of 0 — a peer the kernel will not name — passes through
    // unjudged; the root's doc says why, and `askpass.dialOwner` is the one
    // place the rule lives.
    return .{ .uid = @intCast(uid), .pid = @intCast(pid) };
}

pub fn sendNoSig(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 because there
    // is no one place every fd that reaches here is created, and the option
    // is idempotent.
    //
    // 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 (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;
    // Flags 0, not DONTWAIT: this side BLOCKS. The root's doc says why —
    // the agent probe writes five bytes and then polls for the answer.
    return std.posix.sendto(fd, bytes, 0, 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.
        //
        // Seen once in a Mac `make check` on 2026-09-04, as `attempt to unwrap
        // error: SocketNotConnected` out of this call, in the probe that asks
        // whether an ssh agent is listening. 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 sendNoSigNoWait(fd: std.posix.socket_t, bytes: []const u8) !usize {
    const flags = try std.posix.fcntl(fd, std.posix.F.GETFL, 0);
    const nonblock: u32 = @bitCast(std.posix.O{ .NONBLOCK = true });
    _ = try std.posix.fcntl(fd, std.posix.F.SETFL, flags | nonblock);
    return sendNoSig(fd, bytes);
}

/// sysctl KERN_PROC_PID: the kernel's own record of the process, the Darwin
/// answer to /proc/PID/stat. A pid the kernel no longer has answers 0 with
/// a length of 0 rather than an error, so the length is read as well as the
/// return, and both mean the same 0 the Linux arm gives for a stat file
/// that will not open.
pub fn parentOf(pid: std.posix.pid_t) std.posix.pid_t {
    var mib = [_]c_int{ c.CTL_KERN, c.KERN_PROC, c.KERN_PROC_PID, pid };
    var kp: c.struct_kinfo_proc = undefined;
    var len: usize = @sizeOf(c.struct_kinfo_proc);
    if (c.sysctl(&mib, @intCast(mib.len), &kp, &len, null, 0) != 0 or len == 0) return 0;
    return @intCast(kp.kp_eproc.e_ppid);
}

pub fn geteuid() std.posix.uid_t {
    return std.c.geteuid();
}

pub fn winSize(fd: std.posix.fd_t) ?std.posix.winsize {
    // Through the C struct and copied field by field rather than casting a
    // pointer: the two layouts agree on Darwin today, and a copy cannot
    // stop agreeing silently.
    var ws: c.struct_winsize = undefined;
    if (c.ioctl(fd, c.TIOCGWINSZ, &ws) != 0) return null;
    return .{ .row = ws.ws_row, .col = ws.ws_col, .xpixel = ws.ws_xpixel, .ypixel = ws.ws_ypixel };
}

pub fn setWinSize(fd: std.posix.fd_t, ws: std.posix.winsize) error{Unsupported}!void {
    var cws: c.struct_winsize = .{
        .ws_row = ws.row,
        .ws_col = ws.col,
        .ws_xpixel = ws.xpixel,
        .ws_ypixel = ws.ypixel,
    };
    if (c.ioctl(fd, c.TIOCSWINSZ, &cws) != 0) return error.Unsupported;
}

pub fn openPtyPair() error{Unsupported}!root.PtyPair {
    // `openpty` and not the Linux arm's /dev/ptmx walk: Darwin's ptmx wants
    // grantpt and unlockpt before the slave name is valid, and openpty is
    // the libc call that does exactly that sequence.
    var master: c_int = undefined;
    var slave: c_int = undefined;
    if (c.openpty(&master, &slave, null, null, null) != 0) return error.Unsupported;
    return .{ .master = master, .slave = slave };
}