# DF-4893 — ah_osdep.c ALQ logging global `ath_hal_alq` TOCTOU (use-after-free of `struct alq`) — LATENT / COMPILED-OUT

File: `sys/dev/netif/ath/ath_hal/ah_osdep.c` (pass 2, GLM 5.3)
Severity: Info (latent — not reachable in any stock DragonFly build)

## The defect (code shape, certain)

Inside `#ifdef AH_DEBUG_ALQ` (ah_osdep.c:188-364) the ALQ register-tracing
global `ath_hal_alq` is consumed **without any lock**:

* `ath_hal_reg_write`  ah_osdep.c:297  `if (ath_hal_alq)` → :298 `ath_hal_alq_get(ah)`
* `ath_hal_reg_read`   ah_osdep.c:336  `if (ath_hal_alq)` → :337 `ath_hal_alq_get(ah)`
* `OS_MARK`            ah_osdep.c:353  `if (ath_hal_alq)` → :354 `ath_hal_alq_get(ah)`

`ath_hal_alq_get` (:258-280) dereferences the global twice more
(`alq_get(ath_hal_alq, ALQ_NOWAIT)` at :264 and :276).

The *closer* is the sysctl handler `sysctl_hw_ath_hal_log` (:239-250) →
`ath_hal_setlogging(0)` (:216-237):

```c
if (ath_hal_alq)
        alq_close(ath_hal_alq);   /* frees the struct alq */
ath_hal_alq = NULL;               /* :232 — no lock, no barrier */
```

Window: CPU0 in a register access passes the `if (ath_hal_alq)` check at
:297/:336/:353 → CPU1 (root, `sysctl hw.ath.hal.alq=0`) runs `alq_close()`
(free) + NULL-store → CPU0 calls `alq_get()` on the freed `struct alq` and
`alq_post()` writes through it → use-after-free write in kernel context.

Classic CWE-367. Inherited FreeBSD lineage: the block is verbatim FreeBSD
HAL OS-glue of the 9.x era (note `/*#include <sys/alq.h> FreeBSD */` at :202).

## Why it is unreachable on stock DragonFly (triple-gated)

1. **Option is off**: the whole block is `#ifdef AH_DEBUG_ALQ`; the
   `options AH_DEBUG_ALQ → opt_ah.h` mapping is commented out at
   `sys/conf/options:551`, so config(8) rejects any kernel config that
   names it. You cannot build this code without editing source.
2. **No alq(9) exists**: `#include <sys/alq.h>` is commented out (:202) and
   there is no `alq.h`, no `kern_alq.c`, no `alq_open/alq_get/alq_post`
   definition anywhere in the DF tree (verified: `find sys -name "alq*"` →
   only DF-local `if_ath_alq.h` / `ieee80211_alq.h`, whose own .c files are
   excluded from the build or gate their alq use behind the same dead
   option). Even with the option force-defined, this TU cannot compile.
3. **Second build break**: the ALQ-branch `ath_hal_reg_read` (:316-348)
   calls `ath_hal_reg_whilst_asleep(ah, reg)` at :324, but that helper is
   defined only under `#ifdef AH_DEBUG` (:153-171) — AH_DEBUG_ALQ without
   AH_DEBUG is a second compile error. The block was never compile-tested
   on DF.

Runtime proof on the audit guest (ath compiled into GENERIC, no Atheros NIC):

```
# sysctl hw.ath.hal.alq
sysctl: unknown oid 'hw.ath.hal.alq'
# sysctl hw.ath.hal.debug
sysctl: unknown oid 'hw.ath.hal.debug'
# sysctl hw.ath.hal.alq_logfile
sysctl: unknown oid 'hw.ath.hal.alq_logfile'
```

Additionally, even in FreeBSD where the code does build: the trigger needs a
privileged actor toggling `hw.ath.hal.alq` at the same instant another CPU is
inside a register access (any ath traffic). Root-gated race → low value even
upstream.

## Reproduce (documentary)

`build.sh` / `run.sh` probe the preconditions on a DragonFly host:

```sh
./run.sh   # reports: option mapping state, presence of sys/alq.h,
           # presence of hw.ath.hal.alq oid, presence of ath hardware
```

Expected on any stock DF: preconditions ABSENT → race not reachable
(exit 3). The race itself would need: hand-patched `opt_ah.h`
(AH_DEBUG_ALQ+AH_DEBUG), an imported alq(9), an Atheros NIC, and root
toggling the sysctl concurrent with register traffic (trigger.c documents
that two-thread shape).

## Fix

Recommended: delete the dead block (it cannot build), or — if/when alq(9) is
imported — re-add it with the global protected by a mutex on both the
consumer and closer paths (check/get under one lock; disable path takes the
same lock, closes, NULLs), and fix the `ath_hal_reg_whilst_asleep` guard.
See fix.diff.
