# DF-1071 — pnp_create_devices large-tag length bytes never subtracted from scanning (heap OOB)

## Verdict

**NOT REPRODUCED (runtime) — STATIC VERIFICATION CONFIRMED.**

The cited code path and bug exist verbatim in `sys/bus/isa/pnp.c:386-396`.
`pnp.c` **is built into the default GENERIC kernel** (`bus/isa/pnp.c
optional isa`, `sys/conf/files:2124`; `device isa` at
`sys/config/X86_64_GENERIC:58`), and `pnp_identify` (the entry point that
calls `pnp_isolation_protocol` → `pnp_create_devices`) **is in the running
kernel** (`0xffffffff809d2340 t pnp_identify`).

The runtime trigger, however, requires a **malicious ISA-PnP card** (or a
hostile QEMU ISA-PnP device model) that, after winning the serial-
isolation protocol, returns crafted resource data containing a large tag.
The audit's default QEMU guest has **no PnP cards** — `dmesg` shows zero
`pnp` device lines and the boot-time `pnp_isolation_protocol` returns
zero devices, so the vulnerable loop in `pnp_create_devices` is **never
entered** on this guest. There is no sysctl/ioctl path that re-invokes
`pnp_identify` after boot, so the bug cannot be triggered from inside
the guest as an unprivileged user.

This is a **hardware-gated** defect: real in source, present in the
default kernel, but requiring attacker-controlled PnP hardware. CVSS
`AV:P/AC:L` (physical plug-in / malicious peripheral) reflects this.

## Mechanism (confirmed by source trace)

`pnp_create_devices` (`pnp.c:365-`) parses a TLV resource blob returned
by a PnP card. The large-resource branch (`:382-397`):

```c
/* pnp.c:386-396 — the bug */
if (scanning < 2) { scanning = 0; continue; }
large_len = resp[0] + (resp[1] << 8);     /* :387  consume 2 length bytes */
resp += 2;                                 /* :388  advance pointer     */
                                           /* !!! scanning NOT decremented by 2 */
if (scanning < large_len) { scanning = 0; continue; }
resinfo = resp;
resp += large_len;                         /* :395  advance by payload  */
scanning -= large_len;                     /* :396  only payload subtracted */
```

Each large tag therefore inflates `scanning` by 2 (the unaccounted-for
length bytes), so after all valid resource bytes are consumed the loop
keeps iterating and reads `*resp++` (`:380`), `resp[0]`/`resp[1]`
(`:387`), and `bcopy(resinfo, buf, large_len)` (`:401`) past the
`kmalloc`'d resource buffer — heap OOB reads.

Compare the **correct sibling pattern** in `pnpparse.c:529-536` and
`:591-593`, which both do `l = I16(p); p += 2; len -= 2;` — they
**subtract the 2 length bytes**. Only `pnp_create_devices` forgets to.

The OOB-read tag bytes can then drive:
- (a) `*resp++` reads heap memory past `resources + len` (`:380`);
- (b) if the OOB byte looks like `PNP_TAG_LOGICAL_DEVICE` (`0x0f`, len 2),
  `bcopy(resinfo, &logical_id, 4)` (`:444`) reads further OOB heap into
  `logical_id`, which then drives `BUS_ADD_CHILD` / `pnp_set_config`;
- (c) if it looks like a large tag (high bit set), `large_len` is decoded
  from two further OOB bytes and `bcopy(resinfo, buf, min(large_len,99))`
  (`:401`) copies up to 99 bytes of kernel heap into the stack buffer,
  which becomes the device description via `device_set_desc_copy` (`:411`)
  — observable through `devinfo(8)` / `dmesg`.

## Why it cannot be triggered from the audit guest

The vulnerable loop runs only when `pnp_isolation_protocol`
(`pnp.c:594-`) finds at least one PnP card. PnP isolation is a bus-
master protocol: the kernel writes a wake-csn to the PnP ADDRESS port,
then bit-bangs 9 bytes (vendor id + serial) out of the READ_DATA port
for each CSN, computing an 8-bit LFSR checksum (`:189-195`). A real or
emulated PnP card must drive READ_DATA with valid isolation data; only
then does the kernel issue `PNP_READ_DATA` to fetch the resource blob.

The QEMU ISA bridge (`piix-isa` / `ich9-isa`) does **not** emulate a PnP
serial-isolation backend — there is no PnP card in the audit guest, so
`pnp_isolation_protocol` returns 0 and `pnp_create_devices` is never
called. There is no sysctl/ioctl to re-invoke `pnp_identify` after boot.
A demonstration would require a custom QEMU device model returning
crafted resource bytes — outside the audit's threat model.

## Exploit chain

None developed — the primitive (OOB heap read; the finding rates it as
info-leak via device description strings) requires attacker-controlled
PnP hardware not present in the audit guest. CVSS `AV:P` (physical).

## PoC

`verify.sh` — static-verification script that walks the cited path with
`grep`/`sed` against `sys/`, confirming: (1) the buggy `resp += 2`
without `scanning -= 2` at `pnp.c:386-396`; (2) the **correct sibling
pattern** in `pnpparse.c:529-536` and `:591-593` (`l = I16(p); p += 2;
len -= 2;`); (3) `device isa` is in `X86_64_GENERIC` so `pnp.c` is built
into the default kernel; (4) `pnp_identify` is in the running kernel but
no PnP cards were detected at boot (`dmesg | grep -i ^pnp` returns
nothing). Run from the repo root: `sh findings/poc/DF-1071/verify.sh`.

## Fix

`fix.diff` — adds the missing `scanning -= 2;` between `resp += 2;`
and the `if (scanning < large_len)` check at `pnp.c:389`, exactly
matching the sibling `pnpparse.c` pattern. After this fix the loop's
invariants match the buffer exactly. **Matches** the finding markdown's
recommended fix.

Validated end-to-end: the diff applies cleanly with `git apply --check`,
and was built into a single-fix `X86_64_GENERIC` kernel that boots and
runs the audit's full PoC suite without regression. (Runtime PoC for
*this* bug cannot run on the guest because no PnP card is present, so
the validation is `not_testable` per the framework — the build/compile/
boot correctness is the demonstrated result.) See `fix_build.log`.

## Reproduce

```
sh findings/poc/DF-1071/verify.sh     # static source verification
```
