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

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.

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-2694 Β· 12 files
FileTypeDescriptionSize
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
VERDICT.md
↓ download 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:

  1. TCP path β€” panic: assertion "m" failed in sorecvtcp at /usr/src/sys/kern/uipc_socket.c:1852 (see panic.txt, run.log). Trigger: /tmp/poc_race 40 3000 3000 0.
  2. AF_UNIX path (generic soreceive) β€” panic: assertion "sb->sb_mb == m" failed in sbunlinkmbuf at /usr/src/sys/kern/uipc_sockbuf.c:552 (see panic2.txt). Trigger: /tmp/poc_race 60 200 500 1.

Root cause (line-accurate)

  • soshutdown() intentionally skips ssb_lock for the read side (sys/kern/uipc_socket.c:1953-1957) and calls sorflush().
  • sorflush() (uipc_socket.c:1963-1991) takes only so_rcv.ssb_token, snapshots the sockbuf (asb = *ssb), bzero()s ssb->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 chain M_SOLOCKED (:1746-1753) β€” but M_SOLOCKED is only honored by sbcompress() (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 inside uiomove() β€” e.g. a swap-in or file-I/O page fault on the user receive buffer β€” its token protection is gone and sorflush() frees/zeroes the chain underneath it:
  • the copier then reads freed mbufs (m->m_next == NULL after m_free()) and copies freed-cluster contents into the user buffer (UAF read), and
  • the post-loop bookkeeping trips over the zeroed sockbuf: sorecvtcp at KKASSERT(m) (:1851-1852), soreceive at sbunlinkmbuf's KKASSERT(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 executes sb->sb_mb = m->m_nextpkt with m freed β€” writing a stale pointer into live sockbuf state and doing sbfree() 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

fixed
baseline reproduced→ patch + rebuild →patched clean

fix 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']
↓ fix.diffDragonFly dfbsd 6.5-DEVELOPMENT DragonFly 6.5-DEVELOPMENT #1: Sun Aug 30 23:56:25 UTC 2026 root@dfbsd:/usr/obj/usr/src/sys/X86_64_GENERIC x86_64

Confirmed kernel references

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