# DF-5776 VERDICT — reproduced (root-gated defect, no unpriv impact); fix validated

## The claim (pass-2 lead from task_log 2026-10-07)

> "pi_reg has NO upper bound at the ioctl layer (pci_user.c:610-611 checks
> only >=0 + alignment) — cfgreg's clamp is the only net before outl(0xcf8)"

## Verification: TRUE, and the net holds — but it launders failure into success

### Source trace

1. `sys/bus/pci/pci_user.c:602-614` — `PCIOCREAD`/`PCIOCWRITE` validate only
   `io->pi_reg < 0` and `io->pi_reg & (io->pi_width - 1)`. Any aligned
   non-negative `int` (0 .. 0x7FFFFFFC) is accepted. There is no upper bound.
2. Dispatch: `PCIB_WRITE_CONFIG(brdev, bus, dev, func, io->pi_reg, io->pi_data, io->pi_width)`
   at `pci_user.c:633-639` and the read twin at `:651-657` →
   `legacy_pcib_write_config` (`sys/bus/pci/x86_64/pci_bus.c:71-74`) →
   `pci_cfgregwrite/read` (`sys/bus/pci/x86_64/pci_cfgreg.c:167/:143`).
3. The only net is the MD layer:
   - type-1: `pci_cfgenable` (`pci_cfgreg.c:187-198`) rejects
     `(unsigned)reg > PCI_REGMAX` (255) by returning `dataport = 0`;
     `pcireg_cfgread` then returns its `-1` sentinel (`:214`) and
     `pcireg_cfgwrite` does nothing (`:243-244`). The `outl(0xcf8)` at `:194`
     is only ever reached with `reg <= 255` masked `& ~0x03` — **there is no
     path from pi_reg to an unbounded raw port access.**
   - ECAM: `pciereg_cfgread/cfgwrite` (`:322-324`, `:348-350`) reject
     `reg > PCIE_REGMAX` (4095) and mask `reg & 0xfff` in `PCIE_VADDR`
     (`:308-313`), so 256..4095 is the legitimate extended-config range.
4. But `pci_user.c:658` sets `error = 0` unconditionally: an access that the
   MD layer dropped is reported to userland as **success**, and a dropped
   read returns `pi_data = 0xffffffff` — indistinguishable from the genuine
   all-ones value read from a vacant function. `pciconf -w`-style tools
   cannot detect that their write never happened.

### Live proof (guest, kernel #0, uid=0, securelevel=-1, i440FX ⇒ type-1)

`run.log` (decisive) and `run.baseline-freshboot.out` (re-verified after
`vm.sh reset with-src`):

- `reg=0 w=4` → `rc=0 data=0x10001af4` (plumbing works; virtio NIC 0:3:0)
- `reg=256/4092/4096/65536/2147483644 w=4` → **`rc=0 errno=0 data=0xffffffff`**
  (silent success + sentinel data, up to INT_MAX-3)
- `PCIOCWRITE reg=256/4096/2147483644 val=0xdeadbeef` → **`rc=0 errno=0`**
  for all three; readback 0xffffffff; device vendor register unchanged;
  guest healthy (writes dropped — harmless by construction on type-1)
- `reg=-4` and `reg=2 w=4` → `rc=-1 errno=22` — these two checks are the
  *only* validation at the ioctl layer
- unpriv control (`/tmp/opengate` as maxx): `O_RDWR` → EACCES, `O_RDONLY` →
  EPERM (`pci_user.c:82`, `caps_priv_check_self(SYSCAP_RESTRICTEDROOT)`) —
  the whole surface is root-gated, matching the DF-5570 live probes

### Fix validation (mandatory-cycle executed)

`fix.diff` (generated against pristine `sys/`) bounds `pi_reg` at the ioctl
layer using the overflow-safe form `pi_reg > PCIE_REGMAX + 1 - pi_width` and
adds sign/bound/alignment validation for `PCIOCGETBAR`'s `pbi_reg`
(`pci_user.c:685-698` post-patch; GETBAR previously passed `pbi_reg` to
`pci_read_config(..., 4)` with zero validation at :711 — contained in
practice only by the `resource_list_find` rid match, live-probed EINVAL).

