DragonFlyBSD Kernel Audit
DF-2753 / verdict.json
← back to finding ↓ download raw
{
  "finding_id": "DF-2753",
  "status": "reproduced",
  "reproduced": 1,
  "impact": "panic",
  "confidence": "certain",
  "verdict": "fork1() publishes the embryonic SIDL child to the allproc list/pid hash at kern_fork.c:491 before p_pgrp (set by the :496 bcopy) and p_ucred (:509) are initialized, while pfind() (kern_proc.c:502) and allproc_scan() (kern_proc.c:1401) filter only SZOMB. Unprivileged syscall paths that dereference those fields WITHOUT holding p_token then NULL-fault. Reproduced live as unprivileged user maxx on the stock INVARIANTS 6.5-DEVELOPMENT #0 guest: kill(-1, SIGWINCH) spray (allproc_scan -> killpg_all_callback -> CANSIGNAL -> p_trespass -> caps_get(NULL)) racing a fork loop panicked in <90 s: 'Fatal trap 12 ... fault virtual address = 0xa0 ... Stopped at p_trespass+0x3f: movl 0xa0(%r12),%eax' -- exactly the predicted sink. Same-class unprivileged sinks: sched_getscheduler/sched_getparam/etc via CAN_AFFECT (kern_p1003_1b.c:151, pre-token p_ucred deref), getsid(pid) (kern_prot.c:153, no token, p_session == p_pgrp->pg_session), and jailed-only ptrace PT_ATTACH (sys_process.c:153) / sysctl kern.proc.pid (kern_proc.c:1690) via PRISON_CHECK(cr1, NULL). Primitive ceiling rigorously characterized: fixed-VA NULL READ (attacker controls when the field is NULL, not its content; fault VA constant 0xa0; DFly treats kernel access to user VA 0 as fatal regardless of mapping) -- no write, no UAF, no controlled pointer, so uid0 is NOT reachable from this bug; the deliverable is a reliable unprivileged local kernel panic (DoS). Fix authored after reproduction (move startcopy bcopy + cred install before proc_add_allproc; SIDL skips in pfind/allproc_scan/alllwp_scan), built in-guest (make -j6 nativekernel RC=0), rebooted (#1: Mon Aug 31 19:48:28 UTC 2026), and validated: identical PoC load ran 120 s / 69,921 forks / 19,063,267 kill(-1) scans with NO panic (plus a second 60 s / 9.3M-scan confirmation run); baseline panicked in <90 s.",
  "exploit_chain": "unpriv user -> 2 threads (fork storm + kill(-1,SIGWINCH) spray) -> allproc_scan visits embryonic SIDL proc in the kern_fork.c:491->509 window -> CANSIGNAL -> p_trespass(cr1, p->p_ucred==NULL) -> caps_get(NULL+0xa0) -> Fatal trap 12 kernel panic. Chain terminates at panic: read-only NULL deref with no attacker-controlled pointer value; no escalation path exists for this primitive (fixed VA, fatal-on-user-VA-0 kernel policy).",
  "evidence": [
    "panic.txt: 'Fatal trap 12: page fault while in kernel mode', 'fault virtual address = 0xa0', 'Stopped at p_trespass+0x3f: movl 0xa0(%r12),%eax', 'Fatal user address access from kernel mode from sidl_race'",
    "run.log: panic run capture (ssh died mid-run; guest wedged at db>, vm.sh status => down)",
    "fix_run.log: patched kernel, 120 s, forks=69921 kills=19063267, 'SURVIVED ... (no panic)', RUN_RC=0, guest up",
    "fix_run.2.log: second confirmation run, 60 s, kills=9335585, SURVIVED",
    "fix_build.log: full 35635-line make -j6 nativekernel log, RC=0",
    "fix.diff: reorder + SIDL-skip fix, git-apply-able (validated against pristine sys/ tree)",
    "VERDICT.md section 3: primitive characterization (why panic and not uid0)"
  ],
  "kernel_refs": [
    "sys/kern/kern_fork.c:491",
    "sys/kern/kern_fork.c:496",
    "sys/kern/kern_fork.c:509",
    "sys/kern/kern_proc.c:502",
    "sys/kern/kern_proc.c:1401",
    "sys/kern/kern_sig.c:711",
    "sys/kern/kern_sig.c:721",
    "sys/kern/kern_prot.c:1023",
    "sys/kern/kern_prot.c:1038",
    "sys/kern/kern_p1003_1b.c:73",
    "sys/kern/kern_p1003_1b.c:151",
    "sys/kern/kern_prot.c:153",
    "sys/kern/sys_process.c:153",
    "sys/kern/kern_proc.c:1687",
    "sys/kern/kern_proc.c:1717",
    "sys/sys/proc.h:344",
    "sys/sys/proc.h:462"
  ],
  "poc_changes": "Wrote PoC from scratch (no seed existed). Used kill(-1, SIGWINCH) broadcast spray instead of pid-guessing: exercises CANSIGNAL on every proc per scan, needs no pid prediction, and SIGWINCH's default-ignore action guarantees the only effect is the permission-check deref.",
  "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": 90,
  "guest_dirty": 0,
  "build_cmd": "cc -O2 -Wall -pthread -o sidl_race sidl_race.c",
  "run_cmd": "./sidl_race 60 4   (as unprivileged user; 1 fork thread + 4 kill(-1,SIGWINCH) spray threads)",
  "code_hash": "edf8a6756d22b14db7fb9b37231a6894374b2ec836b4a7fe4b9edb62bd342d3e",
  "notes": "Sinks killed/protected by design: kern_kill(pid) direct path takes p_token before CANSIGNAL (kern_sig.c:800-804) -- safe; pg-signal path excluded by the RFPGLOCK pg_lock protocol (fork1 :385-387/:612-615 vs dokillpg LK_EXCLUSIVE kern_sig.c:728). Window is ~0.4us per fork but continuously retryable; 6-cpu guest hits in <90 s. Impact ceiling is panic (fixed-VA NULL read; no write primitive; DFly fatal-traps kernel access to user VA 0 even if page 0 were mapped). df-bsd-reviewer pass-2 of sys/kern/kern_fork.c; known findings DF-0032/DF-0033 not re-reported.",
  "recommended_fix": "Initialize the startcopy region (p_pgrp) and p2->p_ucred BEFORE proc_add_allproc() publishes the SIDL child in fork1(), and skip SIDL procs in pfind()/allproc_scan()/alllwp_scan()",
  "fix_status": "fixed",
  "fix_kernel_uname": "DragonFly dfbsd 6.5-DEVELOPMENT DragonFly 6.5-DEVELOPMENT #1: Mon Aug 31 19:48:28 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 applied to guest /usr/src after vm.sh reset with-src; make -j6 nativekernel RC=0; installkernel RC=0; rebooted into #1 kernel; identical PoC re-run 120 s (69921 forks, 19063267 kill(-1) scans) + confirmation 60 s (9335585 scans): no panic, guest healthy, vs baseline panic <90 s on stock #0.",
  "fix_evidence": "fix_build.log (RC=0, 35635 lines), fix_build.note, fix_run.log (SURVIVED, RUN_RC=0), fix_run.2.log (SURVIVED), fix.diff"
}