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)
PoC verification
Evidence pack
findings/poc/DF-4310 Β· 12 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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 modulebuild.sh/build.logβ guest build (-Werror clean, BUILD_RC=0)run.sh/run.logβ full session: presence probe, control, wedge, psenv.txtβ guest uname, clean-reset confirmationVERDICT.mdβ full narrative with path:line tracefix.diffβ try-enter + re-arm-outside fix (git-apply-able)manifest.json,verdict.json
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)
- 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 - 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)psshows the caller permanently asleep in the synchronous-stop wait:1076 D4 costp sysctl kern.sterplica.stop_blk=1(costp=sys/kern/kern_timeout.c:920ssleep(...,"costp",...)inside_callout_cancel_or_stop(sync=1)reached fromcallout_stop:1091.) - Corroboration: the wedged guest could no longer perform a clean
shutdown -p nowβste_stats_updateis a non-MP callout (callout_initat :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_updateis non-MP (holds MP lock) and its first action (:790) is the blockinglwkt_serialize_enter(ifp->if_serializer).ste_stop/ste_intrcall thatcallout_stopwhile holding the very serializer the tick needs β circular wait: stopper sleeps incostpholding 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_testablefix.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'
Confirmed kernel references
- sys/dev/netif/ste/if_ste.c:914
- sys/dev/netif/ste/if_ste.c:1220
- sys/dev/netif/ste/if_ste.c:790
- sys/dev/netif/ste/if_ste.c:811
- sys/dev/netif/ste/if_ste.c:1231
- sys/dev/netif/ste/if_ste.c:607
- sys/dev/netif/ste/if_ste.c:997
- sys/dev/netif/ste/if_ste.c:1130
- sys/dev/netif/ste/if_ste.c:1322
- sys/dev/netif/ste/if_ste.c:1494
- sys/dev/netif/ste/if_ste.c:1507
- sys/dev/netif/ste/if_ste.c:1475
- sys/dev/netif/ste/if_ste.c:974
- sys/kern/kern_timeout.c:1091
- sys/kern/kern_timeout.c:920
- sys/kern/kern_timeout.c:580
- sys/net/if.c:1900
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).
No comments yet.