β¬’ DragonFlyBSD Kernel Audit
← triage Β· dashboard
DF-2836

sonewconn_faddr() inherits the listener's entire so_state β€” accepted connections born half-shut (SS_CANTSENDMORE/SS_CANTRCVMORE/SS_ASYNC) after listener shutdown

Field Value
ID DF-2836
Status new
Severity Low
CVSS 3.1 CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:N/I:N/A:L
CWE CWE-459 Incorrect Element Assignment
File sys/kern/uipc_socket2.c
Lines 383
Area kern
Confidence certain
Discovered 2026-08-31
Pass 2 (GLM 5.3 second pass)
Bucket base:kern
Reported pending
Known CVE none
CVE match novel

Summary

uipc_socket2.c:383 assigns so->so_state = head->so_state | SS_NOFDREF | SS_ASSERTINPROG, copying every dynamic listener state bit into each inbound-connection socket. A listener that has been shutdown(SHUT_WR) carries SS_CANTSENDMORE, so every child created afterwards (AF_UNIX unp_connect β†’ sonewconn_faddr) is born write-dead: accept() returns a socket whose first send(2) is EPIPE while recv(2) delivers the peer's data. FreeBSD fixed this class by inheriting only SS_NBIO; DragonFly keeps NBIO in fp->f_flag so nothing should be inherited. Canonical trigger: the graceful daemon-restart pattern (shutdown listener while draining the accept queue) β€” connections accepted in that window are silently dead-on-arrival.

Proof of contest

VERIFIED on the stock INVARIANTS guest (findings/poc/DF-2836/ stateinherit.c, unpriv): AF_UNIX bind+listen, shutdown(l, SHUT_WR), connect, accept β†’ recv(accepted)=4 (live) then send(accepted)=βˆ’1 EPIPE errno 32 (3/3 runs; first untrapped run died of SIGPIPE). Fix (so->so_state = SS_NOFDREF | SS_ASSERTINPROG) validated via in-guest nativekernel rebuild: patched 3/3 success.

Inherit no listener state bits (validated diff in the pack).

Timeline

  • 2026-08-31 Discovered during pass-2 audit of uipc_socket2.c (GLM 5.3); deterministic unpriv EPIPE reproduced + fix validated same run.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-2836 Β· 12 files
FileTypeDescriptionSize
README.md β€” 2.0 KB ↓ raw
VERDICT.md β€” 2.4 KB ↓ raw
stateinherit.c β€” 3.5 KB view raw
build.sh β€” 47 B view raw
run.sh β€” 25 B view raw
run.log β€” 374 B view raw
run.2.log β€” 367 B view raw
fix_validation.log β€” 386 B view raw
fix.diff β€” 607 B view raw
env.txt β€” 261 B view raw
manifest.json β€” 713 B view raw
verdict.json β€” 3.0 KB view raw

DF-2836 β€” sonewconn_faddr() inherits the listener's entire so_state

What

sys/kern/uipc_socket2.c:383:

so->so_state = head->so_state | SS_NOFDREF | SS_ASSERTINPROG;

A socket created for an inbound connection inherits every state bit of the listener at birth. A listener that has been shutdown(SHUT_WR) carries SS_CANTSENDMORE (and SHUT_RD carries SS_CANTRCVMORE, fcntl(F_SETFL, O_ASYNC carries SS_ASYNC, etc.), so every subsequently accepted socket is born half-shut / pre-flagged. Upstream FreeBSD fixed this decades ago by inheriting only the NBIO bit (so->so_state &= SS_NBIO in sonewconn); DragonFly has no SS_NBIO in so_state (nonblocking lives in fp->f_flag), so the correct DragonFly form is to inherit nothing.

Impact

Unprivileged local user (or a peer connecting to a service performing a graceful listener shutdown during restart): accept() returns a socket that is dead-on-arrival in one or both directions (send() β†’ EPIPE, recv() β†’ immediate EOF) although the connection is fully live at the transport layer. Correctness/availability defect, no memory unsafety.

Reproduce

Unprivileged:

cc -O -o stateinherit stateinherit.c
./stateinherit

Expected on a correct stack (and on the patched kernel validated in this pack): unix: accepted send = 4 β€” the accepted socket behaves like a fresh connection.

Observed on the stock kernel: unix: accepted send = -1 errno=32 [Broken pipe] while accepted recv = 4 proves the connection is live β€” the child was born with the listener's SS_CANTSENDMORE.

(The TCP variant is masked in this scenario because shutdown(SHUT_WR) on a TCP listener makes tcp refuse new SYNs β€” "Connection refused" β€” so the AF_UNIX listen path is the demonstrable path.)

Files

  • stateinherit.c β€” PoC source
  • run.log, run.2.log β€” decisive runs (stock kernel)
  • fix_validation.log β€” same PoC on the one-fix kernel (DF-2836+DF-2838)
  • fix.diff β€” proposed fix (validated in-guest)
  • env.txt β€” guest kernel/compiler state
VERDICT.md
↓ download raw

DF-2836 VERDICT

Status: reproduced (unprivileged, 3/3 runs on the stock INVARIANTS kernel) Impact: dos β€” accepted connections born half-shut; availability/correctness defect, no memory unsafety.

Root cause (path:line)

sys/kern/uipc_socket2.c:383 β€” sonewconn_faddr():

so->so_state = head->so_state | SS_NOFDREF | SS_ASSERTINPROG;

The child socket inherits the listener's entire so_state word. A listener that has been shutdown(SHUT_WR) carries SS_CANTSENDMORE (soshutdown β†’ so_pru_shutdown β†’ socantsendmore, uipc_socket.c), so every child born afterwards (AF_UNIX unp_connect β†’ sonewconn_faddr, uipc_usrreq.c:1215) is born with SS_CANTSENDMORE. accept() hands the user a socket whose first send(2) returns EPIPE while the connection is fully live (data flows in the other direction β€” proven by recv() returning the peer's bytes in the same run).

