# 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 `kfree`s 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.
