sys_rtprio() NULL lwp dereference on processes in the fork (SIDL) window β unprivileged EFAULT read of &((struct lwp*)0)->lwp_rtprio; privileged raw kernel write to 0x198 panics the kernel
| Field | Value |
|---|---|
| ID | DF-2791 |
| Status | new |
| Severity | Low |
| CVSS 3.1 | CVSS:3.1/AV:L/AC:H/PR:H/UI:N/S:U/C:N/I:N/A:H |
| CWE | CWE-476 NULL Pointer Dereference |
| File | sys/kern/kern_resource.c |
| Lines | 704 (window kern_fork.c:491β848; pfind kern_proc.c:524) |
| Area | kern |
| Confidence | certain |
| Discovered | 2026-08-31 |
| Pass | 2 (GLM 5.3 second pass) |
| Bucket | memcorrupt |
| Reported | pending |
| Known CVE | none |
| CVE match | novel |
Summary
sys_rtprio() uses FIRST_LWP_IN_PROC(p) with no NULL check, unlike its
sibling sys_lwp_rtprio(). Between proc_add_allproc() and the first lwp
insertion in lwp_fork2 β a window containing vm_fork() and several
M_WAITOK allocations β a process is on allproc with p_stat=SIDL and an
empty lwp tree, and pfind() returns it (only SZOMB is skipped). With
lp==NULL: RTP_LOOKUP copyout reads absolute 0x198 (EFAULT, contained by
pcb_onfault); RTP_SET executes lp->lwp_rtprio=rtp β a raw
kernel-mode store to unmappable 0x198 with pcb_onfault==NULL β panic.
Reachable in practice because LWKT drops all tokens on deschedule: an
unprivileged sibling pthread churning mmap/munmap makes fork1's vm_fork
block on the parent vm_map token mid-window, dropping p2->p_token.
Threat model & preconditions
Any local user can observe the NULL deref (RTP_LOOKUP EFAULT on a pid caught mid-fork); a privileged caller (root / SYSCAP_NOSCHED) racing RTP_SET against fork reliably panics. Unpriv callers cannot reach the write; no unprivβroot path (fixed, unmappable write address).
Proof of contest
VERIFIED on the stock guest (findings/poc/DF-2791/rtprio_sidl.c):
20k-entry forker + token-churning pthread + pid-predicting sprayer β
3Γ EFAULT in 105 syscalls as uid 1001; as root with RTP_SET the guest
panicked <50s with Stopped at sys_rtprio+0x1b0: movl %eax,0x198(%rdx).
Fix (NULL check + LWPHOLD, mirroring sys_lwp_rtprio) validated on a
rebuilt kernel: EFAULT=0 over 1,015,185 probes, root race survived
65s+.
Recommended fix
See findings/poc/DF-2791/fix.diff (validated).
References
- DF-2753 (the same SIDL publication window, kern_fork.c side)
Timeline
- 2026-08-31 Discovered during pass-2 audit of kern_resource.c (GLM 5.3); both legs reproduced + fix validated same run.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-2791 Β· 14 files| File | Type | Description | Size | |
|---|---|---|---|---|
| rtprio_sidl.c | β | 5.6 KB | view raw | |
| build.sh | β | 92 B | view raw | |
| run.sh | β | 175 B | view raw | |
| run.log | β | 391 B | view raw | |
| panic.txt | β | 684 B | view raw | |
| panic_full_serial.log | β | 2.2 KB | view raw | |
| fixed_lookup.log | β | 144 B | view raw | |
| fixed_set.log | β | 169 B | view raw | |
| env.txt | β | 253 B | view raw | |
| fix.diff | β | 742 B | view raw | |
| README.md | β | 2.8 KB | β raw | |
| VERDICT.md | β | 4.6 KB | β raw | |
| manifest.json | β | 1.3 KB | view raw | |
| verdict.json | β | 4.4 KB | view raw |
DF-2791 β sys_rtprio() NULL lwp deref on a process in its fork (SIDL) window
What
sys_rtprio() (sys/kern/kern_resource.c:704) takes FIRST_LWP_IN_PROC(p)
with no NULL check, unlike its sibling sys_lwp_rtprio() which checks every
lookup. A process is observable with zero lwps between
proc_add_allproc() (sys/kern/kern_fork.c:491) and the first
lwp_rb_tree_RB_INSERT() in lwp_fork2() (sys/kern/kern_fork.c:848), and
pfind() returns such processes (it only skips SZOMB β
sys/kern/kern_proc.c:524). With lp == NULL:
RTP_LOOKUPβcopyout(&lp->lwp_rtprio, β¦)reads absolute address 0x198 inside copyout's pcb_onfault window β converted to EFAULT (sys/platform/pc64/x86_64/trap.c:985-995). Observable by any unprivileged user.RTP_SET(privileged only; unpriv is stopped at kern_resource.c:718-723) βlp->lwp_rtprio = rtpis a raw kernel-mode write to 0x198 withpcb_onfault == NULLβ "Fatal user address access from kernel mode" β panic.
Race window opener (fully unprivileged): fork1 holds p2->p_token across the
window, but LWKT drops all of a thread's tokens when it deschedules
(lwkt_relalltokens, sys/kern/lwkt_token.c:539-558). The window contains
vm_fork(), which must acquire the parent's vm_map token; a pthread in the
forking process churning mmap/munmap makes the forking thread block on
that token mid-window, dropping p2->p_token long enough for another CPU's
rtprio() to acquire it and observe the empty lwp tree.
Build
cc -O2 -pthread -o rtprio_sidl rtprio_sidl.c
Run
Unprivileged EFAULT leg (harmless, returns 0 on first hits):
./rtprio_sidl lookup
Privileged panic leg (root; kills the guest within ~a minute):
./rtprio_sidl set
Expected output
lookup (unprivileged, baseline kernel):
[hit] pid <N> EFAULT after 105 syscalls (NULL lwp deref inside copyout) ... REPRODUCED: rtprio(RTP_LOOKUP) hit the SIDL zero-lwp window
set (root, baseline kernel) β serial console:
Fatal user address access from kernel mode from rtprio_sidl at ffffffff806543d0 Fatal trap 12: page fault while in kernel mode fault virtual address = 0x198 fault code = supervisor write data, page not present Stopped at sys_rtprio+0x1b0: movl %eax,0x198(%rdx)
With fix.diff applied (patched kernel): lookup reports
EFAULT=0 over >10^6 probes and returns 1 ("NOT reproduced"); set-mode
survives indefinitely.
Fix validation (performed 2026-09-01)
- baseline kernel
#0: lookup β 3 EFAULT in 105 syscalls; set β panic (sys_rtprio+0x1b0 movl %eax,0x198(%rdx), see panic.txt). - fix.diff applied to guest /usr/src,
make nativekernel && make installkernel, reboot into kernel#1: - lookup β
syscalls=1015185 ok=261860 EFAULT=0 β¦ NOT reproduced - set (root, 65 s) β guest alive, no panic.
DF-2791 VERDICT
Finding: sys_rtprio() missing FIRST_LWP_IN_PROC(p) NULL check
(sys/kern/kern_resource.c:704) β NULL lwp pointer dereference against a
process caught in its fork (SIDL) window.
Status: REPRODUCED (both legs). Severity Low (privileged panic / unprivileged EFAULT).
Root cause chain (all source-verified)
pfind()returns any non-SZOMB process on allproc (sys/kern/kern_proc.c:522-531). A freshly-forked process is inserted into allproc atproc_add_allproc()(sys/kern/kern_fork.c:491) withp_stat = SIDL(kern_fork.c:458) and an emptyp_lwp_tree(RB_INIT at kern_fork.c:468).- The first lwp enters the tree only inside
lwp_fork2()(kern_fork.c:848), aftervm_fork()(kern_fork.c:675) and severalM_WAITOKallocations.p_statbecomes SACTIVE even later, instart_forked_proc()(kern_fork.c:950). So SIDL β zero-lwp alone, but SIDL β§ pre-848 β zero lwp, and pfind does not filter SIDL. sys_rtprio()(kern_resource.c:701-704) takesp->p_tokenand dereferencesFIRST_LWP_IN_PROC(p)with no NULL check: * RTP_LOOKUP:copyout(&lp->lwp_rtprio, uap->rtp, β¦)(:707) β reads address 0x198. copyout executes underpcb_onfault, and trap_pfault() for a kernel-mode fault on a user-range address returns to the onfault label when one is registered (sys/platform/pc64/x86_64/trap.c:985-995) β syscall returns EFAULT. * RTP_SET:lp->lwp_rtprio = rtp;(:748) β a plain 4-byte kernel store to 0x198; no onfault protection; kernel-mode fault on a user-range address withpcb_onfault == NULLis fatal (trap.c:917-926) β panic.- Reachability of the token: fork1 holds
p2->p_tokenacross the whole window, but DragonFly LWKT tokens are all released when the owning thread deschedules (lwkt_relalltokens, sys/kern/lwkt_token.c:539-558 β the same property the comment at kern_resource.c:602-606 relies on).vm_fork()must acquire the parent's vm_map token; a sibling pthread churningmmap/munmapmakes the forking thread block on that token in the middle of the window, droppingp2->p_token. The sprayer (a second process predicting the sequential pids) then acquiresp->p_token, sees the empty tree, and hits the NULL. - Privilege gates:
* RTP_LOOKUP has no credential check before the copyout β any
local user can trigger the NULL read (result: EFAULT only).
* RTP_SET for unprivileged callers is stopped at kern_resource.c:718-723
("can't set someone else's" when
caps_priv_check(NOSCHED)fails anduap->pid != 0; pid==0 is curproc which always has an lwp). Hence the write leg requires root / SYSCAP_NOSCHED β severity stays Low.
What was run (guest dfbsd 6.5-DEVELOPMENT, X86_64_GENERIC #0, 6 vCPU)
- Unprivileged leg:
/tmp/rtprio_sidl lookupas uid 1001 β 3 Γ EFAULT on pid 10115 within 105 syscalls (run.log), including on the very first probes. Two earlier variants without the vm_map-token churn thread produced 0 hits (memory-pressure filler pressures user pages, not the token path) β the churn thread is what makes the forker deschedule mid-window. - Privileged leg:
/tmp/rtprio_sidl setas root β guest panicked in DDB within ~50 s. Serial console (panic.txt / panic_full_serial.log):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)β disassembly matcheslp->lwp_rtprio = rtpat kern_resource.c:748 withlp == NULL, and 0x198 == offsetof(struct lwp, lwp_rtprio).
Exploitability ceiling
- Unprivileged: EFAULT side-channel only (the read is contained by copyout's onfault; the source address 0x198 resolves against the attacker's own user map and page 0 is unmappable). No memory disclosure, no control flow.
- Privileged (root or SYSCAP_NOSCHED): reliable kernel panic β a 4-byte write of semi-controlled data (type/prio u_int16 pair) at fixed address 0x198, which is unmappable user space, so it is a crash, not a controlled write. Low: requires an already-privileged caller.
Fix validation
Authored fix.diff (NULL check returning ESRCH + LWPHOLD/LWPRELE around the
blocking copyout, mirroring sys_lwp_rtprio). Applied to the guest's
/usr/src copy, make nativekernel -j6 + make installkernel, reboot into
kernel #1:
- lookup:
syscalls=1015185 ok=261860 EFAULT=0 ESRCH=753325β EFAULT leg gone (fixed_lookup.log). - set (root, β₯65 s): guest alive, no panic (fixed_set.log) β panic leg gone.
fix_status: fixed.
Fix verification
fixedfix.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).
['fixed_lookup.log', 'fixed_set.log', 'fix.diff']
Confirmed kernel references
Detail
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 (decisive lines)
["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']
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.
Verified 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).
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.
No comments yet.