fix(core): wait for nRF to boot before deciding to start advertising
What changed, and why it matters
This commit fixes a timing issue during startup of the Trezor hardware wallet's Bluetooth (BLE) subsystem. Previously, the firmware might decide whether to start broadcasting Bluetooth advertisements before the wireless chip (nRF) had finished booting. The fix adds a new check that waits up to 5 seconds for the BLE subsystem to report it is fully started before making that decision. If the subsystem is not ready, the device may skip advertising for bonded peers. This is a defensive fix that could prevent Bluetooth pairing/connectivity problems or unexpected behavior at boot, but the commit itself does not describe a security vulnerability.
Treat as a normal reliability/defensive fix. Review whether the 5-second timeout is sufficient for all boot conditions and whether failure to start advertising could have security implications (e.g., preventing legitimate bonded devices from connecting, or causing fallback behavior). No immediate security response is indicated by the commit alone.
Security signals we found
Timing/race condition in startup sequence between host MCU and nRF BLE coprocessor
New `is_started()` BLE state query exposed to Python layer
Advertising decision now gated on BLE subsystem readiness
5-second bounded wait loop added to startup path
No changelog entry and no explicit security disclosure in commit message
Evidence from the diff
The change exposes a new is_started() function from the Rust BLE HAL to MicroPython (trezorble), adds the corresponding qstr and mock, and uses it in core/src/ble.py. At firmware startup, after calling ble.start_comm(), the code now polls ble.is_started() for up to 5 seconds before deciding whether to call start_advertising(True, ...). The underlying Rust function is_started() already existed; only the MicroPython binding and Python startup logic are new. The commit title says this prevents deciding to advertise before the nRF has booted.
Changed components
core/src/ble.pycore/embed/rust/src/trezorhal/ble/micropython.rscore/embed/rust/librust_qstr.hcore/mocks/generated/trezorble.pyiInspect captured patch +26 / −0
diff --git a/core/embed/rust/librust_qstr.h b/core/embed/rust/librust_qstr.h
index 331a44dc6..1853eb375 100644
--- a/core/embed/rust/librust_qstr.h
+++ b/core/embed/rust/librust_qstr.h
@@ -410,6 +410,7 @@ static void _librust_qstrs(void) {
MP_QSTR_is_data;
MP_QSTR_is_pairing;
MP_QSTR_is_pairing_requested;
+ MP_QSTR_is_started;
MP_QSTR_is_type_of;
MP_QSTR_items;
MP_QSTR_joint__title;
diff --git a/core/embed/rust/src/trezorhal/ble/micropython.rs b/core/embed/rust/src/trezorhal/ble/micropython.rs
index db0e6bf94..e94feee2b 100644
--- a/core/embed/rust/src/trezorhal/ble/micropython.rs
+++ b/core/embed/rust/src/trezorhal/ble/micropython.rs
@@ -140,6 +140,10 @@ extern "C" fn py_is_pairing_requested() -> Obj {
is_pairing_requested().into()
}
+extern "C" fn py_is_started() -> Obj {
+ is_started().into()
+}
+
extern "C" fn py_connection_flags() -> Obj {
let block = || {
let mut result = List::with_capacity(4)?;
@@ -363,6 +367,12 @@ pub static mp_module_trezorble: Module = obj_module! {
/// """
Qstr::MP_QSTR_is_pairing_requested => obj_fn_0!(py_is_pairing_requested).as_obj(),
+ /// def is_started() -> bool:
+ /// """
+ /// True if BLE subsystem is started.
+ /// """
+ Qstr::MP_QSTR_is_started => obj_fn_0!(py_is_started).as_obj(),
+
/// def connection_flags() -> list[str]:
/// """
/// Returns current connection state as a list of string flags.
diff --git a/core/mocks/generated/trezorble.pyi b/core/mocks/generated/trezorble.pyi
index bb4b41381..280e4df16 100644
--- a/core/mocks/generated/trezorble.pyi
+++ b/core/mocks/generated/trezorble.pyi
@@ -118,6 +118,13 @@ def is_pairing_requested() -> bool:
"""
+# rust/src/trezorhal/ble/micropython.rs
+def is_started() -> bool:
+ """
+ True if BLE subsystem is started.
+ """
+
+
# rust/src/trezorhal/ble/micropython.rs
def connection_flags() -> list[str]:
"""
diff --git a/core/src/ble.py b/core/src/ble.py
index dab039771..053036df9 100644
--- a/core/src/ble.py
+++ b/core/src/ble.py
@@ -1,6 +1,8 @@
# BLE setup - called from main.py near the end of firmware startup
# please note BLE may already be set up by bootloader
+import utime
+
import storage.device
import trezorble as ble
from trezor import log
@@ -8,6 +10,12 @@ from trezor import log
try:
ble.start_comm()
+ start_ms = utime.ticks_ms()
+
+ while utime.ticks_diff(utime.ticks_ms(), start_ms) < 5000:
+ if ble.is_started():
+ break
+
# allow connections from bonded peers if any
if ble.peer_count() > 0:
ble.start_advertising(True, storage.device.get_label())
Why this scored 35/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.