da14531: restore BLE UART backpressure
What changed, and why it matters
This update fixes a crash in the BitBox02 hardware wallet when a user lists many backups over a Bluetooth connection. Previously, the device could run out of temporary buffer space while sending a large response, causing it to panic and stop working. The fix makes the Bluetooth sender check whether the whole next chunk fits before writing it, and if not, wait until space becomes available. USB connections were not affected because they use a different path.
Apply the patch. For users, upgrade to a firmware release that includes this fix before performing ListBackups or similar large-response operations over Bluetooth. Continue to monitor for any remaining panic paths in the BLE UART queue and consider extending the atomic enqueue pattern to other callers if they handle untrusted or variable-length data.
Security signals we found
Denial-of-service via buffer overflow/panic on Bluetooth path
Missing backpressure leading to unbounded queue growth and crash
Atomic all-or-nothing enqueue to preserve serial frame boundaries
Crash reproducible with large ListBackups responses over BLE
USB path unaffected because it bypasses the UART ByteQueue
Evidence from the diff
The commit restores backpressure on the BLE UART path to the DA14531 Bluetooth chip. The Rust ByteQueue port kept a panicking single-byte put() API, and the BLE poller used it for every framed byte. When ListBackbacks produced a large protobuf response, the 64-byte HWW/U2FHID reports were serial-framed and queued in a fixed 2048-byte UART ByteQueue. If data was produced faster than the UART drained it, put() overflowed and panicked. The fix adds ByteQueue::try_put_slice() and an extern C wrapper that atomically enqueues a slice only if the entire slice fits. da14531_protocol_poll() now formats the 64-byte report into a temporary frame and only clears hww_data after try_put_slice() succeeds; if the queue is full, it returns NULL and retries the same report on the next poll. Other callers keep the panicking put() behavior to keep the change scoped.
Changed components
src/rust/bitbox-bytequeue/src/lib.rssrc/da14531/da14531_protocol.cBluetooth Low Energy (BLE) UART framing pathDA14531 protocol pollerHWW/U2FHID report forwarding over BLEInspect captured patch +87 / −3
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 5f561e4..c9d19df 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -7,6 +7,7 @@ customers cannot upgrade their bootloader, its changes are recorded separately.
## Firmware
### [Unreleased]
+- Fixed a crash when listing many backups over Bluetooth
### v9.26.1
- Fix a payment request validation issue
diff --git a/src/da14531/da14531_protocol.c b/src/da14531/da14531_protocol.c
index 7f61170..e6d991f 100644
--- a/src/da14531/da14531_protocol.c
+++ b/src/da14531/da14531_protocol.c
@@ -346,10 +346,11 @@ struct da14531_protocol_frame* da14531_protocol_poll(
int len = da14531_protocol_format(
&tmp[0], sizeof(tmp), DA14531_PROTOCOL_PACKET_TYPE_BLE_DATA, *hww_data, 64);
ASSERT(len <= (int)sizeof(tmp));
- util_log("out: %s", util_dbg_hex(*hww_data, 64));
- for (int i = 0; i < len; i++) {
- rust_bytequeue_put(out_queue, tmp[i]);
+ if (!rust_bytequeue_try_put_slice(out_queue, rust_util_bytes(tmp, len))) {
+ util_log("bytequeue full");
+ return NULL;
}
+ util_log("out: %s", util_dbg_hex(*hww_data, 64));
*hww_data = NULL;
}
struct da14531_protocol_frame* frame = NULL;
diff --git a/src/rust/Cargo.lock b/src/rust/Cargo.lock
index d926ca1..863d67f 100644
--- a/src/rust/Cargo.lock
+++ b/src/rust/Cargo.lock
@@ -121,6 +121,9 @@ dependencies = [
[[package]]
name = "bitbox-bytequeue"
version = "0.1.0"
+dependencies = [
+ "util",
+]
[[package]]
name = "bitbox-da14531"
diff --git a/src/rust/bitbox-bytequeue/Cargo.toml b/src/rust/bitbox-bytequeue/Cargo.toml
index 3423f10..e569cbf 100644
--- a/src/rust/bitbox-bytequeue/Cargo.toml
+++ b/src/rust/bitbox-bytequeue/Cargo.toml
@@ -6,3 +6,6 @@ version = "0.1.0"
authors = ["Shift Crypto AG <support@bitbox.swiss>"]
edition = "2024"
license = "Apache-2.0"
+
+[dependencies]
+util = { path = "../util" }
diff --git a/src/rust/bitbox-bytequeue/src/lib.rs b/src/rust/bitbox-bytequeue/src/lib.rs
index 9e2c253..cb31f38 100644
--- a/src/rust/bitbox-bytequeue/src/lib.rs
+++ b/src/rust/bitbox-bytequeue/src/lib.rs
@@ -6,6 +6,7 @@ extern crate alloc;
use alloc::boxed::Box;
use alloc::collections::VecDeque;
+use util::bytes::Bytes;
pub struct ByteQueue {
queue: VecDeque<u8>,
@@ -39,6 +40,18 @@ impl ByteQueue {
self.queue.push_back(data);
}
+ /// Pushes all bytes to the back of the queue.
+ ///
+ /// Returns `false` if inserting would exceed the initial capacity. In that case, no bytes are
+ /// inserted.
+ pub fn try_put_slice(&mut self, data: &[u8]) -> bool {
+ if data.len() > self.initial_capacity - self.queue.len() {
+ return false;
+ }
+ self.queue.extend(data.iter().copied());
+ true
+ }
+
/// Pops one byte from the front of the queue.
///
/// Returns `None` if the queue is empty.
@@ -133,6 +146,24 @@ pub unsafe extern "C" fn rust_bytequeue_put(rb: *mut RustByteQueue, data: u8) {
}
}
+/// Pushes all bytes to the back of the queue.
+///
+/// Returns false if inserting would exceed the queue capacity. In that case, no bytes are inserted.
+///
+/// # Safety
+/// `rb` must be either null or a valid pointer to a [`ByteQueue`] for the
+/// duration of this call. If non-null, the pointed-to bytequeue must not be
+/// aliased for mutable access elsewhere.
+///
+/// `data` must reference a valid byte buffer for the duration of this call.
+#[unsafe(no_mangle)]
+pub unsafe extern "C" fn rust_bytequeue_try_put_slice(rb: *mut RustByteQueue, data: Bytes) -> bool {
+ let Some(rb) = (unsafe { bytequeue_mut(rb) }) else {
+ return false;
+ };
+ rb.try_put_slice(data.as_ref())
+}
+
/// Returns the current number of queued bytes.
///
/// # Safety
@@ -167,6 +198,7 @@ mod tests {
use super::*;
use core::ptr;
+ use util::bytes::rust_util_bytes;
struct TestByteQueue {
ptr: *mut RustByteQueue,
@@ -224,6 +256,19 @@ mod tests {
rb.put(2);
}
+ #[test]
+ fn test_bytequeue_try_put_slice_overflow_is_atomic() {
+ let mut rb = ByteQueue::with_capacity(3);
+ rb.put(1);
+ assert!(rb.try_put_slice(&[2]));
+ assert!(!rb.try_put_slice(&[3, 4]));
+
+ assert_eq!(rb.num(), 2);
+ assert_eq!(rb.get(), Some(1));
+ assert_eq!(rb.get(), Some(2));
+ assert_eq!(rb.get(), None);
+ }
+
#[test]
fn test_rust_bytequeue_init_free() {
let rb = rust_bytequeue_init(0);
@@ -251,6 +296,31 @@ mod tests {
}
}
+ #[test]
+ fn test_rust_bytequeue_try_put_slice_overflow_is_atomic() {
+ let rb = TestByteQueue::with_capacity(3);
+ let data = [1, 2];
+ let too_much = [3, 4];
+ unsafe {
+ assert!(rust_bytequeue_try_put_slice(
+ rb.ptr,
+ rust_util_bytes(data.as_ptr(), data.len())
+ ));
+ assert!(!rust_bytequeue_try_put_slice(
+ rb.ptr,
+ rust_util_bytes(too_much.as_ptr(), too_much.len())
+ ));
+ assert_eq!(rust_bytequeue_num(rb.ptr), 2);
+
+ let mut out = 0;
+ assert!(rust_bytequeue_get(rb.ptr, &mut out));
+ assert_eq!(out, 1);
+ assert!(rust_bytequeue_get(rb.ptr, &mut out));
+ assert_eq!(out, 2);
+ assert!(!rust_bytequeue_get(rb.ptr, &mut out));
+ }
+ }
+
#[test]
fn test_rust_bytequeue_get_empty() {
let rb = TestByteQueue::new();
diff --git a/test/simulator-graphical-bb03/Cargo.lock b/test/simulator-graphical-bb03/Cargo.lock
index bd91703..720889c 100644
--- a/test/simulator-graphical-bb03/Cargo.lock
+++ b/test/simulator-graphical-bb03/Cargo.lock
@@ -332,6 +332,9 @@ dependencies = [
[[package]]
name = "bitbox-bytequeue"
version = "0.1.0"
+dependencies = [
+ "util",
+]
[[package]]
name = "bitbox-da14531"
diff --git a/test/simulator-graphical/Cargo.lock b/test/simulator-graphical/Cargo.lock
index 00e4840..2b41837 100644
--- a/test/simulator-graphical/Cargo.lock
+++ b/test/simulator-graphical/Cargo.lock
@@ -294,6 +294,9 @@ dependencies = [
[[package]]
name = "bitbox-bytequeue"
version = "0.1.0"
+dependencies = [
+ "util",
+]
[[package]]
name = "bitbox-da14531"
Why this scored 60/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.