Missing large_len>0 guard in ANSI-string trim walks buf[] backwards (OOB stack read + NUL write + stack info-leak via device desc)
Summary
When parsing PNP_TAG_ID_ANSI large resource with declared length 0 trailing-space trim loop evaluates buf[large_len-1]=buf[-1] with no lower-bound check. One-byte OOB stack read unconditional; if that byte is 0x20 loop walks backwards then writes NUL at negative offset and leaves buf uninitialized. desc pointed at uninitialized buf then device_set_desc_copy does strlen+strcpy leaking uninitialized stack contents into device description visible via devinfo/dmesg. Sibling parser pnpparse.c:231 has correct guard while (len>0 && buf[len-1]<=32) but pnp.c never updated. Attacker controls PnP ISA card resource bytes during boot (malicious host in VM or malicious ISA card). Leaked stack bytes contain return addresses/pointers useful for KASLR defeat. [VERIFIED pass2: reproduced - length-0 ANSI tag reads buf-1, walks 12 bytes back on 0x20 residue and NUL-writes at buf-12 by snapshot diff, desc becomes 18 bytes of uninitialized stack reaching world-readable dmesg; boot-time hardware-attacker reach only, no KASLR on DFly so leak ceiling; guard fix-validated mirroring pnpparse.c]
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-2314 Β· 14 files| File | Type | Description | Size | |
|---|---|---|---|---|
| VERDICT.md | verdict | full narrative: mechanism, evidence, reachability, privesc assessment, fix validation | 7.3 KB | β raw |
| run.log | run-log | baseline decisive output incl. stack-window dumps and the leaked description bytes | 5.2 KB | view raw |
| fix_run.log | run-log | fixed-kernel A/B: no NUL signature, empty desc, sanity intact | 1.7 KB | view raw |
| leak_sample.txt | leak-sample | leaked device-description bytes across runs (18 bytes of stack residue) | 1.7 KB | view raw |
| pnp_harness.c | source | kernel harness: verbatim pnp.c TU-compiled + /dev/pnph0 scenarios + stack groom | 23.8 KB | view raw |
| pnphctl.c | source | userland scenario driver | 2.9 KB | view raw |
| pnph.h | source | shared ioctl definitions | 1.9 KB | view raw |
| prep.sh | build-script | guest prep with verbatim sha256 gate (pnp.c PRISTINE 2ba5773d...) | 1.9 KB | view raw |
| build.sh | build-script | nativekernel KERNCONF=PNPH + installkernel | 126 B | view raw |
| run.sh | run-script | baseline scenario sequence | 172 B | view raw |
| build.log | build-log | baseline harness-kernel build evidence | 1.4 KB | view raw |
| fix_build.log | build-log | fixed-kernel build evidence | 1.4 KB | view raw |
| fix.diff | suggested-fix | validated one-line trim guard (git-apply-able, hunk verified) | 270 B | view raw |
| env.txt | environment | guest uname, build, gates | 1.2 KB | view raw |
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)
-
Sanity β an ANSI tag of length 16 ("LEGIT-DEV-NAME ") yields desc
LEGIT-DEV-NAME: the rig drives the real code and the realdevice_set_desc_copy(pnp.c:411). -
Control β stack residue below
bufgroomed 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). -
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 loopwhile (buf[large_len-1] == ' ') large_len--;(pnp.c:406-407) readsbuf[-1]and walks 12 bytes backwards through residue before stopping at a non-0x20 byte. -
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 belowbuf[0], insidepnp_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 belowbuf). -
Uninitialized-stack leak into the device description β with
large_len < 0at 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 todevice_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 applicationif (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. -
Unprivileged visibility β on DragonFly the description reaches userspace through
device_print_child()(sys/kern/subr_bus.c:2929-2930device_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.) -
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.
Fix verification
fixedBoth fixes (DF-2314 trim guard and DF-2315 quirk bounds, same file) applied to guest /usr/src, kernel rebuilt and booted: baseline symptoms gone, positive behavior intact.
fix_run.log: scenario 10 prints no NUL-write signature (baseline had one at buf[-12]) and empty desc; probe11 desc len 0 (baseline 18-byte stack residue); scenario 3 sanity desc LEGIT-DEV-NAME unchanged
Confirmed kernel references
Detail
Evidence (decisive lines)
run.log: PNPH smart NUL-write signature at pad+8032 = 12 bytes below buf[0] at pad+8044, and the walk window dump (ctrl 0x12 fill vs walk 0x20 fill with 00 at buf[-12]); probe11 line: child desc = 18-byte stack residue (leak_sample.txt); fix_run.log: no NUL-write signature, probe11 desc len 0, sanity LEGIT-DEV-NAME unchanged
PoC changes
seed had no runnable harness; built the established verbatim-interposition kernel harness (pnp.c TU-compiled, stock unit disabled in conf/files, PNPH = X86_64_GENERIC + device pnp_harness, /dev/pnph0 root-only control, real isa0 parent, 8KB kernel-stack groom with 64-byte marker blocks and phase retry, inline snapshot diff to isolate the OOB NUL write). Six guest kernel build/boot cycles were needed: d_open stub, -Wcast-qual volatile casts, sys/device.h include, then buf location via planted marker, deterministic phase selection, and the leak-completing ANSI-first stream.
Verified recommended fix
guard the trim loop with a lower bound: while (large_len > 0 && buf[large_len-1] == 0x20) large_len--; - matching the sibling guard at pnpparse.c:231
Verdict
Reproduced mechanically through verbatim pnp.c compiled into the PNPH harness kernel (guest pnp.c sha256-gated PRISTINE against the audit tree). A PNP_TAG_ID_ANSI tag of declared length 0 makes the trim loop at pnp.c:406-407 read buf[-1] and, with 0x20 boot-stack residue, walk backwards (12 bytes observed) and NUL-write at buf[-12] via the store at pnp.c:408 - the OOB read walk and the negative-index 1-byte stack write are position-proven by control/walk groom snapshots. buf[0..99] stays uninitialized and is handed to the real device_set_desc_copy, so kernel stack residue becomes the device description: captured desc is 18 bytes of stack residue (12 x 0x20 plus a groom marker that exists nowhere in the crafted card stream), set through pnp.c own call sites (deferred application at pnp.c:448-449, hit naturally by multi-logical-device cards and by the ANSI-first ordering; also on a second LD tag). At real boot the description prints to the world-readable dmesg buffer via device_print_child (subr_bus.c:2929-2930). Not a privesc: DragonFly has no KASLR so the stack disclosure defeats nothing, and the only trigger is a malicious physical ISA-PnP card or VM firmware at boot (no PnP hardware on VM guests; no post-boot rescan). Fix validated on a rebuilt kernel: with the large_len > 0 guard the NUL signature disappears, the description is empty and no stack bytes leak, while normal ANSI strings still work.
No comments yet.