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)
PoC verification
Evidence pack
findings/poc/DF-3806 Β· 11 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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 |
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 inlwkt_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 withcallout_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 itssleep()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 β frommsk_ioctlSIOCSIFFLAGS up/down (:880-881, :895), andmsk_watchdog()(:2663, :2686) β TX-timeout reinitmsk_detach():1762-1764 β detach / kldunload if_mskmskc_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 insidecallout_stop's wait (kern_timeout.c:923)softclock 1 B1 slizeβ the msk_tick-shaped callback permanently parked inlwkt_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_testablefix.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')
Confirmed kernel references
- sys/dev/netif/msk/if_msk.c:1375
- sys/dev/netif/msk/if_msk.c:2930
- sys/dev/netif/msk/if_msk.c:2936
- sys/dev/netif/msk/if_msk.c:2943
- sys/dev/netif/msk/if_msk.c:3687
- sys/dev/netif/msk/if_msk.c:3689
- sys/dev/netif/msk/if_msk.c:3352
- sys/dev/netif/msk/if_msk.c:880
- sys/dev/netif/msk/if_msk.c:1762
- sys/dev/netif/msk/if_msk.c:2663
- sys/dev/netif/msk/if_msk.c:2699
- sys/dev/netif/msk/if_msk.c:2719
- sys/dev/netif/msk/if_msk.c:2756
- sys/kern/kern_timeout.c:923
- sys/kern/kern_timeout.c:1091
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.
No comments yet.