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)
PoC verification
Evidence pack
findings/poc/DF-5510 Β· 15 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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).
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
fixedfix.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.
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.
No comments yet.