src/os/client_os.zig
Ref: Size: 9.8 KiB History
//! The wall's and askpass's platform layer: the few calls the client side
//! makes that differ by OS. Same shape as `server_os` — this root is the
//! contract, a child per OS spells it — and deliberately a SEPARATE row:
//! the client never links a fork or a pty, and an app that links the
//! engine and a client must not either.
const std = @import("std");
const builtin = @import("builtin");
pub const impl = switch (builtin.os.tag) {
.linux => @import("client_os_linux.zig"),
.macos => @import("client_os_macos.zig"),
else => @compileError("mux has no client platform arm for " ++ @tagName(builtin.os.tag)),
};
/// This process's pid, for `mux-ask-PID.sock` and the hub's banner.
pub fn getpid() std.posix.pid_t {
return impl.getpid();
}
/// Who is on the other end of the askpass socket. The 0700 runtime
/// directory is the boundary and mux takes it as found; where it is not
/// private, the uid here is what stops another local user raising a prompt
/// and reading the answer, and the pid is what attributes a prompt to the
/// ssh THIS wall spawned.
///
/// Unlike `server_os.peerCred`, a pid of 0 — what a kernel reports for a
/// peer it cannot name — passes through this root unjudged, because the
/// consumer already has the rule: `askpass.dialOwner` walks up from the
/// peer under `at > 0`, so a 0 ends the walk without matching anything.
/// Rejecting it here as well would be a second copy of one rule.
pub const PeerCred = struct { uid: std.posix.uid_t, pid: std.posix.pid_t };
pub fn peerCred(fd: std.posix.socket_t) ?PeerCred {
return impl.peerCred(fd);
}
/// A BLOCKING send that cannot raise SIGPIPE. `server_os.sendNoSigNoWait`
/// is the daemon's twin and is NON-blocking, because a stalled client must
/// never stall the pump; this one waits for room, because its caller is the
/// agent probe, which writes five bytes and then polls for the answer. The
/// daemon's name carries the difference so neither side is reached for by
/// habit.
///
/// The signal half is the reason the operation exists here at all. The
/// probe runs before the client installs any signal handling, and the peer
/// may already have closed: the daemon accepts a forwarded agent socket
/// and only then closes it when no attached client is offering an agent.
/// A send that signalled would end the client outright instead of handing
/// back BrokenPipe for the probe to read as "no agent".
pub fn sendNoSig(fd: std.posix.socket_t, bytes: []const u8) std.posix.SendError!usize {
return impl.sendNoSig(fd, bytes);
}
/// A nonblocking TCP send which cannot raise SIGPIPE, used by the native
/// forwarding pump so one stalled local reader never stalls its owner thread.
pub fn sendNoSigNoWait(fd: std.posix.socket_t, bytes: []const u8) !usize {
return impl.sendNoSigNoWait(fd, bytes);
}
/// The parent of `pid`, or 0 when the OS will not say or `pid` is not
/// positive. One step of the walk from an askpass helper up to the ssh a
/// dial spawned.
pub fn parentOf(pid: std.posix.pid_t) std.posix.pid_t {
if (pid <= 0) return 0;
return impl.parentOf(pid);
}
/// The effective uid, for the askpass caller check above.
pub fn geteuid() std.posix.uid_t {
return impl.geteuid();
}
/// This terminal's size, or null when `fd` is not a terminal. The 0x0 case
/// and the daemon's floor are the caller's to judge (`interact.ttySize`).
pub fn winSize(fd: std.posix.fd_t) ?std.posix.winsize {
return impl.winSize(fd);
}
/// Size a pty. Test-only in practice, but a contract because the wall's
/// own `ttySize` is judged against it.
pub fn setWinSize(fd: std.posix.fd_t, ws: std.posix.winsize) error{Unsupported}!void {
return impl.setWinSize(fd, ws);
}
/// A real master/slave pty pair, the OS answering about the OS. Named here
/// rather than returned anonymously because an anonymous struct in the root
/// and one in an arm are two distinct types, and the arm could then never
/// satisfy the contract.
pub const PtyPair = struct { master: std.posix.fd_t, slave: std.posix.fd_t };
/// Declared ONLY in a test binary, so this row's header stays true of every
/// shipped build: the wall never opens a pty, it lives in one, and a client
/// that could open one is a client an app might link a pty through. The two
/// callers that need a real terminal to size — this file's own test and
/// `interact.ptsPair` — are reached only from test blocks, and a test build
/// is the compilation where `builtin.is_test` holds and this decl exists.
/// A production line that reached for it gets the message below instead.
pub const openPtyPair = if (builtin.is_test) impl.openPtyPair else @compileError(
"client_os.openPtyPair is test-only: the client side links no pty",
);
test "client_os: the arm compiles and answers for the process it is in" {
try std.testing.expect(getpid() > 0);
}
test "client_os.peerCred and parentOf: asked of the OS, not a fixture" {
// Both socketpair ends belong to this process, so the kernel must name
// it; and `parentOf` is graded against the ppid the OS itself reports,
// because the field it reads is positional and a comment naming that
// field cannot fail.
var sp: [2]std.posix.fd_t = undefined;
try std.testing.expectEqual(@as(c_int, 0), std.c.socketpair(std.posix.AF.UNIX, std.posix.SOCK.STREAM, 0, &sp));
defer std.posix.close(sp[0]);
defer std.posix.close(sp[1]);
const cred = peerCred(sp[0]) orelse return error.NoCred;
try std.testing.expectEqual(getpid(), cred.pid);
try std.testing.expectEqual(geteuid(), cred.uid);
try std.testing.expectEqual(std.c.getppid(), parentOf(getpid()));
try std.testing.expectEqual(@as(std.posix.pid_t, 0), parentOf(0));
}
test "client_os.winSize reads what setWinSize wrote, off a real pty" {
const p = try openPtyPair();
defer std.posix.close(p.master);
defer std.posix.close(p.slave);
try setWinSize(p.master, .{ .row = 17, .col = 91, .xpixel = 0, .ypixel = 0 });
const ws = winSize(p.slave) orelse return error.NoSize;
try std.testing.expectEqual(@as(u16, 91), ws.col);
try std.testing.expectEqual(@as(u16, 17), ws.row);
}
test "client_os.sendNoSig: a closed peer is an error, not a signal" {
// Judged in a CHILD, because this process cannot be asked. Zig's own
// startup code installs a no-op SIGPIPE handler in every binary it
// starts, the test runner included, so a plain send with no
// MSG_NOSIGNAL also returns BrokenPipe here — a test written in this
// process stays green with the flag deleted, which is the one mistake
// it exists to catch. The child puts SIGPIPE back at SIG_DFL first, so
// a send that raises the signal DIES and the parent reads a status that
// never exited.
//
// A raw fork rather than the server row's `forkPty`: this row links no
// pty and no fork by design, and build.zig's folder rule 6 reads only
// production lines, so the call is legal exactly here. The price is
// that the child inherits fd 1 and fd 2, and fd 1 is the build runner's
// protocol stream, which one stray byte wedges — so pointing both at
// /dev/null is the first thing the child does, before anything that
// could print.
const pid = try std.posix.fork();
if (pid == 0) {
const devnull = std.posix.open("/dev/null", .{ .ACCMODE = .RDWR }, 0) catch std.c._exit(2);
std.posix.dup2(devnull, std.posix.STDOUT_FILENO) catch std.c._exit(2);
std.posix.dup2(devnull, std.posix.STDERR_FILENO) catch std.c._exit(2);
var dfl: std.posix.Sigaction = .{
.handler = .{ .handler = std.posix.SIG.DFL },
.mask = std.posix.sigemptyset(),
.flags = 0,
};
std.posix.sigaction(std.posix.SIG.PIPE, &dfl, null);
// Two legs, because "the peer is gone" is two different states to
// the kernel, and the first is the one the probe meets:
// `mux_main.agentReachable` sends once on a socket it just connected
// and then closes it, so a daemon that hung up in between leaves a
// socket that was never written to. The second leg — a socket that
// carried bytes and then lost its peer — is asked because this
// operation is a contract and not one caller's helper, and on Darwin
// the two states differ where they do not on Linux: the flag
// that suppresses the signal is a socket option there, and a socket
// the kernel has already shut down refuses to take one (see
// `client_os_macos.sendNoSig`). A test that asked only the first
// would pass on an arm that can never arm a live socket, and one
// that asked only the second would pass on an arm that only ever
// works after a successful send.
//
// A send that SUCCEEDED to a closed peer is as wrong as one that
// signalled, and neither is 0. `_exit` rather than an exit that runs
// atexit handlers: this child is a copy of a test runner mid-run and
// must flush nothing of its parent's.
var gone: [2]std.posix.fd_t = undefined;
if (std.c.socketpair(std.posix.AF.UNIX, std.posix.SOCK.STREAM, 0, &gone) != 0) std.c._exit(2);
std.posix.close(gone[1]);
if (sendNoSig(gone[0], "x")) |_| std.c._exit(2) else |e| if (e != error.BrokenPipe) std.c._exit(2);
var live: [2]std.posix.fd_t = undefined;
if (std.c.socketpair(std.posix.AF.UNIX, std.posix.SOCK.STREAM, 0, &live) != 0) std.c._exit(2);
_ = sendNoSig(live[0], "x") catch std.c._exit(2);
std.posix.close(live[1]);
if (sendNoSig(live[0], "x")) |_| std.c._exit(2) else |e| if (e != error.BrokenPipe) std.c._exit(2);
std.c._exit(0);
}
const status = std.posix.waitpid(pid, 0).status;
try std.testing.expect(std.posix.W.IFEXITED(status));
try std.testing.expectEqual(@as(u32, 0), std.posix.W.EXITSTATUS(status));
}
test {
std.testing.refAllDeclsRecursive(@This());
}