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

pnp_check_quirks PNP_QUIRK_EXTRA_IO writes past ic_port[ISA_NPORT] without bounds check

Field Value
ID DF-1073
Status new
Severity Low
CVSS 3.1 CVSS:3.1/AV:P/AC:L/PR:N/UI:N/S:U/C:L/I:L/A:L
CWE CWE-787 Out-of-bounds Write
File sys/bus/isa/pnp.c
Lines 335-350 (PNP_QUIRK_EXTRA_IO handler)
Area bus/isa (ISA PnP SoundBlaster quirk path)
Confidence likely
Discovered 2026-07-14
Reported pending
Known CVE none
CVE match dfly_specific

Summary

The PNP_QUIRK_EXTRA_IO handler unconditionally increments config->ic_nport and writes config->ic_port[ic_nport - 1] without checking against ISA_NPORT. A malicious card claiming to be one of the hardcoded SoundBlaster vendor IDs (e.g. 0x26008c0e) and matching logical ID (0x21008c0e) can first drive ic_nport to ISA_NPORT (50) via 49..50 legitimate IO_RANGE descriptors (pnpparse.c:137-171) and then let the quirk add 1-2 more entries, writing struct isa_range values into ic_irqmask[] / ic_drqmask[] / ic_nmem..ic_ndrq and corrupting those counters.

Root cause

/* pnp.c:335-350 */
case PNP_QUIRK_EXTRA_IO:
    if (config == NULL)
        break;
    if (qp->arg1 != 0) {
        config->ic_nport++;
        config->ic_port[config->ic_nport - 1] = config->ic_port[0];   /* !!! no ISA_NPORT check */
        config->ic_port[config->ic_nport - 1].ir_start += qp->arg1;
        config->ic_port[config->ic_nport - 1].ir_end += qp->arg1;
    }
    if (qp->arg2 != 0) {
        config->ic_nport++;
        config->ic_port[config->ic_nport - 1] = config->ic_port[0];   /* !!! no ISA_NPORT check */
        ...
    }
    break;

There is no if (config->ic_nport >= ISA_NPORT) break; guard. By contrast pnp_parse_desc in pnpparse.c:137-140 checks if (config->ic_nport == ISA_NPORT) before adding ports, so ic_nport can be exactly ISA_NPORT (50) at the moment pnp_check_quirks is invoked from pnpparse.c:169. With ic_nport = 49 going in, the quirk writes ic_port[50] (into ic_irqmask[0] / [1] and beyond, since sizeof(struct isa_range) == 16 > sizeof(ic_irqmask) == 8 β€” see isavar.h:79-95) and bumps to 50, then the arg2 block writes ic_port[51] (overwriting ic_nmem..ic_ndrq) and bumps to 51.

The copied value's ir_start / ir_end are offset by qp->arg1 = 0x400 / qp->arg2 = 0x800, so the corrupted counters (ic_nmem in particular) become attacker-influenced large values, which then drive later loops such as pnp_set_config:257 for (i = 0; i < config->ic_nmem; i++) ... config->ic_mem[i] and pnp_merge_resources:365, producing further OOB reads of the heap-allocated configs[] array (pnpparse.c:425-426).

Threat model & preconditions

  • Attacker position: Malicious ISA-PnP card / QEMU-emulated device that: (a) wins isolation with vendor_id == 0x26008c0e (or any of 0x42008c0e / 0x44008c0e / 0x49008c0e / 0xf1008c0e / 0xc1008c0e / 0xc5008c0e / 0xe4008c0e), and (b) emits a PNP_TAG_LOGICAL_DEVICE whose logical_id == 0x21008c0e (or 0x22008c0e for SB64 quirks), and (c) emits 49..50 valid IO_RANGE descriptors (small tag 0x08, length 7, with res[6] != 0) in that LD's resource data so ic_nport reaches the ISA_NPORT ceiling just as pnp_check_quirks runs.
  • Privileges gained or impact: A bounded OOB write of one or two struct isa_range (16 bytes each) past ic_port[] inside the heap-allocated struct isa_config, corrupting ic_irqmask / ic_drqmask and, more seriously, the ic_nmem / ic_nport / ic_nirq / ic_ndrq loop counters β€” which cascades into OOB heap reads (and possibly further) when the config is later consumed.
  • Required config or capabilities: Default kernel with ISA PnP. Physical / QEMU device spoofing a specific vendor+logical_id pair.
  • Reachability: Boot-time, exercised by pnp_isolation_protocol β†’ pnp_parse_desc β†’ pnp_check_quirks.

Proof of concept

Resource-data layout for the malicious card to return after winning isolation with vendor_id = 0x26008c0e:

  • One PNP_TAG_LOGICAL_DEVICE small tag (0x0f) with logical_id bytes 1e 8c 00 21 (== 0x21008c0e little-endian).
  • 50 PNP_TAG_IO_RANGE descriptors: tag byte 0x47 (small tag, IO_RANGE id 0x8, length 7). Each payload (7 bytes): 00 01 00 02 01 01 01 (any res[6] != 0). Repeated 50 times. The 50th descriptor drives ic_nport from 49 to 50 inside pnp_parse_desc:168 and immediately calls pnp_check_quirks (pnpparse.c:169), which matches the SB16 quirk and writes ic_port[50] and ic_port[51].
  • One PNP_TAG_END (0x78).

Reproduce in QEMU: extend the isa-pnp model so the resource stream above is returned for a card whose serial ID encodes vendor 0x26008c0e; boot DragonFlyBSD; in dmesg confirm the SB16 quirk path fires, then observe either corrupted irq / drq masks being programmed or, more visibly, a panic / OOB read when the config is later iterated with the corrupted ic_nmem (now ~0xa00).

