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

et_rxeof trusts the NIC-DMA'd rxstat length field: m_len/pkthdr.len set to up to 65535 with no bound against the actual buffer size (MHLEN/MCLBYTES/ET_JUMBO_FRAMELEN) β€” heap OOB read of up to ~64KB handed to if_input/BPF (DF-3604 family instance)

Summary

buflen comes from the RX stat ring entry written by the NIC over DMA (16-bit) and is applied after only checking >= ETHER_CRC_LEN; the receiving mbuf was allocated with fixed capacity (MHLEN ring0 / MCLBYTES ring1 / jumbo). A device reporting a length larger than the buffer yields an mbuf whose m_len runs past its allocation; ether_input/BPF read up to 65531 bytes past the cluster (kernel heap disclosure to unprivileged listeners) and parser walks can panic. bce DF-3604 pattern minus the underflow. Clamp-to-bufsize fix.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-4055 Β· 16 files
FileTypeDescriptionSize
dfet_poc.c β€” 32.5 KB view raw
Makefile β€” 103 B ↓ download
build.sh β€” 112 B view raw
run.sh β€” 103 B view raw
run.log β€” 788 B view raw
dmesg_s1.txt β€” 2.4 KB view raw
leak_sample.txt β€” 501 B view raw
panic.txt β€” 1.7 KB view raw
fix.diff β€” 3.6 KB view raw
fix_build.log β€” 1.1 KB view raw
fix_run_s1.log β€” 1.4 KB view raw
fix_run_s1_dmesg.txt β€” 498 B view raw
VERDICT.md β€” 4.2 KB ↓ raw
env.txt β€” 1.3 KB view raw
verdict.json β€” 4.8 KB view raw
manifest.json β€” 938 B view raw
VERDICT.md
↓ download raw

