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

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-3638 Β· 9 files
FileTypeDescriptionSize
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) moves bf_m/bf_node into nbf (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 nbf to sc->sc_txbuf without decref'ing nbf->bf_node or clearing nbf->bf_m (if_ath_tx.c:4579-4581).
  • _ath_getbuf_locked() (if_ath.c:3080) does not clear bf_m/bf_node on 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()'s bus_dmamap_unload on bf->bf_m != NULL is 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.

VERDICT.md
↓ download raw

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):

  1. 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) MOVES bf_m and bf_node (the held ieee80211_node reference) into nbf β€” an ownership transfer, no incref, and NULLs them in the original.
  2. 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.
  3. The error path (if_ath_tx.c:4569-4583) then does ath_returnbuf_head(sc, nbf) and returns NULL β€” without dropping nbf->bf_node and without clearing nbf->bf_m (now dangling at the freed mbuf chain).
  4. 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 successful ath_tx_dmasetup overwrites 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's if (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_testable
baseline no→ patch + rebuild →patched clean

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

Confirmed kernel references

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.