DF-2791 / verdict.json
{ "finding_id": "DF-2791", "status": "reproduced", "reproduced": 1, "impact": "panic", "confidence": "certain", "verdict": "sys_rtprio() (kern_resource.c:704) dereferences FIRST_LWP_IN_PROC(p) without a NULL check. A process is pfind()-visible with zero lwps during its fork/SIDL window (allproc insertion at kern_fork.c:491 until the first lwp tree insert at kern_fork.c:848), and fork1's p_token is dropped whenever the forking thread deschedules (lwkt_relalltokens), which an unprivileged mmap/munmap churn thread in the forker forces inside vm_fork(). Unprivileged rtprio(RTP_LOOKUP) then reads &((struct lwp*)0)->lwp_rtprio (0x198) inside copyout -> EFAULT (3 hits in 105 syscalls as uid 1001). Privileged rtprio(RTP_SET) executes the raw store lp->lwp_rtprio = rtp at 0x198 with pcb_onfault == NULL -> 'Fatal user address access from kernel mode' -> panic: verified on the serial console ('Stopped at sys_rtprio+0x1b0: movl %eax,0x198(%rdx)'). Unprivileged callers cannot reach the write (blocked at kern_resource.c:718-723), so impact ceiling for a non-root user is the EFAULT side channel; the panic requires an already-privileged caller — severity Low.", "exploit_chain": "unpriv: fork-bomber (p_thread churn via mmap/munmap widens the SIDL zero-lwp window) + pid-predicting rtprio(RTP_LOOKUP) sprayer -> kernel NULL deref read contained by copyout onfault -> EFAULT observability only. priv (root/SYSCAP_NOSCHED): same race with RTP_SET -> 4-byte semi-controlled write at fixed unmappable user address 0x198 -> fatal trap -> panic. No path to uid0: the write address is not mappable (page 0) and not controllable.", "evidence": [ "run.log: '[hit] pid 10115 EFAULT after 97/100/103 syscalls' + 'REPRODUCED' (unpriv leg, kernel #0)", "panic.txt: 'Fatal user address access from kernel mode from rtprio_sidl', 'fault virtual address = 0x198', 'supervisor write data', 'Stopped at sys_rtprio+0x1b0: movl %eax,0x198(%rdx)' (priv leg, kernel #0)", "fixed_lookup.log: 'syscalls=1015185 ok=261860 EFAULT=0' on fix.diff-patched kernel #1", "fixed_set.log: root RTP_SET race >=65s on patched kernel #1, guest alive", "VERDICT.md: full path:line trace of the SIDL window and token-drop mechanism" ], "kernel_refs": [ "sys/kern/kern_resource.c:704", "sys/kern/kern_resource.c:707", "sys/kern/kern_resource.c:718", "sys/kern/kern_resource.c:748", "sys/kern/kern_fork.c:491", "sys/kern/kern_fork.c:848", "sys/kern/kern_proc.c:524", "sys/platform/pc64/x86_64/trap.c:917", "sys/kern/lwkt_token.c:539" ], "poc_changes": "Original seed sketch assumed the SIDL window was reachable by mere forking; in fact fork1 holds p2->p_token and only deschedules drop it. Added a sibling pthread churning mmap/munmap so vm_fork()'s vm_map-token acquisition blocks mid-window (this made the race hit on the first attempt after 0 hits across two earlier variants), fixed zombie reaping (maxprocperuid EAGAIN), pid prediction from a shared ring buffer, and added signal.h/pthread linkage.", "attempts": 4, "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": 420, "guest_dirty": 0, "build_cmd": "cc -O2 -pthread -o rtprio_sidl rtprio_sidl.c", "run_cmd": "./rtprio_sidl lookup ; ./rtprio_sidl set", "code_hash": "5536c9af2c72242f6f6a21eedba94ce41a1171672317b6e42ad51b2d0b9dac9c", "notes": "Guest was reset (vm.sh reset with-src) after verification; guest_dirty refers to the returned state. offsetof(struct lwp, lwp_rtprio) = 0x198 confirmed by the panic fault address.", "recommended_fix": "In sys_rtprio(), return ESRCH when FIRST_LWP_IN_PROC(p) is NULL and LWPHOLD/LWPRELE the lwp across the blocking copyout, mirroring sys_lwp_rtprio() (fix.diff, validated).", "fix_status": "fixed", "fix_kernel_uname": "DragonFly dfbsd 6.5-DEVELOPMENT DragonFly 6.5-DEVELOPMENT #1: Tue Sep 1 10:01:19 UTC 2026 root@dfbsd:/usr/obj/usr/src/sys/X86_64_GENERIC x86_64", "fix_baseline_reproduced": 1, "fix_patched_reproduced": 0, "fix_verdict": "fix.diff (NULL check -> ESRCH + LWPHOLD/LWPRELE) applied to guest /usr/src, make nativekernel -j6 + make installkernel, reboot into kernel #1. Baseline behaviors gone: lookup 0 EFAULT over 1,015,185 probes (was 3 in 105); root RTP_SET race ran 65+s with the guest alive (baseline panicked <50s).", "fix_evidence": [ "fixed_lookup.log", "fixed_set.log", "fix.diff" ] } |