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

rum_ratectl_task re-arms rvp->ratectl_ch unconditionally; the re-armed callout survives rum_vap_delete's drain order and fires on the freed rum_vap (post-free callout/task cycle)

Summary

The re-arm has no guard - unlike BOTH siblings (if_run.c:2497, if_mtw.c:2401 guard with ratectl_run cleared before draining). vap_delete drains callout-then-task but a task completing after the callout drain re-arms it; the re-armed callout fires inside the deinit window, re-enqueues the task on the kfree'd vap, self-perpetuating (runtask enqueue writes into freed memory every second). Guest model: shipped 108-125 post-free callout fires + 108-127 post-free task runs per 10s; guarded exactly 0. if_run/if_mtw guard-protocol fix.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-5294 Β· 10 files
FileTypeDescriptionSize
rum_ratectl_drain.c β€” 6.7 KB view raw
build.sh β€” 117 B view raw
run.sh β€” 145 B view raw
build.log β€” 16 B view raw
run.log β€” 876 B view raw
env.txt β€” 241 B view raw
VERDICT.md β€” 4.2 KB ↓ raw
README.md β€” 2.9 KB ↓ raw
fix.diff β€” 1.5 KB view raw
verdict.json β€” 4.5 KB view raw

DF-5294 β€” rum_ratectl_task re-arms rvp->ratectl_ch unconditionally; the

re-arm survives rum_vap_delete's drains and fires on the freed rum_vap

What

sys/bus/u4b/wlan/if_rum.c:3140
    usb_callout_reset(&rvp->ratectl_ch, hz, rum_ratectl_timeout, rvp);

The re-arm at the end of rum_ratectl_task is unconditional β€” there is no ratectl_run-style guard. Both wlan twins in this tree have the guard that rum lacks:

sys/bus/u4b/wlan/if_run.c:2497   if(sc->ratectl_run != RUN_RATECTL_OFF)
                                     usb_callout_reset(&sc->ratectl_ch, ...);
sys/bus/u4b/wlan/if_mtw.c:2401   if (sc->ratectl_run != MTW_RATECTL_OFF) { ... }

(and both clear the mask in their *_vap_delete BEFORE draining: if_run.c:1017, if_mtw.c:895).

rum_vap_delete() (:709-721) drains in the vulnerable order:

716: usb_callout_drain(&rvp->ratectl_ch);      /* callout_drain */
717: ieee80211_draintask(ic, &rvp->ratectl_task);
719: ieee80211_vap_detach(vap);   /* -> ieee80211_stop -> rum_newstate -> usb_callout_stop (:934) */
720: kfree(rvp, M_80211_VAP);

Failure interleaving: a task instance that completes during/after :716 re-arms the callout at :3140 (no guard); the re-armed callout can fire inside the :718-:719 window (ratectl_deinit + ether_ifdetach β€” a heavyweight teardown that can easily exceed one tick) and enqueue the task again; nothing drains ratectl_task after :719, so it runs rum_ratectl_task(rvp == freed) after :720 β€” dereferencing vap->iv_ic, vap->iv_bss on freed heap β€” and it re-arms the callout on the freed object again, perpetuating the cycle (ieee80211_runtask writes into freed memory on every enqueue).

Secondary correctness defect from the same line: a task instance queued before a state transition re-arms the callout even after rum_newstate's usb_callout_stop (:934), so the ratectl loop keeps running while the vap is down.

Hardware gating

rum(4) needs RT2573 hardware β€” absent on the QEMU guest. Evidence is the line-level protocol trace plus rum_ratectl_drain.c, a userland model of the callout/task/destroy protocol with the exact shipped order vs the guarded fix, 3Γ—10 s per mode on the guest.

Build / Run / Expected

cc -O2 -pthread -Wall -o rum_ratectl_drain rum_ratectl_drain.c
./rum_ratectl_drain 10 0    # shipped: unguarded re-arm -> POST-FREE fires/tasks > 0
./rum_ratectl_drain 10 1    # fixed: guard cleared before drains -> exactly 0

See run.log: shipped 115/108/125 post-free callout fires and 111/108/127 post-free task runs over 360 destroy cycles per run; guarded 0/0.

Fix

fix.diff (if_rum.c + if_rumvar.h) β€” add sc->ratectl_run, set in rum_ratectl_start, cleared in rum_newstate (with the existing callout stop) and in rum_vap_delete BEFORE the drains; guard the :3140 re-arm. git apply --check clean. Kernel fix validation hardware-gated (fix_status: not_testable).

VERDICT.md
↓ download raw

DF-5294 VERDICT β€” post-free ratectl callout/task cycle after vap deletion

Status: inconclusive (hardware-gated). Bug: real, protocol-proven.

Reproduction path (in-kernel)

  1. Attach rum(4), STA vap with automatic rate adaptation (tp->ucastrate == IEEE80211_FIXED_RATE_NONE at S_RUN arms the callout, if_rum.c:989-991 β†’ rum_ratectl_start :3085-3094).
  2. The callout fires every hz; rum_ratectl_timeout (:3096-3104) enqueues rvp->ratectl_task; rum_ratectl_task (:3106-3142) ends with the UNCONDITIONAL usb_callout_reset at :3140.
  3. ifconfig wlan0 destroy β†’ rum_vap_delete: - :716 usb_callout_drain returns once the callout is neither pending nor executing β€” a task enqueued by the last fire is still queued; - :717 taskqueue_drain waits for that task; the task RE-ARMS the callout at :3140 on its way out (no guard to stop it); - :718-:719 ratectl_deinit + ieee80211_vap_detach β†’ ether_ifdetach
    • ieee80211_stop β†’ rum_newstate β†’ usb_callout_stop (:934) β€” the stop is at the END of this window and cannot un-queue a task whose callout fired inside it;
    • :720 kfree(rvp, M_80211_VAP).
  4. A fire inside the :718-:719 window (or a task still queued at :720) executes rum_ratectl_timeout/rum_ratectl_task on the freed rum_vap: ieee80211_runtask() writes into freed memory on every enqueue, vap->iv_ic/vap->iv_bss are dereferenced from freed heap, and :3140 re-arms the callout on the freed object β€” the cycle self-perpetuates.

