//! mgf-hrb-priv-ipc — the typed IPC protocol between an unprivileged
//! USB worker and its dedicated privileged helper subprocess (Part 11 /
//! `mgf-hrb-privileged-core-hardening.md`, D-031).
//!
//! Why a binary protocol and not JSON: the helper's *entire input
//! surface* must be the handful of typed kabi ops. JSON would force
//! the helper to embed a JSON parser — exactly the kind of large
//! attack surface the hardening pass exists to eliminate. So the
//! protocol is fixed-size, length-prefixed binary, validated at the
//! type level (a malformed op tag is a 1-byte mismatch the helper
//! rejects without ever entering a parser state machine).
//!
//! Wire shape, per request:
//!
//!   [u8  op_tag]
//!   [u8  pad ]
//!   [u16 payload_len_le]   // bounded — refused at the gate
//!   [payload_len bytes]    // fixed-shape per op_tag
//!
//! Per response:
//!
//!   [u8  err_tag]          // 0 = Ok, else maps to Err.*
//!   [u8  pad ]
//!   [u16 payload_len_le]
//!   [payload_len bytes]    // op-specific response
//!   [optional SCM_RIGHTS fd in cmsg, for acquire / raw_channel]
//!
//! The helper accepts ONLY:
//!   • op_tag values that name kabi ops (acquire, release, reset,
//!     raw_channel, control). Anything else: reject + close socket.
//!   • payload_len within a tight bound (≤256 bytes).
//!   • A URN that parses through `parseUsbUrn` (validated downstream,
//!     not in the helper's parser — but the helper enforces the
//!     128-byte URN cap before forwarding).
//!
//! It performs NO policy decisions, NO capability checks, NO client
//! JSON parsing. The worker has already done all of that.

const std = @import("std");
const model = @import("mgf_hrb_model");
const linux = std.os.linux;

const Urn = model.Urn;
const Err = model.Err;
const ClaimMode = model.ClaimMode;
const ControlVerb = model.ControlVerb;

/// The op tag — a closed enum. The helper rejects any byte not in
/// this set without entering any parsing logic.
pub const OpTag = enum(u8) {
    acquire = 1,
    release = 2,
    reset = 3,
    raw_channel = 4,
    control = 5,
};

/// Maps Zig's `Err` enum (model.Err) to a one-byte tag the helper
/// can send back without a runtime string lookup. The worker
/// reverses on receipt.
pub const ErrTag = enum(u8) {
    ok = 0,
    not_found = 1,
    not_permitted = 2,
    conflict = 3,
    unsupported = 4,
    hardware_fault = 5,
    invalid_token = 6,
    bad_request = 7,
    timeout = 8,
    overloaded = 9,
    revoked = 10,
    unavailable = 11,
};

pub fn errToTag(e: Err) ErrTag {
    return switch (e) {
        Err.NotFound => .not_found,
        Err.NotPermitted => .not_permitted,
        Err.Conflict => .conflict,
        Err.Unsupported => .unsupported,
        Err.HardwareFault => .hardware_fault,
        Err.InvalidToken => .invalid_token,
        Err.BadRequest => .bad_request,
        Err.Timeout => .timeout,
        Err.Overloaded => .overloaded,
        Err.Revoked => .revoked,
        Err.Unavailable => .unavailable,
    };
}

pub fn tagToErr(t: ErrTag) ?Err {
    return switch (t) {
        .ok => null,
        .not_found => Err.NotFound,
        .not_permitted => Err.NotPermitted,
        .conflict => Err.Conflict,
        .unsupported => Err.Unsupported,
        .hardware_fault => Err.HardwareFault,
        .invalid_token => Err.InvalidToken,
        .bad_request => Err.BadRequest,
        .timeout => Err.Timeout,
        .overloaded => Err.Overloaded,
        .revoked => Err.Revoked,
        .unavailable => Err.Unavailable,
    };
}

/// Acquire-request payload: URN + claim mode + deadline (ms) +
/// detach flag. All fixed-size; the helper does no decoding beyond
/// the field offsets.
pub const AcquirePayload = extern struct {
    urn_len: u8,
    mode: u8, // matches enum int value
    detach: u8, // 0 or 1
    pad: u8 = 0,
    deadline_ms: u64,
    urn_buf: [128]u8,
};

/// Acquire-response payload — the AcquireToken id. The fd (if any)
/// rides as SCM_RIGHTS in the same recvmsg.
pub const AcquireResponse = extern struct {
    token_id: u64,
};

