β¬’ DragonFlyBSD Kernel Audit
← triage Β· dashboard
DF-2314

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-2314 Β· 14 files
FileTypeDescriptionSize
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
VERDICT.md verdict full narrative: mechanism, evidence, reachability, privesc assessment, fix validation
↓ download 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)

  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.

Fix verification

fixed
baseline reproduced→ patch + rebuild →patched clean

Both 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
↓ fix.diffDragonFly dfbsd 6.5-DEVELOPMENT DragonFly 6.5-DEVELOPMENT #6: Sun Oct 18 16:13:52 UTC 2026 root@dfbsd:/usr/obj/usr/src/sys/PNPH x86_64

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.