/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)
PoC verification
Evidence pack
findings/poc/DF-5776 Β· 13 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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 |
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
sys/bus/pci/pci_user.c:602-614βPCIOCREAD/PCIOCWRITEvalidate onlyio->pi_reg < 0andio->pi_reg & (io->pi_width - 1). Any aligned non-negativeint(0 .. 0x7FFFFFFC) is accepted. There is no upper bound.- Dispatch:
PCIB_WRITE_CONFIG(brdev, bus, dev, func, io->pi_reg, io->pi_data, io->pi_width)atpci_user.c:633-639and 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). - The only net is the MD layer:
- type-1:
pci_cfgenable(pci_cfgreg.c:187-198) rejects(unsigned)reg > PCI_REGMAX(255) by returningdataport = 0;pcireg_cfgreadthen returns its-1sentinel (:214) andpcireg_cfgwritedoes nothing (:243-244). Theoutl(0xcf8)at:194is only ever reached withreg <= 255masked& ~0x03β there is no path from pi_reg to an unbounded raw port access. - ECAM:pciereg_cfgread/cfgwrite(:322-324,:348-350) rejectreg > PCIE_REGMAX(4095) and maskreg & 0xfffinPCIE_VADDR(:308-313), so 256..4095 is the legitimate extended-config range. - But
pci_user.c:658setserror = 0unconditionally: an access that the MD layer dropped is reported to userland as success, and a dropped read returnspi_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=0for all three; readback 0xffffffff; device vendor register unchanged; guest healthy (writes dropped β harmless by construction on type-1)reg=-4andreg=2 w=4βrc=-1 errno=22β these two checks are the only validation at the ioctl layer- unpriv control (
/tmp/opengateas 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/2147483644reads and writes βrc=-1 errno=22 (EINVAL)reg=0β still0x10001af4; 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-697marshals exactlyIOCPARM_LEN(cmd)bytes into kernel memory (bzero for IOC_OUT); every_IOWRsize (pciio.h:125-129,pci_user.c:227-229) covers all fields the driver touches (pci_io_old16 B,pi_dataat offset 12;pci_conf_io48 B used by both GETCONF variants). No mismatch. a_datais kernel memory β no user-pointer deref in this driver; thePCIOCREAD_OLDrepoint ofap->a_datato stackiodata(pci_user.c:591-599) writes results back viaio_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_lenexact-size check:420-425;offset >= pci_numdevsrejected:365; live-probed withmatch_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_findrid 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-664references undeclaredcmdunder#ifdef COMPAT_FREEBSD4β unbuildable if ever enabled; compiled out everywhere (PRE7_COMPAT comes from__DragonFly__,:171-173). brdevNULL deref (:625-626): unreachable on x86 β everypci_devqdevice 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
fixedfixed: 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'.
Confirmed kernel references
- sys/bus/pci/pci_user.c:602
- sys/bus/pci/pci_user.c:610
- sys/bus/pci/pci_user.c:633
- sys/bus/pci/pci_user.c:651
- sys/bus/pci/pci_user.c:658
- sys/bus/pci/pci_user.c:711
- sys/bus/pci/x86_64/pci_bus.c:71
- sys/bus/pci/x86_64/pci_cfgreg.c:187
- sys/bus/pci/x86_64/pci_cfgreg.c:194
- sys/bus/pci/x86_64/pci_cfgreg.c:212
- sys/bus/pci/x86_64/pci_cfgreg.c:237
- sys/bus/pci/x86_64/pci_cfgreg.c:322
- sys/bus/pci/x86_64/pci_cfgreg.c:348
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.
No comments yet.