/// Release-request payload — the token_id to release. No response
/// payload (release is total + idempotent per kabi §6.3); the err
/// tag alone tells the worker the helper finished. Helper-side
/// release also drops the held fd.
pub const ReleasePayload = extern struct {
    token_id: u64,
};

/// Reset-request payload — URN + level (0=function_level, 1=bus_level).
pub const ResetPayload = extern struct {
    urn_len: u8,
    level: u8, // matches ResetLevel int value
    pad: [2]u8 = .{ 0, 0 },
    deadline_ms: u64,
    urn_buf: [128]u8,
};

/// Raw-channel-request payload — the token_id to derive the raw fd
/// from. The fd itself rides back via SCM_RIGHTS.
pub const RawChannelPayload = extern struct {
    token_id: u64,
};

/// Raw-channel-response payload — empty; the meaningful return is
/// the SCM_RIGHTS fd attached to this response message. The
/// presence of the fd in the cmsg is what proves "yes you have
/// raw authority"; an empty payload + no fd is the "no fd-shaped
/// raw channel here" honest emptiness path.
pub const RawChannelResponse = extern struct {
    /// Token_id echoed back so the worker can correlate.
    token_id: u64,
};

/// Control-request payload — typed verb dispatch (Part 13 §5.1).
/// Carries URN + verb + value bytes + writer cap_id; the helper
/// runs through `LinuxUsbBackend.vControl`'s serialiser and
/// returns the disclosure (generation / applied_value / queue_depth
/// / superseded_by) via ControlResponse.
pub const ControlPayload = extern struct {
    urn_len: u8,
    verb: u8, // matches ControlVerb int value
    value_len: u8,
    pad: u8 = 0,
    cap_id: u64,
    deadline_ms: u64,
    urn_buf: [128]u8,
    value_buf: [64]u8,
};

/// Control-response payload — the disclosure shape from
/// `kabi.ControlOutcome`. `superseded_by_present` is the flat
/// flag that selects between null (0) and populated (1) — extern
/// structs can't carry optionals.
pub const ControlResponse = extern struct {
    generation: u64,
    queue_depth: u32,
    applied_value_len: u8,
    superseded_by_present: u8,
    pad: [2]u8 = .{ 0, 0 },
    superseded_by_generation: u64,
    superseded_by_cap_id: u64,
    superseded_by_at_ms: u64,
    applied_value_buf: [64]u8,
};

/// Header is 4 bytes; payload follows.
pub const Header = extern struct {
    tag: u8,
    pad: u8 = 0,
    payload_len: u16,
};

pub const MAX_PAYLOAD_BYTES: usize = 256;

/// Send one typed request over a connected socket. Caller already
/// has the socket open; this is allocation-free.
pub fn sendRequest(sock: i32, op: OpTag, payload: []const u8) !void {
    if (payload.len > MAX_PAYLOAD_BYTES) return error.PayloadTooLarge;
    var hdr = Header{
        .tag = @intFromEnum(op),
        .payload_len = @intCast(payload.len),
    };
    var iov_data = [_]u8{0} ** (@sizeOf(Header) + MAX_PAYLOAD_BYTES);
    @memcpy(iov_data[0..@sizeOf(Header)], std.mem.asBytes(&hdr));
    @memcpy(iov_data[@sizeOf(Header) .. @sizeOf(Header) + payload.len], payload);
    const total = @sizeOf(Header) + payload.len;

    var written: usize = 0;
    while (written < total) {
        const n = linux.write(sock, iov_data[written..].ptr, total - written);
        switch (linux.errno(n)) {
            .SUCCESS => {
                if (n == 0) return error.PeerClosed;
                written += n;
            },
            .INTR => continue,
            else => return error.WriteFailed,
        }
    }
}

/// Receive one typed request. Returns the op tag and a slice of
/// the payload (into the caller's `payload_buf`). Returns
/// `error.MalformedRequest` on any frame the helper should reject
/// without parsing — unknown op tag, oversized payload, short
/// read.
pub fn recvRequest(sock: i32, payload_buf: []u8) !struct {
    op: OpTag,
    payload: []u8,
} {
    var hdr_bytes: [@sizeOf(Header)]u8 = undefined;
    try recvAll(sock, &hdr_bytes);
    var hdr: Header = undefined;
    @memcpy(std.mem.asBytes(&hdr), &hdr_bytes);

    if (hdr.payload_len > MAX_PAYLOAD_BYTES) return error.MalformedRequest;
    if (hdr.payload_len > payload_buf.len) return error.MalformedRequest;
    if (hdr.tag < 1 or hdr.tag > 5) return error.MalformedRequest;

    try recvAll(sock, payload_buf[0..hdr.payload_len]);
    return .{
        .op = @enumFromInt(hdr.tag),
        .payload = payload_buf[0..hdr.payload_len],
    };
}

