Device-supplied register offsets from CREATE_SQ/CREATE_CQ replies cached unvalidated β every doorbell/unmask becomes an arbitrary-offset kernel write by the guest CPU
Summary
ena_com_create_io_sq/create_io_cq store sq_doorbell_offset, cq_interrupt_unmask_register_offset, cq_head_db_register_offset and numa_node_register_offset - raw u32 values from device DMA replies - as register pointers with no validation. This DragonFly port never assigns ena_dev->reg_bar (dropped FreeBSD's reg_bar=reg_bar_h line), so the pointers are the raw device offsets and ENA_REG_WRITE32 is a plain CPU store at BAR-KVA + attacker-chosen u32. Sinks: RX-refill and xmit doorbells and every IO MSI-X unmask. REPRODUCED on the audit guest (protocol-replica, no ENA hardware needed): full-path replica compiles the unmodified audited ena-com sources, emulates the device completion, drives the REAL ena_com_create_io_cq() (returned 0, unmask_reg=0x86fc0008) and the REAL ena_com_unmask_intr() -> 'VICTIM OVERWRITTEN via device-chosen register offset'. Fatal variant: 'Fatal trap 12: page fault while in kernel mode, supervisor write' at BAR+0x7ffffff8; DDB: 'Stopped at enarep_run+0x3b9: movl $0x40190014,0x7ffffff8(%rax)'. Fix validated: patched build -> EINVAL, no write, guest up. Malicious nitro card / hostile hypervisor: guest-CPU-executed u32 write anywhere in [BAR KVA, +4G) - bypasses IOMMU device isolation entirely (not a device DMA). Primitive proven; full chain requires a live malicious peer for value grooming.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-3797 Β· 17 files| File | Type | Description | Size | |
|---|---|---|---|---|
| README.md | β | 4.0 KB | β raw | |
| VERDICT.md | β | 5.9 KB | β raw | |
| enarep.c | β | 8.0 KB | view raw | |
| enafull.c | β | 8.4 KB | view raw | |
| Makefile | β | 439 B | β download | |
| Makefile.full | β | 798 B | β download | |
| build.sh | β | 161 B | view raw | |
| run.sh | β | 175 B | view raw | |
| build.log | β | 1.8 KB | view raw | |
| run.log | β | 933 B | view raw | |
| fix_build.log | β | 88 B | view raw | |
| fix_run.log | β | 374 B | view raw | |
| panic.txt | β | 2.8 KB | view raw | |
| env.txt | β | 1.0 KB | view raw | |
| fix.diff | β | 3.0 KB | view raw | |
| verdict.json | β | 4.2 KB | view raw | |
| ena-com-fix/ | β | 4.0 KB | β download |
DF-3797 β ENA device-supplied register offsets (CREATE_SQ/CREATE_CQ replies) used unvalidated as kernel write targets
What (one paragraph)
ena_com_create_io_sq() stores cmd_completion.sq_doorbell_offset and
ena_com_create_io_cq() stores cq_interrupt_unmask_register_offset,
cq_head_db_register_offset and numa_node_register_offset β all raw
32-bit values from device DMA replies β as register pointers, with no
bounds validation (sys/dev/virtual/amazon/ena/ena-com/ena_com.c:1156-1159,
1333-1346). On this DragonFly port ena_dev->reg_bar is never assigned
(ena.c:3671-3675 dropped FreeBSD's reg_bar = reg_bar_h line), so the
pointers are the raw device offsets, and ENA_REG_WRITE32
(ena-com/ena_plat.h:325-329 β bus_space_write_4, sys/cpu/x86_64/include/bus_dma.h:404-410)
expands to a plain CPU store at (BAR KVA + attacker-chosen u32).
Every SQ doorbell (ena_com_write_sq_doorbell, called from ena.c:1049,
2967, 2978) and every interrupt unmask (ena_com_unmask_intr, called from
ena.c:1813, 2168) then writes attacker-placed kernel addresses. A malicious
"device" (compromised nitro card / hostile hypervisor on the EC2
host-trust boundary) gets a guest-CPU-executed, arbitrary-offset u32
write into kernel virtual memory β immune to IOMMU containment because
it is not a device DMA at all.
Threat model
Amazon ENA is a paravirtual NIC: the peer is the host/nitro card. Same boundary as vmxnet3. A malicious peer that controls admin-completion content controls these offsets.
Reproduce (guest is KVM without ENA hardware β protocol-replica method)
# host side cd findings/poc/DF-3797 tar czf /tmp/enarep.tgz Makefile Makefile.full enarep.c enafull.c ena-com ena-com-fix scp -F dfbsd-qemu/config /tmp/enarep.tgz dfbsd:/tmp/ # guest side (root) mkdir -p /root/kmod/enarep && cd /root/kmod/enarep && tar xzf /tmp/enarep.tgz make # inline replica kldload ./enarep.ko sysctl hw.enarep=1 # CQ-unmask write lands on out-of-BAR victim mapping sysctl hw.enarep=2 # SQ-doorbell value channel (tail counter) sysctl hw.enarep=3 # FATAL: unmapped-KVA write -> kernel panic # full-path replica: REAL ena_com_create_io_cq through an emulated admin queue make -f Makefile.full && kldload ./enafull.ko && sysctl hw.enafull=1 # fix validation mkdir -p ../fixbuild && cp -r ena-com-fix enafull.c Makefile.full ../fixbuild/ ( cd ../fixbuild && make -f Makefile.full FIXED=1 ) kldload ../fixbuild/enafull.ko && sysctl hw.enafull=1 # -> EINVAL, no write
Expected
hw.enarep=1/2:enarep: after victim ...: <written value>β an object in a kernel mapping outside the BAR stand-in is modified by the real driver inline functions, offset fully chosen by the emulated device.hw.enarep=3:Fatal trap 12: page fault while in kernel mode,fault code = supervisor write data, guest down (see panic.txt).- full-path baseline:
ena_com_create_io_cq() = 0 (io_cq.idx=7 unmask_reg=0x86fc0008)thenVICTIM OVERWRITTEN via device-chosen register offset β primitive confirmed. - fixed build:
ena_com_create_io_cq() = 22 ... NO WRITE PERFORMED (fix active), guest stays up.
Fix
fix.diff β remember the register BAR extent
(ena_dev->reg_bar_len = rman_get_size(), ena.c) and reject CREATE_SQ /
CREATE_CQ replies whose offsets fall outside it. Validated baseline vs
patched on the guest (see VERDICT.md).
Value/address constraints (honest exploitation assessment)
- unmask / head-db / numa sinks write mostly fixed values
(
intr_control = 0x40190014with the driver's constant intervals). - the SQ-doorbell sink writes
io_sq->tail, a u16 ring position that the peer can advance by an arbitrary amount via delivered TX traffic β a groomable small-integer value. - offset: any u32 in [0, 4G) relative to the register BAR's KVA. No uid0 chain was built (needs a live malicious device to drive the ring); the primitive itself β arbitrary-offset kernel write executed by the guest CPU β is proven end to end.
DF-3797 β VERDICT
Status: REPRODUCED (protocol-replica on the audit guest; no ENA hardware present)
Reproduced: 1 β mechanism, sink and full command path proven with the driver's own code; fatal-panic variant captured; fix validated baseline vs patched on the same guest.
Root cause (audited tree, line-accurate)
sys/dev/virtual/amazon/ena/ena-com/ena_com.c:1156-1159β after the CREATE_SQ admin command completes,io_sq->idxandio_sq->db_addr = (u32 *)((uintptr_t)ena_dev->reg_bar + (uintptr_t)cmd_completion.sq_doorbell_offset)are stored with no validation of the device-suppliedsq_doorbell_offset(u32).sys/dev/virtual/amazon/ena/ena-com/ena_com.c:1333-1346β same for the CREATE_CQ reply:unmask_reg,cq_head_db_reg,numa_node_cfg_regfromcq_interrupt_unmask_register_offset,cq_head_db_register_offset,numa_node_register_offsetβ no validation.sys/dev/virtual/amazon/ena/ena.c:3671-3675β the DragonFly port never assignsena_dev->reg_bar(FreeBSD's attach has the extra lineena_dev->reg_bar = ((struct ena_bus*)(ena_dev->bus))->reg_bar_h;, dropped here).reg_bartherefore stays NULL, so the pointers built in (1)/(2) are the raw device offsets.sys/dev/virtual/amazon/ena/ena-com/ena_plat.h:325-329βENA_REG_WRITE32(bus, v, reg)isbus_space_write_4(reg_bar_t, reg_bar_h, (bus_size_t)reg, v), and DragonFly's x86_64bus_space_write_4(sys/cpu/x86_64/include/bus_dma.h:404-410) is*(volatile u_int32_t *)(bsh + offset) = valueβ a plain CPU store at BAR-KVA + offset.- Live sinks reachable from ena.c:
ena_com_write_sq_doorbellat ena.c:1049 (RX refill), 2967/2978 (ena_start_xmit doorbells, value =io_sq->tail);ena_com_unmask_intrat ena.c:1813 (every IO MSI-X), 2168 (ena_unmask_all_io_irqs).
Net effect: a malicious peer controls a u32 write offset into
[BAR KVA, +4G) of kernel virtual space and semi-controls the value (fixed
intr_control bits for the unmask sink; the groomable tail counter for
the doorbell sink). The store is executed by the guest CPU, so IOMMU
device-isolation is irrelevant β this is the classic escape for a
compartmentalized smart-NIC threat model, and a memory-corruption
primitive under the hypervisor-trust model this file is audited under.
Evidence (this pack)
run.logβ decisive dmesg lines:- inline replica mode 1: BAR=0xfffff80118830000, device offset 0x1008 β
real
ena_com_unmask_intr()overwrote a sentinel in an adjacent, unrelated kernel mapping (0xfffff80118831008):a1b2c3d4 β 40190014. - mode 2: real
ena_com_write_sq_doorbell()wrote0x1337(tail counter) at a device-chosen offset. - full-path baseline: REAL
ena_com_create_io_cq()returned 0 withunmask_reg=0x86fc0008from an emulated hostile completion;VICTIM OVERWRITTEN via device-chosen register offset β primitive confirmed. panic.txtβ mode 3:Fatal trap 12: page fault while in kernel mode,fault virtual address = 0xfffff80196b9eff8(= BAR + 0x7ffffff8),fault code = supervisor write data, page not present, DDB stopped atenarep_run+0x3b9: movl $0x40190014,0x7ffffff8(%rax)β the literal store instruction.fix_run.logβ patched build (fix.diff applied to the shipped ena-com copy,reg_bar_len = PAGE_SIZE): same hostile completion βena_com_create_io_cq() = 22,unmask_reg=0,NO WRITE PERFORMED (fix active),GUEST_STILL_UP.build.log/fix_build.logβ untrimmed builds (baseline and-DENAFULL_FIXED).enarep.cβ inline replica (real driver static inlines + hostile reply values; the only simulated bytes are the device-reply fields).enafull.cβ full-path replica: compiles the unmodified auditedena-com/ena_com.c+ena_eth_com.cinto the module, builds a real admin queue (realena_com_init_comp_ctxt, real submit β doorbell β polling completion path), lays a hostile CREATE_CQ completion into the admin CQ (the exact memory a malicious device DMA-writes), then calls the realena_com_create_io_cq()and the realena_com_unmask_intr().fix.diffβgit apply --checkclean against sys/.env.txtβ guest uname, cc 8.3, if_ena.ko present (not loaded, no ENA PCI device: expected on KVM).
Why not runtime-driven from a real device
The audit guest is QEMU/KVM with virtio only; no Amazon ENA function exists (pciconf in env.txt). The replica method (established for this audit by DF-3779/DF-3715) runs the driver's real code against the hostile peer bytes, which is the entire attack-relevant surface here.
Exploitation ceiling (honest)
- Primitive: arbitrary-offset u32 write in [BAR, BAR+4G), value either
fixed (
0x40190014family) or a groomable u16-derived counter (io_sq->tail), repeatable (every interrupt unmask, every doorbell). - Target selection depends on guest KVA layout: the BAR is mapped in the kernel map; the +4G window covers subsequent kernel-map allocations (other device mappings on this guest; heap depending on layout β in the observed runs kmalloc pages sat below the BAR, out of unsigned reach, while adjacent pmap_mapdev mappings were reachable).
- No uid=0 chain assembled: driving
tail/traffic grooming requires a live malicious peer. Demonstrated impact is kernel-memory corruption of a chosen KVA (sentinel) and a deterministic guest panic.
Fix validation
- baseline: overwrite via real path β reproduced (run.log).
- patched:
fix.diffapplied β EINVAL from create, no write, guest up (fix_run.log). fix_status = fixed.
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
fixedWith fix.diff applied to the ena-com copy, the identical hostile CREATE_CQ completion is rejected (ena_com_create_io_cq -> ENA_COM_INVAL=22), unmask_reg stays NULL, no write occurs, guest remains up. Baseline on the same guest overwrote the victim through the real path.
fix_run.log: 'ena_com_create_io_cq() = 22 (io_cq.idx=0 unmask_reg=0)' + 'NO WRITE PERFORMED (fix active)' + GUEST_STILL_UP; baseline in run.log: 'VICTIM OVERWRITTEN ... primitive confirmed'; fix_build.log shows -DENAFULL_FIXED build.
Confirmed kernel references
Detail
Exploit chain
malicious ENA peer returns cq_interrupt_unmask_register_offset/sq_doorbell_offset pointing at target KVA (BAR-relative u32) -> driver caches pointer on queue create -> every MSI-X unmask / TX doorbell writes intr_control (0x40190014, fixed bits) or io_sq->tail (u16 ring position groomable via peer-delivered traffic) at the chosen kernel VA, executed by the guest CPU, bypassing IOMMU containment; deterministic panic variant writes at unmapped KVA. No uid0 chain (needs live malicious peer for value grooming).
Evidence (decisive lines)
run.log (full-path baseline 'VICTIM OVERWRITTEN via device-chosen register offset' + inline modes 1/2), panic.txt ('Stopped at enarep_run+0x3b9: movl $0x40190014,0x7ffffff8(%rax)'), fix_run.log ('ena_com_create_io_cq() = 22 ... NO WRITE PERFORMED (fix active)'), enafull.c/enarep.c sources, fix.diff (git apply --check clean)
PoC changes
Seed had no runnable PoC. Built two replicas: (1) inline replica using the driver's real static inlines with hostile reply values; (2) full-path replica compiling the unmodified audited ena-com sources and driving the real admin submit->completion->create_io_cq path with an emulated device completion; required adding ena_log_level/ena_dma_alloc stand-ins (they live in ena.c), device_if/bus_if/pci_if generated headers, and COPTS (not CFLAGS) for the FIXED=1 variant.
Verified recommended fix
Store the register BAR extent (rman_get_size) in ena_com_dev and reject CREATE_SQ/CREATE_CQ replies whose doorbell/unmask/head-db/numa offsets fall outside it (fix.diff).
Verdict
Device-supplied register offsets from ENA CREATE_SQ/CREATE_CQ admin replies (ena_com.c:1156-1159, 1333-1346) are cached unvalidated as write targets; with the port's never-assigned reg_bar (ena.c:3671-3675) they arrive raw and every doorbell/unmask becomes a guest-CPU store at BAR+u32 via bus_space_write_4 (ena_plat.h:325-329). Proven on the audit guest with the driver's real code: full-path replica drove real ena_com_create_io_cq() with a hostile completion (unmask_reg=0x86fc0008) and the real ena_com_unmask_intr() overwrote a chosen kernel-VA sentinel; fatal variant produced 'Fatal trap 12: supervisor write' at BAR+0x7ffffff8 (DDB shows the literal movl); fix.diff validated baseline-overwrite vs patched-EINVAL-no-write.
No comments yet.