From ac2e921a2f0711df5915d6fa91b85484ab14732d Mon Sep 17 00:00:00 2001 From: David Brown Date: Wed, 29 Jul 2026 09:30:03 -0600 Subject: [PATCH 1/2] sim: export boot_scratch_trailer_sz through the C shim The simulator's swap-scratch trailer estimate re-derives the scratch trailer size by hand as 3 * align + (boot_trailer_sz - boot_status_sz), which hardcodes BOOT_STATUS_STATE_COUNT and recovers boot_trailer_info_sz() by subtraction. Expose the firmware's own boot_scratch_trailer_sz() the way boot_trailer_sz() and boot_status_sz() are already exposed, so the sim can call it directly instead. bootutil_area.c only compiles the function for swap-using-scratch builds, so run.c supplies a stub returning zero for the other configurations; the Rust side only calls it on the scratch path. Signed-off-by: David Brown Assisted-by: Claude:opus-5 --- sim/mcuboot-sys/csupport/run.c | 14 ++++++++++++++ sim/mcuboot-sys/src/c.rs | 8 ++++++++ 2 files changed, 22 insertions(+) diff --git a/sim/mcuboot-sys/csupport/run.c b/sim/mcuboot-sys/csupport/run.c index 847a0f22a6..665f5e7107 100644 --- a/sim/mcuboot-sys/csupport/run.c +++ b/sim/mcuboot-sys/csupport/run.c @@ -610,3 +610,17 @@ uint32_t boot_magic_sz(void) { return BOOT_MAGIC_ALIGN_SIZE; } + +#if !MCUBOOT_SWAP_USING_SCRATCH +/* + * bootutil_area.c only compiles boot_scratch_trailer_sz() for + * swap-using-scratch builds, but the simulator declares it unconditionally so + * that the Rust side links in every configuration. The Rust side only calls + * it when swap-using-scratch is active. + */ +uint32_t boot_scratch_trailer_sz(uint32_t min_write_sz) +{ + (void)min_write_sz; + return 0; +} +#endif diff --git a/sim/mcuboot-sys/src/c.rs b/sim/mcuboot-sys/src/c.rs index a7907e1f61..c492385651 100644 --- a/sim/mcuboot-sys/src/c.rs +++ b/sim/mcuboot-sys/src/c.rs @@ -157,6 +157,13 @@ pub fn boot_status_sz(align: u32) -> u32 { unsafe { raw::boot_status_sz(align) } } +/// The size of the trailer written to the scratch area. Only meaningful in +/// swap-using-scratch builds; other configurations link against a stub in +/// `csupport/run.c` that returns zero. +pub fn boot_scratch_trailer_sz(align: u32) -> u32 { + unsafe { raw::boot_scratch_trailer_sz(align) } +} + pub fn boot_magic_sz() -> usize { unsafe { raw::boot_magic_sz() as usize } } @@ -220,6 +227,7 @@ mod raw { pub fn boot_trailer_sz(min_write_sz: u32) -> u32; pub fn boot_status_sz(min_write_sz: u32) -> u32; + pub fn boot_scratch_trailer_sz(min_write_sz: u32) -> u32; pub fn boot_magic_sz() -> u32; pub fn boot_max_align() -> u32; From 5cf44b7c23b8ddc2210c574bc805273fc2a26f90 Mon Sep 17 00:00:00 2001 From: David Brown Date: Wed, 29 Jul 2026 09:34:03 -0600 Subject: [PATCH 2/2] sim: consider both slots in the swap-scratch trailer estimate estimate_swap_scratch_trailer_size() computed the trailer padding from the geometry of a single slot -- whichever one the caller happened to be installing an image into. The firmware does not: app_max_size_adjust_ to_trailer() in swap_scratch.c evaluates get_first_trailer_sector_end_ off() for both the primary and the secondary slot and takes the larger, because the swap logic copies on the common sector boundary of the two slots and so the sector that matters is the larger one. On a device whose two slots have different sector sizes the sim could therefore compute padding from the small sector while the bootloader computed it from the large one, understating the space available to an image. Since the maximum image size is a per-image property rather than a per-slot one, an image installed into the primary and one installed into the secondary also disagreed with each other. Mirror the firmware: take the maximum of the two slots' first-trailer- sector remainders, and drop the now-redundant slot index from the caller chain. Also match the firmware's stopping condition when walking back over the trailer sectors -- boot_get_first_trailer_sector() stops once the accumulated sectors reach the trailer size, so an exact fit leaves a whole sector rather than zero -- and use boot_scratch_trailer_sz() instead of re-deriving it. This is a test-fixture change; no bootloader behaviour is affected. It is very nearly a no-op today: the only configuration whose numbers move is Nrf52840SpiFlash (4K primary sectors, 8K secondary sectors) at max-align 32, where images installed into the primary slot gain the 96 bytes the bootloader had always been willing to give them. It matters more for the pending work raising MCUBOOT_BOOT_MAX_ALIGN to 64 and 128, where the divergence grows large enough to make the oversized_secondary_slot test build an image that is not actually oversized. Signed-off-by: David Brown Assisted-by: Claude:opus-5 --- sim/src/image.rs | 83 +++++++++++++++++++++++++++++------------------- 1 file changed, 51 insertions(+), 32 deletions(-) diff --git a/sim/src/image.rs b/sim/src/image.rs index d5fa19f2de..19754b7840 100644 --- a/sim/src/image.rs +++ b/sim/src/image.rs @@ -2072,18 +2072,11 @@ fn boot_sector_size(dev: &dyn Flash) -> usize { } } -/// Estimate the number of bytes in each slot that must be reserved for the trailer when -/// swap-scratch is used. -fn estimate_swap_scratch_trailer_size(dev: &dyn Flash, areadesc: &AreaDesc, slot: &SlotInfo) -> usize { - // Compute the minimal size that must be allocated to the trailer, without considering the - // trailer in the sratch area. - let mut trailer_sz = c::boot_trailer_sz(dev.align() as u32) as usize; - - // If the trailer is not a multiple of the sector size, the last sector that can hold firmware - // data also contains the trailer or a part of it. Let's compute the size of the part of the - // trailer that is in the last firmware sector. - let mut trailer_sz_in_fw_sector = trailer_sz; - +/// The number of bytes of a `trailer_sz`-byte trailer that land in the first +/// (lowest-addressed) sector of `slot` that holds any trailer data. Mirrors +/// `get_first_trailer_sector_end_off() - slot_trailer_off` in swap_scratch.c. +fn trailer_sz_in_first_trailer_sector(areadesc: &AreaDesc, slot: &SlotInfo, + trailer_sz: usize) -> usize { let flash_id = match slot.index { 0 => FlashId::Image0, 1 => FlashId::Image1, @@ -2098,32 +2091,56 @@ fn estimate_swap_scratch_trailer_size(dev: &dyn Flash, areadesc: &AreaDesc, slot size => vec![size; slot.len / size], }; + // Walk back from the end of the slot, consuming whole sectors that are + // entirely trailer. The firmware stops as soon as the accumulated sectors + // reach the trailer size, so an exact fit leaves a whole sector here, not + // zero. + let mut remaining = trailer_sz; for §or_sz in slot_sectors.iter().rev() { - if sector_sz > trailer_sz_in_fw_sector { - break; + if sector_sz >= remaining { + return remaining; } - trailer_sz_in_fw_sector -= sector_sz; + remaining -= sector_sz; } - // If the trailer is not a multiple of the sector size, when the last sector containing firmware - // data will be copied to the scratch area, it must be ensured enough space is left to write the - // scratch trailer. - if trailer_sz_in_fw_sector != 0 { - // The scratch contains a single boot status entry - let boot_status_entry_sz = 3 * dev.align(); - let trailer_info_sz = trailer_sz - c::boot_status_sz(dev.align() as u32) as usize; - let scratch_trailer_sz = boot_status_entry_sz + trailer_info_sz; + panic!("Trailer does not fit in slot {}", slot.index); +} - if scratch_trailer_sz > trailer_sz_in_fw_sector { - trailer_sz += scratch_trailer_sz - trailer_sz_in_fw_sector; - } +/// Estimate the number of bytes in each slot that must be reserved for the trailer when +/// swap-scratch is used. Mirrors `app_max_size_adjust_to_trailer()` in +/// boot/bootutil/src/swap_scratch.c. +fn estimate_swap_scratch_trailer_size(dev: &dyn Flash, areadesc: &AreaDesc, + slots: &[SlotInfo]) -> usize { + // Compute the minimal size that must be allocated to the trailer, without considering the + // trailer in the sratch area. + let mut trailer_sz = c::boot_trailer_sz(dev.align() as u32) as usize; + + debug_assert!(slots.iter().all(|s| s.len == slots[0].len), + "swap-scratch trailer estimate assumes equally-sized slots"); + + // If the trailer is not a multiple of the sector size, the last sector that can hold firmware + // data also contains the trailer or a part of it. The swap logic copies on the *common* sector + // boundary of the two slots, so the sector that matters is the larger one. The firmware takes + // the larger of the two slots' first-trailer-sector end offsets; with equally-sized slots the + // trailer starts at the same offset in both, so that is the larger of the two remainders. + let trailer_sz_in_fw_sector = slots.iter() + .map(|slot| trailer_sz_in_first_trailer_sector(areadesc, slot, trailer_sz)) + .max() + .expect("no slots"); + + // When the last sector containing firmware data is copied to the scratch area, enough space + // must be left to write the scratch trailer. + let scratch_trailer_sz = c::boot_scratch_trailer_sz(dev.align() as u32) as usize; + + if scratch_trailer_sz > trailer_sz_in_fw_sector { + trailer_sz += scratch_trailer_sz - trailer_sz_in_fw_sector; } trailer_sz } -fn image_largest_trailer(dev: &dyn Flash, areadesc: &AreaDesc, slot: &SlotInfo) -> usize { +fn image_largest_trailer(dev: &dyn Flash, areadesc: &AreaDesc, slots: &[SlotInfo]) -> usize { // Using the header size we know, the trailer size, and the slot size, we can compute // the largest image possible. let trailer = if Caps::OverwriteUpgrade.present() { @@ -2133,7 +2150,7 @@ fn image_largest_trailer(dev: &dyn Flash, areadesc: &AreaDesc, slot: &SlotInfo) let sector_size = boot_sector_size(dev) as u32; align_up(c::boot_trailer_sz(dev.align() as u32), sector_size) as usize } else if Caps::SwapUsingScratch.present() { - estimate_swap_scratch_trailer_size(dev, areadesc, slot) + estimate_swap_scratch_trailer_size(dev, areadesc, slots) } else { panic!("The maximum image size can't be calculated.") }; @@ -2156,14 +2173,16 @@ fn required_slot_padding(dev: &dyn Flash) -> usize { // Computes the largest possible firmware image size, not including the header and TLV area. fn compute_largest_image_size(dev: &dyn Flash, areadesc: &AreaDesc, slots: &[SlotInfo], - slot_ind: usize, hdr_size: usize, tlv: &dyn ManifestGen) -> usize { + hdr_size: usize, tlv: &dyn ManifestGen) -> usize { let slot_len = if Caps::SwapUsingOffset.present() { slots[1].len } else { slots[0].len }; - let trailer = image_largest_trailer(dev, areadesc, &slots[slot_ind]); + // Note that the trailer reservation is a property of the image, not of the slot being + // installed into: the firmware's app_max_size() considers both slots. + let trailer = image_largest_trailer(dev, areadesc, slots); let padding = required_slot_padding(dev); let tlv_len = tlv.estimate_size(); info!("slot: 0x{:x}, HDR: 0x{:x}, trailer: 0x{:x}, tlv_len: 0x{:x}, padding: 0x{:x}", @@ -2255,10 +2274,10 @@ fn install_image_with_key( let len = match len { ImageSize::Given(size) => size, - ImageSize::Largest => compute_largest_image_size(dev, areadesc, slots, slot_ind, + ImageSize::Largest => compute_largest_image_size(dev, areadesc, slots, HDR_SIZE, tlv.as_ref()), ImageSize::Oversized => { - let largest_img_sz = compute_largest_image_size(dev, areadesc, slots, slot_ind, + let largest_img_sz = compute_largest_image_size(dev, areadesc, slots, HDR_SIZE, tlv.as_ref()); largest_img_sz + dev.align() }