igb_detach never terminates the self-re-arming sc->timer and per-ring tx_gc_timer (callout_stop only, which does not interlock against an in-progress callback): re-arm after detach β use-after-free on freed softc/rings and, under kldunload, execution of unmapped module text
Summary
igb_timer() blocks in lwkt_serialize_enter(&sc->main_serialize) and unconditionally re-arms itself; igb_txgc_timer() re-arms itself on the try-serialize-failure path with a 1-tick timeout. Teardown uses callout_stop() only (documented as not interlocking against an in-progress callback), and igb_detach() contains no callout_terminate() for either class. An igb_timer callback parked on main_serialize during the detach critical section survives callout_stop, resumes when the serializers are dropped, and deterministically re-arms for +1s - after ether_ifdetach, mem_res release, igb_free_rings and kfree(sc->serializes). The delayed fire then executes against freed memory (MMIO via released mapping, lwkt_serialize_enter on freed lock, callout-internal writes into the freed slab); under kldunload if_igb the callback text itself is unmapped. ix(4) gained callout_terminate at detach after DF-3669 - igb has nothing. DF-3669 class.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-3681 Β· 10 files| File | Type | Description | Size | |
|---|---|---|---|---|
| README.md | β | 2.6 KB | β raw | |
| VERDICT.md | β | 3.9 KB | β raw | |
| trigger.c | β | 1.4 KB | view raw | |
| build.sh | β | 97 B | view raw | |
| run.sh | β | 163 B | view raw | |
| build.log | β | 136 B | view raw | |
| run.log | β | 98 B | view raw | |
| env.txt | β | 894 B | view raw | |
| fix.diff | β | 897 B | view raw | |
| verdict.json | β | 2.8 KB | view raw |
DF-3681 β igb never terminates its self-re-arming timers at detach (UAF)
What
igb(4) runs two self-re-arming callouts and tears both down with
callout_stop() only β never callout_terminate():
sc->timerβigb_timer()(sys/dev/netif/igb/if_igb.c:1308-1321) takeslwkt_serialize_enter(&sc->main_serialize)(blocking) and unconditionally re-arms itself at:1318(callout_reset_bycpu(..., hz, igb_timer, sc, ...)).txr->tx_gc_timer(one per TX ring) βigb_txgc_timer()(:327-350) re-arms itself at:349(callout_reset(..., 1, ...)) on the try-serialize-failure path (goto done).
igb_stop() stops them with callout_stop() (:1436, :1447), which per
sys/kern/kern_timeout.c:1087-1098 "does not interlock against a callout
that is in-progress": a callback that is currently executing (or blocked)
can still callout_reset() itself after callout_stop() returns.
igb_detach() (:742-802) contains no callout_terminate for either
timer.
Race (wide window for sc->timer)
- CPU1:
igb_timerfires; blocks inlwkt_serialize_enter(&sc->main_serialize)(detach holds all serializers viaifnet_serialize_all,:750). - CPU0:
igb_detachβigb_stopβcallout_stop(&sc->timer)β the callout is INPROG, not pending, so nothing is cancelled βifnet_deserialize_all(:763) βether_ifdetach(:765) β resource release (:773-799) βigb_free_rings(:784) βkfree(sc->serializes)(:791). - CPU1: acquires the (about-to-be-freed) serializer, runs, and re-arms
sc->timerforhzlater (:1318). - One second later the callout fires into freed memory: the callout
structure lives inside the softc (freed by devclass after detach /
kldunload), the callback executes
igb_timerreading freedsc,lwkt_serialize_enteron freed memory, MMIO through the releasedmem_resmapping; underkldunload if_igbthe module text itself is unmapped. Use-after-free (read + callout-internal writes on freed slab).
Same class as DF-3669 (ix). Notably, ix(4) received
callout_terminate(&sc->timer) at detach (sys/dev/netif/ix/if_ix.c:693)
β igb(4) lacks even that mitigation for both timer classes.
Reproduce / verify
Requires igb hardware (timers only run once the interface is up). The audit
guest has none (run.log). Source-level verification only.
Expected result (on hardware)
kldunload if_igb / devctl detach racing a fired igb_timer/igb_txgc_timer
β delayed panic or heap corruption in callout internals after the softc/rings
are freed. With the fix, callout_terminate() after the serializers are
dropped guarantees no post-detach fire.
DF-3681 VERDICT β self-re-arming timers not terminated at detach
Status: inconclusive on guest (no igb hardware) β source-likely
Root cause (path:line)
Two self-re-arming callouts; teardown uses only the non-interlocking stop:
igb_timer()sys/dev/netif/igb/if_igb.c:1308-1321c 1313: lwkt_serialize_enter(&sc->main_serialize); /* BLOCKING */ ... 1318: callout_reset_bycpu(&sc->timer, hz, igb_timer, sc, sc->timer_cpuid);igb_txgc_timer():327-350c 337: if (!lwkt_serialize_try(&txr->tx_serialize)) 338: goto done; ... 349: callout_reset(&txr->tx_gc_timer, 1, igb_txgc_timer, txr); /* done: */
Teardown:
- igb_stop() :1436 callout_stop(&sc->timer); and :1447
callout_stop(&txr->tx_gc_timer);
- igb_detach() :742-802 β no callout_terminate anywhere.
sys/kern/kern_timeout.c:1087-1098 documents callout_stop() as
non-interlocking against an in-progress callback; callout_terminate()
(:1104-1128) is the API that synchronously cancels and prevents further
callout_reset() (CALLOUT_CANCEL semantics), and frees the internal _callout
safely.
Race proof (sc->timer β deterministic re-arm)
igb_stop() runs under ifnet_serialize_all() (detach :750), i.e. with
main_serialize held. An igb_timer callback that fired before the flag
clear is guaranteed to be parked at :1313 on that serializer for the whole
stop/detach critical section (which includes e1000_reset_hw, PHY ops, and
up to msec_delay(IGB_I210_LINK_DELAY) = 1000 ms inside
igb_update_link_status on i210 β a very wide window). callout_stop() at
:1436 cannot cancel it (INPROG, not pending). When detach releases the
serializers (:763), the parked callback resumes, runs its body against the
stopped/reset device, and re-arms at :1318 for +1 s β after detach has
already called ether_ifdetach (:765), released mem_res (:779-782),
freed the rings (:784) and sc->serializes (:791).
The re-armed fire then touches:
- sc->hw MMIO via sc->osdep.mem_bus_space_handle β mapping released at
:779-782 (unmapped/reassignable region);
- lwkt_serialize_enter(&sc->main_serialize) on freed memory;
- the callout subsystem writes into sc->timer's freed slab on each cycle.
If the interface was detached via kldunload if_igb, the softc (and the
module text containing igb_timer) are freed/unmapped when the timer fires β
guaranteed fault/UAF; absent module unload, a perpetually re-arming timer
operating on a dead softc persists.
tx_gc_timer has the same hole via the goto done re-arm (:337-349): a
callback that failed lwkt_serialize_try while detach held the serializers
re-arms with a 1-tick timeout and dereferences txr->sc->arpcom.ac_if after
the rings are freed (:784, kfree(sc->tx_rings) inside igb_free_rings).
Cross-reference
DF-3669 filed the same class against ix(4) (ix_txgc_timer); this tree's
ix(4) detach already calls callout_terminate(&sc->timer)
(sys/dev/netif/ix/if_ix.c:693) β igb(4) has no equivalent for either
timer, making it strictly weaker than the post-DF-3669 state of ix.
Why not reproduced on the guest
No igb hardware: the guest's only NIC is virtio vtnet0; igb_attach()
never runs so the timers are never armed. See run.log. (The callout-semantics
half of the argument is kernel-generic and verified against
sys/kern/kern_timeout.c; the arming half needs the device.)
Fix
callout_terminate() for both timer classes in igb_detach() after the
serializers are released (fix.diff). Not hardware-testable:
fix_status = not_testable.
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 against the read-only sys/ tree; cannot be validated without igb hardware.
findings/poc/DF-3681/fix.diff
Confirmed kernel references
- sys/dev/netif/igb/if_igb.c:1313
- sys/dev/netif/igb/if_igb.c:1318
- sys/dev/netif/igb/if_igb.c:327
- sys/dev/netif/igb/if_igb.c:337
- sys/dev/netif/igb/if_igb.c:349
- sys/dev/netif/igb/if_igb.c:742
- sys/dev/netif/igb/if_igb.c:784
- sys/dev/netif/igb/if_igb.c:1436
- sys/dev/netif/igb/if_igb.c:1447
- sys/kern/kern_timeout.c:1091
- sys/kern/kern_timeout.c:1104
- sys/dev/netif/ix/if_ix.c:693
Detail
Evidence (decisive lines)
findings/poc/DF-3681/VERDICT.md (re-arm sites :1318/:349; stop sites :1436/:1447; detach :742-802 with no terminate; kern_timeout.c callout_stop vs callout_terminate semantics); run.log (no igb device on guest); fix.diff (callout_terminate in detach)
PoC changes
No runnable PoC without igb hardware; trigger.c documents the kldunload-vs-igb_timer race procedure; run.sh performs the module-presence/device-absence probe.
Verified recommended fix
callout_terminate() sc->timer and every tx_gc_timer in igb_detach() after ifnet_deserialize_all(), before ether_ifdetach()/ring free.
Verdict
Source-likely, runtime-inconclusive: igb_timer() (blocking lwkt_serialize_enter at if_igb.c:1313, self re-arm at :1318) and igb_txgc_timer() (goto-done re-arm at :349) are torn down only with callout_stop(), which sys/kern/kern_timeout.c:1087-1098 documents as not interlocking against an in-progress callback; igb_detach() (:742-802) never calls callout_terminate() for either. A callback parked on main_serialize during the detach critical section deterministically re-arms after the serializers are dropped and fires ~1s later into the freed softc/rings (UAF; unmapped text under kldunload). ix(4) already gained callout_terminate for its timer after DF-3669 β igb lacks it entirely. Guest has no igb hardware so the race cannot be exercised.
No comments yet.