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)
PoC verification
Evidence pack
findings/poc/DF-5278 Β· 7 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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):
-
Dead oversize check (pci.c:1095-1101). When a long-form item's length exceeds the VPD address space, the code sets
state = -1and prints "invalid VPD data" β but then falls through into theswitch (name)which unconditionally overwritesstate(1, 2 or 5). The check has no effect;remainup to 65535 is accepted. Demonstrated by harness modet2a. -
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) drivesremain -= 3below zero. Every exit transition testsremain == 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 cyclekmalloc()s a value buffer and (for non-zerodflen) 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.
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)
- 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.
- Negative
remainβ unterminatable state machine (harnesst3): a VPD-R item with declared length 1 (smaller than its own 3-byte keyword header) drivesremain -= 3(pci.c:1189,pci.c:1260) to -2; every exit transition testsremain == 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
fixedOn 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
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.
No comments yet.