/// Send a typed response. Caller computes err_tag + payload.
pub fn sendResponse(sock: i32, err_tag: ErrTag, payload: []const u8) !void {
    if (payload.len > MAX_PAYLOAD_BYTES) return error.PayloadTooLarge;
    var hdr = Header{
        .tag = @intFromEnum(err_tag),
        .payload_len = @intCast(payload.len),
    };
    var iov_data = [_]u8{0} ** (@sizeOf(Header) + MAX_PAYLOAD_BYTES);
    @memcpy(iov_data[0..@sizeOf(Header)], std.mem.asBytes(&hdr));
    @memcpy(iov_data[@sizeOf(Header) .. @sizeOf(Header) + payload.len], payload);
    const total = @sizeOf(Header) + payload.len;

    var written: usize = 0;
    while (written < total) {
        const n = linux.write(sock, iov_data[written..].ptr, total - written);
        switch (linux.errno(n)) {
            .SUCCESS => {
                if (n == 0) return error.PeerClosed;
                written += n;
            },
            .INTR => continue,
            else => return error.WriteFailed,
        }
    }
}

/// Receive one typed response. Returns the err tag and payload slice.
pub fn recvResponse(sock: i32, payload_buf: []u8) !struct {
    err: ErrTag,
    payload: []u8,
} {
    var hdr_bytes: [@sizeOf(Header)]u8 = undefined;
    try recvAll(sock, &hdr_bytes);
    var hdr: Header = undefined;
    @memcpy(std.mem.asBytes(&hdr), &hdr_bytes);

    if (hdr.payload_len > MAX_PAYLOAD_BYTES) return error.MalformedResponse;
    if (hdr.payload_len > payload_buf.len) return error.MalformedResponse;

    try recvAll(sock, payload_buf[0..hdr.payload_len]);
    return .{
        .err = @enumFromInt(hdr.tag),
        .payload = payload_buf[0..hdr.payload_len],
    };
}

/// Send a typed response WITH an attached fd via SCM_RIGHTS. The
/// fd rides in the same `sendmsg` as the response header + payload
/// — one syscall, atomic from the kernel's point of view. Used by
/// the helper to return the raw_channel fd. The caller-supplied
/// `fd_to_send` is duped in the kernel; the caller keeps its copy.
pub fn sendResponseWithFd(
    sock: i32,
    err_tag: ErrTag,
    payload: []const u8,
    fd_to_send: i32,
) !void {
    if (payload.len > MAX_PAYLOAD_BYTES) return error.PayloadTooLarge;
    var hdr = Header{
        .tag = @intFromEnum(err_tag),
        .payload_len = @intCast(payload.len),
    };
    var iov_data = [_]u8{0} ** (@sizeOf(Header) + MAX_PAYLOAD_BYTES);
    @memcpy(iov_data[0..@sizeOf(Header)], std.mem.asBytes(&hdr));
    @memcpy(iov_data[@sizeOf(Header) .. @sizeOf(Header) + payload.len], payload);
    const total = @sizeOf(Header) + payload.len;

    var iov = [_]std.posix.iovec_const{.{
        .base = &iov_data,
        .len = total,
    }};
    const cmsg_space = cmsgSpace(@sizeOf(i32));
    var cbuf: [64]u8 align(@alignOf(linux.cmsghdr)) = undefined;
    @memset(&cbuf, 0);

    const cmsg: *linux.cmsghdr = @ptrCast(@alignCast(&cbuf));
    cmsg.len = cmsgLen(@sizeOf(i32));
    cmsg.level = std.posix.SOL.SOCKET;
    cmsg.type = 0x01; // SCM_RIGHTS (Linux UAPI; see fdpass.zig K2 note)
    const data_ptr: [*]u8 = @ptrCast(cmsg);
    const fd_off = cmsgAlign(@sizeOf(linux.cmsghdr));
    @memcpy(data_ptr[fd_off .. fd_off + @sizeOf(i32)], std.mem.asBytes(&fd_to_send));

    var msg = std.mem.zeroes(linux.msghdr_const);
    msg.iov = &iov;
    msg.iovlen = 1;
    msg.control = &cbuf;
    msg.controllen = cmsg_space;

    while (true) {
        const n = linux.sendmsg(sock, &msg, 0);
        switch (linux.errno(n)) {
            .SUCCESS => return,
            .INTR => continue,
            else => return error.SendMsgFailed,
        }
    }
}

