callout_stop(&sc->timer) under main_serialize deadlocks against emx_timer's blocking lwkt_serialize_enter (AB-BA): emx_stop/emx_intr_body/emx_npoll_status vs the 1 Hz emx_timer β mechanism PROVEN on the guest kernel via protocol replica; fix validated at protocol level (DF-3715 em(4) twin)
Summary
emx_timer() is an MP callout whose callback blocks on the driver serializer (lwkt_serialize_enter, self re-arm inside it). Three paths call the synchronous callout_stop(&sc->timer) while holding that serializer: emx_stop() (reached from every ifconfig down/up, MTU/media/cap change, detach/kldunload, suspend, watchdog reinit), the emx_intr_body() LSC path (ISR holds main + skipmain serializers), and emx_npoll_status(). callout_stop sleeps on 'costp' until the in-progress callback finishes, while the blocked callback sleeps on 'slize' until the stopper releases the serializer - a permanent AB-BA deadlock that also strands one CPU's softclock thread and starves all its future callouts. PROVEN on the guest kernel with an if_emx-protocol replica module: callout_stop NEVER returns - live ps shows 'D4 costp' AND '(softclock 0) B0 slize'; negative control (try-based shape) fully unloads; fix shape (try + 1-tick retry) completes the same stop cleanly. Triggered by ordinary privileged operations racing the 1 Hz timer (repeatable with a down/up loop); permanent system degradation requiring reboot.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-3738 Β· 12 files| File | Type | Description | Size | |
|---|---|---|---|---|
| emx_callout_replica.c | β | 7.8 KB | view raw | |
| emx_callout_replica_fixed.c | β | 9.6 KB | view raw | |
| build.sh | β | 125 B | view raw | |
| run.sh | β | 307 B | view raw | |
| build.log | β | 879 B | view raw | |
| run.log | β | 2.7 KB | view raw | |
| env.txt | β | 819 B | view raw | |
| fix.diff | β | 1013 B | view raw | |
| VERDICT.md | β | 4.3 KB | β raw | |
| README.md | β | 2.6 KB | β raw | |
| verdict.json | β | 5.0 KB | view raw | |
| manifest.json | β | 1.6 KB | view raw |
DF-3738 β emx(4) callout_stop(&sc->timer) AB-BA deadlock (DF-3715 em(4) twin)
What
emx_timer() is an MP callout whose callback blocks on the driver
serializer; emx_stop() (and two other paths) call callout_stop() on it
while holding that same serializer. DragonFly's callout_stop() is
synchronous β it sleeps ("costp", sys/kern/kern_timeout.c:920) until the
in-progress callback finishes β and lwkt_serialize_enter() sleeps
("slize", sys/kern/lwkt_serialize.c:234) until the serializer is free.
Callback parked on the serializer + stopper parked in callout_stop()
holding the serializer = permanent AB-BA deadlock: the stopping thread
AND the softclock thread of that CPU wedge forever.
Driver sites:
- emx_timer() β sys/dev/netif/emx/if_emx.c:1908-1931
(lwkt_serialize_enter(&sc->main_serialize) at :1914, self re-arm at
:1928 inside the serializer).
- callout_stop(&sc->timer) under ASSERT_IFNET_SERIALIZED_ALL:
- emx_stop() β :2030 (callers: SIOCSIFFLAGS :1227, emx_init :1354,
emx_detach :989, emx_suspend :1059 β every ifconfig down/up,
MTU/media/cap change, detach, kldunload, suspend, watchdog reinit);
- emx_intr_body() LSC path β :1534 (ISR holds main_serialize; then
emx_serialize_skipmain adds tx+rx serializers);
- emx_npoll_status() β :4196 (polling status, main serializer).
The try-based emx_txgc_timer() (:426-449) is not affected (it never
blocks); this is exactly the em(4) DF-3715 structure.
Build / run (guest)
# tarball Makefile + emx_callout_replica.c into /root/emxkmod cd /root/emxkmod && make kldload /root/emxkmod/emx_callout_replica.ko sysctl kern.emxreplica.arm=1; sleep 2 sysctl kern.emxreplica.stop_gc=1 # negative control: returns (sysctl kern.emxreplica.stop_blk=1 &) # AB-BA: NEVER returns sleep 6; dmesg | grep emxreplica | tail ps -o pid,stat,wchan,command -ax | grep stop_blk # D4 costp # then reset the guest
Expected (and observed) output
Console ends with:
emxreplica[blk]: handler BLOCKED on our serializer (fired=14 ran=13) - entering callout_stop() [emx_stop shape] emxreplica[blk]: IF THIS IS THE LAST emxreplica LINE, AB-BA DEADLOCK IS PROVEN
β and the "callout_stop returned" line NEVER appears. ps shows the
stopper in D4 costp; ps -ax shows softclock 0 in B0 slize (parked
inside the callback's lwkt_serialize_enter). Negative control
(stop_gc): callout_stop returned, pending=0.
Fixed-shape validation (emx_callout_replica_fixed.c, replicating the
fix.diff protocol): sysctl kern.emxreplica.stop_fixed=1 β
callout_stop returned, pending=0, STOP_FIXED_RC=0, clean kldunload.
DF-3738 β VERDICT
Status: inconclusive per family convention (the emx device path is hardware-gated on this guest), but the deadlock MECHANISM is PROVEN live on the guest kernel via a deterministic protocol replica β same standard as the em(4) twin DF-3715.
Mechanism proof (guest, 2026-09-15, full transcript in run.log)
Kernel module emx_callout_replica.ko replicates if_emx.c's exact
callout/serializer protocol with two callouts:
blk_timer==emx_timershape (if_emx.c:1908-1931): blockinglwkt_serialize_enter(:1914) + self re-arm INSIDE the serializer (:1928, before exit :1930);gc_timer==emx_txgc_timershape (:426-449): non-blockinglwkt_serialize_try(:436) + unconditionaldone:re-arm (:448).
Observed on the guest (serial console + second session):
1. Negative control (sysctl kern.emxreplica.stop_gc=1, emx_stop
shape against the try-based handler): callout_stop returned,
pending=0 β non-blocking callbacks are torn down fine.
2. AB-BA (sysctl kern.emxreplica.stop_blk=1 in background):
emxreplica[blk]: handler BLOCKED on our serializer (fired=14 ran=13) -
entering callout_stop() [emx_stop shape] β then silence forever;
the writing ssh session hung and was killed host-side;
ps: 1105 D4 costp sysctl kern.emxreplica.stop_blk=1
(the synchronous sleep INSIDE callout_stop(),
sys/kern/kern_timeout.c:920);
ps -ax: (softclock 0) B0 slize β CPU0's softclock thread parked in
lwkt_serialize_enter() (sys/kern/lwkt_serialize.c:234), i.e. INSIDE
the emx_timer-shaped callback. Both sides of the AB-BA captured.
3. Guest recovered only via vm.sh reset with-src.
Why the real driver deadlocks (path:line)
callout_stop()is synchronous: sys/kern/kern_timeout.c:1091 β_callout_cancel_or_stop(..., sync=1)(:859),ssleep(c, &c->spin, 0, "costp", 0)at :920 waits for the INPROG callback to finish.lwkt_serialize_enter()blocks: sys/kern/lwkt_serialize.c:234 (tsleep(s, PINTERLOCKED, "slize", 0)).emx_timercallback blocks onmain_serialize(if_emx.c:1914);emx_stop()/emx_intr_body()/emx_npoll_status()callcallout_stop(&sc->timer)(if_emx.c:2030/:1534/:4196) while holding it (ASSERT_IFNET_SERIALIZED_ALLat :2026; ISR +emx_serialize_skipmainat :1532; polling status serializer at :4236).- Consequence: permanent wedge of the stopping thread (any ifconfig
down/up, MTU/media/cap change, detach/kldunload, suspend, watchdog
reinit β all reach
emx_initβemx_stoporemx_stopdirectly) AND of one CPU's softclock thread (all future callouts on that CPU starve).
Race window: the 1 Hz timer must be INPROG-parked when the stop runs β small per call, but a down/up loop races it ~1/s per interface. Same judgment as DF-3715: Medium (system-level deadlock/DoS from ordinary privileged interface operations).
Fix validated at protocol level
fix.diff (git-apply clean against the read-only tree) converts
emx_timer() to the non-blocking try-and-retry pattern already used by
emx_txgc_timer(): lwkt_serialize_try(&sc->main_serialize); on failure
re-arm in 1 tick and return; double-check IFF_RUNNING|IFF_UP both before
the try (cheap racy read) and under the serializer.
Protocol validation on the guest (emx_callout_replica_fixed.c, the
fixed-shape replica): sysctl kern.emxreplica.stop_fixed=1 β
emxreplica[fx]: callout_stop returned, pending=0,
emxreplica[fx]: RESULT pending=0 (fix effective), STOP_FIXED_RC=0,
clean kldunload. Contrast with the unfixed shape under the identical
stop: permanent D4/costp wedge. (fix_status therefore fixed at the
protocol-replica level; a full kernel rebuild validation is not possible
without emx hardware β noted in fix_verdict.)
Note on the gc timer: the try-based emx_txgc_timer re-arm (:448) cannot
survive callout_stop post-processing on this kern_timeout.c (negative
control, pending=0) β the em(4) pass-2 reached the same conclusion, so no
separate post-stop re-arm finding is filed.
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
fixedProtocol-level validation on the guest: baseline (current emx_timer shape) = permanent AB-BA wedge (stopper D4 'costp', softclock B0 'slize'); patched shape (exact fix.diff protocol: try + 1-tick retry + double-check under serializer) = the same emx_stop-shaped callout_stop completes, pending=0, module unloads cleanly. Full driver-kernel rebuild validation not possible without emx hardware.
findings/poc/DF-3738/fix.diff; run.log 'fixed-shape validation' section
Confirmed kernel references
- sys/dev/netif/emx/if_emx.c:1914
- sys/dev/netif/emx/if_emx.c:1928
- sys/dev/netif/emx/if_emx.c:2030
- sys/dev/netif/emx/if_emx.c:1534
- sys/dev/netif/emx/if_emx.c:4196
- sys/dev/netif/emx/if_emx.c:2026
- sys/dev/netif/emx/if_emx.c:1227
- sys/kern/kern_timeout.c:1091
- sys/kern/kern_timeout.c:920
- sys/kern/lwkt_serialize.c:234
Detail
Evidence (decisive lines)
findings/poc/DF-3738/VERDICT.md; run.log (replica session transcript + serial console + ps captures: stopper D3/D4 wchan 'costp' = kern_timeout.c:920; softclock 0 B0 wchan 'slize' = lwkt_serialize.c:234; fixed-shape validation 'callout_stop returned, pending=0'); build.log (module built on guest, -Werror clean); fix.diff (try-enter + 1-tick retry for emx_timer)
PoC changes
No seed PoC. Authored emx_callout_replica.ko (out-of-tree kmodule replicating the emx callout protocol; adapted from DF-3715's em replica with emx line citations) plus emx_callout_replica_fixed.c (fix.diff shape) because the guest has no emx hardware; the deadlock is in the generic protocol emx uses, so the replica exercises the identical kernel paths. One replica iteration had a module-local bug (fx_c missing callout_init_mp -> KKASSERT panic in MY module); fixed and rerun.
Verified recommended fix
Make emx_timer() non-blocking: lwkt_serialize_try() with a 1-tick callout_reset retry (emx_txgc_timer pattern).
Verdict
Mechanism PROVEN on the guest kernel by deterministic replica of if_emx.c's exact callout/serializer protocol; emx-specific runtime hardware-gated (inconclusive per family convention, same standard as the em(4) twin DF-3715). emx_timer (sys/dev/netif/emx/if_emx.c:1908-1931) is an MP callout whose callback BLOCKS in lwkt_serialize_enter(&sc->main_serialize) (:1914) and self re-arms inside the serializer (:1928); emx_stop tears it down with callout_stop(&sc->timer) (:2030) while holding ALL serializers (ASSERT_IFNET_SERIALIZED_ALL :2026; callers: SIOCSIFFLAGS :1227, emx_init :1354, emx_detach :989, emx_suspend :1059); the same stop-under-serializer exists in emx_intr_body LSC path (:1534, ISR holds main + skipmain serializers) and emx_npoll_status (:4196). DragonFly callout_stop synchronously waits for an in-progress callback (sys/kern/kern_timeout.c:1091 -> _callout_cancel_or_stop sync=1, ssleep 'costp' at :920) while lwkt_serialize_enter blocks (tsleep 'slize', sys/kern/lwkt_serialize.c:234). A callback dequeued and parked on the serializer therefore deadlocks against the stopper (AB-BA): both threads wedge permanently and the affected CPU's softclock thread stops delivering callouts. Kernel-module replica on the guest: stop under a held serializer with the emx_timer-shaped handler parked -> callout_stop NEVER returns (ps: '1105 D4 costp sysctl kern.emxreplica.stop_blk=1'; ps -ax: '(softclock 0) B0 slize'; console output ends at the pre-callout_stop marker line). Negative control: the try-based emx_txgc_timer shape is fully unloaded by callout_stop (pending=0) - so the try-based gc timer's 'done:' re-arm (:448) cannot survive callout_stop post-processing on this kern_timeout.c, and no separate post-stop re-arm finding is filed (same judgment as em(4) pass 2). Fix validated at protocol level: replica of the fix.diff shape (try + 1-tick retry + double-check) completes the same stop cleanly (pending=0, clean kldunload).
No comments yet.