Skip to content

Report AF_UNIX datagram peer close as EOF - #262

Merged
jserv merged 1 commit into
mainfrom
recvfrom
Aug 2, 2026
Merged

Report AF_UNIX datagram peer close as EOF#262
jserv merged 1 commit into
mainfrom
recvfrom

Conversation

@jserv

@jserv jserv commented Aug 2, 2026

Copy link
Copy Markdown
Contributor

macOS has no AF_UNIX SOCK_SEQPACKET, so sys_socketpair substitutes SOCK_DGRAM. A macOS AF_UNIX datagram socket reports ECONNRESET (errno 54 -> Linux 104) on recv/read once the peer closes, whereas Linux never surfaces ECONNRESET on an AF_UNIX datagram receive -- SEQPACKET returns a clean EOF (0). Rust std uses an AF_UNIX SOCK_SEQPACKET socketpair as its exec CLOEXEC status channel and treats a recv error there as unreachable, so every Rust program that spawns a subprocess aborted with SIGABRT ("the CLOEXEC pipe failed: ... ConnectionReset"). On Ubuntu 25.10, whose coreutils are the Rust uutils build, this reached ordinary shell usage.

recv_eof_or_errno() folds the substitute's peer-close ECONNRESET into a clean EOF. It scopes the translation by probing the pinned host fd (getsockopt SO_TYPE + getsockname) and firing only for AF_UNIX SOCK_DGRAM, so a genuine reset on the AF_UNIX SOCK_STREAM substitute (socket()/accept SEQPACKET) or an INET UDP ICMP-unreachable ECONNRESET is left untouched. Probing the pinned host fd on the rare error tail is race-free against guest fd reuse and off the socket hot path. It is applied at every receive site: sys_recvfrom, both sys_recvmsg paths, sys_recvmmsg, sys_read, sys_readv, and the vCPU-loop fast-path read. The recvfrom/recvmsg/recvmmsg EOF paths also write back the empty addrlen/msg_namelen/msg_controllen/msg_flags a real 0-length receive would report.

Close #260


Summary by cubic

Treat AF_UNIX SEQPACKET-over-DGRAM peer close as EOF on macOS to match Linux, fixing Rust subprocess aborts caused by ECONNRESET. Applies to all recv/read paths, clears address/control metadata, and stops recvmmsg(vlen>1) at EOF to avoid hangs.

  • Bug Fixes
    • Added recv_eof_or_errno() to fold host ECONNRESET into EOF only for AF_UNIX SOCK_DGRAM when the guest cached type is SOCK_SEQPACKET (via SO_TYPE + getsockname), preserving errno across probes.
    • Wired into sys_recvfrom, both sys_recvmsg paths, sys_recvmmsg (records one empty message, then ends the batch), sys_read, sys_readv, and the vCPU fast-path read.
    • Preserves real resets for AF_UNIX SOCK_STREAM, genuine AF_UNIX SOCK_DGRAM, and INET UDP (ICMP unreachable).
    • On EOF, writes back empty addrlen/msg_namelen/msg_controllen/msg_flags to match Linux.
    • Added tests: EOF across recv/read/readv/recvfrom/recvmsg/recvmmsg, no fold for AF_UNIX SOCK_DGRAM, and recvmmsg(vlen>1) does not hang.

Written for commit 1e42743. Summary will update on new commits.

Review in cubic

@jserv
jserv requested review from Max042004 and doanbaotrung August 2, 2026 06:26
cubic-dev-ai[bot]

This comment was marked as resolved.

@doanbaotrung

doanbaotrung commented Aug 2, 2026

Copy link
Copy Markdown
Collaborator

Reviewed and tested d109016 on macOS arm64 (Apple silicon). Looks correct — no blocking issues, approve from me.

Verified

  • Rust process::spawn reproducer (Rust-uutils timeout from an Ubuntu 25.10 arm64 sysroot): 10/10 pass, was 0/10 before the change.
  • tests/test-socket: 18/18 pass, including the new 16 / 17 / 18.
  • tests/test-matrix.sh elfuse-aarch64: 243 passed / 1 failed. The one failure is test-hello, which I can't build locally (no aarch64-none-elf-as) — unrelated to this change.
  • The fold doesn't mask real errors: a failed exec still gives rc=127 + "No such file or directory", child exit status still propagates (rc=1), and a real timeout still gives rc=124.
  • End-to-end: a D-Bus-activated GTK application in the same sysroot now gets through its Rust-based subprocess spawns and completes full toolkit startup, which it never did before this change. It later fails for an unrelated reason on my side, but the ECONNRESET abort is gone.

On the design

The three-part predicate in recv_eof_or_errno() looks tight:

  • ECONNRESET short-circuits first, so the probes stay off the hot path.
  • errno is saved and restored around them.
  • Test 17 pins down that genuine AF_UNIX datagram sockets still surface ECONNRESET rather than being folded.

Coverage across read / readv / recv / recvfrom / recvmsg / recvmmsg plus the syscall_dispatch fast path looks complete, and the fast path correctly probes before releasing the pinned host fd.

I also checked whether EXPECTED_BASELINES in test-matrix.sh needed bumping — it doesn't, 238 is a floor and the run clears it.

macOS has no AF_UNIX SOCK_SEQPACKET, so sys_socketpair substitutes
SOCK_DGRAM. A macOS AF_UNIX datagram socket reports ECONNRESET (errno
54 -> Linux 104) on recv/read once the peer closes, whereas Linux
SEQPACKET returns a clean EOF (0). Rust std uses AF_UNIX SOCK_SEQPACKET
socketpair as its exec CLOEXEC status channel and treats a recv error
there as unreachable, so every Rust program that spawns a subprocess
aborted with SIGABRT ("the CLOEXEC pipe failed: ... ConnectionReset").
On Ubuntu 25.10, whose coreutils are the Rust uutils build, this reached
ordinary shell usage.

recv_eof_or_errno() folds that peer-close ECONNRESET into a clean EOF.
It fires only when errno is ECONNRESET, the guest's cached SO_TYPE is
SEQPACKET, and the pinned host fd is an AF_UNIX datagram socket -- the
two facts that together identify the socketpair substitute. This leaves
alone: genuine AF_UNIX SOCK_DGRAM sockets (Linux never returns EOF
there, it blocks or reports EAGAIN); the AF_UNIX SOCK_STREAM substitute
used for socket()/accept SEQPACKET (a genuine abort there is a real
reset); and INET UDP (ICMP-unreachable ECONNRESET, which Linux surfaces
as ECONNREFUSED). Probing the pinned host fd on the rare error tail is
race-free against guest fd reuse and off the socket hot path.

Close #260
@jserv
jserv merged commit 67d9ef8 into main Aug 2, 2026
10 checks passed
@jserv
jserv deleted the recvfrom branch August 2, 2026 12:53
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.

Rust programs abort when spawning a subprocess: recvfrom returns ECONNRESET where Linux returns EOF

2 participants