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

ste_stop()/ste_intr STATS_OFLOW issue the synchronous callout_stop() while holding if_serializer while the 1 Hz non-MP ste_stats_update callback must first block in lwkt_serialize_enter on that serializer β€” AB-BA deadlock (DF-3715 family), plus STOP-override timer resurrection at detach (UAF)

Summary

ste_stats_update is a non-MP callout whose FIRST action is the blocking serialize_enter and which re-arms itself INSIDE the serializer; ste_stop and the intr STATS_OFLOW path tear it down with the SYNC callout_stop while holding that same serializer. A dequeued tick parked on the serializer deadlocks against the stopper: stopper sleeps forever holding if_serializer, softclock sleeps forever holding the MP lock - interface, one CPU's callout pipeline, netisr0, and every non-MPSAFE path stall permanently. Secondary: the in-body re-arm is processed recursively and overrides a racing STOP, so stop can return with the timer armed; the resurrected tick dereferences the freed softc one second later. MECHANISM PROVEN on the guest via protocol replica (stop never returned; D4/costp; wedged guest FAILED clean shutdown - MP lock held; positive control validated the fix shape). Try-enter + outside-re-arm fix.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-4310 Β· 12 files
FileTypeDescriptionSize
ste_callout_replica.c β€” 7.9 KB view raw
Makefile β€” 78 B ↓ download
build.sh β€” 122 B view raw
build.log β€” 5.5 KB view raw
run.sh β€” 298 B view raw
run.log β€” 1.4 KB view raw
env.txt β€” 192 B view raw
VERDICT.md β€” 6.1 KB ↓ raw
README.md β€” 1.9 KB ↓ raw
fix.diff β€” 2.3 KB view raw
manifest.json β€” 1.6 KB view raw
verdict.json β€” 6.7 KB view raw

DF-4310 β€” ste(4) callout AB-BA (DF-3715 family) β€” evidence pack

Finding: ste_stop() / ste_intr() STATS_OFLOW path issue the synchronous callout_stop(&sc->ste_stat_timer) while holding ifp->if_serializer, while the 1 Hz non-MP ste_stats_update callback must first block in lwkt_serialize_enter() on that same serializer (holding the MP lock) β€” permanent AB-BA deadlock; secondary: the tick's in-body callout_reset overrides a racing STOP and resurrects the timer after detach (UAF).

Build (on the guest, /usr/src present)

# copy ste_callout_replica.c + Makefile to /root/kmod on the guest
cd /root/kmod && make           # -Werror clean, see build.log

Run (on the guest)

kldload /root/kmod/ste_callout_replica.ko
sysctl kern.sterplica.arm=1
sleep 2
sysctl kern.sterplica.stop_fixed=1      # positive control: returns
(sysctl kern.sterplica.stop_blk=1 &)    # NEVER returns -> wedge
sleep 10; dmesg | grep sterplica | tail -8
ps -o pid,stat,wchan,command -ax | grep stop_blk
# guest now wedged (D4/costp thread + softclock thread + MP lock) β€” reset:
#   vm.sh reset with-src

Expected decisive output (observed, run.log)

sterplica[fixed]: callout_stop RETURNED (no deadlock), pending=0
sterplica[blk]: ste_stats_update handler BLOCKED on our serializer (fired=14 ran=13) - entering callout_stop()
sterplica[blk]: IF THIS IS THE LAST sterplica LINE, AB-BA DEADLOCK IS PROVEN (ste_stats_update vs ste_stop)
1076 D4     costp    sysctl kern.sterplica.stop_blk=1

Contents

  • ste_callout_replica.c / Makefile β€” protocol replica module
  • build.sh / build.log β€” guest build (-Werror clean, BUILD_RC=0)
  • run.sh / run.log β€” full session: presence probe, control, wedge, ps
  • env.txt β€” guest uname, clean-reset confirmation
  • VERDICT.md β€” full narrative with path:line trace
  • fix.diff β€” try-enter + re-arm-outside fix (git-apply-able)
  • manifest.json, verdict.json
VERDICT.md
↓ download raw

DF-4310 β€” ste(4) callout AB-BA deadlock (DF-3715 family instance)

File: sys/dev/netif/ste/if_ste.c (Sundance ST201 / D-Link DFE-550TX)

What was tested and how

The guest has no ST201 hardware (pciconf -l β†’ NO_SUNDANCE_PCI; QEMU cannot emulate the chip), so the driver-attached path cannot run. Per DF-3715 family convention, the callout/serializer protocol if_ste.c uses was replicated 1:1 in an out-of-tree kernel module (ste_callout_replica.c) and exercised on the guest kernel β€” the deadlock lives in the generic protocol, not in the NIC registers.

Replicated protocol elements (exact if_ste.c line numbers):

