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

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-5157 Β· 6 files
FileTypeDescriptionSize
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:

  1. ath_stop(sc) β€” cancels sc_ledtimer once (if_ath.c:2703) β€” then disables interrupts (2713);
  2. 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 calls ath_led_event() -> ath_led_blink() -> callout_reset(&sc->sc_ledtimer, ...) β€” after the cancel, re-arming the timer;
  3. 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).

VERDICT.md
↓ download raw

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)

  1. Arming path. tx completion β†’ ath_tx_proc_q*() workers on sc->sc_tq call if (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-arms ath_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).
  2. Teardown ordering. ath_detach (if_ath.c:1385): ath_stop (1411) β†’ inside, gated only by sc_running and !sc->sc_invalid (2700): callout_cancel(&sc->sc_ledtimer) (2703, DragonFly branch), then ath_hal_intrset(ah, 0) (2713) β€” but a tx task enqueued before that is still queued. Back in ath_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's ath_led_event re-arms the LED callout after the cancel. (Note sc_invalid is only ever set on the attach-failure path at if_ath.c:1375-1378, before any tx can occur β€” so the !sc_invalid gate never protects a live teardown; the re-arm hole above is the live path.)
  3. 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 fires ath_led_off (if_ath_led.c:166) ledon-ms later: reads sc->sc_ah from freed memory, calls ath_hal_gpioset through the freed HAL (function pointers/state), then callout_reset(&sc->sc_ledtimer, sc->sc_ledoff, ath_led_done, sc) (172) β†’ ath_led_done (154) writes sc->sc_blinking (159) on freed memory. UAF read + indirect call + UAF write.
  4. Callout-core safety net verified (does NOT cover this). DragonFly's callout_cancel is 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 no callout_drain/cancel for sc_ledtimer exists 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_done with period 0: the chain always terminates (ath_led_done never re-arms; if_ath_led.c:154-160); periods are not sysctl-settable (only the sc_ledon polarity 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 pass sc->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-only dev.ath.X.ledpin sysctl accepts unvalidated pins (if_ath_sysctl.c:212-228) β€” HAL-side range handling, privileged-only, out of this file's scope.
  • ath_led_config power-state bracket (119-152) is properly locked (ATH_LOCK around set/restore).
  • Unlocked sc_blinking/sc_ledoff data 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_testable
baseline no→ patch + rebuild →patched clean

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

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.