a73x

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