Dead AH_DEBUG_ALQ block: unlocked global `ath_hal_alq` TOCTOU β use-after-free of `struct alq` if ever compiled (latent, triple-unreachable on stock DF)
Summary
The ALQ register-tracing consumers NULL-check the module-global alq pointer unlocked and pass it to alq_get/post while the sysctl disable path frees+NULLs it unlocked - a kernel-context UAF write if revived. Inherited FreeBSD glue, TRIPLE-unreachable on DF (option commented out, no alq(9) in tree, calls an AH_DEBUG-only helper - cannot even compile); guest-runtime-proven absent (unknown oid). Delete-the-block fix (or mutex on revival).
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-4893 Β· 13 files| File | Type | Description | Size | |
|---|---|---|---|---|
| README.md | β | 3.9 KB | β raw | |
| VERDICT.md | β | 3.9 KB | β raw | |
| trigger.c | β | 3.1 KB | view raw | |
| build.sh | β | 107 B | view raw | |
| run.sh | β | 216 B | view raw | |
| build.log | β | 591 B | view raw | |
| run.log | β | 317 B | view raw | |
| run.2.log | β | 510 B | view raw | |
| sysctl_proof.txt | β | 263 B | view raw | |
| env.txt | β | 655 B | view raw | |
| fix.diff | β | 6.2 KB | view raw | |
| manifest.json | β | 1.4 KB | view raw | |
| verdict.json | β | 4.2 KB | view raw |
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_writeah_osdep.c:297if (ath_hal_alq)β :298ath_hal_alq_get(ah)ath_hal_reg_readah_osdep.c:336if (ath_hal_alq)β :337ath_hal_alq_get(ah)OS_MARKah_osdep.c:353if (ath_hal_alq)β :354ath_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):
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)
- Option is off: the whole block is
#ifdef AH_DEBUG_ALQ; theoptions AH_DEBUG_ALQ β opt_ah.hmapping is commented out atsys/conf/options:551, so config(8) rejects any kernel config that names it. You cannot build this code without editing source. - No alq(9) exists:
#include <sys/alq.h>is commented out (:202) and there is noalq.h, nokern_alq.c, noalq_open/alq_get/alq_postdefinition anywhere in the DF tree (verified:find sys -name "alq*"β only DF-localif_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. - Second build break: the ALQ-branch
ath_hal_reg_read(:316-348) callsath_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:
./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.
DF-4893 VERDICT β ah_osdep.c ALQ TOCTOU/UAF: latent, unreachable on stock DF β untested (no runnable path)
Bottom line
The AH_DEBUG_ALQ block of sys/dev/netif/ath/ath_hal/ah_osdep.c contains a
textbook TOCTOU on the module-global struct alq *ath_hal_alq: unlocked
consumers (if (ath_hal_alq) at sys/dev/netif/ath/ath_hal/ah_osdep.c:297,
:336, :353 β alq_get(ath_hal_alq, β¦) at :264/:276) racing the sysctl
disable path alq_close(ath_hal_alq); ath_hal_alq = NULL;
(ah_osdep.c:230-232) yield a use-after-free write (alq_post through a
freed struct alq). The code shape is certain (CWE-367, FreeBSD-inherited).
It is triple-dead on DragonFly:
AH_DEBUG_ALQcannot be selected β itssys/conf/optionsmapping is commented out (sys/conf/options:551); config(8) rejects the option name.- The TU cannot compile even if force-defined β
#include <sys/alq.h>is commented out (ah_osdep.c:202) and no alq(9) implementation or header exists anywhere in the tree (verifiedfind sys -name "*alq*"; the other would-be consumers, ieee80211_alq.c / if_ath_alq.c, are themselves excluded from the build or guard alq calls behind the same dead option β if_ath_alq.c:51-53 has its alq include#ifdef'd empty for DF). - Second compile error inside the block β ALQ-branch
ath_hal_reg_readcallsath_hal_reg_whilst_asleep()at ah_osdep.c:324, defined only under#ifdef AH_DEBUG(:153-171).
Guest evidence (DragonFly 6.5-DEVELOPMENT x86_64, ath compiled into GENERIC
β hw.ath.* nodes live, ath_hal.ko not loaded, no Atheros PCI device):
sysctl hw.ath.hal.alq/.debug/.alq_logfileβ unknown oid (run.log) β the block is absent from the running kernel, as predicted.pciconf -l | grep -ciE "ath|1002"β 0 β no Atheros or AMD GPU hardware in the guest, so no runtime register traffic can be produced either.
Even on FreeBSD where this code builds, exploitation requires root to
toggle hw.ath.hal.alq precisely while another CPU is between the NULL
check and alq_get β a privileged-user-triggered race, not an unpriv path.
Verification honesty
- No escalation / panic was attempted on the guest because there is no compiled target and no hardware: every avenue ends at "code does not exist in the running kernel" (runtime-proven, not assumed).
trigger.cdocuments the would-be reproducer shape (root sysctl toggler thread vs. register-traffic thread) andrun.shprobes each precondition; on this guest all probes report ABSENT (exit 3).- Classification: status
untested, impactnoneon stock DF; filed as Info so the race is on record before anyone imports alq(9) and flips the option on wholesale.
Baseline vs fix validation
Not applicable β nothing reproduced, so there is no bad behavior to validate a fix against. fix.diff is a source-level hardening patch (delete the dead block) authored from the line-accurate analysis above; per audit rules it is NOT applied to sys/.
Production-active surface of ah_osdep.c (audited clean)
ath_hal_malloc M_ZERO|M_INTWAIT (ah_osdep.c:120-124 β zeroed, cannot
fail, no uninit leak to HAL softcs) Β· ath_hal_vprintfβkvprintf with
HAL-internal fmt (:132-136) Β· ether_sprintf DF per-CPU buffer (safe)
Β· non-ALQ ath_hal_reg_write/reg_read (:378-424): optional serialise
lock taken/released around the bus op + correct barrier placement
(barrier-before-read, barrier-after-write) Β· SYSCTL_STRING logfile node:
sizeof(kernelname) == MAXPATHLEN == sizeof(ath_hal_logfile)
(sys/kern/kern_mib.c:178) β no write overflow Β· module glue serialized
(:440-461).
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_testableNot applicable - nothing reproduced on any build (the defect is compiled out of every stock kernel), so there is no bad behavior to validate a fix against. fix.diff is a source-level hardening deletion authored from line-accurate analysis; not applied to sys/ per audit rules.
fix.diff; VERDICT.md 'Baseline vs fix validation' section
Confirmed kernel references
- sys/dev/netif/ath/ath_hal/ah_osdep.c:188-364
- sys/dev/netif/ath/ath_hal/ah_osdep.c:202
- sys/dev/netif/ath/ath_hal/ah_osdep.c:216-256
- sys/dev/netif/ath/ath_hal/ah_osdep.c:258-280
- sys/dev/netif/ath/ath_hal/ah_osdep.c:297-307
- sys/dev/netif/ath/ath_hal/ah_osdep.c:316-348
- sys/dev/netif/ath/ath_hal/ah_osdep.c:324
- sys/dev/netif/ath/ath_hal/ah_osdep.c:350-364
- sys/conf/options:551
Detail
Evidence (decisive lines)
run.log (guest: BUILD_OK; hw.ath.hal.alq/alq_logfile ABSENT; trigger_rc=3; static: '#AH_DEBUG_ALQ opt_ah.h' at sys/conf/options:551; sys/alq.h ABSENT; 0 ath/AMD PCI devices); run.2.log (first probe incl. unknown-oid sysctl session); sysctl_proof.txt; env.txt (uname, cc 8.3); VERDICT.md (full path:line narrative); fix.diff (delete the dead block)
PoC changes
No seed PoC existed. Authored documentary probe trigger.c (precondition checker for the sysctl oid / alq.h / option mapping) plus run.sh; compiled and executed on the guest; fixed one missing
Verified recommended fix
Delete the dead AH_DEBUG_ALQ block (or, if alq(9) is ever imported, guard ath_hal_alq with a mutex on both consumer and sysctl-closer paths and fix the ath_hal_reg_whilst_asleep AH_DEBUG guard); see fix.diff
Verdict
The AH_DEBUG_ALQ block of ah_osdep.c contains a certain-by-inspection TOCTOU on the module-global struct alq *ath_hal_alq: unlocked consumers (if (ath_hal_alq) at ah_osdep.c:297/:336/:353, then alq_get/alq_post via ath_hal_alq_get :258-280) race the sysctl disable path (alq_close(ath_hal_alq); ath_hal_alq = NULL; at :230-232, no lock), yielding a use-after-free write if ever compiled. It is unreachable on any stock DragonFly build, proven three ways: (1) the option mapping is commented out at sys/conf/options:551 so config(8) cannot enable AH_DEBUG_ALQ; (2) the TU cannot compile even if force-defined - no sysctl hw.ath.hal.alq -> unknown oid, root and uid=1001 alike) and there is zero Atheros hardware (pciconf count 0). Probe trigger built and run on the guest: TARGET ABSENT, exit 3 (run.log). Filed as Info/latent so the race is on record before anyone imports alq(9) and flips the option on wholesale; even upstream on FreeBSD the trigger requires root to toggle the sysctl concurrent with register traffic.
No comments yet.