/// Receive a typed response WITH an attached fd. Reads the header
/// + payload + cmsg in one `recvmsg`. Returns the err tag, the
/// payload slice (into `payload_buf`), and the installed fd (or
/// `-1` if no fd accompanied the response — honest emptiness).
pub fn recvResponseWithFd(sock: i32, payload_buf: []u8) !struct {
    err: ErrTag,
    payload: []u8,
    fd: i32,
} {
    var rxbuf: [@sizeOf(Header) + MAX_PAYLOAD_BYTES]u8 = undefined;
    var iov = [_]std.posix.iovec{.{
        .base = &rxbuf,
        .len = rxbuf.len,
    }};
    const cmsg_space = cmsgSpace(@sizeOf(i32));
    var cbuf: [64]u8 align(@alignOf(linux.cmsghdr)) = undefined;
    @memset(&cbuf, 0);

    var msg = std.mem.zeroes(linux.msghdr);
    msg.iov = &iov;
    msg.iovlen = 1;
    msg.control = &cbuf;
    msg.controllen = cmsg_space;

    const n = linux.recvmsg(sock, &msg, 0);
    if (linux.errno(n) != .SUCCESS) return error.RecvMsgFailed;
    if (n == 0) return error.PeerClosed;

    // G3 (review wave 2) — fd-leak hardening.
    //
    // The kernel INSTALLS the SCM_RIGHTS fd into our fd table as
    // part of `recvmsg`, BEFORE we get to any payload validation.
    // The previous shape of this function returned `error.MalformedResponse`
    // on a too-large or under-read payload WITHOUT first extracting
    // the installed fd — so a malformed (or crafted) helper response
    // carrying a SCM_RIGHTS fd leaked one descriptor per malformed
    // reply. Sustained malformed traffic → EMFILE → broker dies.
    //
    // Fix: extract the fd FIRST (immediately after `recvmsg`
    // succeeds), guard it with a `defer close` that fires on every
    // early-return error path, then clear the guard only on the
    // successful-return path so the caller takes ownership.
    var fd: i32 = -1;
    if (msg.controllen >= @sizeOf(linux.cmsghdr)) {
        const cmsg: *linux.cmsghdr = @ptrCast(@alignCast(&cbuf));
        // 0x01 is SCM_RIGHTS — Linux UAPI constant; full rationale at
        // fdpass.zig's K2 doc-comment. Same value across every arch.
        if (cmsg.level == std.posix.SOL.SOCKET and cmsg.type == 0x01) {
            const data_ptr: [*]u8 = @ptrCast(cmsg);
            const fd_off = cmsgAlign(@sizeOf(linux.cmsghdr));
            // M6 (review wave 3) — multi-fd SCM_RIGHTS leak fix.
            //
            // A single SCM_RIGHTS cmsg can carry MULTIPLE file
            // descriptors as an i32 array. Our protocol expects
            // exactly one. Pre-M6 the code extracted the first fd
            // and IGNORED the rest — but the kernel had already
            // installed EVERY fd in our table. A misbehaving (or
            // compromised) helper that attached e.g. 5 fds leaked
            // 4 of them per call. With cbuf=64 bytes, up to ~10
            // extra fds can fit per cmsg.
            //
            // Fix: compute fd_count from cmsg.len; extract fds[0];
            // close fds[1..fd_count] explicitly. Log per extra fd
            // so a sustained leak attempt is visible.
            const hdr_size = cmsgAlign(@sizeOf(linux.cmsghdr));
            const payload_bytes: usize = if (cmsg.len > hdr_size)
                @as(usize, @intCast(cmsg.len)) - hdr_size
            else
                0;
            const fd_count = payload_bytes / @sizeOf(i32);
            if (fd_count >= 1) {
                @memcpy(std.mem.asBytes(&fd), data_ptr[fd_off .. fd_off + @sizeOf(i32)]);
                // Close extras (defensive — protocol says one fd).
                var extra_idx: usize = 1;
                while (extra_idx < fd_count) : (extra_idx += 1) {
                    var extra_fd: i32 = -1;
                    const extra_off = fd_off + extra_idx * @sizeOf(i32);
                    @memcpy(std.mem.asBytes(&extra_fd), data_ptr[extra_off .. extra_off + @sizeOf(i32)]);
                    if (extra_fd >= 0) {
                        _ = linux.close(extra_fd);
                        std.debug.print(
                            "priv_ipc: closed UNEXPECTED extra SCM_RIGHTS fd={d} " ++
                                "(idx={d} of {d}) on sock={d} — helper protocol " ++
                                "violation; protocol carries exactly one fd\n",
                            .{ extra_fd, extra_idx, fd_count, sock },
                        );
                    }
                }
            }
        }
    }
    // `errdefer` registered FIRST (before any conditional return)
    // so every error path below — including the MSG_CTRUNC reject —
    // releases the fd we already extracted. Only the success path
    // doesn't fire the errdefer; ownership transfers to caller.
    errdefer if (fd >= 0) {
        _ = linux.close(fd);
        // Log so a sustained leak doesn't go silent in production —
        // a crafted helper could attempt the leak path repeatedly;
        // we want operators to see the noise.
        std.debug.print(
            "priv_ipc: dropped SCM_RIGHTS fd={d} due to malformed/short response on sock={d}\n",
            .{ fd, sock },
        );
    };

    // M6 (continued) — reject if the kernel signalled MSG_CTRUNC.
    // Control data was truncated past cbuf; some cmsg fds may have
    // been installed but we couldn't see them to close. The fds we
    // DID see are closed by the errdefer above; the unrecoverable
    // ones are logged. Surface as MalformedResponse so the helper
    // supervisor kills the helper on next op.
    if ((msg.flags & 0x08) != 0) { // MSG_CTRUNC = 0x08 on Linux
        std.debug.print(
            "priv_ipc: MSG_CTRUNC on recvmsg sock={d} — helper sent more " ++
                "control data than cbuf can hold; potential unrecoverable fd leak\n",
            .{sock},
        );
        return error.MalformedResponse;
    }

    if (n < @sizeOf(Header)) return error.MalformedResponse;

    var hdr: Header = undefined;
    @memcpy(std.mem.asBytes(&hdr), rxbuf[0..@sizeOf(Header)]);
    if (hdr.payload_len > MAX_PAYLOAD_BYTES) return error.MalformedResponse;
    if (hdr.payload_len > payload_buf.len) return error.MalformedResponse;
    if (n < @sizeOf(Header) + hdr.payload_len) return error.MalformedResponse;
    @memcpy(payload_buf[0..hdr.payload_len], rxbuf[@sizeOf(Header) .. @sizeOf(Header) + hdr.payload_len]);

    // On the SUCCESS path the errdefer above doesn't fire, so the
    // fd ownership transfers to the caller via the return value.
    return .{
        .err = @enumFromInt(hdr.tag),
        .payload = payload_buf[0..hdr.payload_len],
        .fd = fd,
    };
}

