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

NULL pointer dereference in ppb_pnp_detect via crafted IEEE 1284 PnP string

Summary

ppb_pnp_detect at ppbconf.c:242/249/255/259/264/271 calls search_token(token,UNKNOWN_LENGTH,":") and does NULL+1 unconditionally without checking return value. search_token at ppbconf.c:177-206 returns NULL when the requested sub-token is not found within the scan window (line 205). With UNKNOWN_LENGTH=-1, search_token scans from token to next NUL (lines 185-188); if keyword token (MFG/MDL/VER/REV/CLS/CMD) found but no : appears before next NUL, loop at 197-203 exhausts without match and returns NULL. Caller computes NULL+1=(char*)0x1 and passes to kprintf("%s",...) which dereferences address 1 in kernel VA - unmapped, page fault, kernel panic. Six call sites: MFG/MANUFACTURER at 242, MDL/MODEL at 249, VER at 255, REV at 259, CLS at 264 (also feeds class to search_token(class,...) at 278 if survived), CMD/COMMAND at 271. Trigger: malicious IEEE 1284 peripheral sends PnP string with keyword but no : separator (e.g. MFG\0 or MFG; where ; replaced by \0 at line 237). Runs at every ppbus attach (boot or kldload) via ppbus_attach -> ppb_scan_bus -> ppb_pnp_detect at :378. DONTPROBE_1284 is NOT defined by default. Impact: reliable kernel panic at boot before multi-user mode.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-1093 Β· 13 files
FileTypeDescriptionSize
df1093_harness.c trigger-source userspace harness with SIGSEGV handler proving NULL+1 deref 3.4 KB view raw
verify.sh trigger-source 7 static source-tree checks; counts 6 unguarded +1 sites 1.5 KB view raw
verify.log run-log verify.sh output (all 7 pass) 436 B view raw
run.log run-log harness output: SIGSEGV at addr 1 unpatched; safe skip patched 325 B view raw
fix.diff suggested-fix add NULL check at each of the 6 search_token(:) + 1 sites 2.2 KB view raw
build.sh build-script build both harness variants 301 B view raw
run.sh run-script verify.sh + both harness variants 252 B view raw
env.txt environment uname, cc, securelevel, HW presence 526 B view raw
README.md readme how to reproduce + bug shape + impact 2.0 KB ↓ raw
VERDICT.md verdict full narrative: mechanism, harness, fix 3.9 KB ↓ raw
fix_build.log build-log compile-validation: kernel+module build with fix applied, rc=0, no errors 5.7 MB ↓ download
../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
README.md readme how to reproduce + bug shape + impact
↓ download raw

DF-1093 β€” NULL pointer dereference in ppb_pnp_detect via crafted IEEE 1284 PnP string

Build

cc -O0 -o df1093_harness df1093_harness.c
cc -O0 -DFIX -o df1093_harness_fix df1093_harness.c

Run

./df1093_harness        # SIGSEGV at addr 1 (kernel panic equivalent)
./df1093_harness_fix    # safe return, no deref
sh verify.sh            # 7 static source checks

Expected (bug present)

  • verify.sh reports PASS=7 FAIL=0 and counts 6 unguarded search_token(token, UNKNOWN_LENGTH, ":") + 1 sites in ppbconf.c.
  • df1093_harness traps a SIGSEGV at address 1 with BUG: NULL+1 deref trapped (SIGSEGV at addr 1).
  • df1093_harness_fix returns normally with MFG keyword found but no ':' β€” skip safely.

Bug shape

ppb_pnp_detect at sys/bus/ppbus/ppbconf.c:213-288 parses an IEEE 1284 PnP ID string and prints the device info. For each of six keywords (MFG/MANUFACTURER, MDL/MODEL, VER, REV, CLS, CMD/COMMAND) the function does:

if ((token = search_token(str, len, "MFG")) != NULL || ...)
    kprintf("ppbus%d: <%s", unit,
        search_token(token, UNKNOWN_LENGTH, ":") + 1);   /* :242 */

search_token (:177-206) returns NULL when the inner token (here ":") is not found in the scan window. When the malicious peripheral supplies MFG\0 (keyword found, no : before the next NUL), the inner search_token returns NULL. The unconditional + 1 then yields (char *)0x1, which kprintf("%s", ...) dereferences β€” unmapped address in kernel VA, page fault, kernel panic.

A malicious peripheral can craft any of the six keyword-only strings to trigger the panic at boot or on kldload ppbus. DONTPROBE_1284 is not defined by default.

Impact / preconditions

