# DF-5337 — VERDICT

**status: reproduced** (race-induced vkernel kernel memory corruption;
in-vkernel DoS/panic demonstrated; in-vkernel privesc chain analyzed,
not weaponized to uid=0 in this pack — the demonstrated artifact is the
corruption itself, which is the root-cause defect)

## Root cause (path:line)

* `sys/platform/vkernel64/x86_64/trap.c:167-173` — `userenter()`: unlocked
  `crhold(curp->p_ucred)` on the syscall/trap entry path of every emulated
  user thread.
* Writer: `sys/kern/kern_prot.c:1184-1189` (`cratom_proc`): `p->p_ucred =
  newcr` under `p_spin`, then `crfree(oldcr)` → `kfree(M_CRED)` at refcount
  0 (`kern_prot.c:1098-1126`).
* pc64 fixed this exact race years ago: `sys/platform/pc64/x86_64/trap.c:186-204`
  (comment: "To avoid races with another thread updating p_ucred we obtain
  p_spin"). The vkernel twin (file under audit) never got the fix.

Race window: between the second load of `curp->p_ucred` (inside
`crhold(curp->p_ucred)`, trap.c:168) and the `atomic_add_long(&cr->cr_ref,1)`
(kern_prot.c:1082-1087). If the sibling thread executes swap+`crfree`+`kfree`
for that cred in the window, the victim increments the refcount of a freed
object (resurrection), then later `crfree`s it again (double free /
premature free of a live cred), and each bad `crfree` over-drops
`uidrop(cr->cr_uidinfo)` — freeing a uidinfo still on the hash chain.

## How it was reproduced

Guest: DragonFly 6.5-DEVELOPMENT #0 Thu Jul 2 2026 (X86_64_GENERIC), 6 CPUs.
vkernel: same-tree VKERNEL64 build, `-m 512m -r /vkroot.img -n 2` (SMP).
Racer: in-vkernel root userland, 8 pthreads; each thread loops
`seteuid(1)` (cratom when euid 0) / `seteuid(0)` (cratom when euid 1) /
`geteuid()` (returns through the racy `userenter`). No privilege beyond
euid changes is needed on the trigger side (POSIX saved-id semantics allow
the 0↔svuid churn from any setuid-root program).

Result: **fatal kernel-mode page fault within the first 10 seconds of
full-rate churn, 3 out of 3 baseline runs**, identical RIP:

```
Fatal trap 12: page fault while in kernel mode
fault virtual address = 0x20
instruction pointer = 0x2b:0x4c6a2b
current process = 4 (df5337)
Stopped at 0x4c6a2b: cmpl %edi,0x20(%rax)
trace:
uifind() at 0x4c6a2b        <- resolved via nm: uifind+0x2b
change_euid()
sys_seteuid()
syscall2()
go_user()
```

`uifind` is called by `change_euid()` immediately after `cratom_proc()`
(`uireplace(&cr->cr_uidinfo, uifind(euid))`) — i.e. the crash is on the
writer side of the race, walking the uidinfo hash into an entry freed by
the refcount corruption the racing `userenter` caused. Exactly the
predicted cascade.

### Controls

1. **Real host kernel (pc64 — has the p_spin fix): identical racer, 8
   threads, 240s: 279,648,000 ops, exit 0, no kernel messages**
   (`host_control.log`). Same shared `kern_prot.c` cred/uidinfo code, ~5×
   the churn rate of the vkernel → the crash is not a kern_prot.c bug, it
   is the vkernel64 `userenter` divergence.
2. **Fixed vkernel** (`fix.diff`: p_spin + `<sys/spinlock2.h>`): 240s ×
   51,490,833 ops and 120s × 28,940,348 ops, clean, vkernel alive
   (run.fixed.log inline below, run.fixed2.log). Bad behavior gone.

The intermediate "control" in run.sh (`control` mode = churn-only) ALSO
crashed — correctly so, since churn threads' own syscall returns still
traverse the racy `userenter`; this is expected and documented, not an
anomaly: there is no in-vkernel syscall path that avoids `userenter`.

## Exploit chain (analysis; corruption itself is demonstrated)

1. Emulated process P (any setuid-root program pattern, e.g. after
   `setuid(getuid())`-style drops, or plain root in the vkernel) with ≥2
   threads on an SMP vkernel; thread B churns creds (seteuid ruid↔svuid),
   thread A syscalls.
2. A's `userenter` `crhold`s a freed cred (resurrection) → later double
   `crfree` → premature `kfree` of a live cred / double `kfree`.
3. Recycling: `crget()` hands the same M_CRED slot to another owner; P's
   victim thread then caches (userenter) the aliased cred, and its next
   syscall authorizes with the other owner's credential
   (`nd->nl_cred = td_ucred` — vfs_nlookup.c:155; `sys_geteuid` —
   kern_prot.c:177) — an in-vkernel privilege-confusion primitive
   (uid → root inside the vkernel) with a one-syscall window, plus
   arbitrary M_CRED-zone UAF corruption for heap grooming.
4. Demonstrated stopping point: steps 1-2 verified live (kernel crash);
   step 3 is source-derived; weaponizing the one-syscall confusion window
   to a stable uid=0 shell inside the vkernel was not attempted (the
   corruption crash is the finding's proof).

## False-positive checks done

* Confirmed pc64 has the lock and vkernel does not (diff of both trap.c).
* Confirmed `cratom_proc` (all 15+ setXid call sites, kern_prot.c:498-877)
  uses p_spin in this tree — the writer-side interlock the reader is
  missing exists.
* Confirmed the crash is not the host kernel (host control survived).
* Confirmed the crash is not the racer's own logic (fixed vkernel runs the
  identical binary cleanly; host runs it cleanly).

## Fix validation

`fix.diff` (authored against the read-only tree, applied only inside the
guest's /usr/src copy): p_spin interlock + spinlock2.h include. Baseline:
fatal trap <10s (3/3). Patched: 240s+120s clean survival, ~80M total ops.
fix_status: fixed.

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