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

pnp_check_quirks PNP_QUIRK_EXTRA_IO writes ic_port[] past ISA_NPORT without bounds check

Summary

pnp_check_quirks PNP_QUIRK_EXTRA_IO unconditionally increments config->ic_nport and writes config->ic_port[ic_nport-1] without verifying room in ic_port[ISA_NPORT] array. ISA_NPORT=50. Card spoofing SoundBlaster vendor ID with 50 IO_RANGE descriptors drives ic_nport to limit; quirk writes ic_port[50]/[51] aliasing ic_irqmask/ic_drqmask/ic_nmem inside same struct isa_config corrupting IRQ/DMA/memory programming. Parser itself guards before incrementing but pnp_check_quirks called AFTER ic_nport++ so can exceed. OOB stays inside kmallocd isa_config not heap corruption but causes wrong IRQ programming (DoS). [VERIFIED pass2: reproduced with offsetof proof - ic_port 50 51 alias ic_irqmask ic_drqmask ic_nmem exactly as claimed, 17-tag stream corrupts masks that escape into the device config, 200 tags reach ic_port 599 past the 8640-byte kmalloc - linear attacker-paced heap overwrite ceiling quantified; bounds fix-validated, legal extra-port behavior preserved]

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-2315 Β· 13 files
FileTypeDescriptionSize
VERDICT.md verdict full narrative: layout proof, direct + end-to-end results, heap escape, fix validation 5.3 KB ↓ raw
run.log run-log baseline decisive output: before/after config dumps for all scenarios 4.1 KB view raw
fix_run.log run-log fixed-kernel A/B: canaries intact, nport clamped at 50 2.5 KB view raw
pnp_harness.c source kernel harness: verbatim pnp.c TU-compiled + /dev/pnph0 scenarios 23.8 KB view raw
pnphctl.c source userland scenario driver 2.9 KB view raw
pnph.h source shared ioctl definitions 1.9 KB view raw
prep.sh build-script guest prep with verbatim sha256 gate 1.9 KB view raw
build.sh build-script nativekernel KERNCONF=PNPH + installkernel 126 B view raw
run.sh run-script baseline scenario sequence 136 B view raw
build.log build-log baseline harness-kernel build evidence 1.4 KB view raw
fix_build.log build-log fixed-kernel build evidence 1.4 KB view raw
fix.diff suggested-fix validated two-guard bounds fix (git-apply-able, hunks verified) 1.1 KB view raw
env.txt environment guest uname, build, gates 1.2 KB view raw
VERDICT.md verdict full narrative: layout proof, direct + end-to-end results, heap escape, fix validation
↓ download raw

DF-2315 β€” pnp_check_quirks PNP_QUIRK_EXTRA_IO writes ic_port[] past ISA_NPORT without a bounds check

Verdict: REPRODUCED (mechanically, direct and end-to-end through the real parser). Impact: struct isa_config field corruption (IRQ/DMA masks, all four resource counters) + unbounded ic_port[] index writes that escape the configs heap allocation at scale. Confidence: certain.

What was run

Same verbatim-interposition PNPH kernel harness as DF-2314 (pnp.c TU-compiled unmodified, sha256-gated). Two drive modes:

  • direct: the REAL pnp_check_quirks() (pnp.c:320, non-static) on a fabricated struct isa_config with ic_port[0] = {0x220,0x22F,0x10,0x10}, canaries in the masks/counters, and the SoundBlaster quirk identity (vendor 0xc1008c0e SB64 / logical 0x22008c0e β€” quirk table pnp.c:91-92, arg1=0x400, arg2=0x800).
  • end-to-end: the REAL stock pnp_parse_resources() (pnpparse.c) on a crafted card stream of N IO_RANGE tags (tag 0x47, base 0x220) with the SB64 identity set on a real isa0 child β€” the parser calls pnp_check_quirks(..., config) after every IO_RANGE tag (pnpparse.c:169), each tag netting +3 ports (parser +1, quirk +2).

