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

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-3717 Β· 10 files
FileTypeDescriptionSize
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

sys/dev/netif/mxge/if_mxge.c:

  • mxge_tick() (:3712-3750) is a self-re-arming MP callout (sc->co_hdl, armed at attach with callout_reset_bycpu :4445, re-armed at :3747 with callout_reset from inside the held sc->main_serialize). Its first act (:3720) is a blocking lwkt_serialize_enter(&sc->main_serialize).
  • mxge_detach() (:4454-4517) runs ifnet_serialize_all(ifp) (:4463 β€” this takes main_serialize first, it is serializes[0], :4133) and then calls synchronous callout_stop(&sc->co_hdl) (:4468) while holding that serializer. (callout_terminate at :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_tick shape: first statement blocking lwkt_serialize_enter, self re-arm via callout_reset inside the lock.
  • fix sysctl = mxge_detach with the recommended fix: enter serializer, callout_stop_async, release, callout_terminate (mirrors :4463 + fixed :4468 + existing :4474).
  • stop sysctl = mxge_detach as shipped: enter serializer, sync callout_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 unrelated panic: 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 in B2 slize while the waiter sat in D2 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).

VERDICT.md
↓ download raw

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:

  1. Fix shape (detach with callout_stop_async under the serializer + callout_terminate after 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.
  2. Shipped shape (:4463-4468 exactly): tick parked; callout_stop entered and never returned. Live state: stopper D1 costp (inside kern_timeout.c:920 wait) holding the serializer; softclock of the test CPU B2 slize (inside lwkt_serialize_enter) β€” both halves of the AB-BA visible in ps. 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_testable
baseline no→ patch + rebuild →patched clean

fix.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
↓ fix.diffper-fix-DF-3717

Confirmed kernel references

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.