Upstream FreeBSD inherits only the NBIO bit in its sonewconn (their fix for this same defect); DragonFly keeps NBIO in fp->f_flag, so nothing needs to be inherited at all.

Reproduction

Unprivileged ./stateinherit (see run.log / run.2.log):

unix: shutdown(listener, SHUT_WR) = 0
unix: connect ok, accept = 7
unix: client  send  = 4
unix: accepted recv = 4 (No such file or directory)   <- connection is LIVE
unix: accepted send = -1 errno=32 [Broken pipe]  <-- want 4; EPIPE(32) = BUG

First run before SIGPIPE was trapped died of SIGPIPE β€” itself proof that the kernel treated the freshly accepted socket as write-dead.

TCP variant: shutdown(SHUT_WR) on a TCP listener makes tcp refuse new connections outright ("Connection refused"), so the AF_UNIX listen path is the demonstrable path; the defective line is family-independent.

Fix validation

fix.diff (inherit only SS_NOFDREF | SS_ASSERTINPROG) applied to the guest's /usr/src copy together with the DF-2838 padding fix, kernel rebuilt (make nativekernel KERNCONF=X86_64_GENERIC) and the exact same PoC re-run:

  • baseline (stock): accepted send = -1 errno=32 (3/3)
  • patched: accepted send = 4, full bidirectional data flow (see fix_validation.log)

Behavior change is gone β‡’ fix validated.

Threat

Unprivileged local (AF_UNIX) or any remote peer (TCP configs where the listener keeps accepting after shutdown): connections accepted during a graceful-listener-shutdown window (the canonical daemon-restart pattern) are silently dead-on-arrival. No privilege boundary is crossed.

Fix verification

fixed
baseline reproduced→ patch + rebuild →patched clean

Applied 'so->so_state = SS_NOFDREF | SS_ASSERTINPROG' to the guest /usr/src copy, rebuilt with make nativekernel KERNCONF=X86_64_GENERIC, rebooted, re-ran the identical PoC: baseline 'accepted send = -1 errno=32' 3/3 -> patched 'accepted send = 4' with full bidirectional flow (fix_validation.log). Bad behavior gone; fix validated.

['findings/poc/DF-2836/fix.diff', 'findings/poc/DF-2836/fix_validation.log']
↓ fix.diffDragonFly dfbsd 6.5-DEVELOPMENT #1: Wed Sep 2 04:31:48 UTC 2026 root@dfbsd:/usr/obj/usr/src/sys/X86_64_GENERIC x86_64 (one-fix nativekernel: DF-2836 + DF-2838 edits in uipc_socket2.c)

Confirmed kernel references

Detail

Evidence (decisive lines)

["run.log / run.2.log: 'unix: accepted recv = 4' followed by 'unix: accepted send = -1 errno=32 [Broken pipe]'", 'VERDICT.md: root cause path:line and fix validation', 'fix_validation.log: same PoC on patched kernel returns send=4']

PoC changes

Seed written fresh for this finding: added signal(SIGPIPE, SIG_IGN) and unbuffered stdout after the first run died of SIGPIPE (itself confirmation of the born-half-shut socket); TCP variant retained for documentation (refused at connect because shutdown(SHUT_WR) on a TCP listener stops SYN processing), AF_UNIX variant is the demonstrable path.

Verified recommended fix

In sonewconn_faddr set so->so_state = SS_NOFDREF | SS_ASSERTINPROG (inherit no listener state bits); validated in-guest.

Verdict

sonewconn_faddr (uipc_socket2.c:383) copies the listener's entire so_state into every inbound-connection socket. A listener that has been shutdown(SHUT_WR) carries SS_CANTSENDMORE, so accepted connections are born half-shut: unprivileged AF_UNIX listen+shutdown(SHUT_WR)+connect+accept yields an accepted socket whose first send(2) returns EPIPE while recv(2) on the same socket delivers the peer's data (connection provably live). Reproduced 3/3 on the stock INVARIANTS kernel; FreeBSD inherits only the NBIO bit in the same function. Fixed kernel (inherit only SS_NOFDREF|SS_ASSERTINPROG) validated: send returns 4.