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

Unvalidated 32-bit CIS longlink/MFC target address causes wild bus_space_read_1 and kernel panic

Field Value
ID DF-1040
Status new
Severity Medium
CVSS 3.1 CVSS:3.1/AV:P/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H
CWE CWE-125 Out-of-bounds Read; CWE-787 (wild read past mapped resource)
File sys/bus/pccard/pccard_cis.c
Lines 212, 334, 386-437 (esp. 394, 406, 413)
Area bus/pccard (PCMCIA/CardBus CIS parser)
Confidence certain
Discovered 2026-07-14
Reported pending
Known CVE none
CVE match novel

Summary

When the DragonFlyBSD CIS chain walker follows a CISTPL_LONGLINK_A/CISTPL_LONGLINK_C target or a CISTPL_LONGLINK_MFC entry address, it loads an attacker-controlled 32-bit value directly into tuple.ptr and immediately dereferences it via pccard_cis_read_1() with no bounds check. A malicious PCMCIA/CardBus card supplying a large address makes the kernel perform a wild bus_space_read_1 far past the 4096-byte (or 64 KB window) attribute memory mapping, which deterministically page-faults and panics the kernel β€” a reliable local/physical denial of service from untrusted removable-device data.

Root cause

longlink_addr is read verbatim from the card at sys/bus/pccard/pccard_cis.c:212 (longlink_addr = pccard_tuple_read_4(&tuple, 0)) and each mfc[i].addr at pccard_cis.c:334 (mfc[i].addr = pccard_tuple_read_4(&tuple, 1+5*i+1)). Both are full 32-bit card-supplied values stored in u_long longlink_addr (line 107) / u_long addr (line 115).

In the chain-transition loop (pccard_cis.c:386-437), tuple.ptr = longlink_addr (line 394) or tuple.ptr = mfc[mfc_index].addr (line 406) is assigned with no validation, and then line 413 executes:

tuple.code = pccard_cis_read_1(&tuple, tuple.ptr);

pccard_cis_read_1 (sys/bus/pccard/pccardvar.h:255-256) expands to bus_space_read_1(memt, memh, mult*(idx)), i.e. bus_space_read_1(memt, memh, mult*tuple.ptr). With mult=2 (attribute memory, line 393) and tuple.ptr=0xFFFFFFFF, the bus offset is 0x1FFFFFFFE β€” vastly beyond the resource allocated at lines 130-131 (bus_alloc_resource(..., PCCARD_CIS_SIZE=4096, ...)).

The main-loop boundary check at line 164 (tuple.mult * tuple.ptr >= PCCARD_CIS_SIZE - 1 - 32) does not cover this code path: it sits inside the inner tuple-walk loop (line 157), not the chain-transition loop (386), and the wild read at 413 executes before control returns to line 164. On 32-bit platforms the multiplication mult*ptr also overflows to ~0, which would additionally defeat any multiplication-based check.

Threat model & preconditions

  • Attacker position: A malicious 16-bit PC Card or CardBus card (PCMCIA attribute memory is attacker-controlled and hot-pluggable), or any entity that can present crafted CIS to the bridge. No kernel privilege or user account is required.
  • Privileges gained or impact: Kernel page-fault panic β€” full system denial of service. A side effect of the wild read is a single byte of kernel/bridge memory being compared to CISTPL_LINKTARGET (0x13); the value is not stored or returned, so no direct info leak.
  • Required config or capabilities: Default kernel with pccard (or CardBus) configured; physical access to the PCMCIA slot (or a virtual PCMCIA bridge under QEMU).
  • Reachability: pccard_read_cis() (sys/bus/pccard/pccard.c:195) is invoked from pccard_attach_card() on every card insertion, which calls pccard_scan_cis() (line 95). The chain transition is reached as soon as the primary chain terminates (CISTPL_END at line 185, or the line-164 length guard forcing CISTPL_END at line 167). The malicious card need only include a single CISTPL_LONGLINK_A (0x11) / CISTPL_LONGLINK_C (0x12) tuple with a 4-byte address β‰₯ 0x80000000, or a CISTPL_LONGLINK_MFC (0x06) tuple with one entry whose address is out of range.