replica element if_ste.c source
callout_init(&tick_c) (non-MP) :914 callout_init(&sc->ste_stat_timer)
hz-periodic self re-arm :1220 (ste_init), :811 (inside callback)
callback's 1st action = BLOCKING lwkt_serialize_enter(ifp->if_serializer) :790
re-arm INSIDE the serializer :811 callout_reset(...hz...) before :812 lwkt_serialize_exit
callout_stop(&tick_c) while holding the serializer :1231 ste_stop, :608 ste_intr STATS_OFLOW

ste_stop() is reached with ifp->if_serializer held from ste_detach (:997-998), ste_init (:1130, reached from ste_ioctl SIOCSIFFLAGS :1322/:1326, ste_intr HOSTERR :617, ste_txeoc :723/:736, ste_watchdog :1494) and ste_shutdown (:1507). ste_watchdog itself runs serialized on netisr0 (sys/net/if.c if_slowtimo_dispatch β†’ ifnet_tryserialize_all). ste_intr is INTR_MPSAFE with ifp->if_serializer (:974-976), so the STATS_OFLOW callout_stop (:608) executes in interrupt thread context under the serializer.

Result β€” MECHANISM PROVEN (see run.log)

  1. Positive control (fix shape): the try-enter timer is fully unloaded by callout_stop() issued under a held serializer: sterplica[fixed]: callout_stop RETURNED (no deadlock), pending=0
  2. ste shape (AB-BA): with the ste_stats_update-shaped handler parked on the serializer (fired=14 ran=13), callout_stop() NEVER RETURNED: sterplica[blk]: ste_stats_update handler BLOCKED on our serializer (fired=14 ran=13) - entering callout_stop() sterplica[blk]: IF THIS IS THE LAST sterplica LINE, AB-BA DEADLOCK IS PROVEN (ste_stats_update vs ste_stop) ps shows the caller permanently asleep in the synchronous-stop wait: 1076 D4 costp sysctl kern.sterplica.stop_blk=1 (costp = sys/kern/kern_timeout.c:920 ssleep(...,"costp",...) inside _callout_cancel_or_stop(sync=1) reached from callout_stop :1091.)
  3. Corroboration: the wedged guest could no longer perform a clean shutdown -p now β€” ste_stats_update is a non-MP callout (callout_init at :914), so its callback runs holding the MP lock (sys/kern/kern_timeout.c:580-585) and parks on the serializer still holding it; every non-MPSAFE shutdown/system path then blocks. QEMU had to be force-killed and the disk reverted (vm.sh reset with-src).

Guest reset to the clean with-src snapshot afterwards (verified: dmesg | grep -c sterplica β†’ 0, module not loaded).

Why the driver instance follows deterministically

  • callout_stop() is the synchronous flavor in DragonFly (sys/kern/kern_timeout.c:1091 β†’ _callout_cancel_or_stop(..., sync=1) β†’ ssleep(c,&c->spin,0,"costp",0) :911-921): it waits for an in-progress callback to finish.
  • ste_stats_update is non-MP (holds MP lock) and its first action (:790) is the blocking lwkt_serialize_enter(ifp->if_serializer).
  • ste_stop/ste_intr call that callout_stop while holding the very serializer the tick needs β†’ circular wait: stopper sleeps in costp holding the serializer; tick sleeps on the serializer holding the MP lock. Permanent: interface wedged, one softclock thread stops delivering all callouts for its CPU, netisr0 (watchdog path) and every MP-lock consumer stall.

Timing feasibility in the real driver: the 1 Hz tick is dequeued/INPROG on every boundary; ste_stop holds the serializer for β‰₯100 ms of busy-wait (ste_reset's DELAY(100000) at :1244/:1282) on every ifconfig ste0 down, ste_init, or watchdog/TX-error reinit, and the mii-poll body of ste_stats_update itself extends the INPROG window β€” collision is expected under ordinary admin activity or unprivileged TX traffic (ste_start sets if_timer=5 :1475 β†’ watchdog β†’ ste_init β†’ ste_stop).

Secondary outcome (same root cause, source-level)

If the STOP lands while the tick is INPROG inside the body, the callback's own callout_reset (:811) is processed recursively and overrides the STOP (kern_timeout.c: RESET beats STOP; callout_stop's own comment documents re-arm-before-return semantics). ste_stop then returns with the timer armed, and at detach (ste_detach frees ste_ldata via contigfree :1012-1015, deletes the miibus child :1004-1005, releases the I/O resource :1010-1011) the resurrected tick dereferences the freed softc/miibus one second later β€” post-detach use-after-free/panic.

Fix

fix.diff (git-apply-able, dry-run clean): make ste_stats_update never block on the serializer (lwkt_serialize_try, skip the tick's work when contended) and re-arm OUTSIDE the serializer β€” exactly the shape validated by the positive control on this guest and the fix shape merged for em(4) in DF-3715.

Status classification

Mechanism proven on the guest kernel; the ste-specific runtime path is hardware-gated (no ST201 NIC on any QEMU backend). Per family convention (DF-3715/DF-4046/DF-4051/DF-4064/DF-4303): status inconclusive, reproduced=0 at the driver level, mechanism certainty certain.

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 apply-verified against the tree layout); 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 Sundance ST201 hardware.

