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

Length-confused class-token search in ppb_pnp_detect(): OOB stack read past str[PPB_PnP_STRING_SIZE+1] and cross-field PnP class misclassification

Summary

search_token(class, len, ...) pairs an INTERIOR pointer (class = token+1) with the ABSOLUTE device-ID length - the bounded memcmp scans up to ~251 bytes past the 257-byte stack object (CLS at offset 248 of a 256-byte ID reads str[252..507] into ppb_scan_bus frames) and also scans across following PnP fields in-bounds ('CLS:QQ' classifies as PRINTER). Production-code rig 4/4 boots: cross-field misclassification + MODEM matched in OOB stack bytes (dirt map '-164'); clamp-to-field fix -> all UNKNOWN. Read-only, class_id has no in-tree readers - latent memory-safety defect, hardware-gated. Distinct from DF-2328/1093/2327.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-5510 Β· 15 files
FileTypeDescriptionSize
Makefile β€” 479 B ↓ download
README.md β€” 1.4 KB ↓ raw
VERDICT.md β€” 3.0 KB ↓ raw
build.log β€” 8.8 KB view raw
build.sh β€” 74 B view raw
env.txt β€” 456 B view raw
fix.diff β€” 2.2 KB view raw
fix_patched_ppbconf.c β€” 14.1 KB view raw
fix_run.log β€” 877 B view raw
fixrun2.dmesg β€” 13.3 KB ↓ download
harness.c β€” 21.5 KB view raw
run.log β€” 3.1 KB view raw
run.sh β€” 98 B view raw
run4.dmesg β€” 13.5 KB ↓ download
verdict.json β€” 4.2 KB view raw

DF-5510 -- OOB stack read + cross-field misclassification in ppb_pnp_detect() class search (search_token(class, len))

What

ppbconf.c:278 passes the interior pointer class' with the ABSOLUTE device-ID lengthlen' to search_token(); the scan runs up to (class-str) bytes past the 257-byte stack object str[PPB_PnP_STRING_SIZE+1], and even in-bounds it scans across the following PnP fields instead of stopping at the class field's end.

Build / run / expected (inside the DragonFly guest)

build (guest): sh build.sh     # compiles VERBATIM /usr/src/sys/bus/ppbus/ppbconf.c + ppb_1284.c + ppb_base.c into ppbharness2.ko
run   (guest): sh run.sh       # kldload; sleep 75; dmesg | grep PPBX2
expected pristine: "DF-5510 Ta ... class_id=0 (PRINTER)" (deterministic cross-field misclassification of class value 'QQ')
                  "DF-5510 Tb OOB result: class_id=1 (MODEM)"  (token matched in bytes past the str[] object; dirt map shows MODEM@-164)
                  "DF-5510 Tc control ... class_id=10 (UNKNOWN)" (in-bounds control with identical dirt)
expected patched: Ta=10 UNKNOWN, Tb=10 UNKNOWN, Tc=10 UNKNOWN

Rig

ppbharness2.ko compiles the VERBATIM production sources /usr/src/sys/bus/ppbus/{ppbconf.c,ppb_1284.c,ppb_base.c}; a fake "ppc" controller on nexus provides a fully scripted (malicious) IEEE1284 peer. Details in VERDICT.md; shared rig sources in the DF-5510 pack (identical copies here).

VERDICT.md
↓ download raw

DF-5510 VERDICT

OOB stack read + cross-field misclassification in ppb_pnp_detect() class search (search_token(class, len))

