build(core): collect micropython GC root pointers
What changed, and why it matters
This commit updates the Trezor firmware build system to use a newer MicroPython mechanism for tracking special memory pointers called 'root pointers.' It removes hard-coded lists of these pointers from configuration files and instead collects them automatically from source files during the build. The change itself is a build-system modernization and does not appear to fix an active security bug, but it helps prevent a class of future memory-management mistakes that could, in theory, lead to device instability or security issues.
Treat as a routine build-system maintenance commit. Reviewers should verify that the new CollectRootPointers/GenerateRootPointers build steps correctly capture all previously manually declared root pointers (trezorconfig_ui_wait_callback, readline_hist, mmap_region_head) and that generated root_pointers.h is included in the firmware build. No immediate security response is indicated, but downstream firmware builds should confirm no root pointer regressions occur.
Security signals we found
GC root pointer handling changed
Build system now auto-collects root pointers instead of hard-coding them
Removes manual MICROPY_PORT_ROOT_POINTERS lists
No explicit vulnerability or CVE mentioned in commit
No changelog entry (marked [no changelog])
Evidence from the diff
The patch migrates the Trezor Core build from the legacy MICROPY_PORT_ROOT_POINTERS macro to MicroPython’s newer MP_REGISTER_ROOT_POINTER registration system. It adds build steps (both SCons and Rust MpyBuilder) that scan preprocessed .upydef files for MP_REGISTER_ROOT_POINTER(…) statements, collect them, and run vendor/micropython/py/make_root_pointers.py to generate genhdr/root_pointers.h. The only concrete root pointer currently being registered is trezorconfig_ui_wait_callback in modtrezorconfig.c. The change also broadens the module registration grep to catch MP_REGISTER_EXTENSIBLE_MODULE. This is infrastructure work to stay in sync with upstream MicroPython and reduce manual root-pointer bookkeeping.
Changed components
core/SConscript.firmwarecore/SConscript.unixcore/embed/projects/firmware/mpconfigport.hcore/embed/projects/unix/mpconfigport.hcore/embed/upymod/build.rscore/embed/upymod/modtrezorconfig/modtrezorconfig.ccore/site_scons/site_tools/micropython/__init__.pyMicroPython GC root pointer generationInspect captured patch +75 / −10
diff --git a/core/SConscript.firmware b/core/SConscript.firmware
index 6bc16164..b817b019 100644
--- a/core/SConscript.firmware
+++ b/core/SConscript.firmware
@@ -559,6 +559,7 @@ env.Replace(
MAKEQSTRDATA='$PYTHON vendor/micropython/py/makeqstrdata.py',
MAKEVERSIONHDR='$PYTHON vendor/micropython/py/makeversionhdr.py',
MAKEMODULEDEFS='$PYTHON vendor/micropython/py/makemoduledefs.py',
+ MAKEROOTPOINTERS='$PYTHON vendor/micropython/py/make_root_pointers.py',
MAKECMAKELISTS='$PYTHON tools/make_cmakelists.py',
MPY_TOOL='$PYTHON vendor/micropython/tools/mpy-tool.py',
MPY_CROSS='vendor/micropython/mpy-cross/build/mpy-cross -O' + BYTECODE_OPTIMIZATION,
@@ -635,6 +636,18 @@ compressed_data = env.GenerateCompressed(
env.Ignore(micropy_defines, compressed_data)
+#
+# Micropython root pointers
+#
+
+rootpointers_collected = env.CollectRootPointers(
+ target='genhdr/root_pointers.collected', source=micropy_defines)
+
+rootpointers_data = env.GenerateRootPointers(
+ target='genhdr/root_pointers.h', source=rootpointers_collected)
+
+env.Ignore(micropy_defines, rootpointers_data)
+
#
# Micropython version
#
diff --git a/core/SConscript.unix b/core/SConscript.unix
index 5a858fd4..9a0a8298 100644
--- a/core/SConscript.unix
+++ b/core/SConscript.unix
@@ -578,6 +578,7 @@ env.Replace(
MAKEQSTRDATA='$PYTHON vendor/micropython/py/makeqstrdata.py',
MAKEVERSIONHDR='$PYTHON vendor/micropython/py/makeversionhdr.py',
MAKEMODULEDEFS='$PYTHON vendor/micropython/py/makemoduledefs.py',
+ MAKEROOTPOINTERS='$PYTHON vendor/micropython/py/make_root_pointers.py',
MAKECMAKELISTS='$PYTHON tools/make_cmakelists.py',
MPY_TOOL='$PYTHON vendor/micropython/tools/mpy-tool.py',
MPY_CROSS='vendor/micropython/mpy-cross/build/mpy-cross -O' + BYTECODE_OPTIMIZATION,
@@ -655,6 +656,18 @@ compressed_data = env.GenerateCompressed(
env.Ignore(micropy_defines, compressed_data)
+#
+# Micropython root pointers
+#
+
+rootpointers_collected = env.CollectRootPointers(
+ target='genhdr/root_pointers.collected', source=micropy_defines)
+
+rootpointers_data = env.GenerateRootPointers(
+ target='genhdr/root_pointers.h', source=rootpointers_collected)
+
+env.Ignore(micropy_defines, rootpointers_data)
+
#
# Micropython version
#
diff --git a/core/embed/projects/firmware/mpconfigport.h b/core/embed/projects/firmware/mpconfigport.h
index cdd773c9..f5c710ae 100644
--- a/core/embed/projects/firmware/mpconfigport.h
+++ b/core/embed/projects/firmware/mpconfigport.h
@@ -219,9 +219,6 @@ typedef long mp_off_t;
#define free(p) m_free(p)
#define realloc(p, n) m_realloc(p, n)
-#define MICROPY_PORT_ROOT_POINTERS \
- mp_obj_t trezorconfig_ui_wait_callback; \
-
// We need to provide a declaration/definition of alloca()
#include <alloca.h>
diff --git a/core/embed/projects/unix/mpconfigport.h b/core/embed/projects/unix/mpconfigport.h
index 046c0df7..3852e03a 100644
--- a/core/embed/projects/unix/mpconfigport.h
+++ b/core/embed/projects/unix/mpconfigport.h
@@ -286,11 +286,6 @@ void mp_unix_mark_exec(void);
// with EINTR, updates remaining timeout value.
#define MICROPY_SELECT_REMAINING_TIME (1)
-#define MICROPY_PORT_ROOT_POINTERS \
- const char *readline_hist[50]; \
- void *mmap_region_head; \
- mp_obj_t trezorconfig_ui_wait_callback; \
-
// We need to provide a declaration/definition of alloca()
// unless support for it is disabled.
#if !defined(MICROPY_NO_ALLOCA) || MICROPY_NO_ALLOCA == 0
diff --git a/core/embed/upymod/build.rs b/core/embed/upymod/build.rs
index 591ff49d..1077c156 100644
--- a/core/embed/upymod/build.rs
+++ b/core/embed/upymod/build.rs
@@ -505,6 +505,14 @@ impl<'a> MpyBuilder<'a> {
// can be included directly in firmware.
self.build_compressed_data(&compressed_collected)?;
+ // Extract all MP_REGISTER_ROOT_POINTER(...); statements from preprocessed
+ // .upydef files and store them in root_pointers.collected.h.
+ let root_pointers_collected = self.build_root_pointers_collected(&upydefs)?;
+
+ // Run make_root_pointers.py on root_pointers.collected.h to generate
+ // root_pointers.h that micropython uses in the definition of `mp_state_vm_t`.
+ self.build_root_pointers_data(&root_pointers_collected)?;
+
// Generate protobuf blobs based on .proto for Rust code.
self.build_protobuf_blobs(&qstr_generated)?;
@@ -633,7 +641,7 @@ impl<'a> MpyBuilder<'a> {
let output = self.genhdr_dir.join("moduledefs.collected.h");
let mut cmd = std::process::Command::new("sh");
cmd.arg("-c")
- .arg(r#"out="$1"; shift; grep '^MP_REGISTER_MODULE' "$@" > "$out""#)
+ .arg(r#"out="$1"; shift; grep -E '^MP_REGISTER(_EXTENSIBLE)?_MODULE' "$@" > "$out""#)
.arg("sh")
.arg(&output)
.args(upydef_files);
@@ -777,6 +785,35 @@ impl<'a> MpyBuilder<'a> {
Ok(output)
}
+ fn build_root_pointers_collected(&self, upydef_files: &[PathBuf]) -> Result<PathBuf> {
+ let output = self.genhdr_dir.join("root_pointers.collected.h");
+ let mut cmd = std::process::Command::new("sh");
+ cmd
+ .arg("-c")
+ .arg(r#"out="$1"; shift; cat "$@" | sed -nr 's/.*(MP_REGISTER_ROOT_POINTER\(.*\);).*/\1/p' > "$out""#)
+ .arg("sh")
+ .arg(&output)
+ .args(upydef_files);
+
+ let inputs = upydef_files.iter().collect::<Vec<_>>();
+ xbuild::run_command(&mut cmd, &inputs, [&output])
+ .context("Failed to build root_pointers collected")?;
+
+ Ok(output)
+ }
+
+ fn build_root_pointers_data(&self, root_pointers_collected: &Path) -> Result<PathBuf> {
+ let mut cmd = std::process::Command::new("python3");
+ let tool = self.mpy_dir.join("py/make_root_pointers.py");
+ cmd.arg(&tool).arg(root_pointers_collected);
+
+ let output = self.genhdr_dir.join("root_pointers.h");
+ let inputs = [tool, root_pointers_collected.to_path_buf()];
+ xbuild::run_command_to_file(&mut cmd, &inputs, &output)
+ .context("Failed to build root_pointers data")?;
+ Ok(output)
+ }
+
fn build_mpy_cross(&self) -> Result<PathBuf> {
// Here we build `mpy-cross` by calling make directly, so dependency
// tracking is left entirely to the makefiles in the mpy-cross source.
diff --git a/core/embed/upymod/modtrezorconfig/modtrezorconfig.c b/core/embed/upymod/modtrezorconfig/modtrezorconfig.c
index 678450bc..ac8ecc42 100644
--- a/core/embed/upymod/modtrezorconfig/modtrezorconfig.c
+++ b/core/embed/upymod/modtrezorconfig/modtrezorconfig.c
@@ -31,6 +31,8 @@
#include "memzero.h"
+MP_REGISTER_ROOT_POINTER(mp_obj_t trezorconfig_ui_wait_callback);
+
static secbool wrapped_ui_wait_callback(uint32_t wait, uint32_t progress,
enum storage_ui_message_t message) {
if (mp_obj_is_callable(MP_STATE_VM(trezorconfig_ui_wait_callback))) {
diff --git a/core/site_scons/site_tools/micropython/__init__.py b/core/site_scons/site_tools/micropython/__init__.py
index d354404d..53123440 100644
--- a/core/site_scons/site_tools/micropython/__init__.py
+++ b/core/site_scons/site_tools/micropython/__init__.py
@@ -30,7 +30,7 @@ def generate(env):
)
env["BUILDERS"]["CollectModules"] = SCons.Builder.Builder(
- action="grep ^MP_REGISTER_MODULE $SOURCES > $TARGET"
+ action="grep -E '^MP_REGISTER(_EXTENSIBLE)?_MODULE' $SOURCES > $TARGET"
# action="$CC -E $CCFLAGS_QSTR $CFLAGS $CCFLAGS $_CCCOMCOM $SOURCES"
# " | $PYTHON $MODULECOL > $TARGET"
)
@@ -43,6 +43,14 @@ def generate(env):
action="$MAKECOMPRESSEDDATA $SOURCE > $TARGET",
)
+ env["BUILDERS"]["CollectRootPointers"] = SCons.Builder.Builder(
+ action="cat $SOURCES | sed -nr 's/.*(MP_REGISTER_ROOT_POINTER\\(.*\\);).*/\\1/p' > $TARGET",
+ )
+
+ env["BUILDERS"]["GenerateRootPointers"] = SCons.Builder.Builder(
+ action="$MAKEROOTPOINTERS $SOURCE > $TARGET"
+ )
+
def generate_frozen_module(source, target, env, for_signature):
target = str(target[0])
source = str(source[0])
Why this scored 30/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.