ath LED blink callout is re-armed by draining tx-completion tasks AFTER ath_stop()'s single cancel β no drain in ath_detach, callout fires into freed softc/HAL (use-after-free)
Summary
taskqueue_free runs pending tx-completion tasks whose ath_led_event re-arms sc_ledtimer after ath_stop's single cancel; no post-drain cancel exists - the timer fires up to ~500ms later, reading sc_ah from freed memory and calling through the freed HAL. Ath-hardware + teardown-race gated (QEMU has no ath emulation). Post-drain-callout_cancel fix.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-5157 Β· 6 files| File | Type | Description | Size | |
|---|---|---|---|---|
| README.md | β | 2.9 KB | β raw | |
| VERDICT.md | β | 5.0 KB | β raw | |
| env.txt | β | 389 B | view raw | |
| fix.diff | β | 591 B | view raw | |
| verdict.json | β | 3.6 KB | view raw | |
| manifest.json | β | 1.1 KB | view raw |
DF-5157 β ath LED blink callout survives ath_detach() teardown (re-armed by a draining tx task) β evidence pack
Status: UNTESTED on the audit guest (no Atheros NIC exists in QEMU; there is
no ath hardware emulation). The finding rests on a verified line-by-line
code trace of the teardown ordering. See VERDICT.md.
What the finding is
ath_led_event() (if_ath_led.c:188-195) is called from tx-completion
taskqueue workers (if_ath.c:4570, 4628, 4676) with no sc_running gate and no
lock held (the LED call sits between the ATH_LOCK sections at if_ath.c:4554
and 4573). It re-arms the LED callout chain
(ath_led_blink -> ath_led_off -> ath_led_done, if_ath_led.c:179-186)
which keeps sc_ledtimer continuously armed for the whole blink cycle
(ledon/ledoff up to 500ms/130ms, if_ath.c:6213-6232).
ath_detach() (if_ath.c:1385-1436) does:
ath_stop(sc)β cancelssc_ledtimeronce (if_ath.c:2703) β then disables interrupts (2713);taskqueue_free(sc->sc_tq)(if_ath.c:1416) β which runs every still pending task (taskqueue_run(queue, 1), kern/subr_taskqueue.c:151-157). A tx-completion task that was enqueued before interrupts were disabled now executes and callsath_led_event()->ath_led_blink()->callout_reset(&sc->sc_ledtimer, ...)β after the cancel, re-arming the timer;ath_hal_detach()(if_ath.c:1435) frees the HAL; the softc is freed by newbus when the device is destroyed after detach returns.
Nothing cancels/drains sc_ledtimer after step 2. The re-armed callout fires
ath_led_off() (if_ath_led.c:166-173) hundreds of milliseconds later β well
after detach completed β dereferencing the freed softc and the freed HAL
(sc->sc_ah), then re-arms ath_led_done() which writes sc->sc_blinking on
freed memory: a use-after-free (read + call through freed HAL, write to freed
softc).
Preconditions: Atheros NIC present; sc_softled active (auto-on for
AR5212_DEVID_IBM/AR5211_DEVID, if_ath.c:908, or enabled at runtime by root via
dev.ath.X.softled, if_ath_sysctl.c:189-206); a tx completion pending/in
flight at teardown (interface under load during kldunload if_ath /
cardbus-PCI hot removal).
Why untested (and what would be needed)
The audit guest is QEMU 7.2 with virtio/e1000-class devices only (see
env.txt: pciconf -l); QEMU has no Atheros WLAN emulation, so ath0 cannot
exist. A reproduction would need real IBM-devid ath hardware (or any ath NIC +
sysctl dev.ath.X.softled=1) under tx load, then unplug/unload during a
blink β hardware-lab work outside this environment.
Fix
fix.diff β re-cancel sc_ledtimer in ath_detach() immediately after
taskqueue_free(), when no task can re-arm it again (DragonFly's
callout_cancel is synchronous β kern_timeout.c:1032-1034 β and CANCEL takes
precedence over a handler-side RESET at INPROG-clear, kern_timeout.c:218-229,
so one cancel after the task drain fully quenches the chain).
DF-5157 VERDICT β UNTESTED (no ath hardware on the guest); code-trace confidence
Bottom line: the LED blink callout (sc_ledtimer) can be re-armed after
its only teardown cancellation, because ath_detach() cancels it in
ath_stop() (if_ath.c:2703) before draining the tx taskqueue
(taskqueue_free(sc->sc_tq), if_ath.c:1416), and the drained tx-completion
tasks unconditionally call ath_led_event() (if_ath.c:4570/4628/4676) which
re-arms the chain (if_ath_led.c:185). No later cancel/drain exists, so the
timer fires into a detached/freed softc and freed HAL: use-after-free.
Not runnable on the audit guest: QEMU has no Atheros NIC emulation.
Trace (every step cited)
- Arming path. tx completion β
ath_tx_proc_q*()workers onsc->sc_tqcallif (sc->sc_softled) ath_led_event(sc, sc->sc_txrix)(if_ath.c:4570, 4628, 4676) β outside any ATH_LOCK section (lock sections bracket it at 4554-4556 / 4573-4575).ath_led_event(if_ath_led.c:188) βath_led_blink(179) βcallout_reset(&sc->sc_ledtimer, on, ath_led_off, sc)(185).ath_led_off(166) re-armsath_led_done(172). During blinking the callout is thus armed essentially always, with periods from the rate map: ledon up to 500ms / ledoff 130ms defaults (if_ath.c:6213-6214) or the blink-rate table (6231-6232). - Teardown ordering.
ath_detach(if_ath.c:1385):ath_stop(1411) β inside, gated only bysc_runningand!sc->sc_invalid(2700):callout_cancel(&sc->sc_ledtimer)(2703, DragonFly branch), thenath_hal_intrset(ah, 0)(2713) β but a tx task enqueued before that is still queued. Back inath_detach:taskqueue_free(sc->sc_tq)(1416) which runs all pending tasks synchronously (kern/subr_taskqueue.c:151-157:taskqueue_run(queue, 1)before terminate) β that task'sath_led_eventre-arms the LED callout after the cancel. (Notesc_invalidis only ever set on the attach-failure path at if_ath.c:1375-1378, before any tx can occur β so the!sc_invalidgate never protects a live teardown; the re-arm hole above is the live path.) - Post-free fire.
ath_hal_detach(if_ath.c:1435) frees the HAL; the softc is freed by newbus after detach returns (device destruction / devclass free on module unload). The re-armed timer firesath_led_off(if_ath_led.c:166)ledon-ms later: readssc->sc_ahfrom freed memory, callsath_hal_gpiosetthrough the freed HAL (function pointers/state), thencallout_reset(&sc->sc_ledtimer, sc->sc_ledoff, ath_led_done, sc)(172) βath_led_done(154) writessc->sc_blinking(159) on freed memory. UAF read + indirect call + UAF write. - Callout-core safety net verified (does NOT cover this). DragonFly's
callout_cancelis synchronous (_callout_cancel_or_stop(..., sync=1), kern_timeout.c:1032-1034, wait loop at 907-921) and CANCEL beats a handler-side RESET at INPROG-clear (kern_timeout.c:218-229) β so the existing cancel does quench an in-flight handler chain correctly. The hole is purely that a later re-arm can happen after it, from the task drain. Confirmed nocallout_drain/cancelforsc_ledtimerexists anywhere else in if_ath.c (only init at 884/886 and the single stop-site at 2703/2705).
What was checked and killed (negative results)
ath_led_off/ath_led_donewith period 0: the chain always terminates (ath_led_donenever re-arms; if_ath_led.c:154-160); periods are not sysctl-settable (only thesc_ledonpolarity is; if_ath_sysctl.c:857). No infinite-callout loop.sc_hwmap[rix]OOB (if_ath_led.c:194): DF-2152 (known, not re-filed); the rx-side 0xff sentinel variant is DF-4315 (known). The tx-side callers passsc->sc_txrix(u_int8_t rate-table index, if_ath_tx.c:1401).- GPIO pin handling (
ath_led_config, if_ath_led.c:127-131): routed through the HAL; the ar5416 CfgInput DRV_ALL defect is DF-5092 (known) β hal file, not re-filed. The root-onlydev.ath.X.ledpinsysctl accepts unvalidated pins (if_ath_sysctl.c:212-228) β HAL-side range handling, privileged-only, out of this file's scope. ath_led_configpower-state bracket (119-152) is properly locked (ATH_LOCK around set/restore).- Unlocked
sc_blinking/sc_ledoffdata races: benign int/LED-state races only (MPSAFE callout, kern_timeout.c internal spinlocks protect the callout itself).
Honest classification
untested / impact none (not demonstrated on this guest; no ath hardware
exists in QEMU). Analysis confidence: the structural ordering defect is
certain from source; the exploitability of the ms-scale race window and the
rare softled prerequisites keep the finding at Low severity / likely
confidence overall.
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 validation not performable on this guest (no ath hardware; baseline cannot reproduce). fix.diff (one callout_cancel after taskqueue_free in ath_detach) applies cleanly with patch(1) against the read-only sys/ tree.
n/a
Confirmed kernel references
Detail
Exploit chain
unproven (hardware-bound): root enables softled on ath NIC -> tx traffic keeps sc_ledtimer armed -> kldunload if_ath / PCI hot-removal while a tx-completion task is queued -> ath_stop cancels the timer -> taskqueue_free drains the task -> ath_led_event re-arms it -> ath_hal_detach frees the HAL, newbus frees the softc -> ath_led_off fires on freed memory (UAF read of sc->sc_ah + indirect call, UAF write of sc->sc_blinking).
Evidence (decisive lines)
VERDICT.md: full cited trace (if_ath.c:1385-1436 ordering, 2703 cancel site, 4570/4628/4676 LED call sites; kern_timeout.c:1032/218 cancel semantics; subr_taskqueue.c:151-157 drain-runs-pending-tasks). env.txt: guest pciconf -l proving no ath device exists.
PoC changes
No PoC authored: no Atheros hardware emulation exists in QEMU, so no trigger program can reach the driver on the audit guest (env.txt). The pass-2 analysis seed had no PoC sketch.
Verified recommended fix
Re-cancel (drain) sc_ledtimer in ath_detach() immediately after taskqueue_free(), when no task can re-arm it; optionally also gate ath_led_event()/blink arming on sc_running.
Verdict
The ath LED blink callout is re-armed after its only teardown cancellation: ath_detach() cancels sc_ledtimer inside ath_stop() (if_ath.c:2703) BEFORE taskqueue_free(sc->sc_tq) (if_ath.c:1416), and taskqueue_free runs all pending tasks (kern/subr_taskqueue.c:151-157), whose tx-completion path calls ath_led_event() (if_ath.c:4570/4628/4676, ungated by sc_running, outside ATH_LOCK) which callout_reset()s the chain again (if_ath_led.c:185). No later cancel or drain exists; the timer fires ath_led_off (if_ath_led.c:166) up to ~500ms later into a detached and then freed softc/HAL (ath_hal_detach at if_ath.c:1435) β use-after-free (read + indirect call via sc->sc_ah, then a write via ath_led_done). Untestable on the audit guest: QEMU has no Atheros NIC emulation, so ath0 cannot exist; reproduction requires real ath hardware with softled enabled (IBM devids auto-enable, if_ath.c:908, or root sysctl dev.ath.X.softled=1) under tx load during kldunload/hot-unplug.
No comments yet.