# 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)

- `sys/bus/pccard/pccard_cis.c:212` — `longlink_addr` read verbatim from card
- `sys/bus/pccard/pccard_cis.c:334` — `mfc[i].addr` read verbatim from card
- `sys/bus/pccard/pccard_cis.c:394` — `tuple.ptr = longlink_addr` (no check)
- `sys/bus/pccard/pccard_cis.c:406` — `tuple.ptr = mfc[mfc_index].addr` (no check)
- `sys/bus/pccard/pccard_cis.c:413` — wild `bus_space_read_1` dereference
- `sys/bus/pccard/pccard_cis.c:164` — existing inner-loop check (does NOT cover this path)
- `sys/bus/pccard/pccard_cis.c:130-131` — `PCCARD_CIS_SIZE=4096` resource allocation
- `sys/bus/pccard/pccardvar.h:255-256` — `pccard_cis_read_1` macro = `bus_space_read_1(memt, memh, mult*idx)`
- `sys/bus/pccard/pccard.c:195` — `pccard_read_cis(sc)` entry from `pccard_attach_card`
- `sys/config/X86_64_GENERIC:198-200` — `pccard`/`cardbus`/`cbb` compiled into GENERIC

## Recommended fix

`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.
