callout_stop() does not interlock with the self-serializing, self-re-arming wb_tick: repeating post-detach use-after-free of the softc
Summary
wb_tick self-serializes and re-arms itself under the serializer; wb_stop issues only callout_stop, which for non-lock-aided callouts does not interlock against an in-progress callback. During detach a blocked tick proceeds after the serializer release - post-teardown it runs mii_tick against the deleted miibus child and re-arms a timer embedded in the softc that newbus frees when detach returns; the callout then fires into freed memory every hz. drain-outside-the-serializer fix.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-4271 Β· 5 files| File | Type | Description | Size | |
|---|---|---|---|---|
| README.md | β | 2.4 KB | β raw | |
| VERDICT.md | β | 2.3 KB | β raw | |
| fix.diff | β | 1.1 KB | view raw | |
| verdict.json | β | 2.6 KB | view raw | |
| env.txt | β | 700 B | view raw |
DF-4271 β wb_stop()/wb_detach() use non-interlocked callout_stop() against a self-re-arming wb_tick β post-detach UAF
Files: sys/dev/netif/wb/if_wb.c:1590 (callout_stop in wb_stop),
:839-846 (detach under serializer), :1191-1201 (wb_tick).
What
static void
wb_tick(void *xsc)
{
...
lwkt_serialize_enter(ifp->if_serializer); /* :1197 - self-serializing */
mii_tick(mii);
callout_reset(&sc->wb_stat_timer, hz, wb_tick, sc); /* :1199 - re-arms itself */
lwkt_serialize_exit(ifp->if_serializer);
}
wb_stop() (serialized context) calls callout_stop(&sc->wb_stat_timer)
(:1590) only. Per sys/kern/kern_timeout.c (callout_stop documentation for
non-lock-aided callouts β wb uses plain callout_init(), :715): "Stops a
callout if it is pending or queued, does not block. This function does not
interlock against a callout that is in-progress."
Detach sequence (:839-850): lwkt_serialize_enter β wb_stop() (cancels
pending timer, does NOT wait) β bus_teardown_intr β
lwkt_serialize_exit β ether_ifdetach β device_delete_child(miibus) β
bus_generic_detach β resources released β return (softc freed by newbus).
Race
- wb_tick fires on CPU1 and blocks in
lwkt_serialize_enter(the detach path on CPU0 holds the serializer). - CPU0: wb_stop β callout_stop (pending cancelled; in-progress callback not interlocked) β releases serializer β completes teardown β softc freed.
- CPU1: wb_tick acquires the serializer, calls
mii_tick()on thewb_miibuschild being/mostly deleted (:1198), thencallout_reset(&sc->wb_stat_timer, ...)re-arms a callout living in the freed softc. - One second later the callout fires on freed memory:
wb_tick(sc)readssc->wb_miibusout of freed kernel memory and dereferences it β a persistent use-after-free (read/write through freed softc every hz), crash or worse depending on slab reuse.
Trigger
Driver detach with the interface up: kldunload if_wb.ko, devctl removal,
or cardbus eject on machines with the W89C840F/RL100-ATX NIC. Privileged
unload; hardware-gated. Window is one tick (hz) but the re-arm makes it
persistent once lost.
Fix
callout_drain(&sc->wb_stat_timer) in wb_detach after the serializer is
released (draining while holding the serializer would deadlock against
wb_tick's own serialize_enter, so drain after exit) β see fix.diff.
VERDICT - DF-4271 (callout_stop does not interlock with in-progress, self-re-arming wb_tick)
Status: untested - no W89C840F NIC in the guest (env.txt); the race is established from the DragonFly callout API contract, which is explicit:
sys/kern/kern_timeout.c (callout_stop, non-lock-aided callouts): "Stops a callout if it is pending or queued, does not block. This function does not interlock against a callout that is in-progress."
wb's callout is plain callout_init() (if_wb.c:715) β non-lock-aided.
Why the race is real
- wb_tick (:1191-1201) self-serializes (lwkt_serialize_enter at :1197) and self-re-arms (callout_reset at :1199) β both under the serializer.
- wb_stop() runs under the serializer too (from wb_init/ioctl/intr/detach)
and issues only
callout_stop()(:1590), which by contract cannot wait for a callback that is blocked at :1197 waiting for the very serializer the stopper holds. - Detach releases the serializer at :843 and continues teardown (ether_ifdetach, miibus child deletion at :848-849, resource release, softc free by newbus after device_detach returns).
- The pending callback then proceeds: mii_tick on a deleted/being-deleted miibus child, and callout_reset re-arming a timer embedded in memory that is about to be (or already is) freed. The re-arm guarantees the UAF repeats every hz until the freed callout state happens to be overwritten.
Impact
Use-after-free (read + indirect call chain through freed softc) after driver detach on hardware-bearing systems; privileged trigger (kldunload/device removal). Severity Low: small window, privileged trigger, hardware-gated β but the consequence is an unbounded repeating UAF, not a one-shot.
Fix validation
Not possible on this guest (no hardware). fix.diff adds the drain on the correct side of the serializer release; draining while holding the serializer would self-deadlock (wb_tick needs the serializer to finish), hence the placement after :843.
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 validation requires the NIC; the guest has none.
fix.diff; VERDICT.md
Confirmed kernel references
Detail
Exploit chain
kldunload if_wb.ko with interface up while wb_tick is blocked on if_serializer -> teardown completes -> wb_tick runs on deleted miibus and re-arms timer in freed softc -> callout fires into freed memory every hz (repeating UAF: reads sc->wb_miibus from freed softc, dereferences it, writes callout state).
Evidence (decisive lines)
VERDICT.md (API-contract citation + interleaving); sys/kern/kern_timeout.c callout_stop doc block; env.txt
PoC changes
No runnable PoC (hardware-gated race); analysis pack.
Verified recommended fix
Add callout_drain(&sc->wb_stat_timer) in wb_detach after lwkt_serialize_exit (draining while holding the serializer would deadlock).
Verdict
Race established from the DragonFly callout API contract. wb_tick (if_wb.c:1191-1201) self-serializes (:1197) and self-re-arms (:1199); wb_stop() only issues callout_stop() (:1590), which kern_timeout.c documents for non-lock-aided callouts (wb uses plain callout_init, :715) as 'does not block... does not interlock against a callout that is in-progress'. During detach (:839-846) a wb_tick blocked on the serializer proceeds after :843's release, post-teardown: mii_tick against the miibus child deleted at :848-849, and callout_reset re-arming a timer inside the softc that newbus frees when detach returns - a repeating use-after-free every hz until the freed callout state is overwritten. Privileged trigger (kldunload / device removal) on hardware-bearing systems; window is one tick. Not kernel-reproduced: no W89C840F NIC in the guest (env.txt).
No comments yet.