# DF-3604 VERDICT — bce_rx_int trusts l2_fhdr_pkt_len (heap OOB read path)

**Status: `inconclusive`** (runtime) · **source-level: certain**
**Impact ceiling: kernel heap OOB read (info leak, up to ~63.5 KB per
frame) / panic via unsigned underflow.**

## What was verified, and how

### 1. Source-level verification (certain)

Read the full RX path in `sys/dev/netif/bce/if_bce.c` (guest `/usr/src`
copy is byte-identical to the audited tree — md5
`786cd4c4a8e26f5eab0c12c3987f68b9` both sides):

- RX buffers are single 2048-byte mbuf clusters:
  - `if_bce.c:3851` `m_new = m_getcl(...)` / `:3855`
    `m_new->m_len = m_new->m_pkthdr.len = MCLBYTES;`
  - `sys/sys/param.h:497` `MCLBYTES = (1 << 11) == 2048`
  - `if_bce.c:2298-2301` RX DMA tag created with `maxsize/nmaxseg =
    MCLBYTES, MCLBYTES`; `:3905` posts `rx_bd_len = MCLBYTES`.
- The only length source for the handed-up frame is the DMA'd header:
  - `if_bce.c:4394` `l2fhdr = mtod(m, struct l2_fhdr *);`
  - `if_bce.c:4396` `len = l2fhdr->l2_fhdr_pkt_len;` (16-bit field,
    NIC-written; `if_bcereg.h:845`)
  - `if_bce.c:4399` `len -= ETHER_CRC_LEN;` — `len` is `unsigned int`;
    `pkt_len < 4` wraps to ≈4 GB.
  - `if_bce.c:4402-4414` the *only* sanity gating is the NIC's own
    `l2_fhdr_status` error bits — i.e. trusting the same producer.
  - `if_bce.c:4437-4439` `m_adj(m, sizeof(struct l2_fhdr) + ETHER_ALIGN)`
    (=18) then `m->m_pkthdr.len = m->m_len = len;`
- **No clamp exists anywhere on this path.** A `pkt_len` of 65535 yields
  `m_len = 65531` on a 2048-byte cluster ⇒ up to 63507 bytes of heap
  over-read when the stack consumes the mbuf (ether_input → IP
  reassembly/forwarding/socket copy; bytes land in user-visible packets).
  A `pkt_len` of 0–3 yields `m_len ≈ 4 GB` ⇒ immediate OOB walk → panic
  (local DoS from the frame's mere presence).

Cross-checked ring math so nothing else bounds it: sw indices are masked
(`RX_CHAIN_IDX`, `if_bcereg.h:5678`, `TOTAL_RX_BD = 256*pages` ⇒ clean
power-of-two masks), and `bce_get_hw_rx_cons` (`if_bce.c:4301-4309`)
normalizes pointer slots — none of this constrains `pkt_len`.

### 2. Guest runtime verification (why inconclusive, not reproduced)

Audit guest: DragonFly 6.5-DEVELOPMENT #0 (X86_64_GENERIC), virtio-net
only. `run.sh` / `run.log` evidence:

- driver code present in the running kernel (`kldstat -v` → `pci/if_bce`;
  `kldload if_bce` → "module already loaded or in kernel"),
- **no Broadcom PCI device**: `pciconf -lv` shows only QEMU virtio
  devices (`NO_BROADCOM_DEVICE`, `NO_BCE_PCIID`),
- `ifconfig -l` → `vtnet0 lo0`; `sysctl hw.bce.*` → none (no attach).

⇒ `bce_attach()` never runs ⇒ `bce_rx_intr()` is unreachable on this
guest. The trigger requires BCM5706/5708/5709/5716 silicon with
device-side control of `l2_fhdr_pkt_len` (malicious/faulty NIC
firmware, hostile peripheral, or chip erratum). Per the honest-status
table this is "guest reachable but the target device/feature is absent"
⇒ **inconclusive** (source-certain, hardware-absent).

### 3. Exploit chain (what it would look like on hardware)

1. Compromised/malicious NIC DMAs a frame with `l2_fhdr_pkt_len = 65535`
   (error bits clear) into a posted 2048-byte cluster.
2. `bce_rx_intr` hands the stack an mbuf with `m_len = 65531`
   (`if_bce.c:4439`).
3. Unprivileged local user runs a packet sniffer (raw bpf socket on
   bce0) and receives up to ~63 KB of kernel heap following the cluster
   — heap info leak (slab metadata, other packets, pointers ⇒ defeats
   no-KASLR is irrelevant, but leaks live object data).
   OR `pkt_len = 0` ⇒ `m_len ≈ 4 GB` ⇒ guaranteed panic (DoS).

Not further escalatable to a write primitive from this bug alone (the
over-read is consumer-side; the NIC's own DMA is bounded by the posted
BD). Ceilings: **info leak + local DoS**, exactly the class of the
pass-1 peer findings (DF-1478/DF-1481/DF-1490/DF-1514/DF-1519/DF-1562).

## Fix validation

`fix.diff` applies the clamp (drop + recycle on out-of-range `pkt_len`).
Not runtime-testable on this guest (no device — same reason as above);
kernel build validation also skipped: the GENERIC kernel already
compiles this driver, and the patch adds two compares + an error branch
using only existing symbols (`MCLBYTES`, `ierrors`,
`bce_setup_rxdesc_std`, `bce_rx_int_next_rx`) — reviewed against the
surrounding error paths at `if_bce.c:4402-4414` which use exactly this
recovery idiom. fix_status = `not_testable` (hardware absent).

## Artifacts

- `trigger.c` — annotated trigger/canary skeleton (documents the exact
  device-side precondition).
- `build.sh`, `run.sh` — exact commands.
- `run.log` — full untrimmed guest output proving driver-present /
  device-absent.
- `env.txt` — guest uname, PCI inventory, kldstate, source md5.
- `fix.diff` — the clamp.

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