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

bnx_rxeof trusts NIC-DMA'd return-ring bge_len with no MCLBYTES bound: heap OOB read / kernel info leak up to ~63.5KB past the 2048-byte RX cluster

Summary

bnx_rxeof assigns m->m_pkthdr.len = m->m_len = cur_rx->bge_len - ETHER_CRC_LEN straight from the return-ring descriptor the NIC fills via DMA, with no upper bound against the 2046-byte usable cluster and no lower bound. A faulty or malicious BCM5717/5719/5720/577xx writing bge_len=65535 hands the stack an mbuf describing 65531 bytes in a 2048-byte cluster; ether_input's ETHER_BPF_MTAP copies the bogus length to bpf listeners (kernel heap disclosure) and IP-stack parsing walks the same OOB range (fault/panic on unmapped pages). The bge_len<18 variant yields a negative m_len that if_ethersubr drops harmlessly - no underflow panic on DragonFly, unlike the bce sibling DF-3604. Hardware-side clamps (BD len 2046, RX_MTU 1522, RCB 1536) are all device-controlled and thus not a defense. The pass-1 sibling sweep and pass-2 bce never covered bnx (DB-verified) - not a re-report. Requires a NIC that writes an out-of-range bge_len (buggy firmware/silicon, or malicious PCIe/DMA peripheral); on such a device an unprivileged bpf listener reads up to ~63.5KB of adjacent kernel heap per frame. Guest hardware-absent: inconclusive, source-certain.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-3639 Β· 10 files
FileTypeDescriptionSize
README.md β€” 1.7 KB ↓ raw
VERDICT.md β€” 4.9 KB ↓ raw
trigger.c β€” 3.4 KB view raw
build.sh β€” 117 B view raw
run.sh β€” 231 B view raw
build.log β€” 74 B view raw
run.log β€” 650 B view raw
env.txt β€” 800 B view raw
fix.diff β€” 982 B view raw
verdict.json β€” 4.4 KB view raw

DF-3639 β€” bnx(4) RX length trust (bge_len) β€” evidence pack

What: bnx_rxeof() (sys/dev/netif/bnx/if_bnx.c:2848) assigns m->m_pkthdr.len = m->m_len = cur_rx->bge_len - ETHER_CRC_LEN from the NIC-DMA'd return-ring descriptor with no bound against the 2046-byte usable cluster and no lower bound.

Impact (hardware-conditional): faulty/malicious BCM5717/5719/5720/577xx hands the stack an mbuf describing up to 65531 bytes in a 2048-byte cluster β†’ kernel heap OOB read of up to ~63.5 KB disclosed via bpf (sys/net/if_ethersubr.c:1512) and the IP stack. bge_len < 18 variant is dropped harmlessly by ether_input (if_ethersubr.c:1499-1503).

Reproduce (audit guest β€” honest negative):

  cc -O -Wall -o trigger trigger.c     # guest; builds clean
  ./run.sh                             # via dfbsd-qemu/vm.sh run_root

Expected on the audit guest: trigger prints the hardware precondition; evidence block shows pci/if_bnx in the kernel, NO_BROADCOM_DEVICE, ifconfig -l = vtnet0 lo0, sysctl hw.bnx unknown oid, and /usr/src/sys/dev/netif/bnx/if_bnx.c md5 identical to the audited tree β†’ runtime verdict inconclusive (source-level certainty).

On real hardware: force the device to write bge_len = 65535 (error flag clear) into an RX return-ring BD (modified firmware / malicious peripheral / erratum), then tcpdump -i bnx0 -w out.pcap β€” captured frames exceed 2046 bytes and contain adjacent kernel heap.

