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

NULL-pointer dereference in pccard_child_pnpinfo_str via pccard_safe_quote on cards lacking a valid CISTPL_VERS_1

Summary

pccard_child_pnpinfo_str() passes sc->card.cis1_info[0] and [1] directly to pccard_safe_quote() with no NULL check. These pointers remain NULL when inserted card CIS omits CISTPL_VERS_1 or fails to emit null-terminated string before 0xff terminator. CIS parser sets NO error flag in that case so pccard_attach_card proceeds and creates child devices. pccard_safe_quote() dereferences *src unconditionally at :1001. Panics automatically when any driver attaches to such a child (via devadded -> bus_child_pnpinfo_str) and re-triggerable on demand by any unprivileged local user reading child pnpinfo sysctl. Every other consumer (pccard_set_default_descr:159 pccard_do_product_lookup:374 pccard_probe_nomatch:974 pccard_print_cis) guards against NULL; this one does not.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-2318 Β· 25 files
FileTypeDescriptionSize
rig2_main.c β€” 18.2 KB view raw
rig2_shims.h β€” 13.8 KB view raw
extract2.sh β€” 8.1 KB view raw
build.sh β€” 66 B view raw
run.sh β€” 106 B view raw
run_matrix.sh β€” 1.9 KB view raw
run.log β€” 20.7 KB view raw
run.2.log β€” 20.7 KB view raw
run.3.log β€” 20.7 KB view raw
disasm_proof.txt β€” 6.4 KB view raw
build.log β€” 2.5 KB view raw
extract2_manifest.txt β€” 2.4 KB view raw
matrix.log β€” 1.6 KB view raw
env.txt β€” 323 B view raw
fix.diff β€” 743 B view raw
fixes/df2318.diff β€” 743 B view raw
fixes/df5355.diff β€” 1.1 KB view raw
fixes/df5356.diff β€” 601 B view raw
kernel_apply.log β€” 231 B view raw
kernel_build.tail.log β€” 4.6 KB view raw
postboot_extract_manifest.txt β€” 2.4 KB view raw
postboot_run.log β€” 16.8 KB view raw
VERDICT.md β€” 7.3 KB ↓ raw
README.md β€” 1.7 KB ↓ raw
verdict.json β€” 5.8 KB view raw

DF-2318 -- pccard_child_pnpinfo_str NULL deref (no CISTPL_VERS_1)

Impact: kernel panic at evil-card insertion; re-triggerable on demand by any unprivileged local user (devinfo -v / sysctl hw.bus.devices) while the card is seated. Physical-access DoS (16-bit PC Card socket).

Reproduce

The rig lives in this directory; it machine-extracts the production pccard.c / subr_bus.c / pccard_cis.c code byte-identical from the guest's /usr/src (sha256-gated) and drives the real entry pccard_attach_card() with crafted CIS images.

# on the DragonFly guest (pristine /usr/src):
sh build.sh                     # extract + cc; prints BYTE_IDENTICAL=yes
./run.sh                        # all scenarios

Success criterion (baseline):

SCENARIO no_vers1_devadded ... RIG SIGSEGV si_addr=0x0 (signal 11)   <- DF-2318
SCENARIO no_vers1_nomatch  ... RIG SIGSEGV si_addr=0x0               <- DF-2318
SCENARIO no_vers1_sysctl   ... RIG SIGSEGV si_addr=0x0               <- DF-2318
SCENARIO vers1_pnpinfo_ok  ... exit 0  (negative control)
SCENARIO busypanic         ... RIG KERNEL PANIC: resource entry is busy + exit 42  (DF-5355)
SCENARIO reentry_leak      ... exit 0, leak=19176 bytes after 51 bounce cycles    (DF-5356)
SCENARIO benign_cycle      ... exit 0, leak=0                        (negative control)

The same binary covers sibling findings DF-5355 (busypanic) and DF-5356 (reentry_leak); see their packs for their own verdicts.

Fix A/B

sh run_matrix.sh matrix          # baseline + f2318 + f5355 + f5356 variants

