What changed, and why it matters
This commit fixes a concurrency bug in the BitBox02 hardware wallet's USB handling. When a U2F (two-factor authentication) workflow is still running on the device's screen, a new hardware wallet request could previously start and reset or cancel the U2F workflow, potentially corrupting its user-interface state. The fix makes new hardware wallet requests return 'busy' while any U2F workflow is active, and tracks U2F workflow lifetimes independently of their public result state.
Treat this as a security-hardening fix with denial-of-service and potential UI-state integrity implications. Review whether any other endpoints or reset paths can still preempt U2F workflows, and ensure the busy response is handled correctly by host software to avoid user confusion.
Security signals we found
Concurrency/lifetime bug between HWW and U2F USB endpoints
Use-after-free or invalidation risk for live U2F UI objects
Session reset/cancellation path could corrupt shared UI state
New busy-response guard prevents overlapping workflows
Regression tests added for both lifetime guard and busy response
Evidence from the diff
The patch adds an active-workflow reference counter (ActiveWorkflowGuard/ACTIVE_WORKFLOW_COUNT) in the Rust U2F C API and exposes rust_workflow_u2f_is_active(). In src/hww.c, _process_packet() now checks this flag before spawning an async HWW request and returns HWW_RSP_BUSY if a U2F workflow is alive. The guard is moved into spawned unlock and confirm async futures so the counter stays nonzero for the entire UI lifetime, not just until the USB response is sent. Unit tests cover both the guard counter and the busy HWW response behavior.
Changed components
src/hww.csrc/rust/bitbox02-rust-c/src/u2f_c_api.rssrc/rust/bitbox02-rust-c/src/u2f_c_api_stubs.rstest/unit-test/test_hww.cInspect captured patch +114 / −0
diff --git a/src/hww.c b/src/hww.c
index c85046c..88b6d78 100644
--- a/src/hww.c
+++ b/src/hww.c
@@ -133,6 +133,13 @@ static void _maybe_write_response(hww_packet_rsp_t* response)
static void _process_packet(const in_buffer_t* in_req, hww_packet_rsp_t* out_rsp)
{
out_rsp->status = HWW_RSP_NACK;
+#if APP_U2F == 1
+ // U2F workflows outlive their USB response and can still own the shared UI.
+ if (rust_workflow_u2f_is_active()) {
+ out_rsp->status = HWW_RSP_BUSY;
+ return;
+ }
+#endif
// Spawn async task, which is polled in the main loop.
rust_async_usb_on_request_hww(rust_util_bytes(in_req->data, in_req->len));
// Lock USB stack so U2F requests get a BUSY response.
diff --git a/src/rust/bitbox02-rust-c/src/u2f_c_api.rs b/src/rust/bitbox02-rust-c/src/u2f_c_api.rs
index 465a51f..32c02fa 100644
--- a/src/rust/bitbox02-rust-c/src/u2f_c_api.rs
+++ b/src/rust/bitbox02-rust-c/src/u2f_c_api.rs
@@ -26,10 +26,32 @@ impl<O> ConstInit for TaskState<O> {
}
static NEXT_TASK_TOKEN: AtomicU32 = AtomicU32::new(0);
+static ACTIVE_WORKFLOW_COUNT: AtomicU32 = AtomicU32::new(0);
static UNLOCK_STATE: GroundedCell<TaskState<Result<(), ()>>> = GroundedCell::const_init();
static CONFIRM_STATE: GroundedCell<TaskState<Result<(), UserAbort>>> = GroundedCell::const_init();
static BITBOX02_HAL: GroundedCell<crate::HalImpl> = GroundedCell::const_init();
+struct ActiveWorkflowGuard;
+
+impl ActiveWorkflowGuard {
+ fn new() -> Self {
+ ACTIVE_WORKFLOW_COUNT.fetch_add(1, Ordering::Relaxed);
+ Self
+ }
+}
+
+impl Drop for ActiveWorkflowGuard {
+ fn drop(&mut self) {
+ ACTIVE_WORKFLOW_COUNT.fetch_sub(1, Ordering::Relaxed);
+ }
+}
+
+/// Returns whether a detached U2F workflow future is still alive.
+#[unsafe(no_mangle)]
+pub extern "C" fn rust_workflow_u2f_is_active() -> bool {
+ ACTIVE_WORKFLOW_COUNT.load(Ordering::Relaxed) != 0
+}
+
fn next_task_token() -> u32 {
NEXT_TASK_TOKEN.fetch_add(1, Ordering::Relaxed)
}
@@ -71,10 +93,12 @@ unsafe fn complete_confirm(token: u32, result: Result<(), UserAbort>) {
#[unsafe(no_mangle)]
pub unsafe extern "C" fn rust_workflow_spawn_unlock() {
let token = next_task_token();
+ let active_workflow_guard = ActiveWorkflowGuard::new();
unsafe {
UNLOCK_STATE.get().write(TaskState::Running(token));
}
bitbox02_rust::main_loop::spawn(Box::pin(async move {
+ let _active_workflow_guard = active_workflow_guard;
let result = unsafe {
bitbox02_rust::workflow::unlock::unlock(BITBOX02_HAL.get().as_mut().unwrap()).await
};
@@ -96,10 +120,12 @@ pub unsafe extern "C" fn rust_workflow_spawn_confirm(
let title: String = unsafe { CStr::from_ptr(title).to_str().unwrap().into() };
let body: String = unsafe { CStr::from_ptr(body).to_str().unwrap().into() };
let token = next_task_token();
+ let active_workflow_guard = ActiveWorkflowGuard::new();
unsafe {
CONFIRM_STATE.get().write(TaskState::Running(token));
}
bitbox02_rust::main_loop::spawn(Box::pin(async move {
+ let _active_workflow_guard = active_workflow_guard;
let params = ConfirmParams {
title: &title,
body: &body,
@@ -175,3 +201,25 @@ pub unsafe extern "C" fn rust_workflow_abort_current() {
CONFIRM_STATE.get().write(TaskState::Nothing);
}
}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn test_rust_workflow_u2f_is_active() {
+ assert!(!rust_workflow_u2f_is_active());
+
+ let first_guard = ActiveWorkflowGuard::new();
+ assert!(rust_workflow_u2f_is_active());
+
+ {
+ let _second_guard = ActiveWorkflowGuard::new();
+ assert!(rust_workflow_u2f_is_active());
+ }
+ assert!(rust_workflow_u2f_is_active());
+
+ drop(first_guard);
+ assert!(!rust_workflow_u2f_is_active());
+ }
+}
diff --git a/src/rust/bitbox02-rust-c/src/u2f_c_api_stubs.rs b/src/rust/bitbox02-rust-c/src/u2f_c_api_stubs.rs
index aacd011..197ebb8 100644
--- a/src/rust/bitbox02-rust-c/src/u2f_c_api_stubs.rs
+++ b/src/rust/bitbox02-rust-c/src/u2f_c_api_stubs.rs
@@ -3,6 +3,11 @@
#[unsafe(no_mangle)]
pub unsafe extern "C" fn rust_workflow_spawn_unlock() {}
+#[unsafe(no_mangle)]
+pub extern "C" fn rust_workflow_u2f_is_active() -> bool {
+ false
+}
+
#[unsafe(no_mangle)]
pub unsafe extern "C" fn rust_workflow_spawn_confirm(
_title: *const core::ffi::c_char,
diff --git a/test/unit-test/CMakeLists.txt b/test/unit-test/CMakeLists.txt
index e1fa4ac..833d9e5 100644
--- a/test/unit-test/CMakeLists.txt
+++ b/test/unit-test/CMakeLists.txt
@@ -35,6 +35,8 @@ else()
"-Wl,--wrap=util_cleanup_32"
gestures
""
+ hww
+ "-Wl,--wrap=rust_workflow_u2f_is_active"
random
"-Wl,--wrap=rand,--wrap=rust_sha256"
screen_process
diff --git a/test/unit-test/test_hww.c b/test/unit-test/test_hww.c
new file mode 100644
index 0000000..5991964
--- /dev/null
+++ b/test/unit-test/test_hww.c
@@ -0,0 +1,52 @@
+// SPDX-License-Identifier: Apache-2.0
+
+#include <setjmp.h>
+#include <stdarg.h>
+#include <stddef.h>
+#include <cmocka.h>
+
+#include <hww.h>
+#include <rust/rust.h>
+#include <usb/usb_frame.h>
+#include <usb/usb_processing.h>
+
+static bool _u2f_workflow_active = false;
+
+bool __wrap_rust_workflow_u2f_is_active(void)
+{
+ return _u2f_workflow_active;
+}
+
+static void test_hww_new_request_is_busy_while_u2f_active(void** state)
+{
+ (void)state;
+
+ RustUsbReportQueue* queue = rust_usb_report_queue_init();
+ assert_non_null(queue);
+ usb_processing_init(queue);
+ hww_setup();
+
+ _u2f_workflow_active = true;
+ const uint8_t request[] = {0x00, 'h'};
+ const uint32_t cid = 0x12345678;
+ assert_true(
+ usb_processing_enqueue(usb_processing_hww(), request, sizeof(request), HWW_MSG, cid));
+ usb_processing_process(usb_processing_hww());
+
+ USB_FRAME response;
+ assert_true(rust_usb_report_queue_pull(queue, (uint8_t*)&response));
+ assert_int_equal(response.cid, cid);
+ assert_int_equal(response.init.cmd, HWW_MSG);
+ assert_int_equal(FRAME_MSG_LEN(response), 1);
+ assert_int_equal(response.init.data[0], 2); // HWW_RSP_BUSY
+ assert_false(rust_usb_report_queue_pull(queue, (uint8_t*)&response));
+ assert_true(rust_usb_report_queue_free(queue));
+}
+
+int main(void)
+{
+ const struct CMUnitTest tests[] = {
+ cmocka_unit_test(test_hww_new_request_is_busy_while_u2f_active),
+ };
+ return cmocka_run_group_tests(tests, NULL, NULL);
+}
Why this scored 59/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.