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

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-3681 Β· 10 files
FileTypeDescriptionSize
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) takes lwkt_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)

  1. CPU1: igb_timer fires; blocks in lwkt_serialize_enter(&sc->main_serialize) (detach holds all serializers via ifnet_serialize_all, :750).
  2. 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).
  3. CPU1: acquires the (about-to-be-freed) serializer, runs, and re-arms sc->timer for hz later (:1318).
  4. 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_timer reading freed sc, lwkt_serialize_enter on freed memory, MMIO through the released mem_res mapping; under kldunload if_igb the 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.

VERDICT.md
↓ download raw

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:

  1. igb_timer() sys/dev/netif/igb/if_igb.c:1308-1321 c 1313: lwkt_serialize_enter(&sc->main_serialize); /* BLOCKING */ ... 1318: callout_reset_bycpu(&sc->timer, hz, igb_timer, sc, sc->timer_cpuid);
  2. igb_txgc_timer() :327-350 c 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_testable
baseline no→ patch + rebuild →patched clean

fix.diff authored against the read-only sys/ tree; cannot be validated without igb hardware.

findings/poc/DF-3681/fix.diff
↓ fix.diffper-fix-DF-3681

Confirmed kernel references

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.