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

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-4316 Β· 12 files
FileTypeDescriptionSize
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.sh
  • build.log β€” full module build (BUILD_RC=0, -Werror)
  • run.log β€” control + deadlock sessions + post-wedge captures
  • env.txt β€” guest uname / NICs / compiler
  • fix.diff β€” lge_tick try-enter + 1-tick retry (never blocks)
  • VERDICT.md, manifest.json, verdict.json
VERDICT.md
↓ download raw

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)

  1. sys/dev/netif/lge/if_lge.c:435 β€” callout_init(&sc->lge_stat_timer); armed at :1319 (lge_init) with period hz, re-armed at :1017 (lge_tick_serialized) β€” 1 Hz stat tick.
  2. sys/dev/netif/lge/if_lge.c:984 β€” lge_tick() does a blocking lwkt_serialize_enter(ifp->if_serializer); the work body and the self re-arm (:1017) run inside the serializer; exit at :986.
  3. sys/dev/netif/lge/if_lge.c:1442 β€” lge_stop() calls callout_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 under ifp->if_serializer, handed to bus_setup_intr at :553-555) - lge_ioctl :1391 (SIOCSIFFLAGS down transition) (Only lge_shutdown :1486 calls lge_stop without the serializer β€” no AB-BA there, stopper holds nothing.) Additionally lge_intr :1055 (PHY_INTR) calls callout_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.
  4. sys/kern/kern_timeout.c:1091-1094 β€” callout_stop() β†’ _callout_cancel_or_stop(..., CALLOUT_STOP, sync=1); when the callout is INPROG the caller ssleep(c, &c->spin, 0, "costp", 0) at :920 until the callback completes.
  5. β‡’ If lge_tick is dequeued (INPROG) while a stop-path thread holds the serializer: the callback parks spinning in lwkt_serialize_enter waiting 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.ko on the guest against /usr/src/sys (build.log, BUILD_RC=0, -Werror clean). The module replicates lge's exact protocol: one callout, period hz (=:1319/:1017), callback = blocking lwkt_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 printed handler PARKED on our serializer (fired=14 ran=13) - entering callout_stop() then IF 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 showed 1223 D4 costp sysctl kern.lgereplica.stop_lge=1 (state D4, wchan costp = 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_testable
baseline no→ patch + rebuild →patched clean

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

Confirmed kernel references

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.