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

bge_rxeof trusts NIC-DMA'd return-ring bge_len with no MCLBYTES/jumbo bound: heap OOB read / kernel info leak up to ~63.5KB past the RX cluster (OOB write on i386 ALIGNBUG path)

Summary

bge_rxeof sets m->m_pkthdr.len = m->m_len = cur_rx->bge_len - ETHER_CRC_LEN straight from the NIC-DMA'd RX return-ring descriptor (uint16_t) with no bound by the posted buffer size - 2046 usable bytes for the std cluster or 9016 for a jumbo pool slot. A faulty or malicious BCM57xx handing up bge_len=65535 produces an mbuf describing 65531 bytes in that small buffer; ether_input's ETHER_BPF_MTAP copies the bogus length to bpf listeners => kernel heap disclosure; IP-stack parsing reads the same OOB range (leak/panic). On !x86_64 builds the BGE_FLAG_RX_ALIGNBUG path (BCM5701 PCI-X) additionally bcopy's cur_rx->bge_len bytes into the cluster => heap OOB WRITE; compiled out on x86_64. bge_len<18 gives a negative m_len harmlessly dropped (no bce/DF-3604-style underflow panic). Producer-side bounds (BD len, RX_MTU, RCB) are all device-enforced and thus no defense against the defecting device this check guards against. The bce/bnx lesson applied to bge; DB-verified not covered by DF-1159 or the DF-1478..1562 sweep. Fix: clamp-and-drop before ANY use of the length including the i386 bcopy.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-3663 Β· 11 files
FileTypeDescriptionSize
trigger.c β€” 4.5 KB view raw
build.sh β€” 107 B view raw
run.sh β€” 167 B view raw
build.log β€” 84 B view raw
run.log β€” 735 B view raw
env.txt β€” 440 B view raw
fix.diff β€” 593 B view raw
README.md β€” 1.7 KB ↓ raw
VERDICT.md β€” 4.9 KB ↓ raw
manifest.json β€” 1.2 KB view raw
verdict.json β€” 5.3 KB view raw

DF-3663 β€” bge_rxeof trusts NIC-DMA'd return-ring bge_len with no buffer bound

Build / Run / Expected

./build.sh          # cc -O -Wall -o trigger trigger.c   (in a DragonFly guest)
./run.sh            # runs the trigger + records the guest-side reachability proof

Expected on real BCM57xx hardware (NOT available in this environment): the device DMAs an RX return-ring BD with bge_len larger than the posted buffer (2046 std / 9016 jumbo usable bytes) and error flags clear; bge_rxeof (sys/dev/netif/bge/if_bge.c:3144) hands the stack an mbuf whose m_len/m_pkthdr.len is up to 65531 inside that small buffer. Any bpf(4) listener (tcpdump -i bge0 -w out.pcap) then receives up to ~63.5 KB of kernel heap adjacent to the RX cluster β€” a kernel info disclosure. On i386 builds the BGE_FLAG_RX_ALIGNBUG path (if_bge.c:3138-3141) first writes that many bytes past the cluster (heap overflow).

Actual on the audit guest: no Broadcom PCI device exists (virtio-net only), so bge_attach/bge_rxeof are unreachable β€” the trigger builds clean and prints its hardware-precondition notice (run.log). Verification is therefore honestly inconclusive (source-level certain; see VERDICT.md).

