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

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-5249 Β· 11 files
FileTypeDescriptionSize
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

sys/bus/u4b/wlan/if_run.c

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:

  1. Torn slot: consumer sees .func != NULL before the producer's .arg0/.arg1/.k/.mac stores land β†’ func(NULL); for run_update_beacon_cb that is an immediate NULL dereference (vap = arg; ni = vap->iv_bss).
  2. Identity mismatch after wrap: RUN_CMDQ_GET wraps at 16 outstanding commands; a slot is overwritten between the consumer's .func and .arg0 loads (or before it runs) β†’ func_new(arg0_old): e.g. run_key_set_cb invoked with a vap pointer as struct run_cmdq * β†’ cmdq->k loaded from vap memory β†’ wild pointer dereference (type confusion).
  3. Lost commands: wrap overwrites a not-yet-executed entry β€” a lost run_key_delete leaves 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).

VERDICT.md
↓ download raw

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_MASQ if_runvar.h:225-227).
  • Producers, no lock:
  • run_key_set() 2368-2375: func first, then arg0/arg1/k/mac, then ieee80211_runtask().
  • run_key_delete() 2443-2453 (same shape, copies key fields).
  • run_newassoc() 2669-2675 (stores raw ni).
  • 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 (under RUN_LOCK): for (i = cmdq_exec; cmdq[i].func && pending; i = cmdq_exec, pending--) then func(arg0) / func(&cmdq[i]) β€” the .func read in the loop condition and the .arg0 read in the body are separate, unordered loads against unlocked producers.

Failure modes (all simulated in cmdq_race.c)

  1. Torn publish (func visible, arg0 not yet) β†’ run_update_beacon_cb(NULL) β†’ NULL deref panic.
  2. Wrap overwrite between consumer loads (or of a pending slot) β†’ func_new(arg0_old) type confusion: run_key_set_cb receives a struct run_cmdq * that is actually a vap (or vice versa), loads cmdq->k/cmdq->key from the wrong object β†’ wild dereference, kernel memory disclosure into device register writes.
  3. 16 outstanding commands β†’ overwritten slot = silently lost command, e.g. lost run_key_delete leaves 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_testable
baseline no→ patch + rebuild →patched clean

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

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.