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

msk_stop()/msk_tick() AB-BA callout deadlock: synchronous callout_stop() under ifnet serializer vs blocking lwkt_serialize_enter in the self-re-arming non-MPSAFE tick (DF-3715/3738/3760/3776 family, if_msk.c instance)

Summary

msk_attach arms a NON-MPSAFE tick callout. msk_tick BLOCKS in lwkt_serialize_enter(ifp->if_serializer), does MII work, then self re-arms at hz. msk_stop runs under that same serializer and tears the tick down with the SYNCHRONOUS callout_stop. A tick callback dequeued and parked on the serializer held by the msk_stop caller deadlocks both threads permanently; because the msk callback is non-MPSAFE, the wedged softclock thread also holds the MP lock and stops delivering every callout on its CPU (observed live: a bystander 'sleep 6' starved in nanslp). Reachable from every msk_stop caller holding the serializer (init via ioctl up/down and watchdog reinit, detach, shutdown, suspend, resume). MECHANISM PROVEN on the guest via protocol replica: ps shows 'D0 costp' (kern_timeout.c:923) stopper and 'softclock 1 B1 slize' parked callback; fix-shape control returns pending=0. Privileged local user or watchdog reinit races the 1 Hz tick -> permanent unrecoverable wedge + CPU-wide callout blackout + MP-lock hold.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-3806 Β· 11 files
FileTypeDescriptionSize
msk_callout_replica.c β€” 7.9 KB view raw
Makefile β€” 78 B ↓ download
build.sh β€” 80 B view raw
run.sh β€” 339 B view raw
build.log β€” 1.1 KB view raw
run.log β€” 235 B view raw
wedge.log β€” 586 B view raw
env.txt β€” 866 B view raw
VERDICT.md β€” 4.8 KB ↓ raw
fix.diff β€” 756 B view raw
verdict.json β€” 7.0 KB view raw
VERDICT.md
↓ download raw

DF-3806 β€” msk(4) msk_stop()/msk_tick() AB-BA callout deadlock (DF-3715 family)

Verdict

Mechanism PROVEN on the guest kernel by a deterministic replica of if_msk.c's exact tick/stop callout protocol; msk-specific runtime hardware-gated (inconclusive per family convention β€” the guest has no Marvell Yukon hardware). Severity Medium, confidence certain. This is the DF-3715 (if_em.c) / DF-3738 (if_emx.c) / DF-3760 (if_alc.c) / DF-3776 (if_jme.c) family β€” a fifth, independent instance, first filed for this file (pass 1 marked the file clean; there were no pass-1 findings to exclude).

Root cause (path:line)

  • msk_attach() arms a non-MPSAFE callout: callout_init(&sc_if->msk_tick_ch) (sys/dev/netif/msk/if_msk.c:1375) β€” unlike the alc(4)/em(4) twins this callback also runs under the MP lock.
  • msk_tick() (if_msk.c:2930-2946) blocks in lwkt_serialize_enter(ifp->if_serializer) (:2936), does the MII work, then self re-arms at hz (:2943) and exits the serializer (:2945).
  • msk_stop() (if_msk.c:3675-3823) runs under that same serializer (ASSERT_SERIALIZED, :3687) and tears the tick down with callout_stop(&sc_if->msk_tick_ch) (:3689).
  • DragonFly's callout_stop() is the synchronous variant (sys/kern/kern_timeout.c:1091 β†’ _callout_cancel_or_stop(..., CALLOUT_STOP, sync=1)); when the callback is INPROG it ssleep()s on "costp" (kern_timeout.c:923) until the callback completes.

