fxp_stop() callout_stop under the ifnet serializer can deadlock the callout subsystem against fxp_tick (AB-BA): non-MPSAFE fxp_stat_timer callback blocks in lwkt_serialize_enter and holds the MP lock while parked β DF-3715 (em) / DF-3738 (emx) family twin, mechanism PROVEN on the guest kernel via fxp-faithful replica
Summary
fxp_stat_timer is a plain non-MPSAFE callout whose callback BLOCKS in lwkt_serialize_enter and self-re-arms INSIDE the serializer. Every teardown path issues the synchronous callout_stop while holding the serializer: a dequeued callback parked on the serializer deadlocks against the stopper - both threads wedge permanently, the CPU's softclock stops delivering callouts, and because the callout is non-MPSAFE the parked callback additionally holds the MP lock, stalling BGL-dependent paths system-wide. Live-tested on a REAL fxp0 attached to QEMU's i82557b (temp launcher): four adversarial campaigns could not reach the park window (structural reason proven: same-CPU callout wheel - tick only runs when the ifnet queue drains). Mechanism then PROVEN deterministically with an fxp-faithful replica (stopper D0/costp + softclock B5/slize holding MP lock; callout_stop never returns); fixed-shape replica completes cleanly. lwkt_serialize_try + retry fix.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-4064 Β· 22 files| File | Type | Description | Size | |
|---|---|---|---|---|
| README.md | β | 2.8 KB | β raw | |
| VERDICT.md | β | 6.3 KB | β raw | |
| run.log | β | 1.5 KB | view raw | |
| run.patched.log | β | 625 B | view raw | |
| build.log | β | 5.6 KB | view raw | |
| build.fixed.log | β | 5.9 KB | view raw | |
| fxp_callout_replica.c | β | 4.7 KB | view raw | |
| fxp_callout_replica_fixed.c | β | 2.6 KB | view raw | |
| Makefile | β | 77 B | β download | |
| Makefile.fixed | β | 89 B | β download | |
| build.sh | β | 173 B | view raw | |
| run.sh | β | 226 B | view raw | |
| boot-fxp-full.log | β | 54.8 KB | view raw | |
| guest_pre_reset.txt | β | 6.6 KB | view raw | |
| fxp_race.sh | β | 952 B | view raw | |
| fxp_race2.sh | β | 1.4 KB | view raw | |
| fxp_race3.sh | β | 1.3 KB | view raw | |
| fxp_race4.sh | β | 1.1 KB | view raw | |
| vm_fxp.sh | β | 1.2 KB | view raw | |
| fix.diff | β | 1.1 KB | view raw | |
| manifest.json | β | 2.3 KB | view raw | |
| verdict.json | β | 4.8 KB | view raw |
DF-4064 β fxp(4) fxp_stat_timer AB-BA vs fxp_stop (callout subsystem deadlock)
What
fxp_tick (if_fxp.c:1441) is a non-MPSAFE callout (plain callout_init,
if_fxp.c:399) whose callback blocks in lwkt_serialize_enter(if_serializer)
(if_fxp.c:1449) and re-arms itself inside the serializer (if_fxp.c:1552).
fxp_stop() tears it down with callout_stop(&sc->fxp_stat_timer)
(if_fxp.c:1577) while holding the serializer (ASSERT_SERIALIZED :1568;
reachable from fxp_detach :738, fxp_shutdown :787, fxp_suspend :805,
fxp_init :1651, fxp_ioctl SIOCSIFFLAGS :2078, fxp_watchdog via fxp_init).
DragonFly's callout_stop() is synchronous (kern_timeout.c:1091 β
_callout_cancel_or_stop sync=1; ssleep "costp" at :920) β it waits for an
in-progress callback. A callback dequeued and parked on the serializer
therefore deadlocks against the stopper: stopper sleeps in callout_stop
holding the serializer; softclock sleeps in lwkt_serialize_enter wanting it.
Because the callout is non-MPSAFE, the parked softclock thread also holds the
MP lock (kern_timeout.c:575-580), widening the wedge system-wide.
Build / run / expected
- Driver-level attempt (real hardware): boot the guest with the temp
launcher
dfbsd-qemu/vm_fxp.sh(adds-device i82557b,netdev=n1; vm.sh untouched) βfxp0attaches for real (device fxpis in X86_64_GENERIC). Runfxp_race*.sh(up/down hammers + state-dump storms). Expected: a permanent wedge if the 1 Hz tick fires while a non-stop-holder holds the serializer. Observed: no wedge β structurally unreachable via ifconfig on this kernel (see VERDICT.md: the timer is armed from the fxp netisr CPU, so it only ever runs when that CPU's ifnet queue is drained, i.e. the serializer is free). - Deterministic protocol replica (the proof):
cd /root/fxpkmod && make # fxp_callout_replica.ko (-Werror clean) kldload /root/fxpkmod/fxp_callout_replica.ko sysctl kern.fxr.arm=1 sysctl kern.fxr.stop_blk=1 # fxp_stop shapeExpected (serial console):fxr: handler BLOCKED on our serializer β¦thenfxr: IF THIS IS THE LAST fxr LINE, AB-BA DEADLOCK IS PROVEN β¦and nothing after it βcallout_stop()never returns.pson a second ssh: stopperD0 costp,softclock 5B5 slize. Observed: exactly that (run.log, guest_pre_reset.txt). - Fixed shape (fix.diff protocol):
cd /root/fxpkmod_fixed && make # fxp_callout_replica_fixed.ko kldload /root/fxpkmod_fixed/fxp_callout_replica_fixed.ko sysctl kern.fxrf.arm=1 sysctl kern.fxrf.stop_blk=1 # same hold + callout_stop kldunload fxp_callout_replica_fixedExpected:fxrf: callout_stop RETURNED cleanly, pending=0+ clean unload. Observed: exactly that (run.patched.log).
After any wedged run: vm.sh reset with-src.
DF-4064 β VERDICT
Bottom line
Mechanism PROVEN on the guest kernel by deterministic replica of if_fxp.c's exact callout/serializer protocol; fxp-specific runtime hardware/timing-gated (inconclusive per family convention β same standard as em(4) DF-3715 and emx(4) DF-3738).
The code shape (source-certain)
fxp_stat_timeris a non-MPSAFE callout: plaincallout_initat sys/dev/netif/fxp/if_fxp.c:399 (vs em/emx which usecallout_init_mpβ the fxp variant additionally runs the callback under the MP lock, sys/kern/kern_timeout.c:575-580).fxp_tick(if_fxp.c:1441-1555) blocks inlwkt_serialize_enter(sc->arpcom.ac_if.if_serializer)at :1449, runs its whole body under it, and self re-arms inside it at :1552 (callout_reset(&sc->fxp_stat_timer, hz, fxp_tick, sc)).fxp_stopcallscallout_stop(&sc->fxp_stat_timer)at :1577 while holding the serializer (ASSERT_SERIALIZED :1568). Callers: fxp_detach :738, fxp_shutdown :787, fxp_suspend :805, fxp_init :1651 (thus everyifconfig fxp0 up, every SIOCSIFFLAGS re-init, and the watchdog path fxp_watchdog :1632), fxp_ioctl SIOCSIFFLAGS down :2078.- DragonFly
callout_stopis synchronous: kern_timeout.c:1091 β_callout_cancel_or_stop(cc, CALLOUT_STOP, 1);ssleep(c, &c->spin, 0, "costp", 0)at kern_timeout.c:920 while the callback is INPROG. lwkt_serialize_enterblocks:atomic_intr_cond_enter(..., lwkt_serialize_sleep, ...);tsleep(s, PINTERLOCKED, "slize", 0)at lwkt_serialize.c:227-235.
A callback that has been dequeued (INPROG) and parked on the serializer deadlocks against any stopper (AB-BA); both threads wedge permanently, the affected CPU's softclock stops delivering callouts, and β fxp-only twist β the parked callback holds the MP lock, stalling every BGL-dependent path system-wide.
Proven on the guest (replica, deterministic)
fxp_callout_replica.ko replicates the protocol exactly (non-MP
callout_init, blocking lwkt_serialize_enter at the fxp_tick:1449
position, hz self re-arm at the :1552 position, callout_stop under the
serializer at the :1577 position). Run transcript (run.log / console):
fxr: armed (fxp_stat_timer shape, hz period) fxr: handler BLOCKED on our serializer (fired=28 ran=27) - entering callout_stop() [fxp_stop:1577 shape] fxr: IF THIS IS THE LAST fxr LINE, AB-BA DEADLOCK IS PROVEN ... <nothing β callout_stop never returned>
ps on the wedged guest (guest_pre_reset.txt):
-1 B5 slize softclock 5 <- parked in fxp_tick's lwkt_serialize_enter 231052 D0 costp sysctl <- fxp_stop shape, stuck in callout_stop
100% deterministic (armed handler parks the moment the stopper-side holder sleeps holding the serializer; callout_stop then waits forever).
Driver-level attempt on real emulated fxp0 (honest negative)
fxp0 was attached for real on the guest by booting QEMU with a temp
launcher adding -device i82557b,netdev=n1 (vm_fxp.sh; vm.sh untouched;
device fxp is compiled into X86_64_GENERIC:233):
fxp0: <Intel 82557 Pro/100 Ethernet> port 0xc080-0xc0bf irq 11 at device 4.0 on pci0 miibus0: <MII bus> on fxp0
Four adversarial hammer campaigns were run (~10 minutes total: 4-way up/down loops; phased arm/hold/stop cycles; 6-way saturating state-dump storms; quiet-period diagnostics). The tick demonstrably fires ("fxp0: device timeout" via the rx_idle β fxp_mc_setup β watchdog chain appeared on the console exactly as the code predicts), but the wedge was never reached, and the reason is structural on this kernel:
- the timer is always armed from the fxp netisr CPU (fxp_init runs in the ioctl netmsg on the ifnet's serializer CPU), so the callback fires on that same CPU's softclock;
- the tick's only blocking point is its
lwkt_serialize_enterentry (:1449); softclock on that CPU only gets to run when the netisr queue is drained β at which moment the serializer is free, so the enter succeeds immediately and the callback never parks; - no other path sleeps while holding the ifnet serializer before issuing a callout_stop (fxp_stop's callout_stop is the first thing it does, before any long-held work β unlike em(4)/emx(4) whose stop does long work around the stop), so the em-family interleaving cannot be reconstructed with userland ifconfig.
Conclusion at the driver level: the hazard is carried in the code (identical shape to the proven-family bugs; any future change that arms the callout from another CPU, sleeps holding the serializer, or splits fxp_stop re-opens the deadlock), but on the current kernel + driver the trigger is hardware/timing-gated beyond what ifconfig storming reaches β hence inconclusive per family convention.
Fix validation (protocol level, same guest)
fxp_callout_replica_fixed.ko implements the exact fix.diff protocol
(lwkt_serialize_try() + 1-tick callout_reset retry β the emx_txgc_timer
pattern validated for DF-3738). Same fxp_stop-shaped stopper holding the
serializer for 3 s:
fxrf: armed (FIXED fxp_tick shape) fxrf: holding serializer, callout_stop() [fxp_stop shape]... fxrf: callout_stop RETURNED cleanly, pending=0 kldunload fxp_callout_replica_fixed -> FIXED_SHAPE_OK
Baseline shape = permanent wedge; fixed shape = clean stop + clean unload. Full driver-kernel rebuild validation not performed (module-level protocol proof, family standard).
Artifacts
- run.log β baseline replica transcript + ps evidence + driver-level context
- run.patched.log β fixed-shape transcript
- build.log / build.fixed.log β full untrimmed -Werror-clean guest builds
- boot-fxp-full.log β complete serial console of the driver-level session (fxp0 attach, race campaigns, DMA-timeout flood, replica wedge lines)
- guest_pre_reset.txt β full ps + logs pulled off the wedged guest
- fxp_race*.sh β the four driver-level hammer campaigns
- vm_fxp.sh β temp QEMU launcher (vm.sh untouched)
- fix.diff β try-enter + 1-tick retry for fxp_tick
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
fixedProtocol-level validation on the guest: baseline (current fxp_tick shape) = permanent AB-BA wedge (stopper D0 'costp', softclock 5 B5 'slize'); patched shape (exact fix.diff protocol: try + 1-tick retry + re-arm under serializer) = the same fxp_stop-shaped callout_stop completes, pending=0, module unloads cleanly. Full driver-kernel rebuild validation not possible in-module without rebuilding the in-kernel fxp driver.
findings/poc/DF-4064/fix.diff; run.patched.log 'fxrf: callout_stop RETURNED cleanly, pending=0' + FIXED_SHAPE_OK
Confirmed kernel references
Detail
Evidence (decisive lines)
findings/poc/DF-4064/VERDICT.md; run.log (replica transcript: 'fxr: handler BLOCKED ... entering callout_stop' then silence + ps '231052 D0 costp sysctl' / 'softclock 5 B5 slize'); run.patched.log ('fxrf: callout_stop RETURNED cleanly, pending=0' + FIXED_SHAPE_OK); boot-fxp-full.log (real fxp0 attach on i82557b + race campaigns); build.log / build.fixed.log (-Werror clean)
PoC changes
No seed PoC. Authored fxp_callout_replica.ko (out-of-tree kmodule replicating the fxp callout protocol; adapted from DF-3738's emx replica with fxp line citations and the fxp-specific NON-MPSAFE callout_init fidelity) plus fxp_callout_replica_fixed.c (fix.diff protocol). Driver-level campaigns fxp_race*.sh authored for the real i82557b fxp0; the resulting structural negative (same-CPU callout wheel closes the ifconfig window) is documented in VERDICT.md.
Verified recommended fix
Make fxp_tick() non-blocking: lwkt_serialize_try() with a 1-tick callout_reset retry (emx_txgc_timer pattern).
Verdict
Mechanism PROVEN on the guest kernel by deterministic replica of if_fxp.c's exact callout/serializer protocol; fxp-specific runtime hardware/timing-gated (inconclusive per family convention, same standard as em(4) DF-3715 and emx(4) DF-3738). fxp_stat_timer is a NON-MPSAFE callout (plain callout_init, if_fxp.c:399 - unlike em/emx, its callback also runs under the MP lock, kern_timeout.c:575-580); fxp_tick (if_fxp.c:1441-1555) BLOCKS in lwkt_serialize_enter at :1449 and self re-arms INSIDE the serializer at :1552; fxp_stop tears it down with callout_stop at :1577 while holding the serializer (:1568; callers: fxp_detach :738, fxp_shutdown :787, fxp_suspend :805, fxp_init :1651 -> every ifconfig up + watchdog, SIOCSIFFLAGS down :2078). DragonFly callout_stop synchronously waits for an in-progress callback (kern_timeout.c:1091 -> sync=1, ssleep 'costp' :920) while lwkt_serialize_enter blocks (tsleep 'slize', lwkt_serialize.c:234). Replica on the guest: the fxp_stop-shaped stopper wedges permanently (ps: '231052 D0 costp sysctl' + 'softclock 5 B5 slize'), console ends at the pre-callout_stop marker. Driver-level attempt on REAL emulated fxp0 (QEMU i82557b via temp launcher vm_fxp.sh, vm.sh untouched; fxp0+miibus0 attached, tick proven firing via the rx_idle->mc_setup->watchdog 'device timeout' chain) could not reach the park window across four adversarial campaigns (~10 min) - structural on this kernel: the timer is always armed from the fxp netisr CPU, whose softclock only runs when the ifnet queue is drained (serializer free), and no path sleeps holding the serializer before callout_stop (fxp_stop stop-first ordering). Fix validated at protocol level on the same guest: try-enter + 1-tick retry shape = the same stop completes cleanly, pending=0, module unloads.
No comments yet.