ena_com_rx_pkt propagates the device-controlled RX completion req_id with no bounds check β driver indexes rx_buffer_info[req_id] far out of bounds (OOB read β wild mbuf* writes + kernel-memory disclosure)
Summary
RX completion loop copies cdesc->req_id (device-written u16) unvalidated, while the TX path in the same file rejects req_id >= q_depth (ena_eth_com.c:614) and a validate helper exists but is only used on the kernel-owned refill path. Consumer ena_rx_mbuf indexes rx_buffer_info[req_id] (array == q_depth) then derefs/writes through the OOB-derived mbuf pointer and hands it to if_input. REPRODUCED on guest via kld compiling the real source: req_id=0xffff vs 1024-ring -> 4031KB OOB -> Fatal trap 12; fix (mirror TX check) validated on rebuilt module, clean reject. Hostile-device (hypervisor) trust model - the exact class upstream FreeBSD later fixed.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-4754 Β· 14 files| File | Type | Description | Size | |
|---|---|---|---|---|
| df_rxreqid.c | β | 5.7 KB | view raw | |
| Makefile | β | 482 B | β download | |
| build.sh | β | 136 B | view raw | |
| run.sh | β | 123 B | view raw | |
| build.log | β | 125 B | view raw | |
| run.log | β | 1.3 KB | view raw | |
| panic.txt | β | 1.3 KB | view raw | |
| env.txt | β | 618 B | view raw | |
| fix.diff | β | 458 B | view raw | |
| fix_build.log | β | 6.7 KB | view raw | |
| fix_run.log | β | 791 B | view raw | |
| VERDICT.md | β | 4.7 KB | β raw | |
| README.md | β | 2.5 KB | β raw | |
| verdict.json | β | 4.7 KB | view raw |
DF-4754 β ena_com_rx_pkt: unvalidated device-controlled rx req_id β OOB driver indexing
What
ena_com_rx_pkt() (sys/dev/virtual/amazon/ena/ena-com/ena_eth_com.c:536-542)
copies the completion descriptor's req_id (a u16 written by the device)
into ena_rx_ctx->ena_bufs[] with no bounds check β while the TX
sibling ena_com_tx_comp_req_id_get() (same file, line 614) explicitly
rejects cdesc->req_id >= io_cq->q_depth.
The driver consumer ena_rx_mbuf() (sys/dev/virtual/amazon/ena/ena.c:1459
and 1491) then does rx_info = &rx_ring->rx_buffer_info[req_id]; where the
array holds exactly ring_size == io_cq->q_depth entries (ena.c:736) β and
immediately dereferences rx_info->mbuf / rx_info->map / rx_info->ena_buf
and later writes through the resulting mbuf pointer
(mbuf->m_flags |= M_PKTHDR, m_pkthdr.len = <device len>, β¦).
A compromised host (the ENA "device" is the hypervisor) that DMAs an RX
completion with req_id = 0xFFFF against a 1024-entry ring drives reads
~4 MB past the array and wild-pointer writes/leak through the OOB mbuf*.
How to reproduce (on the dfbsd-qemu guest)
./build.sh # kld module in /root/df4754; compiles the REAL
# /usr/src/sys/dev/virtual/amazon/ena/ena-com/ena_eth_com.c
./run.sh # kldload ./df_rxreqid.ko (test runs at MOD_LOAD)
Expected output (baseline / vulnerable)
DF4754: [1] TX contrast ... rc=22 -> REJECTED by bounds check DF4754: [2] RX path ... ena_com_rx_pkt rc=0 descs=1 DF4754: ... ena_bufs[0].req_id=0xffff *** UNVALIDATED req_id PROPAGATED *** DF4754: [3] ... rx_buffer_info[0xffff] -> ... (4031 KB past ring end) Fatal trap 12: page fault while in kernel mode fault virtual address = <the OOB rx_info address> (guest dies in DDB)
With fix.diff applied to /usr/src and the module rebuilt:
DF4754: ena_com_rx_pkt rc=22 descs=0 DF4754: ... req_id=0x0000 rejected (no propagation, no panic)
Files
df_rxreqid.c harness (synthesizes the hostile completion ring) Makefile kmod build; SRCS includes the real /usr/src ena_eth_com.c build.sh run.sh exact commands build.log baseline module build (clean, -Werror) run.log panic.txt baseline run + kernel panic from the serial console fix_build.log fix_run.log patched-source rebuild + re-run env.txt guest identity fix.diff one-hunk fix mirroring the TX-side check VERDICT.md full narrative manifest.json / verdict.json machine-readable results
DF-4754 VERDICT β REPRODUCED (panic; hostile-device OOB read + wild-pointer primitive)
Bottom line
Reproduced on the DragonFly 6.5-DEVELOPMENT QEMU guest (X86_64_GENERIC,
2026-07-02 build). A kld-module harness compiles the real, unmodified
/usr/src/sys/dev/virtual/amazon/ena/ena-com/ena_eth_com.c (the exact file
the finding is filed against) and drives ena_com_rx_pkt() with a
synthesized RX completion ring containing precisely what a compromised
host would DMA: one cdesc with PHASE|LAST set, length=0x4141,
req_id=0xFFFF against q_depth=1024.
Baseline run (vulnerable) β run.log / panic.txt
- TX contrast:
ena_com_tx_comp_req_id_get(req_id=0xffff)βrc=22 (EINVAL)β REJECTED by the bounds check at ena_eth_com.c:614. - RX path:
ena_com_rx_pktβrc=0, descs=1, andena_bufs[0].req_id=0xffffβ the device-controlled req_id is propagated to the driver with no validation (ena_eth_com.c:540). - Driver consumer replication (the exact arithmetic of ena.c:1459):
rx_buffer_info = 0xfffff8011bce0000(1024 entries, 65536 bytes, allocated like ena.c:736);&rx_buffer_info[0xffff] = 0xfffff8011c0dffc0β 4031 KB past the end of the ring. - Performing the exact next dereference the driver makes (ena.c:1462,
mbuf = rx_info->mbuf):Fatal trap 12: page fault while in kernel mode,fault virtual address = 0xfffff8011c0dffc0,supervisor read data, page not present,Stopped at df4754_run+0x2e1: movq (%rbx),%rsiβ guest dies in DDB.
That is the OOB-read primitive: with heap memory mapped at the OOB offset
(which a groomed/normal kernel heap often has β it depends on what lies
4 MB past a 64 KB kmem allocation), the driver instead obtains a foreign
kernel-memory-derived struct mbuf *, then writes through it
(m_flags |= M_PKTHDR, m_pkthdr.len = <device-chosen len>,
m_pkthdr.rcvif = ifp, hash store) and hands the "packet" to if_input β
kernel memory disclosure into the network stack plus semi-controlled
wild-pointer writes. In this harness run the offset happened to be
unmapped, converting the primitive into an immediate kernel panic.
Why no uid=0 attempt
The attacker in this threat model is the host (compromised hypervisor / buggy PF-equivalent device). The host already owns guest memory, so guest-internal privilege escalation is not the prize; the finding is guest-hardening against device-side memory corruption. The in-guest primitive is real and demonstrated (OOB index β OOB read β panic); pushing it to controlled guest-memory corruption only matters for a sandboxed-device threat model this port does not have.
Fix validation β fix.diff / fix_build.log / fix_run.log
fix.diff adds the missing check inside the copy loop of
ena_com_rx_pkt(), mirroring the TX-side check at line 614 (this also
matches the check upstream FreeBSD later added to this function):
if (unlikely(cdesc->req_id >= io_cq->q_depth))
return ENA_COM_INVAL;
Applied inside the guest's /usr/src (patch -p1, hunk at line 536),
module rebuilt (compiles the patched /usr/src file β see fix_build.log),
exact same PoC re-run (fix_run.log):
- baseline:
rc=0, req_id=0xffff propagated, index +4031 KB OOB, Fatal trap 12 panic. - patched:
rc=22 (EINVAL),descs=0,req_id=0x0000 rejected, in-bounds index, no panic, guest stays up (vm.sh status= up).
Scoping note: validation was performed at module level against the real ena_eth_com.c source file (both baseline and patched builds compile the same guest /usr/src file); a full kernel rebuild was not needed because the audited code ships verbatim inside the harness module, and the in-kernel driver path cannot be exercised without ENA hardware.
Post-run state
Guest reset to the clean-source snapshot (vm.sh reset with-src) after
both runs; module unloaded/reverted with the snapshot. guest_dirty = 0.
Kernel references
- sys/dev/virtual/amazon/ena/ena-com/ena_eth_com.c:536-542 (missing check β root cause)
- sys/dev/virtual/amazon/ena/ena-com/ena_eth_com.c:614 (TX-side check that proves the contract)
- sys/dev/virtual/amazon/ena/ena.c:1459,1491 (OOB consumer)
- sys/dev/virtual/amazon/ena/ena.c:736 (array sized ring_size)
- sys/dev/virtual/amazon/ena/ena.c:700-709 (validate_rx_req_id exists but is only used on the kernel-owned refill path, ena.c:1011)
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
fixedfix.diff applied to the guest /usr/src copy of ena_eth_com.c; module rebuilt from the patched file and the exact PoC re-run: baseline panic (Fatal trap 12 at the OOB rx_info address) is gone, ena_com_rx_pkt now returns EINVAL and propagates no req_id, guest stays up. Validated at module level against the real source file; in-driver validation would additionally require ENA hardware.
fix_build.log (clean rebuild of patched ena_eth_com.c); fix_run.log ('ena_com_rx_pkt rc=22 descs=0', 'req_id=0x0000 rejected', no trap, vm stays up) vs baseline panic.txt (Fatal trap 12, fault va 0xfffff8011c0dffc0).
Confirmed kernel references
Detail
Exploit chain
compromised host DMAs rx cdesc {PHASE|LAST, len, req_id=0xFFFF} -> ena_com_rx_pkt copies req_id into ena_bufs[0] with no check (ena_eth_com.c:540) -> ena_rx_mbuf: rx_info = &rx_buffer_info[0xFFFF] (ena.c:1459, ~4MB past 64KB array) -> mbuf = rx_info->mbuf (OOB read; unmapped => kernel panic as demonstrated; mapped => wild struct mbuf*) -> driver writes m_pkthdr.len/m_len (device-chosen), m_flags, rcvif through the wild pointer, unloads an OOB dmamap, and passes the mbuf to if_input => kernel memory disclosure to the network stack + constrained arbitrary writes. Demonstrated through the OOB read (panic); wild-pointer write/leak stages deliberately not driven (host already owns guest memory; hardening is the deliverable).
Evidence (decisive lines)
run.log/panic.txt lines: 'ena_com_rx_pkt rc=0 descs=1', 'ena_bufs[0].req_id=0xffff *** UNVALIDATED req_id PROPAGATED TO DRIVER ***', 'rx_buffer_info[0xffff] -> 0xfffff8011c0dffc0 (4031 KB past ring end)', 'Fatal trap 12: page fault ... fault virtual address = 0xfffff8011c0dffc0', 'Stopped at df4754_run+0x2e1'. Contrast: 'tx_comp_req_id_get(req_id=0xffff) rc=22 -> REJECTED'. Fix: fix_run.log 'ena_com_rx_pkt rc=22 descs=0', 'req_id=0x0000 rejected', no panic, guest up.
PoC changes
No seed PoC existed (pass-2 finding). Harness written from scratch: kld module compiling the guest's real /usr/src ena_eth_com.c; had to (a) include ena_eth_com.h (not just ena_com.h) for the prototypes, (b) add device_if.h/bus_if.h/pci_if.h to SRCS, (c) build with 'make -m /usr/share/mk -m /usr/src/sys/conf SYSDIR=/usr/src/sys', (d) define the ena_log_level extern, (e) mirror struct ena_rx_buffer's 64-byte __cachealign layout.
Verified recommended fix
Add 'if (unlikely(cdesc->req_id >= io_cq->q_depth)) return ENA_COM_INVAL;' to the copy loop of ena_com_rx_pkt (mirror of the TX check at ena_eth_com.c:614), plus defense-in-depth validate_rx_req_id() use in ena_rx_mbuf (ena.c:1458/1490).
Verdict
Reproduced on the DragonFly 6.5-DEVELOPMENT QEMU guest: a completion descriptor with req_id=0xFFFF (hostile host DMA payload) passes through ena_com_rx_pkt unvalidated (rc=0) while the TX path rejects the same value; replicating the driver's consumer line (ena.c:1459) indexes rx_buffer_info[0xFFFF] 4031 KB past a 1024-entry ring, and the driver's next dereference (ena.c:1462, rx_info->mbuf) page-faults the kernel (Fatal trap 12, guest dead in DDB). With mapped memory at the OOB offset the same primitive yields a foreign-memory-derived mbuf* that the driver writes through and delivers to if_input (kernel-memory disclosure + semi-controlled wild-pointer writes). Attacker is the compromised host (paravirt device), so this is guest hardening, not guest->guest escalation. fix.diff (mirror of the TX-side q_depth check) validated on the same real source file: patched build returns EINVAL, propagates nothing, no panic, guest stays up.
No comments yet.