If msk_tick is INPROG and parked inside lwkt_serialize_enter on the serializer held by the msk_stop() caller, neither thread can progress: permanent AB-BA deadlock. Extras vs. the family: the parked callback is non-MPSAFE, so the wedged softclock thread also holds the MP lock, and its CPU stops delivering all callouts (observed live: an unrelated sleep 6 process starved because its nanosleep timeout lived on the wedged softclock's wheel).

Callers that reach msk_stop() under the serializer

  • msk_init() :3352 β€” from msk_ioctl SIOCSIFFLAGS up/down (:880-881, :895), and msk_watchdog() (:2663, :2686) β€” TX-timeout reinit
  • msk_detach() :1762-1764 β€” detach / kldunload if_msk
  • mskc_shutdown() :2699-2701, mskc_suspend() :2719-2722, mskc_resume() β†’ msk_init :2756

Trigger on real hardware: any ifconfig mskN down, re-up, detach, suspend or TX-timeout reinit that races the 1 Hz tick β€” a privileged local user flaps the interface and can retry indefinitely; watchdog-triggered reinits can occur from unprivileged TX activity on a flapping link. Race window ~0.1-1% per stop (family-measured); consequences are permanent.

Guest proof (deterministic replica, no msk hardware needed)

msk_callout_replica.ko replicates the protocol exactly (non-MPSAFE callout_init, blocking enter, hz self re-arm, sync callout_stop under the serializer) and exposes two shapes:

  • fix-shape control (lwkt_serialize_try + 1-tick retry = this pack's fix.diff): mskrep[try]: callout_stop returned, pending=0 β€” validated live, twice.
  • shipped-shape (blocking enter): park caught after 99 ticks (fired=4 ran=3), then:
  • 1045 D0 costp sysctl kern.mskrep.stop_blk=1 β€” stopper permanently inside callout_stop's wait (kern_timeout.c:923)
  • softclock 1 B1 slize β€” the msk_tick-shaped callback permanently parked in lwkt_serialize_enter (if_msk.c:2936 shape)
  • bystander sleep 6 (I1 nanslp) starved β€” softclock blackout cascade

An earlier orchestration variant (tsleep-based park detection) wedged one step earlier β€” the stopper's own sleep timeout was delivered by the very softclock thread the parked callback took out β€” an additional cascade unique to the non-MPSAFE msk shape (see run.log history / VERDICT notes).

Files: run.log (session transcript), wedge.log (dmesg + ps capture of the D0/costp + B1/slize pair), build.log, msk_callout_replica.c, fix.diff, env.txt.

Fix

fix.diff: make msk_tick() non-blocking β€” lwkt_serialize_try() with a 1-tick callout_reset retry when contended (DF-3715 fix pattern). Mechanism validated live on this kernel via the replica's fix-shape control. A driver-level build/boot validation requires Yukon hardware (not available on the audit guest), hence fix_status=not_testable.

Kernel references

sys/dev/netif/msk/if_msk.c:1375, :2930, :2936, :2940-2943, :2945, :3352, :880-881, :1762-1764, :2663, :2686, :2699-2701, :2719-2722, :2756, :3687, :3689; sys/kern/kern_timeout.c:923, :1091.

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 (git apply --check clean) switches msk_tick to lwkt_serialize_try + 1-tick retry, mirroring the DF-3715 fix for if_em.c. The mechanism of the fix is validated live on the guest: the replica's fix-shape control (identical try-enter + retry protocol) shows callout_stop under the serializer returns with pending=0, no deadlock (run.log, both sessions). Full driver-level validation is not possible on a guest without Marvell Yukon hardware.

fix.diff; run.log/wedge.log stop_try section ('mskrep[try]: callout_stop returned, pending=0' / 'RESULT pending=0')
↓ fix.diffper-fix-DF-3806

Confirmed kernel references

Detail

Exploit chain

Privileged local user repeatedly flaps the interface (ifconfig mskN down / kldunload if_msk / suspend-resume) until a 1 Hz non-MPSAFE tick callback is dequeued-and-parked during an msk_stop; both threads wedge permanently (unkillable D-state), both ports' serializer is never released, the wedged softclock CPU stops delivering all callouts and holds the MP lock (system-wide timer degradation). No privilege-escalation primitive; impact is permanent system degradation/DoS.

Evidence (decisive lines)

wedge.log (decisive: 'mskrep[blk]: msk_tick handler BLOCKED ... waited=99' followed by NO return line, ps capture '1045 D0 costp' + 'softclock 1 B1 slize' + starved 'sleep 6 nanslp'); run.log (session transcript: fix-shape control 'mskrep[try]: callout_stop returned, pending=0' twice, plus attempt-1 tsleep-orchestration cascade); build.log; env.txt (no Marvell PCI, src md5 == tree); VERDICT.md (path:line trace); msk_callout_replica.c; fix.diff (try-enter + 1-tick retry)

PoC changes

No seed PoC. Authored msk_callout_replica.ko (out-of-tree kmodule replicating if_msk.c's callout protocol with the msk-specific shapes: NON-MPSAFE callout_init per :1375, hz re-arm, single tick callout) because the guest has no Marvell hardware; the deadlock is in the generic protocol msk uses, so the replica exercises the identical kernel paths. Adapted from the DF-3760 replica with the msk shapes and the kern.mskrep sysctl namespace. Three orchestration iterations: (1) tsleep-based park detection -> wedge occurred one step early via softclock-timeout starvation (itself evidence of the non-MPSAFE/MP-lock cascade); (2) fixed-iteration spin (300M) shorter than the 1 s tick period -> missed the park, callout_stop returned on a merely-pending callout; (3) FINAL: ticks-based spin (hardclock-immune) + lwkt_user_yield -> park caught (fired=4 ran=3 waited=99), callout_stop wedged with the canonical costp/slize pair.

Verified recommended fix

Make msk_tick() non-blocking: lwkt_serialize_try() with a 1-tick callout_reset retry (see fix.diff; DF-3715 fix pattern).

Verdict

Mechanism PROVEN on the guest kernel by deterministic replica of if_msk.c's exact callout/serializer protocol; msk-specific runtime hardware-gated (inconclusive per family convention, DF-3715/3738/3760/3776 precedent). msk_attach arms a NON-MPSAFE callout (callout_init, if_msk.c:1375); msk_tick (if_msk.c:2930-2946) BLOCKS in lwkt_serialize_enter(ifp->if_serializer) (:2936) and self re-arms at hz (:2943); msk_stop tears it down with callout_stop (:3689) while holding that same serializer (ASSERT_SERIALIZED :3687). Callers holding the serializer: msk_init (:3352; from msk_ioctl SIOCSIFFLAGS up/down :880-881/:895 and watchdog reinit :2663/:2686), msk_detach (:1762-1764), mskc_shutdown (:2699-2701), mskc_suspend (:2719-2722), mskc_resume (:2756). DragonFly callout_stop synchronously waits for an in-progress callback (sys/kern/kern_timeout.c:1091 -> _callout_cancel_or_stop sync=1; ssleep 'costp' at :923), so a tick callback dequeued and parked on the serializer deadlocks against the stopper (AB-BA): both threads wedge permanently, the interface serializer is never released, and because the msk callback is NON-MPSAFE the wedged softclock thread also holds the MP lock and stops delivering every callout on its CPU (observed live: an unrelated 'sleep 6' starved in nanslp). Kernel-module replica on the guest (msk_callout_replica.ko, exact if_msk.c protocol, non-mp callout_init): stop under a held serializer with the msk_tick-shaped handler parked -> callout_stop NEVER returned (ps: '1045 D0 costp sysctl kern.mskrep.stop_blk=1' == kern_timeout.c:923, and 'softclock 1 B1 slize' == parked in lwkt_serialize_enter, wedge.log); fix-shape control (try-enter + 1-tick retry, i.e. this pack's fix.diff) -> callout_stop returned, pending=0 (fix mechanism validated live). An earlier tsleep-based orchestration additionally demonstrated the softclock-blackout cascade one step before callout_stop (sysctl stuck D1 'blkw', softclock B1 'slize'). The msk instance itself needs Marvell Yukon hardware (guest: NO_MARVELL_PCI, ifconfig=vtnet0 lo0, if_msk.ko exists only as a module file; guest src md5 == audited tree). Trigger on real hardware: ifconfig mskN down/re-up, detach/kldunload, suspend/resume, watchdog reinit - any msk_stop while msk_tick is INPROG-parked; ~0.1-1% race per stop but repeatable by root.