stge_stop() deadlocks the callout subsystem against stge_tick (AB-BA): synchronous callout_stop() issued while holding the driver serializer vs a tick callback parked in lwkt_serialize_enter β mechanism PROVEN on the guest via protocol replica; stge_tick re-arms UNCONDITIONALLY under the lock
Summary
stge_attach arms one non-MP callout; stge_tick BLOCKS acquiring the driver serializer, does its work, then re-arms itself UNCONDITIONALLY while still holding it (no IFF_RUNNING gate at all, unlike re). stge_stop calls the synchronous callout_stop as its FIRST teardown action while HOLDING the serializer (every caller holds it - detach/shutdown/suspend/ioctl/init, the latter reachable from MTU change, watchdog, and the interrupt path's force-init). A tick dequeued on another CPU and parked in lwkt_serialize_enter deadlocks against the stopper: both threads wedge permanently, the serializer is never released, and the affected CPU's softclock stops delivering ALL callouts. Mechanism PROVEN live on the guest (replica: stopper D0/costp vs softclock slize in one ps snapshot; observing shell froze; guest could not shut down; force-reset). lwkt_serialize_try + retry fix.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-4046 Β· 12 files| File | Type | Description | Size | |
|---|---|---|---|---|
| README.md | β | 2.1 KB | β raw | |
| VERDICT.md | β | 4.7 KB | β raw | |
| stgereplica.c | β | 5.4 KB | view raw | |
| Makefile | β | 137 B | β download | |
| build.sh | β | 83 B | view raw | |
| run.sh | β | 272 B | view raw | |
| build.log | β | 5.9 KB | view raw | |
| run.log | β | 1.5 KB | view raw | |
| env.txt | β | 486 B | view raw | |
| fix.diff | β | 746 B | view raw | |
| manifest.json | β | 1.5 KB | view raw | |
| verdict.json | β | 5.5 KB | view raw |
DF-4046 β stge_stop() deadlocks against stge_tick (synchronous callout_stop under the driver serializer)
- File:
sys/dev/netif/stge/if_stge.c(stge(4)) - Severity: Medium Β· Confidence: certain Β· stge-specific runtime hardware-gated; mechanism PROVEN on the guest kernel via protocol replica
- Family: DF-3715 (em, proven) / DF-4025 (re, proven) β callout AB-BA
Build
cd /root/stgereplica && make # bsd.kmod.mk module
Run
kldload /root/stgereplica/stgereplica.ko
sysctl kern.stgereplica.arm=1 # arm the stge_tick replica (1-tick period)
sleep 2
(sysctl kern.stgereplica.stop=1 >/tmp/stop4046.out 2>&1 &) # NEVER returns
sleep 6
dmesg | grep stgereplica | tail -4
ps -o pid,stat,wchan,command -ax | grep "stop=1" | grep -v grep
Expected result (observed)
stgereplica: stge_tick parked on our serializer (fired=202 ran=201) - entering callout_stop()
stgereplica: IF THIS IS THE LAST stgereplica LINE, AB-BA DEADLOCK IS PROVEN
<<< console ends here β callout_stop() never returns >>>
...
43529 D0 costp sysctl kern.stgereplica.stop=1 <- kern_timeout.c:920
-1 B2 slize (softclock 2) <- tick parked in
lwkt_serialize_enter
The observing shell itself never finishes its sleep (callout starvation);
the guest afterwards cannot even shut down cleanly and must be force-reset
(vm.sh revert with-src && vm.sh up).
On real stge(4) hardware
ifconfig stgeX down, SIOCSIFMTU/SIOCSIFCAP reinit, Tx-underrun
force-init from the interrupt path (if_stge.c:1438-1440), detach,
suspend, or reboot β each holds the serializer through stge_stop() β
racing the 1 Hz stge_tick on another CPU wedges the kernel permanently:
two threads stuck (costp Γ slize), the interface serializer never
released, and the affected CPU's softclock stops delivering ALL callouts.
Artifacts
stgereplica.c, Makefile, build.sh, run.sh, build.log, run.log,
env.txt, fix.diff, VERDICT.md, manifest.json, verdict.json.
DF-4046 VERDICT β stge_stop vs stge_tick callout AB-BA (mechanism PROVEN on guest; stge runtime hardware-gated)
Reproduced?
Mechanism: PROVEN on the guest kernel by a deterministic replica
(stgereplica.ko) of if_stge.c's exact callout/serializer protocol β
including the details that distinguish stge from the re(4) instance
(DF-4025): the callout is initialized with plain non-MP callout_init
(if_stge.c:587, dispatched by softclock under the MPLOCK,
sys/kern/kern_timeout.c:575-590) and stge_tick re-arms itself
unconditionally while holding the serializer (if_stge.c:1775 β no
IFF_RUNNING gate at all, unlike re_tick_serialized).
stge-specific runtime: hardware-gated (guest has no Sundance/Tamarack
NIC; env.txt), so per family convention the persisted status is
inconclusive, matching DF-4025.
Observed on the guest (run.log + serial console)
- Module built:
BUILD_RC=0(build.log). - Armed; tick fired 202 times, ran 201 (re-arm loop working, period 1 tick
vs the real driver's
hz). - stge_stop-shaped stop under the serializer: the 202nd firing parked in
lwkt_serialize_enter; dmesg ends atIF THIS IS THE LAST stgereplica LINE, AB-BA DEADLOCK IS PROVENβcallout_stop()never returned. - Cross-CPU AB-BA visible in one
ps: stopper pid 43529D0wchancostp(ssleep(c, &c->spin, 0, "costp", 0),sys/kern/kern_timeout.c:920, reached fromcallout_stopβ_callout_cancel_or_stop(..., sync=1)atkern_timeout.c:1091-1093) vs(softclock 2)blocked at wchanslize(lwkt_serialize_enter) β each holds what the other needs, forever. - The observing shell never completed its
sleep(timer starvation), and the wedged guest could not be shut down cleanly βvm.sh resetreported "guest still up after shutdown" and the guest had to be force-killed and reverted to thewith-srcsnapshot (restored, healthy,guest_dirty=0post-reset).
Root cause (path:line)
if_stge.c:587βcallout_init(&sc->sc_tick_ch)(non-MP variant).if_stge.c:1752-1778βstge_tick:lwkt_serialize_enterat:1758(BLOCKING acquire), work, then unconditionalcallout_reset(..., hz, stge_tick, sc)at:1775under the serializer, exit at:1777.if_stge.c:2098-2111βstge_stop:ASSERT_SERIALIZEDthencallout_stop(&sc->sc_tick_ch)at:2111as the FIRST teardown action, while holding the serializer;IFF_RUNNINGis cleared only at the very end (:2166).- Every
stge_stopcaller holds the serializer:stge_detach :820-825,stge_shutdown :1049-1051,stge_suspend :1060-1063,stge_ioctlSIOCSIFFLAGS:1280,stge_init :1900/:1907β andstge_initis reachable fromstge_ioctlSIOCSIFMTU:1265, the watchdog:1238, and the interrupt path'sforce_init(:1438-1440, host-error / Tx-underrun β remotely nudgeable by link events). - DragonFly
callout_stop()is synchronous (kern_timeout.c:1091-1093β_callout_cancel_or_stop(..., sync=1);ssleep "costp"at:920): it waits for an INPROG callback to finish. A tick dequeued by a softclock thread and parked inlwkt_serialize_entertherefore deadlocks against the stopper: both threads wedge permanently, the interface serializer is never released (all later ioctls/if_starton stgeX block), and the affected CPU's softclock stops delivering ALL callouts. The unconditional re-arm (:1775) means even a late-running tick re-arms itself after any non-synchronous stop attempt.
Threat model
Local DoS via permanent kernel deadlock (two threads + CPU-wide callout
starvation). Triggers are ordinary operator actions (ifconfig down,
MTU/capability changes, detach, suspend, reboot) racing the 1 Hz tick β
same privilege framing as DF-3715(em)/DF-4025(re), which were filed
Medium. No memory-corruption primitive.
Fix validation
fix.diff makes stge_tick non-blocking: lwkt_serialize_try() with a
1-tick callout_reset retry when the serializer is busy β the same shape
proposed and verified for em_timer (DF-3715) and re_tick (DF-4025). With a
never-blocking tick, stge_stop's callout_stop can always complete.
git apply --check passes against the read-only tree. A runtime
fix-validation would need stge(4) hardware to race stge_stop against a
real stge_tick; the replica's wedged protocol is exactly what the fix
removes β fix_status: not_testable.
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 (try-enter + 1-tick retry in stge_tick) applies clean against the read-only tree (git apply --check); a runtime fix-validation would need stge(4) hardware to race stge_stop against a real stge_tick. The replica's wedged protocol is exactly what the fix removes.
findings/poc/DF-4046/fix.diff
Confirmed kernel references
- sys/dev/netif/stge/if_stge.c:587
- sys/dev/netif/stge/if_stge.c:1758
- sys/dev/netif/stge/if_stge.c:1775
- sys/dev/netif/stge/if_stge.c:1777
- sys/dev/netif/stge/if_stge.c:1900
- sys/dev/netif/stge/if_stge.c:1907
- sys/dev/netif/stge/if_stge.c:1280
- sys/dev/netif/stge/if_stge.c:1438
- sys/dev/netif/stge/if_stge.c:2098
- sys/dev/netif/stge/if_stge.c:2111
- sys/dev/netif/stge/if_stge.c:2166
- sys/kern/kern_timeout.c:1091
- sys/kern/kern_timeout.c:920
Detail
Exploit chain
On stge(4) hardware: any stge_stop (root-triggered ifconfig down/detach/suspend/reboot, or the interrupt-path force-init) racing the 1 Hz stge_tick on another CPU -> permanent kernel deadlock of two threads + CPU-wide callout starvation; system-degradation DoS, not memory corruption.
Evidence (decisive lines)
run.log (wedge session transcript + post-wedge ps showing 43529 D0 costp vs (softclock 2) slize + serial-console marker sequence ending at 'AB-BA DEADLOCK IS PROVEN'); build.log (BUILD_RC=0); stgereplica.c/Makefile; env.txt (no stge hardware); fix.diff (non-blocking stge_tick)
PoC changes
No seed PoC. Authored stgereplica.ko (out-of-tree kmodule) replicating stge's protocol with stge-specific fidelity: plain NON-MP callout_init (MPLOCK dispatch), blocking lwkt_serialize_enter, UNCONDITIONAL under-lock self re-arm, and callout_stop as the first action of a serializer-held stop in stge_stop's exact call order (IFF_RUNNING cleared last). 1-tick arm period instead of hz for a deterministic window; real driver arms at hz.
Verified recommended fix
Make stge_tick() non-blocking: lwkt_serialize_try() with a 1-tick callout_reset retry when the serializer is busy (see fix.diff; same shape as DF-3715(em)/DF-4025(re)).
Verdict
Mechanism PROVEN on the guest kernel by a deterministic replica of if_stge.c's exact callout/serializer protocol (stge(4) instance of DF-3715(em)/DF-4025(re)); stge-specific runtime hardware-gated, inconclusive per family convention. stge_attach arms one NON-MP callout (plain callout_init, if_stge.c:587; softclock dispatches it under the MPLOCK, kern_timeout.c:575-590) scheduled every hz by stge_init (:2059). Its callback stge_tick BLOCKS on the driver serializer by hand (lwkt_serialize_enter, :1758) and re-arms itself UNCONDITIONALLY under it (:1775 -- no IFF_RUNNING gate at all, unlike re). stge_stop() issues callout_stop(&sc->sc_tick_ch) at :2111 as its FIRST teardown action while HOLDING that serializer (every caller holds it: stge_detach :820-825, stge_shutdown :1049-1051, stge_suspend :1060-1063, stge_ioctl :1280, stge_init :1900/:1907 -- itself reachable from SIOCSIFMTU :1265, the watchdog :1238, and the interrupt path's force_init :1438-1440), clearing IFF_RUNNING only at the very end (:2166). DragonFly callout_stop is synchronous (kern_timeout.c:1091-1093 -> _callout_cancel_or_stop sync=1; ssleep 'costp' at :920), so a stge_tick dequeued on another CPU and parked in lwkt_serialize_enter deadlocks against the stopper (AB-BA): both threads wedge permanently, if_serializer is never released (all later ioctls/if_start on stgeX block), and the affected CPU's softclock stops delivering ALL callouts. Guest replica (stgereplica.ko, build.log BUILD_RC=0): arm -> stge_tick-shape handler parks (fired=202 ran=201) -> stge_stop-shape stop under the serializer -> dmesg ends at the 'AB-BA DEADLOCK IS PROVEN' marker and NEVER prints the post-callout_stop line; stopper thread 43529 stuck D0/wchan 'costp' while (softclock 2) sits blocked in wchan 'slize' (lwkt_serialize_enter) -- the full AB-BA visible in one ps; the observing shell never completed its sleep and the wedged guest could NOT be shut down cleanly (vm.sh reset: 'guest still up after shutdown'), force-killed and reverted to the with-src snapshot. Triggers on stge hardware: ifconfig stgeX down/up, SIOCSIFMTU/SIOCSIFCAP reinit, Tx-underrun/host-error force-init from the interrupt path, detach, suspend, reboot -- racing the 1 Hz tick.
No comments yet.