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

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-3850 Β· 13 files
FileTypeDescriptionSize
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 by IF THIS IS THE LAST dcreplica LINE callout_stop NEVER RETURNED and permanent silence from the replica;
  • ps: stopper in state D1 wchan costp (kern_timeout.c:920), (softclock 0) in state B0 wchan slize (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
VERDICT.md
↓ download raw

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)

  1. 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()).
  2. sys/dev/netif/dc/if_dc.c:2725 β€” dc_tick() does a blocking lwkt_serialize_enter(ifp->if_serializer) (sys/kern/lwkt_serialize.c:105 β†’ atomic_intr_cond_enter(…, lwkt_serialize_sleep) β†’ tsleep, wchan slize); it self-re-arms at :2786-2789 and releases at :2791.
  3. sys/dev/netif/dc/if_dc.c:3489 β€” dc_stop() calls callout_stop(&sc->dc_stat_timer) while holding the same serializer. Every dc_stop() caller holds it: dc_detach :2213, dc_intr :2949, dc_init :3196 (reached from SIOCSIFFLAGS-up via dc_ioctl :3432, watchdog dc_watchdog :3468, media/MTU reinit), dc_shutdown :3546, dc_suspend :3564.
  4. sys/kern/kern_timeout.c:1091 β†’ :859-932 β€” callout_stop() is synchronous: when the callout is INPROG (:567), the caller ssleep(c, &c->spin, 0, "costp", 0) (:920) until the callback completes.
  5. β‡’ A dc_tick dequeued and parked in lwkt_serialize_enter() on the serializer held by the dc_stop caller 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 since dc_stat_timer is 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: plain callout_init, tick callback doing blocking lwkt_serialize_enter + self re-arm, stop sysctl emulating dc_stop (serializer held across callout_stop).
  • Positive test (dc shape): first attempt reproduced β€” serial console: dcreplica[stop]: tick INPROG and PARKED on the serializer (fired=21 ran=20) then IF 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=1 and (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.c replicates the fixed protocol (lwkt_serialize_try + 1-tick retry, the exact shape of this pack's fix.diff): holding the serializer for 3 s produced 318 fires / 308 retries / 0 parks, callout_stop() returned in 3.09 s with callout_pending()==0 β€” deadlock eliminated, timer truly stopped. (fix_validation.log)
  • dc hardware absent on this guest (ifconfig -l β†’ vtnet0 lo0; pciconf -lv shows only QEMU i440fx + virtio devices), so the if_dc.c-specific race cannot be fired end-to-end here β€” hence inconclusive per 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

inconclusive
baseline no→ patch + rebuild →patched clean

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

Confirmed kernel references

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.