ath_tx_retry_clone() dmasetup-failure path: node reference leaked forever and nbf recycled to the free list with bf_m dangling at a freed mbuf
Summary
For BUSY frames retried/filtered in an aggregate session, ath_buf_clone() MOVES bf_m and the held bf_node reference into the clone nbf. If the follow-up DMA reload fails, the mbuf chain is freed inside dmasetup and the error path pushes nbf back via ath_returnbuf_head() WITHOUT decref'ing nbf->bf_node or clearing nbf->bf_m. Result: (a) one ieee80211_node+ath_node (17 TIDs, BAW arrays, rate state) leaked per occurrence - slow station/memory exhaustion under memory pressure; (b) _ath_getbuf_locked does not sanitize bf_m/bf_node on reuse, leaving a dangling mbuf pointer in a recycled kernel buffer - a latent UAF one refactor away (ath_tx_freebuf's bus_dmamap_unload on bf_m != NULL is the nearest would-be sink) and a silent violation of the ath_freebuf KASSERT contract. VERIFIED: machine-check 8/8 PASS (clone ownership move, error-path recycling without decref/detach, getbuf not clearing). Fix: detach moved ownership on the error path + clear stale pointers in _ath_getbuf_locked.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-3638 Β· 9 files| File | Type | Description | Size | |
|---|---|---|---|---|
| README.md | β | 2.1 KB | β raw | |
| VERDICT.md | β | 3.3 KB | β raw | |
| verify_df3638.py | β | 3.4 KB | view raw | |
| run.sh | β | 111 B | view raw | |
| run.log | β | 681 B | view raw | |
| env.txt | β | 708 B | view raw | |
| fix.diff | β | 1.6 KB | view raw | |
| verdict.json | β | 3.2 KB | view raw | |
| manifest.json | β | 899 B | view raw |
DF-3638 β ath_tx_retry_clone() error path leaks the node reference and recycles a buffer with dangling bf_m
What
sys/dev/netif/ath/ath/if_ath_tx.c:4567-4583 β when the DMA-map reload of a
cloned retry/filtered frame fails, the clone nbf is pushed back onto the
free list while still owning the moved bf_node reference (never
decref'd) and a bf_m pointer that dangles at the mbuf chain freed inside
ath_tx_dmasetup().
Chain (source-level, machine-verified)
ath_tx_retry_clone()is called for BUSY buffers during software retry of aggregate subframes (ath_tx_retry_subframe, if_ath_tx.c:4723) and filtered-frame handling (ath_tx_tid_filt_comp_single, if_ath_tx.c:3495).ath_buf_clone()(if_ath.c:3170) movesbf_m/bf_nodeintonbf(ownership transfer; original NULLed).ath_tx_dmasetup(sc, nbf, nbf->bf_m)fails (bus_dmamap_load NOWAIT ENOMEM / linearize failure under memory pressure) β the mbuf is freed inside dmasetup (if_ath_tx.c:320/338/349/356).- Error path returns
nbftosc->sc_txbufwithout decref'ingnbf->bf_nodeor clearingnbf->bf_m(if_ath_tx.c:4579-4581). _ath_getbuf_locked()(if_ath.c:3080) does not clearbf_m/bf_nodeon reuse β next consumer inherits the stale fields until a successful reload overwrites them.
Impact
- Permanent leak of one ieee80211_node + ath_node (β KBs each, incl. 17 TIDs, BAW tracking, rate state) per occurrence β slow station-slot/memory exhaustion (DoS) on hosts with ath(4) hardware under memory pressure.
- Latent use-after-free pointer field in a recycled kernel buffer (not
dereferenced along current call graphs, but one refactor away from a real
UAF;
ath_tx_freebuf()'sbus_dmamap_unloadonbf->bf_m != NULLis the nearest would-be sink).
Reachability
ath(4) interface + a bus_dmamap_load NOWAIT failure during retry-clone.
Guest has no Atheros hardware (see env.txt) β runtime reproduction
impossible; verify_df3638.py proves the control flow (8/8 PASS).
Files
verify_df3638.py (the PoC), run.sh, run.log, env.txt, VERDICT.md, fix.diff, verdict.json, manifest.json.
DF-3638 β ath_tx_retry_clone() error path: node-reference leak + recycled buffer with dangling bf_m
REPRODUCED: No β not at runtime (guest has no Atheros hardware; see
env.txt). Honest verdict: not_reproduced, hardware-absent.
CONFIRMED: Yes β at source level, 8/8 assertions (run.log):
ath_tx_retry_clone()(sys/dev/netif/ath/ath/if_ath_tx.c:4539) clones a BUSY retry/filtered buffer:ath_buf_clone()(if_ath.c:3170) MOVESbf_mandbf_node(the held ieee80211_node reference) intonbfβ an ownership transfer, no incref, and NULLs them in the original.ath_tx_dmasetup(sc, nbf, nbf->bf_m)(if_ath_tx.c:4568) reloads the DMA map for the moved mbuf. On failure it frees the mbuf chain itself (if_ath_tx.c:320/338/349/356) and returns error.- The error path (if_ath_tx.c:4569-4583) then does
ath_returnbuf_head(sc, nbf)and returns NULL β without droppingnbf->bf_nodeand without clearingnbf->bf_m(now dangling at the freed mbuf chain). - Consequences:
* The moved node reference is never decref'd β the ieee80211_node +
ath_node (with 17 ath_tid's, BAW arrays, rate-control state) is
leaked permanently. Repeated failures (memory-pressure windows during
aggregate retry/filter handling against a peer) leak one station each.
*
_ath_getbuf_locked()(if_ath.c:3080) sanitizes only bf_flags, bf_next, bf_last, bf_comp and bf_state on reuse β NOT bf_m/bf_node β so the recycled buffer carries a dangling mbuf pointer until a later successfulath_tx_dmasetupoverwrites it. Any future consumer that reads bf->bf_m before a successful reload (or an error-path that unloads the map, e.g.ath_tx_freebuf'sif (bf->bf_m != NULL) bus_dmamap_unload(...), if_ath.c:4888-4891) would operate on freed memory. With the current call graph the stale pointer happens to be overwritten before any such read, making this a latent use-after-free hazard and a hard reference leak today. *ath_freebuf()KASSERTs the invariant this path breaks (bf_node==NULL/bf_m==NULL at hand-in, if_ath.c:4849-4850) β release kernels compile the KASSERT out, hiding the contract violation.
EXPLOIT CHAIN: none β leak/DoS class (station-slot exhaustion, latent UAF pointer). Trigger requires ath(4) hardware and a bus_dmamap_load NOWAIT failure during retry-clone of a BUSY filtered/retried aggregate frame.
WHY NOT A FALSE POSITIVE: control flow machine-verified (8/8 PASS); inherited from FreeBSD's if_ath_tx.c (same error path upstream).
FIX VALIDATION: not_testable β no ath hardware on the guest. fix.diff detaches node/mbuf on the error path and additionally hardens _ath_getbuf_locked to clear bf_m/bf_node on every reuse (defense in depth, mirrors the existing KASSERT contract).
Files: verify_df3638.py, run.sh, run.log, env.txt, fix.diff, verdict.json, manifest.json, README.md
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 detaches node/mbuf on the error path and clears bf_m/bf_node in _ath_getbuf_locked (defense in depth matching the ath_freebuf KASSERT contract); cannot be runtime-validated without ath hardware.
findings/poc/DF-3638/fix.diff (two hunks); run.log assertions 3, 5, 7 identify exactly the lines patched.
Confirmed kernel references
- sys/dev/netif/ath/ath/if_ath_tx.c:4540
- sys/dev/netif/ath/ath/if_ath_tx.c:4568
- sys/dev/netif/ath/ath/if_ath_tx.c:4579
- sys/dev/netif/ath/ath/if_ath_tx.c:294
- sys/dev/netif/ath/ath/if_ath_tx.c:4723
- sys/dev/netif/ath/ath/if_ath_tx.c:3495
- sys/dev/netif/ath/ath/if_ath.c:3170
- sys/dev/netif/ath/ath/if_ath.c:3080
- sys/dev/netif/ath/ath/if_ath.c:4844
Detail
Evidence (decisive lines)
findings/poc/DF-3638/run.log (8/8 PASS: clone ownership move at if_ath.c:3170+, error-path recycling without decref/mbuf-detach at if_ath_tx.c:4571-4585, _ath_getbuf_locked not clearing bf_m/bf_node); VERDICT.md; env.txt.
PoC changes
No seeded PoC; authored verify_df3638.py (source-level artifact) - hardware-gated defect.
Verified recommended fix
In ath_tx_retry_clone()'s dmasetup-error path, ieee80211_node_decref(nbf->bf_node) and NULL bf_m/bf_node before ath_returnbuf_head(); harden _ath_getbuf_locked() to clear bf_m/bf_node on reuse.
Verdict
Source-level confirmed (8/8 machine-checked assertions, run.log): when the DMA reload of a cloned retry/filtered frame fails, ath_tx_retry_clone() (sys/dev/netif/ath/ath/if_ath_tx.c:4567-4583) recycles nbf via ath_returnbuf_head() without dropping the node reference moved into it by ath_buf_clone() (if_ath.c:3186-3187, 3212-3213) and without clearing nbf->bf_m, which dangles at the mbuf chain freed inside ath_tx_dmasetup() (if_ath_tx.c:320/338/349). _ath_getbuf_locked() (if_ath.c:3080) does not sanitize bf_m/bf_node on reuse, so the recycled buffer carries the stale pointers. Net effect: one ieee80211_node/ath_node leaked per occurrence (slow station/memory exhaustion DoS on ath(4) hosts under memory pressure) plus a latent UAF pointer field one refactor away from a real use-after-free. Runtime reproduction impossible on the hardware-less guest (env.txt): honest not_reproduced; impact 'none' reflects the run, not the ceiling.
No comments yet.