Proof of concept

PoC source: findings/poc/DF-1040/cis_image.bin and findings/poc/DF-1040/README.md

Craft a minimal CIS attribute-memory image (PCMCIA attribute space is byte-addressed at even offsets with mult=2):

Offset Value Meaning
0x00 0x11 CISTPL_LONGLINK_A code
0x02 0x04 length = 4
0x04 0xFF addr byte 0
0x06 0xFF addr byte 1
0x08 0xFF addr byte 2
0x0A 0xFF addr byte 3 β†’ longlink_addr=0xFFFFFFFF
0x0C 0xFF CISTPL_END

When the kernel parses this, pccard_scan_cis reads longlink_addr = pccard_tuple_read_4(&tuple,0) = 0xFFFFFFFF (line 212). The primary chain terminates at the CISTPL_END at offset 0x0C. The chain-transition loop then runs: tuple.mult = 2 (line 393), tuple.ptr = 0xFFFFFFFF (line 394), and line 413 issues bus_space_read_1(memt, memh, 2*0xFFFFFFFF) β†’ wild read β†’ kernel page fault β†’ panic "fatal trap" / "page fault while in kernel mode".

Build & run

# Flash cis_image.bin into a PCMCIA attribute-memory EEPROM and insert the
# card, OR boot DragonFlyBSD under QEMU with a PCMCIA/PCIC bridge and inject
# the image via a QEMU nvram patch.
# Alternate: a unit test that constructs a struct pccard_tuple with mult=2,
# ptr=0xFFFFFFFF and a fake bus_space_read_1 that faults, then calls the
# chain-transition helper to show the unguarded dereference.

Expected output

Fatal trap 12: page fault while in kernel mode
cpuid = 0; apic id = 00000000
fault virtual address   = 0x1fffffffe
[...]
pccard_scan_cis(...) at pccard_cis.c:413
pccard_read_cis(...) at pccard.c:195
pccard_attach_card(...) at pccard.c:...

Impact

Local/physical denial of service from any hot-pluggable PCMCIA or CardBus card with malicious CIS data. Reliable trigger, no privilege required, no special kernel config beyond the default pccard device. Modern hardware rarely ships with PCMCIA slots, but legacy laptops, embedded systems, and QEMU guests using PCMCIA emulation remain exposed.

Bounds-check the link target against the mapped window before dereferencing it. Use a division-based comparison to avoid multiplication overflow on 32-bit (mult is 1 or 2). Apply in the chain-transition loop after tuple.ptr/tuple.mult are set and before the line-413 read:

--- a/sys/bus/pccard/pccard_cis.c
+++ b/sys/bus/pccard/pccard_cis.c
@@ -408,6 +408,15 @@
            goto done;
        }

