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)
PoC verification
Evidence pack
findings/poc/DF-4055 Β· 16 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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 β 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 ownet_newbuf_hdr/et_newbuf_cluster(productionet_init_rx_ring:1485loop), 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, andET_INTR_STATUS = ET_INTR_RXEOFin 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 soup0000010052540012 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 mbufatm_free(KASSERT M_TRAILINGSPACE >= 0, sys/kern/uipc_mbuf.c:1317), backtracem_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
fixedKernel #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
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).
No comments yet.