From 947f036456ae8959316d2ae86425c7d74a311051 Mon Sep 17 00:00:00 2001 From: Luc Fauvel Date: Wed, 23 Sep 2026 17:35:24 -0400 Subject: [PATCH] daemon: Cancel request when the UI fails to launch When launching the UI failed, the flow controller returned an error without cancelling the request it had initialized. The request context was never cleared, so every subsequent request was rejected as "Already a request in progress" until the daemon was restarted. Co-Authored-By: Claude Opus 5.5 (1M context) --- CHANGELOG.md | 1 + credentialsd/src/dbus/flow_control.rs | 123 +++++++++++++++++++++++++- 2 files changed, 123 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index dbce1c2..2a67a93 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ ## Improvements - daemon: Allow Firefox to contact daemon on more distros with different app IDs (Thank you, @michaelbeaumont!) +- daemon: Fix all subsequent requests being rejected after the UI failed to launch. - ui: Add Bulgarian translation (Thank you, @salif!) - webext: Try to read app ID from associated desktop entry (Thank you, @michaelbeaumont!) diff --git a/credentialsd/src/dbus/flow_control.rs b/credentialsd/src/dbus/flow_control.rs index 63cb299..cbdc9c5 100644 --- a/credentialsd/src/dbus/flow_control.rs +++ b/credentialsd/src/dbus/flow_control.rs @@ -132,11 +132,16 @@ async fn handle rx, Err(err) => { tracing::error!("Failed to launch UI for credentials: {err}. Cancelling request."); - return Err(CredentialServiceError::Internal(err.to_string())); + // Release the request slot, otherwise all subsequent requests + // are rejected as "already in progress". + svc.lock().await.cancel_request(request_id).await; + return Err(CredentialServiceError::Internal(err)); } }; tokio::spawn(async move { @@ -355,3 +360,119 @@ impl CredentialRequestController for CredentialRequestControllerClient { }) } } + +#[cfg(test)] +mod tests { + use std::error::Error; + use std::pin::Pin; + + use credentialsd_common::model::{Device, Operation}; + use tokio_util::sync::CancellationToken; + + use super::*; + use crate::credential_service::RequestId; + + const TEST_REQUEST_ID: RequestId = 42; + + #[derive(Debug, Default)] + struct MockDeviceManager { + cancelled: Mutex>, + } + + #[async_trait] + impl ManageDevice for MockDeviceManager { + async fn init_request( + &self, + _request: &CredentialRequest, + _tx: oneshot::Sender>, + ) -> Result<(RequestId, CancellationToken), CredentialServiceError> { + Ok((TEST_REQUEST_ID, CancellationToken::new())) + } + + async fn cancel_request(&self, request_id: RequestId) { + self.cancelled.lock().unwrap().push(request_id); + } + + async fn get_available_public_key_devices(&self) -> Result, ()> { + Ok(Vec::new()) + } + + async fn start_discovery( + &self, + ) -> Pin + Send + 'static>> { + Box::pin(futures_lite::stream::empty()) + } + } + + #[derive(Debug)] + struct FailingUiController; + + impl UiController for FailingUiController { + async fn create_session( + &self, + _session_handle: OwnedObjectPath, + _parent_window: Option, + _origin: String, + _type: Operation, + _devices: Vec, + _app_id: String, + _app_pid: u32, + _options: PortalBackendOptions, + ) -> Result> { + Err("UI not available".into()) + } + } + + async fn create_test_request() -> CredentialRequest { + use libwebauthn::ops::webauthn::{ + MakeCredentialRequest, OriginValidation, RequestSettings, idl::origin::RequestOrigin, + }; + + let request_json = r#" + { + "rp": {"id": "example.com", "name": "Example Relying Party"}, + "user": { + "id": "MTIzNDU2NzgxMjM0NTY3ODEyMzQ1Njc4MTIzNDU2Nzg", + "name": "test@example.com", + "displayName": "Test User" + }, + "challenge": "MTIzNDU2NzgxMjM0NTY3ODEyMzQ1Njc4MTIzNDU2Nzg", + "pubKeyCredParams": [{"type": "public-key", "alg": -7}] + } + "#; + let request_origin: RequestOrigin = + "https://example.com".try_into().expect("Invalid origin"); + let settings = RequestSettings { + origin: OriginValidation::Trust, + }; + let request = MakeCredentialRequest::prepare(&request_origin, request_json, &settings) + .await + .expect("Failed to parse request JSON"); + CredentialRequest::CreatePublicKeyCredentialRequest(request) + } + + #[tokio::test] + async fn test_ui_launch_failure_cancels_request() { + let svc = Arc::new(AsyncMutex::new(MockDeviceManager::default())); + let app = ClientDetails { + app_id: "org.example.App".to_string(), + pid: 1, + }; + + let result = handle( + svc.clone(), + FailingUiController, + create_test_request().await, + app, + None, + None, + ) + .await; + + assert!(result.is_err()); + assert_eq!( + *svc.lock().await.cancelled.lock().unwrap(), + vec![TEST_REQUEST_ID] + ); + } +}