f2318 flips exactly the three no_vers1 scenarios to clean exit 0 (cisvendor="" cisproduct="" strings built); all other scenarios unchanged.

fix.diff == fixes/df2318.diff (validated, never applied to sys/).

VERDICT.md
↓ download raw

DF-2318 -- VERDICT

Verdict in one line

REPRODUCED (verbatim-rig level; deterministic across 3/3 runs) -- impact: panic. pccard_safe_quote() dereferences sc->card.cis1_info[0]/[1] == NULL at sys/bus/pccard/pccard.c:1001 inside pccard_child_pnpinfo_str() (:1020-1021); the kernel entry path is hardware-gated (no PCMCIA bridge in QEMU -- env.txt), so the byte-identical production pccard.c code was machine-extracted from the guest's own /usr/src and driven through the real entry pccard_attach_card() with the real devadded/devnomatch/sysctl-trigger surfaces.

What was executed (the rig, DF-2319 protocol)

  • extract2.sh sed/awk-extracts, sha256-gated against the pristine audit tree (pccard.c = 5b610669...), byte-identical (cross-checked two ways, PCCARD_BYTE_IDENTICAL=yes in extract2_manifest.txt):
  • pccard.c: DPRINTF set + pccard_ccr_read/write, pccard_set_default_descr, pccard_attach_card, pccard_detach_card, pccard_function_init/_free/ _enable/_disable, pccard_mfc_adjust_iobase, pccard_safe_quote, pccard_child_pnpinfo_str, pccard_read_ivar (verbatim).
  • kern/subr_bus.c: resource_list_init/free/add/find/delete + bus_child_pnpinfo_str (verbatim; SUBR_BUS_BYTE_IDENTICAL=yes).
  • pccard_cis.c parser (verbatim; the same ranges DF-2319 validated).
  • pccardvar.h structs/macros + constant headers (verbatim ranges).
  • rig2_shims.h emulates ONLY the kernel environment (device tree, bus_alloc_resource bridge-window model, rman, power/CARD methods, kmalloc ledger). device_probe_and_attach() mirrors production subr_bus.c semantics: attach success -> devadded(dev) -> bus_child_pnpinfo_str() (subr_bus.c:2107-2114, :633-645); probe failure -> devnomatch(dev) does the same (subr_bus.c:1984/2039 -> :673); the sysctl walk (hw.bus.devices, read by devinfo(8) at subr_bus.c:3863-3899, world readable -- verified live as user maxx on this guest) asks every device for its pnpinfo string.

The reproduced defect chain

CIS image: MANFID + CONFIG + CFTABLE_ENTRY(io) -- a perfectly legal card that simply omits CISTPL_VERS_1.

  1. The CIS gate does not help (run.log PRE: line): the verbatim parser returns card.error=0, pf_head non-empty, and leaves cis1_info[0]=cis1_info[1]=NULL (they are only ever set by CISTPL_VERS_1, pccard_cis.c:735-760, initialized NULL at :86-89). pccard_attach_card proceeds (pccard.c:205-212 gates only on error/empty-pf_head) and creates child devices (:253-259).
  2. Any of the three pnpinfo surfaces then faults at NULL: * no_vers1_devadded (driver attaches): SIGSEGV si_addr=0x0 * no_vers1_nomatch (no driver): SIGSEGV si_addr=0x0 * no_vers1_sysctl (unprivileged devinfo -v/sysctl walk after a silent probe failure): SIGSEGV si_addr=0x0
  3. Instruction-level pinning (disasm_proof.txt, cc 8.3 -O2, inlined pccard_safe_quote inside pccard_child_pnpinfo_str.isra):

    4019a6: mov 0x110(%r11),%rcx ; rcx = sc->card.cis1_info[0] (NULL) 4019c0: movzbl (%rcx),%r8d ; :1001 if (*src == '"') <-- FAULT

