Skip to content

[draft] fix(net): symmetric named-AF_UNIX datagram gating (reference implementation)#130

Draft
dzerik wants to merge 1 commit into
multikernel:mainfrom
dzerik:fix/net-unix-dgram-symmetry
Draft

[draft] fix(net): symmetric named-AF_UNIX datagram gating (reference implementation)#130
dzerik wants to merge 1 commit into
multikernel:mainfrom
dzerik:fix/net-unix-dgram-symmetry

Conversation

@dzerik

@dzerik dzerik commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

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_UNIX SOCK_DGRAM sendmsg/sendmmsg under a destination policy with the unix fs-gate off returns EAFNOSUPPORT, while the identical sendto Continues and works. The IP send path (send_msghdr_on_behalfparse_ip_from_sockaddrNone) fails closed for a non-IP address, but sendto's non-IP branch (_ => Continue) does not — so the two syscalls disagree on the same destination.

Separately, sendto's _ => Continue decides on the transient address family: an AF_INET socket presenting an AF_UNIX address at check time Continues, and a racing thread can then swap msg_name to 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_behalf returns a MsgOutcome: a non-connected, non-IP address Continues when the socket is AF_UNIX (the kernel constrains an AF_UNIX socket to AF_UNIX destinations, so a swap to an IP is kernel-rejected — the re-read race can't bypass the IP policy), and fails closed with EAFNOSUPPORT on an AF_INET socket.
  • sendmsg/sendmmsg return Continue on that outcome (in a batch only at entry 0, since a unix socket carries only unix entries).
  • sendto's _ => Continue is tightened the same way, closing the TOCTOU above.
  • Connected 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): execve needs a Landlock EXECUTE grant, and any grant turns the fs-gate on. fork()/work_fn installs 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_behalf under an off fs-gate is the OCI in-sandbox PID-1 (ChildEntry::InProcess): a no-exec child 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-core lib suite is green (444) with no regressions; cargo clippy adds no new warnings. Happy to fold this in (or drop it) per your read on priority.

@congwang-mk

Copy link
Copy Markdown
Contributor

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.
@dzerik
dzerik force-pushed the fix/net-unix-dgram-symmetry branch from f8e3ea9 to a4f6d97 Compare July 15, 2026 09:15
@dzerik

dzerik commented Jul 15, 2026

Copy link
Copy Markdown
Contributor Author

You're right — the sendmsg/sendmmsg Continue was TOCTOU-exploitable, and gating on the socket domain doesn't fix it: socket_is_unix reads the pinned dup snapshot, but Continue makes the kernel re-read the live sockfd, so a racing dup2(inet_sock, sockfd) + msg_name swap still rides out to a denied IP.

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 dup_fd — the kernel never re-reads sockfd, so the swap window is gone. sendto, sendmsg, and sendmmsg now behave identically and never Continue under a destination policy:

  • Non-IP address on a unix-domain socket → send on-behalf, no IP check (the fs-gate-on case is still handled upstream by unix_sendmsg_gate).
  • Non-IP address on a non-unix socket → EAFNOSUPPORT (the address-family-swap shape).
  • No destination policy → Continue (nothing to bypass), unchanged.

The decision is a pure classify_send_path(connected, ip, is_unix_socket) in verdict.rs with unit tests for all four arms; the handler wiring is exercised by the kernel integration suite. Rust CI is green.

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.)

@congwang-mk

Copy link
Copy Markdown
Contributor

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants