# DF-2314 — pnp_create_devices ANSI-trim has no lower bound: OOB stack read below buf[0], NUL write at negative index, uninitialized-stack leak into the device description

**Verdict: REPRODUCED (mechanically, through verbatim pnp.c in the kernel).**
**Impact: kernel stack info leak (device description) + conditional 1-byte OOB NUL stack write. Confidence: certain.**

## What was run

Established verbatim-interposition kernel-compiled harness (pattern of
DF-2300/DF-2319): the guest `/usr/src/sys/bus/isa/pnp.c` (sha256-gated
PRISTINE = audit tree) is compiled unmodified inside `pnp_harness.c`
(TU-compiled; the stock pnp.c unit is dropped from the build, so the
harness provides pnp_eisaformat/pnp_check_quirks/DRIVER_MODULE exactly as
stock). A `PNPH` kernel (X86_64_GENERIC + device pnp_harness) is built in
the guest and booted; `/dev/pnph0` drives the REAL `pnp_create_devices()`
(the static is callable because its TU is compiled into the harness) with
crafted PnP card resource byte streams — exactly the bytes a malicious
ISA-PnP card / VM firmware returns — against the REAL `isa0` bus device,
with groomed kernel-stack residue below the parser frame.

The crafted stream is:

    17 78 56 34 12 00 00 00   logical device 0x12345678
    82 00 00                  PNP_TAG_ID_ANSI large tag, declared length 0
    79                        trailing byte (consumed, loop exits)

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

1. **Sanity** — an ANSI tag of length 16 ("LEGIT-DEV-NAME   ") yields
   desc `LEGIT-DEV-NAME`: the rig drives the real code and the real
   `device_set_desc_copy` (pnp.c:411).

2. **Control** — stack residue below `buf` groomed to non-space (0x12):
   `buf[-1] != ' '`, the trim loop exits immediately, `buf[0]='\0'`,
   desc is the empty string. No OOB effects (snapshot diff clean).

3. **OOB read walk below buf[0]** — residue groomed to 0x20 below `buf`
   (buf located precisely at pad+8044 by planting "LEGIT-DEV-NAME" there
   in a prior sanity pass; the 8KB groom pad models boot-time stack
   residue): with declared length 0 the loop `while (buf[large_len-1] ==
   ' ') large_len--;` (pnp.c:406-407) reads `buf[-1]` and walks 12 bytes
   backwards through residue before stopping at a non-0x20 byte.

4. **OOB NUL write below buf[0]** — `buf[large_len] = '\0'`
   (pnp.c:408) then writes 0x00 at **buf[-12]** (pad+8032): position-
   resolved by a control/walk snapshot diff — the single groom byte that
   changed 0x12→0x00 (control) / was 0x20→0x00 (walk). The byte sits 12
   bytes below `buf[0]`, inside `pnp_create_devices`' own frame — the
   only 0x00-writing store on the executed path is this one, so the
   negative-index store is proven, not inferred. The walk distance is
   residue-determined (it stops at the first non-space below `buf`).

5. **Uninitialized-stack leak into the device description** — with
   `large_len < 0` at loop exit, `buf[0..99]` is never written (the
   bcopy copied 0 bytes); `desc = buf` (pnp.c:409) and the code hands
   the raw stack to `device_set_desc_copy` (subr_bus.c:1749-1754:
   `kmalloc(strlen(desc)+1); strcpy(...)`). Demonstrated end-to-end:
   the captured description is
   `"            DF2314"` (18 bytes = 12×0x20 residue + the groom
   marker) — bytes that exist nowhere in the crafted card stream. Two
   independent manifestations:
   - probe11 (ANSI tag BEFORE the logical-device tag): the deferred
     application `if (desc) device_set_desc_copy(dev, desc)`
     (pnp.c:448-449 — the natural multi-logical-device card pattern,
     since PnP cards interleave per-LD ANSI strings) sets the leaked
     description on the newly created device.
   - probe17 (len-0 walk + second LD tag): the second device again
     receives the 18-byte stack-residue description.
   Repeated runs are byte-identical under a fixed groom.

6. **Unprivileged visibility** — on DragonFly the description reaches
   userspace through `device_print_child()`
   (sys/kern/subr_bus.c:2929-2930 `device_printf(child, "<%s>",
   device_get_desc(child))`), which prints for every device probed at
   boot — the same boot log contains e.g. `atkbd0: <AT Keyboard>`.
   A malicious card whose logical device binds any isa driver prints
   the leaked residue into the world-readable dmesg buffer.
   (DF devinfo(8) itself does not print device descriptions — checked
   against /usr/src/usr.sbin/devinfo/devinfo.c.)

7. **Rig observation, documented honestly** — when the ANSI tag follows
   the logical-device tag, the *immediate* call at pnp.c:410-411
   produced an empty description in this build even when the walk went
   negative (probe18 proves the call runs: it overwrote a previously
   set good description with ""). The deferred pnp.c:448-449 site —
   hit naturally by any card with a following logical-device tag or by
   the ANSI-first ordering — leaks verbatim, so the vulnerability class
   is demonstrated through pnp.c's own call sites either way. The OOB
   read (3) and OOB write (4) are position-proven regardless of call
   site.

## Reachability / attacker model (honest)

PnP card resource data is parsed at isa-bus attach during boot
(pnp_identify → pnp_isolation_protocol → pnp_read_resources →
pnp_create_devices). No ISA-PnP hardware exists on the QEMU guest (or on
virtio/modern VMs); the attacker is a **malicious physical ISA-PnP card or
VM firmware** (CVSS AV:P). The leak triggers when the stack byte at
`buf[-1]` (boot-path residue, not attacker-chosen) is 0x20 — plausible at
boot given the volume of strings/pointers on the early boot stack, but
probabilistic per boot; every all-spaces payload (any length) reaches the
same `buf[-1]` read deterministically.

## privesc assessment (team directive)

No privesc. DragonFly has **no KASLR**, so a stack disclosure defeats no
randomization; the leak cannot be triggered by any local or remote
unprivileged user (no PnP hardware path exists post-boot; no re-scan
supported — pnp_identify returns early when the bus is DS_ATTACHED). The
OOB write is a single 0x00 byte at a residue-determined offset within the
parser frame. Close as: boot-time hardware-attacker kernel-stack info
leak + conditional 1-byte stack NUL write. Medium severity stands.

## Fix validation

`fix.diff` adds the same lower bound the sibling parser already has
(pnpparse.c:231 `while (len > 0 && buf[len-1] <= ' ')`):

    -			while (buf[large_len-1] == ' ')
    +			while (large_len > 0 && buf[large_len-1] == ' ')

Applied to the guest tree, kernel rebuilt and booted: sanity desc still
"LEGIT-DEV-NAME" (no regression); smart-walk run shows **no NUL-write
signature** (baseline: 1 at buf[-12]) and **empty desc**; probe11 leak
path yields desc len 0 — the leak is eliminated. 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
    ./pnphctl 3; ./pnphctl 10; ./pnphctl 11   # 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.
