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 of0x42008c0e / 0x44008c0e / 0x49008c0e / 0xf1008c0e / 0xc1008c0e / 0xc5008c0e / 0xe4008c0e), and (b) emits aPNP_TAG_LOGICAL_DEVICEwhoselogical_id == 0x21008c0e(or0x22008c0efor SB64 quirks), and (c) emits 49..50 validIO_RANGEdescriptors (small tag0x08, length 7, withres[6] != 0) in that LD's resource data soic_nportreaches theISA_NPORTceiling just aspnp_check_quirksruns. - Privileges gained or impact: A bounded OOB write of one or two
struct isa_range(16 bytes each) pastic_port[]inside the heap-allocatedstruct isa_config, corruptingic_irqmask/ic_drqmaskand, more seriously, theic_nmem / ic_nport / ic_nirq / ic_ndrqloop 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_DEVICEsmall tag (0x0f) withlogical_idbytes1e 8c 00 21(==0x21008c0elittle-endian). - 50
PNP_TAG_IO_RANGEdescriptors: tag byte0x47(small tag, IO_RANGE id 0x8, length 7). Each payload (7 bytes):00 01 00 02 01 01 01(anyres[6] != 0). Repeated 50 times. The 50th descriptor drivesic_nportfrom 49 to 50 insidepnp_parse_desc:168and immediately callspnp_check_quirks(pnpparse.c:169), which matches the SB16 quirk and writesic_port[50]andic_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".
Recommended fix
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
sys/bus/isa/pnp.c:335-350βPNP_QUIRK_EXTRA_IOhandler (the bug)sys/bus/isa/pnpparse.c:137-140β correct bounds-check pattern (ISA_NPORTceiling)sys/bus/isa/pnpparse.c:169β callsite that can driveic_nport == ISA_NPORTsys/bus/isa/isavar.h:79-95βstruct isa_configlayout (ic_port then ic_irqmask etc.)sys/bus/isa/pnp.c:257, pnp.c:365β later loops driven by corruptedic_nmem/ic_nport- CWE-787 Out-of-bounds Write
Timeline
- 2026-07-14 Discovered during automated audit.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-1073 Β· 3 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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 |
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/.
Recommended fix
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
fixedVALIDATED 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
Confirmed kernel references
- s
- y
- s
- /
- b
- u
- s
- /
- i
- s
- a
- /
- p
- n
- p
- .
- c
- :
- 3
- 3
- 9
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.
No comments yet.