⬢ DragonFlyBSD Kernel Audit
DF-5673 / verdict.json
← back to finding ↓ download raw
{
  "finding_id": "DF-5673",
  "status": "reproduced",
  "reproduced": true,
  "impact": "none",
  "confidence": "certain",
  "verdict": "REPRODUCED on the stock guest kernel (3/3 runs) and FIXED by fix.diff on a rebuilt kernel (2 boots): the /dev/io IOPL grant (cpu_set_iopl, sys/platform/pc64/x86_64/sysarch.c:52-57) is unaccounted per-LWP trap-frame state. A child forked while a parent holds /dev/io inherits PSL_IOPL via the verbatim frame bcopy in vm_machdep.c:158-159; when the LAST fd closes, mmclose (kern_memio.c:210-212) clears only the closing LWP, so the child keeps IOPL=3 with zero fds open anywhere (io(4) contract violated), and a setuid() privilege drop never revokes it (nothing re-checks PSL_IOPL outside exec_setregs/EFL_SECURE). Demonstrated: root opens /dev/io -> forks -> child closes its fd, parent last-closes -> child pushfq IOPL=3, then child setuid(65534) -> IOPL still 3. Unprivileged reachability was hunted and is closed: syscall 165 is a hard EOPNOTSUPP stub on pc64 (no IOPERM/LDT dispatch, verified live for 9 cmd values), /dev/io open as uid 1001 gives EPERM, and sigreturn/ptrace rflags forging is rejected by EFL_SECURE (kernel console shows the machdep.c:1024 rejection line). Severity Low: entry requires an already-privileged open; the escaped grant is permanent ring-3 port I/O that outlives the fd and the privilege. Primary observable is the RFLAGS IOPL bits (pushfq); port-access lines in the logs are corroboration only because this QEMU rig intermittently fails to enforce CPL3 IOPL=0 port faults (documented in rig-flip.txt).",
  "exploit_chain": "root opens /dev/io (mmopen -> cpu_set_iopl, IOPL=3 in opener LWP frame) -> fork (vm_machdep.c:159 frame bcopy grants child IOPL=3; child holds fd) -> child closes its fd; parents close is the last reference -> mmclose -> cpu_clr_iopl() clears the PARENT (closer) frame -> child retains IOPL=3 with no /dev/io fd open in the system -> child setuid(65534): grant survives the privilege drop -> unprivileged-uid process retains full raw port I/O (in/out on all 65536 ports) until it execs or exits. Patched: cpu_fork strips PSL_IOPL from the child frame -> chain broken at step 2 (observed IOPL=0).",
  "evidence": "findings/poc/DF-5673/run.lifecycle.log + run.2.log (baseline chain: step4 child: all /dev/io fds closed, IOPL=3, step5 uid=65534 IOPL=3); run.surface.log (sysarch EOPNOTSUPP x9, /dev/io EPERM as uid 1001); fix_run.log (patched kernel: step4/5 IOPL=0); fix_build.log (6976-line quickkernel build of custom #1); fix.diff (1-hunk cpu_fork PSL_IOPL strip); rig-flip.txt (rig enforcement caveat); VERDICT.md for the full narrative",
  "kernel_refs": [
    "sys/platform/pc64/x86_64/sysarch.c:46-49",
    "sys/platform/pc64/x86_64/sysarch.c:52-57",
    "sys/platform/pc64/x86_64/sysarch.c:59-65",
    "sys/kern/kern_memio.c:184-195",
    "sys/kern/kern_memio.c:210-212",
    "sys/platform/pc64/x86_64/vm_machdep.c:158-159",
    "sys/platform/pc64/x86_64/machdep.c:1023-1026",
    "sys/platform/pc64/x86_64/machdep.c:1481-1490",
    "sys/platform/pc64/x86_64/machdep.c:3025",
    "sys/cpu/x86_64/include/psl.h:81-82",
    "share/man/man4/io.4"
  ],
  "poc_changes": "No orchestrator seed pack existed (empty reservation); both programs written fresh. Debug iterations required: (1) unbuffered stdout (ssh pipe buffering swallowed output on crash), (2) catch SIGBUS not just SIGSEGV \u2014 DragonFly delivers usermode #GP (T_PROTFLT) as SIGBUS per trap.c:512-515, (3) atoi() does not parse hex port arguments, (4) child output lost to _exit() buffering \u2014 setvbuf at main. sysarch cmd constants are irrelevant on pc64 (hard stub), so FreeBSD i386 IOPERM values were probed as representative garbage.",
  "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": 4,
  "guest_dirty": 0,
  "build_cmd": "cc -O1 -o /tmp/iopl_lifecycle /tmp/iopl_lifecycle.c && cc -O1 -o /tmp/sysarch_surface /tmp/sysarch_surface.c",
  "run_cmd": "/tmp/sysarch_surface (uid 1001); /tmp/iopl_lifecycle (root)",
  "code_hash": "56f04aa5c1c0d710903f6ab37291cbdf7115c628f3f4aaa4e7283a90de8c6953 (iopl_lifecycle.c); b02f51a3cbfd9ed8773fd8880bc6c3e639d659e951b6f965e357783ae71ddc01 (sysarch_surface.c)",
  "notes": "File otherwise clean: sys_sysarch is an unconditional EOPNOTSUPP stub (no dispatch bounds, no copyin sizes, no IOPERM bitmap, no LDT) \u2014 pass-2 checklist items (a)-(d) all terminate at the stub or at priv-gated /dev/io; syscons KDENABIO (syscons.c:1383-1395) duplicates the grant inline with the same shape (noted, other file). exec clears IOPL; sigreturn/ptrace forging closed by EFL_SECURE; no TSS ioperm state is ever set on pc64 (machdep.c:2881 bitmap-absent arrangement; private-TSS path in swtch.s:482-512 is #if 0). Guest reset to clean-source snapshot after fix validation (uname back to #0 Jul 2).",
  "recommended_fix": "Strip PSL_IOPL from the child trap frame in cpu_fork() (vm_machdep.c) so the /dev/io IOPL grant cannot be inherited across fork; a complete fix additionally revokes the grant on credential/privilege drop.",
  "fix_status": "fixed",
  "fix_baseline_reproduced": 1,
  "fix_patched_reproduced": 0,
  "fix_verdict": "fix.diff (1 hunk: lp2->lwp_md.md_regs->tf_rflags &= ~PSL_IOPL after the frame bcopy in cpu_fork) applied to the guest /usr/src, quickkernel built (fix_build.log, 6976 lines), installed, rebooted into custom kernel #1 Fri Oct 9 19:21:17 UTC 2026. Re-running the exact baseline PoC: child IOPL=0 at step4 and step5 (was IOPL=3 on baseline, 3/3 runs) on two separate boots of the patched kernel \u2014 fork-inheritance vector closed; opener-side setuid persistence remains and is documented as residual (matches FreeBSD io(4) opener semantics; full revocation needs per-LWP grant tracking, upstream-design scope).",
  "fix_evidence": "findings/poc/DF-5673/fix.diff; fix_build.log (buildkernel log); fix_run.log (patched-kernel PoC run: [step4] child: ... IOPL=0, [step5] child: uid=65534 IOPL=0); fix_run.surface.log (post-fix surface regression: sysarch still EOPNOTSUPP, /dev/io still EPERM, sigreturn forge still rejected)",
  "fix_kernel_uname": "DragonFly dfbsd 6.5-DEVELOPMENT DragonFly 6.5-DEVELOPMENT #1: Fri Oct  9 19:21:17 UTC 2026     root@dfbsd:/usr/obj/usr/src/sys/X86_64_GENERIC  x86_64"
}