mxge_detach() AB-BA deadlock: synchronous callout_stop() under ifnet_serialize_all vs mxge_tick blocked on main_serialize β mechanism PROVEN on guest via protocol replica (wchan 'costp' + softclock 'slize' wedge)
Summary
mxge_tick is a self-re-arming MP callout whose first act is a BLOCKING lwkt_serialize_enter of sc->main_serialize, re-arming inside the lock. mxge_detach takes all serializers via ifnet_serialize_all then calls SYNC callout_stop. callout_stop sleeps on 'costp' until the INPROG callback completes, while the callback sleeps in lwkt_serialize_enter ('slize') on the serializer the stopper holds - permanent wedge of the detaching thread AND the callback CPU's softclock thread (all callouts/tsleep timeouts on that CPU dead). Same mechanism class as DF-3715 (em). PROVEN on this guest kernel with an if_mxge-protocol replica module: shipped shape -> callout_stop never returned, live ps shows 'D1 costp' stopper + 'B2 slize (softclock 2)'; fix shape (callout_stop_async + the existing terminate after release) returned cleanly pending=0 in three runs. Any mxge_detach on a live device racing the default hz/2 tick: two wedged kernel threads + one CPU's callout subsystem dead.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-3717 Β· 10 files| File | Type | Description | Size | |
|---|---|---|---|---|
| README.md | β | 5.1 KB | β raw | |
| VERDICT.md | β | 3.6 KB | β raw | |
| mxge_callout_replica.c | β | 7.4 KB | view raw | |
| build.sh | β | 137 B | view raw | |
| run.sh | β | 264 B | view raw | |
| build.log | β | 5.4 KB | view raw | |
| run.log | β | 4.1 KB | view raw | |
| fix.diff | β | 711 B | view raw | |
| verdict.json | β | 4.5 KB | view raw | |
| env.txt | β | 404 B | view raw |
DF-3717 β mxge_detach() callout_stop() AB-BA deadlock vs mxge_tick (protocol replica, guest-verified mechanism)
What this finding is
mxge_tick()(:3712-3750) is a self-re-arming MP callout (sc->co_hdl, armed at attach withcallout_reset_bycpu:4445, re-armed at :3747 withcallout_resetfrom inside the heldsc->main_serialize). Its first act (:3720) is a blockinglwkt_serialize_enter(&sc->main_serialize).mxge_detach()(:4454-4517) runsifnet_serialize_all(ifp)(:4463 β this takesmain_serializefirst, it isserializes[0], :4133) and then calls synchronouscallout_stop(&sc->co_hdl)(:4468) while holding that serializer. (callout_terminateat :4474 is correctly placed outside the serializers, but the deadlock happens earlier, at :4468.)
DragonFly's callout_stop() (sys/kern/kern_timeout.c:1091-1093) calls
_callout_cancel_or_stop(cc, CALLOUT_STOP, 1) β sync=1 β which sleeps on
"costp" (kern_timeout.c:920) until an INPROG callback completes. A
mxge_tick callback that was dequeued and has parked in
lwkt_serialize_enter (lwkt_serialize.c:84-90 β atomic_intr_cond_enter β
lwkt_serialize_sleep β tsleep(..., "slize", ...)) can never complete
until the serializer is released β and the stopper holds it while it sleeps.
AB-BA: permanent wedge of (a) the detaching thread and (b) the per-CPU
softclock thread of the callback's CPU β all callouts on that CPU stop
firing. Same mechanism class as DF-3715 (em(4)), here in the mxge(4)
detachment path.
Guest verification (no mxge hardware β protocol replica)
The guest has no Myricom NIC (no 0x14c1:0x0008/0x0009 PCI device;
if_mxge.ko present in /boot/kernel but not loaded), so the driver path
itself cannot run. Per family convention (DF-3713..DF-3715), the kernel
primitives were verified on the guest with an out-of-tree module
(mxge_callout_replica.c) that replicates the exact mxge protocol:
tick_fix/tick_stop=mxge_tickshape: first statement blockinglwkt_serialize_enter, self re-arm viacallout_resetinside the lock.fixsysctl = mxge_detach with the recommended fix: enter serializer,callout_stop_async, release,callout_terminate(mirrors :4463 + fixed :4468 + existing :4474).stopsysctl = mxge_detach as shipped: enter serializer, synccallout_stop(:4463-:4468).
Result (decisive run, DragonFly 6.5-DEVELOPMENT x86_64, hz=100)
Fix shape β no deadlock, clean stop (serial console):
mxge_replica[fix]: tick parked (started=9 ran=8); callout_stop_async + release + callout_terminate mxge_replica[fix]: returned; pending=0 ran=9 (0 == cleanly stopped, NO deadlock)
(sysctl returned FIX_RC=0.)
Shipped shape β AB-BA deadlock proven (serial console; last lines ever printed by the module):
mxge_replica: armed STOP-test tick (hz/4) mxge_replica[stop]: tick BLOCKED on our serializer (started=9 ran=8) - entering callout_stop() :4468 mxge_replica[stop]: IF THIS IS THE LAST mxge_replica LINE, THE mxge_detach AB-BA DEADLOCK IS PROVEN
callout_stop() never returned. Live wedge state observed from a second
session (ps -ax -o pid,stat,wchan,command):
-1 B2 slize (softclock 2) 859 D1 costp sysctl kern.mxgereplica.stop=1
i.e. the softclock thread of CPU 2 is parked inside
lwkt_serialize_enter (wchan slize, the mxge_tick half) while the
"detaching" thread sleeps inside callout_stop's sync wait (wchan
costp, kern_timeout.c:920 β the mxge_detach half) holding the very
serializer the callback needs. Both wedged permanently; CPU 2's callout
delivery dead. Guest was reset with vm.sh reset with-src afterwards.
Harness notes (honesty section)
- v1 of the replica re-armed a
callout_terminated callout and hit an unrelatedpanic: assertion "cc->flags & CALLOUT_DID_INIT"β a harness bug (terminate is terminal; mxge never re-arms after detach), fixed by using two independent callouts. - v2 polled for the parked callback with
tsleep(timeout=2)β the poll froze because tsleep timeouts are themselves callouts serviced by the very softclock thread that parks (observed live: softclock N inB2 slizewhile the waiter sat inD2 mxgw). Replaced with a bounded busy-wait. Both artifacts are documented in run.log; neither affects the final decisive run, whose protocol matches if_mxge.c exactly.
Build / run
build.sh # tarball -> /root/kmod on guest; make (DragonFly bsd.kmod.mk) run.sh # kldload; fix-shape test; then shipped-shape test (wedges guest)
Expected: fix-shape returns pending=0; shipped-shape prints the two
[stop] lines and never returns; ps shows costp + slize wedge.
Fix
fix.diff: replace the synchronous callout_stop(&sc->co_hdl) at
if_mxge.c:4468 with callout_stop_async(&sc->co_hdl) (the existing
callout_terminate(&sc->co_hdl) at :4474 β already outside the serializers
β then performs the sync drain safely). Fix-shape validated on the guest
(see above). vmxge cannot be diff-tested without hardware
(fix_status=not_testable per family convention).
DF-3717 VERDICT β mxge_detach() AB-BA deadlock (callout_stop vs mxge_tick)
Finding. mxge_detach() (sys/dev/netif/mxge/if_mxge.c:4463-4468) calls
synchronous callout_stop(&sc->co_hdl) while holding
sc->main_serialize (via ifnet_serialize_all, :4463 β main_serialize is
sc->serializes[0], installed at :4133). The mxge_tick callback
(:3712-3750) begins with a blocking lwkt_serialize_enter(&sc->main_serialize)
(:3720) and re-arms itself inside the lock (:3747). DragonFly's
callout_stop (kern_timeout.c:1091 β _callout_cancel_or_stop(..., sync=1))
sleeps on "costp" (kern_timeout.c:920) until an INPROG callback finishes;
the parked callback sleeps in lwkt_serialize_enter (lwkt_serialize.c:84-90,
wchan "slize") until the stopper releases the serializer. If the tick
(armed at :4445, every mxge_ticks = hz/2 by default) fires in the window
after the detaching thread acquired main_serialize, the two threads wedge
permanently and the callback's CPU loses its softclock thread (every callout
on that CPU stops firing: timers, tsleep timeouts).
Verification. Guest has no Myricom hardware (no 0x14c1 PCI device;
if_mxge.ko not loaded), so the driver path is hardware-gated β per family
convention (DF-3713/3714/3715) the mechanism was proven with a kernel-module
replica of the exact protocol on the same kernel primitives:
- Fix shape (detach with
callout_stop_asyncunder the serializer +callout_terminateafter release β mirroring fixed :4468 + existing :4474): tick parked on the serializer; stop returned immediately;pending=0; callback completed (ran=9). NO deadlock β the recommended fix is validated on-guest. - Shipped shape (:4463-4468 exactly): tick parked;
callout_stopentered and never returned. Live state: stopperD1 costp(inside kern_timeout.c:920 wait) holding the serializer; softclock of the test CPUB2 slize(inside lwkt_serialize_enter) β both halves of the AB-BA visible inps. This is the same mechanism DF-3715 proved for em(4); the mxge locus is the detach path rather than em_stop.
Classification. Per family convention: status=inconclusive,
reproduced=0, impact=none, confidence=certain β the mxge-specific
runtime requires hardware the guest lacks; the underlying kernel-primitive
mechanism is proven deterministically (three clean fix-shape runs, one
decisive shipped-shape wedge, full state capture, guest reset afterwards).
Trigger conditions on real hardware. Any mxge_detach() on a live
device β kldunload if_mxge/kldunload mxge_fw_*, devd/ACPI-driven device
removal, surprise hotplug β racing the hz/2 tick. Root-initiated action
required (as with DF-3715), but the result is a permanent system-wide
degradation, not a clean unload: two wedged kernel threads and one CPU's
callout subsystem dead.
Harness incidents (documented, not affecting the decisive run).
v1 re-armed a terminated callout β unrelated DID_INIT assertion panic;
v2's tsleep-timeout poll froze on the parked softclock (itself evidence
that the parked softclock stops servicing tsleep timeouts). Both fixed;
final replica replicates if_mxge.c's protocol one-to-one and uses a bounded
busy-wait observer. Guest left clean (vm.sh reset with-src).
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 (git apply --check clean); the fix SHAPE (stop_async under serializer + terminate after release) was validated on the guest via the replica fix-test (returned, pending=0, no deadlock), but the mxge kernel itself cannot be diff-built without hardware.
findings/poc/DF-3717/fix.diff; run.log fix-shape section
Confirmed kernel references
- sys/dev/netif/mxge/if_mxge.c:3720
- sys/dev/netif/mxge/if_mxge.c:3747
- sys/dev/netif/mxge/if_mxge.c:4445
- sys/dev/netif/mxge/if_mxge.c:4463
- sys/dev/netif/mxge/if_mxge.c:4468
- sys/dev/netif/mxge/if_mxge.c:4474
- sys/dev/netif/mxge/if_mxge.c:4133
- sys/kern/kern_timeout.c:920
- sys/kern/kern_timeout.c:1091
- sys/kern/lwkt_serialize.c:84
Detail
Evidence (decisive lines)
findings/poc/DF-3717/run.log (decisive run: serial-console '[stop] ... IF THIS IS THE LAST mxge_replica LINE' then silence; second-session ps '-1 B2 slize (softclock 2)' + '859 D1 costp sysctl kern.mxgereplica.stop=1'; fix-shape 'returned; pending=0 ran=9' FIX_RC=0); build.log; VERDICT.md (incl. v1/v2 harness-incident honesty record); fix.diff (git-apply clean; callout_stop -> callout_stop_async at :4468)
PoC changes
No seed PoC. Authored mxge_callout_replica.ko (out-of-tree module replicating the mxge_tick/mxge_detach protocol: blocking-enter-first callback, self re-arm inside the lock, sync stop under the lock) because the guest has no Myricom hardware; two harness iterations were needed (v1 wrongly re-armed a terminated callout -> unrelated DID_INIT panic; v2's tsleep-timeout poll froze on the parked softclock) - both documented; final replica uses two independent callouts and a bounded busy-wait observer.
Verified recommended fix
Replace callout_stop(&sc->co_hdl) at if_mxge.c:4468 with callout_stop_async(); the existing callout_terminate() at :4474 (already outside ifnet_serialize_all) then drains synchronously and safely (validated on-guest by the fix-shape replica run).
Verdict
Mechanism PROVEN on the guest kernel by deterministic replica of if_mxge.c's exact callout/serializer teardown protocol; mxge-specific runtime hardware-gated (inconclusive per family convention, cf. DF-3715). mxge_tick (sys/dev/netif/mxge/if_mxge.c:3712-3750) is a self-re-arming MP callout (armed :4445, re-armed :3747 inside the held lock) whose FIRST act is a BLOCKING lwkt_serialize_enter of sc->main_serialize (:3720); mxge_detach runs ifnet_serialize_all (:4463, main_serialize is serializes[0] :4133) and then SYNCHRONOUS callout_stop(&sc->co_hdl) (:4468) while holding that serializer. DragonFly callout_stop sleeps on 'costp' (sys/kern/kern_timeout.c:920 via :1091 sync=1) until the INPROG callback completes; the callback sleeps in lwkt_serialize_enter (sys/kern/lwkt_serialize.c:84-90, wchan 'slize') until the stopper releases the serializer it holds. AB-BA wedge. Kernel-module replica on the guest: shipped shape -> callout_stop NEVER returns, ps shows the stopper 'D1 costp' while 'softclock N' sits 'B2 slize' (both halves of the deadlock captured live; that CPU's callout delivery dead); fix shape (callout_stop_async under the serializer + the driver's existing callout_terminate :4474 after release) returns cleanly with pending=0 in three independent runs - the recommended fix is validated on-guest. Trigger on real hardware: any mxge_detach (kldunload, devd/hotplug removal) racing the hz/2 tick.
No comments yet.