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

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-4303 Β· 12 files
FileTypeDescriptionSize
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
VERDICT.md
↓ download raw

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)

  1. callout_init(&sc->bfe_stat_timer) β€” :323, non-MP flavor.
  2. Armed callout_reset(&sc->bfe_stat_timer, hz, bfe_tick, sc) β€” :1386 (from bfe_init).
  3. 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 then lwkt_serialize_sleep) - callout_reset(..., hz, bfe_tick, sc) :1497 β€” self re-arm INSIDE - lwkt_serialize_exit :1508.
  4. 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 β€” waits ssleep(c, &c->spin, 0, "costp", 0) :920 until the INPROG callback completes).
  5. bfe_stop callers holding the serializer: bfe_ioctl SIOCSIFFLAGS :1449-1450, bfe_init :1365 ← bfe_watchdog :1480 ← if_slowtimo_dispatch (sys/net/if.c:1900-1912, ifnet_tryserialize_all), bfe_intr ISTAT_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)

  • make in /root/kmod: BUILD_RC=0, -Werror clean (build.log).
  • kldload OK; 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=1 in 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 required vm.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_fixed control here, pending=0) shows this kern_timeout.c unloads a racing re-arm after STOP β€” the defect is purely the AB-BA.
  • bfe_stop cannot simply switch to callout_stop_async: bfe_tick re-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 + ps capture (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_testable
baseline no→ patch + rebuild →patched clean

fix.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'
↓ fix.diffper-fix-DF-4303

Confirmed kernel references

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.