Cycle: `vm.sh reset with-src` → `patch -p0` in guest `/usr/src` → baseline
re-verified on the fresh boot (silent success still present) →
`make nativekernel KERNCONF=X86_64_GENERIC` + `make installkernel`
(`fix_build.log`, build_rc=0) → reboot into kernel #1 (Sun Oct 11 19:51:10)
→ same probe (`run.patched.out`):

- `reg=4096/65536/2147483644` reads **and** writes → `rc=-1 errno=22 (EINVAL)`
- `reg=0` → still `0x10001af4`; reg 256..4095 still accepted (required for
  ECAM extended config space); GETCONF pagination byte-identical; GETBAR
  valid BAR (0:2:0 reg 0x10 → base 0xfd000008 len 16M) unchanged; guest
  healthy

**fix_status = fixed** (bad behavior gone on the patched kernel; baseline
reproduced 1, patched reproduced 0).

### Residual (documented, not fixed here)

On type-1-only machines, `pi_reg` in [256, 4095] is still a silent
no-op-with-success: that range must remain accepted because it is the real
PCIe extended config space on ECAM machines, and mechanism capability is
MD-private. Closing it fully needs failure propagation from the cfgreg layer
(`pcireg_cfgread/write` returning void / sentinel) — that is DF-5570's
layer (`sys/bus/pci/x86_64/pci_cfgreg.c`), kept separate on purpose.

## Impact ceiling

None beyond privileged misuse: the device is root-gated at open
(`pci_user.c:82-88`) and read/write ioctls additionally require FWRITE with
securelevel <= 0 (`pci_user.c:85-88`, `:324-330`). No OOB access exists —
`outl(0xcf8)`/ECAM writes are clamped and masked in `pci_cfgreg.c`. The
defect is a validation/honest-error gap that misleads privileged tooling
(and, per DF-5570's companion gap, would let misaligned/straddling offsets
through on ECAM machines). Severity Low; CWE-20.

## Negative results (pass-2 classes hunted and killed)

- **ioctl copyin sizes / struct versioning**: `sys/kern/sys_generic.c:668-697`
  marshals exactly `IOCPARM_LEN(cmd)` bytes into kernel memory (bzero for
  IOC_OUT); every `_IOWR` size (`pciio.h:125-129`, `pci_user.c:227-229`)
  covers all fields the driver touches (`pci_io_old` 16 B, `pi_data` at
  offset 12; `pci_conf_io` 48 B used by both GETCONF variants). No mismatch.
- **`a_data` is kernel memory** — no user-pointer deref in this driver; the
  `PCIOCREAD_OLD` repoint of `ap->a_data` to stack `iodata`
  (`pci_user.c:591-599`) writes results back via `io_old` (the marshalled
  buffer, `:641-648`), and the framework copies back from its own buffer
  (`sys_generic.c:729-730`). Safe.
- **GETCONF pagination/counter arithmetic** (`pci_user.c:386-393, 548-551`):
  `iolen = min(round_down(match_buf_len), pci_numdevs * confsz)` bounds every
  copyout; `num_patterns * pbufsz != pat_buf_len` exact-size check `:420-425`;
  `offset >= pci_numdevs` rejected `:365`; live-probed with
  `match_buf_len=0xFFFFFFFF` (7 bounded records), page size 2 (offset 2→4),
  bad pattern length (EINVAL), stale generation (LIST_CHANGED). No overflow:
  all size math is 64-bit size_t.
- **PCIOCGETBAR garbage pbi_reg**: contained by `resource_list_find` rid
  match (`:697-704`, live EINVAL for 1 / -1); hardened anyway in fix.diff.
- **devq traversal/lifetime races**: DF-1068 / DF-2326 — not re-reported.
- **PCIOCGETCONF_OLD stack leak**: DF-1067 — not re-reported.
- **No selinfo/kqueue and no PCIOCDETACH/PCIOCMATCH ioctls exist in this
  file** (prompt description stale; `rg selrecord|knote|DETACH` = no hits).
- **Dead code** `pci_user.c:660-664` references undeclared `cmd` under
  `#ifdef COMPAT_FREEBSD4` — unbuildable if ever enabled; compiled out
  everywhere (PRE7_COMPAT comes from `__DragonFly__`, `:171-173`).
- **`brdev` NULL deref** (`:625-626`): unreachable on x86 — every `pci_devq`
  device has a pci-bus parent and pcib grandparent.
- **securelevel at open only** (`:85-88`): standard BSD fd semantics, same
  as FreeBSD; not a defect.

## 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.
