lge_stop() callout_stop-under-serializer AB-BA deadlock: lge_tick() parks on if_serializer every stop-path caller holds (DF-3715 family) β permanent kernel hang + CPU-wide callout starvation
Summary
lge_tick does a BLOCKING serialize_enter and re-arms itself inside that lock. lge_stop tears the timer with the synchronous callout_stop while holding the same serializer - and every in-tree caller holds it (detach, init via ioctl/watchdog/rxeoc, intr, ioctl). A dequeued tick parked on the serializer + the stopper sleeping on 'costp' = permanent AB-BA: interface serializer never released, one CPU's softclock stops delivering ALL callouts; the PHY_INTR path additionally wedges the driver ithread. MECHANISM PROVEN deterministically on the guest with an lge-exact protocol replica (stopper D4/costp never returned; recommended-fix shape live-validated as the clean control). Try-enter + retry fix.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-4316 Β· 12 files| File | Type | Description | Size | |
|---|---|---|---|---|
| README.md | β | 3.2 KB | β raw | |
| VERDICT.md | β | 5.4 KB | β raw | |
| lge_callout_replica.c | β | 7.1 KB | view raw | |
| Makefile | β | 78 B | β download | |
| build.sh | β | 144 B | view raw | |
| run.sh | β | 291 B | view raw | |
| build.log | β | 5.6 KB | view raw | |
| run.log | β | 1.4 KB | view raw | |
| env.txt | β | 210 B | view raw | |
| fix.diff | β | 907 B | view raw | |
| manifest.json | β | 1.6 KB | view raw | |
| verdict.json | β | 4.9 KB | view raw |
DF-4316 β lge_stop()/lge_tick() callout AB-BA deadlock (DF-3715 family, lge member)
What: lge_tick() blocks in lwkt_serialize_enter(ifp->if_serializer)
(if_lge.c:984) and re-arms itself inside that serializer (:1017). Every
in-tree caller of lge_stop() β which does callout_stop(&sc->lge_stat_timer)
(:1442) β holds that same serializer (lge_detach :576-580, lge_init :1211 via
SIOCSIFFLAGS :1387 / lge_rxeoc :933 / lge_watchdog :1425, lge_intr :1029,
lge_ioctl :1391). DragonFly's callout_stop() is synchronous
(kern_timeout.c:1091 β sync=1 β ssleep "costp" :920): it sleeps until an
INPROG callback returns. A lge_tick dequeued while a stop-path thread holds
the serializer therefore parks inside lwkt_serialize_enter forever, and the
stopper sleeps in costp forever holding the serializer β permanent AB-BA
deadlock. Collateral: the interface serializer is never released (every later
lge path blocks) and the parked CPU's softclock thread stops delivering all
callouts on that CPU.
This is the same protocol defect proven for em(4) in DF-3715; this pack proves it deterministically for lge's exact callout shape.
Build / run / expected
Everything runs on the QEMU guest (DragonFly 6.5-DEVELOPMENT x86_64, 6 vCPU):
# on host
dfbsd-qemu/vm.sh up # if down
# copy lge_callout_replica.c + Makefile to guest:/root/lgerep
dfbsd-qemu/vm.sh run_root 'cd /root/lgerep && make' # see build.log
# negative control (recommended-fix shape: try-enter + retry):
dfbsd-qemu/vm.sh run_root 'kldload /root/lgerep/lge_callout_replica.ko; \
sysctl kern.lgereplica.arm=1; sleep 2; sysctl kern.lgereplica.stop_fix=1'
# EXPECT (dmesg): "lgereplica[fix]: RESULT pending=0 ... does NOT deadlock"
# and the sysctl command returns (exit 0).
# positive test (exact lge_tick / lge_stop shape):
dfbsd-qemu/vm.sh run_root '(sysctl kern.lgereplica.stop_lge=1 &); sleep 25; \
ps -axo pid,stat,wchan,command | grep stop_lge; dmesg | tail -8'
# EXPECT (dmesg): "handler PARKED on our serializer ... entering callout_stop()"
# "IF THIS IS THE LAST lgereplica LINE, ..." -- then silence
# (ps): "<pid> D4 costp sysctl kern.lgereplica.stop_lge=1"
# The sysctl never returns; the session that ran it wedges; a second ssh
# session still works (6 vCPU guest) and shows the D4/costp thread.
# After the test the guest is deliberately wedged -> vm.sh reset with-src.
On real lge hardware the trigger is: ifconfig lge0 down (or up β
lge_initβlge_stop), kldunload if_lge, watchdog reinit, or RXCMDFIFO-empty
reinit, with the 1 Hz stat tick firing inside the stop-path's serialized
section. The guest has no LXT1001 NIC (ifconfig -l β vtnet0 lo0), so the
in-driver instance cannot be fired there β hence the kernel-module replica of
lge's exact callout protocol.
Files
lge_callout_replica.cβ KLD replica (lge_tick shape + fix-shape control)Makefile,build.sh,run.shbuild.logβ full module build (BUILD_RC=0, -Werror)run.logβ control + deadlock sessions + post-wedge capturesenv.txtβ guest uname / NICs / compilerfix.diffβ lge_tick try-enter + 1-tick retry (never blocks)VERDICT.md,manifest.json,verdict.json
DF-4316 β VERDICT
Status: mechanism PROVEN on the guest kernel via a deterministic replica of if_lge.c's exact callout/serializer protocol; the lge-specific runtime is hardware-gated (no Level-One LXT1001 NIC on the guest) β inconclusive per DF-3715 family convention, reproduced=0, impact=none-on-this-guest. The class of impact when it fires on lge hardware is a permanent kernel deadlock (local DoS): admin thread D-state forever, interface serializer held forever, one CPU's softclock thread wedged (all callouts on that CPU stop).
Root cause (path:line)
sys/dev/netif/lge/if_lge.c:435βcallout_init(&sc->lge_stat_timer); armed at:1319(lge_init) with periodhz, re-armed at:1017(lge_tick_serialized) β 1 Hz stat tick.sys/dev/netif/lge/if_lge.c:984βlge_tick()does a blockinglwkt_serialize_enter(ifp->if_serializer); the work body and the self re-arm (:1017) run inside the serializer; exit at:986.sys/dev/netif/lge/if_lge.c:1442βlge_stop()callscallout_stop(&sc->lge_stat_timer)while holding that serializer. Every in-tree lge_stop() caller holds it: -lge_detach:576-580 (explicit serialize_enter around lge_reset/lge_stop) -lge_init:1211 β invoked from SIOCSIFFLAGS :1387, lge_rxeoc :933 (from lge_intr), lge_watchdog :1425, if_up -lge_intr:1029 (!(IFF_UP)β lge_stop; intr runs underifp->if_serializer, handed to bus_setup_intr at :553-555) -lge_ioctl:1391 (SIOCSIFFLAGS down transition) (Onlylge_shutdown:1486 calls lge_stop without the serializer β no AB-BA there, stopper holds nothing.) Additionallylge_intr:1055 (PHY_INTR) callscallout_stop()from interrupt-thread context; if the callback is INPROG there, the sync wait (ssleep) happens from an ithread β wedging the driver's interrupt thread.sys/kern/kern_timeout.c:1091-1094βcallout_stop()β_callout_cancel_or_stop(..., CALLOUT_STOP, sync=1); when the callout is INPROG the callerssleep(c, &c->spin, 0, "costp", 0)at:920until the callback completes.- β If
lge_tickis dequeued (INPROG) while a stop-path thread holds the serializer: the callback parks spinning inlwkt_serialize_enterwaiting for the serializer; the stopper sleeps on "costp" waiting for the callback; neither can proceed. The serializer is held forever (all later lge paths block), and the softclock thread of the CPU that armed the timer stops delivering every callout on that CPU (tsleep timeouts included).
Race window: the 1 Hz tick must be dequeued while a stop-path caller is
between serialize_enter and callout_stop β once parked it stays parked,
so the effective window is the whole serialized stop-path preamble up to
:1442 (Β΅s-ms per stop, one die-roll per second of uptime per stop
invocation). Triggers on hardware: ifconfig lge0 down/up, MTU change,
kldunload if_lge/detach, watchdog reinit, RXCMDFIFO-empty reinit, PHY
interrupts. Repeatable admin/hardware events Γ 1 Hz tick β eventual
occurrence on long-lived boxes is plausible, exactly as argued for DF-3715.
Verification performed (guest, DragonFly 6.5-DEVELOPMENT x86_64, 6 vCPU)
- Built
lge_callout_replica.koon the guest against /usr/src/sys (build.log, BUILD_RC=0, -Werror clean). The module replicates lge's exact protocol: one callout, periodhz(=:1319/:1017), callback = blockinglwkt_serialize_enterβ work + self re-arm inside β exit (=:978-987); stopper sysctl = serializer acquire (as every lge_stop caller) βcallout_stop()(=:1442) β release. - Negative control (
kern.lgereplica.stop_fix): the recommended-fix shape (lwkt_serialize_try+ 1-tick retry) βcallout_stop()returned while the stopper held the serializer,pending=0; sysctl exited 0. This also live-validates the fix mechanism on this kernel. - Positive test (
kern.lgereplica.stop_lge): after arming, the stopper printedhandler PARKED on our serializer (fired=14 ran=13) - entering callout_stop()thenIF THIS IS THE LAST lgereplica LINE, THE LGE_STOP AB-BA DEADLOCK IS PROVENβ and nothing more. The sysctl command never returned; its whole ssh session wedged; a second ssh session showed1223 D4 costp sysctl kern.lgereplica.stop_lge=1(state D4, wchancostp= kern_timeout.c:920), captured in run.log. Deterministic, not probabilistic: the harness waits for the park before calling callout_stop. - lge hardware absent (
ifconfig -lβvtnet0 lo0) β the if_lge.c instance of the race itself cannot be fired on this guest. - Guest reset (
vm.sh reset with-src) after the wedge; guest_dirty=0.
Fix
fix.diff: make lge_tick() never block β lwkt_serialize_try; on failure
re-arm in 1 tick and return (same shape em(4)'s txgc timer uses, validated
live by the negative control). With that, callout_stop()'s sync wait always
completes and the AB-BA edge is gone. Full end-to-end fix validation would
require lge hardware (absent) β fix_status=not_testable; the replica control
demonstrates the fixed protocol does not deadlock under the identical
stop-while-held-serializer scenario.
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 (applies cleanly to sys/dev/netif/lge/if_lge.c). Cannot be validated end-to-end on a guest without lge hardware; the replica's negative control demonstrates the fixed protocol shape does not deadlock under the identical stop-while-held-serializer scenario (callout_stop returned, pending=0).
findings/poc/DF-4316/fix.diff; findings/poc/DF-4316/run.log (stop_fix control)
Confirmed kernel references
- sys/dev/netif/lge/if_lge.c:435
- sys/dev/netif/lge/if_lge.c:984
- sys/dev/netif/lge/if_lge.c:1017
- sys/dev/netif/lge/if_lge.c:1055
- sys/dev/netif/lge/if_lge.c:1211
- sys/dev/netif/lge/if_lge.c:1319
- sys/dev/netif/lge/if_lge.c:1442
- sys/dev/netif/lge/if_lge.c:576
- sys/dev/netif/lge/if_lge.c:1029
- sys/dev/netif/lge/if_lge.c:1391
- sys/kern/kern_timeout.c:920
- sys/kern/kern_timeout.c:1091
Detail
Evidence (decisive lines)
findings/poc/DF-4316/VERDICT.md; run.log (control: 'RESULT pending=0 ... does NOT deadlock'; positive: 'handler PARKED ... entering callout_stop()' then silence, ps '1223 D4 costp sysctl kern.lgereplica.stop_lge=1' captured from a second session); build.log (module built on guest, BUILD_RC=0, -Werror clean); env.txt (guest uname, no lge NIC)
PoC changes
No seed PoC. Authored lge_callout_replica.ko (out-of-tree kmodule replicating lge's exact one-callout protocol: blocking serialize_enter, self re-arm inside the lock at hz period; stopper sysctl holding the serializer) because the guest has no LGE NIC; the deadlock lives in the generic protocol lge uses, so the replica exercises the identical kernel paths.
Verified recommended fix
Make lge_tick() non-blocking: lwkt_serialize_try(ifp->if_serializer) with a 1-tick callout_reset retry on failure (fix.diff).
Verdict
Mechanism PROVEN on the guest kernel by a deterministic replica of if_lge.c's exact callout/serializer protocol; the lge-specific runtime is hardware-gated (no Level-One LXT1001 NIC on the guest: ifconfig -l -> 'vtnet0 lo0') -> inconclusive per DF-3715 family convention. lge_tick() (if_lge.c:978-987) BLOCKS in lwkt_serialize_enter(ifp->if_serializer) (:984) and re-arms itself INSIDE that lock (:1017); lge_stop() tears the callout down with callout_stop() (:1442) while holding the same serializer -- and every in-tree lge_stop caller holds it (lge_detach :576-580, lge_init :1211 via SIOCSIFFLAGS :1387 / lge_rxeoc :933 / lge_watchdog :1425, lge_intr :1029, lge_ioctl :1391). DragonFly callout_stop() is synchronous (kern_timeout.c:1091 -> sync=1): when the callback is INPROG the caller ssleep('costp')s (kern_timeout.c:920) until it completes. A lge_tick dequeued while a stop-path thread holds the serializer parks on it and deadlocks the stopper (AB-BA): admin thread D-state forever, interface serializer never released, and the parked CPU's softclock thread stops delivering every callout on that CPU. lge_intr :1055 additionally calls callout_stop() from interrupt-thread context (PHY_INTR), wedging the driver ithread under the same conditions. Guest replica: with the exact lge_tick/lge_stop shapes, callout_stop() NEVER returned (caller stuck '1223 D4 costp sysctl kern.lgereplica.stop_lge=1', dmesg ends at 'IF THIS IS THE LAST lgereplica LINE, THE LGE_STOP AB-BA DEADLOCK IS PROVEN'); negative control with the recommended-fix shape (lwkt_serialize_try + 1-tick retry) stopped cleanly under a held serializer (pending=0), live-validating the fix mechanism. On lge hardware the trigger is any lge_stop racing the 1 Hz stat tick: ifconfig lge0 down/up, MTU change, kldunload/detach, watchdog reinit, RXCMDFIFO-empty reinit, PHY interrupt.
No comments yet.