em_stop() can deadlock the callout subsystem against em_timer/em_82547_move_tail (AB-BA): callout_stop() synchronously waits for a callback that is blocked on the serializer the stopper holds β mechanism PROVEN on the guest kernel via protocol-replica module (caller stuck D3/wchan 'costp')
Summary
em(4) arms three MP-safe callouts whose callbacks take the driver serializer by hand: em_timer() BLOCKS in lwkt_serialize_enter and self re-arms; em_82547_move_tail() BLOCKS likewise. em_stop() - reached from every down/detach/reinit path (SIOCSIFFLAGS, SIOCSIFMTU, watchdog, suspend, detach) - calls callout_stop() on these WHILE HOLDING that serializer. DragonFly's callout_stop() synchronously waits for an in-progress callback (ssleep 'costp' at kern_timeout.c:923). A callback that was dequeued and is parked in lwkt_serialize_enter on the stopper's serializer therefore deadlocks against it: both threads wedge permanently, the interface serializer is never released, and the wedged softclock thread stops delivering ALL callouts for its CPU (system-wide timer starvation). PROVEN deterministically on the guest kernel with an out-of-tree module replicating the exact protocol: the em_timer-shaped test wedged with the caller in state D3, wchan 'costp' - callout_stop() never returned. The negative control (try-serialize shape) fully unloaded - showing that on this kern_timeout.c the DF-3669/3681 're-arm survives callout_stop' premise does NOT hold for em, i.e. the em defect is the deadlock, not a post-stop re-arm UAF. Fix: try-enter + retry pattern (like em_txgc_timer).
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-3715 Β· 10 files| File | Type | Description | Size | |
|---|---|---|---|---|
| README.md | β | 3.7 KB | β raw | |
| VERDICT.md | β | 4.3 KB | β raw | |
| build.sh | β | 76 B | view raw | |
| run.sh | β | 235 B | view raw | |
| build.log | β | 5.4 KB | view raw | |
| run.log | β | 3.4 KB | view raw | |
| env.txt | β | 634 B | view raw | |
| fix.diff | β | 2.2 KB | view raw | |
| verdict.json | β | 3.8 KB | view raw | |
| callout_replica.c | β | 6.3 KB | view raw |
DF-3715 β em_stop() can deadlock the callout subsystem against em_timer / em_82547_move_tail (AB-BA)
File: sys/dev/netif/em/if_em.c
Lines: 2216-2239 (em_timer: blocking lwkt_serialize_enter at :2222, self re-arm at :2236), 2027-2036 (em_82547_move_tail: blocking enter at :2033, re-arm via :2016-2017), 2343-2350 (em_stop: callout_stop() under the same serializer), 1047-1065 (em_detach)
Kernel refs: sys/kern/kern_timeout.c:859-932 (_callout_cancel_or_stop sync wait, ssleep(..., "costp", 0) at :923), :1091-1098 (callout_stop)
The bug
em_timer() is armed with callout_init_mp() (if_em.c:548) and its callback
blocks acquiring the driver serializer (:2222). em_stop() β reached
from every down/detach/MTU-change/reinit path (SIOCSIFFLAGS :1295, SIOCSIFMTU
:1280, watchdog :1407, suspend :1112, detach :1049) β calls
callout_stop(&adapter->timer) (:2343) while holding that serializer.
DragonFly's callout_stop() synchronously waits for an in-progress callback
(_callout_cancel_or_stop, sync=1 β ssleep("costp") until the callback
completes). If em_timer was dequeued (INPROG) and is blocked in
lwkt_serialize_enter() on the serializer held by the em_stop caller, the
two wait on each other forever:
softclock thread: em_timer -> lwkt_serialize_enter(ser) ... blocked
em_stop caller: holds ser -> callout_stop() -> ssleep("costp") ... blocked
Both threads wedge permanently; the serializer is never released (every later
driver entry point on that interface blocks), and the wedged softclock thread
stops delivering all callouts for its CPU (system-wide timer starvation).
The 82547 tx_fifo_timer has the same shape (em_82547_move_tail :2033).
(em_txgc_timer uses non-blocking lwkt_serialize_try β immune.)
Proven on the guest kernel (deterministic kernel-module replica)
callout_replica.c reproduces the exact protocol (MP callout + hand-rolled
serializer): kern.emreplica.stop_blk=1 holds the serializer, waits until
the em_timer-shaped callback is parked inside lwkt_serialize_enter, then
calls callout_stop() β which never returns:
replica[blk]: handler BLOCKED on our serializer (fired=91 ran=90) - entering callout_stop() replica[blk]: IF THIS IS THE LAST replica LINE, AB-BA DEADLOCK IS PROVEN (no further output; "callout_stop returned (NO deadlock)" never printed) ps -o pid,stat,wchan,command -ax | grep stop_blk 1051 D3 costp sysctl kern.emreplica.stop_blk=1
costp is the wait-channel inside kern_timeout.c:923 β direct proof the
caller is stuck in callout_stop()'s sync wait. Negative control on the same
module: the try-based (em_txgc_timer-shaped) handler is fully unloaded by
callout_stop() (pending=0 after return) β so the try-based timers are fine,
the blocking-enter handlers are the defect.
Build/run (guest, DragonFly 6.5-DEVELOPMENT, /usr/src present)
cd /root/kmod && make # build.log kldload ./callout_replica.ko # loads fine on a fresh boot sysctl kern.emreplica.arm=1 sysctl kern.emreplica.stop_gc=1 # negative control (returns) (sysctl kern.emreplica.stop_blk=1 &) # DEADLOCK - second session: ps wchan
No em hardware required β the mechanism is the generic callout/serializer
protocol if_em.c uses; em-specific reachability is the source-proven race
window (timer dequeued in the Β΅s before em_stop's serializer acquire wins
over the callback's acquire).
Fix
fix.diff: convert em_timer() and em_82547_move_tail() to non-blocking
lwkt_serialize_try() with a 1-tick retry on contention (the exact pattern
em_txgc_timer already uses) β a callback that never blocks can always
complete, so callout_stop()'s sync wait always terminates.
DF-3715 β VERDICT
Status: mechanism PROVEN on the guest kernel via deterministic replica module (same callout/serializer protocol as if_em.c); em-specific runtime hardware-gated β inconclusive per family convention, reproduced=0, impact=none-on-this-guest. The class of impact, when it fires on em hardware, is a permanent kernel deadlock (DoS).
Root cause (path:line)
sys/dev/netif/em/if_em.c:548-550β all three callouts are MP-safe (callout_init_mp), i.e. handlers run on a softclock thread and manage locking by hand.sys/dev/netif/em/if_em.c:2222βem_timer()does a blockinglwkt_serialize_enter(ifp->if_serializer);:2033βem_82547_move_tail()(tx_fifo_timer callback) likewise.sys/dev/netif/em/if_em.c:2343and:2344βem_stop()callscallout_stop(&adapter->timer)/callout_stop(&adapter->tx_fifo_timer)while holding the same serializer (em_stoprequiresASSERT_SERIALIZED, :2339). Callers ofem_stop:em_init(:1422 β every reinit: SIOCSIFFLAGS :1292-1295, SIOCSIFMTU :1279-1280, media change :1795-1796, watchdog :1407, npoll :1581-1582),em_detach(:1049),em_suspend(:1112).sys/kern/kern_timeout.c:859-932β_callout_cancel_or_stop(..., sync=1)(whichcallout_stopat:1091uses): when the callout is INPROG, the callerssleep(c, &c->spin, 0, "costp", 0)until the callback completes (:923).- β If
em_timeris INPROG and parked inlwkt_serialize_enter()on the serializer held by theem_stopcaller: the callback waits for the serializer, the stopper waits (costp) for the callback β permanent AB-BA deadlock. Collateral: the interface serializer is held forever (all later driver paths on that interface block), and the softclock thread of the CPU that armed the timer stops firing every callout on that CPU.
Race window: em_timer must be dequeued-and-not-yet-inside within the Β΅s
around em_stop's serializer acquire β once per second of runtime per
em_stop invocation; hit rate is low but the trigger (ifconfig down/up, MTU
change on a running interface, detach, suspend/resume, watchdog reinit) is
repeatable, making eventual occurrence plausible on long-lived boxes.
Why this is NOT the DF-3669/DF-3681 finding
The ix/igb pass-2 findings asserted a re-arm-after-stop UAF on
callout_stop grounds ("does not interlock against an in-progress
callback"). On this kern_timeout.c, callout_stop()'s STOP flag is
processed after the callback returns and unloads a racing re-arm
(_callout_update_spinlocked, RESET-then-STOP ordering) β verified live by
the replica's negative control (stop_gc: pending=0 after stop). For em
the actual defect is therefore not a post-stop re-arm, but the sync-wait
deadlock above. (em_detach also frees everything only after em_stop
returns, which under this analysis it always does β no stop-side UAF.)
Verification performed (guest, DragonFly 6.5-DEVELOPMENT x86_64)
callout_replica.c(this pack) β exact protocol replica; seerun.log:- negative control (try-based gc shape): stop fully unloaded β no bug for
em_txgc_timer; - positive test (blocking em_timer shape):
callout_stop()never returned; caller in stateD3wchancostp(kern_timeout.c:923) β deadlock proven deterministically; - guest wedged (sysctl thread + softclock thread), evidence captured from a
second session, then
vm.sh reset with-src. - em hardware absent on guest (
ifconfig -lβvtnet0 lo0; no 8086 NIC) β the if_em.c instance of the race itself cannot be fired here.
Fix
fix.diff (applies clean to read-only sys/): make em_timer() and
em_82547_move_tail() non-blocking β lwkt_serialize_try() with a 1-tick
callout_reset retry on contention, exactly the em_txgc_timer pattern
(:494-506). A never-blocking callback always completes, so callout_stop's
sync wait always terminates. Not hardware-testable on this guest
(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 (patch --dry-run clean); cannot be validated on a guest without em hardware.
findings/poc/DF-3715/fix.diff
Confirmed kernel references
Detail
Evidence (decisive lines)
findings/poc/DF-3715/VERDICT.md; run.log (replica session transcript + raw dmesg/ps captures: 'IF THIS IS THE LAST replica LINE' then silence; ps '1051 D3 costp sysctl kern.emreplica.stop_blk=1'); build.log (module built on guest, BUILD_RC=0); fix.diff (try-enter + 1-tick retry for em_timer/em_82547_move_tail)
PoC changes
No seed PoC. Authored callout_replica.ko (out-of-tree kmodule replicating the em callout protocol) because the guest has no em hardware; the deadlock is in the generic protocol em uses, so the replica exercises the identical kernel paths.
Verified recommended fix
Make em_timer() and em_82547_move_tail() non-blocking: lwkt_serialize_try() with a 1-tick callout_reset retry (em_txgc_timer pattern).
Verdict
Mechanism PROVEN on the guest kernel by deterministic replica of if_em.c's exact callout/serializer protocol; em-specific runtime hardware-gated (inconclusive per family convention). em_timer (sys/dev/netif/em/if_em.c:2216-2239) and em_82547_move_tail (:2027-2036) are MP callouts whose callbacks BLOCK in lwkt_serialize_enter (:2222/:2033) on the driver serializer; em_stop tears them down with callout_stop (:2343-2344) while holding that same serializer; DragonFly callout_stop synchronously waits for an in-progress callback (sys/kern/kern_timeout.c:859-932, ssleep 'costp' at :923). A callback dequeued and parked on the serializer therefore deadlocks against the stopper (AB-BA): both threads wedge permanently, the interface serializer is never released and the affected CPU's softclock thread stops delivering all callouts. Kernel-module replica on the guest: stop under a held serializer with the em_timer-shaped handler parked -> callout_stop NEVER returns (caller D3 wchan 'costp'); negative control: the try-based em_txgc_timer shape is fully unloaded by callout_stop (pending=0) - which also shows the DF-3669/DF-3681 're-arm survives callout_stop' premise does not hold on this kern_timeout.c, so for em the defect is the deadlock, not a post-stop re-arm UAF.
No comments yet.