fix(core): wipe BLE bonds after entering wipe code
What changed, and why it matters
This update fixes a bug in the Trezor hardware wallet where entering a 'wipe code' (a special PIN that securely erases the device) did not remove saved Bluetooth pairings on the T3W1 model. After the fix, the device reboots into a dedicated wipe routine in the bootloader that both erases storage and clears Bluetooth bonding information, preventing a wiped device from remaining paired to a previously trusted phone or computer.
Treat this as a security fix and include it in the next firmware release. Users who rely on wipe-code or PIN-attempt wipe features on T3W1 should upgrade, because older firmware may leave Bluetooth pairings behind after a wipe, allowing a recovered or stolen device to reconnect to a previously paired host without re-pairing.
Security signals we found
Incomplete data wipe: wipe code previously erased user storage but left BLE bond data intact
New secure wipe boot path: BOOT_COMMAND_WIPE with dedicated bootloader handling
Bluetooth bond clearing added to wipe flow via wipe_bonds(NULL)
BLE readiness synchronization added via ble_wait_until_ready() before bond wipe
Wipe metadata (title/message/footer) now passed through bootargs
Evidence from the diff
The commit introduces a new BOOT_COMMAND_WIPE boot argument and a reboot_and_wipe() path. When a wipe code or too many wrong PINs triggers a wipe, the firmware now reboots into the bootloader with wipe metadata, instead of just showing a shutdown screen. The bootloader’s boot_sequence() recognizes BOOT_COMMAND_WIPE, initializes the display if needed, calls erase_storage(NULL), initializes BLE, waits for the BLE driver to be ready via the new ble_wait_until_ready() helper, and calls wipe_bonds(NULL) to clear bonded peers. The existing wipe_bonds() function is made non-static and exposed in workflow.h so the bootloader can reuse it. A new show_wipe_info() helper renders the wipe screen from the bootargs payload.
Changed components
Trezor Core bootloader (core/embed/projects/bootloader)BLE I/O driver (core/embed/io/ble/stm32/ble.c and unix stub)Error handling / wipe-code screen (core/embed/rtl/error_handling.c)Boot argument and utility layer (core/embed/sys/startup)T3W1 device modelInspect captured patch +145 / −27
diff --git a/core/.changelog.d/5939.fixed b/core/.changelog.d/5939.fixed
new file mode 100644
index 00000000..2c0d5094
--- /dev/null
+++ b/core/.changelog.d/5939.fixed
@@ -0,0 +1 @@
+[T3W1] Erase BLE bonds too after entering wipe code.
diff --git a/core/embed/io/ble/inc/io/ble.h b/core/embed/io/ble/inc/io/ble.h
index af4c43be..17e8145d 100644
--- a/core/embed/io/ble/inc/io/ble.h
+++ b/core/embed/io/ble/inc/io/ble.h
@@ -394,3 +394,10 @@ void ble_set_enabled(bool enabled);
* @return true if enabled, false otherwise
*/
bool ble_get_enabled(void);
+
+/**
+ * @brief Wait until BLE driver is ready or timeout occurs
+ *
+ * @return true if BLE driver is ready, false if timeout occurred
+ */
+bool ble_wait_until_ready(void);
diff --git a/core/embed/io/ble/stm32/ble.c b/core/embed/io/ble/stm32/ble.c
index 55eddd27..cbab519f 100644
--- a/core/embed/io/ble/stm32/ble.c
+++ b/core/embed/io/ble/stm32/ble.c
@@ -1449,4 +1449,17 @@ static const syshandle_vmt_t ble_handle_vmt = {
.poll = on_ble_poll,
};
+bool ble_wait_until_ready(void) {
+ uint32_t timeout = ticks_timeout(5000);
+ ble_state_t state = {0};
+ do {
+ ble_get_state(&state);
+ if (state.state_known) {
+ return true;
+ }
+ } while (!ticks_expired(timeout));
+
+ return false;
+}
+
#endif
diff --git a/core/embed/io/ble/unix/ble.c b/core/embed/io/ble/unix/ble.c
index e9045c46..be8b583a 100644
--- a/core/embed/io/ble/unix/ble.c
+++ b/core/embed/io/ble/unix/ble.c
@@ -62,3 +62,5 @@ void ble_notify(const uint8_t *data, size_t len){};
void ble_set_enabled(bool enabled) {}
bool ble_get_enabled(void) { return false; }
+
+bool ble_wait_until_ready(void) { return true; }
diff --git a/core/embed/projects/bootloader/.changelog.d/5939.fixed b/core/embed/projects/bootloader/.changelog.d/5939.fixed
new file mode 100644
index 00000000..2c0d5094
--- /dev/null
+++ b/core/embed/projects/bootloader/.changelog.d/5939.fixed
@@ -0,0 +1 @@
+[T3W1] Erase BLE bonds too after entering wipe code.
diff --git a/core/embed/projects/bootloader/main.c b/core/embed/projects/bootloader/main.c
index a2fb4203..8599df11 100644
--- a/core/embed/projects/bootloader/main.c
+++ b/core/embed/projects/bootloader/main.c
@@ -165,7 +165,8 @@ static secbool boot_sequence(void) {
bool turn_on =
(cmd == BOOT_COMMAND_INSTALL_UPGRADE || cmd == BOOT_COMMAND_REBOOT ||
- cmd == BOOT_COMMAND_SHOW_RSOD || cmd == BOOT_COMMAND_STOP_AND_WAIT);
+ cmd == BOOT_COMMAND_SHOW_RSOD || cmd == BOOT_COMMAND_WIPE ||
+ cmd == BOOT_COMMAND_STOP_AND_WAIT);
if (cmd != BOOT_COMMAND_POWER_OFF) {
turn_on = true;
@@ -178,16 +179,7 @@ static secbool boot_sequence(void) {
if (cmd == BOOT_COMMAND_POWER_OFF) {
#ifdef USE_BLE
ble_init();
-
- 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));
-
+ ble_wait_until_ready();
ble_switch_off();
#endif
}
@@ -530,6 +522,27 @@ int bootloader_main(void) {
}
#endif // USE_BOOTARGS_RSOD
+ if (bootargs_get_command() == BOOT_COMMAND_WIPE) {
+#ifdef LAZY_DISPLAY_INIT
+ display_init(DISPLAY_RESET_CONTENT);
+#endif
+
+ erase_storage(NULL);
+
+#ifdef USE_BLE
+ ble_init();
+ ble_wait_until_ready();
+ wipe_bonds(NULL);
+#endif
+
+ // wipe info was left in bootargs
+ boot_args_t args;
+ bootargs_get_args(&args);
+
+ show_wipe_info(&args.wipeinfo);
+ reboot_or_halt_after_rsod();
+ }
+
ui_screen_boot_stage_1(false);
#ifdef TREZOR_EMULATOR
diff --git a/core/embed/projects/bootloader/workflow/wf_empty_device.c b/core/embed/projects/bootloader/workflow/wf_empty_device.c
index 21d8f41c..f5d8cf78 100644
--- a/core/embed/projects/bootloader/workflow/wf_empty_device.c
+++ b/core/embed/projects/bootloader/workflow/wf_empty_device.c
@@ -36,6 +36,8 @@
#ifdef USE_BLE
#include <io/ble.h>
+
+#include "wire/wire_iface_ble.h"
#endif
#include "bootui.h"
@@ -55,14 +57,7 @@ workflow_result_t workflow_empty_device(void) {
#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));
+ ble_wait_until_ready();
#endif
protob_ios_t ios;
diff --git a/core/embed/projects/bootloader/workflow/wf_wipe_device.c b/core/embed/projects/bootloader/workflow/wf_wipe_device.c
index cde4b86d..5c789c85 100644
--- a/core/embed/projects/bootloader/workflow/wf_wipe_device.c
+++ b/core/embed/projects/bootloader/workflow/wf_wipe_device.c
@@ -50,7 +50,7 @@ static void send_error_conditionally(protob_io_t* iface, char* msg) {
}
#ifdef USE_BLE
-static bool wipe_bonds(protob_io_t* iface) {
+bool wipe_bonds(protob_io_t* iface) {
ble_state_t state = {0};
ble_get_state(&state);
diff --git a/core/embed/projects/bootloader/workflow/workflow.h b/core/embed/projects/bootloader/workflow/workflow.h
index cf833caf..fb1ca3db 100644
--- a/core/embed/projects/bootloader/workflow/workflow.h
+++ b/core/embed/projects/bootloader/workflow/workflow.h
@@ -70,6 +70,9 @@ workflow_result_t workflow_host_control(const fw_info_t *fw,
workflow_result_t workflow_auto_update(const fw_info_t *fw);
#ifdef USE_BLE
+
+bool wipe_bonds(protob_io_t *iface);
+
workflow_result_t workflow_ble_pairing_request(const fw_info_t *fw);
workflow_result_t workflow_wireless_setup(const fw_info_t *fw,
diff --git a/core/embed/rtl/error_handling.c b/core/embed/rtl/error_handling.c
index d8a04f2d..79835579 100644
--- a/core/embed/rtl/error_handling.c
+++ b/core/embed/rtl/error_handling.c
@@ -19,8 +19,14 @@
#include <trezor_rtl.h>
+#include <rtl/mini_printf.h>
+#include <sys/bootutils.h>
#include <sys/system.h>
+#ifdef FANCY_FATAL_ERROR
+#include "rust_ui_common.h"
+#endif
+
#ifndef TREZOR_EMULATOR
// Stack check guard value set in startup code.
// This is used if stack protection is enabled.
@@ -61,13 +67,56 @@ __fatal_error(const char *msg, const char *file, int line) {
}
void __attribute__((noreturn)) show_wipe_code_screen(void) {
- error_shutdown_ex("Wipe code entered", ALL_DATA_ERASED_MESSAGE,
- RECONNECT_DEVICE_MESSAGE);
+ bootutils_wipe_info_t info = {0};
+
+ const char *title = "Wipe code entered";
+
+ mini_snprintf(info.title, sizeof(info.title), "%s", title);
+ mini_snprintf(info.message, sizeof(info.message), "%s",
+ ALL_DATA_ERASED_MESSAGE);
+ mini_snprintf(info.footer, sizeof(info.footer), "%s",
+ RECONNECT_DEVICE_MESSAGE);
+
+ reboot_and_wipe(&info);
+
+ while (1)
+ ;
+}
+
+#ifdef FANCY_FATAL_ERROR
+void show_wipe_info(const bootutils_wipe_info_t *info) {
+ const char *title = "Device wiped";
+ const char *message = ALL_DATA_ERASED_MESSAGE;
+ const char *footer = "Please visit trezor.io/rsod";
+
+ if (info->title[0] != '\0') {
+ title = info->title;
+ }
+ if (info->message[0] != '\0') {
+ message = info->message;
+ }
+ if (info->footer[0] != '\0') {
+ footer = info->footer;
+ }
+
+ display_rsod_rust(title, message, footer);
}
+#endif
void __attribute__((noreturn)) show_pin_too_many_screen(void) {
- error_shutdown_ex("Pin attempts exceeded", ALL_DATA_ERASED_MESSAGE,
- RECONNECT_DEVICE_MESSAGE);
+ bootutils_wipe_info_t info = {0};
+
+ const char *title = "Pin attempts exceeded";
+
+ mini_snprintf(info.title, sizeof(info.title), "%s", title);
+ mini_snprintf(info.message, sizeof(info.message), "%s",
+ ALL_DATA_ERASED_MESSAGE);
+ mini_snprintf(info.footer, sizeof(info.footer), "%s",
+ RECONNECT_DEVICE_MESSAGE);
+
+ reboot_and_wipe(&info);
+ while (1)
+ ;
}
void __attribute__((noreturn)) show_install_restricted_screen(void) {
diff --git a/core/embed/rtl/inc/rtl/error_handling.h b/core/embed/rtl/inc/rtl/error_handling.h
index 574309ef..43c274e0 100644
--- a/core/embed/rtl/inc/rtl/error_handling.h
+++ b/core/embed/rtl/inc/rtl/error_handling.h
@@ -17,8 +17,9 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
-#ifndef LIB_ERROR_HANDLING_H
-#define LIB_ERROR_HANDLING_H
+#pragma once
+
+#include <sys/bootutils.h>
// Shows an error message and shuts down the device.
//
@@ -51,4 +52,5 @@ void __attribute__((noreturn)) show_pin_too_many_screen(void);
// Shows INSTALL RESTRICTED screen and shuts down the device.
void __attribute__((noreturn)) show_install_restricted_screen(void);
-#endif // LIB_ERRORS_H
+// Shows wipe information screen
+void show_wipe_info(const bootutils_wipe_info_t *info);
diff --git a/core/embed/sys/startup/inc/sys/bootargs.h b/core/embed/sys/startup/inc/sys/bootargs.h
index e24174a5..b25e6c1a 100644
--- a/core/embed/sys/startup/inc/sys/bootargs.h
+++ b/core/embed/sys/startup/inc/sys/bootargs.h
@@ -20,6 +20,7 @@
#ifndef TREZORHAL_BOOTARGS_H
#define TREZORHAL_BOOTARGS_H
+#include <sys/bootutils.h>
#include <sys/systask.h>
#include <trezor_types.h>
@@ -33,6 +34,8 @@ typedef enum {
BOOT_COMMAND_INSTALL_UPGRADE = 0xFA4A5C8D,
// Show RSOD and wait for user input
BOOT_COMMAND_SHOW_RSOD = 0x7CD945A0,
+ // Wipe the device
+ BOOT_COMMAND_WIPE = 0xD965CE36,
// Reboot the device as if it was powered on
BOOT_COMMAND_REBOOT = 0xA5C3D4E2,
// Power of the device
@@ -48,6 +51,8 @@ typedef union {
uint8_t hash[32];
// error information, BOOT_COMMAND_SHOW_RSOD
systask_postmortem_t pminfo;
+ // wipe information, BOOT_COMMAND_WIPE
+ bootutils_wipe_info_t wipeinfo;
} boot_args_t;
_Static_assert(sizeof(boot_args_t) == BOOT_ARGS_MAX_SIZE,
diff --git a/core/embed/sys/startup/inc/sys/bootutils.h b/core/embed/sys/startup/inc/sys/bootutils.h
index 489ea874..130d006e 100644
--- a/core/embed/sys/startup/inc/sys/bootutils.h
+++ b/core/embed/sys/startup/inc/sys/bootutils.h
@@ -21,6 +21,13 @@
#include <sys/systask.h>
+// Wipe information structure
+typedef struct {
+ char title[64];
+ char message[64];
+ char footer[64];
+} bootutils_wipe_info_t;
+
// Immediately resets the device and initiates the normal boot sequence as if
// the device was powered on
void __attribute__((noreturn)) reboot_device(void);
@@ -48,6 +55,11 @@ void __attribute__((noreturn))
reboot_with_rsod(const systask_postmortem_t *pminfo);
#endif
+// Resets the device and wipes all the user data.
+// RSOD with wipe information is displayed.
+void __attribute__((noreturn))
+reboot_and_wipe(const bootutils_wipe_info_t *info);
+
// Allows the user to read the displayed error message and then
// reboots the device or waits for power-off.
//
diff --git a/core/embed/sys/startup/stm32/bootutils.c b/core/embed/sys/startup/stm32/bootutils.c
index fd53daee..675cd07c 100644
--- a/core/embed/sys/startup/stm32/bootutils.c
+++ b/core/embed/sys/startup/stm32/bootutils.c
@@ -225,6 +225,10 @@ __attribute__((noreturn)) void reboot_with_rsod(
// Set bootargs area to the new command and arguments
reboot_with_args(BOOT_COMMAND_SHOW_RSOD, pminfo, sizeof(*pminfo));
}
+__attribute__((noreturn)) void reboot_and_wipe(
+ const bootutils_wipe_info_t* info) {
+ reboot_with_args(BOOT_COMMAND_WIPE, info, sizeof(*info));
+}
__attribute__((noreturn)) void reboot_or_halt_after_rsod(void) {
#ifndef RSOD_INFINITE_LOOP
diff --git a/core/embed/sys/startup/unix/bootutils.c b/core/embed/sys/startup/unix/bootutils.c
index ef25a419..54e8ed21 100644
--- a/core/embed/sys/startup/unix/bootutils.c
+++ b/core/embed/sys/startup/unix/bootutils.c
@@ -73,6 +73,17 @@ __attribute__((noreturn)) void reboot_to_off(void) {
exit(3);
}
+__attribute__((noreturn)) void reboot_and_wipe(
+ const bootutils_wipe_info_t* info) {
+ show_wipe_info(info);
+
+ printf("reboot (wipe)\n");
+
+ systick_delay_ms(3000);
+
+ exit(3);
+}
+
__attribute__((noreturn)) void reboot_or_halt_after_rsod(void) {
printf("reboot (with timeout)\n");
Why this scored 58/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.