r11 = device_get_softc() (PCCARD_SOFTC); the rig prints offsetof(pccard_softc, card.cis1_info[0]) = 0x110 -- exact match with the displacement. si_addr=0x0 == src, deterministic read at NULL. In kernel context this is a data-access page fault on the socket event thread -> panic at card insertion, and re-triggerable on demand by any unprivileged local user (devinfo -v, sysctl -a) while the card is in.

Negative control: vers1_pnpinfo_ok (identical card WITH VERS_1) builds manufacturer=0x1234 product=0x5678 cisvendor="ACME" cisproduct="Widget" function_type=-1 and exits clean -- the crash is exactly the NULL cis1_info, not the pnpinfo machinery.

Determinism: run.log / run.2.log / run.3.log -- identical results 3/3.

Why this is production behavior

The crashing text is the exact sys/bus/pccard/pccard.c that ships in the guest's kernel (sha256-gated extraction), compiled by the guest toolchain; the trigger sequence (devadded/devnomatch/sysctl-walk -> bus_child_pnpinfo_str -> pccard_child_pnpinfo_str -> pccard_safe_quote) is the production dispatch (subr_bus.c:645/:673/:3899 -> DEVMETHOD bus_child_pnpinfo_str, pccard.c:1340). The only non-production element is the card -- a crafted CIS image in a guard buffer instead of physical attribute memory (no PCMCIA bridge exists in QEMU; env.txt: no /dev/pccard*, 0 pccard devices in devinfo, while device pccard+device cbb are compiled into X86_64_GENERIC).

