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

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-5276 Β· 23 files
FileTypeDescriptionSize
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.

VERDICT.md
↓ download raw

DF-5276 β€” VERDICT

Status: reproduced (impact: kernel heap memory corruption; confidence: certain)

What was run

  1. Userspace verbatim harness (vpd_harness, guest /tmp/df5276, built from vpd_extracted.h pulled byte-identical from the guest's /usr/src/sys/bus/pci/pci.c, sha256 198a32bea84f635ab6c2b674094f2d19ed7099ee78efe4f1f7d74c5aa8220aeb, identical to the audit host's sys/). 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).

  1. 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 real kmalloc(). 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) at pci.c:1109 (with remain == 0 from 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 (real kmalloc proof). All defect logic between those boundaries is the kernel's own.
  • Exploit chain to uid=0 was 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 calls pci_get_vpd_* at probe today; /dev/pci ioctls are SYSCAP_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

fixed
baseline reproduced→ patch + rebuild →patched clean

fix.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
↓ fix.diffper-fix-DF-5276

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