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 Β· 18 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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 ofVERDICT.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 takessb_lockbeforesorflush().sys/kern/uipc_socket.c:1964-1991βsorflush()takes only the receive token, snapshots the sockbuf,bzero()s it and frees every mbuf viassb_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 mbufsM_SOLOCKED(uipc_socket.c:1746-1753); genericsoreceive()holds the token across its loop (uipc_socket.c:1322). Either way, the moment the copier sleeps insideuiomove()(page fault on a non-resident/swapped page) its token protection evaporates andsorflush()can free the chain being read. - Nothing on the free path checks
M_SOLOCKED(onlysbcompress()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 storessb->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 everyrecv()page fault sleeps invm_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).
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).
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)
-
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.csosend:866-867 β from thembufphdrcluster_cachezone whose back allocator iskmalloc(MSIZE=512, M_MBUF)βuipc_mbuf.cmbinit:833-836). Arecv(MSG_PEEK)into a pre-swapped 512 KB window walks the chain and sleeps in swap-invm_faults insideuiomove()for ~200-400 Β΅s per page (a blocked DFly thread releases its lwkt tokens, andsoshutdown()never takesssb_lock, so nothing stops the flush). -
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.cobjcache_put), so the rig first exhausts thembuf pkthdr+cldepot 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 furtherobjcache_puttakes the corner case atkern_objcache.c:706-717andkfree()s DIRECTLY into the shared 512-byte slab bucket (kern_slaballoc.cβ zoneindex buckets are 32-byte-granular; 481..512 share the mbufs' bucket). -
kfree keeps a walkable freelist inside the "mbufs".
kern_slaballoc.c:1584: everykfreestores the freelist link at chunk offset 0 β which ism_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 followsm_nextDOWN the bucket freelist. -
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).
- Clean exit, no asserts. MSG_PEEK makes the whole walk
bookkeeping-free: no
sbunlinkmbuf(itsKKASSERT(sb->sb_mb == m)atuipc_sockbuf.c:552is never reached), nom_adjwrites, nossb_ccaccounting, andrelease:/ssb_unlockpass (SSB_LOCK survivessorflushβ 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 ofleakO.log), round r06 (delay 6100 Β΅s, recv=524288): 21 byte-exact copies of the kernelversion[]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 ofleakU.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.versionbyte-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_reclaimunder 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
soreceivenon-PEEK advance:sbunlinkmbuf(uipc_sockbuf.c:552KKASSERT(sb->sb_mb == m)) fires the moment a post-flush walk consumes a chunk (sb_mb is NULL afterbzero) β pass-1'spanic2.txt. sorecvtcpsync block:KKASSERT(m)(uipc_socket.c:1852) fires for any nonzero copied offset β pass-1'spanic.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:
- 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; - socket close β
sbflushwalks the forged chain andm_free()s a forgedM_EXTmbuf β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); - 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.
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
fixedfix 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']
Confirmed kernel references
- sys/kern/uipc_socket.c:1466-1577
- sys/kern/uipc_socket.c:1473
- sys/kern/uipc_socket.c:1490-1493
- sys/kern/uipc_socket.c:1953-1957
- sys/kern/uipc_socket.c:1963-1991
- sys/kern/uipc_sockbuf.c:552
- sys/kern/uipc_sockbuf.c:562
- sys/kern/uipc_mbuf.c:826-836
- sys/kern/uipc_mbuf.c:1310-1348
- sys/kern/uipc_mbuf.c:1433
- sys/kern/kern_objcache.c:706-717
- sys/kern/kern_slaballoc.c:1584
- sys/kern/kern_slaballoc.c:638-676
- sys/kern/kern_subr.c:448-490
- sys/kern/kern_usched.c:296-325
- sys/sys/socketvar.h:106-107
- sys/platform/pc64/x86_64/support.s:228-258
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).
No comments yet.