pnp_parse_desc accesses fixed descriptor offsets without validating payload length, causing heap OOB read and OOB byte-write
Summary
pnp_parse_desc receives payload pointer res and declared length len/l from TLV tag, then accesses fixed offsets (res[0] through res[16] depending on descriptor type) without checking that len is sufficient for the descriptor type. Every descriptor type except PNP_TAG_ID_ANSI (which correctly clamps len to sizeof(buf)-1 before bcopy at line 224) is affected: COMPAT_DEVICE bcopy(res,&compat_id,4) needs len>=4 (:95); IRQ_FORMAT I16(res) needs len>=2 (:105); DMA_FORMAT res[0] needs len>=1 (:123); IO_RANGE res[6] read + I16(res+1)/I16(res+3) + res[5]=1 WRITE at :165 when res[5]==0 needs len>=7 (:141-167); IO_FIXED res[2]/I16(res) needs len>=3 (:178); MEMORY_RANGE I16(res+7)/I16(res+5) needs len>=9 (:242); MEMORY32_RANGE I32(res+13)/I32(res+9)/I32(res+5) needs len>=17 (:278); MEMORY32_FIXED I32(res+5)/I32(res+1) needs len>=9 (:308). Resource buffer allocated by pnp_read_bytes (pnp.c:503) as kmalloc(space,M_TEMP,M_WAITOK) WITHOUT M_ZERO growing in 1024-byte increments. A malicious ISA-PnP card supplies crafted TLV with truncated length field. Heap OOB read up to 17 bytes populates isa_config fields printed via pnp_printf when bootverbose; heap OOB WRITE of byte value 1 via IO_RANGE res[5]=1 at :165 when len<6 escapes kmalloc allocation when total_len within 17 bytes of 1024-byte boundary.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-1090 Β· 14 files| File | Type | Description | Size | |
|---|---|---|---|---|
| df1090_harness.c | trigger-source | userspace harness mirroring all 8 affected descriptor types | 5.5 KB | view raw |
| verify.sh | trigger-source | 11 static source-tree grep checks | 3.0 KB | view raw |
| verify.log | run-log | verify.sh output (all 11 pass) | 669 B | view raw |
| run.log | run-log | harness output: 8/8 OOB unpatched, 0/8 patched | 1.9 KB | view raw |
| fix_run.log | run-log | patched-kernel #1 boot + harness rerun (no regression) | 418 B | view raw |
| fix.diff | suggested-fix | 8 per-descriptor-type length guards in pnp_parse_desc | 1.9 KB | view raw |
| fix_build.log | build-log | nativekernel + installkernel output, rc=0 | 5.6 MB | β download |
| build.sh | build-script | build both harness variants | 320 B | view raw |
| run.sh | run-script | verify.sh + both harness variants | 289 B | view raw |
| env.txt | environment | uname, cc, securelevel, HW presence | 526 B | view raw |
| README.md | readme | how to reproduce + bug shape + impact | 3.0 KB | β raw |
| VERDICT.md | verdict | full narrative: mechanism, harness, fix | 3.6 KB | β raw |
| ../fix_build_combined.log | build-log | Combined 41-finding kernel build (rc=0, -Werror clean) | 5.6 MB | β download |
| ../fix_build_summary.txt | build-summary | Summary of the combined 41-finding kernel build | 826 B | view raw |
DF-1090 β pnp_parse_desc accesses fixed descriptor offsets without length validation
Build
cc -O0 -o df1090_harness df1090_harness.c cc -O0 -DFIX -o df1090_harness_fix df1090_harness.c
Run
./df1090_harness # unpatched algorithm β 8/8 OOB ./df1090_harness_fix # patched algorithm β 0/8 OOB sh verify.sh # 11 static source-tree checks
Expected (bug present)
verify.shreportsPASS=11 FAIL=0on the audit tree (the 8 missing guards are pinned, the ANSI control case is pinned, the function signature and call site are pinned).df1090_harnessreportsOOB DETECTEDfor all 8 descriptor types when fed a declared TLV length shorter than the offsets the parser accesses unconditionally.df1090_harness_fixreportsno OOBfor all 8 β each type now bails out cleanly on a short declared length.
Bug shape
pnp_parse_desc at sys/bus/isa/pnpparse.c:60-340 receives the resource
pointer res and the attacker-declared TLV length len (the length
field inside the TLV header, supplied by the PnP card itself). For every
descriptor type except PNP_TAG_ID_ANSI (which correctly clamps len to
sizeof(buf)-1 at line 224 before bcopy), the parser accesses fixed
offsets of res without first checking that len is large enough:
| descriptor | access | needs | cite |
|---|---|---|---|
PNP_TAG_COMPAT_DEVICE |
bcopy(res,&compat_id,4) |
β₯4 | :95 |
PNP_TAG_IRQ_FORMAT |
I16(res) |
β₯2 | :105 |
PNP_TAG_DMA_FORMAT |
res[0] |
β₯1 | :123 |
PNP_TAG_IO_RANGE |
res[6],I16(res+1),I16(res+3),res[5] (write!) |
β₯7 | :141-167 |
PNP_TAG_IO_FIXED |
res[2],I16(res) |
β₯3 | :178 |
PNP_TAG_MEMORY_RANGE |
I16(res+7),I16(res+5) |
β₯9 | :242 |
PNP_TAG_MEMORY32_RANGE |
I32(res+13),I32(res+9),I32(res+5) |
β₯17 | :278 |
PNP_TAG_MEMORY32_FIXED |
I32(res+5),I32(res+1) |
β₯9 | :308 |
The IO_RANGE case is the most interesting because line 165 performs an
OOB write (res[5] = 1 as part of the align-normalization) when the
declared length is < 6, which can escape the 1024-byte kmalloc bucket
the resource buffer was allocated from by pnp_read_bytes.
Impact / preconditions
A malicious ISA-PnP card (or a hostile QEMU PnP device model) supplies
crafted TLV data with a truncated length field. The PnP bus is probed at
boot (pnp_identify β pnp_isolation_protocol β pnp_create_devices β
pnp_parse_resources β pnp_parse_desc) and on kldload pnp. The audit
guest has zero PnP cards, so the path is not exercised at runtime here;
the bug is confirmed by source trace + userspace harness mirroring the
algorithm.
DF-1090 β pnp_parse_desc fixed-offset accesses without length validation
Verdict
NOT REPRODUCED at runtime (hardware-gated) β STATIC VERIFICATION + HARNESS CONFIRMED.
The bug exists verbatim in sys/bus/isa/pnpparse.c:90-328. Every
descriptor-type branch except PNP_TAG_ID_ANSI accesses fixed offsets of
the resource buffer (res[N], I16(res+N), I32(res+N)) without first
checking that the attacker-declared TLV length len is large enough β
even though the parser is called with l (the length field from the TLV
header, supplied by the PnP card) as the len argument
(pnpparse.c:518,538).
The audit QEMU guest has no ISA-PnP cards (dmesg | grep -c pnp = 0),
so pnp_identify finds nothing and pnp_parse_desc is never invoked at
runtime. The trigger requires attacker-controlled PnP hardware (or a
hostile QEMU PnP device model) β AV:P/AC:L. This is the same hardware
constraint as the sibling finding DF-1071.
The df1090_harness userspace C program mirrors the parser algorithm
byte-for-byte for all 8 affected descriptor types. Run with a declared
length shorter than the offsets each type accesses, the unpatched
algorithm reports OOB DETECTED for 8/8 cases; the patched
algorithm (per-descriptor length guards) reports no OOB for 8/8.
Mechanism (confirmed by source trace)
pnp_parse_resources (pnpparse.c:408-545) iterates the TLV resource
blob and for each tag extracts the declared length l:
/* small resource: */
l = PNP_SRES_LEN(tag); /* :438 - 4 bits from the tag byte */
...
pnp_parse_desc(dev, tag, p, l, config, ldn); /* :518 */
/* large resource: */
l = I16(p); /* :529 - 9 bits from card */
p += 2; len -= 2;
...
pnp_parse_desc(dev, tag, p, l, config, ldn); /* :538 */
In both cases l is the length declared by the card, not validated
against the descriptor type's required minimum. pnp_parse_desc then
dispatches on the tag and accesses offsets of res:
case PNP_TAG_IO_RANGE:
...
if (res[6] == 0) { ... } /* :141 β needs len >= 7 */
...
I16(res + 1) /* :154 */
I16(res + 3) + res[6] - 1 /* :155 */
...
if (res[5] == 0) { res[5] = 1; } /* :163-165 β OOB WRITE */
A malicious card sets the PNP_TAG_IO_RANGE length field to 3, sends 3
payload bytes, and the parser reads res[6] (3 bytes past end),
I16(res+1) (1 byte past end), I16(res+3) (3 bytes past end), and
writes res[5] = 1 (2 bytes past end). On the kmalloc(1024) buffer
the resource data lives in, an attacker within 17 bytes of the 1024-byte
boundary writes outside the allocation entirely.
The PNP_TAG_ID_ANSI case at :223-235 is the control β it correctly
clamps len before its bcopy. Every other type lacks that clamp.
Reproduction
$ sh verify.sh # 11/11 static checks pin the missing guards $ cc -O0 -o df1090_harness df1090_harness.c $ cc -O0 -DFIX -o df1090_harness_fix df1090_harness.c $ ./df1090_harness # 8/8 "OOB DETECTED" $ ./df1090_harness_fix # 8/8 "no OOB"
Fix
fix.diff adds a single if (len < N) break; guard at the head of each
of the 8 affected descriptor-type branches, using the minimum length that
type's accesses require (4, 2, 1, 7, 3, 9, 17, 9 respectively). The
guards mirror the existing PNP_TAG_ID_ANSI clamp in spirit: if the
card-declared length is too short for the descriptor type, skip it rather
than read past the buffer. The kernel nativekernel build of the patched
file succeeds (fix_build.log, NK_DONE rc=0); the harness proves the
fix is correct at the algorithm level.
Fix verification
fixedvalidated
kernel build rc=0 + harness before/after
Confirmed kernel references
β
Detail
Exploit chain
none
Evidence (decisive lines)
β
Verdict
Source+harness. pnp_parse_desc fixed offsets no length validation -> heap OOB. No PnP cards. Kernel rebuild fix.
No comments yet.