Results (baseline, kernel #5 15:58:28)

Layout proof (runtime offsetof): ic_port[50] sits at +928 = ic_irqmask[0] (its 16 bytes alias ic_irqmask[0..1] + ic_drqmask[0..1]); ic_port[51] at +944 = ic_nmem (aliases ic_nmem/ic_nport/ic_nirq/ic_ndrq). ISA_NPORT = 50 (isavar.h:63); struct is 960 bytes (isavar.h:86-95).

  1. ic_nport=49 (direct): arg1 β†’ port[49] in bounds (nport 49β†’50); arg2 β†’ port[50] written β†’ ic_irqmask[0]=0xa20, ic_irqmask[1]=0xa2f, ic_drqmask[0]=0x10, ic_drqmask[1]=0x10 β€” the canaries {0xcafe0001..} are overwritten with port-derived values (port0+0x800 start/end and its size/align). nport ends at 51.

  2. ic_nport=50 (direct): arg1 β†’ port[50] β†’ masks = {0x620,0x62f,0x10,0x10}; arg2 β†’ port[51] β†’ counters clobbered: ic_nmem=0xa20, ic_nport=0xa2f (2607), ic_nirq=0x10, ic_ndrq=0x10. The config now claims 2607 IO ports in a 50-entry array β€” downstream isa resource allocation / pnp_set_config iterates i < ic_nport over ic_port[i] (pnp.c:274-278) β†’ mass OOB reads and wrong card programming (IRQ/DMA/memory) β€” the wrong-IRQ/DMA/mem DoS the finding claims.

  3. Real path, 17 IO tags: the stored config that escapes into the device (isa_config_entry via the real ISA_ADD_CONFIG, isa_common.c:939) shows nport=51 and irq={0xa20,0xa2f} drq={0x10,0x10} β€” the aliasing corruption demonstrably reaches the device record on the real parser path (the parser's own == ISA_NPORT check at pnpparse.c:137 is equality-only and never fires on this +3/tag cadence).

  4. Real path, 18 IO tags: the parser itself writes port[51] (its equality check misses 51) β†’ ic_nmem=0x220, ic_nirq/ic_ndrq=0x10 in the stored config; subsequent quirk writes re-read the wild counter and land at offsets 960/976 β€” i.e. inside configs[1] of the 9Γ—960-byte alternatives array.

  5. Real path, 200 IO tags: stored config nport=0x258 (600); last quirk write at ic_port[599] = configs+9712, past the end of the 8640-byte kmalloc β€” with card-controlled field bytes (ir_start = 16-bit card-supplied base, ir_size/ir_align = card bytes). Silent on this guest (the 8640-byte request lands in a larger malloc size class that absorbs the slack), but it is a linear, attacker-paced heap overwrite with semi-controlled data once tags exceed ~178. No panic, guest stayed up through all runs.

Reachability / attacker model (honest)

Same as DF-2314: parsed only at boot from ISA-PnP card resource data β€” malicious physical card or VM firmware (AV:P). A genuine SoundBlaster card never reaches 49+ ports legitimately; the overflow needs the attacker-controlled stream. Low severity stands (hardware-attacker DoS / config corruption, with a heap-overflow ceiling at scale).

Fix validation

fix.diff bounds both EXTRA_IO increments:

-                                if (qp->arg1 != 0) {
+                                if (qp->arg1 != 0 && config->ic_nport < ISA_NPORT) {
...
-                                if (qp->arg2 != 0) {
+                                if (qp->arg2 != 0 && config->ic_nport < ISA_NPORT) {

Applied to the guest tree, kernel rebuilt and booted: direct runs leave ALL canaries intact (nport 49β†’50 with arg1 legally applied; nport stays 50 at start 50 β€” both writes blocked); 17/18/200-tag real-path runs all store a clean config (nport=50, masks/counters untouched). No regression to the legitimate quirk behavior. fix_status: fixed.

Reproduce

# on the audit host (guest up):
sh prep.sh && sh build.sh && reboot    # inside guest /root/pnp
sh run.sh                              # baseline
# apply fix.diff to guest /usr/src/sys/bus/isa/pnp.c, sh build.sh, reboot
sh run.sh                              # A/B after fix

Fix status (batch validation)

Applied with the 36 sibling fix.diffs of this batch in findings/poc/_batch41/all_fixes.patch; single in-guest make -j6 nativekernel KERNCONF=X86_64_GENERIC completed RC=0 with zero errors under -Werror (full log: findings/poc/_batch41/fix_build8.log). Behavior A/B intentionally skipped for this Info-tier item.

Fix verification

fixed
baseline reproduced→ patch + rebuild →patched clean

Both fixes (DF-2314 trim guard and DF-2315 quirk bounds, same file) applied to guest /usr/src, kernel rebuilt and booted: baseline symptoms gone, legal quirk behavior intact.

fix_run.log: scenario 4 nport 49 to 50 with all canaries intact (arg2 blocked); scenario 5 nport stays 50; scenarios 6, 7 and 8 all store a clean config with nport=50 and untouched masks and counters
↓ fix.diffDragonFly dfbsd 6.5-DEVELOPMENT DragonFly 6.5-DEVELOPMENT #6: Sun Oct 18 16:13:52 UTC 2026 root@dfbsd:/usr/obj/usr/src/sys/PNPH x86_64

Confirmed kernel references

Detail

Evidence (decisive lines)

run.log: layout line ic_port[50] at +928 (ic_irqmask[0]) ic_port[51] at +944 (ic_nmem); direct after-dumps nport=51 irq={0xa20,0xa2f} drq={0x10,0x10} and nport=2607 cnt={0xa20,0xa2f,0x10,0x10}; parse(17 tags) stored ice_config nport=0x33 irq={0xa20,0xa2f}; parse(200 tags) last quirk write ic_port[599]=configs+9712 escape=YES; fix_run.log: canaries intact and nport=0x32 at every scenario

PoC changes

seed had no runnable harness; reused the verbatim-interposition PNPH kernel harness built for DF-2314 and added the quirk scenarios: direct pnp_check_quirks on a fabricated isa_config with canaries, a crafted IO_RANGE tag stream builder, a real isa0 child carrying the SB64 vendor and logical ids driven through the REAL stock pnp_parse_resources, and a dump of the isa_config_entry stored on the device via DEVTOISA and the id_configs tailq.

Verified recommended fix

bound both EXTRA_IO increments: if (qp->arg1 != 0 && config->ic_nport < ISA_NPORT) and likewise for arg2

Verdict

Reproduced mechanically through verbatim pnp.c compiled into the PNPH harness kernel (guest pnp.c sha256-gated PRISTINE). Runtime offsetof proof: ic_port[50] sits at +928 on ic_irqmask[0] (its 16 bytes alias ic_irqmask[0..1] and ic_drqmask[0..1]) and ic_port[51] at +944 on ic_nmem (aliasing ic_nmem, ic_nport, ic_nirq, ic_ndrq). The REAL pnp_check_quirks with a SoundBlaster quirk identity (vendor 0xc1008c0e, logical 0x22008c0e) on a fabricated isa_config: at ic_nport=49 the arg2 write lands on ic_port[50] and overwrites the IRQ and DMA mask canaries with port-derived values 0xa20, 0xa2f, 0x10, 0x10; at ic_nport=50 both writes go out of bounds and the counters end up 0xa20, 0xa2f (2607), 0x10, 0x10 - a config claiming 2607 IO ports in a 50-entry array that pnp_set_config and isa allocation then iterate (pnp.c:274-278). End-to-end through the REAL stock pnp_parse_resources with a crafted 17-tag SB64 IO stream: the stored config that escapes into the device via the real ISA_ADD_CONFIG carries the corrupted masks; at 18 tags the parser itself writes ic_port[51] (its equality-only ISA_NPORT check never fires on the +3-per-tag cadence) and the counters clobber; at 200 tags the unbounded index reaches ic_port[599] = configs+9712, past the end of the 8640-byte kmalloc, with card-controlled ir_start (16-bit card base) and card-supplied size and align bytes - silent on this guest only because the malloc size class absorbs the slack. Impact ceiling: wrong IRQ, DMA and memory programming of the card at set-config time (local DoS of the device or bus resources) plus a linear attacker-paced heap overwrite given enough tags. Attacker model: malicious physical ISA-PnP card or VM firmware at boot (AV:P); no local or remote unprivileged trigger. Fix validated on a rebuilt kernel: with the ic_nport < ISA_NPORT bounds on both EXTRA_IO increments all canaries stay intact, nport clamps at 50 for 49, 50, 17, 18 and 200 tag runs, and the legal extra-port quirk behavior (arg1 applied while room remains) still works.