β¬’ DragonFlyBSD Kernel Audit
← triage Β· dashboard
DF-5776

/dev/pci PCIOCREAD/PCIOCWRITE accept unbounded pi_reg and report success (rc=0, sentinel data) for config offsets no mechanism can decode; PCIOCGETBAR validates pbi_reg not at all

Summary

The lead confirmed: only sign+alignment are checked - any aligned offset to 0x7FFFFFFC is accepted and dispatched, contained only by the MD cfgreg clamps (no OOB), but the ioctl then sets error=0: dropped reads return the 0xffffffff sentinel indistinguishable from a vacant function and dropped writes report success while never landing. GETBAR forwards pbi_reg with zero validation. Live as root: reg=256..0x7FFFFFFC all rc=0 (writes provably dropped); only sign/misalign EINVAL. Impact ceiling is privileged-tooling honesty (pciconf -w cannot detect a dropped write; firmware utilities believe out-of-range writes succeeded) + on ECAM the 256..4095 and misaligned offsets reach the check-less ECAM layer (DF-5570). PCIE_REGMAX bound + GETBAR validation fix validated on a rebuilt kernel.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-5776 Β· 13 files
FileTypeDescriptionSize
pci5776.c β€” 6.6 KB view raw
build.sh β€” 99 B view raw
run.sh β€” 94 B view raw
build.log β€” 95 B view raw
run.log β€” 2.5 KB view raw
run.baseline-freshboot.out β€” 2.5 KB view raw
run.patched.out β€” 2.5 KB view raw
fix_build.log β€” 5.1 MB ↓ download
fix_install.log β€” 80.4 KB view raw
fix.diff β€” 1.4 KB view raw
env.txt β€” 247 B view raw
VERDICT.md β€” 7.4 KB ↓ raw
verdict.json β€” 5.3 KB view raw
VERDICT.md
↓ download raw

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.

Fix verification

fixed
baseline reproduced→ patch + rebuild →patched clean

fixed: vm.sh reset with-src, patch -p0 applied to guest /usr/src (both hunks succeeded), make nativekernel KERNCONF=X86_64_GENERIC + installkernel (fix_build.log build_rc=0), reboot into kernel #1, exact same probe: every previously-silent out-of-range read/write (reg 4096, 65536, 0x7FFFFFFC) now returns rc=-1 errno=22 EINVAL, while reg 0x00 reads 0x10001af4, reg 256..4095 stays accepted for ECAM compatibility, GETCONF pagination and GETBAR outputs are byte-identical, and the guest remains healthy (run.patched.out).

run.patched.out lines 'PCIOCREAD reg=4096 w=4 rc=-1 errno=22', 'PCIOCWRITE reg=2147483644 w=4 val=0xdeadbeef rc=-1 errno=22' vs run.baseline-freshboot.out same lines rc=0 errno=0 on kernel #0; fix_build.log tail '>>> Kernel build for X86_64_GENERIC completed ... install_rc=0'.
↓ fix.diffDragonFly dfbsd 6.5-DEVELOPMENT DragonFly 6.5-DEVELOPMENT #1: Sun Oct 11 19:51:10 UTC 2026 root@dfbsd:/usr/obj/usr/src/sys/X86_64_GENERIC x86_64

Confirmed kernel references

Detail

Evidence (decisive lines)

run.log + run.baseline-freshboot.out: 'reg 0x1000 (PCIE_REGMAX+1) PCIOCREAD reg=4096 w=4 rc=0 errno=0 data=0xffffffff' and 'write reg 0x7FFFFFFC PCIOCWRITE ... rc=0 errno=0' on kernel #0; run.patched.out: same lines with rc=-1 errno=22 on the fix.diff-patched kernel #1; fix_build.log full untrimmed kernel build (build_rc=0); VERDICT.md for the full path:line trace and negative results.

PoC changes

No seed code existed (the lead was a task_log note). Probe authored fresh: fixed struct/API usage against sys/sys/pciio.h, chose virtio NIC 0:3:0 as target (not the root disk), kept every destructive probe out-of-range by construction so the type-1 drop makes them harmless, and added GETCONF pagination + GETBAR negative probes and an unpriv open-gate control.

Verified recommended fix

At the ioctl layer, reject undecodable register offsets instead of silently succeeding: pi_reg > PCIE_REGMAX + 1 - pi_width -> EINVAL (overflow-safe subtraction form), and validate PCIOCGETBAR pbi_reg (>= 0, <= PCIE_REGMAX, 4-aligned); see findings/poc/DF-5776/fix.diff.

Verdict

The pass-2 lead is confirmed live: PCIOCREAD/PCIOCWRITE (sys/bus/pci/pci_user.c:602-614) validate only pi_reg >= 0 and width alignment β€” there is no upper bound, so any aligned offset up to 0x7FFFFFFC is accepted and dispatched to PCIB_READ/WRITE_CONFIG (:633-639, :651-657). The MD layer is the only net: type-1 pci_cfgenable (x86_64/pci_cfgreg.c:187-198) rejects reg > 255 and ECAM rejects reg > 4095, so no OOB access exists β€” but pci_user.c:658 then returns error=0, laundering the dropped access into fabricated success (dropped reads return pi_data=0xffffffff, the cfgreg -1 sentinel, indistinguishable from a vacant-function read; dropped writes report rc=0 and never landed β€” live-proven on the guest as root with reg=256/4096/0x7FFFFFFC, readback and device config unchanged, guest healthy). PCIOCGETBAR additionally passes pbi_reg to pci_read_config(...,4) with zero validation of any kind (:711), contained in practice only by the resource_list_find rid match. The whole surface is root-gated (pci_user.c:82-88 + FWRITE/securelevel gate :324-330; unpriv live-probes EACCES/EPERM), so impact is privileged-tooling honesty/robustness, not unpriv memory safety: severity Low, CWE-20. fix.diff (ioctl-layer bound pi_reg > PCIE_REGMAX + 1 - pi_width via overflow-safe subtraction, plus pbi_reg sign/bound/alignment) was built into kernel #1 and validated: reg > 4095 now returns EINVAL while in-range registers, ECAM extended range, GETCONF pagination and GETBAR behavior are unchanged.