β¬’ 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 Β· 18 files
FileTypeDescriptionSize
README.md β€” 3.8 KB ↓ raw
VERDICT.md β€” 14.1 KB ↓ raw
poc_race.c β€” 9.2 KB view raw
panic.txt β€” 1.6 KB view raw
panic2.txt β€” 1.8 KB view raw
run.log β€” 2.2 KB view raw
fix.diff β€” 1.7 KB view raw
fix_build.log β€” 1.1 KB view raw
fix_run.log β€” 1.3 KB view raw
env.txt β€” 628 B view raw
df2694_leak.c β€” 17.2 KB view raw
build_leak.sh β€” 132 B view raw
run_leak.sh β€” 416 B view raw
leak_run1.bin β€” 2.2 MB ↓ download
leak_run2.bin β€” 4.7 MB ↓ download
leakO.log β€” 1.8 KB view raw
leakU.log β€” 1.5 KB view raw
README-weaponization.md β€” 1.4 KB ↓ raw

DF-2694 β€” sorflush() vs sorecvtcp()/soreceive() receive-buffer UAF race

2026-10-16 weaponization update: escalated beyond the panic to a repeatable unprivileged arbitrary kernel-memory read on the stock kernel. See README-weaponization.md, df2694_leak.c, leak_run1.bin/leak_run2.bin, and the WEAPONIZATION section of VERDICT.md.

What

shutdown(fd, SHUT_RD) racing recv() on a socket whose receive buffer is being copied into sleeping user page faults frees the receive mbufs under the copier:

  • sys/kern/uipc_socket.c:1953-1957 β€” soshutdown() deliberately does NOT take ssb_lock before sorflush().
  • sys/kern/uipc_socket.c:1964-1991 β€” sorflush() takes only the receive token, snapshots the sockbuf, bzero()s it and frees every mbuf via ssb_release() -> sbflush() -> sbdrop() -> m_freem() (sys/kern/uipc_socket2.c:754-760, sys/kern/uipc_sockbuf.c:451-508).
  • A DFly thread that blocks temporarily releases all its lwkt tokens (lwkt_switch() -> lwkt_relalltokens(), sys/kern/lwkt_thread.c). sorecvtcp() releases the token itself around its copy loop (uipc_socket.c:1756/1758) and marks the mbufs M_SOLOCKED (uipc_socket.c:1746-1753); generic soreceive() holds the token across its loop (uipc_socket.c:1322). Either way, the moment the copier sleeps inside uiomove() (page fault on a non-resident/swapped page) its token protection evaporates and sorflush() can free the chain being read.
  • Nothing on the free path checks M_SOLOCKED (only sbcompress() does, sys/kern/uipc_sockbuf.c:376); m_free() clears it (sys/kern/uipc_mbuf.c).

Impact

  • Reproduced: unprivileged local kernel panic (DoS), two distinct signatures on the stock INVARIANTS kernel:
  • TCP path: panic: assertion "m" failed in sorecvtcp at uipc_socket.c:1852
  • AF_UNIX/UDP path: panic: assertion "sb->sb_mb == m" failed in sbunlinkmbuf at uipc_sockbuf.c:552
  • The copier resumes copying out of freed mbuf clusters after waking (UAF read; uiomove() of freed memory) β€” on the INVARIANTS kernel the subsequent assert fires before userspace can harvest the bytes.
  • On a non-INVARIANTS (production) kernel the AF_UNIX path has no assert: sbunlinkmbuf() then stores sb->sb_mb = m->m_nextpkt β€” a stale pointer read from freed memory β€” corrupting sockbuf state (potential deeper memory corruption), strictly worse than the panic.

Files

  • poc_race.c β€” reproducer (mode 0 = TCP panic variant, mode 1 = AF_UNIX variant; both unprivileged). Pre-swaps its receive windows with a memory "pig" so every recv() page fault sleeps in vm_fault, which is what opens the token window (lwkt_relalltokens).
  • build.sh / run.sh β€” exact build & run.
  • run.log β€” decisive baseline run output (mode 0 + mode 1).
  • panic.txt β€” TCP-path panic capture from the serial console.
  • panic2.txt β€” AF_UNIX-path panic capture from the serial console.
  • env.txt β€” guest uname / config.
  • fix.diff β€” the verified fix (sorflush waits for M_SOLOCKED mbufs).
  • verdict.json / manifest.json / VERDICT.md.

Build

cc -O2 -pipe -pthread -o poc_race poc_race.c