Files

  • trigger.c β€” annotated hardware-precondition trigger
  • run.log β€” guest proof: pci/if_bge compiled into the kernel, NO_BROADCOM_DEVICE, ifconfig -l = vtnet0 lo0, sysctl hw.bge unknown, /usr/src md5 == audited tree, clean build, trigger output
  • fix.diff β€” git-apply-able clamp-and-drop (bounds the NIC length by the mbuf's own posted length before ANY use, including the i386 ALIGNBUG bcopy)
  • verdict.json / manifest.json β€” machine verdict
VERDICT.md
↓ download raw

DF-3663 VERDICT β€” bge_rxeof trusts NIC-DMA'd return-ring bge_len

Status: INCONCLUSIVE at runtime (no bge hardware on the audit guest); source-level CERTAIN.

Root cause (path:line, audited tree == guest /usr/src, md5-verified)

sys/dev/netif/bge/if_bge.c:3144:

m->m_pkthdr.len = m->m_len = cur_rx->bge_len - ETHER_CRC_LEN;

cur_rx is a struct bge_rx_bd in the host-memory RX return ring (sc->bge_ldata.bge_rx_return_ring[sc->bge_rx_saved_considx], if_bge.c:3058-3059) β€” memory the NIC fills in by DMA. bge_len is a plain uint16_t (sys/dev/netif/bge/if_bgereg.h:2327-2331); nothing on the path bounds it by the buffer that was actually posted to the device:

  • std ring: bge_newbuf_std() (if_bge.c:1016-1057) posts an m_getcl() cluster β€” MCLBYTES = 2048 β€” with m_adj(ETHER_ALIGN) (if_bge.c:1027-1030) leaving 2046 usable bytes; the producer BD length is rc->bge_mbuf->m_len (if_bge.c:1070).
  • jumbo ring (bge supports jumbo, unlike the bnx sibling DF-3639): bge_newbuf_jumbo() (if_bge.c:1079-1123) attaches a driver-pool slot of BGE_JUMBO_FRAMELEN = 9018 bytes (if_bgevar.h:56) with m_adj(ETHER_ALIGN) β†’ 9016 usable; the 384 slots are carved from one contiguous BGE_JMEM DMA block (if_bge.c:910-921), so an oversized bge_len on the last slot reads past the entire allocation.

A faulty or malicious device that writes bge_len = 65535 (error flags clear) makes the driver hand the stack an mbuf describing 65531 bytes inside the 2046/9016-byte buffer.

Sinks

  1. ifp->if_input(ifp, m, NULL, -1) (if_bge.c:3170) β†’ ether_input() runs ETHER_BPF_MTAP(ifp, m) (sys/net/if_ethersubr.c:1512) before any protocol validation β€” the bpf tap copies m_pkthdr.len bytes out of the cluster to every capturing socket β‡’ kernel heap OOB read / info disclosure of up to ~63.5 KB (std) per frame.
  2. IP/ARP/VLAN parsing and socket copies walk the same OOB range (further leak; fault/panic when crossing an unmapped page).
  3. i386-only write variant (CWE-787): with BGE_FLAG_RX_ALIGNBUG (BCM5701 PCI-X, if_bge.c:2206-2208), #if !defined(__x86_64__) code at if_bge.c:3138-3141 executes bcopy(m->m_data, m->m_data + ETHER_ALIGN, cur_rx->bge_len) β€” the same untrusted length used as a copy size into the 2048-byte cluster β‡’ heap OOB write. Compiled out of x86_64 kernels (the audit guest's platform).

Non-issues checked (so the finding is exactly scoped)

  • bge_len < ETHER_HDR_LEN + ETHER_CRC_LEN (18) β‡’ negative m_len, harmlessly dropped at sys/net/if_ethersubr.c:1502-1506 (m_len < sizeof(struct ether_header)) β€” no bce/DF-3604-style unsigned-underflow panic here.
  • The producer-side BD length, BGE_RX_MTU register (if_bge.c:3769) and RCB max-frame (if_bge.c:1575) are all device-side limits a healthy NIC enforces; the defecting device this check guards against is precisely the one that ignores them.
  • The unvalidated bge_idx index issue is DF-1159 (pass 1) and is not re-reported here; this finding is the independent length trust.

Why inconclusive at runtime

The audit QEMU guest has virtio-net only: pciconf -l | grep -i -E "bge|broadcom|14e4" β‡’ NO_BROADCOM_DEVICE, ifconfig -l β‡’ vtnet0 lo0, sysctl hw.bge β‡’ unknown oid (run.log). pci/if_bge is compiled into the running kernel (kldstat -v id 59), and the guest's /usr/src/sys/dev/netif/bge/ md5-matches the audited tree, so the code is present and identical β€” only the device (and therefore bge_attach β†’ bge_rxeof) is unreachable. No emulation of a BCM57xx RX engine exists in the harness. Same classification as the bnx/bce siblings DF-3639/DF-3604.

Fix validation

fix.diff clamps the frame to the posted buffer (m->m_len) and drops it (ierrors++, m_freem, continue) when bge_len is outside [ETHER_HDR_LEN + ETHER_CRC_LEN, m->m_len + ETHER_CRC_LEN], placed before the #if !defined(__x86_64__) block so the i386 ALIGNBUG bcopy is covered too. git apply --check passes against the pristine tree. Runtime baseline-vs-patched comparison is impossible for the same hardware reason (fix_status: not_testable).

Exploitability ceiling

Local info-leak class (kernleak): unprivileged user + bpf listener reads adjacent kernel heap (adjacent mbuf clusters, potentially pointers/data). Not a write primitive on x86_64; on i386 with a BCM5701 PCI-X part it is additionally a NIC-directed heap overwrite. Requires a misbehaving device (malicious peripheral / firmware compromise / DMA attack), hence AC:H and Medium severity, matching the calibrated sibling findings.

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 bounds the NIC-reported length by the mbuf's own posted length (correct for both std 2046/2048 and jumbo 9016/9018 configs, ALIGNBUG or not) and is placed before the #if !defined(x86_64) block so the OOB-write variant is covered too; git apply --check passes against the pristine tree. Cannot be runtime-validated: the guest has no BCM57xx device so neither baseline nor patched behavior is exercisable (same reason the PoC is inconclusive). The patch mirrors the error-drop idiom already used at if_bge.c:3111-3116.

fix.diff (git apply --check OK); baseline absence proof in run.log (NO_BROADCOM_DEVICE, sysctl hw.bge unknown oid)
↓ fix.diffper-fix-DF-3663

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 2046B std cluster (or past a 9016B jumbo slot); (3) unprivileged user with a bpf listener on bge0 reads up to ~63.5KB of adjacent kernel heap (info leak); IP-stack parsing of the same OOB range can also fault (panic/DoS); on i386 + BCM5701 PCI-X the ALIGNBUG bcopy turns the same trusted length into a heap OOB write. No write primitive on x86_64 from this bug alone.

Evidence (decisive lines)

run.log (guest proof: driver in kernel, zero Broadcom devices, no attach, src md5 match, clean -Wall build), VERDICT.md (full path:line trace incl. the benign underflow path and the i386 write variant), trigger.c (annotated hardware-precondition trigger, builds clean -O -Wall), fix.diff (git apply --check OK; clamp-and-drop bounded by the mbuf's own posted length, placed before the i386 bcopy)

PoC changes

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

Verified recommended fix

In bge_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 any use β€” including the i386 ALIGNBUG bcopy (see fix.diff).

Verdict

Source-level certain: bge_rxeof (sys/dev/netif/bge/if_bge.c:3144) 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 bound anywhere on the path β€” a faulty or malicious BCM570x/57xx can hand the stack an mbuf describing up to 65531 bytes in a 2046-byte std cluster (m_getcl + m_adj(ETHER_ALIGN), if_bge.c:1024-1030) or a 9016-byte jumbo pool slot (bge HAS jumbo support, unlike the bnx sibling DF-3639; if_bge.c:1087-1114, slots carved from one contiguous BGE_JMEM block at if_bge.c:910-921, so overflow of the last slot runs past the whole DMA allocation). ether_input's ETHER_BPF_MTAP (sys/net/if_ethersubr.c:1512) then copies the bogus length out of the buffer 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:1502-1506 drops harmlessly (no underflow panic, unlike bce DF-3604). Additional i386-only WRITE variant (CWE-787): the BGE_FLAG_RX_ALIGNBUG path (BCM5701 PCI-X, if_bge.c:2206-2208) bcopy's cur_rx->bge_len bytes into the cluster at if_bge.c:3138-3141 under #if !defined(x86_64) β€” compiled out of x86_64 kernels. Runtime verification honestly inconclusive: the audit guest has virtio-net only; bge is compiled into the running kernel (kldstat -v: pci/if_bge id 59) but no Broadcom PCI device exists, so bge_attach/bge_rxeof are unreachable (run.log: NO_BROADCOM_DEVICE, ifconfig=vtnet0 lo0, sysctl hw.bge unknown oid). Guest /usr/src if_bge.c md5 f6c2d771384a52775cd4bb399d5dcab1 (plus if_bgereg.h/if_bgevar.h) byte-identical to the audited tree.