bfe_stop() can deadlock the callout subsystem against bfe_tick (AB-BA): sync callout_stop under the serializer waits forever for a callback blocked on that serializer β interface + one softclock CPU wedged (DF-3715 family)
Summary
bfe_tick is a non-MP callout whose callback BLOCKS in lwkt_serialize_enter and re-arms itself INSIDE it. bfe_stop calls the synchronous callout_stop while holding that same serializer - reached from ifconfig down/detach/shutdown AND unprivileged-reachable paths: the TX-timeout watchdog (5s) and the bfe_intr error reinit. callout_stop waits 'costp' for the in-progress callback while the callback sleeps on the held serializer - permanent AB-BA: both threads wedged, ifnet serializer never released, the CPU's softclock dead (observed ssh stall). MECHANISM PROVEN on the guest via a bfe-protocol replica (stopper D2/costp; fix-shape control returns pending=0). try-enter + outside re-arm fix.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-4303 Β· 12 files| File | Type | Description | Size | |
|---|---|---|---|---|
| README.md | β | 4.3 KB | β raw | |
| VERDICT.md | β | 5.3 KB | β raw | |
| bfe_tick_replica.c | β | 7.0 KB | view raw | |
| Makefile | β | 72 B | β download | |
| build.sh | β | 76 B | view raw | |
| run.sh | β | 333 B | view raw | |
| build.log | β | 6.0 KB | view raw | |
| run.log | β | 555 B | view raw | |
| env.txt | β | 1.2 KB | view raw | |
| fix.diff | β | 1.4 KB | view raw | |
| manifest.json | β | 1.6 KB | view raw | |
| verdict.json | β | 5.5 KB | view raw |
DF-4303 β bfe_stop() can deadlock the callout subsystem against bfe_tick (AB-BA), wedging the interface and a softclock CPU
File: sys/dev/netif/bfe/if_bfe.c (pass 2; known DF-1509 not touched)
Lines: 1486-1508 (bfe_tick: BLOCKING lwkt_serialize_enter at :1494, hz self re-arm INSIDE the serializer at :1497), 1516-1530 (bfe_stop: sync callout_stop() at :1522 under the same serializer), 323 (callout_init, non-MP), 1386 (arming), plus every bfe_stop caller: bfe_ioctl SIOCSIFFLAGS :1449-1450, bfe_init :1365 (itself called from bfe_watchdog :1480 and the bfe_intr error path :1186), bfe_detach :441, bfe_shutdown :475.
Kernel refs: sys/kern/kern_timeout.c:1091 (callout_stop sync), :859-930 (_callout_cancel_or_stop, ssleep(..., "costp", 0) at :920), sys/kern/lwkt_serialize.c:105 (lwkt_serialize_enter β adaptive spin then sleep).
The bug
bfe_tick() is a NON-MP callout (callout_init, if_bfe.c:323) armed at
hz (if_bfe.c:1386) whose callback blocks acquiring the driver
serializer (:1494) and re-arms itself inside it (:1497). bfe_stop()
calls callout_stop(&sc->bfe_stat_timer) (:1522) while holding that
serializer.
DragonFly's callout_stop() synchronously waits for an in-progress
callback (_callout_cancel_or_stop, sync=1 β ssleep("costp") until the
callback completes). If bfe_tick was dequeued (INPROG) and is blocked in
lwkt_serialize_enter() on the serializer held by the bfe_stop caller,
the two wait on each other forever:
softclock thread: bfe_tick -> lwkt_serialize_enter(ser) ... blocked (sleeping) bfe_stop caller: holds ser -> callout_stop() -> "costp" ... blocked (sleeping)
Both threads wedge permanently; the ifnet serializer is never released (every later driver entry point on bfe0 blocks), and the wedged softclock thread stops delivering all callouts for its CPU (system-wide timer starvation on that core). Identical protocol to the PROVEN DF-3715 family (em), with bfe's own flavor (non-MP callout, hz re-arm inside).
Reachable without root: the watchdog path β bfe_start sets
if_timer = 5 (:1351) on TX; if_slowtimo_dispatch
(sys/net/if.c:1900-1912) fires bfe_watchdog under
ifnet_tryserialize_all β bfe_init (:1480) β bfe_stop (:1365), and the
bfe_intr ISTAT_ERRORS path (:1186) does the same from interrupt context.
An unprivileged local user sending traffic onto a stalled bfe link sets
this up; ifconfig bfe0 down (root/devd) is another entry.
Proven on the guest kernel (deterministic bfe-protocol replica)
bfe_tick_replica.c reproduces the exact bfe protocol (non-MP
callout_init + 1-tick first fire + hz re-arm inside the serializer +
blocking enter) and the bfe_stop shape (hold serializer β
callout_stop). The AB-BA test wedges exactly as predicted:
bferplica: loaded (hz=100) bferplica: armed (tick: 1 tick then hz; fixed: 1 tick) bferplica[fixed]: holding serializer, calling callout_stop() bferplica[fixed]: callout_stop RETURNED (no deadlock), pending=0 bferplica[blk]: bfe_tick handler BLOCKED on our serializer (fired=16 ran=15) - entering callout_stop() bferplica[blk]: IF THIS IS THE LAST bferplica LINE, AB-BA DEADLOCK IS PROVEN (bfe_tick vs bfe_stop) (no further output; "callout_stop returned (NO deadlock)" never printed) ps -o pid,stat,wchan,command -ax | grep stop_blk 1048 D2 costp sysctl kern.bferplica.stop_blk=1
costp is the wait-channel inside kern_timeout.c:920 β direct proof the
bfe_stop-shaped caller is stuck in callout_stop()'s sync wait while the
bfe_tick-shaped handler is stuck on its serializer. The ssh session used
to launch the background test itself stalled (callout starvation on the
affected CPU) β the guest had to be reset. Positive control / fix
validation: the try-enter ("fixed") shape is fully stopped by
callout_stop() under a held serializer (pending=0, returns) β the
family fix works on this kernel.
Build/run (guest, DragonFly 6.5-DEVELOPMENT, /usr/src present)
cd /root/kmod && make # build.log (BUILD_RC=0, -Werror clean) kldload ./bfe_tick_replica.ko sysctl kern.bferplica.arm=1 sysctl kern.bferplica.stop_fixed=1 # control: returns, pending=0 (sysctl kern.bferplica.stop_blk=1 &) # NEVER returns; ps shows D2 costp # guest is wedged afterwards - vm.sh reset with-src
DF-4303 VERDICT β bfe_tick / bfe_stop callout AB-BA deadlock (DF-3715 family)
Bottom line
Mechanism PROVEN on the guest kernel by a deterministic replica of if_bfe.c's exact callout/serializer protocol; bfe-specific runtime is hardware-gated (no Broadcom BCM4401 exists on this guest β QEMU has no such device model β and none can be added), so per family convention (DF-3715) the overall verdict is inconclusive with the defect certain from source and the mechanism certain from execution.
Source trace (sys/dev/netif/bfe/if_bfe.c)
callout_init(&sc->bfe_stat_timer)β :323, non-MP flavor.- Armed
callout_reset(&sc->bfe_stat_timer, hz, bfe_tick, sc)β :1386 (frombfe_init). bfe_tickβ :1486-1508: -mii = device_get_softc(sc->bfe_miibus):1492 (outside serializer) -lwkt_serialize_enter(ifp->if_serializer):1494 β BLOCKING (sys/kern/lwkt_serialize.c:105 β adaptive spin up to SLZ_ADAPTIVE_SPINMAX thenlwkt_serialize_sleep) -callout_reset(..., hz, bfe_tick, sc):1497 β self re-arm INSIDE -lwkt_serialize_exit:1508.bfe_stopβ :1516-1530:ASSERT_SERIALIZED:1520 βcallout_stop:1522 (the sync variant, sys/kern/kern_timeout.c:1091 β_callout_cancel_or_stop(cc, CALLOUT_STOP, 1):859 β waitsssleep(c, &c->spin, 0, "costp", 0):920 until the INPROG callback completes).bfe_stopcallers holding the serializer:bfe_ioctlSIOCSIFFLAGS :1449-1450,bfe_init:1365 βbfe_watchdog:1480 βif_slowtimo_dispatch(sys/net/if.c:1900-1912,ifnet_tryserialize_all),bfe_intrISTAT_ERRORS :1186,bfe_detach:441,bfe_shutdown:475.
Interleaving: tick dequeued β parks sleeping on the serializer β stopper
enters serializer β callout_stop parks sleeping on "costp" β neither can
ever proceed. The ifnet serializer is held forever by a sleeping thread;
the softclock thread of the callback's CPU is consumed forever (no further
callouts on that CPU).
Guest execution (this run, evidence in run.log / build.log)
makein /root/kmod: BUILD_RC=0,-Werrorclean (build.log).kldloadOK;kern.bferplica.arm=1.- Control (fix shape):
kern.bferplica.stop_fixed=1βbferplica[fixed]: callout_stop RETURNED (no deadlock), pending=0β try-enter handlers are safely stopped under a held serializer β the family fix (fix.diff) is validated at mechanism level on this kernel. - AB-BA (bfe shape):
kern.bferplica.stop_blk=1in background β last dmesg lines:bferplica[blk]: bfe_tick handler BLOCKED on our serializer (fired=16 ran=15) - entering callout_stop()bferplica[blk]: IF THIS IS THE LAST bferplica LINE, AB-BA DEADLOCK IS PROVEN (bfe_tick vs bfe_stop)and nothing after.ps:1048 D2 costp sysctl kern.bferplica.stop_blk=1β D-state thread parked on the kern_timeout.c:920 wait channel. The launching ssh session stalled (timer starvation on the affected CPU); guest requiredvm.sh reset with-src(performed; guest restored clean).
Threat model
Local DoS. The watchdog entry (TX timeout) is drivable by an unprivileged
local user pumping traffic at a bfe interface whose link is stalled
(bfe_start sets if_timer=5 :1351; watchdog β bfe_init β bfe_stop).
Every bfe_stop races the 1 Hz tick; the race window is the callback's
park-on-serializer interval. Root paths (ifconfig down, detach, shutdown)
hit the same race. On hit: interface permanently wedged + one CPU's callout
pipeline dead β system-wide degradation (observed: ssh stall).
No memory corruption: availability only. No uid=0 route (not a corruption class).
Why not false-positive / alternatives
- Not the "racing re-arm survives callout_stop" family: the DF-3715
control (and my
stop_fixedcontrol here,pending=0) shows this kern_timeout.c unloads a racing re-arm after STOP β the defect is purely the AB-BA. bfe_stopcannot simply switch tocallout_stop_async:bfe_tickre-arms itself unconditionally (:1497), so an async stop leaves the timer cycling on a stopped/detached device. The complete fix is the try-enter handler (fix.diff), matching the DF-3715 fix merged for em(4).
Fix validation
fix.diff (try-enter + unconditional outside re-arm) β the fixed shape is
exactly the stop_fixed positive control, which returns cleanly with
pending=0 on this guest kernel. The driver-level diff cannot be runtime
tested without BCM4401 hardware β fix_status not_testable (driver),
mechanism validated (control).
Artifacts
bfe_tick_replica.c+Makefileβ the bfe-protocol replica module.build.logβ full module build (+ harness build/run) output, BUILD_RC=0.run.logβ load/arm/control/AB-BA session: dmesg lines +pscapture (1048 D2 costp).env.txtβ guest uname, NIC inventory (vtnet0/lo0 only), no Broadcom PCI, md5(guest if_bfe.c) == md5(audited tree).fix.diffβ bfe_tick try-enter fix (patch -p1 --dry-run clean).
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 authored against the read-only sys/ tree (patch -p1 --dry-run clean); the fixed try-enter shape IS the stop_fixed positive control, which returns cleanly with pending=0 on this guest kernel (mechanism validated); the driver-level diff cannot be runtime-tested without BCM4401 hardware.
findings/poc/DF-4303/fix.diff; run.log 'bferplica[fixed]: callout_stop RETURNED (no deadlock), pending=0'
Confirmed kernel references
- sys/dev/netif/bfe/if_bfe.c:323
- sys/dev/netif/bfe/if_bfe.c:1386
- sys/dev/netif/bfe/if_bfe.c:1494
- sys/dev/netif/bfe/if_bfe.c:1497
- sys/dev/netif/bfe/if_bfe.c:1522
- sys/dev/netif/bfe/if_bfe.c:1365
- sys/dev/netif/bfe/if_bfe.c:1480
- sys/dev/netif/bfe/if_bfe.c:1186
- sys/dev/netif/bfe/if_bfe.c:1351
- sys/kern/kern_timeout.c:859
- sys/kern/kern_timeout.c:920
- sys/kern/kern_timeout.c:1091
- sys/kern/lwkt_serialize.c:105
- sys/net/if.c:1900
Detail
Exploit chain
Local unprivileged user sends UDP/TCP traffic through a bfe(4) interface whose link is stalled (or waits for the 1 Hz tick race during any interface reinit): bfe_start sets if_timer=5, the TX watchdog fires bfe_watchdog -> bfe_init -> bfe_stop which holds the serializer and enters callout_stop while bfe_tick is parked on that serializer -> both threads sleep forever -> interface permanently wedged + one CPU's callout pipeline dead (system-wide timer starvation). Availability only; no memory corruption, no uid=0 route.
Evidence (decisive lines)
findings/poc/DF-4303/VERDICT.md (path:line trace + interleaving); run.log (replica session: 'bferplica[blk]: bfe_tick handler BLOCKED on our serializer (fired=16 ran=15) - entering callout_stop()' then silence; ps '1048 D2 costp sysctl kern.bferplica.stop_blk=1'; control 'callout_stop RETURNED (no deadlock), pending=0'); build.log (module built on guest BUILD_RC=0, -Werror clean); env.txt (no Broadcom PCI, hw.bfe unknown, guest src md5 == audited tree); fix.diff (try-enter bfe_tick, patch --dry-run clean)
PoC changes
No seed PoC (finding new in pass 2). Authored bfe_tick_replica.ko (out-of-tree kmodule replicating bfe's EXACT protocol: non-MP callout_init, hz self re-arm inside the serializer, blocking lwkt_serialize_enter) because the guest has no BCM4401 hardware and QEMU cannot emulate one; the deadlock lives in the generic protocol bfe uses, so the replica exercises the identical kernel paths (kern_timeout.c callout_stop sync wait vs lwkt_serialize_enter sleep).
Verified recommended fix
bfe_tick: replace the blocking lwkt_serialize_enter with lwkt_serialize_try (skip work this tick on failure) and move the hz self re-arm outside the serializer - exactly the fix merged for em(4) in DF-3715; see fix.diff.
Verdict
Mechanism PROVEN on the guest kernel by deterministic replica of if_bfe.c's exact callout/serializer protocol; bfe-specific runtime hardware-gated (inconclusive per DF-3715 family convention). bfe_tick (sys/dev/netif/bfe/if_bfe.c:1486-1508) is a NON-MP callout (callout_init :323, armed hz :1386) whose callback BLOCKS in lwkt_serialize_enter (:1494) on the driver serializer and re-arms itself INSIDE it (:1497); bfe_stop tears it down with the SYNC callout_stop (:1522) while holding that same serializer. DragonFly callout_stop synchronously waits for an in-progress callback (sys/kern/kern_timeout.c:1091 -> :859-930, ssleep 'costp' at :920). A dequeued callback parked on the serializer therefore deadlocks against the stopper (AB-BA): both threads wedge permanently, the interface serializer is never released, and the affected CPU's softclock thread stops delivering all callouts (observed ssh stall). bfe_stop is reached under the serializer from bfe_ioctl SIOCSIFFLAGS (:1449), bfe_detach (:441), bfe_shutdown (:475), and - reachable by unprivileged users - the TX-timeout watchdog chain bfe_start if_timer=5 (:1351) -> if_slowtimo (sys/net/if.c:1900-1912) -> bfe_watchdog (:1480) -> bfe_init (:1365), plus the bfe_intr error reinit (:1186). Kernel-module replica on the guest: stop under a held serializer with the bfe_tick-shaped handler parked -> callout_stop NEVER returns (caller D2 wchan 'costp', ps '1048 D2 costp sysctl kern.bferplica.stop_blk=1'); positive control: the try-enter fixed shape is fully unloaded by callout_stop (returns, pending=0), validating the fix mechanism and showing the defect is the blocking-enter AB-BA, not a post-stop re-arm.
No comments yet.