fn cmsgAlign(len: usize) usize {
    const a: usize = @alignOf(usize);
    return (len + a - 1) & ~(a - 1);
}
fn cmsgLen(payload: usize) u64 {
    return @intCast(cmsgAlign(@sizeOf(linux.cmsghdr)) + payload);
}
fn cmsgSpace(payload: usize) usize {
    return cmsgAlign(@sizeOf(linux.cmsghdr)) + cmsgAlign(payload);
}

fn recvAll(sock: i32, buf: []u8) !void {
    var got: usize = 0;
    while (got < buf.len) {
        const n = linux.read(sock, buf[got..].ptr, buf.len - got);
        switch (linux.errno(n)) {
            .SUCCESS => {
                if (n == 0) return error.PeerClosed;
                got += n;
            },
            .INTR => continue,
            else => return error.ReadFailed,
        }
    }
}

/// U7 (survivability review §3.8) — fd validation on receipt. An fd handed to
/// the broker via `SCM_RIGHTS` from the privileged helper MUST be a real device
/// handle: a CHARACTER device (the `/dev/bus/usb/<bus>/<dev>` usbfs node the
/// helper opens `O_RDWR`). A compromised higher-privileged helper could
/// otherwise smuggle a crafted fd across the seam — a socket, the broker's own
/// listen fd, a regular file, a pipe — and have the broker treat it as a device
/// channel. Never trust an inbound fd: stat it and reject anything that isn't a
/// char device. `statx(AT_EMPTY_PATH)` works straight on the fd (no path, no
/// TOCTOU). Returns false on any stat failure (fail-closed).
pub fn isCharDeviceFd(fd: i32) bool {
    var stx: linux.Statx = undefined;
    const AT_EMPTY_PATH: u32 = 0x1000; // stat the fd itself, not a path
    const rc = linux.statx(fd, "", AT_EMPTY_PATH, .{ .TYPE = true }, &stx);
    if (linux.errno(rc) != .SUCCESS) return false;
    return linux.S.ISCHR(stx.mode);
}

// === tests ================================================================
const testing = std.testing;

