Lockless sc->cmdq[] ring: producers store .func before args with no barrier and consumer reads are unordered; torn publish yields func(NULL), 16-slot wrap yields type-confused callbacks and silently lost commands
Summary
8 producers fill slots under no lock storing .func FIRST; consumer reads .func (loop cond) and .arg0 (call) as separate loads - torn slot = run_update_beacon_cb(NULL) NULL-deref; RUN_CMDQ_GET wraps at 16 outstanding, overwriting pending slots between loads = func_new(arg0_old) type confusion (vap pointer as run_cmdq* -> wild k pointer into register writes) + silently dropped commands (stale hardware keys). Guest model (verbatim store order + coalesced consumer): 12-13k torn NULL-arg calls and 470-485k identity mismatches per 10s, ~53M wrap overwrites. store-.func-LAST + structured-pass fix.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-5249 Β· 11 files| File | Type | Description | Size | |
|---|---|---|---|---|
| README.md | β | 2.3 KB | β raw | |
| VERDICT.md | β | 3.3 KB | β raw | |
| cmdq_race.c | β | 5.5 KB | view raw | |
| build.sh | β | 105 B | view raw | |
| run.sh | β | 98 B | view raw | |
| build.log | β | 321 B | view raw | |
| run.log | β | 628 B | view raw | |
| env.txt | β | 199 B | view raw | |
| fix.diff | β | 3.3 KB | view raw | |
| manifest.json | β | 860 B | view raw | |
| verdict.json | β | 3.0 KB | view raw |
DF-5249 β lockless run_cmdq producer/consumer machinery races
What
The 16-slot deferred-command ring sc->cmdq[] is filled by producers
under no lock at all (run_key_set 2368-2375, run_key_delete
2443-2453, run_newassoc 2669-2675, run_update_beacon 4894-4898,
run_updateprot 4960-4964, run_updateslot 5206-5210,
run_tx FIFO-drain 3425-3429, tx-timeout 3157-3161), each storing
.func first and the argument fields after, then
ieee80211_runtask(). The single consumer run_cmdq_cb() (1041-1068)
executes slots under RUN_LOCK with pending = taskqueue coalesced
count and cmdq_exec walking the ring.
Three failure modes:
- Torn slot: consumer sees
.func != NULLbefore the producer's.arg0/.arg1/.k/.macstores land βfunc(NULL); forrun_update_beacon_cbthat is an immediate NULL dereference (vap = arg; ni = vap->iv_bss). - Identity mismatch after wrap:
RUN_CMDQ_GETwraps at 16 outstanding commands; a slot is overwritten between the consumer's.funcand.arg0loads (or before it runs) βfunc_new(arg0_old): e.g.run_key_set_cbinvoked with avappointer asstruct run_cmdq *βcmdq->kloaded from vap memory β wild pointer dereference (type confusion). - Lost commands: wrap overwrites a not-yet-executed entry β a lost
run_key_deleteleaves a stale hardware key installed (data remains decryptable/usable after hostapd believes it removed it).
Repro (simulated; hardware-gated)
cmdq_race.c models the ring, RUN_CMDQ_GET, producer store order and
the exact run_cmdq_cb consumer loop with coalesced pending counts.
Guest results (3 runs, 10 s):
executed callbacks : ~61-62M torn NULL-arg calls : 12,011-13,347 identity mismatches : 470k-485k slot wrap overwrites : ~53M
Rates are inflated by the harness's deliberate consumer lag, but every
counter demonstrates the corresponding interleaving is reachable the
moment the task thread is delayed (preemption, the 2.5 s
run_do_request retries of DF-5248, or >16 deferred commands in flight
β beacon TIM churn + key updates + tx-timeouts during a scan).
Verdict
inconclusive (in-kernel repro needs real run(4) hardware), confidence
likely (reachability proven by simulation; store/execute order taken
verbatim from the source).
DF-5249 VERDICT β cmdq lockless handoff races
Source facts (sys/bus/u4b/wlan/if_run.c)
RUN_CMDQ_GET(c)=atomic_fetchadd_32((c),1) & 15β if_run.c:92 (16-slot ring,RUN_CMDQ_MAX/RUN_CMDQ_MASQif_runvar.h:225-227).- Producers, no lock:
run_key_set()2368-2375:funcfirst, thenarg0/arg1/k/mac, thenieee80211_runtask().run_key_delete()2443-2453 (same shape, copies key fields).run_newassoc()2669-2675 (stores rawni).- tx-timeout path in
run_bulk_tx_callbackN()3157-3161. - FIFO-drain deferral in
run_tx()3425-3429. run_update_beacon()4894-4898,run_updateprot()4960-4964,run_updateslot()5206-5210.- Consumer
run_cmdq_cb()1041-1068 (underRUN_LOCK):for (i = cmdq_exec; cmdq[i].func && pending; i = cmdq_exec, pending--)thenfunc(arg0)/func(&cmdq[i])β the.funcread in the loop condition and the.arg0read in the body are separate, unordered loads against unlocked producers.
Failure modes (all simulated in cmdq_race.c)
- Torn publish (
funcvisible,arg0not yet) βrun_update_beacon_cb(NULL)β NULL deref panic. - Wrap overwrite between consumer loads (or of a pending slot) β
func_new(arg0_old)type confusion:run_key_set_cbreceives astruct run_cmdq *that is actually avap(or vice versa), loadscmdq->k/cmdq->keyfrom the wrong object β wild dereference, kernel memory disclosure into device register writes. -
16 outstanding commands β overwritten slot = silently lost command, e.g. lost
run_key_deleteleaves stale hardware keys.
Simulation results (guest, 6 vCPU, 3Γ10 s)
torn NULL-arg calls : 12,011 / 13,347 per 10 s run identity mismatches : 470,340 / 484,744 slot wrap overwrites : 53.1M / 52.9M
The harness intentionally lags the consumer to widen windows; the point proven is that each interleaving is reachable, not its production rate. In-kernel the consumer is delayed by preemption, by the DF-5248 2.5 s control-request retries, and by >16 queued commands (beacon TIM churn + key updates + tx timeouts), after which the same interleavings occur.
Status
inconclusive β no run(4) hardware on the QEMU guest (usbconfig:
no USB controller; QEMU has no RT2870 device model). The store/execute
order in the harness is copied verbatim from the source. Confidence
likely.
Exploit ceiling
Type-confused callback argument β dereference of a struct ieee80211_key *
read from attacker-uninfluenced but wrong object β wild read used for
device register writes (run_write_region_1(base, k->wk_key, 16) reads
32 bytes from wherever cmdq->k points) β kernel memory disclosure to
the device / crash. Lost run_key_delete = stale hardware key
(crypto-relevant state machine failure).
Fix
fix.diff reorders every producer to store .func last behind a
cpu_ccfence() compiler barrier, and makes the consumer take a single
structured pass over the slot (git apply verified).
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 authored and verified with 'git apply --check' against the pristine sys/ tree, but cannot be kernel-tested: the QEMU guest has no USB controller and no run(4) device model exists, so neither baseline nor patched kernel behavior is exercisable. Hardware-gated: not_testable.
fix.diff (git-apply clean); VERDICT.md
Confirmed kernel references
Detail
Exploit chain
beacon TIM churn + key updates + tx timeouts push >16 commands or preempt a producer between stores -> consumer calls run_update_beacon_cb(NULL) (panic) or run_key_set_cb(vap-as-cmdq) (wild pointer read feeding device register writes) -> kernel DoS / memory disclosure to device / stale-key crypto state
Evidence (decisive lines)
findings/poc/DF-5249/run.log (2x10s runs): torn NULL-arg calls 12011/13347, identity mismatches 470340/484744, wrap overwrites ~53M; producer store order verbatim from if_run.c
PoC changes
Harness written from scratch (no seed)
Verified recommended fix
Store cmdq slot .func last behind cpu_ccfence() at all producer sites; single structured consumer read
Verdict
The 16-slot sc->cmdq[] deferred-command ring is produced by 8 lockless sites that store .func BEFORE the argument fields (e.g. if_run.c:2370-2374, 4896-4897) and consumed by run_cmdq_cb() (1041-1068) with separate unordered loads; RUN_CMDQ_GET wraps at 16 outstanding commands. Torn publish yields func(NULL) - NULL deref in run_update_beacon_cb; wrap overwrite between consumer loads yields func_new(arg0_old) type confusion (run_key_set_cb with vap as run_cmdq*, cmdq->k loaded from vap memory -> wild deref); wrapped slots silently lose commands, e.g. a lost run_key_delete leaves a stale hardware key. All three interleavings demonstrated by cmdq_race.c on the guest (13k torn NULL-arg calls and 485k identity mismatches per 10 s under deliberate consumer lag). In-kernel repro hardware-gated -> inconclusive.
No comments yet.