Fix: fix.diff (drop out-of-range frames, bounded by the mbuf's own posted length). Not runtime-validatable on this guest (no bnx hardware).

Files: VERDICT.md (full narrative), trigger.c, build.sh/run.sh, build.log/run.log (untrimmed guest transcripts), env.txt, manifest.json, verdict.json.

VERDICT.md
↓ download raw

DF-3639 β€” bnx(4) trusts NIC-DMA'd return-ring bge_len with no buffer bound

File: sys/dev/netif/bnx/if_bnx.c (pass 2, GLM 5.3) Class: kernel heap OOB read / info disclosure (CWE-125 / NIC-trust) Severity filed: Medium, bucket kernleak, confidence certain (source-level) Verdict: inconclusive at runtime β€” the audit guest has no Broadcom hardware; the driver is compiled in but bnx_attach() is unreachable there.

1. Root cause (path:line)

bnx_rxeof() consumes RX return-ring buffer descriptors that the NIC has filled in via DMA into host memory:

struct bge_rx_bd (sys/dev/netif/bge/if_bgereg.h:2327-2346) is the shared host-memory descriptor; its bge_len field is a uint16_t written by the device. The receiving mbuf was allocated as:

  • if_bnx.c:816-822 β€” m_getcl(M_*, MT_DATA, M_PKTHDR) + m_adj(ETHER_ALIGN) => a 2048-byte cluster with 2046 usable bytes.

Between 2842 (bnx_newbuf_std() swaps in a fresh cluster) and 2848 there is no check that bge_len - ETHER_CRC_LEN fits the cluster. There is also no lower bound check (a bge_len < 18 yields a negative m_len).

2. Consequence trace

For a device that writes bge_len = 65535 (firmware compromise, malicious peripheral / Thunderclap-style DMA, or a silicon erratum) with the error flag clear:

  1. m->m_len = m->m_pkthdr.len = 65531 describing a 2048-byte cluster (if_bnx.c:2848).
  2. ifp->if_input(ifp, m, pi, cpuid) (if_bnx.c:2881) β†’ ether_input().
  3. sys/net/if_ethersubr.c:1512 β€” ETHER_BPF_MTAP(ifp, m) copies min(m->m_pkthdr.len, ...) bytes out of the cluster to every attached bpf(4) listener β†’ up to ~63.5 KB of kernel heap adjacent to the RX cluster is disclosed (pointers, stale packet data, etc.).
  4. IP-stack parsing (ip_input) and socket copies walk the same OOB range; crossing an unmapped page faults β†’ panic (local DoS from a driver defect, or crash-for-remote if the OOB range hits a guard page).

For bge_len < ETHER_HDR_LEN + ETHER_CRC_LEN (18): m_len goes negative; DragonFly's ether_input discards it at sys/net/if_ethersubr.c:1499-1503 (m->m_len < sizeof(struct ether_header) β†’ m_freem) β€” a benign drop, no underflow panic on this path (differs from bce DF-3604, where the length is unsigned and the m_adj happens after the assignment).

3. Why hardware bounds don't save you

A healthy NIC clamps frames to the posted BD length (2046, if_bnx.c:843,871), the MAC BGE_RX_MTU = 1522 (if_bnx.c:3620-3621) and the std RCB max frame 1536 (if_bnx.c:1348-1349). All of these are device-side registers/memory the defecting device itself controls; the kernel-side check at 2848 is the missing last line of defense. This is exactly the DF-3604 (bce) pattern; the pass-1 sibling sweep (DF-1478 my, DF-1481 vr, DF-1490 tx, DF-1514 ste, DF-1519 lge, DF-1562 pcn, DF-1351 nfe, DF-1526 sf) never covered bnx (DB-verified), so this is not a re-report.

4. Guest evidence (why inconclusive, honestly)

run.log: - kldstat -v β†’ 61 pci/if_bnx β€” driver compiled into the running kernel. - pciconf -l | grep 14e4 β†’ NO_BROADCOM_DEVICE β€” no Broadcom PCI device. - ifconfig -l β†’ vtnet0 lo0 β€” no bnx interface; sysctl hw.bnx β†’ unknown oid β€” bnx_attach() never executed. - md5 /usr/src/sys/dev/netif/bnx/if_bnx.c = 49a18c4b7642b9b2ebd1d02ad11542e4 == audited tree β†’ the unreachable code is byte-identical to what was audited.

trigger.c builds clean (cc -O -Wall) and runs, printing the hardware precondition. No QEMU emulation of a BCM5717+ exists in this setup, so the DMA-dependent primitive cannot be exercised: verdict inconclusive / reproduced: 0, impact conditional on hostile/faulty hardware.

5. Exploit chain (on real hardware)

device writes bge_len=65535, error-flag clear β†’ driver hands stack an mbuf with m_len=65531 in a 2048B cluster β†’ unprivileged user with a bpf listener on bnx0 (or a TCP/UDP peer receiving forwarded data copied from the OOB range) reads adjacent kernel heap β†’ info leak. No write primitive from this bug alone (the NIC's DMA write is bounded by the posted BD length it honors).

6. Fix

fix.diff β€” drop-and-count when bge_len is outside [ETHER_HDR_LEN + ETHER_CRC_LEN, m->m_len + ETHER_CRC_LEN], using the mbuf's own posted length as the bound (stays correct even if jumbo ring support is ever enabled). Fix is not runtime-validatable on this guest (no bnx hardware): fix_status: not_testable.

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 clamp using the mbuf's own posted length as the bound (correct even if BNX_FLAG_JUMBO is ever enabled). Cannot be runtime-validated: the guest has no BCM5717+ 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 and mirrors the error-drop idiom used at if_bnx.c:2836-2841.

fix.diff; baseline absence proof in run.log (NO_BROADCOM_DEVICE, sysctl hw.bnx unknown oid)
↓ fix.diffper-fix-DF-3639

Confirmed kernel references

Detail

Exploit chain

On real hardware: (1) device DMAs an RX return-ring BD with bge_len=65535 and error flag clear; (2) driver hands the stack an mbuf with m_len=65531 in a 2048B cluster; (3) unprivileged user with a bpf listener on bnx0 reads up to ~63.5KB of adjacent kernel heap (info leak); IP-stack parsing of the same OOB range can also fault (panic/DoS). No write primitive from this bug alone (NIC DMA write is bounded by the posted BD len).

Evidence (decisive lines)

run.log (guest proof: driver in kernel, zero Broadcom devices, no attach, src md5 match), VERDICT.md (full path:line trace incl. the benign underflow path), trigger.c (annotated hardware-precondition trigger, builds clean -O -Wall), fix.diff (drop-frame clamp bounded by the mbuf's own posted length)

PoC changes

No seed existed (new pass-2 finding); trigger written as an annotated hardware-precondition skeleton per the DF-3604 precedent, built and executed on the guest to prove the code path is unreachable there.

Verified recommended fix

In bnx_rxeof, drop the frame (ierrors++ + m_freem) when cur_rx->bge_len is outside [ETHER_HDR_LEN + ETHER_CRC_LEN, m->m_len + ETHER_CRC_LEN] before using it (see fix.diff).

Verdict

Source-level certain: bnx_rxeof (sys/dev/netif/bnx/if_bnx.c:2848) sets m->m_pkthdr.len = m->m_len = cur_rx->bge_len - ETHER_CRC_LEN directly from the NIC-DMA'd return-ring descriptor with no MCLBYTES bound anywhere on the path β€” a faulty or malicious BCM5717/5719/5720/577xx can hand the stack an mbuf describing up to 65531 bytes in a 2048-byte cluster (2046 usable after m_adj(ETHER_ALIGN), if_bnx.c:816-822). ether_input's ETHER_BPF_MTAP (sys/net/if_ethersubr.c:1512) then copies the bogus length out of the cluster to bpf listeners => kernel heap OOB read / info disclosure of up to ~63.5KB; bge_len < 18 gives a negative m_len which if_ethersubr.c:1499-1503 drops harmlessly (no underflow panic, unlike bce DF-3604). Runtime verification honestly inconclusive: the audit guest has virtio-net only; bnx is compiled into the running kernel (kldstat -v: pci/if_bnx) but no Broadcom PCI device exists, so bnx_attach/bnx_rxeof are unreachable (run.log: NO_BROADCOM_DEVICE, ifconfig=vtnet0 lo0, sysctl hw.bnx unknown oid). Guest /usr/src if_bnx.c md5 49a18c4b7642b9b2ebd1d02ad11542e4 is byte-identical to the audited tree.