From 4cdffb9564c58d2a35c1b1d4223872ae684bc646 Mon Sep 17 00:00:00 2001 From: LastExceed Date: Mon, 17 Aug 2026 18:40:07 +0200 Subject: [PATCH 1/6] change drop order --- asio-sys/src/bindings/mod.rs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/asio-sys/src/bindings/mod.rs b/asio-sys/src/bindings/mod.rs index cd6947531..02dc0819b 100644 --- a/asio-sys/src/bindings/mod.rs +++ b/asio-sys/src/bindings/mod.rs @@ -901,7 +901,14 @@ impl Driver { /// Remove the callback with the given ID. pub fn remove_callback(&self, rem_id: BufferCallbackId) { let mut bc = BUFFER_CALLBACK.lock().unwrap(); - bc.retain(|&(id, _)| id != rem_id); + let pos = bc + .iter() + .position(|&(id, _)| id == rem_id) + .expect("rem_id should be valid"); + let _removed = bc.swap_remove(pos); + // the lock must be dropped first, as the removed callback could + // be owning another stream, which would result in a deadlock + drop(bc); } /// Consumes and destroys the `Driver`, stopping the streams if they are running and releasing From 2c8595ee6653692fbb3598cc04f123ba4ece519b Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Mon, 17 Aug 2026 21:49:51 +0200 Subject: [PATCH 2/6] fix(asio): use remove instead of swap_remove in remove_callback swap_remove reorders the vec, breaking add_callback's bc.last().id + 1 scheme and causing duplicate BufferCallbackIds. remove preserves order while still dropping the removed callback after the lock is released. --- asio-sys/src/bindings/mod.rs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/asio-sys/src/bindings/mod.rs b/asio-sys/src/bindings/mod.rs index 02dc0819b..633cb999d 100644 --- a/asio-sys/src/bindings/mod.rs +++ b/asio-sys/src/bindings/mod.rs @@ -901,14 +901,16 @@ impl Driver { /// Remove the callback with the given ID. pub fn remove_callback(&self, rem_id: BufferCallbackId) { let mut bc = BUFFER_CALLBACK.lock().unwrap(); - let pos = bc + // `remove` (not `swap_remove`) to preserve the insertion order that + // `add_callback` relies on for generating the next ID. + let removed = bc .iter() .position(|&(id, _)| id == rem_id) - .expect("rem_id should be valid"); - let _removed = bc.swap_remove(pos); + .map(|pos| bc.remove(pos)); // the lock must be dropped first, as the removed callback could // be owning another stream, which would result in a deadlock drop(bc); + drop(removed); } /// Consumes and destroys the `Driver`, stopping the streams if they are running and releasing From 9556bb1f47b10cac0ed923b88d06db9dd5629273 Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Mon, 17 Aug 2026 22:15:55 +0200 Subject: [PATCH 3/6] fix(asio): drop cleared stream callbacks after releasing BUFFER_CALLBACK bcs.clear() dropped every registered callback in place while still holding the lock. If a callback owned another stream, dropping it here would reenter remove_callback and deadlock on the same lock. --- asio-sys/src/bindings/mod.rs | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/asio-sys/src/bindings/mod.rs b/asio-sys/src/bindings/mod.rs index 633cb999d..5b4a006d8 100644 --- a/asio-sys/src/bindings/mod.rs +++ b/asio-sys/src/bindings/mod.rs @@ -1028,10 +1028,14 @@ impl DriverInner { let mut state = self.lock_state(); state.destroy()?; - // Clear any existing stream callbacks. - if let Ok(mut bcs) = BUFFER_CALLBACK.lock() { - bcs.clear(); - } + // Clear any existing stream callbacks. Take the callbacks out and drop the + // lock before dropping them, as a callback could be owning another stream, + // which would result in a deadlock. + let cleared = BUFFER_CALLBACK + .lock() + .ok() + .map(|mut bcs| std::mem::take(&mut *bcs)); + drop(cleared); } // Signal that the driver has been destroyed. From 38e54f661ddb54cd3ce4f59a151279f2d4a97407 Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Mon, 17 Aug 2026 22:21:35 +0200 Subject: [PATCH 4/6] fix(asio): serialize driver teardown against a concurrent load_driver Weak::upgrade() in load_driver returns None as soon as the old DriverInner's Arc strong count hits zero, which happens before DriverInner::drop (and therefore ASIOExit) has finished running. A second thread could then start ASIOInit before the old driver's ASIOExit had returned. Share loaded_driver's lock with DriverInner so destroy_inner holds it across ASIOExit, same as load_driver already does across ASIOInit. --- asio-sys/src/bindings/mod.rs | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/asio-sys/src/bindings/mod.rs b/asio-sys/src/bindings/mod.rs index 5b4a006d8..9f47babdd 100644 --- a/asio-sys/src/bindings/mod.rs +++ b/asio-sys/src/bindings/mod.rs @@ -31,10 +31,8 @@ use self::asio_import as ai; /// There should only be one instance of this type at any point in time. #[derive(Debug, Default)] pub struct Asio { - // Keeps track of whether or not a driver is already loaded. - // - // This is necessary as ASIO only supports one `Driver` at a time. - loaded_driver: Mutex>, + // Guards driver load and teardown. ASIO only supports one driver at a time. + loaded_driver: Arc>>, } /// A handle to a single ASIO driver. @@ -74,6 +72,9 @@ struct DriverInner { // In the case that the driver has been manually destroyed this flag will be set to `true` // indicating to the `drop` implementation that there is nothing to be done. destroyed: bool, + // Shared with the owning `Asio`; locked during teardown so a concurrent `load_driver` + // can't start `ASIOInit` before this driver's `ASIOExit` finishes. + loaded_driver: Arc>>, } /// All possible states of an ASIO `Driver` instance. @@ -486,6 +487,7 @@ impl Asio { state, streams, destroyed, + loaded_driver: self.loaded_driver.clone(), }); *loaded = Arc::downgrade(&inner); let driver = Driver { inner }; @@ -1025,6 +1027,13 @@ impl DriverInner { fn destroy_inner(&mut self) -> Result<(), AsioError> { { + // Held so a concurrent `load_driver` can't start ASIOInit before this + // driver's ASIOExit finishes. + let _loaded_driver_guard = self + .loaded_driver + .lock() + .expect("failed to acquire loaded driver lock"); + let mut state = self.lock_state(); state.destroy()?; From 6f982252291547247d942ebb3d27766476443757 Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Mon, 17 Aug 2026 22:37:24 +0200 Subject: [PATCH 5/6] fix(asio): avoid deadlock when a callback drops a Stream mid-run buffer_switch_time_info held BUFFER_CALLBACK across running every registered callback. If a callback synchronously dropped a Stream it owned, that Stream's Drop would call remove_callback and try to re-lock BUFFER_CALLBACK on the same thread, deadlocking on the ASIO real-time callback thread. Track whether the current thread is inside buffer_switch_time_info via a thread-local flag; remove_callback checks it and, if set, defers the removal into a thread-local queue instead of blocking on the lock. buffer_switch_time_info drains that queue after running callbacks, still under the lock, before releasing it. --- asio-sys/src/bindings/mod.rs | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/asio-sys/src/bindings/mod.rs b/asio-sys/src/bindings/mod.rs index 9f47babdd..f20d04f7e 100644 --- a/asio-sys/src/bindings/mod.rs +++ b/asio-sys/src/bindings/mod.rs @@ -7,6 +7,7 @@ pub mod errors; #[cfg(target_os = "windows")] use std::os::raw::c_long; use std::{ + cell::{Cell, RefCell}, ffi::{CStr, CString}, os::raw::{c_char, c_double, c_void}, ptr::null_mut, @@ -367,6 +368,14 @@ pub struct BufferCallbackId(usize); /// parameters. static BUFFER_CALLBACK: Mutex> = Mutex::new(Vec::new()); +thread_local! { + // Set while buffer_switch_time_info is running callbacks on this thread. + static RUNNING_CALLBACKS: Cell = const { Cell::new(false) }; + // Removals deferred because remove_callback was called reentrantly (a callback dropped a + // Stream it owns) while RUNNING_CALLBACKS was set, to avoid deadlocking on BUFFER_CALLBACK. + static PENDING_REMOVALS: RefCell> = const { RefCell::new(Vec::new()) }; +} + /// Used to identify when to clear buffers. static CALLBACK_FLAG: AtomicU32 = AtomicU32::new(0); @@ -902,6 +911,14 @@ impl Driver { /// Remove the callback with the given ID. pub fn remove_callback(&self, rem_id: BufferCallbackId) { + if RUNNING_CALLBACKS.with(Cell::get) { + // Reentrant call from within buffer_switch_time_info on this thread (a callback + // dropped a Stream it owns). BUFFER_CALLBACK is already locked by this thread, so + // defer the removal until buffer_switch_time_info returns. + PENDING_REMOVALS.with_borrow_mut(|pending| pending.push(rem_id)); + return; + } + let mut bc = BUFFER_CALLBACK.lock().unwrap(); // `remove` (not `swap_remove`) to preserve the insertion order that // `add_callback` relies on for generating the next ID. @@ -1258,9 +1275,25 @@ extern "C" fn buffer_switch_time_info( system_time: asio_timestamp_to_nanos(asio_time.time_info.system_time), callback_flag, }; + RUNNING_CALLBACKS.with(|flag| flag.set(true)); for &mut (_, ref mut bc) in bcs.iter_mut() { bc.run(&callback_info); } + RUNNING_CALLBACKS.with(|flag| flag.set(false)); + + // Apply any removals a callback above deferred by dropping a Stream it owns mid-run. + let removed = PENDING_REMOVALS.with_borrow_mut(|pending| { + pending + .drain(..) + .filter_map(|id| { + bcs.iter() + .position(|&(bc_id, _)| bc_id == id) + .map(|pos| bcs.remove(pos)) + }) + .collect::>() + }); + drop(bcs); + drop(removed); if CALL_OUTPUT_READY.load(Ordering::Acquire) { unsafe { ai::ASIOOutputReady() }; From a31bbe6dc56d6090f2e6f7c0bd0cbb760b5b7e91 Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Mon, 17 Aug 2026 23:13:47 +0200 Subject: [PATCH 6/6] docs: add CHANGELOG entries for ASIO nested-stream deadlock and driver-load race fixes --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index f9e729df9..70c1a2960 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -35,6 +35,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed - **ALSA**: Fix a remaining timestamp segfault on 32-bit platforms with a 64-bit kernel `time_t`. +- **ASIO**: Fix a deadlock when dropping a `Stream` that owns another ASIO `Stream`. +- **ASIO**: Fix loading a driver while a previous driver was still unloading. - **WASAPI**: Device enumeration no longer panics if the COM enumerator fails to initialize. ## [0.18.2] - 2026-08-16