feat(core/bootloader): improve bootloader entry sequence on T3W1
What changed, and why it matters
This commit changes how the Trezor T3W1 hardware wallet enters its bootloader. It shortens the required power-button hold from 3 seconds to 2 seconds, adds a new on-screen progress message telling the user to release the button, and moves the haptic buzz and display initialization to occur only after the user has committed to bootloader mode. The changes appear to be a user-experience and timing refinement rather than a fix for an exploitable security flaw. There is no vendor statement or independent report linking this commit to a security vulnerability.
Treat as a normal feature/usability commit. If reviewing for security, verify that the shorter 2-second hold still provides adequate protection against accidental or coerced bootloader entry, and that the deferred display/touch initialization does not introduce a race condition or information leak before the user is committed to bootloader mode. No immediate security response is indicated by the diff alone.
Security signals we found
Changed timing of security-sensitive bootloader entry gesture (button hold duration reduced from 3000 ms to 2000 ms)
Moved display/touch initialization and haptic feedback into post-lock path
Added new UI screen during bootloader entry to provide explicit user feedback
Evidence from the diff
The bootloader entry sequence in core/embed/projects/bootloader/main.c is refactored. The power-button hold threshold to lock bootloader entry is reduced from 3000 ms to 2000 ms, and once that threshold is reached the loop breaks immediately instead of waiting for button release. The haptic feedback for bootloader entry and the display/touch initialization are deferred until after bld_locked is set, and a new screen_bootloader_entry_progress(1000, true) call is added to show a ‘Starting bootloader… / Release the button’ screen. Supporting Rust UI code adds screen_bootloader_entry_progress to the bootloader UI trait and implements it for the Eckhart layout. The change also fixes the type of progress parameters from int16_t to uint16_t in the C/Rust FFI and centers the wait message. No security-relevant bug (e.g., buffer overflow, authentication bypass, secret leak) is visible in the diff.
Changed components
Trezor T3W1 bootloadercore/embed/projects/bootloader/main.ccore/embed/rust/src/ui/layout_eckhart/ui_bootloader.rscore/embed/rust/src/ui/api/bootloader_c.rscore/embed/rust/rust_ui_bootloader.hcore/embed/rust/src/ui/ui_bootloader.rsInspect captured patch +93 / −31
diff --git a/core/embed/projects/bootloader/main.c b/core/embed/projects/bootloader/main.c
index d5c9238f..57d3739a 100644
--- a/core/embed/projects/bootloader/main.c
+++ b/core/embed/projects/bootloader/main.c
@@ -117,6 +117,22 @@ static secbool is_manufacturing_mode(vendor_header *vhdr) {
return manufacturing_mode;
}
+static void display_touch_init(secbool manufacturing_mode,
+ secbool *touch_initialized) {
+ display_init(DISPLAY_RESET_CONTENT);
+
+#ifdef USE_TOUCH
+ secbool touch_init_ok = secfalse;
+ touch_init_ok = touch_init();
+ if (manufacturing_mode != sectrue) {
+ ensure(touch_init_ok, "Touch screen panel was not loaded properly.");
+ }
+ if (touch_initialized != NULL) {
+ *touch_initialized = touch_init_ok;
+ }
+#endif
+}
+
static secbool boot_sequence(void) {
secbool stay_in_bootloader = secfalse;
@@ -159,8 +175,10 @@ static secbool boot_sequence(void) {
uint32_t press_start = 0;
bool turn_on_locked = false;
- bool haptic_played = false;
bool bld_locked = false;
+#ifdef USE_HAPTIC
+ bool haptic_played = false;
+#endif
while (!turn_on) {
bool btn_down = button_is_down(BTN_POWER);
@@ -172,23 +190,18 @@ static secbool boot_sequence(void) {
}
uint32_t elapsed = systick_ms() - press_start;
- if (elapsed >= 3000 && !bld_locked) {
-#ifdef USE_HAPTIC
- haptic_play(HAPTIC_BOOTLOADER_ENTRY);
-#endif
+ if (elapsed >= 2000) {
bld_locked = true;
+ break;
}
#ifdef USE_HAPTIC
- else if (elapsed >= 500 && !haptic_played) {
+ if (elapsed >= 500 && !haptic_played) {
haptic_play(HAPTIC_BUTTON_PRESS);
haptic_played = true;
}
#endif
} else if (press_start != 0) {
// Button just released
- if (bld_locked) {
- stay_in_bootloader = sectrue;
- }
if (turn_on_locked) {
break;
}
@@ -220,31 +233,38 @@ static secbool boot_sequence(void) {
while (pm_turn_on() != PM_OK) {
rgb_led_set_color(0x400000);
- systick_delay_ms(1000);
+ systick_delay_ms(400);
+ rgb_led_set_color(0);
+ systick_delay_ms(400);
+ rgb_led_set_color(0x400000);
+ systick_delay_ms(400);
+ rgb_led_set_color(0);
+ systick_delay_ms(400);
+ rgb_led_set_color(0x400000);
+ systick_delay_ms(400);
+ rgb_led_set_color(0);
pm_hibernate();
systick_delay_ms(1000);
reboot_to_off();
}
+ if (bld_locked) {
+#ifdef USE_HAPTIC
+ haptic_play(HAPTIC_BOOTLOADER_ENTRY);
#endif
- return stay_in_bootloader;
-}
+ display_touch_init(secfalse, NULL);
+ screen_bootloader_entry_progress(1000, true);
-static void display_touch_init(secbool manufacturing_mode,
- secbool *touch_initialized) {
- display_init(DISPLAY_RESET_CONTENT);
+ while (button_is_down(BTN_POWER)) {
+ }
-#ifdef USE_TOUCH
- secbool touch_init_ok = secfalse;
- touch_init_ok = touch_init();
- if (manufacturing_mode != sectrue) {
- ensure(touch_init_ok, "Touch screen panel was not loaded properly.");
- }
- if (touch_initialized != NULL) {
- *touch_initialized = touch_init_ok;
+ stay_in_bootloader = sectrue;
}
+
#endif
+
+ return stay_in_bootloader;
}
static void drivers_init(secbool manufacturing_mode,
diff --git a/core/embed/rust/rust_ui_bootloader.h b/core/embed/rust/rust_ui_bootloader.h
index 96d62c9b..4932a650 100644
--- a/core/embed/rust/rust_ui_bootloader.h
+++ b/core/embed/rust/rust_ui_bootloader.h
@@ -26,9 +26,11 @@ uint32_t screen_install_fail(void);
void screen_unlock_bootloader_success(void);
// progress screens
-void screen_install_progress(int16_t progress, bool initialize,
+void screen_install_progress(uint16_t progress, bool initialize,
bool initial_setup);
-void screen_wipe_progress(int16_t progress, bool initialize);
+void screen_wipe_progress(uint16_t progress, bool initialize);
+
+void screen_bootloader_entry_progress(uint16_t progress, bool initialize);
// simple screens with no interaction
diff --git a/core/embed/rust/src/ui/api/bootloader_c.rs b/core/embed/rust/src/ui/api/bootloader_c.rs
index b270a850..63610d34 100644
--- a/core/embed/rust/src/ui/api/bootloader_c.rs
+++ b/core/embed/rust/src/ui/api/bootloader_c.rs
@@ -229,3 +229,9 @@ extern "C" fn screen_wireless_setup(
extern "C" fn screen_pairing_mode_finalizing(initial_setup: bool) -> u32 {
ModelUI::screen_pairing_mode_finalizing(initial_setup)
}
+
+#[cfg(feature = "power_manager")]
+#[no_mangle]
+extern "C" fn screen_bootloader_entry_progress(progress: u16, initialize: bool) {
+ ModelUI::screen_bootloader_entry_progress(progress, initialize)
+}
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 a1fd3949..04434bad 100644
--- a/core/embed/rust/src/ui/layout_eckhart/ui_bootloader.rs
+++ b/core/embed/rust/src/ui/layout_eckhart/ui_bootloader.rs
@@ -43,13 +43,19 @@ const PROGRESS_TEXT_ORIGIN: Point = SCREEN.top_left().ofs(Offset::new(
theme::PADDING,
38 + FONT_SATOSHI_REGULAR_38.text_height(),
));
-const PROGRESS_WAIT_ORIGIN: Point = SCREEN.bottom_left().ofs(Offset::new(168, -35));
+const PROGRESS_WAIT_ORIGIN: Point = SCREEN.bottom_center().ofs(Offset::new(0, -35));
const SCREEN_BORDER_BLUE: ScreenBorder = ScreenBorder::new(BLUE);
const SCREEN_BORDER_RED: ScreenBorder = ScreenBorder::new(RED);
const SCREEN_BORDER_GREEN_LIGHT: ScreenBorder = ScreenBorder::new(GREEN_LIGHT);
impl UIEckhart {
- fn screen_progress(text: &str, initialize: bool, loader_progress: u16, border: &ScreenBorder) {
+ fn screen_progress(
+ text: &str,
+ wait_msg: &str,
+ initialize: bool,
+ loader_progress: u16,
+ border: &ScreenBorder,
+ ) {
if initialize {
Self::fadeout();
}
@@ -61,8 +67,8 @@ impl UIEckhart {
.with_align(Alignment::Start)
.with_fg(GREY_LIGHT)
.render(target);
- shape::Text::new(PROGRESS_WAIT_ORIGIN, WAIT_MESSAGE, FONT_SATOSHI_MEDIUM_26)
- .with_align(Alignment::Start)
+ shape::Text::new(PROGRESS_WAIT_ORIGIN, wait_msg, FONT_SATOSHI_MEDIUM_26)
+ .with_align(Alignment::Center)
.with_fg(GREY)
.render(target);
});
@@ -362,7 +368,13 @@ impl BootloaderUI for UIEckhart {
fn screen_boot_stage_1(_fading: bool) {}
fn screen_wipe_progress(progress: u16, initialize: bool) {
- Self::screen_progress("Resetting Trezor", initialize, progress, &SCREEN_BORDER_RED)
+ Self::screen_progress(
+ "Resetting Trezor",
+ WAIT_MESSAGE,
+ initialize,
+ progress,
+ &SCREEN_BORDER_RED,
+ )
}
fn screen_install_progress(progress: u16, initialize: bool, initial_setup: bool) {
@@ -371,7 +383,13 @@ impl BootloaderUI for UIEckhart {
} else {
&SCREEN_BORDER_BLUE
};
- Self::screen_progress("Installing firmware", initialize, progress, border)
+ Self::screen_progress(
+ "Installing firmware",
+ WAIT_MESSAGE,
+ initialize,
+ progress,
+ border,
+ )
}
fn screen_wipe_success() {
@@ -513,4 +531,15 @@ impl BootloaderUI for UIEckhart {
}
run(&mut screen)
}
+
+ #[cfg(feature = "power_manager")]
+ fn screen_bootloader_entry_progress(progress: u16, initialize: bool) {
+ Self::screen_progress(
+ "Starting bootloader...",
+ "Release the button",
+ initialize,
+ progress,
+ &SCREEN_BORDER_BLUE,
+ )
+ }
}
diff --git a/core/embed/rust/src/ui/ui_bootloader.rs b/core/embed/rust/src/ui/ui_bootloader.rs
index 3bbb4d57..14fc06fb 100644
--- a/core/embed/rust/src/ui/ui_bootloader.rs
+++ b/core/embed/rust/src/ui/ui_bootloader.rs
@@ -65,4 +65,9 @@ pub trait BootloaderUI {
#[cfg(feature = "ble")]
fn screen_pairing_mode_finalizing(initial_setup: bool) -> u32;
+
+ #[cfg(feature = "power_manager")]
+ fn screen_bootloader_entry_progress(_progress: u16, _initialize: bool) {
+ unimplemented!();
+ }
}
Why this scored 28/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.