fix(core/xtask): place combined sections at their flash offsets
What changed, and why it matters
This commit fixes a bug in the tool that builds Trezor hardware wallet flash images. Previously, when combining the boardloader, bootloader, and firmware into one file, later sections were placed too early because earlier files were smaller than their reserved flash space. The fix pads the gaps with zeros so each piece lands at its correct memory address, preventing the resulting image from failing to boot. It is a build-time tooling fix, not a runtime vulnerability in the device itself.
Treat as a build-system bug fix rather than a security vulnerability. Verify that generated combined images now match expected memory layouts and that CI tests pass. No emergency device firmware update is indicated solely from this commit.
Security signals we found
Incorrect binary layout could produce unbootable or misaligned firmware images
Build-time tooling bug with potential reliability/integrity implications for shipped images
No runtime exploit primitive visible in the diff
Fix includes defensive overlap check and regression tests
Evidence from the diff
The change is in core/embed/xtask/src/combine.rs, which produces combined.bin images for flashing. The old code concatenated boardloader.bin, bootloader.bin, and firmware.bin back-to-back, but those .bin files are content-sized (no trailing padding), so bootloader and firmware ended up at incorrect offsets relative to BOARDLOADER_START. The new code reads BOOTLOADER_START and FIRMWARE_START from memory.ld, places each section at its real flash offset, pads intervening gaps with 0x00, and adds an overlap check. Unit tests verify padding and overlap rejection. Affects all models.
Changed components
core/embed/xtask/src/combine.rsTrezor combined firmware image generation for all modelsInspect captured patch +105 / −14
diff --git a/core/embed/xtask/src/combine.rs b/core/embed/xtask/src/combine.rs
index 4ad06fe5..b9fe7faf 100644
--- a/core/embed/xtask/src/combine.rs
+++ b/core/embed/xtask/src/combine.rs
@@ -1,4 +1,4 @@
-use anyhow::{Context, Result};
+use anyhow::{Context, Result, ensure};
use std::fs;
use crate::{
@@ -8,6 +8,10 @@ use crate::{
const COMBINED_PREFIX: &str = "combined-";
+/// Byte used to pad the gaps between combined sections. Matches the original
+/// `combine_firmware.py`, which padded with zero bytes.
+const SECTION_PADDING: u8 = 0x00;
+
fn load_binary(model: Model, project: Project) -> Result<Vec<u8>> {
let path = helpers::artifacts_dir(model)?.join(format!("{}.bin", project.binary_name()));
println!("Loading `{}`", path.display());
@@ -16,37 +20,87 @@ fn load_binary(model: Model, project: Project) -> Result<Vec<u8>> {
Ok(data)
}
+/// Places `data` at `offset` within `binary`, padding any preceding gap with
+/// [`SECTION_PADDING`]. Each section must start at or after the current end of
+/// the image, otherwise the sections would overlap.
+fn place_section(binary: &mut Vec<u8>, offset: usize, data: &[u8]) -> Result<()> {
+ ensure!(
+ binary.len() <= offset,
+ "combined sections overlap: next section starts at 0x{:X} but image is already 0x{:X} bytes",
+ offset,
+ binary.len()
+ );
+ binary.resize(offset, SECTION_PADDING);
+ binary.extend_from_slice(data);
+ Ok(())
+}
+
/// Combines multiple firmware projects into a single binary for flashing.
+///
+/// The combined image starts at `BOARDLOADER_START` (the address it is flashed
+/// to) and places every section at its real offset within flash, padding the
+/// gaps between sections.
pub fn combine(args: CombineArgs) -> Result<()> {
let memory_ld = args.model.model_memory_ld()?;
- // Calculate the offset of boardloader from the start of flash
- let flash_start = helpers::read_symbol(&memory_ld, "FLASH_START")?;
- let boardloader_start = helpers::read_symbol(&memory_ld, "BOARDLOADER_START")?;
- let offset = boardloader_start - flash_start;
+ // All offsets are relative to the boardloader, which sits at the start of
+ // the combined image.
+ let base = helpers::read_symbol(&memory_ld, "BOARDLOADER_START")?;
+ let offset_of = |symbol: &str| -> Result<usize> {
+ Ok((helpers::read_symbol(&memory_ld, symbol)? - base) as usize)
+ };
+
+ let mut binary = Vec::new();
- // Create an binary with leading offset zeroes
- let mut binary = vec![0u8; offset as usize];
+ place_section(
+ &mut binary,
+ 0,
+ &load_binary(args.model, Project::Boardloader)?,
+ )?;
- binary.extend_from_slice(&load_binary(args.model, Project::Boardloader)?);
+ let bootloader_off = offset_of("BOOTLOADER_START")?;
match args.project {
Project::Bootloader => {
- binary.extend_from_slice(&load_binary(args.model, Project::Bootloader)?);
+ place_section(
+ &mut binary,
+ bootloader_off,
+ &load_binary(args.model, Project::Bootloader)?,
+ )?;
}
Project::BootloaderCi => {
- binary.extend_from_slice(&load_binary(args.model, Project::BootloaderCi)?);
+ place_section(
+ &mut binary,
+ bootloader_off,
+ &load_binary(args.model, Project::BootloaderCi)?,
+ )?;
}
Project::Firmware => {
- binary.extend_from_slice(&load_binary(args.model, Project::Bootloader)?);
- binary.extend_from_slice(&load_binary(args.model, Project::Firmware)?);
+ place_section(
+ &mut binary,
+ bootloader_off,
+ &load_binary(args.model, Project::Bootloader)?,
+ )?;
+ place_section(
+ &mut binary,
+ offset_of("FIRMWARE_START")?,
+ &load_binary(args.model, Project::Firmware)?,
+ )?;
}
Project::Prodtest => {
- binary.extend_from_slice(&load_binary(args.model, Project::Bootloader)?);
- binary.extend_from_slice(&load_binary(args.model, Project::Prodtest)?);
+ place_section(
+ &mut binary,
+ bootloader_off,
+ &load_binary(args.model, Project::Bootloader)?,
+ )?;
+ place_section(
+ &mut binary,
+ offset_of("FIRMWARE_START")?,
+ &load_binary(args.model, Project::Prodtest)?,
+ )?;
}
_ => anyhow::bail!(
@@ -84,3 +138,40 @@ pub fn combine(args: CombineArgs) -> Result<()> {
Ok(())
}
+
+#[cfg(test)]
+mod tests {
+ use super::{SECTION_PADDING, place_section};
+
+ #[test]
+ fn places_sections_at_their_offsets_and_pads_gaps() {
+ let mut binary = Vec::new();
+ place_section(&mut binary, 0, &[1, 2, 3]).unwrap();
+ // Gap from 3 to 8 must be padded with the padding byte.
+ place_section(&mut binary, 8, &[4, 5]).unwrap();
+
+ assert_eq!(
+ binary,
+ [
+ 1,
+ 2,
+ 3,
+ SECTION_PADDING,
+ SECTION_PADDING,
+ SECTION_PADDING,
+ SECTION_PADDING,
+ SECTION_PADDING,
+ 4,
+ 5
+ ]
+ );
+ }
+
+ #[test]
+ fn rejects_overlapping_sections() {
+ let mut binary = Vec::new();
+ place_section(&mut binary, 0, &[1, 2, 3, 4]).unwrap();
+ // Offset 2 falls inside the already-placed first section.
+ assert!(place_section(&mut binary, 2, &[5, 6]).is_err());
+ }
+}
Why this scored 29/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.