+       /*
+        * Validate the link target before dereferencing it.
+        * longlink_addr and mfc[].addr are full 32-bit card-supplied
+        * values; without this check a malicious card makes us
+        * bus_space_read_1() at mult*ptr far outside the CIS mapping
+        * and page-faults the kernel.  Division avoids overflow on
+        * 32-bit platforms where mult*ptr could wrap to ~0.
+        */
+       if (tuple.ptr + 4 >= PCCARD_CIS_SIZE / tuple.mult) {
+           device_printf(dev, "CIS longlink/MFC target %lx out of "
+               "bounds (mult=%lx)\n", (u_long)tuple.ptr,
+               (u_long)tuple.mult);
+           continue;
+       }
+
        /* make sure that the link is valid */
        tuple.code = pccard_cis_read_1(&tuple, tuple.ptr);
        if (tuple.code != CISTPL_LINKTARGET) {

The +4 accounts for needing to read code, length, and the 'C'/'I'/'S' magic (5 bytes total) without leaving the window. continue re-enters the chain loop and tries the next MFC entry or falls through to goto done, matching the existing "invalid link" handling philosophy.

References

  • PC Card Standard, Metaformat Specification, CISTPL_LONGLINK_A/C and CISTPL_LONGLINK_MFC tuples
  • sys/bus/pccard/pccardvar.h:255-280 β€” pccard_cis_read_1 / pccard_tuple_read_* helpers (no length check)
  • FreeBSD sys/dev/pccard/pccard_cis.c β€” shared heritage, same parser structure

Timeline

  • 2026-07-14 Discovered during automated audit.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-1040 Β· 13 files
FileTypeDescriptionSize
cis_oob_harness.c trigger-source userspace logic harness replicating pccard_cis_read_1 offset arithmetic; proves OOB offset and fix correctness 6.4 KB view raw
cis_image.bin trigger-source CIS attribute-memory image: CISTPL_LONGLINK_A addr=0xFFFFFFFF + CISTPL_END (for real PCMCIA HW) 8.0 KB ↓ download
fix.diff suggested-fix overflow-safe two-check bounds guard before pccard_cis.c:413 dereference; supersedes finding proposal (adds coarse check to defeat 32-bit ptr+4 wraparound) 1.2 KB view raw
build.sh build-script cc -O2 -Wall -o cis_oob_harness cis_oob_harness.c 379 B view raw
run.sh run-script ./cis_oob_harness 626 B view raw
harness_run.log run-log harness output on unpatched #0 baseline (userspace; identical on both kernels) 2.0 KB view raw
fix_run.log run-log harness output on fixed #1 kernel; PASS, fix rejects all OOB targets 1.9 KB view raw
fix_build.log build-log full make -j6 nativekernel + make installkernel output, rc=0, pccard_cis.o rebuilt with -Werror 5.7 MB ↓ download
env.txt environment uname #1, cc 8.3, 0 PCMCIA bridges, pccard in GENERIC:198, pccard_scan_cis symbol present 534 B view raw
VERDICT.md verdict full narrative: mechanism, harness proof, impact ceiling, fix validation 9.6 KB ↓ raw
README.md readme original PoC README (CIS image layout, hardware/QEMU paths) 2.6 KB ↓ raw
../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 original PoC README (CIS image layout, hardware/QEMU paths)
↓ download raw

DF-1040 PoC β€” CISTPL_LONGLINK_A wild-read panic

Trigger

CIS attribute-memory image, 14 bytes (pad to 8 KiB with 0xFF):

Offset Value Meaning
0x00 0x11 CISTPL_LONGLINK_A code
0x02 0x04 length = 4
0x04 0xFF addr byte 0
0x06 0xFF addr byte 1
0x08 0xFF addr byte 2
0x0A 0xFF addr byte 3 -> longlink_addr=0xFFFFFFFF
0x0C 0xFF CISTPL_END

Generate the binary:

python3 -c "
import sys
img = bytearray([0xFF]) * (8*1024)
img[0x00] = 0x11   # CISTPL_LONGLINK_A
img[0x02] = 0x04   # length
img[0x04] = 0xFF   # addr byte 0
img[0x06] = 0xFF   # addr byte 1
img[0x08] = 0xFF   # addr byte 2
img[0x0A] = 0xFF   # addr byte 3 -> 0xFFFFFFFF
img[0x0C] = 0xFF   # CISTPL_END
sys.stdout.buffer.write(img)
" > cis_image.bin

How to run

PCMCIA hardware is uncommon on modern systems. Two realistic paths:

  1. Physical: flash cis_image.bin into a PCMCIA attribute-memory EEPROM and insert the card. On DragonFlyBSD with pccard configured, the kernel panics on attach.

  2. QEMU: boot DragonFlyBSD under QEMU with the PCMCIA/PCIC bridge (-device pcic,...) and inject the CIS image via a QEMU nvram patch or a small device-model hook that overrides the attribute-memory reads.

  3. Unit-test (no hardware): construct a struct pccard_tuple with mult=2, ptr=0xFFFFFFFF, and a bus_space_read_1 shim that faults past the 4096-byte mapping; call the chain-transition helper to demonstrate the unguarded dereference. This proves the code path; it does not produce a real kernel panic.

Expected output

Fatal trap 12: page fault while in kernel mode
cpuid = 0; apic id = 00000000
fault virtual address   = 0x1fffffffe
[...]
pccard_scan_cis(...) at pccard_cis.c:413
pccard_read_cis(...)    at pccard.c:195
pccard_attach_card(...) at pccard.c:...

Kernel references

VERDICT.md verdict full narrative: mechanism, harness proof, impact ceiling, fix validation
↓ download raw

DF-1040 β€” VERDICT

Verdict

REPRODUCED (source-level / logic-level); INCONCLUSIVE at runtime β€” fix VALIDATED as not_testable.

The bug is real and confirmed by exhaustive source-level tracing of sys/bus/pccard/pccard_cis.c, plus a userspace logic harness that replicates the exact offset-computation arithmetic of the vulnerable chain-transition loop and proves the PoC's longlink_addr = 0xFFFFFFFF yields a wild bus_space_read_1 byte offset of 0x1FFFFFFFE against a 4096-byte mapping. The bug cannot be triggered at runtime on this audit guest because the KVM/QEMU machine has no PCMCIA/CardBus bridge hardware (pciconf -l reports 0 pccard/cbb/cardbus bridges; pccard is compiled into GENERIC at sys/config/X86_64_GENERIC:198 but no bridge ever attaches, so pccard_attach_card() β†’ pccard_read_cis() β†’ pccard_scan_cis() is never called). This is the documented "latent / needs specific HW" class.

Mechanism (trigger β†’ primitive β†’ effect)

  1. Trigger (card insertion). pccard_attach_card(dev) at sys/bus/pccard/pccard.c:178 calls pccard_read_cis(sc) at line 195, which calls pccard_scan_cis(...) at sys/bus/pccard/pccard_cis.c:95.

  2. Attacker-controlled value loaded. When the chain walker encounters a CISTPL_LONGLINK_A (0x11) / CISTPL_LONGLINK_C (0x12) tuple, it stores the card-supplied 32-bit target verbatim: longlink_addr = pccard_tuple_read_4(&tuple, 0) at pccard_cis.c:212. For CISTPL_LONGLINK_MFC (0x06), each entry's address is likewise loaded verbatim: mfc[i].addr = pccard_tuple_read_4(&tuple, 1+5*i+1) at pccard_cis.c:334. Both are full 32-bit values held in u_long (pccard_cis.c:107 / :115).

  3. Chain transition sets tuple.ptr with no validation. After the primary chain terminates (CISTPL_END), the chain-transition loop at pccard_cis.c:386-437 runs. At line 393-394: c tuple.mult = longlink_common ? 1 : 2; tuple.ptr = longlink_addr; /* card-controlled, unchecked */ and at line 405-406 for the MFC case: c tuple.mult = mfc[mfc_index].common ? 1 : 2; tuple.ptr = mfc[mfc_index].addr; /* card-controlled, unchecked */

  4. Unguarded dereference β†’ wild read β†’ panic. Line 413: c tuple.code = pccard_cis_read_1(&tuple, tuple.ptr); pccard_cis_read_1 (sys/bus/pccard/pccardvar.h:255-256) expands to bus_space_read_1(memt, memh, mult*ptr). With mult=2 (attribute memory) and ptr=0xFFFFFFFF, the bus offset is 0x1FFFFFFFE β€” far outside the PCCARD_CIS_SIZE=4096-byte resource allocated at pccard_cis.c:130-131. The kernel page-faults.

  5. Why the existing boundary check doesn't help. The main-loop check at pccard_cis.c:164 (tuple.mult * tuple.ptr >= PCCARD_CIS_SIZE - 1 - 32) sits inside the inner tuple-walk loop (line 157), not the chain-transition loop (line 386). The wild read at line 413 executes before control ever returns to line 164. Additionally, on 32-bit platforms the multiplication mult*ptr overflows to ~0, which would defeat any multiplication-based check anyway.

The PoC image (cis_image.bin) encodes exactly this: offset 0x00 = 0x11 (CISTPL_LONGLINK_A), 0x02 = 0x04 (length), 0x04/0x06/0x08/0x0A = 0xFFΓ—4 (β†’ longlink_addr = 0xFFFFFFFF), 0x0C = 0xFF (CISTPL_END).

Object-level proof (harness)

Because the guest has no PCMCIA bridge, cis_oob_harness.c replicates the exact offset arithmetic of pccard_cis_read_1 (= mult * ptr) and the chain-transition pointer setup, then confirms:

PoC longlink_A 0xFFFFFFFF mult=2 (attr mem)  mult=2  byte_off=8589934590  OOB  fix_rej=yes
longlink_C 0xFFFFFFFF mult=1 (common mem)    mult=1  byte_off=4294967295  OOB  fix_rej=yes
mfc entry 0xDEADBEEF mult=2                  mult=2  byte_off=7471857118  OOB  fix_rej=yes
mfc entry 0x80000000 mult=1                  mult=1  byte_off=2147483648  OOB  fix_rej=yes
boundary ptr=2044 mult=2 (needs 5 bytes)     mult=2  byte_off=4088        OOB  fix_rej=yes
valid   ptr=100  mult=2                      mult=2  byte_off=200         in   fix_rej=no
valid   ptr=2043 mult=2 (last in-window)     mult=2  byte_off=4086        in   fix_rej=no
valid   ptr=4091 mult=1 (last in-window)     mult=1  byte_off=4091        in   fix_rej=no

Proof of primitive (PoC case):
  longlink_addr (card-supplied, 32-bit) = 0xFFFFFFFF
  tuple.mult (pccard_cis.c:393, attr mem) = 2
  tuple.ptr  (pccard_cis.c:394)           = 0xFFFFFFFF
  bus_space_read_1 byte offset (line 413) = 0x1FFFFFFFE
  mapped window size                      = 4096
  offset exceeds window by                = 8589930494 bytes
  fix.diff rejects this target: YES (continue, no deref)

This proves (a) the bug produces an out-of-bounds offset for any large card-supplied address, and (b) the proposed fix rejects every OOB case while accepting every in-window case.

Impact ceiling

  • Panic / local+physical DoS from any hot-pluggable 16-bit PC Card or CardBus card whose attribute-memory CIS contains a CISTPL_LONGLINK_A/C with a 4-byte address β‰₯ 0x80000000, or a CISTPL_LONGLINK_MFC entry with an out-of-range address. No kernel privilege or user account required β€” only physical access to a PCMCIA slot (or a virtual bridge under QEMU with PCMCIA emulation, which this KVM guest does not provide).
  • No info leak, no memory-corruption write. The wild read returns one byte that is only compared to CISTPL_LINKTARGET (0x13) and discarded; it is never stored or copied to userspace. The primitive is a pure wild-read β†’ page-fault β†’ panic. There is no write primitive and therefore no escalation chain.

Exploit chain

none β€” this is a pure wild-read/OOB-read class, not a write primitive. There is no memory corruption to groom or convert; the only effect is a deterministic kernel page-fault panic (DoS). The Phase-6 escalation requirement does not apply to read-only primitives (valid hard blocker).

PoC changes

  • Added cis_oob_harness.c β€” a userspace C harness that replicates the exact offset arithmetic of pccard_cis_read_1 and the chain-transition pointer setup, and proves both the bug (OOB offset for large addresses) and the fix (correct rejection of all OOB targets, acceptance of all in-window targets). This is the object-level proof for a latent bug that cannot be triggered on this guest (no PCMCIA bridge hardware).
  • Added fix.diff β€” an overflow-safe bounds check inserted in the chain-transition loop at pccard_cis.c just before the line-413 dereference. It supersedes the finding markdown's recommended fix by adding a coarse first check (tuple.ptr >= PCCARD_CIS_SIZE) to defeat 32-bit unsigned wraparound in ptr + 4 (the finding's original ptr + 4 alone would wrap to 3 on a 32-bit platform for ptr = 0xFFFFFFFF, silently passing). The two-check form is correct on both 32-bit and 64-bit.

