xl_stop()/xl_stats_update() AB-BA callout deadlock: synchronous callout_stop() under the ifnet serializer vs blocking lwkt_serialize_enter in the self-re-arming NON-MPSAFE 1 Hz stats tick (DF-3715 family, 10th instance) β mechanism PROVEN on guest
Summary
xl_stats_update is the callback of a plain non-MPSAFE callout and does a blocking lwkt_serialize_enter, then self re-arms inside the serialized body (whose work is long: 16 windowed stats reads + MII bit-bang); xl_stop asserts it holds the serializer and calls the synchronous callout_stop which ssleeps waiting for the parked callback. Permanent kernel wedge + Giant MP lock held. MECHANISM PROVEN on the guest via protocol replica (first attempt): stopper D2/'costp', softclock B4/'slize', callout_stop never returned, clean shutdown wedged; fix-shape validated live (312 fires/307 retries/0 parks). Try-enter fix.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-3906 Β· 13 files| File | Type | Description | Size | |
|---|---|---|---|---|
| README.md | β | 2.4 KB | β raw | |
| VERDICT.md | β | 7.4 KB | β raw | |
| xl_callout_replica.c | β | 5.3 KB | view raw | |
| xl_callout_replica_fixed.c | β | 2.9 KB | view raw | |
| build.sh | β | 128 B | view raw | |
| run.sh | β | 209 B | view raw | |
| run.log | β | 312 B | view raw | |
| console_markers.txt | β | 289 B | view raw | |
| fix_validation.log | β | 300 B | view raw | |
| env.txt | β | 150 B | view raw | |
| fix.diff | β | 723 B | view raw | |
| manifest.json | β | 1.3 KB | view raw | |
| verdict.json | β | 5.2 KB | view raw |
DF-3906 β xl(4) AB-BA callout deadlock: xl_stop() vs xl_stats_update()
File: sys/dev/netif/xl/if_xl.c (pass 2, GLM 5.3)
Family: DF-3715 / 3738 / 3760 / 3776 / 3806 / 3850 / 3903 (10th
instance) β this is the 3Com 3c90x xl(4) member; aggravating delta
(like dc/sk): xl_stat_timer is a non-MPSAFE callout.
What to run (on the audit QEMU guest, root)
md5 /usr/src/sys/dev/netif/xl/if_xl.c # 50da613cb89ad7c4e36eca8304bee221 == audited tree
./build.sh # xl_callout_replica.ko (+ _fixed.ko)
kldload /root/kmod/xl_callout_replica.ko
sysctl kern.xlreplica_arm=1 # arm the xl_stats_update-shaped 1-tick timer
sleep 1
(sysctl kern.xlreplica_stop=1 >/dev/null 2>&1 &) # xl_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):
xlreplica[arm]: xl_stat_timer armed (1 tick), waiting xlreplica[stop]: tick INPROG and PARKED on the serializer (fired=2 ran=1) xlreplica[stop]: IF THIS IS THE LAST xlreplica LINE callout_stop NEVER RETURNED (AB-BA deadlock)
No further xlreplica lines appear: callout_stop() never returned. The
wedge survives a clean shutdown attempt (the parked softclock thread holds
the Giant MP lock because xl_stat_timer is a plain, non-MPSAFE callout,
if_xl.c:1320); the guest must be force-reset.
The guest has no 3c90x hardware, so the driver itself is exercised by a
kernel module (xl_callout_replica.c) that replicates if_xl.c's exact
callout/serializer protocol (same callout flavor, same blocking enter,
same self re-arm inside the serialized body, same callout_stop under
the serializer). The deadlock is in that generic protocol.
Fix-shape validation (fresh guest)
kldload /root/kmod/xl_callout_replica_fixed.ko # lwkt_serialize_try + 1-tick retry (fix.diff shape)
sysctl kern.xlrepfix_arm=1; sleep 1
(sysctl kern.xlrepfix_stop=1 >/dev/null 2>&1 &) # holds serializer 3s, then callout_stop
Expected (observed): xlrepfix[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-3906 β VERDICT
Status: mechanism PROVEN on the guest kernel on the FIRST attempt by a
deterministic replica of if_xl.c's exact callout/serializer protocol;
xl-specific runtime hardware-gated (no 3Com 3c90x NIC on the audit guest)
β inconclusive per family convention. reproduced=0 (no xl hardware on
this guest), impact on this guest=none. The class of impact, when it fires
on xl(4) hardware, is a permanent kernel deadlock (local DoS): wedged
interface, wedged stopper thread, softclock blackout on one CPU, and β
because xl_stat_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). This is the 10th member of
the DF-3715 family across the DFly NIC drivers.
Root cause (path:line)
sys/dev/netif/xl/if_xl.c:1320βcallout_init(&sc->xl_stat_timer): plain (non-MPSAFE) callout β the callback executes under the Giant MP lock (sys/kern/kern_timeout.c:583 get_mplock()).sys/dev/netif/xl/if_xl.c:2372-2380βxl_stats_update()does a blockinglwkt_serialize_enter(sc->arpcom.ac_if.if_serializer)at:2377(sys/kern/lwkt_serialize.c:105β tsleep, wchanslize), then runsxl_stats_update_serialized()(16 stats register reads + window juggling +mii_tick) and self re-arms at:2429(callout_reset(..., hz, xl_stats_update, sc), guarded only byxl_stats_no_timeout, which is 0 in the normal 1 Hz tick context; armed fromxl_init :2918on every interface init).sys/dev/netif/xl/if_xl.c:3136-3169βxl_stop()asserts it holds that serializer (:3142) and then calls the synchronouscallout_stop(&sc->xl_stat_timer)at:3169. Everyxl_stop()caller holds it:xl_detach :1573,xl_ioctlSIOCSIFFLAGS-down:3065,xl_init :2712(SIOCSIFFLAGS-up at:3062, media change viaxl_ifmedia_upd :2955βxl_init, watchdog:3125viaxl_init),xl_shutdown :3212,xl_suspend :3222.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.- β An
xl_stats_updatedequeued and parked inlwkt_serialize_enter()on the serializer held by thexl_stopcaller deadlocks against it (AB-BA): both sleep forever; the interface serializer is never released; the affected CPU's softclock thread stops delivering all callouts; and sincexl_stat_timeris non-MPSAFE, the parked softclock thread also holds the Giant MP lock, degrading the whole system.
Trigger on xl(4) hardware: any xl_stop() racing the 1 Hz stats tick β
ifconfig xlX down/up, media change on a running interface, watchdog
reinit, detach/kldunload, suspend/resume, shutdown. The tick fires at 1 Hz
and its serialized body is long (16 windowed register reads + MII
bit-banging with DELAYs inside mii_tick), so the window per stop is
real; each interface stop is one lottery draw.
This is the xl(4) instance of the DF-3715/3738/3760/3776/3806/3850/3903 family (em, emx, alc, jme, msk, dc, sk β 8 drivers, this is the 9th driver / 10th timer: em alone had two callout sites). Aggravating delta (shared with dc DF-3850 and sk DF-3903): 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/xl/if_xl.c=50da613cb89ad7c4e36eca8304bee221β guest source is byte-identical to the audited tree.xl_callout_replica.c(this pack) β kernel module replicating the xl protocol byte-for-byte in shape: plaincallout_init(non-MPSAFE), tick callback doing blockinglwkt_serialize_enter+ self re-arm inside the serialized body, stop sysctl emulatingxl_stop(serializer held across the synchronouscallout_stop).- Positive test (xl shape): first attempt reproduced β serial
console:
xlreplica[stop]: tick INPROG and PARKED on the serializer (fired=2 ran=1)then the terminal marker line; no further xlreplica output ever appeared βcallout_stopnever returned. - Wedged-thread evidence from a second session: stopper
D2 costp sysctl kern.xlreplica_stop=1(pid 1104;costpis kern_timeout.c:920), and(softclock 4)inB4 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 3Com 3c90x PCI NIC; if_xl.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. Same
methodology as DF-3715/3738/3760/3776/3806/3850/3903; the mechanism has
now been reproduced live 8 times with zero false triggers.
Negative results (pass-2 checklist items killed for if_xl.c)
- RX length trust (DF-3604 family): NOT present.
xl_rxeof(:1948-2065) readstotal_len = rxstat & XL_RXSTAT_LENMASK(13 bits, NIC-DMA'd) at:1965but immediately bounds it at:1973:total_len > XL_MAX_FRAMELEN(=ETHER_MAX_LEN + ETHER_VLAN_ENCAP_LEN= 1522,if_xlreg.h:89) setsUP_ERROR|OVERSIZEβ dropped at:1982. 1522 < the 2046-byte usable posted cluster (xl_newbuf :1883-1890). No underflow path (m_len = total_len β₯ 0). - MTU:
xl_ioctlhas no SIOCSIFMTU case βether_ioctlcaps atETHERMTU(sys/net/if_ethersubr.c:716), so no jumboMTU abuse. - TX ring winding: clean.
xl_txeof :2072(boomerang: DOWNLIST_PTR walk) andxl_txeof_90xB :2123(prod/cons walk) free each mbuf exactly once;xl_start_body :2530-2539only pops the free list after a successfulxl_encap;xl_encaperror frees the mbuf and leaves the ring untouched. - Priority queues: this port has none β
if_sndis a single ALTQ queue (noxl_tx_pqueueequivalent exists in the file). - MII/PHY glue:
xl_miibus_readreg :528bzeros the frame first and returnsframe.mii_dataβ no uninitialized memory reaches callers on MII ack failure (worst case a 0 value). - EEPROM reads: all
xl_read_eepromcall sites are compile-time bounded (eaddr[6]w/ cnt=3,devidw/ cnt=1,xcvr[2]w/ cnt=2,xl_capsw/ cnt=1) β no overflow.
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: xl_stats_update uses lwkt_serialize_try + 1-tick retry. Validated at protocol level on the guest with xl_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 3c90x NIC).
findings/poc/DF-3906/fix.diff; findings/poc/DF-3906/fix_validation.log
Confirmed kernel references
- sys/dev/netif/xl/if_xl.c:1320
- sys/dev/netif/xl/if_xl.c:2377
- sys/dev/netif/xl/if_xl.c:2429
- sys/dev/netif/xl/if_xl.c:2918
- sys/dev/netif/xl/if_xl.c:3142
- sys/dev/netif/xl/if_xl.c:3169
- sys/dev/netif/xl/if_xl.c:1573
- sys/dev/netif/xl/if_xl.c:2712
- sys/dev/netif/xl/if_xl.c:3212
- 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-3906/VERDICT.md; run.log (D2 costp stopper + B4 slize softclock 4); console_markers.txt (fired=2 ran=1 + terminal marker line from dfbsd-qemu/boot.log); build_shared.log (in DF-3903 pack; 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 xl_stats_update)
PoC changes
No seed PoC. Authored xl_callout_replica.ko / xl_callout_replica_fixed.ko (out-of-tree kmodules replicating the if_xl.c callout protocol, including the plain/non-MPSAFE callout_init at :1320) because the guest has no 3c90x hardware; the deadlock is in the generic protocol xl uses, so the replica exercises the identical kernel paths.
Verified recommended fix
Make xl_stats_update() 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_xl.c's exact callout/serializer protocol; xl-specific runtime hardware-gated (no 3Com 3c90x NIC on the audit guest) -> inconclusive per family convention. xl_stats_update (sys/dev/netif/xl/if_xl.c:2372) is the callback of a PLAIN (non-MPSAFE) callout (callout_init at :1320 -> callback executes under the Giant MP lock, kern_timeout.c:583) and does a BLOCKING lwkt_serialize_enter(ifp->if_serializer) at :2377, then self re-arms at :2429 inside the serialized body (armed from xl_init :2918, 1 Hz); xl_stop() tears the callout down with the synchronous callout_stop() at :3169 while holding that same serializer (ASSERT_SERIALIZED :3142; callers: xl_detach :1573, xl_ioctl :3065/:3062, xl_init :2712 via xl_ifmedia_upd :2955 and watchdog :3125, xl_shutdown :3212, xl_suspend :3222); DragonFly callout_stop synchronously waits for an INPROG callback (kern_timeout.c:1091 -> :859-932, ssleep 'costp' at :920). A stats tick dequeued and parked on the serializer therefore deadlocks against the stopper (AB-BA). Kernel-module replica on the guest: with the xl-shaped handler parked, callout_stop NEVER returned - stopper observed D2/wchan 'costp' (pid 1104), softclock 4 observed B4/wchan 'slize' holding the Giant MP lock (non-MPSAFE callout, aggravation shared with dc DF-3850 and sk DF-3903), 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). 10th member of the DF-3715/3738/3760/3776/3806/3850/3903 family, now live-proven 8 times across 8 drivers 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.