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

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-1090 Β· 14 files
FileTypeDescriptionSize
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
README.md readme how to reproduce + bug shape + impact
↓ download 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.sh reports PASS=11 FAIL=0 on the audit tree (the 8 missing guards are pinned, the ANSI control case is pinned, the function signature and call site are pinned).
  • df1090_harness reports OOB DETECTED for all 8 descriptor types when fed a declared TLV length shorter than the offsets the parser accesses unconditionally.
  • df1090_harness_fix reports no OOB for 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.

VERDICT.md verdict full narrative: mechanism, harness, fix
↓ download raw

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

fixed

validated

kernel build rc=0 + harness before/after
↓ fix.diffDragonFly 6.5-DEVELOPMENT #1: Sun Jul 19 19:24:29 UTC 2026

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.