Skip to content

Commit c895e92

Browse files
articicepoljar
authored andcommitted
fix: manually drop room if it's last in Arc
1 parent 7c13b60 commit c895e92

5 files changed

Lines changed: 101 additions & 4 deletions

File tree

bindings/matrix-sdk-ffi/Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,8 +133,10 @@ once_cell = "1.21.4"
133133
jvm-getter = "0.1.0"
134134

135135
[dev-dependencies]
136+
matrix-sdk = { workspace = true, features = ["testing"] }
136137
similar-asserts.workspace = true
137138
tempfile.workspace = true
139+
tokio = { workspace = true, features = ["rt-multi-thread", "macros", "time"] }
138140

139141
[build-dependencies]
140142
uniffi = { workspace = true, features = ["build"] }

bindings/matrix-sdk-ffi/src/client.rs

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3367,4 +3367,51 @@ mod tests {
33673367
assert_eq!(token.matrix_server_name, "example.com");
33683368
assert_eq!(token.expires_in_seconds, 3_600);
33693369
}
3370+
3371+
/// Dropping an FFI [`Client`] on a non-tokio thread must not panic.
3372+
///
3373+
/// Regression test: `Client.inner` is wrapped in [`AsyncRuntimeDropped`]
3374+
/// precisely because dropping a sqlite-backed [`MatrixClient`] outside a
3375+
/// tokio runtime context causes `SyncWrapper<rusqlite::Connection>::drop`
3376+
/// to call `tokio::task::spawn_blocking`, which panics and triggers
3377+
/// SIGABRT. This test guards against accidentally removing that wrapper.
3378+
#[cfg(not(target_family = "wasm"))]
3379+
#[tokio::test]
3380+
async fn client_drop_on_non_tokio_thread_does_not_panic() {
3381+
use std::time::Duration;
3382+
3383+
use matrix_sdk::{Client, config::RequestConfig};
3384+
use matrix_sdk_common::cross_process_lock::CrossProcessLockConfig;
3385+
use tempfile::tempdir;
3386+
3387+
let dir = tempdir().unwrap();
3388+
3389+
// SingleProcess lock avoids the session-delegate requirement in
3390+
// `Client::new`, keeping the test self-contained.
3391+
let sdk_client = Client::builder()
3392+
.homeserver_url("https://example.com")
3393+
.request_config(RequestConfig::new().disable_retry())
3394+
.sqlite_store(dir.path(), None)
3395+
.cross_process_store_config(CrossProcessLockConfig::SingleProcess)
3396+
.build()
3397+
.await
3398+
.unwrap();
3399+
3400+
// Allow background init tasks to complete; after this the only strong
3401+
// reference to `ClientInner` is the local `sdk_client` variable.
3402+
tokio::time::sleep(Duration::from_secs(1)).await;
3403+
3404+
let ffi_client = super::Client::new(sdk_client.clone(), None, None).await.unwrap();
3405+
3406+
// Dropping `sdk_client` leaves `ffi_client` as the sole Arc holder of
3407+
// `ClientInner` — mirroring what happens when the last JS reference to
3408+
// a Client is garbage-collected on the Hermes JS thread.
3409+
drop(sdk_client);
3410+
3411+
// Simulate Hermes GC on the JS thread (a non-tokio thread).
3412+
// Without `AsyncRuntimeDropped` wrapping `Client.inner` this SIGABRT.
3413+
std::thread::spawn(move || drop(ffi_client))
3414+
.join()
3415+
.expect("Client::drop panicked on a non-tokio thread");
3416+
}
33703417
}

bindings/matrix-sdk-ffi/src/room/mod.rs

