# 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)

1. `pfind()` returns any non-SZOMB process on allproc
   (sys/kern/kern_proc.c:522-531). A freshly-forked process is inserted
   into allproc at `proc_add_allproc()` (sys/kern/kern_fork.c:491) with
   `p_stat = SIDL` (kern_fork.c:458) and an **empty** `p_lwp_tree`
   (RB_INIT at kern_fork.c:468).
2. The first lwp enters the tree only inside `lwp_fork2()`
   (kern_fork.c:848), after `vm_fork()` (kern_fork.c:675) and several
   `M_WAITOK` allocations. `p_stat` becomes SACTIVE even later, in
   `start_forked_proc()` (kern_fork.c:950). So SIDL ≠ zero-lwp alone, but
   **SIDL ∧ pre-848 ⇔ zero lwp**, and pfind does not filter SIDL.
3. `sys_rtprio()` (kern_resource.c:701-704) takes `p->p_token` and
   dereferences `FIRST_LWP_IN_PROC(p)` with no NULL check:
   * RTP_LOOKUP: `copyout(&lp->lwp_rtprio, uap->rtp, …)` (:707) — reads
     address 0x198. copyout executes under `pcb_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 with `pcb_onfault == NULL` is fatal
     (trap.c:917-926) → panic.
4. Reachability of the token: fork1 holds `p2->p_token` across 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
   churning `mmap`/`munmap` makes the forking thread block on that token
   in the middle of the window, dropping `p2->p_token`. The sprayer (a
   second process predicting the sequential pids) then acquires
   `p->p_token`, sees the empty tree, and hits the NULL.
5. 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 and
     `uap->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 lookup` as 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 set` as 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
  matches `lp->lwp_rtprio = rtp` at kern_resource.c:748 with `lp == 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**.