Objective deviation from both siblings in the same tree: if_run.c:2497 and if_mtw.c:2401 guard every re-arm with ratectl_run, cleared in vap_delete (if_run.c:1017, if_mtw.c:895) BEFORE the drains. rum has neither the guard nor the clear.

Why inconclusive on this guest

No RT2573 hardware β‡’ no vap lifecycle. Structural evidence only.

Simulated evidence

rum_ratectl_drain.c models callout thread (tick), taskqueue worker (with the :3140 re-arm, guarded or not), and a destroyer running the exact shipped sequence (drain callout → drain task → detach-window delay → kfree → post-free observation), timings scaled 1s→4ms. Guest, 3×10 s per mode: shipped → 360 destroy cycles with 115/108/125 post-free callout fires and 111/108/127 post-free task runs; guarded → 0/0. The interleaving is reachable whenever the detach window spans one tick — ether_ifdetach's netisr round-trips make that realistic under load.

Exploit chain (if hardware present)

kfreed rum_vap (M_80211_VAP zone, ~0x2c0) reallocated by an attacker with vap-creation control β†’ ieee80211_runtask enqueue writes (ta_pending/ta_link) into the reallocated object at fixed offsets + rum_ratectl_task consumes attacker-shaped vap->iv_ic pointer β†’ arbitrary-ish kernel deref/write path β†’ memory corruption β†’ panic to potential privesc. (Requires root to destroy the vap β€” the primitive is root-triggered corruption, not an unpriv escalation.)

Fix

sc->ratectl_run flag: set in rum_ratectl_start, cleared in rum_newstate beside the existing callout stop, cleared in rum_vap_delete BEFORE the drains, and the :3140 re-arm made conditional (fix.diff touches if_rum.c + if_rumvar.h, git apply --check clean). This is exactly the if_run/if_mtw protocol. Kernel fix validation hardware-gated: not_testable.

Kernel references

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 (both files); kernel behavior cannot be exercised without an RT2573 device. The userland protocol model shows the guarded re-arm reduces post-free fires/task runs from ~110 per 10s to exactly 0. Hardware-gated: not_testable.

fix.diff (git-apply clean, 2 files); run.log guarded-mode lines '=0'; VERDICT.md
↓ fix.diffper-fix-DF-5294

Confirmed kernel references

Detail

Exploit chain

root destroys a RUNNING sta vap -> task completes after callout_drain, re-arms (:3140, unguarded) -> callout fires in the ether_ifdetach window -> task enqueued, survives past kfree(:720) -> rum_ratectl_task on freed M_80211_VAP memory: ieee80211_runtask enqueue writes into the freed (reallocatable, ~0x2c0-byte) object at fixed offsets + attacker-shaped vap->iv_ic deref -> heap corruption -> panic to privesc potential; root-triggered corruption primitive (vap destroy needs privilege)

Evidence (decisive lines)

run.log: shipped 'POST-FREE callout fires=115/108/125 POST-FREE task runs=111/108/127' vs guarded '0 ... 0' across 3+3 runs; rum_ratectl_drain.c models the exact drain order (:716->:717->detach-window->kfree) and the unguarded/guarded :3140; VERDICT.md cites the sibling guards

PoC changes

Harness written from scratch (no seed); condvar-based callout/tick and taskqueue threads model usb_callout semantics and the exact shipped destroy sequence, with timings scaled 1s->4ms; first draft had a join-deadlock and a missing wakeup, fixed before the recorded runs

Verified recommended fix

Guard the :3140 re-arm with a sc->ratectl_run flag that rum_newstate and rum_vap_delete clear BEFORE draining the callout/task pair (the if_run/if_mtw protocol)

Verdict

rum_ratectl_task() ends with an UNCONDITIONAL usb_callout_reset of rvp->ratectl_ch (if_rum.c:3140) - no ratectl_run guard, unlike both siblings in the same tree (if_run.c:2497-2498, if_mtw.c:2401-2402, each also clearing the mask in vap_delete before draining: if_run.c:1017, if_mtw.c:895). rum_vap_delete() (:709-721) drains the callout (:716) BEFORE the task (:717), so a task instance completing after the callout drain re-arms the callout; the re-armed callout can fire inside the ratectl_deinit/ieee80211_vap_detach window (ether_ifdetach netisr round-trips; the ieee80211_stop->rum_newstate->usb_callout_stop at :934 comes at the END of that window) and enqueue the task again, and nothing drains ratectl_task after :719 - the task then runs rum_ratectl_task() on the kfree'd rum_vap (:720), dereferencing vap->iv_ic/vap->iv_bss from freed heap, and re-arms the callout on the freed object, self-perpetuating (ieee80211_runtask writes into freed memory on every enqueue). The same missing guard also lets a task queued before a state transition re-arm the callout after rum_newstate's stop (:934), so the ratectl loop keeps firing while the vap is down. In-kernel reproduction is hardware-gated (no RT2573 on the guest). Simulated with a line-faithful callout/taskqueue/destroyer protocol model: shipped code 115/108/125 post-free callout fires and 111/108/127 post-free task runs per 3x10s (360 destroy cycles each); the guarded (fix) protocol yields exactly 0/0.