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

pnp_create_devices mis-tracks scanning, causing heap OOB reads (and possible bogus device creation) from crafted PnP resource data

Field Value
ID DF-1071
Status new
Severity Medium
CVSS 3.1 CVSS:3.1/AV:P/AC:L/PR:N/UI:N/S:U/C:H/I:L/A:H
CWE CWE-125 Out-of-bounds Read
File sys/bus/isa/pnp.c
Lines 378-396 (large-tag scanning loop)
Area bus/isa (ISA Plug-and-Play protocol driver)
Confidence certain
Discovered 2026-07-14
Reported pending
Known CVE none
CVE match dfly_specific

Summary

In the large-resource branch of pnp_create_devices(), the 2-byte large-tag length field is consumed from the buffer (resp += 2) but is never subtracted from scanning. The sibling parsers in pnpparse.c all do len -= 2. Each large tag therefore inflates scanning by 2, so after all valid resource bytes are consumed the loop keeps iterating and reads *resp++, resp[0], resp[1], and bcopy(resinfo, buf, large_len) past the kmalloc'd resource buffer. A malicious ISA-PnP card (or QEMU-emulated PnP device) controls this stream during boot probing.

Root cause

/* pnp.c:378-396 */
while (scanning > 0) {
    tag = *resp++;              /* line 379-380 */
    scanning--;
    if (PNP_RES_TYPE(tag) != 0) {
        /* Large resource */
        if (scanning < 2) { ... }
        large_len = resp[0] + (resp[1] << 8);
        resp += 2;              /* !!! scanning is NOT decremented by 2 here */
        if (scanning < large_len) { ... }
        resinfo = resp;
        resp += large_len;
        scanning -= large_len;  /* only the payload is subtracted */
        ...
    }
}

Compare with the correct pattern in pnpparse.c:529-536: l = I16(p); p += 2; len -= 2; and pnpparse.c:591-593.

Once scanning has been inflated by 2 * N (N = number of large tags), the loop's guards scanning < 2 (line 383), scanning < large_len (line 390), and scanning < PNP_SRES_LEN(tag) (line 419) all become liars. Specifically:

  • (a) line 379 tag = *resp++ reads heap memory past resources + len;
  • (b) if that heap byte looks like a small tag with PNP_SRES_LEN 0..2 (e.g. PNP_TAG_END = 0x78, or PNP_TAG_LOGICAL_DEVICE = 0x0f which has len 2), the parser then reads resinfo = resp and dereferences it via bcopy(resinfo, &logical_id, 4) (pnp.c:444) or via the PNP_TAG_END path calling pnp_parse_resources(dev, startres, resinfo - startres - 1, ldn) (pnp.c:469-470) with a length derived from an OOB pointer;
  • (c) if the heap byte looks like a large tag (high bit set), large_len is decoded from two further OOB heap bytes and a bcopy(resinfo, buf, min(large_len,99)) (line 401) copies up to 99 bytes of kernel heap into the stack buffer buf, which then becomes the device description via device_set_desc_copy(dev, desc) (line 411) β€” a kernel heap info leak observable through devinfo(8) / dmesg, and possibly creation of bogus child devices with attacker-influenced (heap-garbage) vendor / logical IDs through PNP_TAG_LOGICAL_DEVICE handling at pnp.c:444-460.

Threat model & preconditions

  • Attacker position: Malicious ISA-PnP peripheral (or QEMU / VM ISA-PnP emulation, or any DMA engine that can drive the READ_DATA port during host / guest boot).
  • Privileges gained or impact: Kernel heap memory disclosed to user space via device description strings; or kernel panic / further memory corruption if the OOB-read tag is interpreted as PNP_TAG_LOGICAL_DEVICE / PNP_TAG_END and drives BUS_ADD_CHILD or pnp_parse_resources with garbage state.
  • Required config or capabilities: Default kernel with ISA PnP. The card must answer the serial-isolation protocol with a valid 8-bit LFSR checksum (computable, pnp.c:189-195) so pnp_get_serial returns true (pnp.c:631), then return resource bytes whose TLV stream contains >=1 large tag.
  • Reachability: Unauthenticated, exercised automatically by pnp_identify() at pnp.c:689-722 every boot.

Proof of concept

Conceptual PoC (the bus master must be a hostile PnP device; this is the resource blob it returns). Place in the resource data: one valid large tag whose length is honoured, then pad with small tags until the buffer end is reached. Example minimum blob (hex): 81 09 00 00 00 00 00 00 00 00 00 00 (PNP_TAG_MEMORY_RANGE, large tag bit set, length=9, 9 payload bytes), followed by 20 79 bytes (PNP_TAG_END small tags, length 0) to consume the rest of the buffer. With buffer length L = 12 + 20 = 32 and one large tag, scanning inflates by 2, so after consuming all 32 valid bytes the loop reads *resp++ from offset 32 (one byte past the allocation) for the tag, then either an OOB resp[0] / resp[1] (large) or bcopy (ANSI) or bcopy(resinfo,&logical_id,4) (LOGICAL_DEVICE).

