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

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-3906 Β· 13 files
FileTypeDescriptionSize
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.

VERDICT.md
↓ download raw

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)

  1. 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()).
  2. sys/dev/netif/xl/if_xl.c:2372-2380 β€” xl_stats_update() does a blocking lwkt_serialize_enter(sc->arpcom.ac_if.if_serializer) at :2377 (sys/kern/lwkt_serialize.c:105 β†’ tsleep, wchan slize), then runs xl_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 by xl_stats_no_timeout, which is 0 in the normal 1 Hz tick context; armed from xl_init :2918 on every interface init).
  3. sys/dev/netif/xl/if_xl.c:3136-3169 β€” xl_stop() asserts it holds that serializer (:3142) and then calls the synchronous callout_stop(&sc->xl_stat_timer) at :3169. Every xl_stop() caller holds it: xl_detach :1573, xl_ioctl SIOCSIFFLAGS-down :3065, xl_init :2712 (SIOCSIFFLAGS-up at :3062, media change via xl_ifmedia_upd :2955 β†’ xl_init, watchdog :3125 via xl_init), xl_shutdown :3212, xl_suspend :3222.
  4. sys/kern/kern_timeout.c:1091 β†’ :859-932 β€” callout_stop() is synchronous: when the callout is INPROG (:567), the caller ssleep(c, &c->spin, 0, "costp", 0) (:920) until the callback completes.
  5. β‡’ An xl_stats_update dequeued and parked in lwkt_serialize_enter() on the serializer held by the xl_stop caller 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 since xl_stat_timer is 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: plain callout_init (non-MPSAFE), tick callback doing blocking lwkt_serialize_enter + self re-arm inside the serialized body, stop sysctl emulating xl_stop (serializer held across the synchronous callout_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_stop never returned.
  • Wedged-thread evidence from a second session: stopper D2 costp sysctl kern.xlreplica_stop=1 (pid 1104; costp is kern_timeout.c:920), and (softclock 4) in B4 slize β€” the parked non-MPSAFE callback holding the Giant MP lock.
  • The wedge survived a clean shutdown attempt β€” vm.sh down fell 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.diff protocol (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) reads total_len = rxstat & XL_RXSTAT_LENMASK (13 bits, NIC-DMA'd) at :1965 but immediately bounds it at :1973: total_len > XL_MAX_FRAMELEN (= ETHER_MAX_LEN + ETHER_VLAN_ENCAP_LEN = 1522, if_xlreg.h:89) sets UP_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_ioctl has no SIOCSIFMTU case β†’ ether_ioctl caps at ETHERMTU (sys/net/if_ethersubr.c:716), so no jumboMTU abuse.
  • TX ring winding: clean. xl_txeof :2072 (boomerang: DOWNLIST_PTR walk) and xl_txeof_90xB :2123 (prod/cons walk) free each mbuf exactly once; xl_start_body :2530-2539 only pops the free list after a successful xl_encap; xl_encap error frees the mbuf and leaves the ring untouched.
  • Priority queues: this port has none β€” if_snd is a single ALTQ queue (no xl_tx_pqueue equivalent exists in the file).
  • MII/PHY glue: xl_miibus_readreg :528 bzeros the frame first and returns frame.mii_data β€” no uninitialized memory reaches callers on MII ack failure (worst case a 0 value).
  • EEPROM reads: all xl_read_eeprom call sites are compile-time bounded (eaddr[6] w/ cnt=3, devid w/ cnt=1, xcvr[2] w/ cnt=2, xl_caps w/ 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

inconclusive
baseline no→ patch + rebuild →patched clean

fix.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
↓ fix.diffper-fix-DF-3906

Confirmed kernel references

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.