From b334da68768af95a548bdffe8cd4ea32e969a96f Mon Sep 17 00:00:00 2001 From: xdustinface Date: Sun, 22 Mar 2026 15:18:50 +0700 Subject: [PATCH 1/3] fix: replace `abort()` with cooperative wait in `wait_for_run_task` `abort()` can interrupt the cleanup sequence in `DashSpvClient::run()` (the `monitor_shutdown.cancel()` + `tokio::join!`), leaving monitor tasks running after FFI callback pointers are freed. Use cooperative wait with a timeout fallback instead. --- dash-spv-ffi/src/client.rs | 30 +++++++++++++++++++++--------- 1 file changed, 21 insertions(+), 9 deletions(-) diff --git a/dash-spv-ffi/src/client.rs b/dash-spv-ffi/src/client.rs index b588a5bb7..8f18dcc0a 100644 --- a/dash-spv-ffi/src/client.rs +++ b/dash-spv-ffi/src/client.rs @@ -110,15 +110,27 @@ pub unsafe extern "C" fn dash_spv_ffi_client_new( } } +/// Maximum time to wait for the run task to exit cooperatively before aborting. +const RUN_TASK_SHUTDOWN_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(5); + impl FFIDashSpvClient { - /// Cancel the run task and wait for it to finish. - fn cancel_run_task(&self) { + /// Wait for the run task to finish cooperatively, aborting only on timeout. + /// + /// The caller must cancel `shutdown_token` before calling this so that + /// `DashSpvClient::run()` exits its loop and cleans up monitor tasks. + /// Only falls back to `abort()` if the task doesn't exit within the timeout. + fn wait_for_run_task(&self) { let task = self.run_task.lock().unwrap().take(); if let Some(task) = task { - task.abort(); - self.runtime.block_on(async { - let _ = task.await; - }); + let finished = self + .runtime + .block_on(async { tokio::time::timeout(RUN_TASK_SHUTDOWN_TIMEOUT, task).await }); + if finished.is_err() { + tracing::warn!( + "Run task did not exit within {:?}, aborting", + RUN_TASK_SHUTDOWN_TIMEOUT + ); + } } } } @@ -126,7 +138,7 @@ impl FFIDashSpvClient { fn stop_client_internal(client: &mut FFIDashSpvClient) -> Result<(), dash_spv::SpvError> { client.shutdown_token.cancel(); - client.cancel_run_task(); + client.wait_for_run_task(); let result = client.runtime.block_on(async { client.inner.stop().await }); @@ -342,8 +354,8 @@ pub unsafe extern "C" fn dash_spv_ffi_client_destroy(client: *mut FFIDashSpvClie let _ = client.inner.stop().await; }); - // Abort and await the run task - client.cancel_run_task(); + // Wait for the run task to finish (cooperative, with timeout fallback) + client.wait_for_run_task(); tracing::info!("FFI client destroyed and all tasks cleaned up"); } From 32922746435bec6741e36b1e86306c160d12396e Mon Sep 17 00:00:00 2001 From: xdustinface Date: Tue, 31 Mar 2026 12:04:48 +1100 Subject: [PATCH 2/3] fix: abort run task on shutdown timeout in `wait_for_run_task` Addresses CodeRabbit review comment on PR #576 https://github.com/dashpay/rust-dashcore/pull/576#discussion_r3012886117 --- dash-spv-ffi/src/client.rs | 24 +++++++++++++++--------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/dash-spv-ffi/src/client.rs b/dash-spv-ffi/src/client.rs index 8f18dcc0a..83d6535a9 100644 --- a/dash-spv-ffi/src/client.rs +++ b/dash-spv-ffi/src/client.rs @@ -121,15 +121,21 @@ impl FFIDashSpvClient { /// Only falls back to `abort()` if the task doesn't exit within the timeout. fn wait_for_run_task(&self) { let task = self.run_task.lock().unwrap().take(); - if let Some(task) = task { - let finished = self - .runtime - .block_on(async { tokio::time::timeout(RUN_TASK_SHUTDOWN_TIMEOUT, task).await }); - if finished.is_err() { - tracing::warn!( - "Run task did not exit within {:?}, aborting", - RUN_TASK_SHUTDOWN_TIMEOUT - ); + if let Some(mut task) = task { + let finished = self.runtime.block_on(async { + tokio::time::timeout(RUN_TASK_SHUTDOWN_TIMEOUT, &mut task).await + }); + match finished { + Ok(Ok(())) => {} + Ok(Err(e)) => tracing::warn!("Run task exited with join error: {}", e), + Err(_) => { + tracing::warn!( + "Run task did not exit within {:?}, aborting", + RUN_TASK_SHUTDOWN_TIMEOUT, + ); + task.abort(); + let _ = self.runtime.block_on(task); + } } } } From 8be09a371effa9384100dd22bf9e07c2d20b6ffa Mon Sep 17 00:00:00 2001 From: xdustinface Date: Tue, 31 Mar 2026 13:53:47 +1100 Subject: [PATCH 3/3] fix: reorder shutdown sequence in `dash_spv_ffi_client_destroy` Wait for the run task before calling `stop()` so `run()` can finish its own cleanup. Matches the ordering in `stop_client_internal`. --- dash-spv-ffi/src/client.rs | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/dash-spv-ffi/src/client.rs b/dash-spv-ffi/src/client.rs index 83d6535a9..b9fab5e09 100644 --- a/dash-spv-ffi/src/client.rs +++ b/dash-spv-ffi/src/client.rs @@ -352,17 +352,18 @@ pub unsafe extern "C" fn dash_spv_ffi_client_destroy(client: *mut FFIDashSpvClie if !client.is_null() { let client = Box::from_raw(client); - // Cancel shutdown token to stop all tasks + // Cancel shutdown token so run() exits its loop and cleans up client.shutdown_token.cancel(); - // Stop the SPV client + // Wait for the run task to finish (cooperative, with timeout fallback) + client.wait_for_run_task(); + + // Stop the SPV client (run() calls stop() internally, but this + // handles the case where run() was never called or was aborted) client.runtime.block_on(async { let _ = client.inner.stop().await; }); - // Wait for the run task to finish (cooperative, with timeout fallback) - client.wait_for_run_task(); - tracing::info!("FFI client destroyed and all tasks cleaned up"); } }