DF-2839 / verdict.json
{ "finding_id": "DF-2839", "status": "reproduced", "reproduced": 1, "impact": "dos", "confidence": "certain", "verdict": "Unprivileged /dev/lpmap page fault on a mapping created by a concurrently-exiting thread lands in the wide drain window of lwp_userunmap() (which sleeps in vm_map_remove with LWP_MP_WEXIT not yet set) and installs a fresh lpmap into the exiting LWP via lwp_usermap(); nothing ever frees it (lwp_userunmap is the only free site and already ran) - a permanent 4KB M_UPMAP kernel heap leak per hit. Demonstrated at ~150 pages/s (~28MB per 45s) from 4 unprivileged processes with constant live-process count, monotonic across runs; the 390M M_UPMAP cap is exhaustible in minutes (local kernel-memory-exhaustion DoS). Narrow same-window edges (pmap_enter racing the drain's pmap_remove; pmap_kextract of the page freed at kern_proc.c:1381) could expose a freed kernel heap page RW to userspace - source-proven only, not demonstrated, documented as speculative. The initially-suspected AB-BA deadlock was disproved: LWKT tokens are re-acquired by the scheduler across tsleep (lwkt_getalltokens), so the exiter does not hold lwp_token while sleeping; two 90s probe runs showed zero stalls.", "exploit_chain": "unpriv user opens /dev/lpmap (0666); thread V mmaps lpmap regions (+faults one for a real lpmap); V pthread_exit -> lwp_exit -> lwp_userunmap parks in vm_map_remove drain; sibling thread T faults a not-yet-removed V-created page -> vm_fault holds map read-lock -> user_kernel_mapping sees lwp_lpmap==NULL -> lwp_usermap passes the LWP_MP_WEXIT guard (not set yet) and allocates+installs a new lpmap; V finishes teardown having already saved/freed the old one; LWP reaped with the new allocation installed -> 4KB M_UPMAP leaked. Repeat at kHz rates -> kmem exhaustion DoS.", "evidence": [ "run.3.log/leak_sample.txt: upmap 15.4K/61.4M -> 22.4K/89.5M -> 29.2K/117M across two 45s unpriv runs, ps count constant 196", "fix.run.log: patched kernel (uname #1 Sep 2 05:47:01) stays at 37/148K across two identical runs (96.6K requests, zero growth)", "VERDICT.md: full interleaving trace with path:line, deadlock negative results (run.log, run.2.log)" ], "kernel_refs": [ "sys/kern/kern_memio.c:801-824", "sys/kern/kern_memio.c:813", "sys/kern/kern_proc.c:1318-1345", "sys/kern/kern_proc.c:1356-1384", "sys/kern/kern_exit.c:688", "sys/kern/kern_exit.c:695", "sys/vm/vm_fault.c:562-580", "sys/vm/vm_map.c:1660-1665" ], "poc_changes": "v1 (deadlock probe) and v2 (storm) never wedged because LWKT tokens do not persist across tsleep; v3 retargeted the same interleaving at the leak: victims mmap 6 regions each + fault one page (real lpmap), 6 storm threads churn the map to stretch the drain window, 3 touchers sweep victims' regions shuffled with time jitter so faults land inside the drain; children setuid(1001) themselves; detection via vmstat -m upmap live-count deltas instead of unkillable-child detection.", "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": 3300, "guest_dirty": 0, "build_cmd": "cc -O -o /tmp/lpdeadlock3 /tmp/lpdeadlock3.c -lpthread", "run_cmd": "/tmp/lpdeadlock3 4 45 (x2, vmstat -m | grep -w upmap between runs)", "code_hash": "68d75cf58b353cc507c8ff63517b5e1f636d8d44de08974cc100f69470b16f3f", "notes": "Fresh-boot baseline on the patched kernel is 37 upmap allocations (148K) - the 15.4K residual seen mid-session on the stock kernel was 100% leak from earlier probe runs. Full guest build log was in /tmp and lost at reboot (see fix.build.log substitute evidence: uname #1 timestamp + /boot/kernel/kernel mtime + hunk-1-succeeded + behavioral zero-growth). Guest reset with-src performed after validation. Suggested bucket: base:kern (dos); speculative memcorrupt edges documented in VERDICT.md for triage.", "recommended_fix": "In lwp_exit() set LWP_MP_WEXIT before lwp_userunmap() so lwp_usermap()'s token-serialized post-allocation guard refuses to install an lpmap into an LWP whose teardown already ran (sys/kern/kern_exit.c, one hunk - see fix.diff).", "fix_status": "fixed", "fix_kernel_uname": "DragonFly dfbsd 6.5-DEVELOPMENT DragonFly 6.5-DEVELOPMENT #1: Wed Sep 2 05:47:01 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 (move LWP_MP_WEXIT set before lwp_userunmap in lwp_exit) to the guest /usr/src, make -j6 nativekernel + installkernel + reboot. Stock kernel: +~7,000 upmap allocations (~28MB) per 45s PoC run. Patched kernel: 37/148K flat across two identical runs (96.6K allocation requests served, wave rates unchanged ~750/child/45s, live-proc count constant). Leak eliminated; guard analysis shows every token interleaving now either frees the late allocation in the drain or refuses it (also closes the stray pmap_enter edge since in-window faults now fail EINVAL).", "fix_evidence": [ "findings/poc/DF-2839/fix.diff", "findings/poc/DF-2839/fix.run.log (patched: baseline 37/148K -> 37/148K -> 37/148K)", "findings/poc/DF-2839/fix.build.log (uname #1 + /boot/kernel/kernel mtime Sep 2 05:50)", "findings/poc/DF-2839/run.3.log (baseline: +7.0K/+6.8K allocations per run)" ] } |