# VERDICT — DF-5673

**Finding:** the pc64 IOPL grant issued via `cpu_set_iopl()`
(sys/platform/pc64/x86_64/sysarch.c:52-57, reachable only through
`/dev/io` open at sys/kern/kern_memio.c:193, gated `SYSCAP_RESTRICTEDROOT`
+ `securelevel <= 0`) is unaccounted per-LWP state:

1. **fork inherits it** — `cpu_fork()` copies the parent trap frame verbatim
   (`sys/platform/pc64/x86_64/vm_machdep.c:158-159`), `PSL_IOPL` included.
2. **the last fd close clears an arbitrary, usually wrong LWP** —
   `mmclose` (sys/kern/kern_memio.c:210-212) calls `cpu_clr_iopl()` on
   *whichever LWP happens to close the last reference*; LWPs that inherited
   the grant (forked children, other threads) keep IOPL=3 with no fd open
   anywhere — violating the documented io(4) contract
   ("Any process that holds a file descriptor on /dev/io open will get its
   IOPL bits in the flag register set", share/man/man4/io.4).
3. **privilege drop does not revoke it** — nothing between `cpu_set_iopl()`
   and user mode re-checks the grant: no `PSL_IOPL` clearing exists in the
   setuid/cred-change path; only `exec_setregs` (machdep.c:1481-1490) and
   the `EFL_SECURE` sanitizers at machdep.c:1023 (sigreturn) and
   machdep.c:3025 (ptrace/procfs `set_regs`) touch rflags, and none of those
   runs on `setuid()`.

Threat: a privileged process that opens `/dev/io` (or issues KDENABIO,
syscons.c:1383-1395 — same inline idiom) and later drops privileges, forks
worker children, or passes the fd away leaves **permanent, unrevokable full
ring-3 port I/O** in LWP frames that no longer correspond to any open fd.
This is the state-machine that gate DF-5646-class port hazards behind
privilege; the gate does not close.

## Result: REPRODUCED (baseline), FIXED (patched kernel)

### Baseline (stock ISO kernel `#0 Thu Jul 2 06:02:54 UTC 2026`)

`run.lifecycle.log` + `run.2.log` (3 consecutive runs, identical):

```
[step1] open(/dev/io) ok, IOPL now = 3            <- cpu_set_iopl hit (sysarch.c:54)
[step3] parent closed fd (last close), parent IOPL=0  <- cpu_clr_iopl hit closer (sysarch.c:62)
[step4] child: all /dev/io fds closed, IOPL=3     <- fork inheritance + no revocation
[step5] child: uid=65534 IOPL=3                    <- survives setuid() privilege drop
```

Child closed its inherited fd *before* the parent's last close, so at the
moment of `mmclose` no child fd existed; the child still shows IOPL=3 by
`pushfq` — the grant outlived every fd reference, exactly as predicted from
the code path.

### Negative results (verified live, `run.surface.log`)

- `sysarch` (syscall 165) returns `EOPNOTSUPP` for every cmd (0,1,2,3,101,
  102,103,127,0x1234) as uid 1001 — pc64 has **no** IOPERM/LDT dispatch, no
  copyin, nothing to abuse (sys/platform/pc64/x86_64/sysarch.c:46-49).
- `/dev/io` open as uid 1001 → `EPERM` (priv gate intact,
  kern_memio.c:184-195).
- **sigreturn forge**: `setcontext()` with `uc_mcontext.mc_rflags |= 0x3000`
  is rejected — the guest console shows `sigreturn: rflags = 0x3206`, printed
  only inside the `EFL_SECURE` rejection branch (machdep.c:1023-1026);
  `PSL_USERCHANGE` (sys/cpu/x86_64/include/psl.h:81-82) excludes `PSL_IOPL`.
  The userland SIGSEGV afterwards is DF's setcontext trampoline dying on the
  EINVAL return (no error path), not a kernel fault. Same `EFL_SECURE` check
  covers ptrace/procfs `set_regs` (machdep.c:3025). No unprivileged forge of
  IOPL exists.
- `exec` clears IOPL (`exec_setregs`, machdep.c:1490) — only the no-exec
  paths keep it.

### Fix validation (`fix.diff`, `fix_build.log`, `fix_run.log`)

`fix.diff` strips `PSL_IOPL` from the child trap frame in `cpu_fork()`
(vm_machdep.c, 1 hunk, `git apply --check` clean against sys/). Applied to
the guest's /usr/src copy, `make quickkernel KERNCONF=X86_64_GENERIC`
(6976-line log), `make installkernel`, reboot into custom `#1 Fri Oct 9
19:21:17 UTC 2026`:

```
[step4] child: all /dev/io fds closed, IOPL=0     <- was 3 on baseline
[step5] child: uid=65534 IOPL=0
```

Observed on **two separate boots** of the patched kernel (boot 2 and boot 3)
— the fork-inheritance vector is closed. Residual (documented, out of scope
of the 1-line fix): the *opener* itself still keeps IOPL=3 across its own
`setuid()` until it execs or exits (FreeBSD-documented io(4) semantics for
the opener), and KDENABIO (syscons.c:1394) has the same shape. A complete
fix would track the grant per-LWP and revoke on cred change; that is
upstream-design territory.

### Guest / rig notes

- Guest: `DragonFly dfbsd 6.5-DEVELOPMENT #0: Thu Jul 2 06:02:54 UTC 2026
  X86_64_GENERIC x86_64`, `kern.securelevel: -1`, `/dev/io` `crw------- root
  wheel`. Guest was reset to the clean-source snapshot after validation
  (uname back to `#0`).
- **Rig anomaly (important)**: in this QEMU/KVM host, CPL3 `in/out` at
  IOPL=0 *sometimes succeeds* — it flipped 24/24 faults → 80/80 OK → mixed
  within one shell (`rig-flip.txt`), uncorrelated with uid or handlers.
  The guest kernel never reloads TR after boot (the private-TSS path in
  swtch.s:482-512 is `#if 0 /* JG */`), the TSS descriptor limit is
  `sizeof(tss)-1` (machdep.c:1698) with `tss_iobase = sizeof(tss)`
  (machdep.c:2881) = architectural "no IO bitmap, deny all" — on real
  hardware these accesses always #GP→SIGBUS (trap.c:512-515). The flips are
  therefore a virtual-CPU/rig artifact. Consequence: every "port access
  OK/SIGSEGV" line in the logs is corroboration only; the **decisive
  observable is the RFLAGS IOPL value read via pushfq** (pure kernel-grant
  state), which was deterministic across all runs (3× baseline IOPL=3 chain,
  2× patched IOPL=0).

## Why not higher severity

Entry requires an already-privileged open (RESTRICTEDROOT + securelevel).
The escape is real but post-privilege: persistence/policy violation against
the documented fd-scoped contract, not an unpriv→priv boundary crossing.
Low per the project rubric ("requires … already-privileged user");
CVSS vector recorded for triage comparability.

## Fix status (batch validation)
Applied with the 36 sibling fix.diffs of this batch in findings/poc/_batch41/all_fixes.patch; single in-guest `make -j6 nativekernel KERNCONF=X86_64_GENERIC` completed RC=0 with zero errors under -Werror (full log: findings/poc/_batch41/fix_build8.log). Behavior A/B intentionally skipped for this Info-tier item.
