NULL pointer dereference in CISTPL_CFTABLE_ENTRY handler when no CISTPL_CONFIG precedes it
Summary
pccard_parse_cis_tuple dereferences state->default_cfe (NULL) at :907 when CISTPL_CFTABLE_ENTRY (0x1B) tuple is parsed before any CISTPL_CONFIG tuple. default_cfe initialized NULL by bzero at :81 only set non-NULL inside CISTPL_CONFIG case at :867. CFTABLE_ENTRY case has NO top-of-case guard immediately reaches if(num!=state->default_cfe->number) at :907 dereferencing NULL. Second related NULL deref at :916 STAILQ_INSERT_TAIL when state->pf is NULL reachable after CISTPL_END resets pf=NULL at :648. Attacker: physical PC Card/CardBus insertion malicious PCI bridge or PCI passthrough into VM. Crafted CIS begins with CFTABLE_ENTRY before CONFIG. Reliable local DoS deterministic kernel panic no user account required.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-2319 Β· 24 files| File | Type | Description | Size | |
|---|---|---|---|---|
| README.md | β | 1.3 KB | β raw | |
| VERDICT.md | β | 10.1 KB | β raw | |
| extract.sh | β | 4.2 KB | view raw | |
| rig_shims.h | β | 7.6 KB | view raw | |
| rig_main.c | β | 16.7 KB | view raw | |
| run_matrix.sh | β | 2.4 KB | view raw | |
| build.sh | β | 104 B | view raw | |
| run.sh | β | 110 B | view raw | |
| build.log | β | 2.0 KB | view raw | |
| run.log | β | 837 B | view raw | |
| fixed_run.log | β | 1.5 KB | view raw | |
| baseline_run.log | β | 4.9 KB | view raw | |
| matrix.log | β | 64.1 KB | view raw | |
| fix.diff | β | 612 B | view raw | |
| extract_manifest.txt | β | 1.4 KB | view raw | |
| postboot_extract_manifest.txt | β | 1.5 KB | view raw | |
| postboot_run.log | β | 6.8 KB | view raw | |
| env.txt | β | 997 B | view raw | |
| kernel_apply.log | β | 65 B | view raw | |
| kernel_build.tail.log | β | 2.6 KB | view raw | |
| verdict.json | β | 5.5 KB | view raw | |
| manifest.json | β | 1.7 KB | view raw | |
| kernel_build.log | β | 5.7 MB | β download | |
| disasm_proof.txt | β | 520 B | view raw |
DF-2319 -- NULL pointer dereference in CISTPL_CFTABLE_ENTRY handler when no CISTPL_CONFIG precedes it
File: sys/bus/pccard/pccard_cis.c (verify pass, GLM 5.3). Full narrative: VERDICT.md.
Reproduce (verbatim production parser, guest toolchain, no PCMCIA HW needed)
./build.sh # sh extract.sh . && cc -O2 -Wall -o rig rig_main.c ./run.sh # baseline scenarios: ./rig nullderef_a && ./rig nullderef_b sh run_matrix.sh # A/B: baseline vs each single-fix variant, all scenarios
Expected decisive output (DF-2319): BASELINE (pristine parser): nullderef_a -> SIGSEGV si_addr=0x0, child_exit=91; nullderef_b -> SIGSEGV si_addr=0x30, child_exit=91. WITH fix.diff (f2319 variant): both return normally, card->error=1, child_exit=0; all other scenarios (incl. benign valid card) unchanged.
What it proves
The rig executes the byte-identical production parser extracted from the
guest's /usr/src (sha256-gated; extract_manifest.txt) through the real entry
pccard_read_cis(). Kernel entry is hardware-gated (no PCMCIA bridge in
QEMU) -- see VERDICT.md for the method, the A/B matrix, and the
booted-combined-fix-kernel validation.
Recommended fix: Guard the CFTABLE_ENTRY case: require state->default_cfe and state->pf non-NULL (error++ + break otherwise). (fix.diff; validated, see fixed_run.log / matrix.log / postboot_run.log).
DF-2319 -- VERDICT
Verdict in one line
REPRODUCED (verbatim-rig level; deterministic) -- impact: panic.
The defect manifests in the byte-identical production
sys/bus/pccard/pccard_cis.c code executed on the DragonFly guest; the
kernel entry path itself is hardware-gated (no PCMCIA bridge in QEMU).
REPRODUCED at the verbatim-rig level on the DragonFly guest; kernel entry is hardware-gated (no PCMCIA bridge in QEMU -- proven in env.txt). The production parser was machine-extracted BYTE-IDENTICAL from the guest's own /usr/src/sys/bus/pccard/pccard_cis.c (extract.sh; sha256 gate: acdd2484...; manifest in pack) and driven through the real entry pccard_read_cis(). Two deterministic NULL dereferences, both pinned by disassembly to the exact claimed statements inside pccard_parse_cis_tuple: (A) CFTABLE_ENTRY as the FIRST tuple of the primary chain: state is bzero'd (pccard_cis.c:81) so default_cfe stays NULL until CISTPL_CONFIG (:867); the CFTABLE case (:871) has NO guard and evaluates 'num != state->default_cfe->number' (:907) -> SIGSEGV si_addr=0x0 at pccard_parse_cis_tuple+0x437, instruction 'cmp %ecx,(%rax)' with rax=state->default_cfe=NULL (disasm_proof.txt) -- in kernel context a read at NULL == deterministic panic at card insertion. (B) after the LONGLINK_MFC reset at CISTPL_END (:635-649) sets state->pf=NULL (:648), a CFTABLE_ENTRY in the next MFC chain reaches STAILQ_INSERT_TAIL(&state->pf->cfe_head,...) (:916) with pf==NULL -> SIGSEGV si_addr=0x30 == offsetof(pccard_function,cfe_head)=40 + offsetof(stqh_last)=8 (rig prints the offsets; second instruction in disasm_proof.txt). Fix validated two ways: single-fix rig A/B (both scenarios clean, nothing else changes) and a combined-five-fix kernel built with -Werror, installed, booted (kernel #1, sha256 in env.txt); the rig re-extracted from the RUNNING kernel's patched source parses both crashing cards without any fault (postboot_run.log).
Method -- the verbatim rig (why this is production code, not a model)
The guest has no PCMCIA/CardBus bridge (env.txt: no /dev/pccard, no pccardc,
0 pccard dmesg lines), so pccard_scan_cis cannot be entered at runtime by
card insertion. Instead of hand-replicating the parser, the rig machine-
extracts the production parser byte-identical from the guest's own kernel
source* and executes it:
extract.shsed-extracts, from/usr/src/sys/bus/pccard/pccard_cis.c, lines 51-444 (DPRINTF macros,PCCARD_CIS_SIZE,struct cis_state,pccard_read_cis,pccard_scan_cis) and 612-1294 (pccard_parse_cis_tuple,decode_funce), plus the needed struct/macro ranges ofpccardvar.h(tuple struct, read macros, A_MEM flags) and the constant headerspccard_cis.h/pccardreg.hwhole.- The extraction is sha256-gated: all four pccard files plus
sys/sys/queue.hmust match the audited pristine tree (acdd2484...for pccard_cis.c);extract_manifest.txtin this pack is the guest-generated record. The userland<sys/queue.h>is hash-identical to the kernel's, so the STAILQ macros the parser expands are the production ones. rig_shims.h(the only hand-written code that touches the parse path) emulates the kernel environment:bus_alloc_resourcereturns a 4096-byte mapping (PCCARD_CIS_SIZE) inside a 64 KiB guard buffer -- [4096,65536) is a 0xA5 canary zone standing in for whatever the bridge maps adjacent to the CIS window, and offsets >= 65536 are recorded but never dereferenced (the userland analogue of a CardBus master abort);CARD_SET_RES_FLAGSswaps attribute/common images;kmalloc/kfreeare canary-banded and ledgered. Everybus_space_read_1is counted and bounds-checked against the real mapping size.rig_main.cbuilds crafted CIS images (attribute-memory spacing: tuple at index t has code at bus offset 2t, length at 2t+2, body byte i at 2t+4+2i, per the verbatimpccard_cis_read_1/pccard_tuple_read_1macros, pccardvar.h:255-280) and calls the real entry pointpccard_read_cis(&sc)in a forked, crash-instrumented child.
So the code that crashes / leaks / loops below is the exact
sys/bus/pccard/pccard_cis.c text that ships in the guest's kernel, compiled
on the guest toolchain (cc 8.3).
A/B discipline
run_matrix.sh builds six rig variants from the SAME pristine extraction --
baseline plus one per fix, each fix applied ONLY to a copy of the extracted
file (patch -p1, applied_fix.diff shows only the fix hunk) -- and runs
every scenario under every variant. Each fix flips exactly its own scenario
and leaves every other scenario (including the negative controls) untouched;
see matrix.log and the summary table in this file.
matrix.log summary (baseline + each single-fix variant)
| scenario | baseline | f2319 | f2320 | f5361 | f5362 | f5363 |
|---|---|---|---|---|---|---|
| nullderef_a | SEGV si_addr=0x0 | ret clean | SEGV | SEGV | SEGV | SEGV |
| nullderef_b | SEGV si_addr=0x30 | ret clean | SEGV | SEGV | SEGV | SEGV |
| wildlink_long | OOB=1 @16777216 | same | OOB=0 | same | same | same |
| wildlink_mfc | OOB=2 max 67108864 | same | OOB=0 | same | same | same |
| mfcleak | LIVE cfe=2 leaked | same | same | LIVE cfe=0 | same | same |
| livelock | NONTERM (periodic) | same | same | same | ret (hop cap) | same |
| leakloop | NONTERM, cfe grows | same | same | same | ret, cfe=64 | same |
| checksum | OOB=2045 max 8184 | same | same | same | same | OOB=0 |
| checksum_ok | OOB=0 | same | same | same | same | same |
| benign (valid) | clean, LIVE cfe=3 | clean | clean | LIVE cfe=2 | clean | clean |
Kernel-level fix validation (combined five-fix kernel)
- All five fix.diffs
patch -p1-applied to the guest's/usr/src(kernel_apply.log; patched pccard_cis.c sha25667334ef6...). make -j6 nativekernel KERNCONF=X86_64_GENERIC-- rc=0, 38k-line log;pccard_cis.crecompiled under-Werrorin both kernel and KLD_MODULE contexts (full log in DF-2319's pack, tail here: kernel_build.tail.log).make installkernel-- rc=0; rebooted into6.5-DEVELOPMENT #1: Wed Oct 14 13:35:33 UTC 2026, kernel sha256c73653c698f0f6149208e4e9b1cf536769430529384fe219b37c0b393e8ee01a(env.txt).- All five DF markers verified present in the RUNNING kernel's source; the
rig re-extracted from that patched tree (extract.sh reports
SOURCE_STATE=PATCHED, postboot_extract_manifest.txt) and re-driven: every scenario clean -- no SIGSEGV, no OOB read, no non-termination, leaks drained, negative controls still pass (postboot_run.log). - Guest reset to the
with-srcclean-source snapshot afterwards; pristine sha256acdd2484...re-verified (guest left up and clean).
Note: in-kernel behavioral exercise of the parser is impossible on this guest (no PCMCIA bridge) -- the same gating as the DF-1040/DF-1041 precedent; the booted-kernel validation above proves compile+boot+source-of-running-kernel, and the rig (verbatim same text) proves behavior.
Faulting instructions (disasm_proof.txt)
nullderef_a -- fault inside pccard_parse_cis_tuple (0x401bf0..0x405790):
402018: and $0x3f,%ecx ; num = reg & PCCARD_TPCE_INDX_NUM_MASK (:885) 40201b: mov 0xa8(%rax),%rax ; rax = state->default_cfe (NULL) 402027: cmp %ecx,(%rax) ; :907 num != state->default_cfe->number <-- FAULT
nullderef_b -- same function:
4025e3: mov 0xb8(%rdi),%eax ; ... state->pf (NULL)
4025e9: mov ...(%rax),.. ; :916 STAILQ_INSERT_TAIL reads pf->cfe_head.stqh_last
; at pf+0x28+8 = 0x30 <-- FAULT
si_addr=0x0 / 0x30 match the source-level analysis exactly (rig prints offsetof(pccard_function, cfe_head)=40; stqh_last sits at +8).
Fix (fix.diff -- validated)
Guard the CFTABLE_ENTRY case: require state->default_cfe and state->pf non-NULL (error++ + break otherwise).
- rig A/B: fixed_run.log (this pack) + matrix.log -- only this finding's scenarios change; negative controls (checksum_ok, benign valid card) stay green under every fix variant.
- kernel: see "Kernel-level fix validation" above.
Kernel references
sys/bus/pccard/pccard_cis.c:81sys/bus/pccard/pccard_cis.c:635sys/bus/pccard/pccard_cis.c:641sys/bus/pccard/pccard_cis.c:644sys/bus/pccard/pccard_cis.c:648sys/bus/pccard/pccard_cis.c:867sys/bus/pccard/pccard_cis.c:871sys/bus/pccard/pccard_cis.c:907sys/bus/pccard/pccard_cis.c:916sys/bus/pccard/pccard.c:195
Exploit chain
evil PC Card/CardBus insert -> pccard_attach_card (socket event, pccard.c:195) -> pccard_read_cis -> pccard_scan_cis -> pccard_parse_cis_tuple(CISTPL_CFTABLE_ENTRY) -> NULL deref (:907 read at NULL / :916 access at NULL+0x30) -> kernel-mode page fault on the socket thread -> panic (local/physical DoS). No privilege boundary is crossed: the faults are at fixed NULL-page offsets only; there is no attacker-controlled mapping of the NULL page from the card-attach path.
Environment
Guest: DragonFly 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 (audits) / booted fixed kernel #1 for fix validation (env.txt). cc 8.3 [DragonFly]. Artifacts: extract_manifest.txt (verbatim proof), run.log (baseline decisive), fixed_run.log (fix A/B), baseline_run.log (full pristine run), matrix.log (6x11 A/B), postboot_run.log (running-kernel source), env.txt, build.log.
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
fixedVALIDATED. (1) Single-fix rig A/B: guard at the top of the CFTABLE_ENTRY case -> both crash scenarios return normally (card->error=1), applied_fix.diff shows only the guard hunk, all 9 other scenarios unchanged incl. benign/checksum_ok. (2) Combined five-fix kernel: -Werror build rc=0, installkernel rc=0, booted kernel #1 (sha256 c73653c6...), markers present in the running source, rig re-extracted from it: no fault. In-kernel behavioral test impossible (no PCMCIA bridge) -- DF-1040/1041 precedent.
fix.diff; fixed_run.log; matrix.log (single-fix A/B); kernel_apply.log + kernel_build.tail.log + env.txt + postboot_extract_manifest.txt + postboot_run.log (combined-fix kernel build/boot/running-source rerun)
Confirmed kernel references
- sys/bus/pccard/pccard_cis.c:81
- sys/bus/pccard/pccard_cis.c:635
- sys/bus/pccard/pccard_cis.c:641
- sys/bus/pccard/pccard_cis.c:644
- sys/bus/pccard/pccard_cis.c:648
- sys/bus/pccard/pccard_cis.c:867
- sys/bus/pccard/pccard_cis.c:871
- sys/bus/pccard/pccard_cis.c:907
- sys/bus/pccard/pccard_cis.c:916
- sys/bus/pccard/pccard.c:195
Detail
Exploit chain
evil PC Card/CardBus insert -> pccard_attach_card (socket event, pccard.c:195) -> pccard_read_cis -> pccard_scan_cis -> pccard_parse_cis_tuple(CISTPL_CFTABLE_ENTRY) -> NULL deref (:907 read at NULL / :916 access at NULL+0x30) -> kernel-mode page fault on the socket thread -> panic (local/physical DoS). No privilege boundary is crossed: the faults are at fixed NULL-page offsets only; there is no attacker-controlled mapping of the NULL page from the card-attach path.
Evidence (decisive lines)
run.log (both scenarios: 'SIGSEGV si_addr=0x0' and 'si_addr=0x30', child_exit=91); disasm_proof.txt ('cmp %ecx,(%rax)' with rax=0 == :907; pf==NULL stqh_last load at +0x30 == :916); fixed_run.log (f2319: both return normally, card->error=1); matrix.log (only f2319 flips these scenarios); postboot_run.log (running fixed kernel's source: no fault)
PoC changes
Pack created fresh (no seeded rig). Authored the verbatim rig (extract.sh + rig_shims.h + rig_main.c) and the two crash scenarios; 2 build iterations on the guest (DragonFly mcontext field name), then deterministic reproduction across two independent boots (identical faulting RIPs 0x402027/0x4025e9).
Verified recommended fix
Guard the CFTABLE_ENTRY case: require state->default_cfe and state->pf non-NULL (error++ + break otherwise).
Verdict
REPRODUCED at the verbatim-rig level on the DragonFly guest; kernel entry is hardware-gated (no PCMCIA bridge in QEMU -- proven in env.txt). The production parser was machine-extracted BYTE-IDENTICAL from the guest's own /usr/src/sys/bus/pccard/pccard_cis.c (extract.sh; sha256 gate: acdd2484...; manifest in pack) and driven through the real entry pccard_read_cis(). Two deterministic NULL dereferences, both pinned by disassembly to the exact claimed statements inside pccard_parse_cis_tuple: (A) CFTABLE_ENTRY as the FIRST tuple of the primary chain: state is bzero'd (pccard_cis.c:81) so default_cfe stays NULL until CISTPL_CONFIG (:867); the CFTABLE case (:871) has NO guard and evaluates 'num != state->default_cfe->number' (:907) -> SIGSEGV si_addr=0x0 at pccard_parse_cis_tuple+0x437, instruction 'cmp %ecx,(%rax)' with rax=state->default_cfe=NULL (disasm_proof.txt) -- in kernel context a read at NULL == deterministic panic at card insertion. (B) after the LONGLINK_MFC reset at CISTPL_END (:635-649) sets state->pf=NULL (:648), a CFTABLE_ENTRY in the next MFC chain reaches STAILQ_INSERT_TAIL(&state->pf->cfe_head,...) (:916) with pf==NULL -> SIGSEGV si_addr=0x30 == offsetof(pccard_function,cfe_head)=40 + offsetof(stqh_last)=8 (rig prints the offsets; second instruction in disasm_proof.txt). Fix validated two ways: single-fix rig A/B (both scenarios clean, nothing else changes) and a combined-five-fix kernel built with -Werror, installed, booted (kernel #1, sha256 in env.txt); the rig re-extracted from the RUNNING kernel's patched source parses both crashing cards without any fault (postboot_run.log).
No comments yet.