ena_down tears down IO queues and frees RX buffers without draining the deferred RX cleanup task (mbuf UAF / double free)
Summary
The per-queue deferred RX cleanup task (cmpl_task, ena_rx_cleanup up to 8x256 packets, unlocked around each if_input) is enqueued by the timer whenever a ring drains. ena_down drains the timer, frees IO IRQs, destroys IO queues (freeing CQ/SQ DMA memory) and frees all RX mbufs/maps BEFORE the task is cancelled/drained - that only happens in ena_free_rx_resources reached later. A pending/running task then reads freed cdesc memory and m_freem's/m_appends freed mbufs (double free / UAF). Malicious/hypervisor peer (this file's trust model): device drains the RX ring so the timer arms the task, then withholds keep-alives to set trigger_reset -> ena_reset_task -> ena_down while the task is still pending/running - fully device-triggerable heap corruption with peer-controlled packet payloads in the freed clusters. Secondary local variant: ifconfig down during RX burst. Needs live ENA peer (guest is KVM/virtio-only, if_ena.ko probe ENXIO - verified). Structural PoC design + interleaving diagram in pack.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-3796 Β· 5 files| File | Type | Description | Size | |
|---|---|---|---|---|
| README.md | β | 3.0 KB | β raw | |
| VERDICT.md | β | 3.6 KB | β raw | |
| fix.diff | β | 1.5 KB | view raw | |
| manifest.json | β | 908 B | view raw | |
| verdict.json | β | 2.9 KB | view raw |
DF-3796 β ena_down tears down IO queues/RX buffers without draining the deferred RX cleanup task (UAF / double m_freem)
What
ena_down() (sys/dev/virtual/amazon/ena/ena.c:2604-2638) drains the timer
callout (2611), frees the IO MSI-X IRQs (2617), destroys all IO queues
(2627 β frees the CQ/SQ DMA descriptor memory), frees all RX buffers
(2630 β m_freem + bus_dmamap_unload on every rx_info) and only much
later, inside ena_free_all_rx_resources (2632 β ena_free_rx_resources,
ena.c:830-833), cancels/drains the per-queue deferred cleanup taskqueue
rx_ring->cmpl_tq / cmpl_task.
The deferred task (ena_deferred_rx_cleanup, ena.c:1574-1590 β up to
CLEAN_BUDGETΓRX_BUDGET packet batches, unlocking the ring around every
if_input call) is enqueued by the timer's check_for_empty_rx_ring
(ena.c:3498) whenever a ring's RX SQ drains completely β a condition a
burst of delivered packets (or a malicious device completing everything
and going quiet) produces on demand. ena_rx_cleanup never checks
adapter->up / IFF_RUNNING.
Interleaving (task pending or mid-run while ena_down proceeds):
deferred task ena_down (ioctl/reset task)
------------ --------------
ena_rx_cleanup: callout_drain(timer)
ena_com_rx_pkt(io_cq, ...) ena_free_io_irq
... taskqueue_thread pre-wakeup ena_destroy_all_io_queues <-- kfree(cdesc)
ena_free_all_rx_bufs <-- m_freem(map unload)
[acquires ring lock β irrelevant:
ena_free_rx_bufs takes NO ring lock,
contrast ena_free_tx_bufs:1111]
ena_com_rx_pkt reads freed cdesc (UAF read / NULL deref after
cdesc_addr.virt_addr=NULL)
ena_rx_mbuf: m = rx_info->mbuf (freed mbuf -> UAF)
... m_freem(m) / m_append (double free / UAF write)
Both teardown entry points race it: the ioctl SIOCSIFFLAGS down path
(ena.c:2417-2421) and β fully device-triggerable, no guest cooperation β
ena_reset_task (ena.c:3541-3615): a malicious device stops keep-alives
and drains the RX ring so the timer enqueues the task, then the missed
keep-alive sets trigger_reset β ena_reset_task β ena_down while the
task is still pending/running.
Why not runtime-reproduced here
The audit guest (QEMU/KVM, virtio-only β env evidence in the DF-3797 pack)
has no Amazon ENA function, so neither the RX-completion traffic that arms
the task nor ena_down on a live adapter can be driven. Source-level
certainty only; the interleaving above is structural (no synchronization
exists between the task and the frees it races).
Fix (fix.diff, git apply --check clean)
ena_down: cancel+drain every queue'scmpl_taskimmediately aftercallout_drain(only the drained timer can re-enqueue, so this is sufficient).ena_free_rx_bufs: take the ring lock, mirroringena_free_tx_bufs(ena.c:1111).
Bucket
memcorrupt (kernel heap UAF / double free driven by hostile peer timing).
DF-3796 β VERDICT
Status: NOT REPRODUCED (no ENA device on the audit guest; source-certain race)
Reproduced: 0 β with explanation
The mechanism is structural and proven by code inspection; it cannot be
runtime-driven on this guest because there is no ENA PCI function (KVM
guest, virtio-only β pciconf -l in ../DF-3797/env.txt, if_ena.ko
present but unloaded, probe ENXIO). Arming the race requires either live
RX traffic draining a ring (to get check_for_empty_rx_ring to enqueue
cmpl_task, ena.c:3498) or a malicious device's completion/keep-alive
timing, plus a concurrent ena_down.
Why the race is real (line-accurate)
- Producer:
ena_timer_serviceβcheck_for_empty_rx_ring(ena.c:3470-3506) βtaskqueue_enqueue(rx_ring->cmpl_tq, &rx_ring->cmpl_task)at 3498, fired wheneverena_com_free_desc(io_sq) == ring_size - 1twice in a row (device drained the ring β normal under burst, trivial for a malicious device that completes everything and stops delivering). - Consumer:
ena_deferred_rx_cleanup(ena.c:1574-1590) runsena_rx_cleanupup toCLEAN_BUDGET(8) Γ RX_BUDGET(256)packets, releasing the ring lock around everyif_input(ena.c:1697-1699), and never checksadapter->up/IFF_RUNNING. - Teardown:
ena_down(ena.c:2604-2638) order is:callout_drain(timer):2611 βena_free_io_irq:2617 βena_destroy_all_io_queues:2627 (kfree of CQ/SQ DMA memory in ena_com_io_queue_free, ena-com/ena_com.c:828-863;cdesc_addr.virt_addrfreed then NULLed) βena_free_all_rx_bufs:2630 (m_freem+bus_dmamap_unloadper buffer, no ring lock β compareena_free_tx_bufswhich locks at ena.c:1111) β β¦ βena_free_all_rx_resources:2632, which is the FIRST place the task is cancelled/drained (ena.c:830-833). ena_destroy_all_io_queuesperforms admin DESTROY_SQ/CQ round trips (millisecond-scale), so the window between the frees in (3) and the drain at the end of (3) is wide.- Second entry:
ena_reset_task(ena.c:3541-3615) calls the sameena_downunderioctl_lockwith the timer already stopped β a task enqueued one tick earlier is still pending when the frees run. A malicious device can arm this deterministically (drain ring β task enqueued; withhold keep-alive β trigger_reset β reset task β ena_down).
Consequences in the window:
- ena_com_rx_pkt on freed/NULLed cdesc memory (UAF read, then
NULL+offset deref β panic).
- ena_rx_mbuf operating on rx_info->mbuf freed by
ena_free_all_rx_bufs: mbuf handed to if_input after free, or
m_freem/m_append on freed clusters β mbuf-zone UAF / double
free (heap corruption; peer controls packet payloads that inhabited
the clusters).
Fix
fix.diff (applies clean): drain cmpl_task per queue right after the
timer drain in ena_down; take the ring lock in ena_free_rx_bufs.
Not runtime-validated (same hardware constraint); logic is the standard
taskqueue-cancel-before-free pattern already used by
ena_free_rx_resources.
Impact assessment
Device-timing-driven kernel heap corruption (mbuf double-free/UAF) or
deterministic panic; on the malicious-peer threat model this file is
audited under, High. As a local-only bug it needs root to run
ifconfig down concurrently with RX burst β secondary.
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
not_testablefix.diff applies clean (git apply --check) and implements the standard cancel/drain-before-free pattern; runtime validation impossible on this guest for the same no-device reason as the baseline.
fix.diff hunks @@ -1058 (ring lock in ena_free_rx_bufs) and @@ -2615 (task drain in ena_down)
Confirmed kernel references
Detail
Evidence (decisive lines)
VERDICT.md interleaving + line citations (ena.c:3498, 1574-1590, 1697-1699, 2611-2632, 830-833, 1111); fix.diff applies clean (git apply --check); module-presence/env evidence in ../DF-3797/env.txt
PoC changes
No runnable PoC possible without the device; pack documents the structural interleaving and the fix.
Verified recommended fix
Drain cmpl_task per queue in ena_down right after callout_drain (only the drained timer re-enqueues it), and take the rx ring lock in ena_free_rx_bufs mirroring ena_free_tx_bufs (fix.diff).
Verdict
Source-certain teardown race, not runtime-reproducible on this guest (no ENA PCI device; if_ena.ko present but probe fails). ena_down (ena.c:2604-2638) destroys IO queues and frees RX mbufs/maps (2627-2630, ena_free_rx_bufs takes no ring lock) BEFORE the deferred RX cleanup task is cancelled/drained β that only happens in ena_free_rx_resources (ena.c:830-833) via ena_free_all_rx_resources at 2632, after the frees. The task is enqueued by the timer on ring-drain (ena.c:3498), runs ena_rx_cleanup up to 8x256 packets unlocking around if_input (ena.c:1697-1699), and never checks adapter->up. A pending/running task therefore operates on freed cdesc memory (UAF read/NULL deref) and freed mbufs (double m_freem / UAF), including via the fully device-triggerable ena_reset_task path.
No comments yet.