[draft] fix(net): symmetric named-AF_UNIX datagram gating (reference implementation)#130
[draft] fix(net): symmetric named-AF_UNIX datagram gating (reference implementation)#130dzerik wants to merge 1 commit into
Conversation
|
I think the sendto part is a reasonable fix, the sendmsg/sendmmsg part may regress from fail-closed to a TOCTOU-exploitable Continue. |
A named/abstract AF_UNIX SOCK_DGRAM sendmsg/sendmmsg under a destination policy (with the unix fs-gate off) failed closed with EAFNOSUPPORT, while the identical sendto Continue'd and worked -- the two syscalls disagreed on the same destination. Rather than resolve this by Continuing sendmsg (which would decide on the transient address family and let a racing dup2(inet_sock, sockfd)+msg_name swap ride out on the kernel's re-read to a denied IP), gate on the stable socket domain and send the unix datagram ON-BEHALF on the pinned fd: - Add a pure `classify_send_path(connected, ip, is_unix_socket)` verdict. A non-IP address on a unix-domain socket -> UnixOnBehalf (send, no IP check); on a non-unix socket -> Reject (EAFNOSUPPORT, the addr-family swap shape). - send_msghdr_on_behalf (sendmsg/sendmmsg) uses it: a unix datagram is materialized and sent on the pinned dup_fd, never Continue. - sendto's non-IP branch matches: no destination policy -> Continue (nothing to bypass); policy active -> send a unix datagram on-behalf, fail closed on a non-unix socket. All three send paths now behave identically for unix datagrams and never Continue under a destination policy, closing the re-read TOCTOU window. The decision is unit-tested in verdict.rs (four SendPath arms); the handler wiring is exercised by the kernel integration suite.
f8e3ea9 to
a4f6d97
Compare
|
You're right — the sendmsg/sendmmsg Continue was TOCTOU-exploitable, and gating on the socket domain doesn't fix it: Reworked (rebased onto current main after the #128 split; force-pushed). Instead of Continue, a named/abstract unix datagram is now sent on-behalf on the pinned
The decision is a pure Still a draft — priority and the in-sandbox path are your call. (Related: the chroot named-AF_UNIX gate has the same Continue-on-allow shape on its lexical fast path; I filed that separately as #143 so this PR stays scoped to the datagram symmetry.) |
|
The sendto tightening is a high-priority fix, in my opinion. Maybe you want to separate it out as a fix which could land quickly. |
|
Split the sendto tightening out into #154 as you suggested — it's the high-priority half and lands standalone (pins the fd, gates on the stable socket domain, fail-closed instead of Continue). This PR keeps the sendmsg/sendmmsg symmetry; I'll rebase it onto main once #154 merges so it becomes a pure consumer of the shared |
…olicy TOCTOU Splits the high-priority sendto half out of multikernel#130 (per the maintainer's request to land it as a fix that can go in quickly). A non-IP sendto with no fs-path gate previously fell through to `_ => Continue`, letting the kernel re-read `sockfd` and the destination after the supervisor's decision. Under an active destination policy a child could race a `dup2(inet_sock, sockfd)` + address swap into that window and ride the Continue out to a denied IP. Under `has_net_destination_policy` the arm no longer Continues: it pins the fd via `dup_fd_from_pid`, classifies on the STABLE socket domain via the new pure `classify_send_path` helper, and either sends the unix datagram on-behalf on the pinned fd (UnixOnBehalf) or fails closed with EAFNOSUPPORT. With no destination policy there is nothing to bypass, so Continue is preserved. The non-UnixOnBehalf outcomes collapse to a single fail-closed errno arm rather than unreachable!(), so any future drift in the classifier fails closed instead of panicking (a panic on this seccomp-notif path would unwind the supervisor and DoS the sandbox). The pure `classify_send_path` helper and its 4 unit tests land here (sendto is its first consumer); `send_path_non_ip_on_non_unix_socket_is_rejected` is the deterministic fail-without-fix witness (pre-fix logic yielded Continue). The sendmsg/sendmmsg symmetric rework consumes the same helper and stays in draft multikernel#130 — so until multikernel#130 lands, sendto and sendmsg differ on a non-IP unix datagram (sendto sends on-behalf, sendmsg still returns EAFNOSUPPORT); this preserves the pre-existing sendto behavior (it already sent via Continue) while closing its TOCTOU, and multikernel#130 aligns sendmsg. Behavior scope (fail-closed hardening, only under a destination policy): a non-IP send on a non-unix/non-IP datagram family (AF_PACKET/AF_VSOCK/...) now returns EAFNOSUPPORT instead of Continue; abstract/named-fs-gate-off unix datagrams now send on-behalf, so the receiver observes the supervisor's SO_PEERCRED rather than the child's. Same Continue-shape as the chroot AF_UNIX gate report (multikernel#143).
fix(net): close a sendto non-IP destination-policy TOCTOU (sendto half of #130)
Draft / reference implementation — not requesting merge. Posted so the concrete shape of the fix is reviewable; you own the OCI in-sandbox path, so priority and testing are your call.
Finding
A named (pathname/abstract)
AF_UNIXSOCK_DGRAMsendmsg/sendmmsgunder a destination policy with the unix fs-gate off returnsEAFNOSUPPORT, while the identicalsendtoContinues and works. The IP send path (send_msghdr_on_behalf→parse_ip_from_sockaddr→None) fails closed for a non-IP address, butsendto's non-IP branch (_ => Continue) does not — so the two syscalls disagree on the same destination.Separately,
sendto's_ => Continuedecides on the transient address family: anAF_INETsocket presenting anAF_UNIXaddress at check timeContinues, and a racing thread can then swapmsg_nameto a denied IP before the kernel re-reads it — a latent TOCTOU on the destination policy.Fix
Gate on the stable socket domain (
socket_is_unix,SO_DOMAIN), not the address family:send_msghdr_on_behalfreturns aMsgOutcome: a non-connected, non-IP addressContinues when the socket isAF_UNIX(the kernel constrains anAF_UNIXsocket toAF_UNIXdestinations, so a swap to an IP is kernel-rejected — the re-read race can't bypass the IP policy), and fails closed withEAFNOSUPPORTon anAF_INETsocket.sendmsg/sendmmsgreturnContinueon that outcome (in a batch only at entry 0, since a unix socket carries only unix entries).sendto's_ => Continueis tightened the same way, closing the TOCTOU above.AF_UNIX(the fix(net): pass through connected AF_UNIX sendmsg under a destination policy #125 path) is unchanged — still on-behalf.Reachability — why this is low-severity and hard to test
The precondition is the unix fs-gate being off, which means empty fs grants, which means Landlock denies all filesystem access. That makes the path unreachable through every exec-based run (
run/spawn):execveneeds a Landlock EXECUTE grant, and any grant turns the fs-gate on.fork()/work_fninstalls a kernel-only seccomp filter with no user-notification supervisor, so the on-behalf handler never runs there either.The only path that reaches
sendmsg_on_behalfunder an off fs-gate is the OCI in-sandbox PID-1 (ChildEntry::InProcess): a no-execchild with the full notif supervisor. So this affects a self-confining process (empty fs, net policy, named-unix datagram) — real, but niche, hence no destination-policy bypass and only an availability/consistency wart. A dedicated end-to-end test needs the OCI in-process harness (the python integration harness can't run under an off fs-gate), which is why this is a draft rather than a merge-ready PR with its own regression test.Full
sandlock-corelib suite is green (444) with no regressions;cargo clippyadds no new warnings. Happy to fold this in (or drop it) per your read on priority.