VERDICT β€” DF-4055 (et_rxeof trusts NIC-DMA'd rxstat length β†’ heap OOB read)

Status: reproduced (production-handler KLD dispatch, DF-4106 vge / DF-4266 wb evidence class) β€” and fix validated on a rebuilt kernel.

What ran

The guest has no ET131x NIC, but device et is built into X86_64_GENERIC, so the driver's real static functions live in the running kernel image. et_rxeof exists only inlined into et_intr (nm has no separate symbol; disassembly of et_intr+0x0..+0x570 contains the STATRING shift-16 extraction and the 0x3ff/0x400 ring bound comparisons), so the RX path was driven through the real et_intr() β€” the production interrupt entry β€” with:

  • a fabricated post-attach softc in module .bss: real busdma rings/tags/maps built with the verbatim et_dma_alloc()/ et_dma_mbuf_create() call sequence, rings armed by the kernel's own et_newbuf_hdr/et_newbuf_cluster (production et_init_rx_ring:1485 loop), a zeroed-mii fake miibus, a real lwkt serializer held across the dispatch exactly as the interrupt context holds it, per-CPU ifdata, real ifaltq subqueues;
  • the "NIC" simulated the only way the driver observes it: an RX stat ring write-back (LEN=60000 BUFIDX=0 RINGIDX=1) plus a 64-byte frame DMA'd into the armed ring-1 cluster, and ET_INTR_STATUS = ET_INTR_RXEOF in the MMIO stand-in.

Struct-layout agreement printed and cross-checked (env.txt).

Baseline (stock kernel #0, Jul 2 2026) β€” the bug

et_rxeof extracts the 16-bit ET_RXST_INFO2_LEN (if_et.c:1863), checks only buflen < ETHER_CRC_LEN (:1892), then publishes m->m_pkthdr.len = m->m_len = buflen (:1896) and hands the mbuf to if_input (:1902). With the device claiming 60000 for a ring-1 buffer (one MCLBYTES = 2048 cluster):

dfet: if_input: m=0xfffff80089786400 m_len=59996 pkthdr.len=59996
      M_EXT=1 ext_size=2048 (backing-store capacity) rcvif=dfet0
dfet:   OOB: m_len exceeds backing store by 57948 bytes; heap bytes
      [2048..2080) past the cluster: <kernel heap>
  • 3/3 iterations over-length (df_oob=3), guest kept alive by the sink's bounded read + deliberate leak.
  • Leak samples differ per run and are kernel-heap content: run A ASCII text "ation an\nd# PasswordAuthenticati"; run B the previous slot's crafted frame (cross-slot bleed); run C pointer soup 0000010052540012 345652550a000202 0800400000000000 (leak_sample.txt). In production this window is BPF/ether_input's.
  • Stock-stack panic corroboration: freeing the delivered mbuf β€” exactly what the real stack does β€” panics the kernel: panic: overflowed mbuf at m_free (KASSERT M_TRAILINGSPACE >= 0, sys/kern/uipc_mbuf.c:1317), backtrace m_free ← m_freem ← et_intr+0x4bf (panic.txt; first rig run before the sink switched to leak-instead-of-free). The kernel's own invariant check confirms m_len exceeded the backing store.

Fix validation (kernel #1, Oct 14 2026, same tree + fix.diff)

fix.diff adds a per-ring capacity check (MHLEN ring 0 / MCLBYTES ring 1 / ET_JUMBO_FRAMELEN jumbo); a device length beyond the backing store drops the frame with ierrors++. make -j6 nativekernel + installkernel (if_et.c recompiled at build-log line 18438, BUILD_OK), reboot, identical rig (addresses re-resolved; et_intr moved 0xffffffff804b8bd0 β†’ 0xffffffff804b8c70):

scenario baseline (#0) fixed (#1)
LEN=60000 claim Γ—3 3 mbufs delivered with m_len=59996 on 2048-B cluster; heap bytes read past the cluster; panic at m_freem delivered=0, oob_events=0, ierrors=3 β€” dropped 3/3, no OOB mbuf, no panic

fix_status: fixed (baseline_reproduced=1, patched_reproduced=0).

Impact

Kernel heap disclosure (up to ~63KB per frame past a 2048-byte cluster) into the if_input/BPF window, plus a deterministic panic once the over-length mbuf is freed on stock configs. Attacker model: faulty/hostile NIC or PCIe DMA tampering β€” hardware-mediated, so severity Medium stands (as filed).

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

fixed
baseline reproduced→ patch + rebuild →patched clean

Kernel #1 (same tree + fix.diff) A/B with the identical rig: baseline 3/3 over-length mbufs delivered with m_len=59996 on 2048-byte clusters + heap bytes read past the cluster + m_free 'overflowed mbuf' panic on free; patched: delivered=0, oob_events=0, ierrors=3, no panic. Defective behavior gone.

findings/poc/DF-4055/fix.diff; fix_run_s1.log; fix_run_s1_dmesg.txt; fix_build.log
↓ fix.diffper-fix-DF-4055

Confirmed kernel references

Detail

Exploit chain

Device/DMA attacker writes an RX stat ring entry with LEN > buffer capacity: et_rxeof publishes an over-length mbuf to if_input -> BPF taps read up to m_len bytes from a 2048-byte cluster (kernel heap disclosure, repeated per frame) -> the stack's m_freem then trips 'overflowed mbuf' KASSERT panic on stock configs. No userland trigger without the hardware; not pursued to uid0 (read-only primitive, class does not permit).

Evidence (decisive lines)

run.log + dmesg_s1.txt (m_len=59996 M_EXT=1 ext_size=2048 3/3, 'S1 BUG CONFIRMED' marker); leak_sample.txt (three differing OOB dumps incl. kernel ASCII text); panic.txt (stock-kernel m_free 'overflowed mbuf' at et_intr+0x4bf); fix_run_s1.log + fix_run_s1_dmesg.txt (patched kernel: dropped 3/3, ierrors=3, no OOB); VERDICT.md; env.txt

PoC changes

Authored the production-dispatch rig fresh (no seed PoC existed; pack was 'untested/hardware-gated'). et_rxeof has no symbol of its own - dispatched real et_intr with ET_INTR_STATUS=ET_INTR_RXEOF in the MMIO stand-in instead. Sink v1 freed the delivered mbuf and legitimately panicked the kernel (m_free overflow KASSERT) - kept as panic.txt evidence; v2 bounds the demonstrated read to 32 bytes past the cluster and deliberately leaks the over-length mbuf so the 3-sample run and the sibling scenarios survive on one boot.

Verified recommended fix

In et_rxeof, drop (ierrors++) any frame whose device-reported length exceeds the per-ring backing-store size (MHLEN ring0 / MCLBYTES ring1 / ET_JUMBO_FRAMELEN jumbo) before assigning m_len

Verdict

REPRODUCED via production-handler KLD dispatch (DF-4106 vge / DF-4266 wb evidence class): the guest kernel's OWN et_intr() - which contains et_rxeof() inlined (no separate symbol; verified by disassembly: STATRING shift-16 extract + 0x3ff/0x400 ring bounds inside et_intr) - was dispatched over a fabricated post-attach softc with real busdma rings armed by the kernel's own et_newbuf_hdr/et_newbuf_cluster. With the NIC write-back claiming LEN=60000 for a ring-1 buffer backed by one MCLBYTES(2048) cluster, the production handler handed if_input an mbuf with m_len=pkthdr.len=59996 on that 2048-byte cluster 3/3 times (if_et.c:1863/1892/1896/1902; the 16-bit ET_RXST_INFO2_LEN field allows up to 65535, guarded only by buflen < ETHER_CRC_LEN). The sink read kernel-heap bytes past the cluster that differed per run: ASCII text ('...ation an\nd# PasswordAuthenticati'), the PREVIOUS slot's crafted frame (cross-slot bleed), and pointer soup - the disclosure window production BPF/ether_input gets. Corroboration: freeing the delivered mbuf (what the real stack does) panics the stock kernel - panic: 'overflowed mbuf', KASSERT M_TRAILINGSPACE>=0 at sys/kern/uipc_mbuf.c:1317, backtrace m_free <- m_freem <- et_intr+0x4bf (panic.txt, first rig run). FIX VALIDATED: kernel #1 rebuilt with fix.diff (per-ring capacity check, drop on violation) - identical rig dropped the over-length claims 3/3 (ierrors=3, oob_events=0, no OOB mbuf, no panic).