test "U7 fd-validation: a char device passes, a fifo/socket/regular fd is rejected" {
    // /dev/null is a character device — the shape a real usbfs device fd has.
    const dn = linux.open("/dev/null", .{ .ACCMODE = .RDONLY }, 0);
    try testing.expect(linux.errno(dn) == .SUCCESS);
    const devnull: i32 = @intCast(dn);
    defer _ = linux.close(devnull);
    try testing.expect(isCharDeviceFd(devnull));

    // A pipe end is a FIFO — a crafted fd a hostile helper might send instead.
    var pfds: [2]i32 = undefined;
    try testing.expect(linux.errno(linux.pipe2(&pfds, .{})) == .SUCCESS);
    defer _ = linux.close(pfds[0]);
    defer _ = linux.close(pfds[1]);
    try testing.expect(!isCharDeviceFd(pfds[0]));

    // A socket (e.g. the broker's own listen fd) must be rejected too.
    const sr = linux.socket(linux.AF.UNIX, linux.SOCK.STREAM, 0);
    const sfd: i32 = @intCast(sr);
    defer _ = linux.close(sfd);
    try testing.expect(!isCharDeviceFd(sfd));
}

test "priv_ipc: errToTag and tagToErr round-trip every Err variant" {
    const variants = [_]Err{
        Err.NotFound,    Err.NotPermitted,  Err.Conflict,    Err.Unsupported,
        Err.HardwareFault, Err.InvalidToken, Err.BadRequest, Err.Timeout,
        Err.Overloaded,  Err.Revoked,       Err.Unavailable,
    };
    for (variants) |v| {
        const tag = errToTag(v);
        const back = tagToErr(tag) orelse return error.UnexpectedNull;
        try testing.expectEqual(v, back);
    }
    // The OK tag has no Err equivalent — that's its purpose.
    try testing.expect(tagToErr(.ok) == null);
}

test "priv_ipc: send + recv request round-trips over a socketpair" {
    var sv: [2]i32 = undefined;
    const rc = linux.socketpair(linux.AF.UNIX, linux.SOCK.STREAM, 0, &sv);
    if (linux.errno(rc) != .SUCCESS) return error.SocketpairFailed;
    defer _ = linux.close(sv[0]);
    defer _ = linux.close(sv[1]);

    var payload = AcquirePayload{
        .urn_len = 32,
        .mode = @intFromEnum(ClaimMode.exclusive),
        .detach = 0,
        .deadline_ms = 5000,
        .urn_buf = undefined,
    };
    const sample = "hw:/usb/bus3/port-9/dev/if0\x00\x00\x00\x00\x00";
    @memcpy(payload.urn_buf[0..32], sample[0..32]);

    try sendRequest(sv[0], .acquire, std.mem.asBytes(&payload)[0..@sizeOf(AcquirePayload)]);

    var rx: [256]u8 = undefined;
    const r = try recvRequest(sv[1], &rx);
    try testing.expectEqual(OpTag.acquire, r.op);
    try testing.expectEqual(@sizeOf(AcquirePayload), r.payload.len);

    var got: AcquirePayload = undefined;
    @memcpy(std.mem.asBytes(&got), r.payload);
    try testing.expectEqual(@as(u64, 5000), got.deadline_ms);
    try testing.expectEqual(@as(u8, 32), got.urn_len);
    try testing.expect(std.mem.eql(u8, sample[0..32], got.urn_buf[0..32]));
}

test "priv_ipc: send + recv response round-trips" {
    var sv: [2]i32 = undefined;
    const rc = linux.socketpair(linux.AF.UNIX, linux.SOCK.STREAM, 0, &sv);
    if (linux.errno(rc) != .SUCCESS) return error.SocketpairFailed;
    defer _ = linux.close(sv[0]);
    defer _ = linux.close(sv[1]);

    const ack = AcquireResponse{ .token_id = 0xDEADBEEF };
    try sendResponse(sv[0], .ok, std.mem.asBytes(&ack)[0..@sizeOf(AcquireResponse)]);

    var rx: [256]u8 = undefined;
    const r = try recvResponse(sv[1], &rx);
    try testing.expectEqual(ErrTag.ok, r.err);
    try testing.expectEqual(@sizeOf(AcquireResponse), r.payload.len);
    var got: AcquireResponse = undefined;
    @memcpy(std.mem.asBytes(&got), r.payload);
    try testing.expectEqual(@as(u64, 0xDEADBEEF), got.token_id);
}

