DF-2745 / verdict.json
{ "finding_id": "DF-2745", "status": "reproduced", "reproduced": 1, "impact": "dos", "confidence": "certain", "verdict": "exit1() unlinks rfork(RFTHREAD) peer-group members from the p_peers singly-linked list with no common lock (kern_exit.c:384-390; insertion kern_fork.c:484-486 is equally unlocked). Two concurrently exiting members race the walk: the walker caches a predecessor that unlinks between the walker's two dependent loads, then 'unlinks' itself through the stale predecessor and stays linked. Nothing ever clears the leader's p_peers afterwards, so the leader blocks FOREVER in the PCATCH-less tsleep at kern_exit.c:341 - before fdfree()/vmspace_relexit(), pinning its fd table and entire address space - and kill -9 has no effect. The lost member is reaped and freed while still linked; the leader's peer-kill walk (kern_exit.c:330-339) then dereferences the freed struct proc (guaranteed freed-slab reads incl. sys_kill() of stale, possibly recycled pids) and any later walker/insert chases or writes through the dangling link (UAF write into freed M_PROC slab when the stale predecessor was reaped first). Demonstrated as an unprivileged user (rfork is un gated): 4/4 workers hit within 521 attempts / 3 seconds on the stock kernel; six unkillable D-state wchan=exit1 leaders accumulated and survived kill -9. uid=0 escalation not developed (write value is a kernel-heap pointer at slab-reuse-dependent address); demonstrated impact is the reliable unprivileged unkillable resource-pinning DoS plus guaranteed freed-slab dereferences. fix.diff (global peers_token serializing all three mutation sites; leader kill-walk snapshots under the token and kills outside it) validated in-guest: 200,000 attempts / 0 hits / no stuck processes on the rebuilt kernel vs 4 hits in 521 attempts on baseline.", "exploit_chain": "unpriv rfork(RFPROC|RFTHREAD) x3 -> leader X + members A,B,C sharing the p_peers list -> members exit simultaneously with jitter -> one member's exit1 unlink walk stalls between its two dependent loads while the upstream member completes its own unlink (write X->p_peers=next) -> walker resumes, reads the unlinked (stale) predecessor's still-intact p_peers, and writes pred->p_peers instead of unlinking itself -> walker stays linked; leader's p_peers never clears -> leader wedges forever in uninterruptible exit1 tsleep BEFORE fdfree/vmspace_relexit (pins fds + full RSS; kill -9 useless; parent wait() hangs; zombie children leak) -> leader's exit peer-kill walk dereferences freed members (freed-slab reads + SIGKILL to recycled pids) and later walkers/fork inserts read/write through the dangling link (heap UAF; corruption of a reallocated struct proc possible).", "evidence": [ "run.log: decisive run - 4x 'RACE HIT ... stuck in exit1' within attempts=521 elapsed=3s, followed by ps showing 6 leaders in D state, wchan=exit1, PPID=1", "kill-proof.txt: kill -9 delivered to wedged leaders; ps shows them still D-state wchan=exit1 (unkillable)", "env.txt: guest uname (kernel #0 baseline), hw.ncpu=6, and the D-state exit1 process listing", "VERDICT.md: full interleaving analysis (lost unlink) + impact analysis + negative notes", "run.fixed.log: patched kernel (fix.diff, kernel #1) - attempts=200000 elapsed=122s hit=0, zero peersrace processes remaining", "kernel_build_tail.log: fix kernel build/install evidence (0 errors, install completed 14:47:24, rebooted into #1)" ], "kernel_refs": [ "sys/kern/kern_exit.c:384", "sys/kern/kern_exit.c:388", "sys/kern/kern_exit.c:326", "sys/kern/kern_exit.c:330", "sys/kern/kern_exit.c:340", "sys/kern/kern_fork.c:484" ], "poc_changes": "trigger authored from scratch (no seed PoC): SYS_rfork(251) with RFPROC|RFFDG|RFTHREAD; leader + 3 members per attempt, members released simultaneously from a MAP_SHARED flag with per-member jitter loops; detection = waitpid(X, WNOHANG) timeout => leader stuck in exit1; 4 parallel workers.", "attempts": 3, "guest_uname": "DragonFly dfbsd 6.5-DEVELOPMENT DragonFly 6.5-DEVELOPMENT #0: Thu Jul 2 06:02:54 UTC 2026 root@dfbsd:/usr/obj/usr/src/sys/X86_64_GENERIC x86_64", "runtime_sec": 262, "guest_dirty": 0, "build_cmd": "cc -O2 -Wall -o peersrace peersrace.c", "run_cmd": "./peersrace 400000 4", "code_hash": "c7be175cbc31c188ccda5e2d42612c219efe226471e40802d0d6ecbc6434ede9", "notes": "Race is highly winnable: first hits within 72-247 iterations per worker on 6 vCPUs. Wedged leaders sit pre-fdfree => each pins its fd table and full vmspace (memory-exhaustion amplification); zombie members leak. Leader kill-walk sys_kill()s stale pids from freed procs (SIGKILL collateral on recycled pids). No panic observed (slab contents benign); wild-pointer chase/panic possible with slab reuse. Fix validated on rebuilt kernel #1 (Aug 31 14:40:37): 200k attempts, 0 hits. Guest reset to clean snapshot afterwards.", "recommended_fix": "Serialize every p_peers mutation (member unlink in exit1, leader kill-walk snapshot, RFTHREAD insert in fork1) behind a dedicated global lwkt token; PHOLD peers while signaling outside the token to avoid a p_token inversion (see fix.diff).", "fix_status": "fixed", "fix_kernel_uname": "DragonFly dfbsd 6.5-DEVELOPMENT DragonFly 6.5-DEVELOPMENT #1: Mon Aug 31 14:40:37 UTC 2026 root@dfbsd:/usr/obj/usr/src/sys/X86_64_GENERIC x86_64", "fix_baseline_reproduced": 1, "fix_patched_reproduced": 0, "fix_verdict": "Applied fix.diff to a fresh clean-source guest, rebuilt (make nativekernel, 0 errors), installed and rebooted into kernel #1. Re-ran the identical PoC: 200,000 attempts / 122s / hit=0 / no processes in exit1 wait / system healthy - versus baseline 4 hits in 521 attempts. The lost-unlink race and the freed-proc peer-list dereferences are gone.", "fix_evidence": [ "run.fixed.log: attempts=200000 elapsed=122s hit=0, zero peersrace processes remaining", "kernel_build_tail.log: build/install/reboot transcript, kernel #1" ] } |