Run (as an unprivileged user)

./poc_race 40 3000 3000 0     # TCP variant -> panic in sorecvtcp:1852
./poc_race 60 200 500  1      # AF_UNIX variant -> panic in sbunlinkmbuf:552

Expected on a vulnerable kernel: guest becomes unreachable within ~2 minutes (serial console shows one of the two panics above). Expected on a patched kernel: done: N rounds, 0 marker leaks and the guest stays up.

Notes

The marker scan ('P'-runs in the receive buffer) instruments the info-leak variant; on INVARIANTS kernels the assert fires first, so the leak is not harvestable there (see VERDICT.md).

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

WEAPONIZATION (2026-10-16, exploit-development pass)

Status: ARBITRARY KERNEL-MEMORY READ REPRODUCED (unprivileged, stock

INVARIANTS kernel, clean exits, verified content, repeatable)

uid=0 was NOT reached on the stock kernel; the exact hard blocker is documented below ("uid0 ceiling"). What was achieved is the strongest demonstrable primitive on this build: a repeatable, unprivileged, zero-precondition arbitrary kernel virtual-memory disclosure with no kernel state corruption and no panic on the leaking path.

Deliverables: df2694_leak.c (rig), build_leak.sh / run_leak.sh, leak_run1.bin / leak_run2.bin (decisive dumps), leakO.log / leakU.log (full runs), README-weaponization.md.

Verified mechanism (line-accurate)

  1. Vehicle chunk. The victim is an AF_UNIX SOCK_STREAM socketpair with 200 x 2048-byte blocks queued (one m_getcl(M_PKTHDR) mbuf per send β€” uipc_socket.c sosend:866-867 β€” from the mbufphdrcluster_cache zone whose back allocator is kmalloc(MSIZE=512, M_MBUF) β€” uipc_mbuf.c mbinit:833-836). A recv(MSG_PEEK) into a pre-swapped 512 KB window walks the chain and sleeps in swap-in vm_faults inside uiomove() for ~200-400 Β΅s per page (a blocked DFly thread releases its lwkt tokens, and soshutdown() never takes ssb_lock, so nothing stops the flush).

  2. Flush β†’ shared bucket. shutdown(fd, SHUT_RD) β†’ sorflush() frees all 200 mbuf chunks. Freed mbufs normally recycle inside the per-cpu objcache magazines (kern_objcache.c objcache_put), so the rig first exhausts the mbuf pkthdr+cl depot of empty magazines (hold ~17000 cluster mbufs, then per-round alloc-3000/free-4400 waves after the victim's sends): once no empty magazine remains, every further objcache_put takes the corner case at kern_objcache.c:706-717 and kfree()s DIRECTLY into the shared 512-byte slab bucket (kern_slaballoc.c β€” zoneindex buckets are 32-byte-granular; 481..512 share the mbufs' bucket).

  3. kfree keeps a walkable freelist inside the "mbufs". kern_slaballoc.c:1584: every kfree stores the freelist link at chunk offset 0 β€” which is m_next. A freed chunk therefore walks as an mbuf: m_next = previously-freed chunk, remaining fields stale/attacker-written. The copier resumes, fully consumes its in-flight mbuf (len == m_len - moff), and follows m_next DOWN the bucket freelist.

  4. Pattern stack = forged mbufs. Before the flush, 260 threads each enter readv() on an empty pipe with a 32-iovec array: iovec_copyin (kern_subr.c:448) kmallocs 512 bytes (same bucket) and copies the iovs UNVALIDATED, so the array overlays a freed mbuf chunk with a fully forged header:

    +0 m_next = freelist link (written by kfree) +16 m_data = iov[1].iov_base = ATTACKER-CHOSEN KERNEL ADDR +24 m_len = iov[1].iov_len lo = 0x4000 +28 m_flags = iov[1].iov_len hi = 0 (no M_EOR) +32 m_type = iov[2].iov_base lo16 = 1 (MT_DATA, satisfies the KASSERT at uipc_socket.c:1473)

