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

pci_read_vpd() cannot terminate on hostile input: dead oversize check plus negative remain drive an endless allocation loop

Summary

The oversize check sets state=-1 then falls through into the switch which overwrites it (remain up to 65535 accepted); a VPD-R/W item whose declared length is 1-2 makes remain-=3 go negative so remain==0 never fires again - the parser cycles keyword/value states as long as the device serves bytes, kmallocing per keyword and krealloc-doubling the array, overwriting the section pointers each header without freeing (leak). Measured 5,010 allocations/20k bytes in 0.11s on guest, still cycling at cap. effective-bound + negative-abort fix (validated).

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-5278 Β· 7 files
FileTypeDescriptionSize
README.md β€” 2.5 KB ↓ raw
VERDICT.md β€” 3.2 KB ↓ raw
run.sh β€” 118 B view raw
../DF-5276/run.t2a.log β€” 229 B view raw
../DF-5276/run.t3.log β€” 565 B view raw
../DF-5276/fix-validation.log β€” 2.4 KB view raw
fix.diff β€” 3.0 KB view raw

DF-5278 β€” VPD parser cannot terminate on hostile input (dead size check + negative remain) β†’ unbounded allocation loop

What

Two independent termination defects in pci_read_vpd() (sys/bus/pci/pci.c:1046-1324):

  1. Dead oversize check (pci.c:1095-1101). When a long-form item's length exceeds the VPD address space, the code sets state = -1 and prints "invalid VPD data" β€” but then falls through into the switch (name) which unconditionally overwrites state (1, 2 or 5). The check has no effect; remain up to 65535 is accepted. Demonstrated by harness mode t2a.

  2. Negative remain (pci.c:1189 / 1260). A VPD-R/VPD-W item whose declared length is 1 or 2 (smaller than its own 3-byte keyword header) drives remain -= 3 below zero. Every exit transition tests remain == 0, which can never be true again, so the state machine cycles between the keyword-header and keyword-value states for as long as the device serves bytes β€” each cycle kmalloc()s a value buffer and (for non-zero dflen) grows the keyword array by doubling (krealloc). A device that always completes VPD transfers (the malicious-device model; also see DF-5276 for the same precondition) keeps the parsing thread in an infinite loop with unbounded kernel memory growth β†’ permanent thread stall + kernel OOM.

Additionally, each new VPD-R/VPD-W/String section header overwrites cfg->vpd.vpd_ros / vpd_w / vpd_ident without freeing the previous allocation (pci.c:1109, 1120, 1128) β€” with the termination bug this is an unbounded leak; even standalone it leaks the prior section's array and values.

Harness mode t3 drives the verbatim parser with an endless keyword stream, capping the simulated device at 300,000 bytes β€” the only thing that stops the parse. Allocation accounting for the capped run is printed.

Reproduce

cd ../DF-5276 && sh build.sh && ./vpd_harness t2a && ./vpd_harness t3

Expected (vulnerable): t2a prints "invalid VPD data" and still produces a 1280-byte identifier (parse continued); t3 shows ~75k allocations consumed for 300k device bytes with the state machine still cycling (device cap, not parser logic, ended the run).

Expected (patched): t2a aborts with vpd_ident NULL; t3 exits at the malformed section header with a handful of allocations.

Fix

fix.diff (shared with DF-5276): make the oversize check effective and abort the item when remain < 0 after the keyword header.

VERDICT.md
↓ download raw

DF-5278 β€” VERDICT

Status: reproduced (impact: denial of service β€” non-terminating parse loop with unbounded kernel memory growth; confidence: certain)

Evidence (userspace verbatim harness, ../DF-5276/run.t2a.log, run.t3.log)

  1. Dead oversize check (harness t2a): a String item with length 0x0500 (> 0x7f*4 - off, the VPD address-space bound):

pci0:2:3:1: invalid VPD data, remain 0x500 t2a: vpd_ident len = 1280 (parse did not abort -- state=-1 was overwritten)

The kernel prints "invalid VPD data" and then keeps parsing: the state = -1 at pci.c:1096 is unconditionally overwritten by the switch (name) at pci.c:1107-1136 (state 1/2/5). remain up to 65535 is accepted.

  1. Negative remain β†’ unterminatable state machine (harness t3): a VPD-R item with declared length 1 (smaller than its own 3-byte keyword header) drives remain -= 3 (pci.c:1189, pci.c:1260) to -2; every exit transition tests remain == 0, which can never be true again. Fed an endless keyword stream (device wraps its VPD data), capped only by the simulator:

