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

bce_rx_intr trusts NIC-DMA'd l2_fhdr_pkt_len with no MCLBYTES bound: heap OOB read up to ~63.5KB past the 2048-byte RX cluster, and unsigned underflow to ~4GB m_len (guaranteed panic)

Summary

The RX completion path takes the received frame's length solely from the l2_fhdr that the NIC itself DMAd into the front of the posted mbuf cluster: len = l2fhdr->l2_fhdr_pkt_len (16-bit, NIC-written); len -= ETHER_CRC_LEN (unsigned underflow for pkt_len<4); m->m_pkthdr.len = m->m_len = len - with no comparison to MCLBYTES (2048) or to the posted BD length anywhere on the path. A faulty or malicious BCM5706-5716 reporting pkt_len=65535 yields an mbuf describing 65531 bytes in a 2048-byte cluster: the stack then reads up to 63507 bytes past the cluster (kernel heap info leak - bytes deliverable to an unprivileged user via bpf/raw socket on bce0; or panic on unmapped page). pkt_len in {0..3} underflows to m_len ~4GB -> immediate panic. Identical pattern filed Medium in pass 1 for seven sibling drivers (DF-1478/1481/1490/1514/1519/1562/1351); no bce finding existed (DB-verified). Linux's bnx2 clamps before skb_put; this driver does not. Requires device-side control of l2_fhdr_pkt_len (hostile PCIe peripheral, firmware bug, erratum); remote attackers cannot set it on a compliant NIC. Guest hardware-absent: honest inconclusive.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-3604 Β· 11 files
FileTypeDescriptionSize
README.md β€” 3.2 KB ↓ raw
VERDICT.md β€” 5.1 KB ↓ raw
trigger.c β€” 2.7 KB view raw
build.sh β€” 93 B view raw
run.sh β€” 152 B view raw
build.log β€” 326 B view raw
run.log β€” 462 B view raw
env.txt β€” 1006 B view raw
fix.diff β€” 1.1 KB view raw
verdict.json β€” 3.9 KB view raw
manifest.json β€” 1.3 KB view raw

DF-3604 β€” bce(4) bce_rx_intr trusts NIC-reported l2_fhdr_pkt_len (no MCLBYTES bound) β†’ kernel heap OOB read / unsigned underflow

  • File: sys/dev/netif/bce/if_bce.c (pass 2, GLM 5.3)
  • Severity: Medium Β· Confidence: certain (code fact) Β· CWE-125 / CWE-191
  • Status: inconclusive (runtime) β€” the audit guest has no Broadcom NetXtreme II hardware; the trigger is device-dependent. Source-level verification is complete and certain.

Build / Run

./build.sh     # cc -o trigger trigger.c  (documentation/canary skeleton)
./run.sh       # guest-side environment proof (module present, no device)

Expected output (on hardware-less guest)

run.sh shows:

  • pci/if_bce registered in the kernel (driver code compiled in),
  • kldload if_bce β†’ "module already loaded or in kernel",
  • NO_BROADCOM_DEVICE / NO_BCE_PCIID β€” only virtio PCI devices,
  • ifconfig -l β†’ vtnet0 lo0,
  • NO_BCE_SYSCTL β€” no attach happened, no hw.bce.* tree exists.

On a guest with a BCM5706–5716 NIC, a device reporting l2_fhdr_pkt_len > MCLBYTES (or < ETHER_CRC_LEN) yields an mbuf whose m_len exceeds the 2048-byte cluster β‡’ heap over-read of up to ~63.5 KB into packets handed to the stack (info leak) or an immediate panic (underflow case / unmapped-page walk).

Root cause (path:line)

Why this matters even though the NIC is "trusted"

