tl(4) callout AB-BA deadlock: tl_stats_update blocks on the driver serializer while tl_stop synchronously stops the timer under it (DF-3715 family)
Summary
tl_stat_timer is a plain 1 Hz callout whose callback does a BLOCKING lwkt_serialize_enter and re-arms itself inside the serializer; tl_stop calls the synchronous callout_stop under the same serializer (all callers hold it). A callback dequeued and parked on the serializer deadlocks callout_stop ('costp') against the stopper: permanent wedge of the caller, one softclock CPU, the interface serializer, and the sysctl subsystem; wedged systems cannot be shut down. Mechanism PROVEN on the guest via exact-protocol replica (stopper D1/costp permanent; fix-shape control validated: try-enter + 1-tick retry fully stops). try-enter fix.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-4109 Β· 12 files| File | Type | Description | Size | |
|---|---|---|---|---|
| README.md | β | 3.2 KB | β raw | |
| VERDICT.md | β | 3.5 KB | β raw | |
| tlnge_replica.c | β | 9.6 KB | view raw | |
| Makefile | β | 66 B | β download | |
| build.sh | β | 130 B | view raw | |
| run.sh | β | 380 B | view raw | |
| build.log | β | 1.3 KB | view raw | |
| run.log | β | 3.9 KB | view raw | |
| env.txt | β | 347 B | view raw | |
| fix.diff | β | 650 B | view raw | |
| manifest.json | β | 1.4 KB | view raw | |
| verdict.json | β | 4.4 KB | view raw |
DF-4109 β tl(4): callout AB-BA deadlock β tl_stats_update blocks on the serializer tl_stop stops the callout under
What this is
sys/dev/netif/tl/if_tl.c arms sc->tl_stat_timer with a plain
callout_init() (if_tl.c:1197) β MP-safe on this kernel
(sys/kern/kern_timeout.c:770) β and re-arms it every hz from
tl_init (if_tl.c:1981). The callback, tl_stats_update()
(if_tl.c:1666-1674), takes the driver serializer by blocking
(lwkt_serialize_enter(ifp->if_serializer), if_tl.c:1671 β parks in
tsleep "slize", sys/kern/lwkt_serialize.c:227-237) and re-arms itself
inside the serializer (if_tl.c:1722).
tl_stop() (if_tl.c:2121) calls callout_stop(&sc->tl_stat_timer)
while its caller holds that same serializer (tl_stop is reached
under the serializer from tl_ioctl if_tl.c:2062, tl_init :1916,
tl_watchdog :2103, tl_detach :1258, and tl_intvec_adchk :1571 in
the ISR).
DragonFly's callout_stop() synchronously waits for an in-progress
callback (_callout_cancel_or_stop(..., sync=1), ssleep "costp",
sys/kern/kern_timeout.c:1091, 920). A callback that was dequeued and
is parked on the stopper's serializer therefore deadlocks against it:
both threads wedge permanently, the interface serializer is never
released, and the parked softclock thread stops delivering all callouts
for its CPU. Same mechanism as DF-3715 (em(4)), PROVEN there on this
guest; re-proven here with the exact tl shape.
Build / run (verification on the audit guest)
# guest has no tl(4) hardware (vtnet0 lo0 only), so the driver protocol
# is replicated verbatim by an out-of-tree module: plain callout_init,
# hz period, BLOCKING lwkt_serialize_enter, re-arm inside the serializer,
# stopper holds the serializer and calls callout_stop.
scp tlnge_replica.c Makefile root@guest:/root/kmod/
vm.sh run_root 'cd /root/kmod && make' # build.log
vm.sh run_root 'kldload /root/kmod/tlnge_replica.ko; sysctl kern.tlreplica.arm=1; sleep 3; \
(sysctl kern.tlreplica.stop_blk=1 >/dev/null 2>&1 &); sleep 6; \
dmesg | grep "replica\[tl\]" | tail -3; \
ps -o pid,stat,wchan,command -ax | grep stop_blk | grep -v grep'
Expected / observed
- dmesg ends with
replica[tl]: handler PARKED on our serializer (fired=4 ran=3) - entering callout_stop()then the marker line β and nothing after (permanent). - stopper thread stuck
D1/D5, wchancostp(kern_timeout.c:920). - Negative control (
stop_plain: callout_stop without the serializer) returns immediately withpending=0. - Fix-shape control (
stop_fix: try-enter + 1-tick retry, the proposed fix):callout_stopunder the serializer returns,pending=0, firing fully stalled β the fix pattern is validated on this kernel. - On real tl(4) hardware the trigger is
ifconfig tl0 down(or MTU change / detach / chip adapter-check) racing the 1-second stats tick.
Trigger on hardware
ifconfig tl0 down / ifconfig tl0 mtu 1500-style reinit / kldunload
if_tl / adapter-check interrupt, while tl_stat_timer fires (once per
second). Narrow per-call window, but the interface cycles on every
down/up and the cost is a permanent system wedge.
DF-4109 β VERDICT
Finding: tl(4) tl_stats_update callout callback blocks on the driver
serializer (lwkt_serialize_enter, sys/dev/netif/tl/if_tl.c:1671) while
tl_stop() calls the synchronous callout_stop(&sc->tl_stat_timer)
(if_tl.c:2121) holding that same serializer β AB-BA deadlock, the
DF-3715 (em(4)) family.
Status: inconclusive per the family convention (target hardware
absent β guest has vtnet0 lo0 only, no ThunderLAN PCI device), with the
mechanism PROVEN deterministically on the guest kernel by a replica
module that reproduces the exact tl protocol:
- callout created with plain
callout_init()(as if_tl.c:1197; MPSAFE on this kernel per sys/kern/kern_timeout.c:770, so the callback is allowed to block and parks the softclock thread), - period
hz(as if_tl.c:1981/1722), - callback does a blocking
lwkt_serialize_enter(if_tl.c:1671 β parks intsleep "slize", sys/kern/lwkt_serialize.c:227-237) and re-arms itself inside the serializer (if_tl.c:1722), - stopper takes the serializer (as every tl_stop caller does:
tl_ioctl if_tl.c:2062, tl_init :1916, tl_watchdog :2103, tl_detach
:1258, tl_intvec_adchk :1571), waits for the handler to park, then
callout_stop()(if_tl.c:2121 β kern_timeout.c:1091 sync wait,ssleep "costp"at :920).
Observed on the guest (DragonFly 6.5-DEVELOPMENT x86_64):
replica[tl]: handler PARKED on our serializer (fired=4 ran=3) - entering callout_stop() replica[tl]: IF THIS IS THE LAST replica LINE, AB-BA DEADLOCK IS PROVEN 1603 D1 costp sysctl kern.tlreplica.stop_blk=1 <- never returns
callout_stop()never returned (re-checked +10 s later; still parked).- Negative control:
callout_stop()without the serializer returns withpending=0β module logic sound. - Fix-shape control: the proposed fix pattern (
lwkt_serialize_try - 1-tick retry β the em_txgc_timer pattern) is fully stopped by
callout_stop()under a held serializer on this exact kernel:fix-shape callout_stop RETURNED ... RESULT pending=0 fired-stalled=YES. - Collateral observed on wedged guests: subsequent sysctl access hangs (the wedged sysctl writer holds the sysctl write lock), and clean shutdown never completes (both post-wedge resets force-killed QEMU) β on hardware this is an un-shutdown-able system.
Why hardware-gated: tl(4) attaches only to TI/Compaq/Olicom ThunderLAN PCI IDs (if_tl.c:227-257); this guest has none, so the in-driver race itself cannot be entered on the guest. The protocol the driver uses, however, is fully generic and was exercised verbatim.
Exploit chain / impact: not a corruption primitive. A privileged local user (ifconfig down/up, MTU change, kldunload if_tl) racing the 1-second stats tick permanently wedges: the calling thread, one softclock thread (callout starvation for that CPU), the interface serializer (any later ifnet consumer blocks), and β as observed β the sysctl subsystem.
Fix: tl_stats_update() must not block: lwkt_serialize_try() with
a 1-tick callout_reset retry (fix.diff). Mechanism of the fix validated
on the guest (fix-shape control above); the patched driver itself cannot
be exercised without hardware β fix_status not_tested.
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 (try-enter + 1-tick retry) authored against the read-only sys/ tree, patch --dry-run clean. The fix MECHANISM is validated on this kernel by the module's fix-shape control (callout_stop under a held serializer returns and fully unloads the try-based handler: pending=0, firing stalled). The patched tl driver itself cannot be exercised without ThunderLAN hardware.
findings/poc/DF-4109/fix.diff; findings/poc/DF-4109/run.log fix-shape section
Confirmed kernel references
- sys/dev/netif/tl/if_tl.c:1197
- sys/dev/netif/tl/if_tl.c:1671
- sys/dev/netif/tl/if_tl.c:1722
- sys/dev/netif/tl/if_tl.c:1981
- sys/dev/netif/tl/if_tl.c:2121
- sys/dev/netif/tl/if_tl.c:2062
- sys/dev/netif/tl/if_tl.c:1916
- sys/dev/netif/tl/if_tl.c:2103
- sys/dev/netif/tl/if_tl.c:1258
- sys/dev/netif/tl/if_tl.c:1571
- sys/kern/kern_timeout.c:920
- sys/kern/kern_timeout.c:1091
- sys/kern/lwkt_serialize.c:234
Detail
Evidence (decisive lines)
findings/poc/DF-4109/run.log (decisive lines: 'handler PARKED on our serializer (fired=4 ran=3) - entering callout_stop()' then marker then silence; ps '1603 D1 costp'; fix-shape 'callout_stop RETURNED ... pending=0 fired-stalled=YES'); build.log (BUILD_RC=0, -Werror); VERDICT.md; fix.diff (try-enter + 1-tick retry)
PoC changes
No seed PoC. Authored tlnge_replica.ko (out-of-tree module) replicating the exact tl protocol because the guest has no ThunderLAN hardware; the deadlock is in the generic protocol the driver uses, so the replica exercises identical kernel paths (plain callout_init, hz period, blocking enter, re-arm inside serializer, stop under serializer).
Verified recommended fix
Make tl_stats_update() non-blocking: lwkt_serialize_try() with a 1-tick callout_reset retry (em_txgc_timer pattern).
Verdict
Mechanism PROVEN on the guest kernel by deterministic replica of if_tl.c's exact callout/serializer protocol; tl-specific runtime hardware-gated (inconclusive per family convention, as DF-3715). tl_stats_update (sys/dev/netif/tl/if_tl.c:1666-1674) is the callback of a plain-callout_init (if_tl.c:1197) 1 Hz timer (if_tl.c:1981) that BLOCKS in lwkt_serialize_enter (if_tl.c:1671) and re-arms itself inside the serializer (if_tl.c:1722); tl_stop() (if_tl.c:2121) tears it down with callout_stop() while the caller holds that same serializer (tl_ioctl :2062, tl_init :1916, tl_watchdog :2103, tl_detach :1258, tl_intvec_adchk :1571). DragonFly callout_stop synchronously waits for an in-progress callback (kern_timeout.c:1091 -> ssleep 'costp' at :920), so a parked callback 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. Kernel-module replica on the guest: stop under a held serializer with the tl-shaped handler parked -> callout_stop NEVER returned (thread D1 wchan 'costp', permanent at +10s); negative control (stop without serializer) returned pending=0; fix-shape control (lwkt_serialize_try + 1-tick retry, the proposed fix) is fully unloaded by callout_stop under the serializer on this kernel (pending=0, firing stalled) - fix mechanism validated. Collateral observed: post-wedge sysctl access hangs (sysctl write lock held by the wedged writer) and clean shutdown never completes.
No comments yet.