feat(core/prodtest): add boardloader update command
What changed, and why it matters
This commit adds a factory-testing command that can rewrite the boardloader (the very first startup code on the device). It is intentionally restricted to development/non-production builds and excluded from the T2T1 model. The change also makes the shared binary buffer 4-byte aligned and adds a new memory-protection mode so the boardloader flash region can be written during this test command.
Treat as a legitimate manufacturing/test feature rather than a vulnerability, but verify that the `PRODUCTION` and `TREZOR_MODEL_T2T1` compile-time guards are enforced in release build pipelines and that the prodtest image is not shipped on consumer devices. Review that MPU reconfiguration and flash lock/unlock paths cannot be triggered from production firmware or untrusted CLI access.
Security signals we found
Adds ability to overwrite boardloader from prodtest CLI
Restricted by compile-time guards to non-production, non-T2T1 builds
Introduces new MPU mode granting privileged RW access to boardloader flash region
Uses flash_area_write_data_padded and explicit flash lock/unlock in finalize callback
Shared binary buffer alignment changed to 4 bytes, likely to satisfy flash write requirements
Evidence from the diff
The patch introduces boardloader-update in the prodtest CLI, gated by #if !PRODUCTION && !TREZOR_MODEL_T2T1. It reuses binary_update() and a new finalize callback that erases and writes the BOARDLOADER_AREA after reconfiguring the MPU to allow privileged RW access to the boardloader region. A new MPU_MODE_BOARDLOADER enum value is added for both stm32f4 and stm32u5 MPU drivers, enabled only when !defined(BOARDLOADER) && !PRODUCTION. The shared binary_buffer in common.c is now 4-byte aligned. The README explicitly states the command only works on development boards, not production firmware.
Changed components
core/embed/projects/prodtest/cmd/prodtest_boardloader.ccore/embed/projects/prodtest/cmd/common.ccore/embed/sys/mpu/inc/sys/mpu.hcore/embed/sys/mpu/stm32f4/mpu.ccore/embed/sys/mpu/stm32u5/mpu.ccore/embed/projects/prodtest/README.mdInspect captured patch +71 / −1
diff --git a/core/embed/projects/prodtest/README.md b/core/embed/projects/prodtest/README.md
index 84515b406..6b70f6dd3 100644
--- a/core/embed/projects/prodtest/README.md
+++ b/core/embed/projects/prodtest/README.md
@@ -133,6 +133,11 @@ boardloader-version
OK 0.2.6
```
+### boardloader-update
+Updates the boardloader to the supplied binary file. Only works on development boards, not in production firmware.
+Use `core/tools/bin_update.py` script to update the boardloader binary.
+
+
### bootloader-version
Retrieves the version of the bootloader. The command returns `OK` followed by the version in the format `<major>.<minor>.<patch>`.
diff --git a/core/embed/projects/prodtest/cmd/common.c b/core/embed/projects/prodtest/cmd/common.c
index 6f0ff4579..fcab7e14e 100644
--- a/core/embed/projects/prodtest/cmd/common.c
+++ b/core/embed/projects/prodtest/cmd/common.c
@@ -462,7 +462,8 @@ bool check_cert_chain(cli_t* cli, const uint8_t* chain, size_t chain_size,
#define BINARY_MAXSIZE BOOTLOADER_MAXSIZE
#endif
-__attribute__((section(".buf"))) static uint8_t binary_buffer[BINARY_MAXSIZE];
+__attribute__((section(".buf"),
+ aligned(4))) static uint8_t binary_buffer[BINARY_MAXSIZE];
static size_t binary_len = 0;
static bool binary_update_in_progress = false;
diff --git a/core/embed/projects/prodtest/cmd/prodtest_boardloader.c b/core/embed/projects/prodtest/cmd/prodtest_boardloader.c
index fca13f8a8..e9da9682e 100644
--- a/core/embed/projects/prodtest/cmd/prodtest_boardloader.c
+++ b/core/embed/projects/prodtest/cmd/prodtest_boardloader.c
@@ -19,8 +19,13 @@
#include <trezor_rtl.h>
+#include <flash_area.h>
#include <rtl/cli.h>
+#include <sys/mpu.h>
#include <util/board_capabilities.h>
+#include <util/flash_layout.h>
+
+#include "common.h"
static void prodtest_boardloader_version(cli_t* cli) {
if (cli_arg_count(cli) > 0) {
@@ -33,6 +38,41 @@ static void prodtest_boardloader_version(cli_t* cli) {
cli_ok(cli, "%d.%d.%d", v.version_major, v.version_minor, v.version_patch);
}
+#if !PRODUCTION && !TREZOR_MODEL_T2T1
+static bool prodtest_boardloader_update_finalize(uint8_t* data, size_t len) {
+ mpu_mode_t mode = mpu_reconfig(MPU_MODE_BOARDLOADER);
+
+ secbool res = flash_area_erase(&BOARDLOADER_AREA, NULL);
+
+ if (res != sectrue) {
+ goto cleanup;
+ }
+
+ res = flash_unlock_write();
+
+ if (res != sectrue) {
+ goto cleanup;
+ }
+
+ res = flash_area_write_data_padded(&BOARDLOADER_AREA, 0, data, len, 0xFF,
+ flash_area_get_size(&BOARDLOADER_AREA));
+
+cleanup:
+
+ (void)!flash_lock_write();
+
+ mpu_restore(mode);
+
+ parse_boardloader_capabilities();
+
+ return res == sectrue;
+}
+
+static void prodtest_boardloader_update(cli_t* cli) {
+ binary_update(cli, prodtest_boardloader_update_finalize);
+}
+#endif
+
// clang-format off
PRODTEST_CLI_CMD(
@@ -41,3 +81,12 @@ PRODTEST_CLI_CMD(
.info = "Retrieve the boardloader version",
.args = ""
);
+
+#if !PRODUCTION && !TREZOR_MODEL_T2T1
+PRODTEST_CLI_CMD(
+ .name = "boardloader-update",
+ .func = prodtest_boardloader_update,
+ .info = "Update boardloader",
+ .args = "<phase> <hex-data>"
+);
+#endif
diff --git a/core/embed/sys/mpu/inc/sys/mpu.h b/core/embed/sys/mpu/inc/sys/mpu.h
index 163edafe6..ffc1bd777 100644
--- a/core/embed/sys/mpu/inc/sys/mpu.h
+++ b/core/embed/sys/mpu/inc/sys/mpu.h
@@ -34,6 +34,7 @@ typedef enum {
MPU_MODE_DISABLED, // MPU is disabled
MPU_MODE_DEFAULT, // Default
MPU_MODE_BOARDCAPS, // + boardloader capabilities (privileged RO)
+ MPU_MODE_BOARDLOADER, // + boardloader (privileged RW, non-production only)
MPU_MODE_BOOTLOADER, // + bootloader area (privileged RW)
MPU_MODE_BOOTARGS, // + boot arguments (privileged RW)
MPU_MODE_BOOTUCB, // + boot update control block (privileged RW)
diff --git a/core/embed/sys/mpu/stm32f4/mpu.c b/core/embed/sys/mpu/stm32f4/mpu.c
index e961d6f2e..2f3c3afd4 100644
--- a/core/embed/sys/mpu/stm32f4/mpu.c
+++ b/core/embed/sys/mpu/stm32f4/mpu.c
@@ -299,6 +299,14 @@ mpu_mode_t mpu_reconfig(mpu_mode_t mode) {
break;
#endif
+#if !defined(BOARDLOADER) && !PRODUCTION
+ case MPU_MODE_BOARDLOADER:
+ // Boardloader (Privileged, Read-Write, Non-Executable)
+ // Subregion: 48KB = 64KB except 2/8 at end
+ SET_REGION( 6, FLASH_BASE, SIZE_64KB, 0xC0, FLASH_DATA, PRIV_RW );
+ break;
+#endif
+
#if !defined(BOARDLOADER) && !defined(BOOTLOADER)
case MPU_MODE_BOOTLOADER:
DIS_REGION( 5 );
diff --git a/core/embed/sys/mpu/stm32u5/mpu.c b/core/embed/sys/mpu/stm32u5/mpu.c
index 30dd76563..da52d66e5 100644
--- a/core/embed/sys/mpu/stm32u5/mpu.c
+++ b/core/embed/sys/mpu/stm32u5/mpu.c
@@ -438,6 +438,12 @@ mpu_mode_t mpu_reconfig(mpu_mode_t mode) {
SET_REGION( 6, BOARDLOADER_START, BOARDLOADER_MAXSIZE,FLASH_DATA, NO, NO );
break;
#endif
+#if !defined(BOARDLOADER) && !PRODUCTION
+ case MPU_MODE_BOARDLOADER:
+ // REGION ADDRESS SIZE TYPE WRITE UNPRIV
+ SET_REGION( 6, BOARDLOADER_START, BOARDLOADER_MAXSIZE,FLASH_DATA, YES, NO );
+ break;
+#endif
#if !defined(BOOTLOADER) && !defined(BOARDLOADER)
case MPU_MODE_BOOTLOADER:
SET_REGION( 6, BOOTLOADER_START, BOOTLOADER_MAXSIZE, FLASH_DATA, YES, NO );
Why this scored 33/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.