Skip to content

Commit 352b7d9

Browse files
committed
fix: manually drop room if it's last in Arc
1 parent 99cfce8 commit 352b7d9

4 files changed

Lines changed: 101 additions & 3 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: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3281,4 +3281,52 @@ mod tests {
32813281
assert_eq!(token.matrix_server_name, "example.com");
32823282
assert_eq!(token.expires_in_seconds, 3_600);
32833283
}
3284+
3285+
/// Dropping an FFI [`Client`] on a non-tokio thread must not panic.
3286+
///
3287+
/// Regression test: `Client.inner` is wrapped in [`AsyncRuntimeDropped`]
3288+
/// precisely because dropping a sqlite-backed [`MatrixClient`] outside a
3289+
/// tokio runtime context causes `SyncWrapper<rusqlite::Connection>::drop`
3290+
/// to call `tokio::task::spawn_blocking`, which panics and triggers
3291+
/// SIGABRT. This test guards against accidentally removing that wrapper.
3292+
#[cfg(not(target_family = "wasm"))]
3293+
#[tokio::test]
3294+
async fn client_drop_on_non_tokio_thread_does_not_panic() {
3295+
use std::time::Duration;
3296+
3297+
use matrix_sdk::Client;
3298+
use matrix_sdk::config::RequestConfig;
3299+
use matrix_sdk_common::cross_process_lock::CrossProcessLockConfig;
3300+
use tempfile::tempdir;
3301+
3302+
let dir = tempdir().unwrap();
3303+
3304+
// SingleProcess lock avoids the session-delegate requirement in
3305+
// `Client::new`, keeping the test self-contained.
3306+
let sdk_client = Client::builder()
3307+
.homeserver_url("https://example.com")
3308+
.request_config(RequestConfig::new().disable_retry())
3309+
.sqlite_store(dir.path(), None)
3310+
.cross_process_store_config(CrossProcessLockConfig::SingleProcess)
3311+
.build()
3312+
.await
3313+
.unwrap();
3314+
3315+
// Allow background init tasks to complete; after this the only strong
3316+
// reference to `ClientInner` is the local `sdk_client` variable.
3317+
tokio::time::sleep(Duration::from_secs(1)).await;
3318+
3319+
let ffi_client = super::Client::new(sdk_client.clone(), None, None).await.unwrap();
3320+
3321+
// Dropping `sdk_client` leaves `ffi_client` as the sole Arc holder of
3322+
// `ClientInner` — mirroring what happens when the last JS reference to
3323+
// a Client is garbage-collected on the Hermes JS thread.
3324+
drop(sdk_client);
3325+
3326+
// Simulate Hermes GC on the JS thread (a non-tokio thread).
3327+
// Without `AsyncRuntimeDropped` wrapping `Client.inner` this SIGABRT.
3328+
std::thread::spawn(move || drop(ffi_client))
3329+
.join()
3330+
.expect("Client::drop panicked on a non-tokio thread");
3331+
}
32843332
}

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/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)