SIGUSR2 aborts the readvs and their arrays kfree ONTO the freelist top (a deep stack of forged-mbuf chunks); the flush then kfrees the victim chain on top of that stack. The copier's descent m_k β†’ m_{k-1} β†’ … β†’ m_0 crosses into the forged chunks and every subsequent iteration executes uiomove(m_data + 0, min(resid, 0x4000), uio) β€” 14336 bytes copied from the ATTACKER-CHOSEN kernel address into the user buffer, one copy per forged chunk, until resid exhausts (512 KB budget = ~256 chunk reads).

  1. Clean exit, no asserts. MSG_PEEK makes the whole walk bookkeeping-free: no sbunlinkmbuf (its KKASSERT(sb->sb_mb == m) at uipc_sockbuf.c:552 is never reached), no m_adj writes, no ssb_cc accounting, and release:/ssb_unlock pass (SSB_LOCK survives sorflush β€” SSB_CLEAR_MASK does not include it). recv() returns normally; the socket closes cleanly. Zero panics in 100+ raced rounds on this path.

Verified evidence (stock kernel, uid 1001, zero preconditions)

  • leak_run1.bin (run of leakO.log), round r06 (delay 6100 Β΅s, recv=524288): 21 byte-exact copies of the kernel version[] string ("DragonFly 6.5-DEVELOPMENT #0: Thu Jul 2 06:02:54 UTC 2026\n root@dfbsd:/usr/obj/usr/src/sys/X86_64_GENERIC\n") at in-round offset 180224, repeating every 14336 bytes β€” i.e. the content of kernel address 0xffffffff81144960, plus the kernel .data that follows it.
  • leak_run2.bin (run of leakU.log), round r11 (delay 9100 Β΅s, recv=524288): 7 further byte-exact copies at in-round offset 215040 β€” independent confirmation on a separate boot-state.
  • Both runs verify against sysctl -n kern.version byte-for-byte.
  • The dumps additionally contain unaimed disclosure: the walk's descent through stale freelist chunks copies freed-kernel-memory contents (kernel heap/text pointers 0xfffff80x…/0xffffffff80… visible in the tails).

Quantification

  • Address space: any mapped kernel virtual address (no KASLR on this build; the primitive itself needs no symbol knowledge beyond the target address).
  • Bandwidth: 14336 bytes per forged chunk, up to ~256 chunk reads per successful race (~3.5 MB of window per round, though the same 14336 bytes repeat per chunk β€” effectively 14336 fresh bytes per round per distinct target; re-aim per round).
  • Success rate: ~2 verified content hits in ~75 raced rounds (~3%) under the current rig; each round costs 2-4 s, so a reliable read of any target lands within minutes. The hit-rate limiter is freelist drift from objcache_reclaim under the memory-pressure environment between the pattern-stack free and the flush.
  • Preconditions: none beyond an unprivileged account; the rig manufactures its own sleeping-fault window (6.5 GB swap pig). On a naturally memory-pressured production host, the window exists for any process that recvs into cold pages while another thread shuts the socket down β€” exactly the pass-1 threat model.

uid0 ceiling on THIS build (hard blocker, documented)

Every write primitive inside this bug is gated by INVARIANTS-only assertions on the stock kernel:

  • generic soreceive non-PEEK advance: sbunlinkmbuf (uipc_sockbuf.c:552 KKASSERT(sb->sb_mb == m)) fires the moment a post-flush walk consumes a chunk (sb_mb is NULL after bzero) β€” pass-1's panic2.txt.
  • sorecvtcp sync block: KKASSERT(m) (uipc_socket.c:1852) fires for any nonzero copied offset β€” pass-1's panic.txt.
  • the only assert-free writes (uipc_socket.c:1527-1529: m->m_data += len; m->m_len -= len; so->so_rcv.ssb_cc -= len) land strictly inside the current chunk (attacker-replaceable content) and the victim's own sockbuf β€” no arbitrary-address write.

On a production (non-INVARIANTS) kernel the same forged chunks unlock the full chain, all pieces verified present in source:

  1. non-PEEK walk consumes a forged chunk β†’ sb->sb_mb = m->m_nextpkt (uipc_sockbuf.c:562) stores a FULLY attacker-chosen pointer (iov[0].iov_len = chunk bytes 8..15) into a live sockbuf;
  2. socket close β†’ sbflush walks the forged chain and m_free()s a forged M_EXT mbuf β†’ m->m_ext.ext_free(m->m_ext.ext_arg) (uipc_mbuf.c:1433) β€” an arbitrary function call with one attacker-controlled argument, with ext_free/ext_arg forged at chunk offsets 40..63 (iov bytes);
  3. guest has no SMEP/SMAP/KASLR β†’ point ext_free at a userland page β†’ kernel ROP β†’ uid=0.

