userenter() ucred cache race β vkernel twin missing pc64's p_spin interlock: unlocked crhold races cratom_proc's swap+crfree, giving crhold-on-freed (M_CRED UAF) from unprivileged in-vkernel userland
Summary
Every emulated-userland entry caches p_ucred into td_ucred with an UNLOCKED crhold while a sibling thread's setuid-family call swaps p_ucred under p_spin then crfrees/kfrees the old cred - pc64 fixed this exact race (its trap.c:186-204 comment names it verbatim) but the vkernel twin never got the fix. Outcome 1: M_CRED/uidinfo UAF refcount-resurrection cascade; Outcome 2: one-syscall td_ucred credential-confusion window (in-vkernel uid->root) + M_CRED-zone UAF for grooming. Guest-verified on an SMP vkernel booted inside the QEMU guest with an 8-pthread POSIX-saved-id seteuid racer: baseline vkernel-kernel dies <10s 3/3 (Fatal trap 12 at uifind<-change_euid<-sys_seteuid<-syscall2<-go_user); differential controls: identical racer on the real pc64 (has the fix) 279.6M ops clean, fixed vkernel 51.5M+28.9M ops clean. Guest-of-guest scope. p_spin-interlock port fix.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-5337 Β· 14 files| File | Type | Description | Size | |
|---|---|---|---|---|
| df5337.c | β | 3.5 KB | view raw | |
| build_kernel.sh | β | 443 B | view raw | |
| build_image.sh | β | 1.2 KB | view raw | |
| run.sh | β | 104 B | view raw | |
| run.log | β | 2.6 KB | view raw | |
| panic.txt | β | 673 B | view raw | |
| run.fixed.log | β | 917 B | view raw | |
| run.fixed2.log | β | 173 B | view raw | |
| host_control.log | β | 787 B | view raw | |
| build.log | β | 0 B | β download | |
| fix-build.log | β | 933.8 KB | β download | |
| env.txt | β | 617 B | view raw | |
| fix.diff | β | 1001 B | view raw | |
| VERDICT.md | β | 5.6 KB | β raw |
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(): unlockedcrhold(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 = newcrunderp_spin, thencrfree(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 crfrees 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
- 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 sharedkern_prot.ccred/uidinfo code, ~5Γ the churn rate of the vkernel β the crash is not a kern_prot.c bug, it is the vkernel64userenterdivergence. - 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)
- 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. - A's
userentercrholds a freed cred (resurrection) β later doublecrfreeβ prematurekfreeof a live cred / doublekfree. - 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. - 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.
Fix verification
fixedBaseline vkernel: Fatal trap 12 at uifind+0x2b within 10s, 3/3 runs (run.log, panic.txt). Patched vkernel (fix.diff applied in guest /usr/src, rebuilt): identical PoC ran the full 240s (51,490,833 ops) and 120s (28,940,348 ops) with the vkernel alive and no trap (run.fixed.log, run.fixed2.log). Bad behavior gone.
run.fixed.log, run.fixed2.log, fix-build.log, fix.diff
Confirmed kernel references
- sys/platform/vkernel64/x86_64/trap.c:167
- sys/platform/vkernel64/x86_64/trap.c:378
- sys/platform/vkernel64/x86_64/trap.c:1032
- sys/platform/vkernel64/x86_64/trap.c:1321
- sys/platform/pc64/x86_64/trap.c:196
- sys/kern/kern_prot.c:1082
- sys/kern/kern_prot.c:1098
- sys/kern/kern_prot.c:1166
- sys/kern/kern_prot.c:177
- sys/kern/vfs_nlookup.c:155
Detail
Exploit chain
(demonstrated) SMP vkernel -> in-vkernel multi-threaded cred churn (seteuid ruid<->svuid, POSIX-legal from any setuid-root program) racing sibling-thread syscall entries -> userenter crhold on kfree'd cred -> refcount resurrection -> double crfree -> uidinfo over-drop -> fatal trap in uifind (vkernel kernel memory corruption). (derived, not weaponized) resurrected M_CRED slot recycled by crget() -> victim thread caches aliased cred -> next syscall authorizes as the reuser's credential (one-syscall window) -> in-vkernel root; alternatively UAF grooming in the M_CRED zone.
Evidence (decisive lines)
run.log (baseline crash + ddb 'trace' backtrace at uifind<-change_euid<-sys_seteuid<-syscall2<-go_user), panic.txt, host_control.log (pc64 control, 279.6M ops clean), run.fixed.log + run.fixed2.log (patched vkernel survival), fix.diff, fix-build.log, VERDICT.md
PoC changes
seed sketch replaced entirely: final racer uses always-permitted seteuid churn (seteuid(1) may fail harmlessly when euid already 1; seteuid(0) always permitted and cratoms when euid was 1) so no thread ever exits under any interleaving; every thread both churns (cratom) and syscalls (racy userenter); live op counter for heartbeats; run.sh boots SMP vkernel (-n 2) and feeds 'trace' to the ddb prompt on crash; added real-kernel control run and fixed-kernel validation runs.
Verified recommended fix
Mirror pc64: take p_spin around the crhold in vkernel64 userenter() and include
Verdict
vkernel64 userenter() (sys/platform/vkernel64/x86_64/trap.c:167-173) caches p_ucred into td_ucred with an UNLOCKED crhold on every trap/syscall entry from emulated userland; the writer cratom_proc() (kern_prot.c:1166-1193) swaps p_ucred under p_spin and kfree()s the old cred. pc64 fixed this exact race (pc64/trap.c:196-204 + comment); the vkernel twin never got the fix. On an SMP vkernel (-n 2) an in-vkernel userland process with churning threads hits the window: crhold() atomic_add on a kfree'd M_CRED object -> refcount resurrection -> double crfree -> premature uidinfo free -> fatal kernel-mode page fault at uifind+0x2b (trace uifind<-change_euid<-sys_seteuid<-syscall2<-go_user), reproduced 3/3 baseline runs in <10s of full-rate churn. Differential proof: identical racer on the real pc64 host kernel (which has the fix) survived 279.6M ops clean; vkernel rebuilt with the 6-line p_spin fix survived 51.5M+28.9M ops clean. Beyond the demonstrated corruption, the resurrected cred aliases another crget() owner, and every authorization path consumes td_ucred (sys_geteuid kern_prot.c:177; nd->nl_cred=vfs_nlookup.c:155), yielding an in-vkernel uid->root cred-confusion primitive (chain source-derived; corruption itself demonstrated live).
No comments yet.