The trigger is physical (AV:P) β€” a malicious IEEE 1284 peripheral plugged into a parallel port. The audit QEMU guest has no parallel port HW (pciconf -l shows no ISA/parallel bridge; dmesg | grep -c pnp = 0), so the path is not exercised at runtime here; the bug is confirmed by source trace + harness.

VERDICT.md verdict full narrative: mechanism, harness, fix
↓ download raw

DF-1093 β€” NULL pointer dereference in ppb_pnp_detect

Verdict

NOT REPRODUCED at runtime (hardware-gated) β€” STATIC VERIFICATION + HARNESS CONFIRMED.

The bug exists verbatim in sys/bus/ppbus/ppbconf.c:213-288. Six call sites in ppb_pnp_detect compute search_token(token, UNKNOWN_LENGTH, ":") + 1 unconditionally without checking whether search_token returned NULL. search_token (:177-206) returns NULL when the inner token is not found in the scan window, which a malicious IEEE 1284 peripheral can arrange by sending a PnP ID string with a keyword (MFG, MDL, VER, REV, CLS, CMD etc.) but no : separator before the next NUL. NULL + 1 = (char *)0x1 is then dereferenced by kprintf("%s", ...), which on the kernel heap/stack-less low addresses page-faults β€” kernel panic at every ppbus attach (boot or kldload ppbus).

The audit QEMU guest has no parallel-port hardware (pciconf -l shows no ISA/parallel bridge; dmesg | grep -c pnp = 0), so the path is not exercised at runtime here. The trigger requires attacker-controlled IEEE 1284 hardware (AV:P/AC:L) β€” same hardware-gating class as the DF-1071 PnP finding.

The df1093_harness userspace C program installs a SIGSEGV handler, mirrors search_token and the MFG-handling block, then feeds it the malicious string "MFG" (keyword, no :). The unpatched harness traps the SIGSEGV at address 1 β€” the kernel equivalent of which is a page-fault panic. The patched harness (NULL check before the + 1) returns normally.

Mechanism (confirmed by source trace)

search_token (ppbconf.c:177-206):

static char *
search_token(char *str, int slen, char *token)
{
    ...
    if (slen == UNKNOWN_LENGTH)
        for (slen = 0, p = str; *p != '\0'; p++)
            slen++;                          /* :187  scan to next NUL */
    ...
    for (i = 0; i <= slen-tlen; i++) {       /* :197 */
        for (j = 0; j < tlen; j++)
            if (str[i+j] != token[j])
                break;
        if (j == tlen)
            return (&str[i]);                /* :202 match */
    }
    return (NULL);                           /* :205 miss */
}

In ppb_pnp_detect the ; characters of the PnP string are first replaced by \0 (:236-237), so each keyword lives in its own NUL-terminated slice. A malicious peripheral supplies MFG\0 (no :), and the inner search_token(token, UNKNOWN_LENGTH, ":") scans from MFG to the next NUL (which is the \0 immediately after MFG), finds no :, returns NULL. The six affected sites at :242, :249, :255, :259, :264, :271 all do NULL + 1 and pass the result to kprintf("%s", ...), which dereferences (char *)0x1 β€” kernel page fault, panic.

This is a particularly reliable crash because (a) it happens at attach time (boot or module load), not at first use; (b) it requires no particular privilege β€” merely plugging the peripheral in is enough; (c) DONTPROBE_1284 is NOT defined by default, so the probe runs unconditionally when a ppbus is present.

Reproduction

$ sh verify.sh        # 7/7 static checks; 6 unguarded +1 sites counted
$ cc -O0 -o df1093_harness df1093_harness.c
$ cc -O0 -DFIX -o df1093_harness_fix df1093_harness.c
$ ./df1093_harness        # SIGSEGV at addr 1 (kernel: panic at boot)
$ ./df1093_harness_fix    # safe skip

Fix

fix.diff adds an explicit if (val != NULL) guard at each of the six sites. The pattern is mechanical:

char *val = search_token(token, UNKNOWN_LENGTH, ":");
if (val != NULL)
    kprintf("...", val + 1);

For the MFG and MDL sites (where the empty-string fallback is harmless), the ternary val != NULL ? val + 1 : "" is used. For the CLS site, the NULL check additionally prevents the later search_token(class, len, ...) call at :278 from receiving a garbage class pointer. The nativekernel build of the patched file succeeds; the harness validates the algorithm-level correctness.

Fix verification

fixed

validated

kernel build rc=0 + harness before/after
↓ fix.diffDragonFly 6.5-DEVELOPMENT #1: Sun Jul 19 19:24:29 UTC 2026

Confirmed kernel references

β€”

Detail

Exploit chain

none

Evidence (decisive lines)

β€”

Verdict

Source+harness. ppb_pnp_detect search_token NULL+1 -> panic at attach. No parallel port HW.