# DF-5392 — VERDICT

**Finding:** ISA PnP `isa_config` count fields are trusted end-to-end;
`pnp_check_quirks()` (pnp.c:338-349) increments `ic_nport` unbounded after
pnpparse.c's `== ISA_NPORT` clamp has run, defeating it, writing
out-of-array `isa_range` slots (including the count fields themselves via
the `ic_port[51]` alias) and persisting poisoned configs through
`isa_common.c:isa_add_config()` (isa_common.c:938-959) with zero validation.
Consumers iterate the counts (`isa_find_memory` isa_common.c:128,
`isa_find_port` :192, `isa_find_irq` :279, `isa_find_drq` :338,
`pnp_set_config` pnp.c:257-307).

## Reproduced? YES

Guest: DragonFly 6.5-DEVELOPMENT #0 (X86_64_GENERIC), QEMU/KVM, no SMAP/SMEP/KASLR.

### run-A (safe proof) — `run_a.log`

KLD harness `pnphack.ko` (driver on isa bus; on kldload the real
`isa_driver_added()` runs our identify against the real
`pnp_parse_resources()` + the real `isa_add_config()` via `ISA_ADD_CONFIG`):

```
PNPHACK[p51]: ice[0] prio=0 nmem=0 nport=51 nirq=0 ndrq=0     <-- == 50 clamp skipped
PNPHACK[p61]: ice[0] prio=0 nmem=768 nport=66 nirq=1 ndrq=1   <-- ALL FOUR counts clobbered
PNPHACK[p61]:   ic_port[50] start=0xa00 end=0xa07 ...         <-- OOB of ic_port[50]
PNPHACK[p61]:   ic_port[51] start=0x300 end=0x42 size=0x1 align=0x1  <-- the alias itself:
   0x300 == ic_nmem(768), 0x42(66) == live ic_nport, size/align == ic_nirq/ic_ndrq(1,1)
PNPHACK[p61]:   ic_port[64..69] ...                            <-- writes past struct isa_config
(null) can't assign resources (port)                           <-- consumer ran poisoned nport=51
(null) can't assign resources (memory)                         <-- consumer ran poisoned nmem=768
```

Arithmetic of the == defeat: each IO_RANGE tag adds 1 descriptor + 2 quirk
appends = +3 to ic_nport, so pre-tag counts are 0,3,...,48,51 — 50 is never
observed by `if (config->ic_nport == ISA_NPORT)` (pnpparse.c:137).

### run-B (silent corruption) — `run_b.log`

POISON_V=0x4000: tag 18 sets ic_nport=0x4000 (ir_end=attacker endbase);
subsequent field writes and quirk appends land at `ic_port[0x4000..]`,
i.e. offset 128+16*16384 ≈ 256KB past the 8640-byte `configs[]`
kmalloc (pnpparse.c:425). Stored count: `nmem=768 nport=1854`. Guest
survived: the writes landed in mapped kernel heap — ~9.6KB of structured
garbage (16/17/8/8 controlled bits per 16-byte slot at chosen indices)
written over unrelated kernel allocations with **no error, no warning**.

### run-C (panic) — `run_c.log`, `panic.txt`

POISON_V=0x100FE + 12000 sequential tags: stored count `nport=36054`;
walking the stored count reads ~1.6MB past the `isa_config_entry`
allocation and hits a kernel thread-stack guard page:

```
panic: vm_fault: fault on stack guard, addr: 0xfffff8008dd20000
trap 000000000000000c, rip = pnphack3 dump_configs+0x95
vm_fault() -> trap_pfault() -> trap() -> calltrap()
```

(The faulting walk is my dump loop — whose `i < 64` guard the compiler had
*deleted*: `c->ic_port[i]` is a member-array subscript, OOB indexing is UB,
so GCC-8 assumed i<=49 and proved the cap dead. The kernel's own
`pnp_set_config` loop (pnp.c:274-282) performs the identical
`for (i = 0; i < config->ic_nport; i++)` walk over the identical stored
count whenever a config assignment succeeds — the panic is the faithful
proxy for that consumer.)

## The write primitive (characterized)

Per crafted IO_RANGE tag after the clobber, at attacker-chosen index n:
* `ic_port[n] = { start: 16 bits, end: ~17 bits, size: 8 bits, align: 8 bits }`
  (pnpparse.c:158-168),
* two mirrored 16-byte quirk appends at n+1, n+2 = port[0] + 0x400/+0x800
  (port[0] fully attacker-set by the first tag).

Index reach: `n` up to 0x100FE (≈1.05MB past array base); sequential tags
extend a contiguous overwrite arbitrarily far (12000 tags ≈ 192KB span
demonstrated). All at boot from PnP card-supplied bytes, before any
authentication exists — kernel-heap write primitive by malicious/counterfeit
ISA-PnP hardware or emulated device.

## Why isa_common.c is the finding anchor

`isa_add_config()` is the single trust boundary where every PnP-supplied
config enters the kernel's resource-assignment machinery; it copies
`*config` into the persistent `id_configs` list with no count validation
(isa_common.c:947). Its consumer loops index fixed arrays with those counts.
Clamping/rejecting here kills the whole class regardless of which parser
defect (quirk table, future callers, merge path) produces the poison; the
same fix also bounds the quirk (pnp.c) and repairs the `==` clamps
(pnpparse.c).

## uid=0?

Not applicable as a local-user escalation: the trigger is PnP card data
(boot-time hardware supply), not an unprivileged syscall. The primitive is
ring-0 kernel heap corruption at boot — the realistic ceiling is
arbitrary kernel code execution by malicious hardware/VM configuration
(slab grooming is trivial at boot; ~40% byte control per 16-byte slot at
chosen offsets, unlimited repeat).

## Fix validation — see fix_run_a.log / fix_run_c.log

fix.diff (git-apply clean on the repo tree) adds:
1. `isa_add_config`: reject configs with counts outside [0, ISA_N*] (the choke point),
2. `pnp_check_quirks`: bound EXTRA_IO appends by `ic_nport < ISA_NPORT`,
3. pnpparse.c: all seven `== ISA_N*` clamps → `>=` (incl. pnp_merge_resources),
4. `isa_find_memory`: `start <= end` wrap guard + `ISA_FIND_MAX_TRIES` (65536)
   attempt cap (DF-5393).

On the patched kernel (nativekernel build of X86_64_GENERIC with fix.diff):
run-A dumps show the p51/p61 configs **rejected** ("isa_add_config: bogus
counts ... rejected" / "no configs stored"), no OOB slot data, and run-C
loads **without a panic**.

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