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

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-3797 Β· 17 files
FileTypeDescriptionSize
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) then VICTIM 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 = 0x40190014 with 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.
VERDICT.md
↓ download raw

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)

  1. sys/dev/virtual/amazon/ena/ena-com/ena_com.c:1156-1159 β€” after the CREATE_SQ admin command completes, io_sq->idx and io_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-supplied sq_doorbell_offset (u32).
  2. 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_reg from cq_interrupt_unmask_register_offset, cq_head_db_register_offset, numa_node_register_offset β€” no validation.
  3. sys/dev/virtual/amazon/ena/ena.c:3671-3675 β€” the DragonFly port never assigns ena_dev->reg_bar (FreeBSD's attach has the extra line ena_dev->reg_bar = ((struct ena_bus*)(ena_dev->bus))->reg_bar_h;, dropped here). reg_bar therefore stays NULL, so the pointers built in (1)/(2) are the raw device offsets.
  4. sys/dev/virtual/amazon/ena/ena-com/ena_plat.h:325-329 β€” ENA_REG_WRITE32(bus, v, reg) is bus_space_write_4(reg_bar_t, reg_bar_h, (bus_size_t)reg, v), and DragonFly's x86_64 bus_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.
  5. Live sinks reachable from ena.c: ena_com_write_sq_doorbell at ena.c:1049 (RX refill), 2967/2978 (ena_start_xmit doorbells, value = io_sq->tail); ena_com_unmask_intr at 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() wrote 0x1337 (tail counter) at a device-chosen offset.
  • full-path baseline: REAL ena_com_create_io_cq() returned 0 with unmask_reg=0x86fc0008 from 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 at enarep_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 audited ena-com/ena_com.c + ena_eth_com.c into the module, builds a real admin queue (real ena_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 real ena_com_create_io_cq() and the real ena_com_unmask_intr().
  • fix.diff β€” git apply --check clean 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 (0x40190014 family) 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.diff applied β†’ 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

fixed
baseline reproduced→ patch + rebuild →patched clean

With 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.
↓ fix.diffper-fix-DF-3797

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.