feat(core/build): handle bootloader padding in memusage
What changed, and why it matters
This commit changes how a build reporting tool calculates memory usage for the Trezor bootloader. Previously, the tool counted the zero padding that fills the bootloader image up to its maximum size as 'used' memory, making the bootloader always appear 100% full. The patch lets the linker export the padding size and subtracts it from the reported usage so developers see real content size versus padding. It is a build tooling and reporting improvement, not a fix for a runtime security bug.
No security action required. Treat as a normal build/telemetry improvement. Reviewers may verify that the new __flash_padding_size symbol is correctly placed after real content and before the zero fill, and that saturating_sub cannot underflow.
Security signals we found
No strong security signals were identified.
Evidence from the diff
The commit modifies two STM32U5 bootloader linker scripts (stm32u58 and stm32u5g) to define a new symbol __flash_padding_size marking the zero-fill padding at the end of the .flash section, and updates core/embed/xtask/src/memusage.rs to parse that symbol and subtract it from FLASH region usage. It also adds Rust unit tests for parsing the symbol and ensuring symbol lines are not mistaken for output sections. No executable code behavior on the device is changed; only the memory-usage reporting tool output is affected.
Changed components
core/embed/sys/linker/stm32u58/bootloader.ldcore/embed/sys/linker/stm32u5g/bootloader.ldcore/embed/xtask/src/memusage.rsInspect captured patch +87 / −4
diff --git a/core/embed/sys/linker/stm32u58/bootloader.ld b/core/embed/sys/linker/stm32u58/bootloader.ld
index c614e507..69a30110 100644
--- a/core/embed/sys/linker/stm32u58/bootloader.ld
+++ b/core/embed/sys/linker/stm32u58/bootloader.ld
@@ -28,6 +28,10 @@ _bootargs_ram_end = BOOTARGS_START + BOOTARGS_SIZE;
_bootloader_code_size = _bootloader_code_end - ADDR(.padding);
_codelen = _bootloader_code_end - ADDR(.flash);
+/* Size of the zero fill that pads the image to the full BOOTLOADER_MAXSIZE.
+ Reported by the memory-usage tool so it can show real content vs padding. */
+__flash_padding_size = _bootloader_code_end - __flash_padding_start;
+
SECTIONS {
.header : ALIGN(4) {
KEEP(*(.header));
@@ -91,6 +95,7 @@ SECTIONS {
.flash : {
/* Pad the rest of bootloader area with zeros */
+ __flash_padding_start = .;
BYTE(0x00)
FILL(0x00)
. = ADDR(.header) + BOOTLOADER_MAXSIZE;
diff --git a/core/embed/sys/linker/stm32u5g/bootloader.ld b/core/embed/sys/linker/stm32u5g/bootloader.ld
index c742d86a..0318cc6d 100644
--- a/core/embed/sys/linker/stm32u5g/bootloader.ld
+++ b/core/embed/sys/linker/stm32u5g/bootloader.ld
@@ -24,6 +24,10 @@ _bootargs_ram_end = BOOTARGS_START + BOOTARGS_SIZE;
_bootloader_code_size = _bootloader_code_end - ADDR(.padding);
+/* Size of the zero fill that pads the image to the full BOOTLOADER_MAXSIZE.
+ Reported by the memory-usage tool so it can show real content vs padding. */
+__flash_padding_size = _bootloader_code_end - __flash_padding_start;
+
SECTIONS {
.header : ALIGN(4) {
KEEP(*(.header));
@@ -87,6 +91,7 @@ SECTIONS {
.flash : {
/* Pad the rest of bootloader area with zeros */
+ __flash_padding_start = .;
BYTE(0x00)
FILL(0x00)
. = ADDR(.header) + BOOTLOADER_MAXSIZE;
diff --git a/core/embed/xtask/src/memusage.rs b/core/embed/xtask/src/memusage.rs
index f4242a34..cf51fe40 100644
--- a/core/embed/xtask/src/memusage.rs
+++ b/core/embed/xtask/src/memusage.rs
@@ -24,6 +24,11 @@ pub fn print_memusage(mapfile: &Path) -> Result<()> {
let regions = parse_memory_regions(&content)?;
let sections = parse_output_sections(&content)?;
+ // Some images (e.g. the boot_ucb bootloader) are zero-padded to fill their
+ // whole flash region. The linker exports `__flash_padding_size` so we can
+ // report real content usage instead of a misleading 100%.
+ let flash_padding = parse_symbol_value(&content, "__flash_padding_size");
+
println!(
"xtask: Memory usage from `{}`",
mapfile
@@ -37,7 +42,19 @@ pub fn print_memusage(mapfile: &Path) -> Result<()> {
);
for region in regions {
- let used = used_bytes_for_region(®ion, §ions);
+ let mut used = used_bytes_for_region(®ion, §ions);
+
+ // Exclude the fixed-size image padding from the reported usage so the
+ // figure reflects real content. The padding only applies to the FLASH
+ // region the image is linked into.
+ let mut note = String::new();
+ if region.name == "FLASH"
+ && let Some(padding) = flash_padding
+ {
+ used = used.saturating_sub(padding);
+ note = format!(" (+{} padding)", format_bytes(padding));
+ }
+
let percent = if region.length == 0 {
0.0
} else {
@@ -45,17 +62,33 @@ pub fn print_memusage(mapfile: &Path) -> Result<()> {
};
println!(
- "{:<16} {:>12} {:>12} {:>7.2}%",
+ "{:<16} {:>12} {:>12} {:>7.2}%{}",
region.name,
format_bytes(used),
format_bytes(region.length),
- percent
+ percent,
+ note
);
}
Ok(())
}
+/// Parses the value of a symbol assignment from the map file, i.e. a line of
+/// the form `0x<value> <name> = ...`.
+fn parse_symbol_value(content: &str, name: &str) -> Option<u64> {
+ for line in content.lines() {
+ let mut parts = line.split_whitespace();
+ let (Some(value), Some(symbol)) = (parts.next(), parts.next()) else {
+ continue;
+ };
+ if symbol == name && parts.next() == Some("=") {
+ return parse_hex(value).ok();
+ }
+ }
+ None
+}
+
/// Parses the memory regions from the "Memory Configuration" part of
/// the map file.
fn parse_memory_regions(content: &str) -> Result<Vec<MemoryRegion>> {
@@ -287,7 +320,9 @@ fn format_bytes(value: u64) -> String {
#[cfg(test)]
mod tests {
- use super::{parse_memory_regions, parse_output_sections, used_bytes_for_region};
+ use super::{
+ parse_memory_regions, parse_output_sections, parse_symbol_value, used_bytes_for_region,
+ };
#[test]
fn counts_runtime_and_load_addresses() {
@@ -319,4 +354,42 @@ Linker script and memory map
assert_eq!(used_bytes_for_region(flash, §ions), 0x1a0);
assert_eq!(used_bytes_for_region(ram, §ions), 0x60);
}
+
+ #[test]
+ fn parses_padding_symbol_value() {
+ // Symbol assignment lines look like this in the map file.
+ let map = r#"
+Linker script and memory map
+
+ 0x000018e4 __flash_padding_size = (_bootloader_code_end - __flash_padding_start)
+ 0x0c03c000 _bootloader_code_end = .
+"#;
+
+ assert_eq!(
+ parse_symbol_value(map, "__flash_padding_size"),
+ Some(0x18e4)
+ );
+ assert_eq!(
+ parse_symbol_value(map, "_bootloader_code_end"),
+ Some(0x0c03c000)
+ );
+ // A symbol that is not present yields None (no padding to subtract).
+ assert_eq!(parse_symbol_value(map, "__missing_symbol"), None);
+ }
+
+ #[test]
+ fn padding_symbol_does_not_confuse_section_parsing() {
+ // A symbol line (leading whitespace, no leading '.') must not be picked
+ // up as an output section.
+ let map = r#"
+Linker script and memory map
+
+.flash 0x0c016000 0x1f5d0
+ 0x000018e4 __flash_padding_size = 0x18e4
+"#;
+
+ let sections = parse_output_sections(map).expect("sections should parse");
+ assert_eq!(sections.len(), 1);
+ assert_eq!(sections[0].address, 0x0c016000);
+ }
}
Why this scored 15/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.