From 124d7ae6cc8e3f110be36dc54f18572d3b38dfa4 Mon Sep 17 00:00:00 2001 From: t8y2 Date: Thu, 24 Sep 2026 18:25:31 +0800 Subject: [PATCH] fix(core): isolate Linux keyring access from async runtimes --- .../dbx-core/src/persistence/secret_codec.rs | 101 +++++++++++++++++- 1 file changed, 100 insertions(+), 1 deletion(-) diff --git a/crates/dbx-core/src/persistence/secret_codec.rs b/crates/dbx-core/src/persistence/secret_codec.rs index c0cc85d038..bca8037540 100644 --- a/crates/dbx-core/src/persistence/secret_codec.rs +++ b/crates/dbx-core/src/persistence/secret_codec.rs @@ -319,6 +319,23 @@ fn platform_keyring_codec(allow_create: bool) -> Result, Str #[cfg(all(feature = "os-keyring", target_os = "linux"))] fn platform_keyring_codec(allow_create: bool) -> Result, String> { + run_secret_service_operation(move || secret_service_keyring_codec(allow_create)) +} + +#[cfg(any(test, all(feature = "os-keyring", target_os = "linux")))] +fn run_secret_service_operation(operation: F) -> Result, String> +where + F: FnOnce() -> Result, String> + Send + 'static, +{ + let worker = std::thread::Builder::new() + .name("dbx-secret-service".to_string()) + .spawn(operation) + .map_err(|_| "KEYRING_ACCESS_FAILED: secret service worker unavailable".to_string())?; + worker.join().map_err(|_| "KEYRING_ACCESS_FAILED: secret service worker failed".to_string())? +} + +#[cfg(all(feature = "os-keyring", target_os = "linux"))] +fn secret_service_keyring_codec(allow_create: bool) -> Result, String> { use secret_service::{blocking::SecretService, EncryptionType}; use std::collections::HashMap; @@ -494,7 +511,7 @@ fn aad(namespace: &str, key: &str) -> String { #[cfg(test)] mod tests { - use super::{managed_key_path, read_key_file_with_retry, SecretCodec, SecretKeyPolicy, SecretKeySource}; + use super::{managed_key_path, read_key_file_with_retry, run_secret_service_operation, SecretCodec, SecretKeyPolicy, SecretKeySource}; use base64::Engine as _; use std::sync::{Mutex, OnceLock}; @@ -503,6 +520,88 @@ mod tests { LOCK.get_or_init(|| Mutex::new(())) } + fn assert_secret_service_runtime_isolated() { + let directory = tempfile::tempdir().unwrap(); + let path = directory.path().join("secret.key"); + let original = SecretCodec::new([7u8; 32]); + let envelope = original.encrypt("connection", "password", "existing secret").unwrap(); + let resolved = SecretCodec::resolve_platform_default(Some(path.clone()), true, |_| { + run_secret_service_operation(|| { + let runtime = tokio::runtime::Builder::new_current_thread().enable_all().build().unwrap(); + runtime.block_on(async { Ok(Some(SecretCodec::new([7u8; 32]))) }) + }) + }) + .unwrap(); + assert_eq!(resolved.source, SecretKeySource::PlatformStore); + assert_eq!(resolved.codec.decrypt("connection", "password", &envelope).unwrap(), "existing secret"); + assert!(!path.exists()); + } + + #[test] + fn secret_service_worker_supports_synchronous_callers() { + assert_secret_service_runtime_isolated(); + } + + #[tokio::test] + async fn secret_service_worker_isolates_current_thread_runtime() { + assert_secret_service_runtime_isolated(); + } + + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] + async fn secret_service_worker_isolates_multi_thread_runtime() { + assert_secret_service_runtime_isolated(); + } + + #[tokio::test] + async fn secret_service_worker_preserves_missing_provider() { + assert!(matches!(run_secret_service_operation(|| Ok(None)), Ok(None))); + } + + #[tokio::test] + async fn secret_service_worker_preserves_access_errors_without_creating_a_fallback() { + let directory = tempfile::tempdir().unwrap(); + let path = directory.path().join("secret.key"); + let result = SecretCodec::resolve_platform_default(Some(path.clone()), true, |_| { + run_secret_service_operation(|| Err("KEYRING_ACCESS_FAILED: locked collection".to_string())) + }); + assert!(matches!(result, Err(error) if error == "KEYRING_ACCESS_FAILED: locked collection")); + assert!(!path.exists()); + } + + #[tokio::test] + async fn secret_service_worker_panics_do_not_create_a_fallback() { + let directory = tempfile::tempdir().unwrap(); + let path = directory.path().join("secret.key"); + let result = SecretCodec::resolve_platform_default(Some(path.clone()), true, |_| { + run_secret_service_operation(|| panic!("secret service failed")) + }); + assert!(matches!(result, Err(error) if error == "KEYRING_ACCESS_FAILED: secret service worker failed")); + assert!(!path.exists()); + } + + #[cfg(all(feature = "os-keyring", target_os = "linux"))] + #[test] + fn linux_keyring_missing_bus_is_safe_for_sync_and_async_callers() { + let _guard = env_lock().lock().unwrap(); + let directory = tempfile::tempdir().unwrap(); + let previous_bus = std::env::var_os("DBUS_SESSION_BUS_ADDRESS"); + std::env::set_var( + "DBUS_SESSION_BUS_ADDRESS", + format!("unix:path={}", directory.path().join("missing-bus").display()), + ); + let mut results = vec![super::platform_keyring_codec(false)]; + for runtime in [ + tokio::runtime::Builder::new_current_thread().enable_all().build().unwrap(), + tokio::runtime::Builder::new_multi_thread().worker_threads(2).enable_all().build().unwrap(), + ] { + results.push(runtime.block_on(async { super::platform_keyring_codec(false) })); + } + restore_env("DBUS_SESSION_BUS_ADDRESS", previous_bus); + for result in results { + assert!(matches!(result, Ok(None))); + } + } + #[test] fn roundtrip_binds_namespace_and_key() { let codec = SecretCodec::new([7u8; 32]);