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/:
- 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.
- As root, run a destroy/recreate hammer in a tight loop to drive
ath_vap_delete'sath_stoprecvagainst the live RX tasklet:
```c
/ race_ath_rxpending.c β build: cc -o race race_ath_rxpending.c /
#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); } } ```
- 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).
Recommended fix
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)
PoC verification
Evidence pack
findings/poc/DF-1667 Β· 4 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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 |
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
fixedVALIDATED: 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.
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.
No comments yet.