⬢ DragonFlyBSD Kernel Audit
DF-5822 / verdict.json
← back to finding ↓ download raw
{
  "finding_id": "DF-5822",
  "status": "reproduced",
  "reproduced": true,
  "impact": "leak",
  "confidence": "certain",
  "verdict": "Reproduced and fix-validated on the live guest. sysctl_kern_proc() caches an unreferenced credential pointer (kern_proc.c:1669 cr1 = curproc->p_ucred) and dereferences it per-row across blocking SYSCTL_OUT copyouts (p_trespass at 1715-1721, PRISON_CHECK at 1768, jailed() at 1789). cratom_proc() (kern_prot.c:1170-1190) replaces and crfree()s p_ucred on every setuid-family change from another thread, leaving cr1 dangling; the freed M_CRED chunk recycled by crget() (M_ZERO) reads cr_uid==0 and p_trespass() returns blanket no-trespass (kern_prot.c:1057-1058). Rig: setegid(0)<->setegid(1001) churn never touches cr_uid, so any root kinfo_proc row emitted to the euid-1001 poller under security.ps_showallprocs=0 is provably from the stale pointer: 78,481/632,433 (12.4%), 73,310/845,892 and 60,297/854,135 bypass iterations across three baseline runs (80,584+73,620+60,635 root rows); control (churn inert): 0 rows in 719,556 iterations. After applying fix.diff (cr1 = curthread->td_ucred, the held trap-layer cache, at all three handler sites 1669/1878/2038), rebuilding and booting the patched kernel: 922,345 + 894,484 poll iterations with ZERO bypass iterations and ZERO root rows. Impact ceiling: unprivileged disclosure of other users (incl. root) full kinfo_proc metadata under the ps_showallprocs=0 hardening config; on default config the stale reads only degrade PRISON_CHECK/jailed decisions (relevant on jail hosts). UAF read only - no write primitive.",
  "exploit_chain": "unprivileged multithreaded process (or any process whose threads legally churn credentials, e.g. setuid-root or ruid!=svuid binaries) -> thread B: setegid churn forcing cratom_proc replace+crfree of p_ucred -> thread A: sysctl(kern.proc.uid.0) whose handler caches the soon-to-be-freed cred at kern_proc.c:1669 -> blocking SYSCTL_OUTs extend the window across the whole call -> per-row p_trespass() reads the freed chunk (recycled M_ZERO -> cr_uid==0 -> return 0) -> other users kinfo_proc rows emitted to the unprivileged poller",
  "evidence": "run.log / run.2.log (baseline: STALE cr1 BYPASS marker lines, 73,310/60,297 bypass iterations, root rows 73,620/60,635); control.log (0 rows / 719,556 iters with churn inert); run_patched.log (0 bypass / 922,345 + 894,484 iters after fix); VERDICT.md (full mechanism, table, fix-validation); fix.diff (3-site td_ucred fix, applies clean with git apply --check)",
  "kernel_refs": [
    "sys/kern/kern_proc.c:1669",
    "sys/kern/kern_proc.c:1715",
    "sys/kern/kern_proc.c:1768",
    "sys/kern/kern_proc.c:1789",
    "sys/kern/kern_proc.c:1878",
    "sys/kern/kern_proc.c:2038",
    "sys/kern/kern_prot.c:1170",
    "sys/kern/kern_prot.c:1057",
    "sys/kern/kern_prot.c:1065",
    "sys/platform/pc64/x86_64/trap.c:199"
  ],
  "poc_changes": "Authored from scratch (no seed). First scaffold attempt (mode 4555) left svgid=1001 so setegid(0) failed EPERM - kept as the control run; final scaffold is mode 6755 (setgid bit keeps svgid=0 through the euid drop). Churn deliberately uses setegid (never touches cr_uid) so bypass rows are attributable to the stale pointer alone.",
  "attempts": 4,
  "guest_uname": "DragonFly dfbsd 6.5-DEVELOPMENT DragonFly v6.5.0.1712.g89e6a-DEVELOPMENT #1: Mon Jun 29 14:18:01 UTC 2026 root@ephemeral-5c2002c44b6c:/usr/obj/usr/src/sys/X86_64_GENERIC x86_64 (baseline); fix-validated on rebuilt #1: Tue Oct 13 06:36:10 UTC 2026 root@dfbsd:/usr/obj/usr/src/sys/X86_64_GENERIC",
  "runtime_sec": 160,
  "guest_dirty": 1,
  "build_cmd": "cc -O2 -pthread -o df5822_rig df5822_rig.c",
  "run_cmd": "sysctl -w security.ps_showallprocs=0 && ./df5822_rig 25",
  "code_hash": "d03279e469c0ff40b41a326cb9c9b59e76ee225ff653fde2472775cdf786104a",
  "notes": "Fix validation used make nativekernel KERNCONF=X86_64_GENERIC (snapshot lacks built-world tools for buildkernel). Guest /tmp is cleared on reboot, so the full kernel build log was lost to the validation reboot; decisive lines (Kernel install for X86_64_GENERIC completed, BUILD_OK) are recorded in build_fix.log, and the new kern.version (Tue Oct 13 06:36:10 UTC 2026) plus the behavior flip independently prove the patched kernel ran. Guest reset (with-src) before return.",
  "recommended_fix": "Use the held per-syscall credential: cr1 = curthread->td_ucred instead of raw curproc->p_ucred at kern_proc.c:1669/1878/2038.",
  "fix_status": "fixed",
  "fix_baseline_reproduced": 1,
  "fix_patched_reproduced": 0,
  "fix_verdict": "Applied fix.diff (cr1 = curthread->td_ucred at all three kern.proc handler sites) to the guests /usr/src, rebuilt with nativekernel, rebooted into the new kernel, re-ran the identical rig and scaffold: baseline 60k-78k bypass iterations per 25-30s run with 60k-80k root rows -> patched 0 bypass iterations / 0 root rows across 922,345 + 894,484 polls. Bypass eliminated.",
  "fix_evidence": "run_patched.log (0/922345 and 0/894484); VERDICT.md fix-validation section; build_fix.log (kernel install completion lines); env.txt (both kernel versions)"
}