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

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-4754 Β· 14 files
FileTypeDescriptionSize
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
VERDICT.md
↓ download raw

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

  1. 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.
  2. RX path: ena_com_rx_pkt β†’ rc=0, descs=1, and ena_bufs[0].req_id=0xffff β€” the device-controlled req_id is propagated to the driver with no validation (ena_eth_com.c:540).
  3. 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.
  4. 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

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

fix.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).
↓ fix.diffper-fix-DF-4754

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.