Build & run

Reproduce on DragonFlyBSD: write a minimal QEMU ISA-PnP device model (or patch QEMU's existing isa-pnp backend) that, after winning isolation, returns the byte stream above from READ_DATA; boot a DragonFlyBSD guest with -device isa-pnp (or attach the custom model); observe either (a) leaked heap bytes in devinfo -v device description strings (info leak), or (b) a kernel panic in BUS_ADD_CHILD / pnp_parse_resources from a bogus OOB-derived logical_id / length.

A simpler proof uses an emulated PnP card returning ANSI large tags whose payloads are 0x41 0x41 0x41... and then walking OOB; the leaked heap contents then appear verbatim in devinfo -v for the matching pnp device.

Expected output

# devinfo -v (excerpt) or dmesg
pnp0: <AAAAAAAAAAA... (leaked kernel heap bytes)> at port ...

Or a panic:

Fatal trap 12: page fault while in kernel mode
fault virtual address   = 0x<address from OOB heap>
pnp_create_devices(...) at pnp.c:444    (bcopy of garbage logical_id)
...

Impact

Kernel heap info leak and / or panic during boot from a malicious ISA-PnP device. Physical plug-in precondition; CVSS AV:P. The corruption cascade (when the OOB tag is interpreted as PNP_TAG_LOGICAL_DEVICE and drives pnp_parse_resources with garbage state) could in principle reach pnp_set_config and cause further OOB writes β€” left as the escalation question for the pocrunner.

Subtract the 2 length bytes from scanning, matching pnpparse.c:

--- a/sys/bus/isa/pnp.c
+++ b/sys/bus/isa/pnp.c
@@ -386,6 +386,7 @@ pnp_create_devices(device_t parent, pnp_id *p, int csn,
            }
            large_len = resp[0] + (resp[1] << 8);
            resp += 2;
+           scanning -= 2;

            if (scanning < large_len) {
                scanning = 0;

After this fix the loop's invariants match the buffer exactly and the sibling parsers in pnpparse.c, eliminating the OOB reads.

References

Timeline

  • 2026-07-14 Discovered during automated audit.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-1071 Β· 10 files
FileTypeDescriptionSize
verify.sh trigger-source static source-verification script (4 checks) 1.2 KB view raw
verify.log run-log verify.sh output on audit commit 2.0 KB view raw
VERDICT.md verdict full narrative: mechanism + why-not-reproduced 5.7 KB ↓ raw
fix.diff suggested-fix add missing scanning -= 2 in large-tag branch 254 B view raw
fix_build.log build-log nativekernel + installkernel output, rc=0, full 36k lines 5.7 MB ↓ download
fix_run.log run-log patched kernel #1 boots clean, no panic (runtime fix behavior not testable - no PnP cards) 229 B view raw
env.txt environment uname, cc, sysctls 713 B view raw
README.md readme how to reproduce 1.2 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
↓ download raw

DF-DF-1071 β€” reproduce

This finding was verified by static source tracing (see VERDICT.md). Runtime reproduction on the audit's default QEMU guest is not possible because the precondition is outside the unprivileged-guest-user threat model (see VERDICT.md "Why it cannot be triggered from the audit guest").

How to verify (static source check)

# from the repo root (sys/ must be present)
sh findings/poc/DF-DF-1071/verify.sh

The script walks the cited code path in sys/ with grep/sed and confirms every claim in the finding markdown.

Files

File Purpose
verify.sh static source-verification script
verify.log output of verify.sh on the audit commit (the evidence)
VERDICT.md full narrative: mechanism, why-not-reproduced, fix rationale
fix.diff git-apply-able fix (validated with git apply --check)
env.txt guest environment (uname, cc, sysctls, modules)
manifest.json machine-readable artifact catalog
VERDICT.md verdict full narrative: mechanism + why-not-reproduced
↓ download raw

DF-1071 β€” pnp_create_devices large-tag length bytes never subtracted from scanning (heap OOB)

Verdict

NOT REPRODUCED (runtime) β€” STATIC VERIFICATION CONFIRMED.

The cited code path and bug exist verbatim in sys/bus/isa/pnp.c:386-396. pnp.c is built into the default GENERIC kernel (bus/isa/pnp.c optional isa, sys/conf/files:2124; device isa at sys/config/X86_64_GENERIC:58), and pnp_identify (the entry point that calls pnp_isolation_protocol β†’ pnp_create_devices) is in the running kernel (0xffffffff809d2340 t pnp_identify).

The runtime trigger, however, requires a malicious ISA-PnP card (or a hostile QEMU ISA-PnP device model) that, after winning the serial- isolation protocol, returns crafted resource data containing a large tag. The audit's default QEMU guest has no PnP cards β€” dmesg shows zero pnp device lines and the boot-time pnp_isolation_protocol returns zero devices, so the vulnerable loop in pnp_create_devices is never entered on this guest. There is no sysctl/ioctl path that re-invokes pnp_identify after boot, so the bug cannot be triggered from inside the guest as an unprivileged user.

This is a hardware-gated defect: real in source, present in the default kernel, but requiring attacker-controlled PnP hardware. CVSS AV:P/AC:L (physical plug-in / malicious peripheral) reflects this.

Mechanism (confirmed by source trace)

pnp_create_devices (pnp.c:365-) parses a TLV resource blob returned by a PnP card. The large-resource branch (:382-397):

/* pnp.c:386-396 β€” the bug */
if (scanning < 2) { scanning = 0; continue; }
large_len = resp[0] + (resp[1] << 8);     /* :387  consume 2 length bytes */
resp += 2;                                 /* :388  advance pointer     */
                                           /* !!! scanning NOT decremented by 2 */
if (scanning < large_len) { scanning = 0; continue; }
resinfo = resp;
resp += large_len;                         /* :395  advance by payload  */
scanning -= large_len;                     /* :396  only payload subtracted */

Each large tag therefore inflates scanning by 2 (the unaccounted-for length bytes), so after all valid resource bytes are consumed the loop keeps iterating and reads *resp++ (:380), resp[0]/resp[1] (:387), and bcopy(resinfo, buf, large_len) (:401) past the kmalloc'd resource buffer β€” heap OOB reads.

Compare the correct sibling pattern in pnpparse.c:529-536 and :591-593, which both do l = I16(p); p += 2; len -= 2; β€” they subtract the 2 length bytes. Only pnp_create_devices forgets to.

The OOB-read tag bytes can then drive: - (a) *resp++ reads heap memory past resources + len (:380); - (b) if the OOB byte looks like PNP_TAG_LOGICAL_DEVICE (0x0f, len 2), bcopy(resinfo, &logical_id, 4) (:444) reads further OOB heap into logical_id, which then drives BUS_ADD_CHILD / pnp_set_config; - (c) if it looks like a large tag (high bit set), large_len is decoded from two further OOB bytes and bcopy(resinfo, buf, min(large_len,99)) (:401) copies up to 99 bytes of kernel heap into the stack buffer, which becomes the device description via device_set_desc_copy (:411) β€” observable through devinfo(8) / dmesg.

Why it cannot be triggered from the audit guest

The vulnerable loop runs only when pnp_isolation_protocol (pnp.c:594-) finds at least one PnP card. PnP isolation is a bus- master protocol: the kernel writes a wake-csn to the PnP ADDRESS port, then bit-bangs 9 bytes (vendor id + serial) out of the READ_DATA port for each CSN, computing an 8-bit LFSR checksum (:189-195). A real or emulated PnP card must drive READ_DATA with valid isolation data; only then does the kernel issue PNP_READ_DATA to fetch the resource blob.

The QEMU ISA bridge (piix-isa / ich9-isa) does not emulate a PnP serial-isolation backend β€” there is no PnP card in the audit guest, so pnp_isolation_protocol returns 0 and pnp_create_devices is never called. There is no sysctl/ioctl to re-invoke pnp_identify after boot. A demonstration would require a custom QEMU device model returning crafted resource bytes β€” outside the audit's threat model.

Exploit chain

None developed β€” the primitive (OOB heap read; the finding rates it as info-leak via device description strings) requires attacker-controlled PnP hardware not present in the audit guest. CVSS AV:P (physical).

PoC

verify.sh β€” static-verification script that walks the cited path with grep/sed against sys/, confirming: (1) the buggy resp += 2 without scanning -= 2 at pnp.c:386-396; (2) the correct sibling pattern in pnpparse.c:529-536 and :591-593 (l = I16(p); p += 2; len -= 2;); (3) device isa is in X86_64_GENERIC so pnp.c is built into the default kernel; (4) pnp_identify is in the running kernel but no PnP cards were detected at boot (dmesg | grep -i ^pnp returns nothing). Run from the repo root: sh findings/poc/DF-1071/verify.sh.

Fix

fix.diff β€” adds the missing scanning -= 2; between resp += 2; and the if (scanning < large_len) check at pnp.c:389, exactly matching the sibling pnpparse.c pattern. After this fix the loop's invariants match the buffer exactly. Matches the finding markdown's recommended fix.

Validated end-to-end: the diff applies cleanly with git apply --check, and was built into a single-fix X86_64_GENERIC kernel that boots and runs the audit's full PoC suite without regression. (Runtime PoC for this bug cannot run on the guest because no PnP card is present, so the validation is not_testable per the framework β€” the build/compile/ boot correctness is the demonstrated result.) See fix_build.log.

Reproduce

sh findings/poc/DF-1071/verify.sh     # static source verification

Fix verification

not_testable

compile+boot validated

see evidence pack

Confirmed kernel references

β€”

Detail

Exploit chain

none

Evidence (decisive lines)

β€”

Verdict

Source-confirmed. pnp_create_devices scanning-=2 missing -> heap OOB. In GENERIC, no PnP cards. Fix built+booted.