Fix validation (rig A/B + booted combined-fix kernel)

  • Single-fix A/B (matrix.log): guard both pccard_safe_quote calls on cis1_info[0]/[1] != NULL (empty string otherwise) -> all three no_vers1 scenarios exit 0 with sane cisvendor="" cisproduct="" strings; every other scenario unchanged (busypanic still panics = DF-5355's bug; reentry still leaks = DF-5356's bug; controls stay green).
  • Combined three-fix kernel: pccard.c with DF-2318+DF-5355+DF-5356 fixes built -Werror (nativekernel rc=0), installed, booted (kernel #1); the rig re-extracted from the RUNNING kernel's source (--allow-patched) and re-driven: every scenario clean (postboot_run.log). Guest reset to clean-source snapshot afterwards.

Kernel references

Exploit chain

evil PC Card without CISTPL_VERS_1 -> socket insert event -> pccard_attach_card (pccard.c:178) -> verbatim CIS parse (no error) -> child created + probed -> devadded OR devnomatch OR later unprivileged devinfo -v/sysctl hw.bus.devices... -> bus_child_pnpinfo_str -> pccard_child_pnpinfo_str -> pccard_safe_quote(NULL) -> kernel-mode read at NULL+0 -> panic (local/physical DoS; re-triggerable at will by any local user while the card is seated). No privilege boundary is crossed: the fault is a fixed read at NULL, not a controllable mapping.

Environment

Guest: DragonFly dfbsd 6.5-DEVELOPMENT #0: Thu Jul 2 06:02:54 UTC 2026 x86_64, cc 8.3 [DragonFly] (env.txt). Artifacts: run.log (decisive baseline, all 7 scenarios), run.2.log/run.3.log (determinism), disasm_proof.txt, build.log, extract2_manifest.txt (verbatim proof), matrix.log (A/B), postboot_run.log (running-kernel source), fixes/df2318.diff (validated).

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

VALIDATED. (1) Single-fix rig A/B (matrix.log): only the three no_vers1 scenarios change -- clean exit 0, devadded prints sane manufacturer=0x1234 product=0x5678 cisvendor="" cisproduct="" strings; busypanic (DF-5355) and reentry_leak (DF-5356) unchanged under the f2318 variant; negative controls green. (2) Combined three-fix kernel: -Werror nativekernel rc=0, installkernel rc=0, booted kernel #1; rig re-extracted from the RUNNING kernel's patched source (--allow-patched) and re-driven: every scenario clean (postboot_run.log). In-kernel behavioral test impossible (no PCMCIA bridge) -- DF-1040/1041/2319 precedent.

fix.diff; matrix.log; postboot_extract_manifest.txt; postboot_run.log; kernel_apply.log; kernel_build.tail.log
↓ fix.diffper-fix-DF-2318

Confirmed kernel references

Detail

Exploit chain

evil PC Card without CISTPL_VERS_1 -> socket insert event -> pccard_attach_card (pccard.c:178) -> CIS parses with no error, cis1_info stays NULL -> child created+probed -> devadded/devnomatch or later unprivileged devinfo -v / sysctl hw.bus.devices walk -> bus_child_pnpinfo_str -> pccard_child_pnpinfo_str -> pccard_safe_quote(cis0, NULL, 128) -> kernel read at NULL (pccard.c:1001) -> panic. No privilege boundary crossed: fixed read at NULL, no attacker-controlled mapping.

Evidence (decisive lines)

run.log (decisive: PRE line error=0/cis1_info[0]=0x0 + 3x 'RIG SIGSEGV si_addr=0x0'); run.2.log/run.3.log (determinism); disasm_proof.txt (faulting movzbl (%rcx) with rcx from sc+0x110, offsetof printed =0x110); extract2_manifest.txt (byte-identity); build.log; matrix.log (f2318 variant flips exactly the three no_vers1 scenarios to clean exit 0 with cisvendor="" strings); postboot_run.log (combined-fix kernel, running-source re-extraction); env.txt

PoC changes

No seed PoC existed for DF-2318 (pack was missing entirely). Wrote the verbatim lifecycle rig (rig2): machine-extracted pccard.c/subr_bus.c/pccard_cis.c cores, kernel-env shims (device tree + devadded/devnomatch/sysctl-walk surfaces mirroring subr_bus.c, bridge-window bus_alloc_resource model, ledgered kmalloc), CIS builders, and 7 scenarios covering DF-2318/5355/5356 plus two negative controls. Extraction needed the production forward-decl block (pccard.c:83-90), ccr_read/write, the PCCARD_IVAR_* enum and PFF_ENABLED ranges, and the subr_bus prototype block to compile verbatim; byte-identity cross-checked sed-vs-awk.

Verified recommended fix

Guard both pccard_safe_quote calls in pccard_child_pnpinfo_str on cis1_info[i] != NULL (emit empty string otherwise); validated by rig A/B and combined-fix kernel.

Verdict

REPRODUCED at the verbatim-rig level on the DragonFly guest (3/3 deterministic runs); the kernel entry path itself is hardware-gated (no PCMCIA bridge in QEMU -- env.txt). The production pccard.c was machine-extracted BYTE-IDENTICAL from the guest's own /usr/src (extract2.sh; sha256 gate 5b610669...; PCCARD_BYTE_IDENTICAL=yes + SUBR_BUS_BYTE_IDENTICAL=yes in extract2_manifest.txt) and driven through the real entry pccard_attach_card(). A perfectly-legal CIS that omits CISTPL_VERS_1 parses with card.error=0 and a non-empty pf_head (run.log PRE line), so pccard_attach_card proceeds and creates children (pccard.c:205-259); cis1_info[0]/[1] remain NULL (only CISTPL_VERS_1 ever sets them, pccard_cis.c:735-760). Every production pnpinfo surface then faults: devadded on driver-attach success (subr_bus.c:645 via :2114), devnomatch on probe failure (subr_bus.c:673), and the unprivileged hw.bus.devices walk used by devinfo(8)/sysctl -a (subr_bus.c:3899, DEVICE_SYSCTL_PNPINFO at :174 -- the walk was verified live as user maxx on this guest). All three scenarios die with SIGSEGV si_addr=0x0 pinned by disassembly to 'movzbl (%rcx),%r8d' == pccard.c:1001 ('if (*src == '"')') with rcx loaded from sc+0x110 == &sc->card.cis1_info[0] (rig prints offsetof=0x110, exact match; disasm_proof.txt). In kernel context: deterministic panic at card insertion, re-triggerable on demand by any unprivileged local user while the card is seated. Negative control vers1_pnpinfo_ok (same card WITH VERS_1) builds the pnpinfo string cleanly.