fix(net): close a sendto non-IP destination-policy TOCTOU (sendto half of #130)#154
fix(net): close a sendto non-IP destination-policy TOCTOU (sendto half of #130)#154dzerik wants to merge 1 commit into
Conversation
…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).
congwang-mk
left a comment
There was a problem hiding this comment.
The TOCTOU diagnosis is correct, and the fail-closed non-unix arm plus the pure classify_send_path helper are the right shape. Two problems in the UnixOnBehalf arm block this as written:
1. On-behalf abstract sends bypass Landlock abstract-socket scoping. The _ arm receives abstract addresses (named_unix_socket_path returns None for them; its comment even says "Landlock scope handles it"). LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET is enforced against the credentials of the process doing the sendto. Pre-PR, Continue ran the child's own syscall under the child's Landlock domain. Post-PR the unconfined supervisor performs the send, so enabling an IP destination policy deterministically disables abstract-socket scoping for unconnected sendto: the child can message any abstract socket on the host. That trades a racy IP-policy bypass for a deterministic Landlock-boundary bypass (same shape as #27).
Fix: fail closed for abstract destinations (EAFNOSUPPORT), matching what send_msghdr_on_behalf already does and what #130 aligns with anyway.
2. Named paths (fs gate off) resolve in the supervisor's context. The arm passes the raw child addr_bytes to resolve_send, so sun_path resolves against the supervisor's cwd/root instead of the child's. A relative sun_path silently targets the wrong path, and a chroot child's absolute path would resolve host-side. Every other named-unix on-behalf path resolves via /proc/<pid>/root (resolve_named_unix_target) for exactly this reason.
Fix: pin the inode via a /proc/<pid>/root resolution variant (grant check skipped) and send to /proc/self/fd/<pin>; rejecting relative sun_path with an errno is fine, resolving it against the wrong cwd is not.
Both fixes are user-visible behavior (abstract datagrams under a destination policy go from racy-working to EAFNOSUPPORT), so please note them in the behavior-scope section. No family carve-out needed for AF_VSOCK/AF_PACKET; fail-closed there is fine as flagged.
Also worth adding: a happy-path integration test that a named unix datagram still sends under a net_allow policy; that is the path both issues live on, and it would have caught the relative-path regression.
Splits the high-priority sendto half out of #130, as you suggested on that PR ("The sendto tightening is a high-priority fix... Maybe you want to separate it out as a fix which could land quickly"). The sendmsg/sendmmsg symmetry stays in draft #130; I'll rebase it onto this once this lands.
The bug
A non-IP
sendtowith no fs-path gate fell through to_ => NotifAction::Continue, so the kernel re-readsockfdand the destination address after the supervisor's decision. Under an active destination policy a child could race adup2(inet_sock, sockfd)+ address swap into that window and ride theContinueout to a denied IP. (SameContinue-shape as the chroot AF_UNIX gate report, #143.)The fix
Under
has_net_destination_policythe arm no longerContinues: it pins the fd viadup_fd_from_pid, classifies on the stable socket domain of the pinned fd (via the new pureclassify_send_path), and either sends the unix datagram on-behalf on that pinned fd or fails closed withEAFNOSUPPORT. With no destination policy there is nothing to bypass, soContinueis preserved unchanged.The non-
UnixOnBehalfoutcomes collapse to a single fail-closed errno arm rather thanunreachable!(), so any future drift in the classifier fails closed instead of panicking — a panic on this seccomp-notif path would unwind the supervisor task and DoS the sandbox.Testing
The pure
classify_send_pathhelper + its 4 unit tests land here (sendto is its first consumer).send_path_non_ip_on_non_unix_socket_is_rejectedis the deterministic fail-without-fix witness: pre-fix logic yieldedContinuefor that input, the helper now yieldsReject/EAFNOSUPPORT.I deliberately did not add an integration test for the fail-closed errno:
sendto(AF_INET dgram fd, AF_UNIX sockaddr)returnsEAFNOSUPPORTfrom the kernel too, so such a test is green pre- and post-fix and would be decorative. The bypass this closes is a race that isn't deterministically reproducible from a guest, so the pure-helper unit test is the honest witness.classify_send_pathispub(crate)(no SDK/FFI surface). It is the shared classifier the sendmsg/sendmmsg rework in #130 consumes fully; landing it here with sendto as first consumer lets #130 become a pure consumer on rebase.Behavior scope (please review)
This is fail-closed hardening that only triggers under a destination policy, but it does change some behavior — flagging explicitly:
sendtoon a non-unix, non-IP datagram family (AF_PACKET/AF_VSOCK/…) now returnsEAFNOSUPPORTinstead ofContinue. (Narrow:AF_NETLINKis virtualized to a unix socket and stays on the on-behalf path;AF_PACKETneedsCAP_NET_RAW.)connect_on_behalfstillContinues these families, so there is a connect-vs-sendto asymmetry.SO_PEERCRED/SCM_CREDENTIALSrather than the child's. Enabling an IP allowlist thus changes unix-datagram credential semantics (D-Bus/journald/etc.). This is intrinsic to closing the TOCTOU (Continue is unsafe once the fd can be swapped).sendtoandsendmsgdiffer on a non-IP unix datagram (sendto sends on-behalf; sendmsg still returnsEAFNOSUPPORT). This preserves sendto's pre-existing behavior (it already sent, viaContinue) while closing its TOCTOU; [draft] fix(net): symmetric named-AF_UNIX datagram gating (reference implementation) #130 aligns sendmsg.Happy to add a family carve-out (fail closed only for
AF_INET/AF_INET6sockets) if you'd prefer to keep exotic non-unix datagram families working.