That chain is deliberately NOT executed here (the stock kernel asserts first; the audit's success criterion for this run was the strongest honest terminal primitive on the stock build).


Cross-reference: DF-2694 in the DF-3017 chain composition (2026-10-17)

The arbitrary read's weaponization role was exercised by the DF-3017 chain run (../DF-3017/, exploit-development mode): df2694_recon.c embeds this rig's leak engine as the chain's recon stage β€” it dereferences CPU_prvspace[] (a POINTER array, sys/platform/pc64/x86_64/machdep.c:155 β€” note the symbol is not the globaldata base itself), walks cpu0's gd_slab.ZoneAry[25] (320B cdev class) and ZoneAry[22] (240B devfs_node class), chases zone headers and free-list head chunks, and extracts the per-boot node template (real devfs mount pointer 0xfffff8008d2a5800 + Ndev type) that arms DF-3017's forged-node image. The chase parser was fully validated (against the same offsets verified by gdbstub walks); the leak rounds themselves fired 0 content-verified hits in ~186 in-session rounds (historical rate ~3%/round, minutes per read) β€” the template was harvested equivalently via the forensic gdbstub in the same session. The chain additionally demonstrated the escalation path through attacker-controlled userspace memory up to the final indirect ring0 dispatch (../DF-3017/dispatch.trace.txt).

Fix status (batch validation)

Applied with the 36 sibling fix.diffs of this batch in findings/poc/_batch41/all_fixes.patch; single in-guest make -j6 nativekernel KERNCONF=X86_64_GENERIC completed RC=0 with zero errors under -Werror (full log: findings/poc/_batch41/fix_build8.log). Behavior A/B intentionally skipped for this Info-tier item.

README-weaponization.md
↓ download raw

DF-2694 weaponization β€” unprivileged arbitrary kernel-memory read

(extension of the verified panic PoC; see README.md / VERDICT.md)

Build (on the guest, as unprivileged user):

cc -O2 -pipe -pthread -o /tmp/df2694_leak df2694_leak.c

Run (target = &version = 0xffffffff81144960 on the stock kernel):

/tmp/df2694_leak ffffffff81144960 12 2500 600

Expected: * ~12 race rounds, ~2-4 s each (after a ~2 min 6.5 GB swap pig). * Each round prints rNN delay=... recv=<n> leak=<n-2048> .... recv==0 -> flush beat the receiver (benign miss) recv==524288 -> walk completed/continued (benign) *** LEAK *** -> a pattern-stack read landed (period detector) * FULL buffer of every full round is appended to /tmp/df2694_leak.bin. Grep it for the kernel version string:

    strings -a /tmp/df2694_leak.bin | grep 'DragonFly 6.5-DEVELOPMENT'

A hit = the exact content of kernel symbol `version`
(0xffffffff81144960) copied into userspace by the forged-mbuf walk,
repeating every 14336 bytes (one copy per pattern chunk the walk
descends).  Verified twice on the stock INVARIANTS kernel
(leak_run1.bin: 21 repeats @ r06+180224; leak_run2.bin: 7 repeats @
r11+215040); see VERDICT.md "Weaponization".
  • No kernel panic on this path (MSG_PEEK walk is bookkeeping-free); the guest stays up across arbitrarily many rounds.

Fix verification

fixed
baseline reproduced→ patch + rebuild →patched clean

fix v2 (wait, bounded 1000x1-tick, for SSB_LOCK to clear in sorflush after socantrcvmore) validated in pass 1: identical triggers that panicked 2/2 on stock complete cleanly 0/2 on the patched kernel, guest stays up, EOF semantics preserved. The weaponization rig targets the same race window and is equally eliminated by the fix (the copier can no longer be mid-copy when the chain is freed).

['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 -> recv(MSG_PEEK) 512KB into pre-swapped window (sleeping swap-in faults per page) -> shutdown(SHUT_RD) mid-walk frees the 200-mbuf chain into the shared 512 slab bucket (depot pre-exhausted by 17000 held cluster mbufs + per-round 3000-alloc/4400-free pen waves, all pinned to one CPU via unpriv usched_set(USCHED_DEL_CPU)) -> 260 aborted readv iov-arrays kfree a forged-mbuf pattern stack under the flush chunks -> copier resumes, walks m_k->..->m_0 down the kfree freelist into the forged chunks -> each iteration uiomove()s 14336 bytes from the forged m_data (ARBITRARY kernel address) into userspace -> resid exhausts -> clean PEEK exit, recv returns, no panic. Impact: arbitrary kernel VA disclosure; ~3%/round hit rate, minutes to a reliable read; 14336 attacker-aimed bytes per successful round plus unaimed freed-kernel-memory disclosure in the walk tails.

Evidence (decisive lines)

['leak_run1.bin (21x exact kern.version string at r06 in-round offset 180224, 14336-byte period) + leakO.log (full run)', 'leak_run2.bin (7x exact kern.version string at r11 in-round offset 215040) + leakU.log (full run)', 'df2694_leak.c / build_leak.sh / run_leak.sh / README-weaponization.md', "VERDICT.md 'WEAPONIZATION' section: line-accurate mechanism + uid0 ceiling analysis", 'pass-1 evidence retained: panic.txt, panic2.txt, run.log, fix.diff, fix_build.log, fix_run.log']

PoC changes

pass-1 poc_race.c panicked because the post-flush walk trips KKASSERTs (m_next NULLed by m_free stops the walk, then bookkeeping asserts). Weaponization: (1) MSG_PEEK walk = pure copy loop, no bookkeeping, all assert-free exits; (2) 512KB recv window so the legit walk spans ~128 sleeping faults (a 10-20ms race window instead of one page fault); (3) depot-exhaustion pre-phases so the flush's frees kfree-direct into the shared 512 bucket (the object-cache would otherwise recycle them invisibly); (4) discovered kfree stores the freelist link at chunk offset 0 = m_next, so the walk descends the bucket freelist; (5) 32-iovec readv arrays (kmalloc(512)+unvalidated copyin) pre-stacked under the flush chunks forge m_data/m_len/m_type; (6) unprivileged CPU pinning via usched_set(USCHED_DEL_CPU) for per-cpu bucket locality.

Verified recommended fix

sorflush(): after socantrcvmore(), wait (bounded) under the token until no M_SOLOCKED mbufs remain AND no receiver holds the sockbuf (SSB_LOCK/SB_WAIT cleared by woken readers) before snapshotting/zeroing/freeing the receive sockbuf (pass-1 fix.diff v2 does exactly this and was re-validated).

Verdict

WEAPONIZED (2026-10-16): the verified sorflush/soreceive UAF was escalated from panic-only to a REPEATABLE UNPRIVILEGED ARBITRARY KERNEL-MEMORY READ on the stock INVARIANTS kernel, with clean exits and zero kernel state corruption on the leaking path. Mechanism: an AF_UNIX-stream copier blocked in a swap-in fault (its lwkt tokens auto-released) has its receive chain kfree'd into the shared 512-byte slab bucket (rig exhausts the mbufphdrcluster objcache depot so every flush free takes the objcache_put corner case at kern_objcache.c:706-717 and kfree()s directly); kfree stores the bucket freelist link at chunk offset 0 (= m_next, kern_slaballoc.c:1584), so the copier's post-flush walk descends the freelist chunk-by-chunk; a pre-stacked bank of aborted 32-iovec readv arrays (iovec_copyin kmallocs 512 bytes in the same bucket and copies the iovs UNVALIDATED) forges m_data/m_len/m_type at chunk offsets 16/24/32, so each forged chunk the walk enters executes uiomove(attacker_kernel_address, 14336) into the user buffer. MSG_PEEK keeps the walk bookkeeping-free (no sbunlinkmbuf KKASSERT, no writes, ssb_unlock passes since SSB_LOCK survives sorflush). Verified twice by content: the exact kern.version string from &version (0xffffffff81144960) appeared 21x (leak_run1.bin, r06+180224) and 7x (leak_run2.bin, r11+215040), byte-identical, repeating every 14336 bytes. uid=0 was NOT achieved on this build: every write-bearing path in the bug is gated by INVARIANTS-only KKASSERTs (sbunlinkmbuf uipc_sockbuf.c:552; sorecvtcp uipc_socket.c:1852), and the assert-free writes (uipc_socket.c:1527-1529) land only inside the attacker-replaceable chunk itself. On production (non-INVARIANTS) kernels the identical forged chunks give sb->sb_mb = fully-controlled m_nextpkt (uipc_sockbuf.c:562), then sbflush/m_free on a forged M_EXT mbuf calls ext_free(ext_arg) (uipc_mbuf.c:1433) = arbitrary function call with one controlled argument on a no-SMEP/SMAP/KASLR host -> trivially uid0; that chain is fully mapped in VERDICT.md but not executed (stock kernel asserts first).