sk_stop()/sk_tick()/sk_yukon_tick() AB-BA callout deadlock: synchronous callout_stop() under the card-global serializer vs blocking lwkt_serialize_enter in the self-re-arming NON-MPSAFE 1 Hz tick (DF-3715 family, 7th instance) β mechanism PROVEN on guest
Summary
sk_tick and sk_yukon_tick block on lwkt_serialize_enter of the ONE card-global serializer shared by both ports of dual-port SK-98xx cards; sk_stop asserts it holds that serializer and calls the synchronous callout_stop, which ssleeps on 'costp' waiting for the very callback parked on the lock the stopper holds. Permanent kernel wedge; because the callout is plain non-MPSAFE, the parked softclock thread additionally holds the Giant MP lock. MECHANISM PROVEN on the guest via protocol replica: stopper D/'costp', softclock B/'slize', callout_stop never returned, wedge survived clean shutdown; fix-shape validated live (try+retry: returned pending=0, 307 retries/0 parks cross-CPU). Try-enter fix.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-3903 Β· 14 files| File | Type | Description | Size | |
|---|---|---|---|---|
| README.md | β | 2.4 KB | β raw | |
| VERDICT.md | β | 7.5 KB | β raw | |
| sk_callout_replica.c | β | 5.3 KB | view raw | |
| sk_callout_replica_fixed.c | β | 3.1 KB | view raw | |
| build.sh | β | 128 B | view raw | |
| run.sh | β | 209 B | view raw | |
| build_shared.log | β | 13 B | view raw | |
| run.log | β | 312 B | view raw | |
| console_markers.txt | β | 200.6 KB | view raw | |
| fix_validation.log | β | 307 B | view raw | |
| env.txt | β | 150 B | view raw | |
| fix.diff | β | 1.3 KB | view raw | |
| manifest.json | β | 1.5 KB | view raw | |
| verdict.json | β | 5.4 KB | view raw |
DF-3903 β sk(4) AB-BA callout deadlock: sk_stop() vs sk_tick()/sk_yukon_tick()
File: sys/dev/netif/sk/if_sk.c (pass 2, GLM 5.3)
Family: DF-3715 / 3738 / 3760 / 3776 / 3806 / 3850 (7th instance) β
this is the SysKonnect sk(4) member; aggravating delta (like dc): the
callout is non-MPSAFE.
What to run (on the audit QEMU guest, root)
md5 /usr/src/sys/dev/netif/sk/if_sk.c # aff2b52bf166e5ebcbe6354b1ce24e6d == audited tree
./build.sh # sk_callout_replica.ko (+ _fixed.ko)
kldload /root/kmod/sk_callout_replica.ko
sysctl kern.skreplica_arm=1 # arm the sk_yukon_tick-shaped 1-tick timer
sleep 1
(sysctl kern.skreplica_stop=1 >/dev/null 2>&1 &) # sk_stop shape: NEVER RETURNS
Expected
The background sysctl never returns. A second session shows the stopper
thread in state D, wchan costp (the synchronous callout_stop() wait,
sys/kern/kern_timeout.c:920), and one (softclock N) thread in state
B, wchan slize (parked in lwkt_serialize_enter). Serial console
(dfbsd-qemu/boot.log):
skreplica[arm]: sk_tick_timer armed (1 tick), waiting skreplica[stop]: tick INPROG and PARKED on the serializer (fired=2 ran=1) skreplica[stop]: IF THIS IS THE LAST skreplica LINE callout_stop NEVER RETURNED (AB-BA deadlock)
No further skreplica lines appear: callout_stop() never returned.
The wedge survives a clean shutdown attempt (the parked softclock thread
holds the Giant MP lock because sk_tick_timer is a plain, non-MPSAFE
callout, if_sk.c:1311); the guest must be force-reset.
The guest has no SysKonnect hardware, so the driver itself is exercised by
a kernel module (sk_callout_replica.c) that replicates if_sk.c's exact
callout/serializer protocol (same callout flavor, same blocking enter,
same unconditional self re-arm, same callout_stop under the serializer).
The deadlock is in that generic protocol, which the replica exercises
byte-for-byte on the live kernel.
Fix-shape validation (fresh guest)
kldload /root/kmod/sk_callout_replica_fixed.ko # lwkt_serialize_try + 1-tick retry (fix.diff shape)
sysctl kern.skrepfix_arm=1; sleep 1
(sysctl kern.skrepfix_stop=1 >/dev/null 2>&1 &) # holds serializer 3s, then callout_stop
Expected (observed): skrepfix[stop]: callout_stop RETURNED (fixed shape):
fired=312 ran=5 retry=307 pending=0 β under cross-CPU contention the tick
retries instead of parking and callout_stop returns.
DF-3903 β VERDICT
Status: mechanism PROVEN on the guest kernel on the FIRST attempt by a
deterministic replica of if_sk.c's exact callout/serializer protocol;
sk-specific runtime hardware-gated (no SysKonnect NIC on the audit guest)
β inconclusive per family convention. reproduced=0 (no sk hardware on
this guest), impact on this guest=none. The class of impact, when it fires
on sk(4) hardware, is a permanent kernel deadlock (local DoS): wedged
interface, wedged stopper thread, softclock blackout on one CPU, and β
because sk_tick_timer is a plain non-MPSAFE callout β the Giant MP
lock held by the parked softclock thread. The wedge also defeated the
guest's clean shutdown (force reset required).
Root cause (path:line)
sys/dev/netif/sk/if_sk.c:1311βcallout_init(&sc_if->sk_tick_timer), per port: plain (non-MPSAFE) callout β the callback executes under the Giant MP lock (sys/kern/kern_timeout.c:583 get_mplock()).sys/dev/netif/sk/if_sk.c:2081-2091βsk_yukon_tick()does a blockinglwkt_serialize_enter(ifp->if_serializer)at:2087(sys/kern/lwkt_serialize.c:105βatomic_intr_cond_enter(..., lwkt_serialize_sleep)β tsleep, wchanslize), then self re-arms at:2089(UNCONDITIONALLY, every 1 Hz fromsk_init :2766) and exits at:2090. Its twinsk_tick()(:2032) does the same blocking enter at:2041with a conditional self re-arm at:2067(link-down GPIO poll loop:2061-2070), reached fromsk_init-time XMAC interrupts (sk_intr_xmac :2159) and the BCom link-interrupt path (sk_intr_bcom :2138).ifp->if_serializeris the ONE card-globalsc->sk_serializershared by both ports (ether_ifattach(ifp, ..., &sc->sk_serializer)at:1281;bus_setup_intr(..., &sc->sk_serializer)at:1514).sys/dev/netif/sk/if_sk.c:2770-2782βsk_stop()asserts it holds that serializer (:2778) and then calls the synchronouscallout_stop(&sc_if->sk_tick_timer)at:2782. Everysk_stop()caller holds it:sk_ioctlSIOCSIFFLAGS-down:992,sk_init :2606(SIOCSIFMTU:977, SIOCSIFFLAGS-up:989,sk_ifmedia_upd :937, watchdogsk_watchdog :1758),sk_detach :1540.sys/kern/kern_timeout.c:1091β:859-932βcallout_stop()is synchronous: when the callout is INPROG (:567), the callerssleep(c, &c->spin, 0, "costp", 0)(:920) until the callback completes.- β A
sk_yukon_tick/sk_tickdequeued and parked inlwkt_serialize_enter()on the serializer held by thesk_stopcaller deadlocks against it (AB-BA): both sleep forever; the shared card serializer is never released (both ports of a dual-port card are dead); the affected CPU's softclock thread stops delivering all callouts; and sincesk_tick_timeris non-MPSAFE, the parked softclock thread also holds the Giant MP lock, degrading the whole system.
Trigger on sk(4) hardware: any sk_stop() racing the 1 Hz tick β
ifconfig skX down/up, MTU change on a running interface
(SIOCSIFMTU β sk_init β sk_stop), media change, watchdog reinit,
detach/kldunload, suspend/resume, shutdown. The tick fires at 1 Hz and the
stop path (BMU stop with SK_TIMEOUTΓDELAY(1) poll loops, :2792-2810)
spends real time under the serializer, so the window per stop is real;
each interface stop is one lottery draw.
This is the sk(4) instance of the DF-3715/3738/3760/3776/3806/3850
family (em, emx, alc, jme, msk, dc). Aggravating delta (shared with
dc(4) DF-3850, vs the em/emx/alc/jme/msk members which use
callout_init_mp): the timer is a non-MPSAFE callout, so the parked
callback additionally holds the Giant MP lock.
Verification performed (guest, DragonFly 6.5-DEVELOPMENT x86_64)
md5 /usr/src/sys/dev/netif/sk/if_sk.c=aff2b52bf166e5ebcbe6354b1ce24e6dβ guest source is byte-identical to the audited tree.sk_callout_replica.c(this pack) β kernel module replicating the sk protocol byte-for-byte in shape: plaincallout_init(non-MPSAFE), tick callback doing blockinglwkt_serialize_enter+ unconditional self re-arm at 1 Hz, stop sysctl emulatingsk_stop(serializer held across the synchronouscallout_stop).- Positive test (sk shape): first attempt reproduced β serial
console:
skreplica[stop]: tick INPROG and PARKED on the serializer (fired=2 ran=1)then the terminal marker line; no further skreplica output ever appeared βcallout_stopnever returned. - Wedged-thread evidence from a second session: stopper
D0 costp sysctl kern.skreplica_stop=1(pid 2051;costpis kern_timeout.c:920), and(softclock 1)inB1 slizeβ the parked non-MPSAFE callback holding the Giant MP lock. - The wedge survived a clean shutdown attempt β
vm.sh downfell back to killing QEMU (guest not answering (likely DDB on panic); killing qemu), confirming the MP-lock aggravation. - Fix-shape validation on a fresh guest with the exact
fix.diffprotocol (lwkt_serialize_try+ 1-tick retry): same-CPU interleave:callout_stop RETURNED (fixed shape): fired=4 ran=4 retry=0 pending=0; cross-CPU contention (stopper busy-holding the serializer 3 s from another session, the interleave that deadlocks the unfixed shape):callout_stop RETURNED: fired=312 ran=5 retry=307 pending=0β 307 retries, 0 parks, timer cleanly stopped. Deadlock eliminated; guest stayed healthy and unloadable.
Why a replica (and not the driver)
The guest has no SysKonnect SK-98xx/Yukon PCI NIC; if_sk.ko cannot even
probe. The deadlock, however, lives entirely in the generic callout +
lwkt_serializer protocol the driver uses β plain callout, blocking
serialize_enter in the callback, synchronous callout_stop under the same
serializer β all of which the replica exercises on the live kernel. This
is the same methodology already used to prove DF-3715/3738/3760/3776/3806/
3850; the mechanism has now been reproduced live 8 times (em, emx, alc,
jme, msk, dc, sk, xl) with zero false triggers.
Negative results (pass-2 checklist items killed for if_sk.c)
- RX length trust (DF-3604 family): NOT present.
sk_rxeof(:1803-1888) bounds the NIC-DMA'd 16-bitSK_RXBYTESlength at:1859:total_len < SK_MIN_FRAMELEN || total_len > max_frmlenrejects, wheremax_frmlenisETHER_MAX_LEN(1518 < the 2046-byte usable posted cluster,if_sk.c:821-832) or, in jumbo mode,SK_JUMBO_FRAMELEN(9018) which is exactly the usable size of the posted jumbo buffer (SK_JLEN9020 =SK_JUMBO_FRAMELEN + ETHER_ALIGN,if_skreg.h:1349-1355, minus them_adj(ETHER_ALIGN)at:796). - TX cleanup walk: clean.
sk_txeof(:1986-2029) walksconsβprodwith an OWN-bit break, frees/unloads exactly once, decrements the same countersk_encapincrements bynsegs;SK_IS_OACTIVE(if_skvar.h:211) keepsmaxsegs β₯ SK_NDESC_SPAREso theKASSERTat:1617cannot fire. - Jumbo MTU:
SIOCSIFMTUcaps atSK_JUMBO_MTU(9000,:972); the jumbo pool math (SK_JSLOTS384 β₯ ring 256) is sound. - Known pass-1 findings DF-1276/DF-1277 (DMA-map cleanup index; partial ring on non-ENOBUFS) excluded per instructions.
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
inconclusivefix.diff authored against the read-only sys/ tree: sk_tick and sk_yukon_tick use lwkt_serialize_try + 1-tick retry. Validated at protocol level on the guest with sk_callout_replica_fixed.ko (callout_stop returns, pending==0, 312 fires / 307 retries / 0 parks while the serializer was held 3s from another CPU). Driver-level validation impossible on this guest (no sk NIC).
findings/poc/DF-3903/fix.diff; findings/poc/DF-3903/fix_validation.log
Confirmed kernel references
- sys/dev/netif/sk/if_sk.c:1311
- sys/dev/netif/sk/if_sk.c:2041
- sys/dev/netif/sk/if_sk.c:2067
- sys/dev/netif/sk/if_sk.c:2087
- sys/dev/netif/sk/if_sk.c:2089
- sys/dev/netif/sk/if_sk.c:2778
- sys/dev/netif/sk/if_sk.c:2782
- sys/dev/netif/sk/if_sk.c:977
- sys/dev/netif/sk/if_sk.c:2606
- sys/kern/kern_timeout.c:583
- sys/kern/kern_timeout.c:920
- sys/kern/kern_timeout.c:1091
- sys/kern/lwkt_serialize.c:105
Detail
Evidence (decisive lines)
findings/poc/DF-3903/VERDICT.md; run.log (D0 costp stopper + B1 slize softclock 1); console_markers.txt (fired=2 ran=1 + terminal marker line from dfbsd-qemu/boot.log); build_shared.log (modules built clean under -Werror); fix_validation.log (fixed shape: callout_stop RETURNED, pending=0, 312 fires/307 retries/0 parks); env.txt (guest uname); fix.diff (try-enter + 1-tick retry for sk_tick and sk_yukon_tick)
PoC changes
No seed PoC. Authored sk_callout_replica.ko / sk_callout_replica_fixed.ko (out-of-tree kmodules replicating the if_sk.c callout protocol, including the plain/non-MPSAFE callout_init at :1311) because the guest has no sk hardware; the deadlock is in the generic protocol sk uses, so the replica exercises the identical kernel paths.
Verified recommended fix
Make sk_tick()/sk_yukon_tick() non-blocking: lwkt_serialize_try() with a 1-tick callout_reset retry (validated live at protocol level).
Verdict
Mechanism PROVEN on the guest kernel on the FIRST attempt by a deterministic replica of if_sk.c's exact callout/serializer protocol; sk-specific runtime hardware-gated (no SysKonnect NIC on the audit guest) -> inconclusive per family convention. sk_tick/sk_yukon_tick (sys/dev/netif/sk/if_sk.c:2032/:2081) are the callbacks of a PLAIN (non-MPSAFE) callout (callout_init at :1311 -> callback executes under the Giant MP lock, kern_timeout.c:583) and do a BLOCKING lwkt_serialize_enter(ifp->if_serializer) at :2041/:2087 with self re-arm at :2067/:2089 (sk_yukon_tick unconditional, 1 Hz from sk_init :2766); sk_stop() tears the callout down with the synchronous callout_stop() at :2782 while holding that same serializer (ASSERT_SERIALIZED :2778; callers: sk_ioctl :992, sk_init :2606 via SIOCSIFMTU :977 / SIOCSIFFLAGS :989 / sk_ifmedia_upd :937, sk_watchdog :1758, sk_detach :1540); DragonFly callout_stop synchronously waits for an INPROG callback (kern_timeout.c:1091 -> :859-932, ssleep 'costp' at :920). A tick dequeued and parked on the serializer therefore deadlocks against the stopper (AB-BA) - and since the serializer is the one card-global lock shared by BOTH ports of a dual-port card (ether_ifattach :1281), both interfaces wedge. Kernel-module replica on the guest: with the sk-shaped handler parked, callout_stop NEVER returned - stopper observed D0/wchan 'costp' (pid 2051), softclock 1 observed B1/wchan 'slize' holding the Giant MP lock (non-MPSAFE callout, aggravation shared with dc DF-3850), serial console shows 'fired=2 ran=1' then the terminal marker line, and the wedge survived a clean shutdown attempt (vm.sh down had to SIGKILL QEMU). 7th member of the DF-3715/3738/3760/3776/3806/3850 family, now live-proven 8 times with zero false triggers. Fix-shape validation on a fresh guest with the exact fix.diff protocol (lwkt_serialize_try + 1-tick retry): callout_stop returned with pending=0 under both interleave shapes, including cross-CPU contention (fired=312 ran=5 retry=307, 0 parks) - deadlock eliminated.
No comments yet.