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

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

file:line citations. Name the exact variable, length, check, or lock that is wrong.>

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

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

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-5792 Β· 22 files
FileTypeDescriptionSize
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
VERDICT.md
↓ download 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 only lwkt_gettoken(&p->p_token) (:3065), then walks the circular chain: fdtol = fdtol->fdl_next (:3071 loop init, :3073 loop step), derefs fdtol->fdl_leader->p_advlock_flag (:3074), increments fdtol->fdl_holdcount++ (:3076), calls VOP_ADVLOCK (may block), then fdtol->fdl_holdcount-- (:3084) and wakeup(fdtol) (:3088).
  • sys/kern/kern_descrip.c:2675-2688 β€” fdfree(): under spin_lock(&fdp->fd_spin) (acquired :2622) decrements fdl_refcount, unlinks the entry from the chain (:2678-2679) and kfrees it (:2686) when refcount==0 and holdcount==0.
  • No lock is shared between the two. p->p_token of the walking process does not exclude the exiting process's fdp->fd_spin critical section. The fdl_holdcount handshake 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) without RFTHREAD creates a new entry per process with a distinct leader (sys/kern/kern_fork.c:557-576, filedesc_to_leader_alloc sys/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 whose v_flag & VMAYHAVELOCKS in a process with p->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, closef rebuilt with two _spin_lock_contested call sites + wakeup inside 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 writes addl $1, 0x4(%rbx) / subl $1, 0x4(%rbx) (holdcount inc/dec at freed-chunk offset 4) and VOP_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_LEADER chunk is recycled by the same rig's forks (M_ZERO) but can in principle be groomed by any same-size slab user; fdl_leader then becomes a chosen pointer whose p_advlock_flag field is read and which is passed as the lock-owner id into lf_advlock unlock 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):

  1. 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 β€” including M_SONAME sockaddr copies. bind(2) on AF_UNIX with sun_len=40 and a short NUL-terminated path lets bytes 8..39 of the kernel-side soname be arbitrary attacker bytes (unp_bind strncpy()s only up to the NUL but dup_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 our fdl_leader/fdl_next.
  2. Park. Forged fdl_leader = user page with byte[0x58] = 1 (the p_advlock_flag gate β€” no SMAP on this guest, the kernel derefs user pages fine) and fdl_next = a user SPIN page whose +0x10 gate is 0 and +0x20 points 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).
  3. Steer + gated arbitrary-address 4-byte zero-write. Flipping SPIN+0x20 aims the walk at ANY kernel address A. The visit does addl $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) = 0 plus wakeup(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.pcblist exports 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:

  1. 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).
  2. 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.
  3. 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.
  4. 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's fdl_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+0x3f8 addl $0x1,(%rax) vs fdfree's split mov (%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.)
  5. No function pointer is ever called through attacker data on this path: VOP_ADVLOCK dispatches 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.

EXPLOIT.md
↓ download raw

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:

  1. 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 forged struct filedesc_to_leader (fdl_leader = user page with byte[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 chase fdtol = *(chunk+0x20) lands on the attacker's SPIN page β€” the walker parks forever inside closef() on a user-controlled self-loop (no SMAP on this guest).

  2. Steering + gated kernel write β€” flipping SPIN+0x20 from userspace aims the parked kernel walk at ANY address A. Visiting A: gate byte at *(A+0x10)+0x58 != 0; ++/-- *(int*)(A+4) (net 0); if *(int*)(A+4)==0 && *(int*)(A+8)!=0 then *(int*)(A+8) = 0 and wakeup(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..+11 dword reads back 0x00000000 via the pcblist sysctl (bcopy of the soname) after a single steering flip.

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

fixed
baseline reproduced→ patch + rebuild →patched clean

Baseline: 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.
↓ fix.diffper-fix-DF-5792

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.