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