A compliant NIC cannot DMA more than the posted BD length, but the driver's job on the receive path is to not hand the stack an mbuf that describes more data than the buffer holds. One faulty firmware push, a malicious peripheral (PCIe attach / Thunderclap class), or a chip erratum converts this into a kernel heap info leak (leaked bytes are deliverable to an unprivileged user via a raw socket / pcap) or a trivial panic (underflow). The DragonFly tree itself treats the identical pattern as a finding in every comparable driver (pass-1 DF-1478 my, DF-1481 vr, DF-1490 tx, DF-1514 ste, DF-1519 lge, DF-1562 pcn, DF-1351 nfe β€” all "NIC length trusted without MCLBYTES bound", all Medium). Linux's sibling driver (bnx2) clamps before skb_put; this driver does not.

Fix

fix.diff β€” drop the frame (count ierrors, recycle the buffer) when pkt_len is outside [sizeof(l2_fhdr) + ETHER_ALIGN + ETHER_CRC_LEN, MCLBYTES], before the subtraction. Zero-cost in the common case (two compares).

VERDICT.md
↓ download raw

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.

Fix verification

not_testable
baseline no→ patch + rebuild →patched clean

fix.diff adds the MCLBYTES/minimum clamp using the file's own error-recovery idiom (ierrors + bce_setup_rxdesc_std + goto bce_rx_int_next_rx, cf. if_bce.c:4402-4414). Cannot be runtime-validated: the guest has no BCM5706-5716 device, so neither baseline nor patched behavior is exercisable (same reason the PoC is inconclusive). The patch touches only existing symbols on an already-compiled path.

fix.diff; baseline absence proof in run.log (NO_BROADCOM_DEVICE / NO_BCE_PCIID)
↓ fix.diffper-fix-DF-3604

Confirmed kernel references

Detail

Exploit chain

On real hardware: (1) device DMAs RX frame with l2_fhdr_pkt_len=65535 and clear error bits into a posted 2048B cluster; (2) driver hands stack mbuf with m_len=65531; (3) unprivileged user with a bpf/raw socket on bce0 reads up to ~63.5KB of adjacent kernel heap (info leak); pkt_len<4 variant gives m_len~4GB -> immediate panic (local DoS). No write primitive from this bug alone (NIC DMA bounded by posted BD len).

Evidence (decisive lines)

run.log (guest proof: driver present in kernel, zero Broadcom devices, no attach), VERDICT.md (full path:line trace), trigger.c (annotated hardware precondition), fix.diff (drop-frame clamp)

PoC changes

Seed sketch rewritten into an annotated hardware-precondition trigger + canary skeleton; the PoC as originally sketched (syscall-level) is meaningless for a DMA-device bug; honest hardware-absent verification substituted per contract.

Verified recommended fix

In bce_rx_intr, drop and recycle the frame when l2_fhdr_pkt_len is outside [sizeof(l2_fhdr)+ETHER_ALIGN+ETHER_CRC_LEN, MCLBYTES] before using it (see fix.diff).

Verdict

Source-level certain: bce_rx_intr (sys/dev/netif/bce/if_bce.c:4396-4439) sets m->m_pkthdr.len = m->m_len from the NIC-DMA'd l2_fhdr_pkt_len after only 'len -= ETHER_CRC_LEN' (unsigned underflow for pkt_len<4) and an 18-byte m_adj, with no MCLBYTES(2048) bound anywhere on the path β€” a faulty/malicious BCM5706-5716 can hand the stack an mbuf describing up to 65531 bytes in a 2048-byte cluster (heap OOB read / info leak) or ~4GB (guaranteed panic). Runtime verification is honestly inconclusive: the audit guest has virtio-net only; bce code is in the running kernel (kldstat -v shows pci/if_bce) but no Broadcom PCI device exists, so bce_attach/bce_rx_intr are unreachable (run.log: NO_BROADCOM_DEVICE, NO_BCE_PCIID, ifconfig=vtnet0 lo0, no hw.bce.* sysctls). Guest /usr/src if_bce.c is byte-identical to the audited tree (md5 786cd4c4a8e26f5eab0c12c3987f68b9).