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.
Recommended fix
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)
PoC verification
Evidence pack
findings/poc/DF-2836 Β· 12 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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
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 sourcerun.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
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 (seefix_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
fixedApplied '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']
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.
No comments yet.