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

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-4064 Β· 22 files
FileTypeDescriptionSize
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

  1. 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) β€” fxp0 attaches for real (device fxp is in X86_64_GENERIC). Run fxp_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).
  2. 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 shape Expected (serial console): fxr: handler BLOCKED on our serializer … then fxr: IF THIS IS THE LAST fxr LINE, AB-BA DEADLOCK IS PROVEN … and nothing after it β€” callout_stop() never returns. ps on a second ssh: stopper D0 costp, softclock 5 B5 slize. Observed: exactly that (run.log, guest_pre_reset.txt).
  3. 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_fixed Expected: fxrf: callout_stop RETURNED cleanly, pending=0 + clean unload. Observed: exactly that (run.patched.log).

After any wedged run: vm.sh reset with-src.

VERDICT.md
↓ download raw

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_timer is a non-MPSAFE callout: plain callout_init at sys/dev/netif/fxp/if_fxp.c:399 (vs em/emx which use callout_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 in lwkt_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_stop calls callout_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 every ifconfig fxp0 up, every SIOCSIFFLAGS re-init, and the watchdog path fxp_watchdog :1632), fxp_ioctl SIOCSIFFLAGS down :2078.
  • DragonFly callout_stop is 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_enter blocks: 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_enter entry (: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

fixed
baseline reproduced→ patch + rebuild →patched clean

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

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.