From fc8ea2ab441c358ed7b8fc48ab7bbe2203202d96 Mon Sep 17 00:00:00 2001 From: Tomasz Chyrowicz Date: Mon, 16 Jun 2025 17:23:23 +0200 Subject: [PATCH] loader: Allow to specify slot number in version Allow to depend on a specific slot while specifying the version number. This functionality is useful when the Direct XIP mode is used and the booting process of other images is done by the next stage, not the MCUboot itself. Signed-off-by: Tomasz Chyrowicz --- boot/bootutil/include/bootutil/image.h | 8 + boot/bootutil/src/loader.c | 167 +++++++++++++++++- boot/zephyr/Kconfig | 9 + .../include/mcuboot_config/mcuboot_config.h | 4 + docs/design.md | 17 ++ docs/imgtool.md | 13 +- scripts/imgtool/image.py | 3 +- scripts/imgtool/main.py | 32 +++- 8 files changed, 247 insertions(+), 6 deletions(-) diff --git a/boot/bootutil/include/bootutil/image.h b/boot/bootutil/include/bootutil/image.h index 57be8b1c45..2bd20061de 100644 --- a/boot/bootutil/include/bootutil/image.h +++ b/boot/bootutil/include/bootutil/image.h @@ -144,6 +144,10 @@ extern "C" { */ #define IMAGE_TLV_ANY 0xffff /* Used to iterate over all TLV */ +#define VERSION_DEP_SLOT_ACTIVE 0x00 /* Check dependency against active slot. */ +#define VERSION_DEP_SLOT_PRIMARY 0x01 /* Check dependency against primary slot. */ +#define VERSION_DEP_SLOT_SECONDARY 0x02 /* Check dependency against secondary slot. */ + STRUCT_PACKED image_version { uint8_t iv_major; uint8_t iv_minor; @@ -153,7 +157,11 @@ STRUCT_PACKED image_version { struct image_dependency { uint8_t image_id; /* Image index (from 0) */ +#ifdef MCUBOOT_VERSION_CMP_USE_SLOT_NUMBER + uint8_t slot; /* Image slot */ +#else uint8_t _pad1; +#endif /* MCUBOOT_VERSION_CMP_USE_SLOT_NUMBER */ uint16_t _pad2; struct image_version image_min_version; /* Indicates at minimum which * version of firmware must be diff --git a/boot/bootutil/src/loader.c b/boot/bootutil/src/loader.c index 37adc5faff..d30cd6c9c1 100644 --- a/boot/bootutil/src/loader.c +++ b/boot/bootutil/src/loader.c @@ -317,6 +317,24 @@ boot_verify_slot_dependency(struct boot_loader_state *state, uint8_t swap_type = state->swap_type[dep->image_id]; dep_slot = BOOT_IS_UPGRADE(swap_type) ? BOOT_SECONDARY_SLOT : BOOT_PRIMARY_SLOT; +#elif defined(MCUBOOT_VERSION_CMP_USE_SLOT_NUMBER) + switch(dep->slot) { + case VERSION_DEP_SLOT_ACTIVE: + dep_slot = state->slot_usage[dep->image_id].active_slot; + break; + case VERSION_DEP_SLOT_PRIMARY: + dep_slot = BOOT_PRIMARY_SLOT; + break; + case VERSION_DEP_SLOT_SECONDARY: + dep_slot = BOOT_SECONDARY_SLOT; + break; + default: + return -1; + } + + if (!state->slot_usage[dep->image_id].slot_available[dep_slot]) { + return -1; + } #else dep_slot = state->slot_usage[dep->image_id].active_slot; #endif @@ -354,7 +372,27 @@ boot_verify_slot_dependency(struct boot_loader_state *state, } #endif - return rc; +#ifdef MCUBOOT_VERSION_CMP_USE_SLOT_NUMBER + if (rc == 0) { + switch(dep->slot) { + case VERSION_DEP_SLOT_PRIMARY: + state->slot_usage[dep->image_id].slot_available[BOOT_PRIMARY_SLOT] = true; + state->slot_usage[dep->image_id].slot_available[BOOT_SECONDARY_SLOT] = false; + state->slot_usage[dep->image_id].active_slot = BOOT_PRIMARY_SLOT; + break; + case VERSION_DEP_SLOT_SECONDARY: + state->slot_usage[dep->image_id].slot_available[BOOT_PRIMARY_SLOT] = false; + state->slot_usage[dep->image_id].slot_available[BOOT_SECONDARY_SLOT] = true; + state->slot_usage[dep->image_id].active_slot = BOOT_SECONDARY_SLOT; + break; + case VERSION_DEP_SLOT_ACTIVE: + default: + break; + } + } +#endif /* MCUBOOT_VERSION_CMP_USE_SLOT_NUMBER */ + +return rc; } #if !defined(MCUBOOT_DIRECT_XIP) && !defined(MCUBOOT_RAM_LOAD) @@ -499,6 +537,19 @@ boot_verify_slot_dependencies(struct boot_loader_state *state, uint32_t slot) goto done; } +#ifdef MCUBOOT_VERSION_CMP_USE_SLOT_NUMBER + /* Validate against possible dependency slot values. */ + switch(dep->slot) { + case VERSION_DEP_SLOT_ACTIVE: + case VERSION_DEP_SLOT_PRIMARY: + case VERSION_DEP_SLOT_SECONDARY: + break; + default: + rc = BOOT_EBADARGS; + goto done; + } +#endif /* MCUBOOT_VERSION_CMP_USE_SLOT_NUMBER */ + /* Verify dependency and modify the swap type if not satisfied. */ rc = boot_verify_slot_dependency(state, &dep); if (rc != 0) { @@ -2674,6 +2725,119 @@ boot_select_or_erase(struct boot_loader_state *state) } #endif /* MCUBOOT_DIRECT_XIP && MCUBOOT_DIRECT_XIP_REVERT */ +#ifdef MCUBOOT_VERSION_CMP_USE_SLOT_NUMBER +/** + * Tries to load a slot for all the images with validation. + * + * @param state Boot loader status information. + * + * @return 0 on success; nonzero on failure. + */ +fih_ret +boot_load_and_validate_images(struct boot_loader_state *state) +{ + uint32_t active_slot; + int rc; + fih_ret fih_rc; + uint32_t slot; + + /* Go over all the images and all slots and validate them */ + IMAGES_ITER(BOOT_CURR_IMG(state)) { + for (slot = 0; slot < BOOT_NUM_SLOTS; slot++) { +#if BOOT_IMAGE_NUMBER > 1 + if (state->img_mask[BOOT_CURR_IMG(state)]) { + continue; + } +#endif + + /* Save the number of the active slot. */ + state->slot_usage[BOOT_CURR_IMG(state)].active_slot = slot; + +#ifdef MCUBOOT_DIRECT_XIP + rc = boot_rom_address_check(state); + if (rc != 0) { + /* The image is placed in an unsuitable slot. */ + state->slot_usage[BOOT_CURR_IMG(state)].slot_available[slot] = false; + state->slot_usage[BOOT_CURR_IMG(state)].active_slot = NO_ACTIVE_SLOT; + continue; + } + +#ifdef MCUBOOT_DIRECT_XIP_REVERT + rc = boot_select_or_erase(state); + if (rc != 0) { + /* The selected image slot has been erased. */ + state->slot_usage[BOOT_CURR_IMG(state)].slot_available[slot] = false; + state->slot_usage[BOOT_CURR_IMG(state)].active_slot = NO_ACTIVE_SLOT; + continue; + } +#endif /* MCUBOOT_DIRECT_XIP_REVERT */ +#endif /* MCUBOOT_DIRECT_XIP */ + +#ifdef MCUBOOT_RAM_LOAD + /* Image is first loaded to RAM and authenticated there in order to + * prevent TOCTOU attack during image copy. This could be applied + * when loading images from external (untrusted) flash to internal + * (trusted) RAM and image is authenticated before copying. + */ + rc = boot_load_image_to_sram(state); + if (rc != 0 ) { + /* Image cannot be ramloaded. */ + boot_remove_image_from_flash(state, slot); + state->slot_usage[BOOT_CURR_IMG(state)].slot_available[slot] = false; + state->slot_usage[BOOT_CURR_IMG(state)].active_slot = NO_ACTIVE_SLOT; + continue; + } +#endif /* MCUBOOT_RAM_LOAD */ + + FIH_CALL(boot_validate_slot, fih_rc, state, slot, NULL, 0); + if (FIH_NOT_EQ(fih_rc, FIH_SUCCESS)) { + /* Image is invalid. */ +#ifdef MCUBOOT_RAM_LOAD + boot_remove_image_from_sram(state); +#endif /* MCUBOOT_RAM_LOAD */ + state->slot_usage[BOOT_CURR_IMG(state)].slot_available[slot] = false; + state->slot_usage[BOOT_CURR_IMG(state)].active_slot = NO_ACTIVE_SLOT; + continue; + } + + /* Valid image loaded from a slot, go to the next slot. */ + state->slot_usage[BOOT_CURR_IMG(state)].active_slot = NO_ACTIVE_SLOT; + } + } + + /* Go over all the images and all slots and validate them */ + IMAGES_ITER(BOOT_CURR_IMG(state)) { + /* All slots tried until a valid image found. Breaking from this loop + * means that a valid image found or already loaded. If no slot is + * found the function returns with error code. */ + while (true) { + /* Go over all the slots and try to load one */ + active_slot = state->slot_usage[BOOT_CURR_IMG(state)].active_slot; + if (active_slot != NO_ACTIVE_SLOT){ + /* A slot is already active, go to next image. */ + break; + } + + active_slot = find_slot_with_highest_version(state); + if (active_slot == NO_ACTIVE_SLOT) { + BOOT_LOG_INF("No slot to load for image %d", + BOOT_CURR_IMG(state)); + FIH_RET(FIH_FAILURE); + } + + /* Save the number of the active slot. */ + state->slot_usage[BOOT_CURR_IMG(state)].active_slot = active_slot; + + /* Valid image loaded from a slot, go to the next image. */ + break; + } + } + + FIH_RET(FIH_SUCCESS); +} + +#else /* MCUBOOT_VERSION_CMP_USE_SLOT_NUMBER */ + /** * Tries to load a slot for all the images with validation. * @@ -2771,6 +2935,7 @@ boot_load_and_validate_images(struct boot_loader_state *state) FIH_RET(FIH_SUCCESS); } +#endif /* MCUBOOT_VERSION_CMP_USE_SLOT_NUMBER */ /** * Updates the security counter for the current image. diff --git a/boot/zephyr/Kconfig b/boot/zephyr/Kconfig index 5e23e6fb87..3b1257ee2d 100644 --- a/boot/zephyr/Kconfig +++ b/boot/zephyr/Kconfig @@ -969,6 +969,15 @@ config BOOT_VERSION_CMP_USE_BUILD_NUMBER minor and revision. Enable this option to take into account the build number as well. +config BOOT_VERSION_CMP_USE_SLOT_NUMBER + bool "Use slot number while comparing image version" + depends on (UPDATEABLE_IMAGE_NUMBER > 1) || BOOT_DIRECT_XIP || \ + BOOT_RAM_LOAD || MCUBOOT_DOWNGRADE_PREVENTION + help + By default, the image slot comparison relies only on active slot. + Enable this option to take into account the specified slot number + instead. + choice BOOT_DOWNGRADE_PREVENTION_CHOICE prompt "Downgrade prevention" optional diff --git a/boot/zephyr/include/mcuboot_config/mcuboot_config.h b/boot/zephyr/include/mcuboot_config/mcuboot_config.h index 0956208466..5697ea66e9 100644 --- a/boot/zephyr/include/mcuboot_config/mcuboot_config.h +++ b/boot/zephyr/include/mcuboot_config/mcuboot_config.h @@ -124,6 +124,10 @@ #define MCUBOOT_VERSION_CMP_USE_BUILD_NUMBER #endif +#ifdef CONFIG_BOOT_VERSION_CMP_USE_SLOT_NUMBER +#define MCUBOOT_VERSION_CMP_USE_SLOT_NUMBER +#endif + #ifdef CONFIG_BOOT_SWAP_SAVE_ENCTLV #define MCUBOOT_SWAP_SAVE_ENCTLV 1 #endif diff --git a/docs/design.md b/docs/design.md index 500cda19c7..e4da1bfa85 100755 --- a/docs/design.md +++ b/docs/design.md @@ -898,6 +898,23 @@ process is presented below. + Boot into image in the primary slot of the 0th image position\ (other image in the boot chain is started by another image). +By enabling the `MCUBOOT_VERSION_CMP_USE_SLOT_NUMBER` configuration option, +the dependency check may be extended to match for a specified slot of a specific +image. This functionality is useful in a multi-core system when Direct XIP mode +is used. +In this case, the main image can be started from one of the two (primary or +secondary) slots. +If there is a fixed connection between the slots of two different images, +e.g. if the main image always chainloads a companion image from the same slot, +the check must take this into account and only consider a matching slot when +resolving dependencies. + +There are three values that can be passed when specifying dependencies: + +1. ``active``: the dependency should be checked against either primary or secondary slot. +2. ``primary``: the dependency should be checked only against primary slot. +3. ``secondary``: the dependency should be checked only against secondary slot. + ### [Multiple image boot for RAM loading and direct-xip](#multiple-image-boot-for-ram-loading-and-direct-xip) The operation of the bootloader is different when the ram-load or the diff --git a/docs/imgtool.md b/docs/imgtool.md index 958e1af154..c68652dcaa 100644 --- a/docs/imgtool.md +++ b/docs/imgtool.md @@ -91,7 +91,8 @@ primary slot and adds a header and trailer that the bootloader is expecting: the `auto` keyword to automatically generate it from the image version. -d, --dependencies TEXT Add dependence on another image, format: - "(,), ... " + "(,[,] + ), ... " --pad-sig Add 0-2 bytes of padding to ECDSA signature (for mcuboot <1.5) -H, --header-size INTEGER [required] @@ -182,6 +183,16 @@ which the current image depends on. The `image_version` is the minimum version of that image to satisfy compliance. For example `-d "(1, 1.2.3+0)"` means this image depends on Image 1 which version has to be at least 1.2.3+0. +In addition, a dependency can specify the slot as follows: +`-d "(image_id, slot, image_version)"`. The `image_id` is the number of the +image on which the current image depends. +The slot specifies which slots of the image are to be taken into account +(`active`: primary or secondary, `primary`: only primary `secondary`: only +secondary slot). The `image_version` is the minimum version of that image to +fulfill the requirements. +For example `-d "(1, primary, 1.2.3+0)"` means that this image depends on the +primary slot of the Image 1, whose version must be at least 1.2.3+0. + The `--public-key-format` argument can be used to distinguish where the public key is stored for image authentication. The `hash` option is used by default, in which case only the hash of the public key is added to the TLV area (the full diff --git a/scripts/imgtool/image.py b/scripts/imgtool/image.py index 542e8f5f88..e26440ef2d 100644 --- a/scripts/imgtool/image.py +++ b/scripts/imgtool/image.py @@ -590,8 +590,9 @@ def create(self, key, public_key_format, enckey, dependencies=None, if dependencies is not None: for i in range(dependencies_num): payload = struct.pack( - e + 'B3x' + 'BBHI', + e + 'BB2x' + 'BBHI', int(dependencies[DEP_IMAGES_KEY][i]), + dependencies[DEP_VERSIONS_KEY][i].slot, dependencies[DEP_VERSIONS_KEY][i].major, dependencies[DEP_VERSIONS_KEY][i].minor, dependencies[DEP_VERSIONS_KEY][i].revision, diff --git a/scripts/imgtool/main.py b/scripts/imgtool/main.py index 5ff1f8f9f3..7f9a536571 100755 --- a/scripts/imgtool/main.py +++ b/scripts/imgtool/main.py @@ -27,6 +27,7 @@ import lzma import hashlib import base64 +from collections import namedtuple from imgtool import image, imgtool_version from imgtool.version import decode_version from imgtool.dumpinfo import dump_imginfo @@ -45,6 +46,14 @@ sys.exit("Python %s.%s or newer is required by imgtool." % MIN_PYTHON_VERSION) +SlottedSemiSemVersion = namedtuple('SemiSemVersion', ['major', 'minor', 'revision', + 'build', 'slot']) + +DEPENDENCY_SLOT_VALUES = { + 'active': 0x00, + 'primary': 0x01, + 'secondary': 0x02 +} def gen_rsa2048(keyfile, passwd): keys.RSA.generate().export_private(path=keyfile, passwd=passwd) @@ -301,16 +310,33 @@ def get_dependencies(ctx, param, value): if len(images) == 0: raise click.BadParameter( "Image dependency format is invalid: {}".format(value)) - raw_versions = re.findall(r",\s*([0-9.+]+)\)", value) + raw_versions = re.findall(r",\s*((active|primary|secondary)\s*,)?\s*([0-9.+]+)\)", value) if len(images) != len(raw_versions): raise click.BadParameter( '''There's a mismatch between the number of dependency images and versions in: {}'''.format(value)) for raw_version in raw_versions: try: - versions.append(decode_version(raw_version)) + decoded_version = decode_version(raw_version[2]) + if len(raw_version[1]) > 0: + slotted_version = SlottedSemiSemVersion( + decoded_version.major, + decoded_version.minor, + decoded_version.revision, + decoded_version.build, + DEPENDENCY_SLOT_VALUES[raw_version[1]] + ) + else: + slotted_version = SlottedSemiSemVersion( + decoded_version.major, + decoded_version.minor, + decoded_version.revision, + decoded_version.build, + 0 + ) except ValueError as e: raise click.BadParameter("{}".format(e)) + versions.append(slotted_version) dependencies = dict() dependencies[image.DEP_IMAGES_KEY] = images dependencies[image.DEP_VERSIONS_KEY] = versions @@ -405,7 +431,7 @@ def convert(self, value, param, ctx): '(for mcuboot <1.5)') @click.option('-d', '--dependencies', callback=get_dependencies, required=False, help='''Add dependence on another image, format: - "(,), ... "''') + "(,[,]), ... "''') @click.option('-s', '--security-counter', callback=validate_security_counter, help='Specify the value of security counter. Use the `auto` ' 'keyword to automatically generate it from the image version.')