fix(core/bootloader): account for slow nRF startup - wait before displaying the start button
What changed, and why it matters
This bootloader-only change adds a short wait and a temporary logo screen when a Trezor device has no firmware installed and Bluetooth (BLE) is enabled. The goal is to give the wireless nRF chip time to finish starting up before the user sees the 'Start' setup button. Without this delay, the device could briefly show the start button while the Bluetooth state is still unknown, which might lead to a confusing or inconsistent setup experience. There is no direct evidence in the commit that this fixes an exploitable security vulnerability.
Treat as a normal firmware/bootloader fix. Review whether the 5-second timeout is sufficient across all nRF firmware versions and whether a failure to reach state.state_known should block setup or fall back to a safe non-BLE mode. No urgent security response is indicated by the diff alone.
Security signals we found
Timing/startup race condition addressed in bootloader
User-interactive screen delayed until subsystem state is known
Bluetooth/BLE state dependency in empty-device setup flow
No changelog entry ([no changelog])
Evidence from the diff
In workflow_empty_device(), when USE_BLE is defined, the patch now calls screen_boot_empty() and polls ble_get_state() for up to 5 seconds until state.state_known becomes true before initializing the protob interfaces. It also adds the corresponding screen_boot_empty() Rust UI hook and an Eckhart-layout implementation that renders the logo and fades in. The Welcome screen’s fade flag is changed from true to false, likely so the empty-device logo screen can handle the fade itself. The change is defensive: it prevents the interactive setup screen from appearing while the BLE subsystem is still initializing.
Changed components
core/embed/projects/bootloader/workflow/wf_empty_device.ccore/embed/rust/rust_ui_bootloader.hcore/embed/rust/src/ui/api/bootloader_c.rscore/embed/rust/src/ui/layout_eckhart/ui_bootloader.rscore/embed/rust/src/ui/ui_bootloader.rsInspect captured patch +37 / −2
diff --git a/core/embed/projects/bootloader/workflow/wf_empty_device.c b/core/embed/projects/bootloader/workflow/wf_empty_device.c
index 11c8d8a2..b676b444 100644
--- a/core/embed/projects/bootloader/workflow/wf_empty_device.c
+++ b/core/embed/projects/bootloader/workflow/wf_empty_device.c
@@ -33,6 +33,10 @@
#include <sys/backup_ram.h>
#endif
+#ifdef USE_BLE
+#include <io/ble.h>
+#endif
+
#include "bootui.h"
#include "rust_ui_bootloader.h"
#include "workflow.h"
@@ -48,6 +52,18 @@ workflow_result_t workflow_empty_device(void) {
ensure(backup_ram_erase_protected() * sectrue, NULL);
#endif
+#ifdef USE_BLE
+ screen_boot_empty();
+ uint32_t timeout = ticks_timeout(5000);
+ ble_state_t state = {0};
+ do {
+ ble_get_state(&state);
+ if (state.state_known) {
+ break;
+ }
+ } while (!ticks_expired(timeout));
+#endif
+
protob_ios_t ios;
workflow_ifaces_init(sectrue, &ios);
diff --git a/core/embed/rust/rust_ui_bootloader.h b/core/embed/rust/rust_ui_bootloader.h
index 4932a650..44949692 100644
--- a/core/embed/rust/rust_ui_bootloader.h
+++ b/core/embed/rust/rust_ui_bootloader.h
@@ -35,6 +35,7 @@ void screen_bootloader_entry_progress(uint16_t progress, bool initialize);
// simple screens with no interaction
void screen_boot_stage_1(bool fading);
+void screen_boot_empty(void);
void screen_boot(bool warning, const char* vendor_str, size_t vendor_str_len,
uint32_t version, const void* vendor_img,
size_t vendor_img_len, int wait);
diff --git a/core/embed/rust/src/ui/api/bootloader_c.rs b/core/embed/rust/src/ui/api/bootloader_c.rs
index 63610d34..dfd5f096 100644
--- a/core/embed/rust/src/ui/api/bootloader_c.rs
+++ b/core/embed/rust/src/ui/api/bootloader_c.rs
@@ -140,6 +140,11 @@ extern "C" fn screen_boot_stage_1(fading: bool) {
ModelUI::screen_boot_stage_1(fading)
}
+#[no_mangle]
+extern "C" fn screen_boot_empty() {
+ ModelUI::screen_boot_empty()
+}
+
#[no_mangle]
extern "C" fn screen_boot(
warning: bool,
diff --git a/core/embed/rust/src/ui/layout_eckhart/ui_bootloader.rs b/core/embed/rust/src/ui/layout_eckhart/ui_bootloader.rs
index 2eb8c229..cfe7b271 100644
--- a/core/embed/rust/src/ui/layout_eckhart/ui_bootloader.rs
+++ b/core/embed/rust/src/ui/layout_eckhart/ui_bootloader.rs
@@ -16,7 +16,7 @@ use super::{
BldActionBar, BldHeader, BldHeaderMsg, BldMenuScreen, BldTextScreen, BldWelcomeScreen,
ConnectScreen, WirelessSetupScreen,
},
- component::Button,
+ component::{render_logo, Button},
cshape::{render_loader, ScreenBorder},
fonts::{FONT_SATOSHI_MEDIUM_26, FONT_SATOSHI_REGULAR_38},
theme::{
@@ -124,7 +124,7 @@ impl BootloaderLayoutType for BootloaderLayout {
fn show(&mut self) -> u32 {
match self {
- BootloaderLayout::Welcome(f) => show(f, true),
+ BootloaderLayout::Welcome(f) => show(f, false),
BootloaderLayout::Menu(f) => show(f, true),
BootloaderLayout::Connect(f) => show(f, true),
#[cfg(feature = "ble")]
@@ -371,6 +371,15 @@ impl BootloaderUI for UIEckhart {
fn screen_boot_stage_1(_fading: bool) {}
+ fn screen_boot_empty() {
+ render_on_display(None, Some(BLACK), |target| {
+ render_logo(target);
+ });
+
+ display::refresh();
+ Self::fadein();
+ }
+
fn screen_wipe_progress(progress: u16, initialize: bool) {
Self::screen_progress(
"Resetting Trezor",
diff --git a/core/embed/rust/src/ui/ui_bootloader.rs b/core/embed/rust/src/ui/ui_bootloader.rs
index 14fc06fb..4aaead7f 100644
--- a/core/embed/rust/src/ui/ui_bootloader.rs
+++ b/core/embed/rust/src/ui/ui_bootloader.rs
@@ -44,6 +44,10 @@ pub trait BootloaderUI {
fn screen_boot_stage_1(fading: bool);
+ fn screen_boot_empty() {
+ unimplemented!();
+ }
+
fn screen_wipe_progress(progress: u16, initialize: bool);
fn screen_install_progress(progress: u16, initialize: bool, initial_setup: bool);
Why this scored 26/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.