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)
PoC verification
Evidence pack
findings/poc/DF-5294 Β· 10 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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).
DF-5294 VERDICT β post-free ratectl callout/task cycle after vap deletion
Status: inconclusive (hardware-gated). Bug: real, protocol-proven.
Reproduction path (in-kernel)
- Attach rum(4), STA vap with automatic rate adaptation
(
tp->ucastrate == IEEE80211_FIXED_RATE_NONEat S_RUN arms the callout, if_rum.c:989-991 β rum_ratectl_start :3085-3094). - The callout fires every
hz;rum_ratectl_timeout(:3096-3104) enqueuesrvp->ratectl_task;rum_ratectl_task(:3106-3142) ends with the UNCONDITIONALusb_callout_resetat :3140. ifconfig wlan0 destroyβrum_vap_delete: - :716usb_callout_drainreturns once the callout is neither pending nor executing β a task enqueued by the last fire is still queued; - :717taskqueue_drainwaits for that task; the task RE-ARMS the callout at :3140 on its way out (no guard to stop it); - :718-:719ratectl_deinit+ieee80211_vap_detachβether_ifdetachieee80211_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).
- A fire inside the :718-:719 window (or a task still queued at :720)
executes
rum_ratectl_timeout/rum_ratectl_taskon the freedrum_vap:ieee80211_runtask()writes into freed memory on every enqueue,vap->iv_ic/vap->iv_bssare 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
- sys/bus/u4b/wlan/if_rum.c:3140 (unguarded re-arm)
- sys/bus/u4b/wlan/if_rum.c:709-721 (drain order; kfree)
- sys/bus/u4b/wlan/if_rum.c:934 (late callout_stop in newstate)
- sys/bus/u4b/wlan/if_rum.c:989-991 (arm at S_RUN)
- sys/bus/u4b/wlan/if_run.c:2497-2498, if_mtw.c:2401-2402 (the guard rum lacks)
- sys/bus/u4b/wlan/if_run.c:1017, if_mtw.c:895 (clear-before-drain)
- sys/netproto/802_11/wlan/ieee80211.c:752 ff. (vap_detach β ieee80211_stop AFTER rum's drains, BEFORE kfree)
- sys/bus/u4b/usbdi.h:411 (usb_callout_drain == callout_drain)
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 (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
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.
No comments yet.