t3: parser stopped ONLY because the simulator capped the t3: device at 20000 bytes (vrs.off=20000, config reads=1010001). t3: allocations during the capped run: 5010 kmalloc/krealloc, 272014 bytes, largest 131072 bytes t3: state machine had not terminated: every keyword kept t3: consuming bytes with remain<0; with an uncapped device the t3: loop and the allocations grow without bound.

20,000 device bytes β†’ 5,010 allocations (272 KB, keyword array doubled to 128 KB) in 0.11 s of wall time, and the state machine was still cycling β€” the device cap, not parser logic, ended the run. Scaling: a device completing VPD reads promptly sustains ~2 MB/s of parse throughput here β†’ hundreds of thousands of allocations per second β†’ kernel heap exhaustion plus a permanently stalled (M_WAITOK-blocked) probing thread. Each new keyword kmallocs a value buffer and the keyword array grows by doubling; additionally every new section header overwrites vpd_ros/vpd_w/vpd_ident without freeing the previous allocations (pci.c:1109,1120,1128) β€” an unbounded leak under the non-terminating loop.

Fix validation

With fix.diff, harness t2a aborts at the (now effective) bound check (vpd_ident NULL) and t3 exits at the malformed section header after a handful of allocations β€” see ../DF-5276/fix-validation.log.

Caveats

Same harness model as DF-5276: verbatim kernel code, device simulated at the PCIB_* boundary (the guest has no VPD-capable emulated device). In-kernel non-termination was additionally observed indirectly: the vpdoflow.ko module's ~1 s of DELAY(1)-spin per stalled register (pci.c:929-933) shows the poll loop cost that bounds a slow device; a fast malicious device removes that bound entirely.

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

On the fix.diff-patched kernel tree the harness (re-extracted from patched source) shows t2a aborting at the now-effective bound check (vpd_ident NULL) and t3 terminating after 4 served bytes with 2 allocations (vs 20000 bytes / 5010 allocations on the unpatched parser).

../DF-5276/fix-validation.log (t2a/t3 sections); fix.diff
↓ fix.diffper-fix-DF-5278

Confirmed kernel references

Detail

Exploit chain

malicious PCIe device -> endless VPD keyword stream with an under-length first item -> pci_read_vpd() never terminates, consumes kernel memory without bound (M_WAITOK blocking under pressure) and permanently stalls the probing thread -> system-wide DoS/OOM

Evidence (decisive lines)

../DF-5276/run.t2a.log ('invalid VPD data' printed, vpd_ident len = 1280); ../DF-5276/run.t3.log ('stopped ONLY because the simulator capped the device at 20000 bytes ... 5010 kmalloc/krealloc, 272014 bytes, largest 131072'); post-fix run in fix-validation.log (t3 ends after 4 bytes / 2 allocations)

PoC changes

Shared verbatim harness (see DF-5276); device simulator caps service at 20000 bytes because the guest userspace VA space cannot hold the unbounded leak the loop would otherwise create -- the cap is the demonstration's stop condition, not the parser's.

Verified recommended fix

Make the oversize-remain check effective (break out of case 0) and abort the item when remain goes negative after the keyword header (state 2 and state 5); see fix.diff hunks 1, 3, 4.

Verdict

Both termination defects reproduced with the verbatim parser. (a) Dead oversize check: a String item of length 0x500 makes the kernel print 'invalid VPD data, remain 0x500' and then CONTINUE parsing to a 1280-byte identifier -- the state=-1 set at pci.c:1096 is unconditionally overwritten by the name switch at 1107-1136. (b) Negative remain: a VPD-R item of length 1 (smaller than its 3-byte keyword header) drives remain-=3 to -2 at pci.c:1189; no exit can ever fire again, so with an endlessly-serving device the parse loops forever, kmallocing a value buffer per keyword and doubling the keyword array -- 20000 served bytes produced 5010 allocations (272014 bytes, array grown to 131072 bytes) in 0.11 s with the state machine still cycling; only the device-simulator cap ended the run. Section-header pointer overwrites (pci.c:1109/1120/1128) additionally leak the previous arrays, unbounded under the loop.