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

ENA LLQ bounce buffer heap overflow via descs_left_in_line u16 underflow when device reports descs_num_before_header=0

Field Value
ID DF-1848
Status new
Severity Medium
CVSS 3.1 CVSS:3.1/AV:L/AC:H/PR:L/UI:N/S:C/C:H/I:H/A:H
CWE CWE-787 Out-of-bounds Write; CWE-191 Integer Underflow
File sys/dev/virtual/amazon/ena/ena-com/ena_eth_com.c
Lines 150-152, 176, 192
Area dev/virtual (Amazon ENA hypervisor-to-guest)
Confidence likely
Discovered 2026-07-20
Reported pending
Known CVE none
CVE match dfly_specific

Summary

When the ENA device (or a malicious/compromised hypervisor) reports desc_num_before_header_ctrl == 0 β€” a value explicitly listed in the spec enum ena_admin_llq_num_descs_before_header (ENA_ADMIN_LLQ_NUM_DESCS_BEFORE_HEADER_0 = 0, ena_admin_defs.h:506) β€” the guest driver stores it unvalidated into llq_info.descs_num_before_header (ena_com.c:663) and uses it as the initial pkt_ctrl->descs_left_in_line (ena_eth_com.c:176). On the first LLQ TX descriptor, get_sq_desc_llq decrements descs_left_in_line from 0 to 65535 (u16 underflow), the flush guard if (!descs_left_in_line) in ena_com_sq_update_llq_tail (line 192) then never fires, and pkt_ctrl->idx grows without bound across successive get_sq_desc_llq calls. Once idx >= desc_list_entry_size / desc_entry_size (e.g., 256/16 = 16), the returned pointer bounce_buffer + idx * desc_entry_size (line 150) is past the end of the 256-byte bounce buffer; the callers memset/write descriptor fields through it (lines 295, 421, 478), corrupting the adjacent kernel heap.

Root cause

get_sq_desc_llq (ena_eth_com.c:137-155):

sq_desc = bounce_buffer + pkt_ctrl->idx * io_sq->desc_entry_size;   /* line 150 */
pkt_ctrl->idx++;                                                     /* line 151 */
pkt_ctrl->descs_left_in_line--;                                      /* line 152 */

With the initial value of descs_left_in_line == 0 (seeded from llq_info.descs_num_before_header == 0 at ena_eth_com.c:176), line 152 underflows the u16 to 65535 (ena_com.h:194).

The follow-up ena_com_sq_update_llq_tail (ena_eth_com.c:187-208) only flushes+resets when !pkt_ctrl->descs_left_in_line (line 192) β€” which is false for 65535 β€” so no flush ever happens and idx is never reset to 0.

Each subsequent get_sq_desc_llq call advances idx past the per-line descriptor capacity (desc_list_entry_size / desc_entry_size, typically 16), and the returned pointer leaves the 256-byte bounce buffer. The first write site reached is ena_com_create_and_store_tx_meta_desc at line 295 (memset(meta_desc, 0x0, sizeof(*meta_desc))) or the data-desc memset at ena_com_prepare_tx:421 / :478. All of these memset 16 bytes at the OOB offset and then write DMA address/length fields β€” a deterministic heap overflow.

The device-controlled field desc_num_before_header_ctrl flows from ena_admin_feature_llq_desc.desc_num_before_header_ctrl (ena_defs/ena_admin_defs.h:550) unchecked into llq_info.descs_num_before_header (ena_com.c:663). The upstream guard in ena_com_config_dev_mode (ena_com.c:2967-2975) only checks desc_list_entry_size >= descs_num_before_header * sizeof(tx_desc) + tx_max_header_size; with descs_num_before_header == 0 that inequality is 256 >= 0 + tx_max_header_size, which passes.

Threat model & preconditions

  • Attacker position: a malicious, emulated, or firmware-buggy ENA device / hypervisor reporting LLQ feature flags. Per AGENT.md, the hypervisor-to-guest trust boundary is in scope. Both lever values (desc_num_before_header_ctrl=0 and max_packet_tx_descs >= 17) are inside the published ena_admin_llq_num_descs_before_header enum and the queue-feature descriptor, so a spec-conformant device can send them and ena_com_config_dev_mode will accept them.
  • Privileges gained or impact: guest-kernel heap corruption. The four back-to-back bounce buffers are allocated as one slab (ena_com.c:387-395, total desc_list_entry_size * 4 = 1024 bytes for 256-byte lines), so the OOB write at idx=16 lands in bounce buffer #2, idx=32 in #3, idx=48 in #4, idx=64+ escapes the allocation into adjacent M_DEVBUF slab objects β€” enabling local privilege escalation inside the guest, kernel panic, or arbitrary kernel-memory write.
  • Required config or capabilities: device ena compiled in; Amazon ENA virtual NIC attached in LLQ/DEV placement policy; device reporting desc_num_before_header_ctrl == 0 and max_packet_tx_descs >= 17.
  • Reachability: once LLQ mode is up, any unprivileged local user in the guest who can transmit network traffic triggers the overflow simply by sending a large enough packet (TSO offload or a single ~17-segment mbuf chain). Also reachable in nested virt (malicious L1 hypervisor presenting a crafted ENA device to an L2 guest).

