pnp_create_devices ANSI tag trim loop reads/writes stack below buf[0] with no lower bound
| Field | Value |
|---|---|
| ID | DF-1072 |
| Status | new |
| Severity | Medium |
| CVSS 3.1 | CVSS:3.1/AV:P/AC:L/PR:N/UI:N/S:U/C:H/I:L/A:L |
| CWE | CWE-125 Out-of-bounds Read; CWE-787 Out-of-bounds Write |
| File | sys/bus/isa/pnp.c |
| Lines | 398-411 (ANSI tag handling) |
| Area | bus/isa (ISA Plug-and-Play protocol driver) |
| Confidence | certain |
| Discovered | 2026-07-14 |
| Reported | pending |
| Known CVE | none |
| CVE match | dfly_specific |
Summary
The trailing-space trim loop after the ANSI-string bcopy has no lower bound on
large_len. A malicious PnP card can return a PNP_TAG_ID_ANSI large tag with length 0
(or a payload consisting entirely of 0x20 spaces), causing the loop to evaluate
buf[large_len-1] with large_len == 0 (i.e. buf[-1]) and decrement without limit,
walking the stack below char buf[100]. The terminating buf[large_len] = '\0' then
writes a NUL at the resulting negative index.
Root cause
/* pnp.c:398-411 */
if (PNP_LRES_NUM(tag) == PNP_TAG_ID_ANSI) {
if (large_len > sizeof(buf) - 1)
large_len = sizeof(buf) - 1;
/* !!! no `if (large_len == 0) continue;` lower bound */
bcopy(resinfo, buf, large_len);
/*
* Trim trailing spaces.
*/
while (buf[large_len-1] == ' ') /* reads buf[-1] when large_len==0 */
large_len--;
buf[large_len] = '\0'; /* writes NUL at the resulting negative index */
desc = buf;
...
device_set_desc_copy(dev, desc); /* strlen() of possibly-uninitialised buf */
}
large_len is card-controlled (16-bit, pnp.c:387). The clamp if (large_len > sizeof(buf)
- 1) large_len = sizeof(buf) - 1; (line 399-400) only enforces an upper bound of 99; it
never enforces a lower bound. With large_len == 0 the loop while (buf[large_len-1] == ' ')
large_len--; (line 406-407) immediately evaluates buf[-1], then buf[-2], etc., each
read being a stack OOB read below buf (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';
(line 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
(line 409) followed by device_set_desc_copy(dev, desc) (line 411) strlen()s the
uninitialized buf[0..] (bcopy wrote 0 bytes), leaking stack contents through the device
description.
Threat model & preconditions
- Attacker position: Malicious ISA-PnP card / QEMU-emulated device, same as DF-1071.
- Privileges gained or impact: Kernel stack memory disclosed via
devinfo/dmesgdescription strings (CWE-909 / CWE-125), plus a conditional single-byte stack OOB NUL write (CWE-787) at the first0x20byte found belowbuf; in principle a long run of0x20bytes (e.g. on a specially groomed stack) could extend the write further down, but the typical effect is a 1-byte stack corruption or an info leak of one or more stack bytes. - Required config or capabilities: Default kernel with ISA PnP. Card must pass isolation (see DF-1071).
- Reachability:
pnp_isolation_protocolβpnp_create_devicesat every boot. Trigger: return a large tag withPNP_LRES_NUM == PNP_TAG_ID_ANSI(0x02, i.e. tag byte 0x82) and length 0 (two zero length bytes), or any ANSI payload composed entirely of0x20.
Proof of concept
Minimum PoC resource blob returned by a malicious card after a successful isolation:
82 00 00 β i.e. PNP_TAG_ID_ANSI large tag (0x82) with 16-bit length field 0x0000 and
zero payload bytes, followed by 79 (PNP_TAG_END, small tag, len 0).
Implement a QEMU ISA-PnP model whose resource-data READ returns 82 00 00 79 after the
card has won isolation; boot DragonFlyBSD with the model attached.
Build & run
# Implement a QEMU isa-pnp model returning the byte stream above, then: qemu-system-x86_64 -enable-kvm -m 512 -hda dfbsd.img -device isa-pnp-custom
Expected output
- (a) The OOB read of
buf[-1](and possiblybuf[-2]..if those stack bytes happen to be0x20). - (b) The device description string for the matching PnP child will then contain
uninitialized stack bytes β visible in
devinfo -vanddmesg. - (c) If any of those bytes equals
0x20,buf[large_len] = '\0'writes a NUL one or more bytes belowbufon the kernel stack, potentially corrupting a local variable or saved register, with outcomes ranging from silent corruption to kernel panic on return.
A purely emulator-driven demonstrator (no real hardware) is the most reproducible form: a
QEMU patch returning 82 00 00 from PNP_RESOURCE_DATA after the card has won isolation.
Impact
Kernel stack info leak + conditional 1-byte stack OOB NUL write from a malicious ISA-PnP
device during boot. Physical plug-in precondition; CVSS AV:P. Medium severity per "info
leak of kernel stack" + possible stack corruption.
Recommended fix
Bound the trim loop below and skip the bcopy / desc path when large_len is 0:
--- a/sys/bus/isa/pnp.c
+++ b/sys/bus/isa/pnp.c
@@ -398,7 +398,9 @@
if (PNP_LRES_NUM(tag) == PNP_TAG_ID_ANSI) {
if (large_len > sizeof(buf) - 1)
large_len = sizeof(buf) - 1;
+ if (large_len == 0)
+ continue;
bcopy(resinfo, buf, large_len);
/*
* Trim trailing spaces.
*/
- while (buf[large_len-1] == ' ')
+ while (large_len > 0 && buf[large_len-1] == ' ')
large_len--;
This both prevents the buf[-1] read and stops the loop from running away past buf[0].
References
sys/bus/isa/pnp.c:398-411β ANSI tag handling (the bug)sys/bus/isa/pnp.c:387βlarge_lenis card-controlled- CWE-125 Out-of-bounds Read
- CWE-787 Out-of-bounds Write
Timeline
- 2026-07-14 Discovered during automated audit.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-1072 Β· 8 files| File | Type | Description | Size | |
|---|---|---|---|---|
| verify.sh | trigger-source | static source-verification script (5 checks) | 1.3 KB | view raw |
| verify.log | run-log | verify.sh output on audit commit | 1.9 KB | view raw |
| VERDICT.md | verdict | full narrative: mechanism + why-not-reproduced | 4.6 KB | β raw |
| fix.diff | suggested-fix | lower-bound large_len==0 skip + bounded trim loop | 524 B | view raw |
| env.txt | environment | uname, cc, sysctls | 713 B | view raw |
| README.md | readme | how to reproduce | 1.2 KB | β raw |
| ../fix_build_combined.log | build-log | Combined 41-finding kernel build (rc=0, -Werror clean) | 5.6 MB | β download |
| ../fix_build_summary.txt | build-summary | Summary of the combined 41-finding kernel build | 826 B | view raw |
DF-DF-1072 β reproduce
This finding was verified by static source tracing (see VERDICT.md).
Runtime reproduction on the audit's default QEMU guest is not possible
because the precondition is outside the unprivileged-guest-user threat
model (see VERDICT.md "Why it cannot be triggered from the audit guest").
How to verify (static source check)
# from the repo root (sys/ must be present)
sh findings/poc/DF-DF-1072/verify.sh
The script walks the cited code path in sys/ with grep/sed and
confirms every claim in the finding markdown.
Files
| File | Purpose |
|---|---|
| verify.sh | static source-verification script |
| verify.log | output of verify.sh on the audit commit (the evidence) |
| VERDICT.md | full narrative: mechanism, why-not-reproduced, fix rationale |
| fix.diff | git-apply-able fix (validated with git apply --check) |
| env.txt | guest environment (uname, cc, sysctls, modules) |
| manifest.json | machine-readable artifact catalog |
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)
/* 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
Fix verification
not_testablecompile validated
see evidence pack
Confirmed kernel references
β
Detail
Exploit chain
none
Evidence (decisive lines)
β
Verdict
Source-confirmed. pnp ANSI tag large_len=0 trim loop buf[-1] stack OOB. In GENERIC, no PnP cards. Fix compiles.
No comments yet.