pci_read_vpd(): zero-length VPD identifier string causes unbounded sequential kernel heap overflow (device-controlled content and length)
Summary
VPD item 0x82 with length 0 makes the parser kmalloc(1) and enter state 1 whose only exit tests remain==0 after decrement - unreachable once remain went negative; every subsequent VPD byte copies 1:1 into vpd_ident[i++] unbounded (stalled-but-honest device still overflows at bytes/sec through the DELAY(1) poll). Malicious PCIe device (Thunderbolt hotplug/VM/corrupt EEPROM) via pci_get_vpd_ident/read-only at probe; IOMMUs do not help (host-initiated config reads). REPRODUCED: verbatim-parser userspace harness SIGSEGV exactly 4096B past the 1-byte alloc (2x); KLD with REAL kernel kmalloc dmesg-verified device-controlled bytes 1024 deep into an 8-byte chunk. Fix validated on rebuilt guest kernel (item rejected, no overflow). reject-zero-length fix.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-5276 Β· 23 files| File | Type | Description | Size | |
|---|---|---|---|---|
| README.md | β | 2.8 KB | β raw | |
| VERDICT.md | β | 5.0 KB | β raw | |
| vpd_shim.h | β | 3.5 KB | view raw | |
| vpd_harness.c | β | 14.5 KB | view raw | |
| extract.sh | β | 2.6 KB | view raw | |
| build.sh | β | 197 B | view raw | |
| run.sh | β | 229 B | view raw | |
| build.log | β | 346 B | view raw | |
| run.t1.log | β | 423 B | view raw | |
| run.t1.2.log | β | 423 B | view raw | |
| run.t2.log | β | 934 B | view raw | |
| run.t2a.log | β | 229 B | view raw | |
| run.t3.log | β | 565 B | view raw | |
| run.t4.log | β | 71 B | view raw | |
| vpd_extracted.h | β | 9.0 KB | view raw | |
| vpd_structs.h | β | 411 B | view raw | |
| dmesg-kmod.txt | β | 848 B | view raw | |
| env.txt | β | 265 B | view raw | |
| kmod/vpdoflow.c | β | 5.2 KB | view raw | |
| kmod/Makefile | β | 103 B | β download | |
| kmod/vpd_extracted.h | β | 8.4 KB | view raw | |
| fix.diff | β | 3.0 KB | view raw | |
| fix-validation.log | β | 2.4 KB | view raw |
DF-5276 β Heap buffer overflow in pci_read_vpd() via zero-length VPD identifier string
What
sys/bus/pci/pci.c, the PCI VPD (Vital Product Data) parser
(pci_read_vpd(), lines 1046-1324). A malicious PCI device (Thunderbolt
peripheral, emulated device, or a device with corrupt VPD EEPROM) that
answers VPD reads with:
82 00 00 long-form item, name 0x2 ("Identifier String"), length 0x0000
<payload bytes> arbitrary attacker-controlled content
drives the kernel into:
case 0x2: /* String */
cfg->vpd.vpd_ident = kmalloc(remain + 1, M_DEVBUF, M_WAITOK); /* == kmalloc(1) */
i = 0;
state = 1;
...
case 1: /* Identifier String */
cfg->vpd.vpd_ident[i++] = byte; /* unbounded */
remain--;
if (remain == 0) { ... } /* never true: remain started at 0 */
With remain == 0 the termination condition can never fire (remain goes
-1, -2, ...), so the parser writes every subsequent VPD byte
sequentially past a 1-byte kmalloc() β a linear kernel-heap overflow
whose content and length are fully device-controlled. It stops only when
the device stops completing VPD transfers (each stalled register costs the
kernel a 1,000,000-iteration DELAY(1) busy-wait, ~1s per 4 bytes β a
glitched-but-honest device still overflows the heap, slowly).
Attack surface: pci_read_vpd() runs lazily via pci_get_vpd_ident() /
pci_get_vpd_readonly() (PCI_GET_VPD_* methods) when a driver or KLD
probes a device's VPD. IOMMUs do not help: config-space-driven parsing is
initiated by the host CPU, not by device DMA.
Reproduce (userspace, verbatim parser)
On the DragonFly guest (or any host with the same sys/bus/pci/pci.c):
cd findings/poc/DF-5276 sh build.sh # extracts the parser VERBATIM from /usr/src and compiles sh run.sh # t4 t2 t2a t3 t1 (+t1 repeat)
Expected (vulnerable kernel source):
run.t1.logβ*** SIGSEGV ... 4096 bytes were written past a 1-byte kmalloc allocation(exit 77). Deterministic across runs.run.t4.logβ well-formed VPD parses fine (harness fidelity check).
Reproduce (kernel context, real kmalloc)
cd kmod && make kldload ./vpdoflow.ko dmesg | tail
Expected (vulnerable kernel): DF5276: KERNEL HEAP OVERFLOW REPRODUCED:
1023 bytes written past a 1-byte M_DEVBUF allocation (payload extent
verified in kernel memory), possible collateral panic from the smashed
neighbourhood β either outcome proves the primitive in-kernel.
Expected (kernel source patched with ../fix.diff):
DF5276: parser rejected the item (fixed kernel) -- no overflow.
Fix
fix.diff (also fixes DF-5277 / DF-5278 / DF-5279, same function family).
Core hunk: reject remain == 0 identifier items and make the oversize
check effective.
DF-5276 β VERDICT
Status: reproduced (impact: kernel heap memory corruption; confidence: certain)
What was run
- Userspace verbatim harness (
vpd_harness, guest/tmp/df5276, built fromvpd_extracted.hpulled byte-identical from the guest's/usr/src/sys/bus/pci/pci.c, sha256198a32bea84f635ab6c2b674094f2d19ed7099ee78efe4f1f7d74c5aa8220aeb, identical to the audit host'ssys/). The only shims are at the hardware/API boundary: a guard-page allocator and a simulated malicious PCI device that completes VPD transfers instantly and serves attacker bytes.run.t1.log/run.t1.2.log(two runs, deterministic):
*** SIGSEGV writing 0x800476000 during phase 't1: zero-length VPD identifier string (DF-5276)' ***
faulting allocation: base=0x800475000 kmalloc size=1 (guard page at +4096)
=> 4096 bytes were written past a 1-byte kmalloc allocation
The parser was fed 82 00 00 (String item, length 0) + 8192 payload
bytes; it kmalloc(1)s the identifier and writes every payload byte
sequentially. Exit 77 both runs; the write would have continued to 8192
bytes and beyond (the device controls the length).
- Kernel-context proof with the REAL kernel allocator (
kmod/vpdoflow.ko,dmesg-kmod.txt): the same verbatim parser compiled into a KLD, using the kernel's realkmalloc(). Result (live dmesg):
DF5276: parser kmalloc(1) -> 0xfffff8004f0c28d8 (real slab chunk: 8 bytes)
pci0:2:3:1: failed to read VPD data.
DF5276: payload pattern found at offsets 8..1023 of the freed chunk (1016/1016 bytes match); chunk size 8
DF5276: KERNEL HEAP OVERFLOW REPRODUCED: device-controlled bytes verified 1024 bytes deep into a 8-byte kernel heap chunk (allocation size 1)
1024 device-controlled bytes landed contiguously in kernel memory
starting at a 1-byte kmalloc (8-byte slab chunk), i.e. ~128 adjacent
M_DEVBUF chunks were smashed. The chunk pointer is recorded by a thin
wrapper over the real allocator because the parser's own error cleanup
frees+NULLs vpd_ident before returning (itself DF-5277's precondition).
The guest survived this run (corruption was latent), which is why the
pattern could be verified post-hoc.
Why this is a real primitive
- Content AND length are fully attacker (device) controlled: each VPD byte
read is copied 1:1 into
vpd_ident[i++](sys/bus/pci/pci.c:1139-1145). - No bound exists anywhere between the
kmalloc(remain + 1)atpci.c:1109(withremain == 0from the two device-supplied length bytes) and the writes; the only exit (remain == 0) can never fire once remain has gone negative. - An honest-but-glitched device also triggers it: a stalled VPD read still
returns data slowly (each 4 bytes costs one 1e6-iteration
DELAY(1)poll,pci.c:918-933), so the overflow proceeds at ~bytes/second even without a "fast" malicious device. - IOMMUs do not protect: config-space parsing is host-CPU initiated.
Threat model (per this audit's file brief): malicious/compromised PCIe device β Thunderbolt hotplug peripheral, VM-emulated device, or a device whose VPD EEPROM is corrupt. On the no-SMAP/SMEP/KASLR reference guest the primitive is a textbook slab-groom -> overwrite adjacent object -> control flow; the device attacker here already equals kernel compromise, but the same code path is what a local attacker with a rented/pass-through PCI function would target.
Fix validation
fix.diff (8 hunks; shared with DF-5277/5278/5279) applied to the guest's
/usr/src; make nativekernel + install; rerun of the exact harness
(re-extracted from the patched tree) and of the KLD β see
fix-validation.log in this pack: t1 no longer faults (item rejected,
vpd_ident NULL), t2/t2a/t3 fixed behaviors, vpdoflow.ko prints
"parser never allocated vpd_ident (item rejected -- fixed kernel)".
Caveats (honesty)
- The QEMU guest has no emulated device with a controllable VPD
capability, so the live config-space entry path could not be driven
end-to-end; instead the exact kernel code (byte-identical extraction,
sha-verified) was executed against a device model at the
PCIB_*boundary β in userspace (guard-page proof) and in-kernel (realkmallocproof). All defect logic between those boundaries is the kernel's own. - Exploit chain to
uid=0was not developed: the attacker in this threat model is the device itself (kernel-memory write already equals ring-0 compromise); no unprivileged-local-user trigger exists for this path in the base system (no in-tree driver callspci_get_vpd_*at probe today;/dev/pciioctls areSYSCAP_RESTRICTEDROOT-gated and have no VPD ioctl).
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
fixedfix.diff applied to guest /usr/src (8 hunks, patch(1) clean), kernel rebuilt (make nativekernel, DragonFly 6.5-DEVELOPMENT #1 Fri Oct 2 00:11:04 UTC 2026) and booted; the exact PoC rerun shows the overflow GONE: harness t1 returns 0 with 'parser rejected the zero-length item -- fixed kernel' and vpdoflow.ko prints 'parser never allocated vpd_ident (item rejected -- fixed kernel)'. Baseline (unpatched) behavior: SIGSEGV + 1024-byte kernel heap smash.
fix-validation.log (harness modes t4/t2/t2a/t3/t1 + kernel dmesg of both KLDs on the patched kernel); fix.diff
Confirmed kernel references
Detail
Exploit chain
malicious/compromised PCIe device (Thunderbolt hotplug, VM-emulated, or corrupt VPD EEPROM) -> driver/KLD probes VPD via pci_get_vpd_ident() -> pci_read_vpd() state 1 writes attacker bytes linearly past a 1-byte kmalloc -> slab groom -> overwrite adjacent M_DEVBUF object -> kernel control flow. uid=0 chain not developed: the attacker in this threat model is the device (kernel write already equals ring-0); no unprivileged-local-user trigger exists in the base system (/dev/pci is SYSCAP_RESTRICTEDROOT-gated, no VPD ioctl, no in-tree probe-time VPD caller).
Evidence (decisive lines)
run.t1.log lines '*** SIGSEGV ... 4096 bytes were written past a 1-byte kmalloc allocation' (x2 runs); dmesg-kmod.txt 'DF5276: KERNEL HEAP OVERFLOW REPRODUCED: device-controlled bytes verified 1024 bytes deep into a 8-byte kernel heap chunk (allocation size 1)'; extraction fidelity: guest /usr/src/sys/bus/pci/pci.c sha256 198a32be... identical to audit tree, harness compiles the byte-identical functions (vpd_extracted.h).
PoC changes
Harness built from verbatim-extracted kernel functions (content-anchored extract.sh; shims only at the PCIB_* config-space boundary: guard-page allocator + simulated instantly-completing malicious VPD device). KLD uses the real kmalloc with a thin recording wrapper because the parser's error cleanup frees vpd_ident before returning. Bootverbose/KASSERT/DELAY mapped per kernel semantics.
Verified recommended fix
Reject zero-length identifier items (remain == 0) in pci_read_vpd() case 0x2 before kmalloc; see fix.diff hunk 2.
Verdict
Heap buffer overflow in pci_read_vpd() reproduced twice at the algorithm level (userspace verbatim-parser harness, deterministic SIGSEGV exactly +4096 bytes past a 1-byte kmalloc, run.t1.log/run.t1.2.log) and once in kernel context with the real kernel slab allocator (vpdoflow.ko: 1024 contiguous device-controlled bytes verified 1024 bytes deep into an 8-byte M_DEVBUF chunk, dmesg-kmod.txt). Root cause: a zero-length VPD identifier-string item makes the parser kmalloc(remain+1)==kmalloc(1) and then copy every subsequent device-supplied VPD byte sequentially with no bound; the only exit tests remain==0 which can never fire once remain has gone negative. Content and length are fully device-controlled; IOMMUs do not protect (host-CPU-initiated config-space parsing).
No comments yet.