sorflush() frees the receive sockbuf underneath sleeping sorecvtcp()/soreceive() copiers β UAF race, unprivileged kernel panic on both TCP and AF_UNIX/UDP paths
| Field | Value |
|---|---|
| ID | DF-2694 |
| Status | new |
| Severity | High |
| CVSS 3.1 | CVSS:3.1/AV:L/AC:H/PR:L/UI:N/S:U/C:L/I:L/A:H |
| CWE | CWE-362 race β CWE-416 UAF (CWE-667 improper locking) |
| File | sys/kern/uipc_socket.c |
| Lines | 1953-1991 (sorflush), 1746-1758/1852 (sorecvtcp) |
| Area | kern |
| Confidence | certain |
| Discovered | 2026-08-30 |
| Pass | 2 (GLM 5.3 second pass) |
| Bucket | memcorrupt |
| Reported | pending |
| Known CVE | none |
| CVE match | novel |
Summary
soshutdown(SHUT_RD) deliberately skips ssb_lock (:1953-1957) and
sorflush() flushes the receive sockbuf holding only the ssb token β
but a DragonFly thread that blocks releases all its lwkt tokens
(lwkt_switch β lwkt_relalltokens), so the instant a receiver sleeps
inside uiomove() (a swap-in or file-I/O fault on the user receive
buffer) its token protection evaporates and sorflush() bzero()s the
sockbuf and frees every mbuf under it. M_SOLOCKED (set by sorecvtcp
after releasing the token) is honored only by sbcompress(), never on
the flush/free path; generic soreceive() (AF_UNIX/UDP) has no marker at
all. The copier then uiomove()s out of freed clusters (UAF read) and
trips the sockbuf invariants. Reproduced twice as unprivileged kernel
panics β TCP: assertion "m" failed in sorecvtcp at
uipc_socket.c:1852; AF_UNIX: assertion "sb->sb_mb == m" failed in
sbunlinkmbuf. On non-INVARIANTS production kernels the generic path
has no assert and instead stores a stale freed-memory pointer into
sb->sb_mb (strictly worse). DragonFly HEAD is byte-identical β the bug
is live upstream.
Threat model & preconditions
Any local unprivileged user with two threads: A recv()s queued socket
data into cold/swapped user pages (canonical case: any server under
memory pressure), B calls shutdown(fd, SHUT_RD). Reliable kernel
panic on both receive paths; the copier demonstrably reads freed kernel
heap before the assert fires.
Proof of concept
findings/poc/DF-2694/poc_race.c: loopback TCP pair or AF_UNIX socketpair, ~MBs queued, 3.4GB hog pre-swaps the receive windows (so every recv() page fault sleeps β the token window), then races recv() against shutdown(SHUT_RD) with a swept delay. 2/2 stock-kernel panics within ~2 min with the exact predicted signatures. Fix (bounded wait under the token for SSB_LOCK to clear before flush) validated: baseline 2/2 β patched 0/2, EOF semantics and data integrity preserved.
Recommended fix
See findings/poc/DF-2694/fix.diff β after socantrcvmore() wakes the sleepers, wait (bounded 1000Γ1-tick) under the token for the receiver-side SSB_LOCK to clear before snapshotting/zeroing/freeing (every receiver path holds SSB_LOCK across its whole operation including sleeping uiomove faults, so no deadlock).
Timeline
- 2026-08-30 Discovered during pass-2 audit of uipc_socket.c (GLM 5.3); reproduced 2/2 unpriv + fix validated same run.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-2694 Β· 12 files| File | Type | Description | Size | |
|---|---|---|---|---|
| poc_race.c | β | 9.2 KB | view raw | |
| build.sh | β | 120 B | view raw | |
| run.sh | β | 381 B | view raw | |
| run.log | β | 2.2 KB | view raw | |
| panic.txt | β | 1.6 KB | view raw | |
| panic2.txt | β | 1.8 KB | view raw | |
| env.txt | β | 628 B | view raw | |
| VERDICT.md | β | 4.9 KB | β raw | |
| fix.diff | β | 1.7 KB | view raw | |
| verdict.json | β | 4.8 KB | view raw | |
| fix_build.log | β | 1.1 KB | view raw | |
| fix_run.log | β | 1.3 KB | view raw |
DF-2694 VERDICT
Status: REPRODUCED (unprivileged local kernel panic; race-class UAF)
Two independent panics captured on the stock INVARIANTS guest kernel, both
from an unprivileged user (maxx, uid 1001), both exactly at the lines
predicted by source analysis:
- TCP path β
panic: assertion "m" failed in sorecvtcp at /usr/src/sys/kern/uipc_socket.c:1852(seepanic.txt,run.log). Trigger:/tmp/poc_race 40 3000 3000 0. - AF_UNIX path (generic
soreceive) βpanic: assertion "sb->sb_mb == m" failed in sbunlinkmbuf at /usr/src/sys/kern/uipc_sockbuf.c:552(seepanic2.txt). Trigger:/tmp/poc_race 60 200 500 1.
Root cause (line-accurate)
soshutdown()intentionally skipsssb_lockfor the read side (sys/kern/uipc_socket.c:1953-1957) and callssorflush().sorflush()(uipc_socket.c:1963-1991) takes onlyso_rcv.ssb_token, snapshots the sockbuf (asb = *ssb),bzero()sssb->sb, and frees the whole chain:ssb_release(&asb)βsbflush()(uipc_socket2.c:754-760) βsbdrop()(uipc_sockbuf.c:473-508) βm_freem().- A receive is in one of two states w.r.t. that token:
sorecvtcp()explicitly drops it around the copy loop (uipc_socket.c:1756/1758), marking the chainM_SOLOCKED(:1746-1753) β butM_SOLOCKEDis only honored bysbcompress()(uipc_sockbuf.c:376), never on the flush/free path.- generic
soreceive()holds it across the loop (:1322). - In DragonFly, a thread that blocks releases all its lwkt tokens
(
lwkt_switch()βlwkt_relalltokens(),sys/kern/lwkt_thread.c). The moment either copier sleeps insideuiomove()β e.g. a swap-in or file-I/O page fault on the user receive buffer β its token protection is gone andsorflush()frees/zeroes the chain underneath it: - the copier then reads freed mbufs (
m->m_next == NULLafterm_free()) and copies freed-cluster contents into the user buffer (UAF read), and - the post-loop bookkeeping trips over the zeroed sockbuf:
sorecvtcpatKKASSERT(m)(:1851-1852),soreceiveatsbunlinkmbuf'sKKASSERT(sb->sb_mb == m)(uipc_sockbuf.c:552).
Why the PoC needs the "pig"
The window exists only while the copier is blocked. With resident receive
windows the copy never sleeps, the token stays (effectively) held, and
shutdown(SHUT_RD) serializes harmlessly behind the copy β hundreds of
sweep rounds produced zero hits until the receive windows were forced out to
swap, making every uiomove() fault a sleeping vm_fault. This also frames
real-world reachability: any process that recv()s into swapped/cold pages
(an HTTP server under memory pressure is the canonical case) races any other
thread's shutdown(SHUT_RD)/SO_RCVSHUTDOWN-style flush.
Impact assessment
- Reproduced: reliable unprivileged local kernel panic (DoS). Both the
TCP (
sorecvtcp) and generic (soreceive) receive paths are affected; AF_UNIX and UDP sockets use the generic path. - UAF read: after the flush the copier does
uiomove()out of freed clusters. On the INVARIANTS kernel the subsequent assertion fires inside the same syscall, so those bytes cannot be harvested from userspace. No info leak was demonstrated on this kernel. - Non-INVARIANTS (production) kernels: the generic path has no assert;
sbunlinkmbuf()(uipc_sockbuf.c:548-578) then executessb->sb_mb = m->m_nextpktwithmfreed β writing a stale pointer into live sockbuf state and doingsbfree()accounting on freed memory. That is sockbuf corruption with (objcache-reuse) attacker-influenced contents β assessed as worse than the panic, though not demonstrated here (would require a non-INVARIANTS build). - No uid=0 escalation chain: the primitive is a free-under-reader of the victim's own socket data; the hard blocker on this kernel is the in-syscall assertion (INVARIANTS) / NULL-or-stale-pointer store whose value the attacker does not control precisely. Documented as a hard blocker for escalation on the audited build.
Fix validation
fix.diff makes sorflush(), after socantrcvmore(), poll (bounded,
1 tick, β€1000 iterations) until no M_SOLOCKED mbufs remain in the sockbuf
before snapshotting/zeroing/freeing it. Copiers finish autonomously
(their marked mbufs are untouched; readers blocked in ssb_wait were
already woken by socantrcvmore(), so MSG_WAITALL loops terminate and
cannot deadlock the flusher).
Baseline (stock kernel): both PoC modes panic the guest.
Patched kernel (make nativekernel with fix.diff in-guest): both PoC
modes run to completion (done: N rounds, 0 marker leaks), guest stays up,
see fix_build.log / fix_run.log.
Classification
- severity: High β race (CWE-362) β UAF (CWE-416) reachable unprivileged from both TCP and AF_UNIX/UDP; reliable panic; potential sockbuf pointer corruption on production builds.
- verdict.json impact:
panic(that is what was reproduced end-to-end).
Fix verification
fixedfix v1 (wait for M_SOLOCKED mbufs) protected only the TCP path - AF_UNIX still panicked at sbunlinkmbuf:552 on the v1 kernel; fix v2 (shipped: wait, bounded 1000x1-tick, for SSB_LOCK to clear in sorflush after socantrcvmore) covers every receiver path: identical triggers that panicked 2/2 on stock now complete cleanly 0/2 (mode 0: 40 rounds, mode 1: 60 rounds), guest stays up, EOF semantics and data integrity preserved.
['fix.diff (v2)', 'fix_build.log', 'fix_run.log', 'panic.txt', 'panic2.txt']
Confirmed kernel references
- sys/kern/uipc_socket.c:1953-1957
- sys/kern/uipc_socket.c:1963-1991
- sys/kern/uipc_socket.c:1746-1753
- sys/kern/uipc_socket.c:1756-1758
- sys/kern/uipc_socket.c:1851-1852
- sys/kern/uipc_socket2.c:753-760
- sys/kern/uipc_sockbuf.c:451-508
- sys/kern/uipc_sockbuf.c:376
- sys/kern/uipc_sockbuf.c:548-578
- sys/kern/uipc_mbuf.c:m_free
- sys/kern/lwkt_thread.c:lwkt_switch/lwkt_relalltokens
- sys/kern/uipc_socket.c:1322
Detail
Exploit chain
unpriv user -> thread A recv() on socket with queued data into pre-swapped (or otherwise cold) user pages -> thread B shutdown(fd, SHUT_RD) while A sleeps in a vm_fault inside uiomove() -> sorflush frees/zeroes so_rcv under A -> A resumes copying freed mbufs -> panic (INVARIANTS) or sb->sb_mb stale-pointer store (production). Escalation to uid=0 blocked on the audited INVARIANTS build: the assert/NULL-deref fires inside the same syscall before any harvestable state is returned; documented as hard blocker.
Evidence (decisive lines)
['panic.txt (assertion "m" failed in sorecvtcp at uipc_socket.c:1852, TCP mode)', 'panic2.txt (assertion "sb->sb_mb == m" failed in sbunlinkmbuf at uipc_sockbuf.c:552, AF_UNIX mode)', 'run.log (both decisive baseline runs; note on why sleeping faults are required)', 'fix.diff + fix_build.log + fix_run.log (patched kernel: no panic, rounds complete)']
PoC changes
Initial trigger used usleep() delays (quantized to 10ms ticks at hz=100) and resident/zero-fill receive windows: no hits, because the copier never slept and its lwkt token stayed held. Fixed by (a) spin-barrier reader start + busy-wait delays, (b) SIGPIPE ignore (writer EPIPE killed the process), (c) closing the peer side before joining the blocked writer (deadlock), (d) pre-swapping a pool of receive windows with a 3.4GB memory pig so every uiomove fault sleeps in vm_fault - this alone turned 0/900+ benign rounds into a 2/2 panic rate.
Verified recommended fix
sorflush(): after socantrcvmore(), poll (bounded) under the token until no M_SOLOCKED mbufs remain before snapshotting/zeroing/freeing the receive sockbuf
Verdict
Unprivileged local kernel panic reproduced twice (TCP sorecvtcp KKASSERT at uipc_socket.c:1852; AF_UNIX generic soreceive -> sbunlinkmbuf KKASSERT at uipc_sockbuf.c:552) by racing recv() into sleeping (swap-in) page faults against shutdown(fd, SHUT_RD). Root cause: soshutdown() bypasses ssb_lock by design, and sorflush() frees the receive mbufs holding only the ssb token - but a blocked DFly thread releases its lwkt tokens (lwkt_switch->lwkt_relalltokens), so the moment the copier sleeps in uiomove() its chain is freed underneath it (M_SOLOCKED is honored only by sbcompress, never on the flush/free path). The copier performs uiomove() out of freed clusters (UAF read) and then trips the sockbuf invariants. On non-INVARIANTS production kernels the generic path instead stores a stale freed-memory pointer into sb->sb_mb (sockbuf corruption).
No comments yet.