ath_tx_start() discards ath_tx_normal_setup() failure ('done: return 0') β leaked ath_buf + 10-descriptor slab + dmamap per failure, phantom TIM set, caller told success
Summary
ath_tx_start() stores the setup result in r, jumps to done: on failure, but done: unconditionally returns 0. Every setup failure path (bus_dmamap_load NOWAIT ENOMEM, defrag/linearize failure, or a type-3 injected frame on the legacy raw path) has already freed the mbuf inside ath_tx_dmasetup and leaves the ath_buf obtained by the caller un-queued and un-returned. The caller believes TX succeeded, sets the watchdog timer, and calls ath_tx_update_tim marking a power-save TIM with nothing queued. Each occurrence permanently removes one ath_buf plus its private 10-descriptor slab and dmamap from the fixed pools; sustained failures under attacker-assistable memory pressure drain sc_txbuf and wedge TX (persistent DoS, needs reset). VERIFIED: source-level control-flow machine-check (verify_df3637.py 8/8 PASS incl. 'done: block returns literal 0'). No memory corruption. Guest hardware-absent: not_reproduced at runtime, source-certain. One-line fix: return r.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-3637 Β· 9 files| File | Type | Description | Size | |
|---|---|---|---|---|
| README.md | β | 2.9 KB | β raw | |
| VERDICT.md | β | 3.3 KB | β raw | |
| verify_df3637.py | β | 2.5 KB | view raw | |
| run.sh | β | 111 B | view raw | |
| run.log | β | 619 B | view raw | |
| env.txt | β | 708 B | view raw | |
| fix.diff | β | 296 B | view raw | |
| verdict.json | β | 3.2 KB | view raw | |
| manifest.json | β | 890 B | view raw |
DF-3637 β ath_tx_start() swallows ath_tx_normal_setup() failure: leaked ath_buf + descriptor slab, phantom TIM
What
sys/dev/netif/ath/ath/if_ath_tx.c:2081-2082 β ath_tx_start() stores the
result of ath_tx_normal_setup() in r, jumps to done: on failure, but
done: unconditionally return 0;. The failure is never propagated.
Consequence (source-level chain)
ath_tx_normal_setup()(if_ath_tx.c:1557) callsath_tx_dmasetup()(if_ath_tx.c:294). Onbus_dmamap_load*failure (ENOMEM with BUS_DMA_NOWAIT, or linearize/defrag failure under memory pressure) the mbuf is freed insideath_tx_dmasetup()(if_ath_tx.c:320/338/349) and a non-zero error is returned. The "bogus frame type" branch (if_ath_tx.c:1706-1712) also frees the mbuf and returns EIO (reachable with an injected type-3 frame via the params==NULL raw path, which needs privileged BPF access).ath_tx_start()seesr != 0, jumps todone:, returns 0.- The
bfobtained by the caller (e.g.ath_raw_xmit()if_ath_tx.c:2407ath_getbuf();ath_transmit()in if_ath.c) is now: not queued anywhere, not returned tosc->sc_txbuf, mbuf already freed,bf_mnever set. The caller believes TX succeeded:ath_raw_xmit()proceeds to setsc->sc_wd_timer = 5and β worse β callsath_tx_update_tim(sc, ni, 1)(if_ath_tx.c:2444), marking the power-save TIM for the node even though nothing is queued. - Each occurrence permanently leaks one
ath_bufplus its private block of ATH_MAX_SCATTER(10) DMA descriptors (allocated 1:10 inath_desc_alloc(), if_ath.c:3813-3816) and its bus_dmamap. ATH_TXBUF is a fixed pool (~500); exhaustion wedges TX on the interface (persistent local/remote-assisted DoS under memory pressure).
Reachability
Requires an ath(4) interface and either (a) bus_dmamap_load_mbuf* NOWAIT
failure β attacker-assistable by driving the system into mbuf/DMA-map
pressure while aggregated TX retries clone buffers β or (b) an injected
802.11 frame with FC type bits 3 via the legacy raw path (needs write access
to /dev/bpf on a monitor vap, i.e. root). Guest has no ath hardware β
runtime reproduction is impossible here; this pack carries the source-level
proof instead.
How to verify (what run.sh does)
verify_df3637.py walks the exact cited lines in the read-only sys/ tree and
asserts: (1) r = ath_tx_normal_setup(...) is followed by
if (r != 0) goto done;; (2) the done: label returns literal 0;
(3) every error return of ath_tx_dmasetup frees the mbuf first. Exit 0 ==
defect confirmed at source level.
Files
verify_df3637.pyβ source-level verification script (the "PoC")run.shβ runs it, tees to run.logrun.logβ decisive outputenv.txtβ audit-host + guest environmentVERDICT.mdβ full narrativefix.diffβ the one-line fix, never applied to sys/verdict.json/manifest.json
DF-3637 β ath_tx_start() swallows ath_tx_normal_setup() failure
REPRODUCED: No β not at runtime. The DragonFly audit guest
(6.5-DEVELOPMENT X86_64_GENERIC, vtnet0/lo0 only) has no Atheros PCI/USB
hardware; if_ath is compiled in but no device attaches (dev.ath sysctl
tree absent, no wlan interfaces). The TX software-queue engine cannot be
exercised on this machine, so the honest runtime verdict is not_reproduced
with hardware-absent recorded in env.txt.
CONFIRMED: Yes β at source level, 8/8 assertions (run.log):
ath_tx_start()(sys/dev/netif/ath/ath/if_ath_tx.c:1866) stores the result ofath_tx_normal_setup()inrat if_ath_tx.c:2021.- On failure (
r != 0) it jumps todone:(if_ath_tx.c:2023-2024) which unconditionallyreturn 0;(if_ath_tx.c:2081-2082) β the error is discarded. - Every failure mode inside
ath_tx_normal_setup()/ath_tx_dmasetup()that can producer != 0has already freed the caller's mbuf (if_ath_tx.c:320, 338, 349, 356, 1599, 1711) β so the mbuf is not leaked, but theath_bufobtained by the caller IS: *ath_raw_xmit()grabsbf = ath_getbuf(sc, ATH_BUFTYPE_MGMT)at if_ath_tx.c:2407 and only returns it viagoto bad2when it sees a non-zero return (if_ath_tx.c:2422-2425). With the swallowed error it takes the success path: setssc->sc_wd_timer = 5, callsath_tx_update_tim(sc, ni, 1)(if_ath_tx.c:2444) β asserting a TIM for a node with nothing queued β and returns 0. *ath_transmit()(if_ath.c) has the same success/failure contract. - Each leaked
ath_bufpermanently removes one buffer plus its private 10-descriptor slab (ATH_MAX_SCATTER=10 per buffer,ath_descdma_setup(..., ath_txbuf, ATH_MAX_SCATTER)at if_ath.c:3813-3816) and its bus_dmamap from the fixed pools. Sustained failure (bus_dmamap_load NOWAIT ENOMEM under memory pressure β attacker-assistable by mbuf/cluster pressure against an associated peer β or a type-3 injected frame on the privileged legacy raw path, if_ath_tx.c:1706-1712) drainssc->sc_txbufuntil_ath_getbuf_lockedreturns NULL (ast_tx_nobuf) and the interface can no longer transmit: a persistent wedge requiring reset.
EXPLOIT CHAIN: none (no memory corruption; resource-exhaustion DoS class). Local or remote-assisted (association + memory pressure) on hosts with ath(4) hardware.
WHY NOT A FALSE POSITIVE: the control flow is machine-verified
(verify_df3637.py, 8/8 PASS); FreeBSD history keeps the same done:
return 0; β this is an upstream-inherited defect, not a DragonFly-only
artifact.
FIX VALIDATION: not_testable β requires ath(4) hardware to observe the
pool drain. fix.diff is the trivially-correct one-liner (return r;);
both paths reaching done: carry the intended value (r==0 on success).
Files: verify_df3637.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 changes 'return 0;' to 'return r;' at the done: label - trivially correct since r==0 on the success fall-through; cannot be runtime-validated on the hardware-less guest.
findings/poc/DF-3637/fix.diff; control-flow proof in run.log lines 3-4.
Confirmed kernel references
- sys/dev/netif/ath/ath/if_ath_tx.c:2021
- sys/dev/netif/ath/ath/if_ath_tx.c:2023
- sys/dev/netif/ath/ath/if_ath_tx.c:2081
- sys/dev/netif/ath/ath/if_ath_tx.c:294
- sys/dev/netif/ath/ath/if_ath_tx.c:320
- sys/dev/netif/ath/ath/if_ath_tx.c:2407
- sys/dev/netif/ath/ath/if_ath_tx.c:2422
- sys/dev/netif/ath/ath/if_ath_tx.c:2444
- sys/dev/netif/ath/ath/if_ath.c:3813
Detail
Evidence (decisive lines)
findings/poc/DF-3637/run.log (8/8 PASS incl. 'done: block returns literal 0 (r discarded)' citing if_ath_tx.c:2081-2084 and the freed-mbuf error paths of ath_tx_dmasetup at :320/338/349); VERDICT.md narrative; env.txt hardware-absence record for the guest.
PoC changes
No seeded PoC existed; authored a source-level verification script (verify_df3637.py) as the reproducible artifact because the defect is hardware-gated (no ath NIC on guest).
Verified recommended fix
ath_tx_start(): return r instead of 0 at the done: label (if_ath_tx.c:2082).
Verdict
Source-level confirmed (8/8 machine-checked assertions, run.log): ath_tx_start() at sys/dev/netif/ath/ath/if_ath_tx.c:2081-2082 discards the error from ath_tx_normal_setup() (r set at :2021, 'if (r != 0) goto done;' at :2023-2024, 'done: return 0;'), so every setup failure (busdmamap_load NOWAIT ENOMEM, linearize failure, injected type-3 frame) permanently leaks one ath_buf plus its 10-descriptor slab and dmamap, while the caller (ath_raw_xmit :2422 / ath_transmit) believes TX succeeded and even sets the node TIM (:2444). Fixed ATH_TXBUF pool drains to ast_tx_nobuf and the interface wedges - a persistent DoS on ath(4) hosts under memory pressure. Runtime reproduction was impossible: the audit guest (DragonFly 6.5-DEVELOPMENT X86_64_GENERIC) has no Atheros hardware (dev.ath sysctl tree absent, only vtnet0/lo0), so honest status is not_reproduced/hardware-absent; impact 'none' reflects the run, not the defect's ceiling (DoS).
No comments yet.