# DF-1072 — pnp_create_devices ANSI tag trim loop has no lower bound (stack OOB read/write)

## Verdict

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

The cited code path and bug exist verbatim in `sys/bus/isa/pnp.c:398-411`.
Like DF-1071, the file is built into the default GENERIC kernel
(`device isa`), but the runtime trigger requires a **malicious ISA-PnP
card** returning a `PNP_TAG_ID_ANSI` large tag with `large_len == 0` (or
a payload of all `0x20` spaces). The audit guest has no PnP cards
(`dmesg` shows none; `pnp_isolation_protocol` returns 0 devices), so the
vulnerable loop is never entered. Same hardware-gated classification as
DF-1071.

## Mechanism (confirmed by source trace)

```c
/* pnp.c:398-411 — the bug */
if (PNP_LRES_NUM(tag) == PNP_TAG_ID_ANSI) {       /* 0x82 = large tag, LRES_NUM 0x02 */
    if (large_len > sizeof(buf) - 1)              /* :399  clamp to 99              */
        large_len = sizeof(buf) - 1;              /* :400  (upper bound only)       */
                                                  /* !!! no `if (large_len == 0) continue;` */
    bcopy(resinfo, buf, large_len);               /* :401  with large_len=0, copies nothing */

    /*
     * Trim trailing spaces.
     */
    while (buf[large_len-1] == ' ')               /* :406  reads buf[-1] when large_len==0 */
        large_len--;                              /* :407  decrements without limit         */
    buf[large_len] = '\0';                        /* :408  writes NUL at resulting index    */
    desc = buf;                                   /* :409  desc points at uninitialized buf */
    if (dev)
        device_set_desc_copy(dev, desc);          /* :411  strlen() on uninitialized buf    */
    continue;
}
```

`large_len` is card-controlled (16-bit, `pnp.c:387`). The clamp at
`:399-400` only enforces an **upper** bound of 99 (`sizeof(buf) - 1`); it
never enforces a lower bound. With `large_len == 0` the loop
`while (buf[large_len-1] == ' ') large_len--;` (`:406-407`) immediately
evaluates `buf[-1]`, then `buf[-2]`, etc. — each read is a stack OOB
read below the `char buf[100]` array (other locals of
`pnp_create_devices` live there: `desc`, `csnldn`, `ldn`, `logical_id`,
`large_len` itself, `retval`, plus caller frame). The loop only stops
when a non-`0x20` byte is found. Then `buf[large_len] = '\0';` (`:408`)
writes a NUL at the (possibly negative) resulting index — a stack OOB
**write** conditional on the prior OOB byte being `0x20`. Even if no
write occurs, `desc = buf` followed by `device_set_desc_copy(dev, desc)`
(`:411`) `strlen()`s the uninitialized `buf[0..]` (the `bcopy` wrote 0
bytes), leaking stack contents through the device description.

Definitions confirmed in `sys/bus/isa/pnpreg.h`:
`PNP_RES_TYPE(a) = (a >> 7)` (`:217`), `PNP_LRES_NUM(a) = (a & 0x7f)`
(`:220`), `PNP_TAG_ID_ANSI = 0x2` (`:238`) — so a tag byte of `0x82`
(bit 7 set, low 7 bits = 2) selects the ANSI branch, with the 16-bit
length field attacker-controlled.

## Why it cannot be triggered from the audit guest

Same as DF-1071: no PnP cards in the audit QEMU guest, no runtime path
to re-invoke `pnp_identify`. The bug requires attacker-controlled PnP
hardware. CVSS `AV:P/AC:L`.

## Exploit chain

None developed — primitive is a stack OOB read + conditional 1-byte NUL
write below `buf`, requiring attacker-controlled PnP hardware. The
finding rates the impact as kernel-stack info leak + possible stack
corruption; not derivable on the audit guest.

## PoC

`verify.sh` — static-verification script that walks the cited path with
`grep`/`sed` against `sys/`, confirming: (1) the clamp at `:399-400`
enforces only an upper bound (no lower-bound `large_len == 0` guard);
(2) the trim loop at `:406-407` reads `buf[large_len-1]` with no lower
bound, so `large_len == 0` reads `buf[-1]`; (3) `buf[large_len] = '\0'`
at `:408` writes NUL at the resulting negative index; (4) the
`PNP_TAG_ID_ANSI` / `PNP_LRES_NUM` / `PNP_RES_TYPE` definitions in
`pnpreg.h:217,220,238`; (5) no PnP cards present in the audit guest so
the loop never executes. Run from the repo root:
`sh findings/poc/DF-1072/verify.sh`.

## Fix

`fix.diff` — adds `if (large_len == 0) continue;` after the upper-bound
clamp (skipping the `bcopy`/`desc` path entirely for empty ANSI tags),
and changes the trim loop to `while (large_len > 0 && buf[large_len-1]
== ' ') large_len--;` to bound it below at `buf[0]`. Both prevent the
`buf[-1]` read and stop the loop from running away past `buf[0]`.
**Matches** the finding markdown's recommended fix.

## Reproduce

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