dc_stop()/dc_tick() AB-BA callout deadlock: synchronous callout_stop under the ifnet serializer vs dc_tick's blocking lwkt_serialize_enter β and dc_stat_timer is a NON-MPSAFE callout, so the parked softclock thread also holds the Giant MP lock (DF-3715 family, dc(4) instance, mechanism PROVEN on guest)
Summary
dc_attach initializes sc->dc_stat_timer with plain callout_init - a NON-MPSAFE callout whose callback executes under the Giant MP lock. The callback does a BLOCKING lwkt_serialize_enter and self-re-arms before releasing. dc_stop() calls the synchronous callout_stop while holding that same serializer - and every dc_stop caller holds it (detach, intr, init via ioctl up/MTU/media and watchdog, shutdown, suspend). A dc_tick that has been dequeued and is parked on the serializer deadlocks against the stopper (AB-BA): both threads sleep forever, the serializer is never released, the affected CPU's softclock stops delivering ALL callouts, and - the dc-specific aggravation - the parked softclock thread holds the Giant MP lock, degrading legacy MP-lock consumers system-wide. PROVEN on the guest FIRST ATTEMPT via protocol replica: stopper 'D1 costp', '(softclock 0) B0 slize' with MP lock held, wedge survived clean shutdown; fix-shape validated live on a fresh guest (try + 1-tick retry: stop returns in 3.09s, pending=0, 318 fires/308 retries/0 parks). Local admin action racing the 1 Hz tick -> permanent kernel wedge.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-3850 Β· 13 files| File | Type | Description | Size | |
|---|---|---|---|---|
| README.md | β | 2.0 KB | β raw | |
| VERDICT.md | β | 5.1 KB | β raw | |
| dc_callout_replica.c | β | 5.1 KB | view raw | |
| dc_callout_replica_fixed.c | β | 3.3 KB | view raw | |
| build.sh | β | 147 B | view raw | |
| run.sh | β | 274 B | view raw | |
| build.log | β | 1.9 KB | view raw | |
| run.log | β | 4.1 KB | view raw | |
| fix_validation.log | β | 1.1 KB | view raw | |
| fix.diff | β | 599 B | view raw | |
| manifest.json | β | 1.7 KB | view raw | |
| verdict.json | β | 4.7 KB | view raw | |
| env.txt | β | 189 B | view raw |
DF-3850 β dc(4) AB-BA callout deadlock (dc_stop vs dc_tick)
Severity: Medium Β· confidence: certain (mechanism proven on guest) Β· hardware-gated at driver level (inconclusive per family convention)
What this is
sys/dev/netif/dc/if_dc.c β dc_stop() can deadlock the callout subsystem
against dc_tick() (AB-BA): callout_stop() synchronously waits for a
callback that is blocked on the very serializer the stopper holds.
DF-3715/3738/3760/3776/3806 family (em, emx, alc, jme, msk); dc(4) instance,
aggravated by dc's non-MPSAFE callout (parked softclock thread holds the
Giant MP lock).
Reproduce (on the audit guest, or any DragonFly 6.x with /usr/src)
./build.sh # build /root/kmod/dc_callout_replica.ko on the guest
./run.sh # arm + fire the dc_stop shape in background;
# observe D1/costp stopper and B0/slize softclock 0
Success criterion (all observed on 2026-09-16, first attempt):
- serial console:
dcreplica[stop]: tick INPROG and PARKED on the serializer (fired=21 ran=20)followed byIF THIS IS THE LAST dcreplica LINE callout_stop NEVER RETURNEDand permanent silence from the replica; ps: stopper in stateD1wchancostp(kern_timeout.c:920),(softclock 0)in stateB0wchanslize(lwkt_serialize park);- guest can no longer shut down cleanly (MP lock held) β requires
vm.sh reset with-src.
Fix validation
dc_callout_replica_fixed.c + fix_validation.log: with the fix shape
(lwkt_serialize_try + 1-tick retry), callout_stop() returns in 3.09 s,
callout_pending()==0 β no deadlock, timer genuinely stopped.
Files
dc_callout_replica.c deadlock replica module (dc protocol) dc_callout_replica_fixed.c fix-shape replica module build.sh / run.sh exact commands build.log / run.log / fix_validation.log full untrimmed logs fix.diff git-apply-able fix vs read-only sys/ tree VERDICT.md full narrative verdict.json machine verdict
DF-3850 β VERDICT
Status: mechanism PROVEN on the guest kernel via deterministic replica of
if_dc.c's exact callout/serializer protocol (first attempt, no retries
needed); dc-specific runtime hardware-gated β inconclusive per family
convention. reproduced=0 (no dc NIC on the audit guest), impact on this
guest=none. The class of impact, when it fires on dc hardware, is a
permanent kernel deadlock (DoS): wedged interface, wedged stopper thread,
softclock blackout on one CPU, and β because dc_stat_timer is a
non-MPSAFE callout β the Giant MP lock held by the parked softclock
thread, which wedged even the guest's clean shutdown.
Root cause (path:line)
sys/dev/netif/dc/if_dc.c:1847βcallout_init(&sc->dc_stat_timer): plain (non-MPSAFE) callout β the callback executes under the Giant MP lock (sys/kern/kern_timeout.c:583 get_mplock()).sys/dev/netif/dc/if_dc.c:2725βdc_tick()does a blockinglwkt_serialize_enter(ifp->if_serializer)(sys/kern/lwkt_serialize.c:105βatomic_intr_cond_enter(β¦, lwkt_serialize_sleep)β tsleep, wchanslize); it self-re-arms at:2786-2789and releases at:2791.sys/dev/netif/dc/if_dc.c:3489βdc_stop()callscallout_stop(&sc->dc_stat_timer)while holding the same serializer. Everydc_stop()caller holds it:dc_detach :2213,dc_intr :2949,dc_init :3196(reached from SIOCSIFFLAGS-up viadc_ioctl :3432, watchdogdc_watchdog :3468, media/MTU reinit),dc_shutdown :3546,dc_suspend :3564.sys/kern/kern_timeout.c:1091β:859-932βcallout_stop()is synchronous: when the callout is INPROG (:567), the callerssleep(c, &c->spin, 0, "costp", 0)(:920) until the callback completes.- β A
dc_tickdequeued and parked inlwkt_serialize_enter()on the serializer held by thedc_stopcaller deadlocks against it (AB-BA): both sleep forever; the interface serializer is never released; the affected CPU's softclock thread stops delivering all callouts; and sincedc_stat_timeris non-MPSAFE, the parked softclock thread also holds the Giant MP lock, degrading the whole system (on the replica run the guest could no longer perform a clean shutdown).
Trigger on dc hardware: any dc_stop() racing a tick β ifconfig dcX
down/up, MTU/media change on a running interface, watchdog reinit,
detach/kldunload, suspend/resume, shutdown. The tick fires at 1 Hz
(10 Hz with DC_21143_NWAY), and spends most of its body under the
serializer (MII bit-banging with DELAY loops), so the window per stop is
real; each interface stop is one lottery draw.
This is the dc(4) instance of the DF-3715/3738/3760/3776/3806 family (em, emx, alc, jme, msk). Aggravating delta vs those: dc's timer is a non-MPSAFE callout, so the parked callback additionally holds the Giant MP lock.
Verification performed (guest, DragonFly 6.5-DEVELOPMENT x86_64)
dc_callout_replica.c(this pack) β kernel module replicating the dc protocol byte-for-byte in shape: plaincallout_init, tick callback doing blockinglwkt_serialize_enter+ self re-arm, stop sysctl emulatingdc_stop(serializer held acrosscallout_stop).- Positive test (dc shape): first attempt reproduced β serial
console:
dcreplica[stop]: tick INPROG and PARKED on the serializer (fired=21 ran=20)thenIF THIS IS THE LAST dcreplica LINE β¦and nothing more:callout_stop()never returned. - Wedged-thread capture (second session): stopper
1530 D1 costp sysctl kern.dcreplica_stop=1and(softclock 0) B0 slizeβ both halves of the cycle visible; softclock 1..5 still delivering. - Collateral: the wedged guest could not be shut down cleanly (shutdown hung behind the parked state); qemu had to be SIGKILLed and the disk snapshot-reverted.
- Fix-shape validation β
dc_callout_replica_fixed.creplicates the fixed protocol (lwkt_serialize_try+ 1-tick retry, the exact shape of this pack'sfix.diff): holding the serializer for 3 s produced 318 fires / 308 retries / 0 parks,callout_stop()returned in 3.09 s withcallout_pending()==0β deadlock eliminated, timer truly stopped. (fix_validation.log) - dc hardware absent on this guest (
ifconfig -lβvtnet0 lo0;pciconf -lvshows only QEMU i440fx + virtio devices), so the if_dc.c-specific race cannot be fired end-to-end here β henceinconclusiveper family convention (identical to DF-3715/3738/3760/ 3776/3806).
Not a re-report
DF-3715/3738/3760/3776/3806 covered em(4), emx(4), alc(4), jme(4),
msk(4). if_dc.c was never covered; its instance additionally demonstrates
the non-MPSAFE-callout (Giant-MP-lock-holding) aggravation, which the
prior family members (all callout_init_mp) do not have.
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
inconclusivefix.diff authored against the read-only sys/ tree (git apply --check clean): dc_tick uses lwkt_serialize_try + 1-tick retry. Validated at protocol level on the guest with dc_callout_replica_fixed.ko (callout_stop returns, pending==0, zero parks over 318 fires while the serializer was held 3s). Driver-level validation impossible on this guest (no dc NIC).
findings/poc/DF-3850/fix.diff; findings/poc/DF-3850/fix_validation.log
Confirmed kernel references
- sys/dev/netif/dc/if_dc.c:1847
- sys/dev/netif/dc/if_dc.c:2725
- sys/dev/netif/dc/if_dc.c:2786
- sys/dev/netif/dc/if_dc.c:3489
- sys/dev/netif/dc/if_dc.c:2213
- sys/dev/netif/dc/if_dc.c:3196
- sys/dev/netif/dc/if_dc.c:3546
- sys/kern/kern_timeout.c:567
- sys/kern/kern_timeout.c:583
- sys/kern/kern_timeout.c:920
- sys/kern/kern_timeout.c:1091
- sys/kern/lwkt_serialize.c:105
Detail
Evidence (decisive lines)
findings/poc/DF-3850/VERDICT.md; run.log (session transcript: D1 costp stopper + B0 slize softclock 0 + shutdown-wedge note + fix-shape run); build.log (module built clean under -Werror, BUILD ok); fix_validation.log (fixed-shape replica: callout_stop RETURNED, pending=0, 3.09s); dfbsd-qemu/boot.log serial markers; fix.diff (try-enter + 1-tick retry for dc_tick)
PoC changes
No seed PoC. Authored dc_callout_replica.ko (out-of-tree kmodule replicating the dc callout protocol, including the plain/non-MPSAFE callout_init) because the guest has no dc hardware; the deadlock is in the generic protocol dc uses, so the replica exercises the identical kernel paths. dc_callout_replica_fixed.ko validates the fix shape live.
Verified recommended fix
Make dc_tick() non-blocking: lwkt_serialize_try() with a 1-tick callout_reset retry (validated live at protocol level).
Verdict
Mechanism PROVEN on the guest kernel on the FIRST attempt by deterministic replica of if_dc.c's exact callout/serializer protocol; dc-specific runtime hardware-gated (inconclusive per family convention). dc_tick (sys/dev/netif/dc/if_dc.c:2717-2792) is the callback of a PLAIN (non-MPSAFE) callout (callout_init at :1847 -> callback executes under the Giant MP lock, kern_timeout.c:583) and does a BLOCKING lwkt_serialize_enter(ifp->if_serializer) at :2725 with self re-arm at :2786-2789; dc_stop() tears it down with callout_stop() at :3489 while holding that same serializer (callers: dc_detach :2213, dc_intr :2949, dc_init :3196, dc_watchdog :3468, dc_shutdown :3546, dc_suspend :3564); DragonFly callout_stop synchronously waits for an INPROG callback (kern_timeout.c:1091 -> :859-932, ssleep 'costp' at :920). A tick dequeued and parked on the serializer therefore deadlocks against the stopper (AB-BA). Kernel-module replica on the guest: with the dc-shaped handler parked, callout_stop NEVER returned - stopper observed D1/wchan 'costp' (pid 1530), softclock 0 observed B0/wchan 'slize' holding the MP lock (non-MPSAFE callout, aggravation over the em/emx/alc/jme/msk family members which use callout_init_mp), serial console shows 'fired=21 ran=20' then the terminal marker line. The wedge survived a clean shutdown attempt (guest could not power off; qemu SIGKILL + snapshot revert required). Fix-shape validation on a fresh guest with the exact fix.diff protocol (lwkt_serialize_try + 1-tick retry): callout_stop returned in 3.09s with callout_pending()==0 after 318 fires / 308 retries / 0 parks - deadlock eliminated, timer truly unloaded.
No comments yet.