REPRODUCED (rig) 4/4 fresh-boot runs

  • Evidence class: no parallel port exists in QEMU, so the rig compiles the UNMODIFIED production ppbconf.c (sha256 c853ab25...) into a KLD together with ppb_1284.c and ppb_base.c, and drives it through a fake "ppc" chipset driver on nexus. ppbconf.c's own DRIVER_MODULE(ppbus, ppc, ...) attaches the real ppbus driver, so ppbus_attach -> ppb_scan_bus -> ppb_pnp_detect -> search_token all run production code; the fake peer hands ppb_1284_read_id() an attacker-chosen 256-byte IEEE1284 device ID nibble by nibble (receipt: the 'ppb: 256 characters: A(0x41)...' line in run4.dmesg shows the exact crafted bytes arriving).

  • Ta (deterministic, in-bounds): ID "CLS:QQ;CMD:PRINTER;" -> class value is "QQ" but search_token(class=str+4, len=19, "PRINTER") scans the whole remainder of the string, finds PRINTER inside the CMD field, and class_id becomes 0 (PRINTER). 4/4 runs. Correct answer: 10 (UNKNOWN).

  • Tb (OOB past the object): 256-byte ID, "CLS:" at offset 248 -> class = str+252; search_token(class, 256, tok) scans str[252..507], i.e. 251 bytes past the end of the 257-byte str[] object. The stack below the attach chain is pre-dirtied with class tokens; in all four runs the scan matched MODEM in out-of-bounds bytes (class_id=1) -- the dirt map confirms MODEM surviving at -164 bytes below the worker frame, exactly the region the OOB scan covers. A 5-byte-or-longer token match cannot be explained by the single uninitialized in-object byte str[256]; it requires reading >=4 bytes past the object.

  • Tc (control): identical dirt, short ID -> the clamped-by-data scan stays inside str[] -> UNKNOWN 4/4. Proves the dirt alone cannot flip the result; only the wrong `len' argument does.

Impact ceiling: silent OOB kernel-stack READ at ppbus attach, driven by peripheral-supplied bytes; ppb->class_id has no readers in-tree, so no exfiltration path, no write primitive, no panic observed (kernel stacks are contiguous and mapped). Hardware-gated (crafted IEEE1284 peripheral). NOT the same defect as DF-2328 (UNKNOWN_LENGTH scan of a non-NUL-terminated buffer) -- Tb's buffer is fully terminated; the OOB here comes from the length argument alone.

FIX VALIDATED: with the scan clamped to the class field (fix.diff hunk 1), Ta and Tb return UNKNOWN on two fresh patched boots (fixrun1/fixrun2).

-- status=reproduced reproduced=True impact=none confidence=certain fix_status=fixed (baseline reproduced, patched behavior gone)

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

fix.diff applied to the guest's /usr/src/sys/bus/ppbus/ppbconf.c, harness rebuilt from the same verbatim source (module-level rebuild; running kernel unchanged), full rig re-run on a fresh boot: the previously observed bad behavior is GONE (Ta/Tb return class_id=10 UNKNOWN on every patched run, verified on two patched boots). Behavior of the sibling known findings (DF-1093/2327/2328/2329) was not targeted by this diff.

fix_run.log / fixrun2.dmesg (patched) vs run.log / run4.dmesg (pristine) in this pack; fix.diff is the exact applied diff.
↓ fix.diffDragonFly dfbsd 6.5-DEVELOPMENT DragonFly 6.5-DEVELOPMENT #0: Thu Jul 2 06:02:54 UTC 2026 root@dfbsd:/usr/obj/usr/src/sys/X86_64_GENERIC x86_64

Confirmed kernel references

Detail

Evidence (decisive lines)

run.log (pristine decisive run: 'DF-5510 Tb OOB result: class_id=1 (MODEM)' + dirt map showing MODEM surviving at -164B below the attach frame + the ppb: <PnP> receipt proving the crafted 256-byte ID 'AAAA..CLS:QQA;' was delivered verbatim); fix_run.log (patched: class_id=10 UNKNOWN); harness.c (hx_worker Ta/Tb/Tc sections); fix.diff hunk 1.

PoC changes

No seed PoC existed (pass-2 finding); rig written from scratch following the DF-5418/5419 ppbharness class: verbatim-source KLD (ppbconf.c+ppb_1284.c+ppb_base.c), scripted IEEE1284 nibble-mode peer, fake ppc controller on nexus, real ppbus driver attach. Bring-up fixes: comment typo; the stack pre-dirtier initially collapsed to a single frame via -O2 tail-call optimization (made noinline with a post-call touch).

Verified recommended fix

Clamp the class-token scan to the class field: search_token(class, clen, tok) where clen spans class..first NUL within str[len] (fix.diff hunk 1).

Verdict

REPRODUCED 4/4 pristine runs in the ppbharness2 KLD rig (verbatim production ppbconf.c compiled inside the guest; scripted malicious IEEE1284 peer delivers a peripheral-chosen device-ID string). ppb_pnp_detect() passes the interior pointer class' together with the ABSOLUTE ID lengthlen' to search_token() (ppbconf.c:278). With a crafted 256-byte ID placing 'CLS:' at offset 248, the token scan reads str[252..507] -- up to 251 bytes past the end of the 257-byte stack object str[PPB_PnP_STRING_SIZE+1] (ppbconf.c:219). In every run the scan matched the MODEM token in out-of-bounds stack bytes (class_id=1) while the in-data class value was 'QQ'; the in-bounds control (same dirt, short ID, scan stays inside str[]) returned UNKNOWN 4/4, proving the flip is caused by the length confusion. The same wrong-length argument also misclassifies fully in-bounds by matching tokens in FOLLOWING PnP fields: 'CLS:QQ;CMD:PRINTER;' is classified PRINTER (4/4) although the class value is 'QQ'. Production ceiling: silent OOB kernel-stack read at every ppbus attach (boot or kldload of ppc/ppbus) driven by peripheral-supplied bytes; class_id has no in-tree readers, so there is no data-exfiltration path and no write primitive -- a latent memory-safety defect (CWE-125). Distinct from DF-2328 (UNKNOWN_LENGTH scan past a non-NUL-terminated buffer) and DF-1093/DF-2327 (NULL+1). Fix validated: with the scan clamped to the class field, Ta and Tb both return UNKNOWN.