test "priv_ipc: a typed Err response surfaces through the round-trip" {
    var sv: [2]i32 = undefined;
    const rc = linux.socketpair(linux.AF.UNIX, linux.SOCK.STREAM, 0, &sv);
    if (linux.errno(rc) != .SUCCESS) return error.SocketpairFailed;
    defer _ = linux.close(sv[0]);
    defer _ = linux.close(sv[1]);

    try sendResponse(sv[0], errToTag(Err.Conflict), &[_]u8{});
    var rx: [256]u8 = undefined;
    const r = try recvResponse(sv[1], &rx);
    try testing.expectEqual(ErrTag.conflict, r.err);
    const back = tagToErr(r.err) orelse return error.UnexpectedNull;
    try testing.expectEqual(Err.Conflict, back);
}

test "priv_ipc: a malformed op tag is rejected at the helper's gate" {
    var sv: [2]i32 = undefined;
    const rc = linux.socketpair(linux.AF.UNIX, linux.SOCK.STREAM, 0, &sv);
    if (linux.errno(rc) != .SUCCESS) return error.SocketpairFailed;
    defer _ = linux.close(sv[0]);
    defer _ = linux.close(sv[1]);

    // Hand-craft a frame with an unknown op tag (99) and an empty
    // payload. The helper's recvRequest must refuse without parsing.
    const bad = [_]u8{ 99, 0, 0, 0 };
    _ = linux.write(sv[0], &bad, bad.len);
    var rx: [256]u8 = undefined;
    try testing.expectError(error.MalformedRequest, recvRequest(sv[1], &rx));
}

// ===========================================================================
// R9 (review wave 2) — fd-leak burst pin for `recvResponseWithFd`.
//
// The G3/M6 hardening (above, lines ~380-470) closes two ways a hostile or
// buggy privileged helper could leak the broker's descriptors via SCM_RIGHTS:
//
//   * G3 — the kernel INSTALLS the cmsg fd into our table during `recvmsg`,
//     BEFORE any payload validation. A malformed/short reply that errors out
//     used to return without closing it → one fd leaked per bad reply →
//     sustained malformed traffic → EMFILE → broker death. The fix extracts
//     the fd first and guards it with an `errdefer close`.
//   * M6 — a single SCM_RIGHTS cmsg can carry an ARRAY of fds; our protocol
//     carries exactly one. The kernel installs every one. The fix closes the
//     extras explicitly (and rejects the MSG_CTRUNC overflow case).
//
// The empirical pin was deferred ("needs a helper-subprocess fuzz harness").
// A socketpair is a faithful stand-in for the helper — and crafting the three
// hostile frame shapes directly is STRICTER than random fuzzing: it's a
// deterministic sweep of every leak path, not a sample. The leak detector
// needs no /proc parsing and catches even a single escaped fd (see below).
// ===========================================================================

/// Send a response frame whose declared header length and attached SCM_RIGHTS
/// fd-array are independently controllable — so a test can forge each hostile
/// shape (short-claim, multi-fd, control-overflow). Mirrors the real
/// `sendResponseWithFd` cmsg construction but writes N fds instead of one.
fn testSendFramedWithFds(
    sock: i32,
    tag_byte: u8,
    declared_payload_len: u16,
    actual_payload: []const u8,
    fds: []const i32,
) !void {
    var hdr = Header{ .tag = tag_byte, .payload_len = declared_payload_len };
    var iov_data = [_]u8{0} ** (@sizeOf(Header) + MAX_PAYLOAD_BYTES);
    @memcpy(iov_data[0..@sizeOf(Header)], std.mem.asBytes(&hdr));
    @memcpy(iov_data[@sizeOf(Header) .. @sizeOf(Header) + actual_payload.len], actual_payload);
    const total = @sizeOf(Header) + actual_payload.len;

    var iov = [_]std.posix.iovec_const{.{ .base = &iov_data, .len = total }};

    const fd_bytes = fds.len * @sizeOf(i32);
    var cbuf: [128]u8 align(@alignOf(linux.cmsghdr)) = undefined;
    @memset(&cbuf, 0);
    const cmsg: *linux.cmsghdr = @ptrCast(@alignCast(&cbuf));
    cmsg.len = cmsgLen(fd_bytes);
    cmsg.level = std.posix.SOL.SOCKET;
    cmsg.type = 0x01; // SCM_RIGHTS (Linux UAPI; see fdpass.zig K2 note)
    const data_ptr: [*]u8 = @ptrCast(cmsg);
    const fd_off = cmsgAlign(@sizeOf(linux.cmsghdr));
    var idx: usize = 0;
    while (idx < fds.len) : (idx += 1) {
        const f = fds[idx];
        @memcpy(
            data_ptr[fd_off + idx * @sizeOf(i32) .. fd_off + (idx + 1) * @sizeOf(i32)],
            std.mem.asBytes(&f),
        );
    }

    var msg = std.mem.zeroes(linux.msghdr_const);
    msg.iov = &iov;
    msg.iovlen = 1;
    msg.control = &cbuf;
    msg.controllen = cmsgSpace(fd_bytes);

    while (true) {
        const n = linux.sendmsg(sock, &msg, 0);
        switch (linux.errno(n)) {
            .SUCCESS => return,
            .INTR => continue,
            else => return error.SendMsgFailed,
        }
    }
}

