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

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-3796 Β· 5 files
FileTypeDescriptionSize
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)

  1. ena_down: cancel+drain every queue's cmpl_task immediately after callout_drain (only the drained timer can re-enqueue, so this is sufficient).
  2. ena_free_rx_bufs: take the ring lock, mirroring ena_free_tx_bufs (ena.c:1111).

Bucket

memcorrupt (kernel heap UAF / double free driven by hostile peer timing).

VERDICT.md
↓ download raw

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)

  1. 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 whenever ena_com_free_desc(io_sq) == ring_size - 1 twice in a row (device drained the ring β€” normal under burst, trivial for a malicious device that completes everything and stops delivering).
  2. Consumer: ena_deferred_rx_cleanup (ena.c:1574-1590) runs ena_rx_cleanup up to CLEAN_BUDGET(8) Γ— RX_BUDGET(256) packets, releasing the ring lock around every if_input (ena.c:1697-1699), and never checks adapter->up/IFF_RUNNING.
  3. 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_addr freed then NULLed) β†’ ena_free_all_rx_bufs :2630 (m_freem + bus_dmamap_unload per buffer, no ring lock β€” compare ena_free_tx_bufs which 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).
  4. ena_destroy_all_io_queues performs 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.
  5. Second entry: ena_reset_task (ena.c:3541-3615) calls the same ena_down under ioctl_lock with 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_testable
baseline no→ patch + rebuild →patched clean

fix.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)
↓ fix.diffper-fix-DF-3796

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.