Lines changed: 50 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -98,13 +98,13 @@ impl From<RoomState> for Membership {
9898

9999
#[derive(uniffi::Object)]
100100
pub struct Room {
101-
pub(super) inner: SdkRoom,
101+
pub(super) inner: AsyncRuntimeDropped<SdkRoom>,
102102
utd_hook_manager: Option<Arc<UtdHookManager>>,
103103
}
104104

105105
impl Room {
106106
pub(crate) fn new(inner: SdkRoom, utd_hook_manager: Option<Arc<UtdHookManager>>) -> Self {
107-
Room { inner, utd_hook_manager }
107+
Room { inner: AsyncRuntimeDropped::new(inner), utd_hook_manager }
108108
}
109109
}
110110

@@ -1981,3 +1981,51 @@ impl TryFrom<SdkRoomSendQueueUpdate> for RoomSendQueueUpdate {
19811981
})
19821982
}
19831983
}
1984+
1985+
#[cfg(all(test, not(target_family = "wasm")))]
1986+
mod tests {
1987+
use std::time::Duration;
1988+
1989+
use matrix_sdk::{ruma::room_id, test_utils::mocks::MatrixMockServer};
1990+
use tempfile::tempdir;
1991+
1992+
use super::*;
1993+
1994+
/// Dropping an FFI [`Room`] on a non-tokio thread must not panic.
1995+
///
1996+
/// Regression test: when `Room.inner` was `SdkRoom` (not wrapped in
1997+
/// [`AsyncRuntimeDropped`]), garbage-collection of an orphaned `Room` on
1998+
/// the Hermes JS thread caused `SyncWrapper<rusqlite::Connection>::drop`
1999+
/// to call `tokio::task::spawn_blocking` from a thread with no tokio
2000+
/// runtime context. Rust turns a panic inside `Drop` into SIGABRT.
2001+
///
2002+
/// The fix wraps `inner` in [`AsyncRuntimeDropped`], which enters the
2003+
/// global tokio runtime context before running `SdkRoom`'s destructor.
2004+
#[tokio::test]
2005+
async fn room_drop_on_non_tokio_thread_does_not_panic() {
2006+
let server = MatrixMockServer::new().await;
2007+
let dir = tempdir().unwrap();
2008+
2009+
let client =
2010+
server.client_builder().on_builder(|b| b.sqlite_store(dir.path(), None)).build().await;
2011+
2012+
// Allow background init tasks to complete; after this the only strong
2013+
// reference to `ClientInner` is the local `client` variable.
2014+
tokio::time::sleep(Duration::from_secs(1)).await;
2015+
2016+
let room_id = room_id!("!test:example.com");
2017+
let sdk_room = server.sync_joined_room(&client, room_id).await;
2018+
let ffi_room = Room::new(sdk_room, None);
2019+
2020+
// Dropping `client` makes `ffi_room` the sole Arc holder of
2021+
// `ClientInner`, mirroring the situation where the FFI client has
2022+
// already been released and GC finalises the last room JS object.
2023+
drop(client);
2024+
2025+
// Simulate Hermes GC on the JS thread (a non-tokio thread).
2026+
// Without the `AsyncRuntimeDropped` wrapper this causes a SIGABRT.
2027+
std::thread::spawn(move || drop(ffi_room))
2028+
.join()
2029+
.expect("Room::drop panicked on a non-tokio thread");
2030+
}
2031+
}

bindings/matrix-sdk-ffi/src/search.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ impl Room {
5454
/// a time.
5555
pub fn search_messages(&self, query: String, num_results_per_batch: u32) -> RoomSearchIterator {
5656
RoomSearchIterator {
57-
sdk_room: self.inner.clone(),
57+
sdk_room: (*self.inner).clone(),
5858
inner: Mutex::new(self.inner.search_messages(query, num_results_per_batch as usize)),
5959
}
6060
}

bindings/matrix-sdk-ffi/src/widget.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ impl WidgetDriver {
5555
};
5656

5757
let capabilities_provider = CapabilitiesProviderWrap(capabilities_provider.into());
58-
if let Err(()) = driver.run(room.inner.clone(), capabilities_provider).await {
58+
if let Err(()) = driver.run((*room.inner).clone(), capabilities_provider).await {
5959
// TODO
6060
}
6161
}

0 commit comments

Comments
 (0)