Fix validation (Phase 8) β€” not_testable

The bug cannot be triggered at runtime on this guest (no PCMCIA bridge hardware; QEMU/KVM does not emulate one), so a runtime before/after panic comparison is impossible on either the unpatched or the patched kernel. Per the procedure this is the not_testable path: I validated that the diff applies cleanly (git apply --check OK; patch -p1 "Hunk #1 succeeded at 409"), compiles cleanly (make -j6 nativekernel rc=0, no errors, pccard_cis.o rebuilt with -Werror), and boots cleanly (make installkernel β†’ reboot β†’ kern.version = 6.5-DEVELOPMENT #1: Tue Jul 14 07:51:38 UTC 2026, guest healthy, ssh up). The harness re-runs identically on the fixed kernel, confirming the fix logic.

The "before/after" contrast is therefore at the source/object level, not a runtime panic comparison: - Before (unpatched #0): pccard_cis.c lines 394/406 set tuple.ptr from a card-controlled 32-bit value; nothing between line 410 and the line-413 dereference bounds-checks it. (Confirmed on the running #0 kernel via grep.) - After (fixed #1): the new guard at pccard_cis.c:425-432 (if (tuple.ptr >= PCCARD_CIS_SIZE || tuple.ptr + 4 >= PCCARD_CIS_SIZE / tuple.mult) { ... continue; }) executes before the line-413 read and rejects any out-of-window target, continuing to the next chain entry. (Confirmed compiled into the #1 kernel that booted.)

Kernel references (confirmed during verification)

fix.diff adds an overflow-safe two-check bounds guard in the chain-transition loop after tuple.ptr/tuple.mult are set and before the line-413 read. Supersedes the finding markdown's proposal: the original ptr + 4 >= PCCARD_CIS_SIZE / mult check alone can be defeated on 32-bit platforms where ptr = 0xFFFFFFFF makes ptr + 4 wrap to 3; the added coarse check ptr >= PCCARD_CIS_SIZE catches that case unconditionally.

Fix verification

not_testable
baseline no→ patch + rebuild →patched clean

not_testable (no PCMCIA HW). Compile+install+boot+harness validated: guard at :425-432 rejects all 5 OOB, accepts all 4 in-window. rc=0 -Werror.

Build KI_DONE rc=0. Boot #1 clean. Harness PASS on #1.
↓ fix.diffDragonFly 6.5-DEVELOPMENT #1: Tue Jul 14 07:51:38 UTC 2026

Confirmed kernel references

Detail

Exploit chain

none -- read-only wild bus_space_read_1, byte compared-and-discarded. No info leak, no write. Phase-6 hard blocker: read-only primitive.

Evidence (decisive lines)

Source trace: :394 ptr=longlink_addr, :406 ptr=mfc[].addr, :413 bus_space_read_1(mult*ptr). Harness: OOB offset=8589934590. Guest: 0 PCMCIA bridges, pccard in GENERIC:198 but no bridge attaches.

PoC changes

Authored from scratch: cis_oob_harness.c (replicates pccard_cis_read_1 offset arithmetic), fix.diff (overflow-safe two-check bounds guard), VERDICT.md, manifest.json.

Verified recommended fix

Insert bounds guard after :410 before :413 deref: if (ptr >= PCCARD_CIS_SIZE || ptr+4 >= PCCARD_CIS_SIZE/mult) { device_printf(...); continue; }. Division (not mult) avoids overflow. Supersedes finding proposal (adds coarse ptr>=SIZE check for 32-bit wraparound). Full diff in findings/poc/DF-1040/fix.diff.

Verdict

INCONCLUSIVE runtime, code-certain. pccard_scan_cis stores card-controlled 32-bit longlink_addr (:394) / mfc[].addr (:406) into tuple.ptr, derefs at :413 via bus_space_read_1(memt,memh,mult*ptr) with no bounds check. longlink_addr=0xFFFFFFFF mult=2 -> byte_off=0x1FFFFFFFE vs 4096-byte window -> page-fault panic. Guest has 0 PCMCIA bridges -> pccard_scan_cis unreachable.