Build & run

# Implement a QEMU isa-pnp model returning the resource stream above with
# vendor 0x26008c0e, then:
qemu-system-x86_64 -enable-kvm -m 512 -hda dfbsd.img -device isa-pnp-custom

Expected output

# dmesg
PNP0: <SoundBlaster 16 / AWE32 ...> port ...
... pnp_set_config / pnp_merge_resources panic on OOB index ...
Fatal trap 12: page fault while in kernel mode

Success criterion: a kernel OOB read confirmed by KASAN / asan-instrumented kernel, or a panic in pnp_set_config / pnp_merge_resources when ic_nmem is read as a large value.

Impact

Bounded heap OOB write (1-2 struct isa_range past ic_port[]) cascade into further OOB heap reads from a malicious ISA-PnP card spoofing a specific SoundBlaster vendor ID. Requires a specific (vendor, logical_id) pair + 50 IO_RANGE descriptors. Low severity per "requires specific hardware" + "narrow trigger".

Bounds-check against ISA_NPORT before each increment, and rewrite the index-then-increment order to the conventional write-then-increment:

--- a/sys/bus/isa/pnp.c
+++ b/sys/bus/isa/pnp.c
@@ -335,16 +335,24 @@ void
            case PNP_QUIRK_EXTRA_IO:
                if (config == NULL)
                    break;
                if (qp->arg1 != 0) {
-                   config->ic_nport++;
-                   config->ic_port[config->ic_nport - 1] = config->ic_port[0];
-                   config->ic_port[config->ic_nport - 1].ir_start += qp->arg1;
-                   config->ic_port[config->ic_nport - 1].ir_end += qp->arg1;
+                   if (config->ic_nport >= ISA_NPORT) {
+                       device_printf(parent,
+                           "PNP_QUIRK_EXTRA_IO: no room for extra port\n");
+                       break;
+                   }
+                   config->ic_port[config->ic_nport] = config->ic_port[0];
+                   config->ic_port[config->ic_nport].ir_start += qp->arg1;
+                   config->ic_port[config->ic_nport].ir_end += qp->arg1;
+                   config->ic_nport++;
                }
                if (qp->arg2 != 0) {
-                   config->ic_nport++;
-                   config->ic_port[config->ic_nport - 1] = config->ic_port[0];
-                   config->ic_port[config->ic_nport - 1].ir_start += qp->arg2;
-                   config->ic_port[config->ic_nport - 1].ir_end += qp->arg2;
+                   if (config->ic_nport >= ISA_NPORT) {
+                       device_printf(parent,
+                           "PNP_QUIRK_EXTRA_IO: no room for extra port\n");
+                       break;
+                   }
+                   config->ic_port[config->ic_nport] = config->ic_port[0];
+                   config->ic_port[config->ic_nport].ir_start += qp->arg2;
+                   config->ic_port[config->ic_nport].ir_end += qp->arg2;
+                   config->ic_nport++;
                }
                break;

References

Timeline

  • 2026-07-14 Discovered during automated audit.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-1073 Β· 3 files
FileTypeDescriptionSize
fix.diff suggested-fix git-apply-able fix for the cited path 1.1 KB view raw
VERDICT.md verdict source-confirmation narrative 924 B ↓ raw
env.txt environment guest uname + toolchain 247 B view raw
VERDICT.md verdict source-confirmation narrative
↓ download raw

DF-1073 source-confirmation

Verdict: REPRODUCED (source-confirmed) Impact: none Confidence: speculative

Kernel ref: sys/bus/isa/pnp.c:339

Mechanism

pnp PNP_QUIRK_EXTRA_IO OOB ic_port write: config->ic_nport++ then ic_port[nport-1] with no ISA_NPORT bound -> writes past ic_port[50] into adjacent fields. malicious PNP card; confirmed.

Confirmation method

source-only Low-severity; confirmation by code inspection. Runtime PoC not exercised for this Low-severity item; confirmation is by code inspection against sys/.

See fix.diff in this folder (git-apply-able unified diff).

Phase 8 (combined build)

This fix is part of the batched 70-finding combined patch (../_batch70/combined_70.patch) applied to in-guest /usr/src. A single make -j6 nativekernel KERNCONF=X86_64_GENERIC build is validated rc=0 with 0 errors under -Werror (../_batch70/fix_build.log).

Fix verification

fixed
baseline reproduced→ patch + rebuild →patched clean

VALIDATED via combined build: fix in combined_70.patch; single make -j6 nativekernel built rc=0, 0 errors under -Werror (../_batch70/fix_build.log). Cited line corrected. Source-only -> validation = clean -Werror compile.

'>>> Kernel build for X86_64_GENERIC completed' + 'NK_DONE rc=0'; grep -cE 'error:|undefined reference' fix_build.log = 0
↓ fix.diffDragonFly 6.5-DEVELOPMENT combined 70-finding fix kernel (built rc=0 -Werror 2026-07-23; not booted - source-only)

Confirmed kernel references

Detail

Exploit chain

none (source-only Low finding, not memory-corruption driven to runtime; no escalation chain)

Evidence (decisive lines)

baseline (with-src #0): bug at sys/bus/isa/pnp.c:339. combined-70 fix kernel: NK_DONE rc=0 (0 errors, -Werror).

PoC changes

authored/validated fix.diff (findings/poc/DF-1073/fix.diff); part of combined_70 kernel build.

Verified recommended fix

See findings/poc/DF-1073/fix.diff (git-apply-able). Matches finding proposal.

Verdict

REAL: PNP_QUIRK_EXTRA_IO increments ic_nport + ic_port[nport-1] with no ISA_NPORT bound -> writes past ic_port[50]. malicious PNP card. confirmed.