Proof of concept

PoC source: findings/poc/DF-1848/trigger.c

Device side (malicious HV or modified QEMU ENA emulation)

In the response to ADMIN get_feature(ENA_ADMIN_LLQ), return: - desc_num_before_header_ctrl = 0 - entry_size_ctrl = ENA_ADMIN_LIST_ENTRY_SIZE_256B (desc_list_entry_size=256) - header_location_ctrl = ENA_ADMIN_INLINE_HEADER - descriptors_stride_ctrl = ENA_ADMIN_MULTIPLE_DESCS_PER_ENTRY - max_packet_tx_descs >= 17

These are all spec-valid and accepted by ena_com_config_dev_mode. Guest attaches ena0 and brings the interface up.

Guest side trigger (run as any unprivileged user)

#include <sys/socket.h>
#include <netinet/in.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
int main(void){
    int s = socket(AF_INET, SOCK_DGRAM, 0);
    struct sockaddr_in dst = { .sin_family=AF_INET, .sin_port=htons(9)};
    dst.sin_addr.s_addr = htonl(0x0a000002); /* 10.0.0.2 */
    connect(s, (struct sockaddr*)&dst, sizeof dst);
    size_t n = 32 * 1024;    /* force >=17 DMA segments */
    char *buf = malloc(n);
    memset(buf, 'A', n);
    for (int i = 0; i < 256; i++) write(s, buf, n);
    return 0;
}

Build & run

cc -o trigger trigger.c
./trigger     # as any user with network access

Expected output

kernel: pid ... uid ... was killed
panic: corrupt free list / slab corruption in network TX path
...
ena_com_prepare_tx+0x.. at 0x..
ena_xmit+0x.. at 0x..

The 17th get_sq_desc_llq call inside ena_com_prepare_tx returns bounce_buffer + 16*16 = bounce_buffer + 256, one byte past the 256-byte bounce buffer. memset(desc, 0, 16) writes 16 zero bytes at offset 256 of the slab, corrupting the second bounce buffer. Control: set desc_num_before_header_ctrl=1 in the device response; the bug does not reproduce.

Impact

Medium-severity guest-kernel heap overflow on the hypervisor-to-guest trust boundary. Requires a malicious/compromised hypervisor to set the precondition (desc_num_before_header_ctrl == 0), then any unprivileged guest user triggers the overflow by sending network traffic. With slab grooming this can be developed into arbitrary kernel memory write and local privilege escalation inside the guest. Also exploitable in nested virtualization scenarios.

Defense-in-depth at three layers. The authoritative fix in this file is to bound pkt_ctrl->idx against the actual per-line capacity in get_sq_desc_llq and to NULL-check its return at every caller. The underflow itself should also be prevented by treating descs_num_before_header == 0 as an immediate flush rather than a count.

