DF-5673 / verdict.json
{ "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" } |