fix(core/build): fix secmon flash usage calculation
What changed, and why it matters
This commit fixes a bug in an internal build tool that reports how much flash memory a Trezor firmware image uses. The tool was incorrectly counting reserved RAM-only sections (like the stack) as if they lived in flash, which could make the reported flash usage look much larger than it actually is. The fix only changes a calculation and a test; it does not change the firmware itself or any runtime behavior.
No security action required. Treat as a normal build-tooling fix. Reviewers may want to confirm that downstream CI size checks now use accurate flash figures, but this is a correctness improvement rather than a vulnerability remediation.
Security signals we found
Build tooling only; no runtime code changed
Fixes an incorrect memory-usage metric, not a memory corruption or cryptographic bug
No input from untrusted sources is parsed at runtime
No privilege boundary crossed
No changelog entry requested by vendor
Evidence from the diff
The change is in core/embed/xtask/src/memusage.rs, a build-time helper that parses GNU ld map files and prints memory usage. Previously, parse_output_sections discarded the section name, so used_bytes_for_region could not distinguish NOBITS sections (.bss, .stack, .heap) from real flash-loaded sections. GNU ld still advances the LMA cursor for NOBITS sections, so the tool was adding their phantom load addresses to the FLASH total. The patch stores the section name, adds is_nobits_section() to identify .bss/.stack/.heap (and dotted subsections), and excludes those sections when summing flash usage. A unit test verifies that a 64 KB stack no longer inflates the reported flash figure.
Changed components
core/embed/xtask/src/memusage.rsInspect captured patch +54 / −1
diff --git a/core/embed/xtask/src/memusage.rs b/core/embed/xtask/src/memusage.rs
index cf51fe40..992d4677 100644
--- a/core/embed/xtask/src/memusage.rs
+++ b/core/embed/xtask/src/memusage.rs
@@ -10,11 +10,25 @@ struct MemoryRegion {
#[derive(Debug, Clone)]
struct OutputSection {
+ name: String,
address: u64,
size: u64,
load_address: Option<u64>,
}
+/// Returns true for zero-initialized / reserved sections (`.bss`, `.stack`,
+/// `.heap`). These are NOBITS: they occupy RAM but carry no bytes in the flash
+/// image. GNU ld nonetheless reports a `load address` for them (the LMA cursor
+/// continues past the preceding `AT>FLASH` `.data` section), which must NOT be
+/// counted as flash usage — otherwise a large reserved stack inflates the
+/// reported figure well beyond the real image size.
+fn is_nobits_section(name: &str) -> bool {
+ const NOBITS: [&str; 3] = [".bss", ".stack", ".heap"];
+ NOBITS
+ .iter()
+ .any(|prefix| name == *prefix || name.starts_with(&format!("{prefix}.")))
+}
+
/// Prints a table of memory usage by region, based on the contents of
/// the given map file.
pub fn print_memusage(mapfile: &Path) -> Result<()> {
@@ -198,7 +212,7 @@ fn parse_output_sections(content: &str) -> Result<Vec<OutputSection>> {
}
let mut parts = line.split_whitespace();
- let Some(_name) = parts.next() else {
+ let Some(name) = parts.next() else {
continue;
};
let Some(address) = parts.next() else {
@@ -224,6 +238,7 @@ fn parse_output_sections(content: &str) -> Result<Vec<OutputSection>> {
}
sections.push(OutputSection {
+ name: name.to_string(),
address,
size,
load_address,
@@ -246,8 +261,12 @@ fn used_bytes_for_region(region: &MemoryRegion, sections: &[OutputSection]) -> u
ranges.push(range);
}
+ // A NOBITS section (.bss/.stack/.heap) carries no bytes in the flash
+ // image; its reported load address is a phantom from the LMA cursor and
+ // must not be counted as flash usage.
if let Some(load_address) = section.load_address
&& load_address != section.address
+ && !is_nobits_section(§ion.name)
&& let Some(range) =
intersect_range(load_address, section.size, region.origin, region_end)
{
@@ -377,6 +396,40 @@ Linker script and memory map
assert_eq!(parse_symbol_value(map, "__missing_symbol"), None);
}
+ #[test]
+ fn ignores_nobits_load_addresses() {
+ // `.bss` and `.stack` are NOBITS: they live in RAM but the linker
+ // reports a flash `load address` for them. That phantom LMA (here a
+ // huge 64 KB stack) must not be counted as flash usage.
+ let map = r#"
+Memory Configuration
+
+Name Origin Length Attributes
+FLASH 0x08000000 0x00010000 xr
+RAM 0x20000000 0x00020000 rw
+*default* 0x00000000 0xffffffff
+
+Linker script and memory map
+
+.flash 0x08000000 0x100
+.data 0x20000000 0x20 load address 0x08000100
+.bss 0x20000020 0x40 load address 0x08000120
+.stack 0x20000060 0x10000 load address 0x08000120
+"#;
+
+ let regions = parse_memory_regions(map).expect("memory regions should parse");
+ let sections = parse_output_sections(map).expect("sections should parse");
+
+ let flash = regions.iter().find(|r| r.name == "FLASH").unwrap();
+ let ram = regions.iter().find(|r| r.name == "RAM").unwrap();
+
+ // Flash: .flash (0x100) + .data load image (0x20); the .bss/.stack
+ // phantom LMAs are excluded.
+ assert_eq!(used_bytes_for_region(flash, §ions), 0x120);
+ // RAM: .data + .bss + .stack VMAs are all counted as usual.
+ assert_eq!(used_bytes_for_region(ram, §ions), 0x10060);
+ }
+
#[test]
fn padding_symbol_does_not_confuse_section_parsing() {
// A symbol line (leading whitespace, no leading '.') must not be picked
Why this scored 18/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.