/// The number a throwaway `/dev/null` open receives. The kernel always hands
/// out the LOWEST free descriptor, so this is a proxy for "how many low fd
/// slots are occupied right now" — sampled before and after the burst, it
/// detects a leak of even one descriptor without parsing /proc/self/fd.
fn probeNextFd() i32 {
    const rc = linux.open("/dev/null", .{ .ACCMODE = .RDONLY }, 0);
    if (linux.errno(rc) != .SUCCESS) return -1;
    const fd: i32 = @intCast(rc);
    _ = linux.close(fd);
    return fd;
}

test "R9 (review wave 2): a burst of hostile fd-bearing responses leaks no descriptor" {
    // /dev/null is a real fd we can attach repeatedly (sending an fd hands the
    // peer a dup — the source stays valid for the whole burst).
    const dn = linux.open("/dev/null", .{ .ACCMODE = .RDONLY }, 0);
    try testing.expect(linux.errno(dn) == .SUCCESS);
    const src: i32 = @intCast(dn);
    defer _ = linux.close(src);

    var sv: [2]i32 = undefined;
    try testing.expect(linux.errno(linux.socketpair(linux.AF.UNIX, linux.SOCK.STREAM, 0, &sv)) == .SUCCESS);
    defer _ = linux.close(sv[0]); // the "helper" end — forges hostile replies
    defer _ = linux.close(sv[1]); // the "broker" end — runs recvResponseWithFd

    // Baseline sampled AFTER every persistent fd (src, sv[0], sv[1]) is open, so
    // a clean burst returns the next-free slot to exactly this number.
    const baseline = probeNextFd();
    try testing.expect(baseline >= 0);

    const ROUNDS = 8; // a sustained burst — the EMFILE threat model is volume
    var i: usize = 0;
    while (i < ROUNDS) : (i += 1) {
        var rx: [256]u8 = undefined;

        // (1) G3 — malformed-short: the header claims a 200-byte payload the
        // frame never carries, but a SCM_RIGHTS fd rides along. recvmsg installs
        // the fd; the length check then rejects → the errdefer must drop the fd.
        try testSendFramedWithFds(sv[0], @intFromEnum(ErrTag.ok), 200, &[_]u8{}, &[_]i32{src});
        try testing.expectError(error.MalformedResponse, recvResponseWithFd(sv[1], &rx));

        // (2) M6 — a WELL-FORMED reply carrying TWO fds (protocol says one).
        // Both fit the receiver's control buffer (no truncation), so the extras-
        // close loop must drop fds[1] and hand fds[0] to us. We close fds[0], so
        // only a genuine leak of the extra would survive to move the probe.
        try testSendFramedWithFds(sv[0], @intFromEnum(ErrTag.ok), 4, &[_]u8{ 1, 2, 3, 4 }, &[_]i32{ src, src });
        const r = try recvResponseWithFd(sv[1], &rx);
        try testing.expect(r.fd >= 0);
        _ = linux.close(r.fd);

        // (3) M6/CTRUNC — THREE fds overflow the receiver's 24-byte control
        // buffer. The kernel installs what fits, closes the overflow itself, and
        // raises MSG_CTRUNC; recvResponseWithFd drops the fds it did see (M6 loop
        // + errdefer) and rejects. Nothing on either side is left open.
        try testSendFramedWithFds(sv[0], @intFromEnum(ErrTag.ok), 0, &[_]u8{}, &[_]i32{ src, src, src });
        try testing.expectError(error.MalformedResponse, recvResponseWithFd(sv[1], &rx));
    }

    // After ~24 hostile fd-bearing frames the next-free fd must be EXACTLY where
    // it started. A regression in the G3 errdefer or the M6 extras-close would
    // have pinned ROUNDS-or-more slots and moved this number well past baseline.
    const after = probeNextFd();
    try testing.expectEqual(baseline, after);
}
