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=0andmax_packet_tx_descs >= 17) are inside the publishedena_admin_llq_num_descs_before_headerenum and the queue-feature descriptor, so a spec-conformant device can send them andena_com_config_dev_modewill 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 adjacentM_DEVBUFslab objects β enabling local privilege escalation inside the guest, kernel panic, or arbitrary kernel-memory write. - Required config or capabilities:
device enacompiled in; Amazon ENA virtual NIC attached in LLQ/DEV placement policy; device reportingdesc_num_before_header_ctrl == 0andmax_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.
Recommended fix
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)
PoC verification
Evidence pack
findings/poc/DF-1848 Β· 2 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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 |
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.
Recommended fix
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 --checkon 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
- s
- y
- s
- /
- d
- e
- v
- /
- v
- i
- r
- t
- u
- a
- l
- /
- a
- m
- a
- z
- o
- n
- /
- e
- n
- a
- /
- e
- n
- a
- -
- c
- o
- m
- /
- e
- n
- a
- _
- c
- o
- m
- .
- c
- :
- 1
- 5
- 0
- -
- 1
- 9
- 2
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.
No comments yet.