findings/poc/DF-4310/fix.diff; run.log 'sterplica[fixed]: callout_stop RETURNED (no deadlock), pending=0'
↓ fix.diffper-fix-DF-4310

Confirmed kernel references

Detail

Exploit chain

Local user (no privileges needed for the TX path): send traffic through a running ste(4) interface -> ste_start sets if_timer=5 (:1475) -> if_slowtimo on netisr0 serializes into if_serializer and calls ste_watchdog (:1481) -> ste_init -> ste_stop -> sync callout_stop under the serializer while the 1 Hz ste_stats_update tick is parked in lwkt_serialize_enter on it -> both threads sleep forever, MP lock held: interface permanently wedged, one CPU's softclock callout pipeline dead, non-MPSAFE system paths (incl. shutdown) stall. Admin path: 'ifconfig ste0 down' (SIOCSIFFLAGS :1326) or kldunload/if_detach races the same tick. Availability only; no memory corruption, no uid=0 route.

Evidence (decisive lines)

findings/poc/DF-4310/VERDICT.md (path:line trace + interleaving + shutdown-failure corroboration); run.log (replica session: control 'sterplica[fixed]: callout_stop RETURNED (no deadlock), pending=0'; wedge 'sterplica[blk]: ... entering callout_stop()' then silence; ps '1076 D4 costp sysctl kern.sterplica.stop_blk=1'); build.log (module built on guest BUILD_RC=0, -Werror clean); env.txt (no Sundance/D-Link PCI on guest, clean post-reset state); fix.diff (try-enter + re-arm-outside, patch-apply verified against tree layout)

PoC changes

No seed PoC (finding new in pass 2). Authored ste_callout_replica.ko (out-of-tree kmodule replicating ste's EXACT protocol: non-MP callout_init at :914 flavor, hz self re-arm inside the serializer at :811 flavor, blocking lwkt_serialize_enter first-action at :790 flavor) because the guest has no Sundance ST201 hardware and QEMU cannot emulate one; the deadlock lives in the generic protocol ste uses, so the replica exercises the identical kernel paths (kern_timeout.c callout_stop sync wait vs lwkt_serialize_enter sleep), per the DF-3715 family methodology.

Verified recommended fix

ste_stats_update: replace the blocking lwkt_serialize_enter with lwkt_serialize_try (skip this tick's work on contention) and move the hz self re-arm outside the serializer β€” the fix shape merged for em(4) in DF-3715 and validated on this kernel by the stop_fixed positive control; see fix.diff.

Verdict

Mechanism PROVEN on the guest kernel by deterministic replica of if_ste.c's exact callout/serializer protocol; ste-specific runtime hardware-gated (inconclusive per DF-3715 family convention). ste_stats_update (sys/dev/netif/ste/if_ste.c:780-813) is a NON-MP callout (callout_init :914, armed hz :1220) whose FIRST action is the blocking lwkt_serialize_enter(ifp->if_serializer) (:790) and which re-arms itself INSIDE that serializer (:811); ste_stop (:1231) and ste_intr's STATS_OFLOW path (:607-610) tear it down with the SYNC callout_stop() 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): stopper sleeps forever holding if_serializer; the softclock thread sleeps forever on the serializer WITH THE MP LOCK HELD (non-MP callback, kern_timeout.c:580-585) -> legacy (non-MPSAFE) system paths stall system-wide. ste_stop is reached under the serializer from ste_detach (:997-998), ste_init (:1130; from ste_ioctl SIOCSIFFLAGS :1322/:1326, ste_intr HOSTERR :617, ste_txeoc :723/:736, and ste_watchdog :1494 which itself runs serialized on netisr0 via sys/net/if.c if_slowtimo_dispatch), and ste_shutdown (:1507); the STATS_OFLOW callout_stop runs in interrupt-thread context. Kernel-module replica on the guest: stop under a held serializer with the ste_stats_update-shaped handler parked -> callout_stop NEVER returns ('sterplica[blk]: ste_stats_update handler BLOCKED on our serializer (fired=14 ran=13) - entering callout_stop()' then silence; ps '1076 D4 costp sysctl kern.sterplica.stop_blk=1'); the wedged guest subsequently failed clean shutdown (MP lock parked) and had to be force-reset. Positive control: the try-enter fixed shape is fully unloaded by callout_stop under the serializer (returns, pending=0), validating the fix mechanism. Secondary source-level outcome of the same root cause: the tick's in-body callout_reset (:811) is processed recursively and overrides a racing STOP (kern_timeout.c RESET beats STOP), so ste_stop can return with the timer re-armed; at detach the resurrected tick dereferences the freed softc/miibus/ste_ldata one second later (post-detach UAF).