# 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.
