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

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)

No comments yet.

PoC verification

Evidence pack

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

  1. wb_tick fires on CPU1 and blocks in lwkt_serialize_enter (the detach path on CPU0 holds the serializer).
  2. CPU0: wb_stop β†’ callout_stop (pending cancelled; in-progress callback not interlocked) β†’ releases serializer β†’ completes teardown β†’ softc freed.
  3. CPU1: wb_tick acquires the serializer, calls mii_tick() on the wb_miibus child being/mostly deleted (:1198), then callout_reset(&sc->wb_stat_timer, ...) re-arms a callout living in the freed softc.
  4. One second later the callout fires on freed memory: wb_tick(sc) reads sc->wb_miibus out 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.md
↓ download raw

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_testable
baseline no→ patch + rebuild →patched clean

Fix validation requires the NIC; the guest has none.

fix.diff; VERDICT.md
↓ fix.diffper-fix-DF-4271

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).