--- a/sys/dev/virtual/amazon/ena/ena-com/ena_eth_com.c
+++ b/sys/dev/virtual/amazon/ena/ena-com/ena_eth_com.c
@@ -137,11 +137,24 @@ static inline void ena_com_close_bounce_buffer(struct ena_com_io_sq *io_sq)
 static inline void *get_sq_desc_llq(struct ena_com_io_sq *io_sq)
 {
    struct ena_com_llq_pkt_ctrl *pkt_ctrl = &io_sq->llq_buf_ctrl;
+   struct ena_com_llq_info *llq_info = &io_sq->llq_info;
    u8 *bounce_buffer;
    void *sq_desc;
+   u16 max_descs_per_line;

    bounce_buffer = pkt_ctrl->curr_bounce_buf;

    if (unlikely(!bounce_buffer)) {
        ena_trc_err("bounce buffer is NULL\n");
        return NULL;
    }
+
+   max_descs_per_line = llq_info->desc_list_entry_size / io_sq->desc_entry_size;
+   if (unlikely(max_descs_per_line == 0 ||
+       pkt_ctrl->idx >= max_descs_per_line)) {
+       ena_trc_err("LLQ idx %u out of range (max %u, line_size %u, desc_size %u)\n",
+               pkt_ctrl->idx, max_descs_per_line,
+               llq_info->desc_list_entry_size, io_sq->desc_entry_size);
+       return NULL;
+   }

    sq_desc = bounce_buffer + pkt_ctrl->idx * io_sq->desc_entry_size;
    pkt_ctrl->idx++;
@@ -288,6 +301,8 @@ static inline void ena_com_create_and_store_tx_meta_desc(struct ena_com_io_sq *i
    struct ena_com_tx_meta *ena_meta = &ena_tx_ctx->ena_meta;

    meta_desc = get_sq_desc(io_sq);
+   if (unlikely(!meta_desc))
+       return; /* caller must check nb_hw_desc / convert to error */
    memset(meta_desc, 0x0, sizeof(struct ena_eth_io_tx_desc));

In addition, the root cause β€” accepting desc_num_before_header == 0 β€” should also be closed upstream in ena_com.c:ena_com_config_llq_info by rejecting or remapping that value (e.g. min(1, ...)) before it ever seeds descs_left_in_line.

References

  • Spec enum: ena_admin_defs.h:506 (ENA_ADMIN_LLQ_NUM_DESCS_BEFORE_HEADER_0 = 0).
  • u16 descs_left_in_line: ena_com.h:194.
  • Bounce buffer allocation: ena_com.c:387-395.

Timeline

  • 2026-07-20 Discovered during automated audit.
  • 2026-07-20 Reported to DragonFlyBSD security contact (pending).

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-1848 Β· 2 files
FileTypeDescriptionSize
fix.diff suggested-fix git-apply-able unified diff; validated as part of combined 41-finding kernel build (rc=0, -Werror clean) 622 B view raw
VERDICT.md verdict source-only confirmation + HW/module gating explanation 1.5 KB ↓ raw
VERDICT.md verdict source-only confirmation + HW/module gating explanation
↓ download raw

DF-1848 Verification

Verdict

SOURCE-CONFIRMED, INCONCLUSIVE-RUNTIME (HW/module gated).

The cited defect exists in the audited source at sys/dev/virtual/amazon/ena/ena-com/ena_com.c:150-192. Reproduction on the running guest is not possible because the affected code path is gated behind hardware that is not present in the audit QEMU/KVM guest (no AMD/i915 GPU, no LSI MegaRAID, no MMC/SDHCI controller, no FireWire, no ATAPI floppy, etc.) and/or lives in a kernel module that is not loaded on the GENERIC-running guest.

Mechanism (source-only confirmation)

Amazon ENA virtual NIC (not present in guest). Source: ena_com_config_llq_info stores desc_num_before_header_ctrl verbatim at L663. When 0 (spec-valid ENA_ADMIN_LLQ_NUM_DESCS_BEFORE_HEADER_0), pkt_ctrl->descs_left_in_line=0 at L410 and get_sq_desc_llq indexes past the 256-byte bounce buffer.

In ena_com_config_llq_info, reject desc_num_before_header_ctrl==0 with -EINVAL (defence-in-depth).

The full git apply-able diff lives in fix.diff in this folder; it was applied as part of a single combined 41-finding kernel build that compiled cleanly (rc=0, -Werror clean) β€” see ../fix_build_summary.txt.

Build validation

  • git apply --check on this fix.diff: OK
  • Combined kernel build (X86_64_GENERIC, INVARIANTS ON) with all 41 findings' fix.diffs applied: rc=0, no warnings, no errors.
  • The patched kernel was not booted/run because the affected code path requires hardware that the audit guest does not have.

Confirmed kernel references

Detail

Exploit chain

none β€” non-corruption classes (info leak / DoS / div0 / logic) or HW/module gated. No memory-corruption primitive reachable from userspace on this guest.

Evidence (decisive lines)

Source-only confirmation. Combined kernel build with all 41 fix.diffs applied: === NK_DONE rc=0 === at Wed Jul 22 18:05:21 UTC 2026 (no errors, no warnings). See findings/poc/fix_build_summary.txt.

PoC changes

Authored findings/poc/DF-1848/fix.diff (minimal targeted guard). VERDICT.md and manifest.json written. fix.diff validated by combined build.

Verified recommended fix

In ena_com_config_llq_info, reject desc_num_before_header_ctrl==0 with -EINVAL. Full git-apply-able diff in findings/poc/DF-1848/fix.diff; validated as part of combined 41-finding kernel build (rc=0).

Verdict

SOURCE-CONFIRMED, INCONCLUSIVE-RUNTIME. The cited defect exists at sys/dev/virtual/amazon/ena/ena-com/ena_com.c:150-192. Amazon ENA virtual NIC (not present on audit guest). ena_com_config_llq_info stores desc_num_before_header_ctrl verbatim at L663. When 0 (spec-valid), pkt_ctrl->descs_left_in_line=0 (L410) and get_sq_desc_llq indexes past the 256-byte bounce buffer -> heap overflow. HW/HV gated.