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 diff --git a/asio-sys/src/bindings/mod.rs b/asio-sys/src/bindings/mod.rs index cd6947531..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, @@ -31,10 +32,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 +73,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. @@ -366,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); @@ -486,6 +496,7 @@ impl Asio { state, streams, destroyed, + loaded_driver: self.loaded_driver.clone(), }); *loaded = Arc::downgrade(&inner); let driver = Driver { inner }; @@ -900,8 +911,25 @@ 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(); - bc.retain(|&(id, _)| id != rem_id); + // `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) + .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 @@ -1016,13 +1044,24 @@ 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()?; - // 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. @@ -1236,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() };