closef() filedesc_to_leader chain walk vs fdfree() unlink+kfree: cross-lock use-after-free (unprivileged kernel fatal trap at closef+0x80)
| Field | Value |
|---|---|
| ID | DF-NNNN |
| Status | new |
| Severity | |
| CVSS 3.1 | |
| CWE | |
| File | |
| Lines | |
| Area | |
| Confidence | |
| Discovered | |
| Pass | <1 = GLM 5.2 first pass / 2 = GLM 5.3 second pass> |
| Bucket | |
| Reported | |
| Known CVE | |
| CVE match |
Summary
<2-4 sentences. What is broken, where, and the security consequence.>
Root cause
Threat model & preconditions
- Attacker position:
- Privileges gained or impact:
- Required config or capabilities:
vfs.mount.<fs> allowed, needs a specific device node> - Reachability:
Proof of concept
PoC source: findings/poc/DF-NNNN/<file>
Build & run
<exact commands to compile and run the PoC on a DragonFlyBSD host/VM>
Expected output
<what a successful trigger looks like β panic dump, leaked bytes, uid=0, etc.>
Impact
Recommended fix
--- a/sys/<path>
+++ b/sys/<path>
@@ -<l>,<n> +<l>,<n> @@
<context>
-<vulnerable line>
+<fixed line>
<context>
References
Timeline
Discovered during automated audit. Reported to DragonFlyBSD security contact.
Verification (latest)
status: reproduced | impact: panic | confidence: certain
Fix status: fixed β validated fix.diff lives at findings/poc/DF-5792/fix.diff
Unprivileged local UAF upgraded from fatal-trap to a USER-STEERABLE KERNEL WRITE MACHINE on the stock kernel, uid 1001: the closef() fdtol-chain walk (kern_descrip.c:3071-3088) chases into entries freed by concurrent fdfree() (2675-2688); reclaiming the freed 40-byte M_FILEDESC_TO_LEADER chunk with a forged 40-byte AF_UNIX soname (sun_len=40 -- M_SONAME shares the exact 40-byte slab zone; unp_bind parses only the NUL-terminated path while dup_sockaddr bcopy()s all 40 attacker bytes) gives the walker attacker-controlled fdl_leader/fdl_next. fdl_leader = user page (no SMAP) with byte[0x58]=1 passes the p_advlock_flag gate; fdl_next = user SPIN page self-loop parks the walker INSIDE closef() forever -- a kernel-resident walk interpreter over user-editable memory. Steering SPIN+0x20 aims it at any address A: visit does ++/--(int)(A+4), VOP_ADVLOCK(vp,(A+0x10),F_UNLCK), and if (int)(A+4)==0 && (int)(A+8)!=0 ZEROES (int*)(A+8) (verified twice via pcblist soname readback, second time RACE-FREE with the storm stopped). uid0 NOT reached: exhaustive layout analysis shows the primitive set (gated zero-write, net-zero ++/-- pair, wakeup/unlock keyed by forged pointers) composes to no privilege on this kernel -- every content-controlled 33..40B zone occupant fails the fdfree-write-pair requirements (refcount==1@+0, holdcount==0@+4, forged ptrs@+0x18/+0x20) or caps forged pointers below 2^48, and no function pointer is ever dispatched through attacker data on this path. Impact ceiling honestly reported: persistent gated arbitrary-address 4-byte kernel zero-write + 1-bit kernel read oracle + cross-identity POSIX-lock strip + panic at will. fix.diff blocks the machine (walk serialized under fd_spin, fix_ab run). NOTE: fix.diff does NOT cover the same-family refcount lost-update (kern_fork.c:570 addl vs fdfree's split mov/sub/mov dec at 0x8063395b/0x8063344a) -- flagged for the maintainers.
Evidence pack: findings/poc/DF-5792/ (README.md, VERDICT.md, sources, full logs, fix.diff, manifest.json).
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-5792 Β· 22 files| File | Type | Description | Size | |
|---|---|---|---|---|
| fdtol_storm.c | β | 4.8 KB | view raw | |
| build.sh | β | 82 B | view raw | |
| run.sh | β | 215 B | view raw | |
| build.log | β | 253 B | view raw | |
| run1.stats | β | 596 B | β download | |
| run2.stats | β | 518 B | β download | |
| fixrun1.stats | β | 646 B | β download | |
| fixrun2.stats | β | 415 B | β download | |
| serial_panic_run1.txt | β | 15.5 KB | view raw | |
| serial_panic_run2.txt | β | 12.9 KB | view raw | |
| env.txt | β | 878 B | view raw | |
| VERDICT.md | β | 14.4 KB | β raw | |
| fix.diff | β | 2.9 KB | view raw | |
| manifest.json | β | 1.8 KB | view raw | |
| verdict.json | β | 7.2 KB | view raw | |
| exp5792.c | β | 16.6 KB | view raw | |
| EXPLOIT.md | β | 3.9 KB | β raw | |
| build_exploit.sh | β | 70 B | view raw | |
| run_exploit.sh | β | 143 B | view raw | |
| run_machine_success.log | β | 655 B | view raw | |
| fix_ab/fixbuild.log | β | 299 B | view raw | |
| fix_ab/fixrun.out | β | 1.5 KB | view raw |
DF-5792 VERDICT β reproduced (panic), unprivileged, 2/2 runs; fix validated
Bottom line
REPRODUCED. An unprivileged rfork(RFPROC|RFMEM) stress rig drove the
kernel into a fatal trap at closef+0x80 on 2 out of 2 baseline runs
(stock DragonFly 6.5-DEVELOPMENT #0 Thu Jul 2 06:02:54 UTC 2026
X86_64_GENERIC, no INVARIANTS). The faulting instruction is the
fdtol->fdl_leader load of the filedesc_to_leader chain walk in
closef() β a use-after-free read of an entry that a concurrent fdfree()
had unlinked and kfree'd under a different lock. The guest halts in DDB:
unprivileged local kernel DoS confirmed. The same loop also issues
fdtol->fdl_holdcount++ / -- (write) through the freed pointer, so a
heap-corruption write primitive exists on the identical interleaving; the
uid0 chain was NOT completed (see "Exploit ceiling").
Root cause (path:line)
sys/kern/kern_descrip.c:3063-3092βclosef(): takes onlylwkt_gettoken(&p->p_token)(:3065), then walks the circular chain:fdtol = fdtol->fdl_next(:3071 loop init, :3073 loop step), derefsfdtol->fdl_leader->p_advlock_flag(:3074), incrementsfdtol->fdl_holdcount++(:3076), callsVOP_ADVLOCK(may block), thenfdtol->fdl_holdcount--(:3084) andwakeup(fdtol)(:3088).sys/kern/kern_descrip.c:2675-2688βfdfree(): underspin_lock(&fdp->fd_spin)(acquired :2622) decrementsfdl_refcount, unlinks the entry from the chain (:2678-2679) andkfrees it (:2686) when refcount==0 and holdcount==0.- No lock is shared between the two.
p->p_tokenof the walking process does not exclude the exiting process'sfdp->fd_spincritical section. Thefdl_holdcounthandshake meant to bridge them is a plain non-atomic++under no common lock, so fdfree's zero-check at :2676-2677 can pass while (or before) a walker's increment lands. - FreeBSD heritage performs this walk under the filedesc lock
(
FILEDESC_XLOCK(p->p_fd)) β the very lock the unlink uses. The DragonFly MPSAFE conversion lost that serialization. - Chain construction:
rfork(RFPROC|RFMEM)withoutRFTHREADcreates a new entry per process with a distinct leader (sys/kern/kern_fork.c:557-576,filedesc_to_leader_allocsys/kern/kern_descrip.c:3346-3368β itself lockless chain surgery).
Reachability / threat model
- Unprivileged local user; needs only rfork, open, close, fcntl(F_SETLK).
- Walk frequency: every
close()of a vnode whosev_flag & VMAYHAVELOCKSin a process withp->p_fdtol != NULL(any RFMEM-sharing process that ever took a POSIX lock). - Free frequency: every exit of an RFMEM process with its own entry.
- The rig multiplies both: ~101 chain entries, 6 walkers (~44k opens+closes per 5s per the stats = ~210k walks / 5s system-wide), fork/exit churn ~15k/5s.
Evidence
serial_panic_run1.txtβ first baseline run (60s storm). Tail:FStopped at closef+0x80: movq 0x10(%rbx),%rax+db>prompt. (The interleaved "Warning: dup()" lines are a leftover DF-5793 rig also running; they even overwrote the trap header in the fixed-size serial log.)serial_panic_run2.txtβ second baseline run on a fresh boot (boot 19:08:39, clean rig, zero DF-5793 lines in the log). Same signature:FStopped at closef+0x80: movq 0x10(%rbx),%rax.- Instruction-level attribution from the shipped kernel:
nmβclosef = 0xffffffff80631ef0:
closef+0x63: mov 0x20(%rbx),%rbx # fdtol = fdtol->fdl_next
closef+0x70: cmp %rbx,0x20(%r13) # fdtol != p->p_fdtol
closef+0x80: mov 0x10(%rbx),%rax # fdtol->fdl_leader << FAULT
closef+0x74: cmpb $0x0,0x58(%rax) # leader->p_advlock_flag
closef+0x7a: addl $0x1,0x4(%rbx) # fdtol->fdl_holdcount++ (write)
closef+0xb4: callq vop_advlock
closef+0xb9: subl $0x1,0x4(%rbx) # fdtol->fdl_holdcount-- (write)
closef+0xd0: mov 0x20(%rbx),%rbx # fdtol = fdtol->fdl_next
closef+0xd8: jne closef+0x80 # loop
struct filedesc_to_leader layout (sys/sys/filedesc.h:109-118):
fdl_refcount@0, fdl_holdcount@4, fdl_wakeup@8, pad@12,
fdl_leader@0x10, fdl_prev@0x18, fdl_next@0x20. The fault at
mov 0x10(%rbx) with a sub-page vaddr is consistent with %rbx==NULL:
the freed chunk was recycled M_ZERO by the rig's next
filedesc_to_leader_alloc, so the previous freed entry's fdl_next
read as NULL and the walk stepped onto NULL.
* fdtol_storm.c β the rig (unprivileged: rfork_thread/open/close/fcntl).
* run1.stats, run2.stats (baseline storms), fixrun1.stats,
fixrun2.stats (patched) β progress lines from the rig.
Fix validation (mandatory for memory-corruption findings)
fix.diff (authored after verification, never applied to the audit tree):
the walk now loads the next entry and takes its fdl_holdcount under
fdp->fd_spin (fdfree's lock; it already sleeps while holdcount!=0 and
re-checks after wakeup), drops the holdcount only after loading the
following entry, and forwards fdl_wakeup/wakeup(); chain insertion in
filedesc_to_leader_alloc() is wrapped in the same spinlock. The blocking
VOP_ADVLOCK stays outside the spin, protected by the holdcount.
- Baseline: 2/2 fatal traps (β€60s; <4min).
- Patched (
#1 Mon Oct 12 19:21:10 UTC 2026,closefrebuilt with two_spin_lock_contestedcall sites +wakeupinside the walk): - storm 1 (240s): 10,673,660 walks / 764,229 unlink+kfree β 0 panics
- storm 2 (240s): 10,117,666 walks / 769,032 unlink+kfree β 0 panics
- ~20.8M walks and ~1.53M frees total with zero panics and a healthy guest (vs 2/2 panics within minutes stock). fix_status: fixed.
Exploit ceiling (uid0 route β honest assessment)
- Demonstrated primitive: UAF read (
fdtol->fdl_leader) that page-faults the kernel, plus UAF writesaddl $1, 0x4(%rbx)/subl $1, 0x4(%rbx)(holdcount inc/dec at freed-chunk offset 4) andVOP_ADVLOCK(..., (caddr_t)wfdtol->fdl_leader, F_UNLCK, ...)with a leader pointer read from freed memory β i.e. advisory-lock teardown keyed by attacker-groomed contents. - The freed 24-byte
M_FILEDESC_TO_LEADERchunk is recycled by the same rig's forks (M_ZERO) but can in principle be groomed by any same-size slab user;fdl_leaderthen becomes a chosen pointer whosep_advlock_flagfield is read and which is passed as the lock-owner id intolf_advlockunlock paths (lock-list manipulation), and the holdcount inc/dec corrupts offset 4 of the victim object. - The race window (pointer-load β deref) is tens of nanoseconds wide, so
each attempt needs the recycling allocation to land inside it; the storm
achieves ~1 visible fault per minutes-of-runtime. Turning that into a
reliable groom+forge (slab shaping to place a fake
fdl_leader/object at offset 4/0x10 and winning the window deterministically) was not completed within this run's budget. Recorded as: panic reproducible at will; write-capable UAF characterized; reliable uid0 chain open.
Environment
DragonFly dfbsd 6.5-DEVELOPMENT #0 Thu Jul 2 06:02:54 UTC 2026 root@dfbsd:/usr/obj/usr/src/sys/X86_64_GENERIC x86_64, 6 vCPUs, no INVARIANTS/WITNESS, no SMAP/SMEP/KASLR. Fix builds: #1 2026-10-12 19:21:10 and #2 19:39:15 UTC. Guest reset to clean source afterwards.
Exploit-development addendum (2026-10-16) β uid0 attempt: honest terminal
What was achieved (all unprivileged, uid 1001, STOCK kernel)
The verified UAF was developed into a user-steerable kernel write
machine (exp5792.c, run_machine_success.log, full untrimmed):
- Controlled reclaim with attacker content. DFly's slab allocator
classes are 8-byte-granular below 128B (
zoneindex: 40 -> zone 4), so the 24-byte... in fact 40-byte (sizeof(struct filedesc_to_leader)= 0x28) chunk shares its zone with every kmalloc of size 33..40 β includingM_SONAMEsockaddr copies.bind(2)on AF_UNIX withsun_len=40and a short NUL-terminated path lets bytes 8..39 of the kernel-side soname be arbitrary attacker bytes (unp_bindstrncpy()s only up to the NUL butdup_sockaddr()bcopy()s all 40). Binder spinayers keep a stream of forged-fdtol sonames flowing so a walker chasing into a just- freed entry reads ourfdl_leader/fdl_next. - Park. Forged
fdl_leader= user page withbyte[0x58] = 1(thep_advlock_flaggate β no SMAP on this guest, the kernel derefs user pages fine) andfdl_next= a user SPIN page whose+0x10gate is 0 and+0x20points to itself: the walker spins forever inside closef() on a user-controlled self-loop. The walk is now a kernel-resident interpreter over user-editable memory: every iteration reads cur, tests*(cur+0x10)+0x58, and chases*(cur+0x20). - Steer + gated arbitrary-address 4-byte zero-write. Flipping
SPIN+0x20aims the walk at ANY kernel address A. The visit doesaddl $1,(A+4),VOP_ADVLOCK(vp,*(A+0x10),F_UNLCK,...),subl $1,(A+4)and β if*(int*)(A+4)==0 && *(int*)(A+8)!=0β*(int*)(A+8) = 0pluswakeup(A), then chases*(A+0x20). Victim sonames (2-char path so bytes 4..7 are zero) with+8 = 0x41414141, leader = FLAG1 page, next = SPIN2 re-park give a clean, observable, repeatable write:net.local.dgram.pcblistexports each soname verbatim (bcopy), so the zeroed dword reads back from unprivileged userspace. Demonstrated twice in the decisive run β the second write performed race-free with the storm completely stopped (the parked walker is steerable at will). Addresses written: 0xfffff8004f145260+8 and 0xfffff8004f145288+8 (live kernel heap chunks of the 40B zone).
The machine additionally implies (not separately demonstrated):
- a 1-bit kernel read oracle per steering round (gate pass/fail
observable via whether the victim's +8 gets zeroed), i.e. arbitrary
byte at *(A+0x10)+0x58 probing;
- a spurious wakeup primitive on arbitrary addresses;
- cross-identity POSIX-lock removal (VOP_ADVLOCK unlock keyed by
forged fdl_leader β any locker whose proc pointer value is known
can have its locks stripped on any vnode the walker closes).
uid0: why it does NOT compose (the hard blockers, with evidence)
Every escalation route was traced to a wall:
- The only strong write is the gated 4-byte zero at A+8. Zeroing cannot set a pointer (sysent[].sy_call -> NULL = panic, not code); zeroing a refcount moves it 0 -> wrap on release (DFly refcounts free on the 1->0 transition; 0 -> -1 never frees); no DFly static holds a privilege flag whose zero value grants anything to a non-root caller (securelevel is -1 on this guest already).
- The chase constraint. After acting on foreign A the walk
follows
*(A+0x20); survival requires that pointer to lead into the live circular chain (or a benign loop). Foreign objects cannot satisfy this, so every foreign write costs a panic β which wipes the write on reboot. Only attacker-owned chunks (sonames, user pages) are safely writable β useful for verification, useless for privilege. - The
++/--pair is net-zero on any stable occupant; the split (realloc between) needs a mid-VOP realloc of the visited chunk, and the only unpriv-sprayable 33..40-byte zone occupants with a lifetime counter at +4 are: none. Audited: sockaddr (no counter), pargs (ar_length is not lifetime + kernel-owned bytes at +0/+4), sembuf/pollfd arrays (transient, no counter), fdtol itself (holdcount at +4 is not a lifetime ref), varsym (refs at +0x20), sigio (48B zone, wrong class, no +4 counter), kevent (32B), sysv msg buffers (wrong class). The mission's candidate list (sigio sio_refcount, knote, fdtol refcount) does not survive the actual layout/zone analysis on this tree. - fdfree-as-a-write-pair (dangling p->p_fdtol + forged
next/prev =>
*(X+0x18)=Y; *(Y+0x20)=X): requires the reclaimed chunk to carry refcount==1 at +0 and holdcount==0 at +4 and attacker-chosen pointers at +0x18/+0x20. Every content-controlled occupant fails: sockaddr has sa_len/family at +0..3 (never 1); pargs has kernel-written ar_ref/ar_length (ar_length>0 forces fdfree to sleep forever); blocked-poll and blocked-semop occupants can have +0==0 (bumped to 1 by a family fork'sfdl_refcount++) but their pointer bytes at +0x18/+0x20 run through 16-bit fields (pollfd revents, sembuf flg/num) that cap forged pointers below 2^48 β kernel addresses are all >= 0xffff800000000000. No unprivileged 33..40-byte allocation provides all four fields. (The fork-side refcount race itself was confirmed real in the shipped kernel β fork1+0x3f8addl $0x1,(%rax)vs fdfree's splitmov (%r14),%esi; sub $1,%esi; mov %esi,(%r14)β and is NOT covered by fix.diff; but its write potential dies on the same occupant wall.) - No function pointer is ever called through attacker data on
this path:
VOP_ADVLOCKdispatches through the (real) vnode ops;lf_setlock(F_UNLCK)only compares owner pointers (never derefs them in DFly's rewritten lockf);wakeup()never calls through the address. So no RIP control exists to convert no-SMEP into payload execution.
Conclusion: on this kernel DF-5792's primitives top out at a persistent, user-steerable, gated arbitrary-address 4-byte zero-write plus read-oracle and lock-strip capabilities β a major escalation of the originally-verified "fatal trap", but with no composed path to uid=0 that survives the layout analysis above. Reported honestly as such.
Fix-kernel A/B
The validated fix.diff (walk serialized under fdp->fd_spin) was
rebuilt in the guest (kernel #1 Fri Oct 16 09:16:49 UTC 2026) and the
IDENTICAL exploit binary re-run for 120s: 2,478,775 binds / 1,131,287
unlink+kfree baby churns / 856,097 walks β ZERO stalls, ZERO parks,
ZERO fatal traps (walker counter monotonically increasing; no
stall-detect, no verify lines; grep count 0). On the stock kernel the
same binary parks a walker and performs the verified kernel write
within ~30-90s (or hits the original closef+0x80 trap ~40-60% of
attempts). The write machine is therefore entirely dependent on the
unfixed walk race β fix.diff blocks it. See fix_ab/fixrun.out.
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-5792 exploit chain β build & run (exploit-development run, 2026-10-16)
What this demonstrates
Starting from the verified DF-5792 primitive (closef() walks the
filedesc_to_leader chain under only p->p_token while fdfree()
unlinks+kfrees entries under fdp->fd_spin), this rig turns the
"unprivileged fatal trap" into a user-steerable kernel write machine,
entirely as uid 1001 on the stock kernel:
-
Race + controlled reclaim β walkers
open()/close()a VMAYHAVELOCKS vnode (every close walks the family's fdtol chain); paced churners fork Β΅s-lived RFMEM babies whose fdfree unlinks+kfrees their 40-byte fdtol entry (the exact 40-byte slab zone class); binder spinners continuously bind 40-byte AF_UNIX sonames (sun_len=40,M_SONAME, same zone) whose bytes 8..39 are a forgedstruct filedesc_to_leader(fdl_leader= user page withbyte[0x58]=1,fdl_next= user SPIN page). A walker that chases into a just-freed entry which a marker reclaimed executes the attacker's fields: the ACT branch runs (addl $1,(chunk+4),VOP_ADVLOCK,subl $1,(chunk+4)) and the chasefdtol = *(chunk+0x20)lands on the attacker's SPIN page β the walker parks forever insideclosef()on a user-controlled self-loop (no SMAP on this guest). -
Steering + gated kernel write β flipping
SPIN+0x20from userspace aims the parked kernel walk at ANY address A. Visiting A: gatebyte at *(A+0x10)+0x58 != 0;++/-- *(int*)(A+4)(net 0); if*(int*)(A+4)==0 && *(int*)(A+8)!=0then*(int*)(A+8) = 0andwakeup(A); chase*(A+0x20). Victim sonames (2-char path => bytes 4..7 == 0;+8 = 0x41414141; leader = FLAG1 page; next = SPIN2 re-park) prove the write: their+8..+11dword reads back 0x00000000 via the pcblist sysctl (bcopy of the soname) after a single steering flip. -
Repeatable + race-free re-steer β after the storm is stopped the parked walker remains under user-page control; a second steering flip performs a second verified zero-write with zero kernel race activity (see run_machine_success.log,
[sess2]).
Readback channel: net.local.dgram.pcblist (DF-5816) exports each
bound soname verbatim (bcopy(unp->un_addr, &xu.xu_addr, sun_len)),
so the zeroed dword is observable from unprivileged userspace.
Build / run
cc -O2 -o exp5792 exp5792.c
./exp5792 [seconds=120] # as uid 1001 on the stock kernel
Expected decisive output (see run_machine_success.log, full untrimmed):
*** walker stall detected; steer-verify *** [verify 0] victim 0xfffff8004f145260 +8..+11 = 00000000 *** KERNEL WRITE VERIFIED *** *** SUCCESS: parked closef() walk steered to victim; kernel zeroed 4 bytes at a chosen kernel address *** [sess2] steering SPIN2 -> victim b 0xfffff8004f145288; pre +8..+11 = 41414141 [sess2] post +8..+11 = 00000000 *** SECOND KERNEL WRITE VERIFIED (race-free steer) *** uid=1001 euid=1001; system alive; one walker parked in kernel spin at our user page
Run-to-run behavior (honest)
The storm necessarily exercises the original racy interleavings, so a
given attempt can end in the ORIGINAL bug's fatal trap
(closef+0x80: movq 0x10(%rbx),%rax β the fork's M_ZERO recycle beats
the marker) or a hard freeze (chain corruption from unsynchronized
insert/unlink; observed ~40-60% loss per attempt across the session).
The machine result above was reproduced cleanly (full stream captured);
re-run on a fresh boot (dfbsd-qemu/vm.sh reset with-src) if an
attempt dies. The parked walker intentionally never returns from
close(): it is the engine; each success burns one CPU until reboot.
Files
exp5792.cβ the full exploit (park/steer/write machine)run_machine_success.logβ full untrimmed decisive run (2 verified writes)exp_trap_excerpt.txtβ one fatal-trap loss (original bug signature)fix_ab/β A/B against the validated fix kernel
Fix verification
fixedBaseline: 2/2 fatal traps at closef+0x80 (<=60s and <4min). Patched: 2x240s storms, 20,791,326 closef walks and 1,533,261 unlink+kfree events, zero panics, guest healthy. fix.diff applied cleanly to the guest /usr/src and booted.
fixrun1.stats (opens=10673660 forks=764229 rc=0), fixrun2.stats (opens=10117666 forks=769032 rc=0), dmesg grep fatal|panic => 0 on patched kernel #1/#2; baseline serial logs in serial_panic_run1/2.txt.
Confirmed kernel references
Detail
Exploit chain
unpriv uid 1001: RFMEM family; walkers open/close a VMAYHAVELOCKS vnode (each close walks the fdtol chain under only p_token); paced churners fork usec-lived RFMEM babies (each exit unlinks+kfrees its 40B fdtol entry under fd_spin); binder spinners continuously bind 40-byte AF_UNIX sonames containing a forged fdtol (leader=user page w/ flag byte 1, next=user SPIN page) -> a mid-walk walker chases a freed entry reclaimed by a marker -> ACT branch runs, chase lands on the user SPIN self-loop -> walker PARKED inside closef() -> flip SPIN+0x20 from userspace to aim the kernel walk at ANY address A -> gated zero-write (int)(A+8)=0 (plus ++/-- pair and wakeup(A)) -> chase *(A+0x20) (victim soname leads back to re-park) -> verified twice via net.local.dgram.pcblist soname readback (victim +8 dword 0x41414141 -> 0x00000000). uid0 blocked: zero-write cannot set pointers; chase-survival constraint confines foreign writes; no 33..40B occupant provides refcount@+0==1 + forged next/prev (sockaddr sa_len/family at +0, pargs kernel-owned +0/+4 with ar_length>0 sleeping fdfree, pollfd/sembuf 16-bit fields capping pointers <2^48); no fn-ptr dispatch through attacker data. Original walk-race still panics ~40-60% of attempts (closef+0x80) when the fork's M_ZERO recycle beats the marker.
Evidence (decisive lines)
run_machine_success.log (full untrimmed decisive run: stall detected -> [verify 0] victim 0xfffff8004f145260 +8..+11 = 00000000 KERNEL WRITE VERIFIED -> SUCCESS -> [sess2] race-free second write at 0xfffff8004f145288+8 -> uid=1001 system alive); exp5792.c (exploit source); EXPLOIT.md (mechanics); serial_panic_run1/2.txt (original 2/2 baseline traps); fix_ab/ (A/B on fix.diff kernel).
PoC changes
exploit-development run: exp5792.c written fresh -- marker-soname reclaim of the freed fdtol chunk (unp_bind parses only to the path NUL; dup_sockaddr copies all 40 attacker bytes), user-page flag gates + SPIN self-loop park, pcblist (DF-5816) as readback channel, victim sonames with +4==0 gate, paced churn (1 fork/~2.5ms) so binders win the LIFO recycle against the churner's own next fork, sticky p_advlock_flag fillers + helper lock ranges to stretch walk windows. Several dead ends mapped first: RFTHREAD refcount lost-update race is REAL in the shipped kernel (fork1+0x3f8 addl vs fdfree split dec) but kern_exit.c:384 p_peers chain walk wedges the guest under churn, and its write potential dies on the occupant analysis.
Verified recommended fix
Serialize ALL filedesc_to_leader mutation under fdp->fd_spin as in fix.diff (validated: walk+insert), and additionally: (a) make kern_fork.c:570's fdtol->fdl_refcount++ atomic or fd_spin-protected to close the lost-update race vs fdfree's decrement; (b) reconsider the p_peers chain handling in kern_exit.c:384 for RFTHREAD processes.
Verdict
Unprivileged local UAF upgraded from fatal-trap to a USER-STEERABLE KERNEL WRITE MACHINE on the stock kernel, uid 1001: the closef() fdtol-chain walk (kern_descrip.c:3071-3088) chases into entries freed by concurrent fdfree() (2675-2688); reclaiming the freed 40-byte M_FILEDESC_TO_LEADER chunk with a forged 40-byte AF_UNIX soname (sun_len=40 -- M_SONAME shares the exact 40-byte slab zone; unp_bind parses only the NUL-terminated path while dup_sockaddr bcopy()s all 40 attacker bytes) gives the walker attacker-controlled fdl_leader/fdl_next. fdl_leader = user page (no SMAP) with byte[0x58]=1 passes the p_advlock_flag gate; fdl_next = user SPIN page self-loop parks the walker INSIDE closef() forever -- a kernel-resident walk interpreter over user-editable memory. Steering SPIN+0x20 aims it at any address A: visit does ++/--(int)(A+4), VOP_ADVLOCK(vp,(A+0x10),F_UNLCK), and if (int)(A+4)==0 && (int)(A+8)!=0 ZEROES (int*)(A+8) (verified twice via pcblist soname readback, second time RACE-FREE with the storm stopped). uid0 NOT reached: exhaustive layout analysis shows the primitive set (gated zero-write, net-zero ++/-- pair, wakeup/unlock keyed by forged pointers) composes to no privilege on this kernel -- every content-controlled 33..40B zone occupant fails the fdfree-write-pair requirements (refcount==1@+0, holdcount==0@+4, forged ptrs@+0x18/+0x20) or caps forged pointers below 2^48, and no function pointer is ever dispatched through attacker data on this path. Impact ceiling honestly reported: persistent gated arbitrary-address 4-byte kernel zero-write + 1-bit kernel read oracle + cross-identity POSIX-lock strip + panic at will. fix.diff blocks the machine (walk serialized under fd_spin, fix_ab run). NOTE: fix.diff does NOT cover the same-family refcount lost-update (kern_fork.c:570 addl vs fdfree's split mov/sub/mov dec at 0x8063395b/0x8063344a) -- flagged for the maintainers.
No comments yet.