# NCS v3.3.1 MCUboot patch set -- secured-line swap-with-revert # # Applies to: /v3.3.1/bootloader/mcuboot @ 3de5b4df5f9f # ("MCUboot v2.3.0-dev-3de5b4df5f9f", NCS v3.3.1-1d7a0b0e49b8) # Apply with: git -C apply ncs-3.3.1-mcuboot-secured-swap.patch # # STATUS: bench-proven on the bench unit (nRF5340 custom board) on # 2026-08-23. With all four hunks applied, acceptance rows 1-3 PASS on the # SECURED (b0 + s0/s1 + AES-128 encrypted, swap-using-move) line over the real # field path (warm mcumgr/BLE reset): # ROW 1 baseline v0.1.2 boots, advertises, [active,confirmed,bootable] # ROW 2 good v0.1.3 swaps in (~35 s), advertises, SELF-CONFIRMS, and persists # across a further reset (still v0.1.3 [active,confirmed]) # ROW 3 broken v0.1.3 swaps in (~36 s), boots, never advertises, never # confirms, does NOT self-reset; on the next reset MCUboot REVERTS to # v0.1.2 (~32 s) and the unit is advertising and serviceable again # No watchdog bite (RESETREAS free of DOG0) and no LOCKUP in any row. # # WITHOUT these hunks the encrypted secured line can NEVER complete an OTA: # the staged image is silently erased on every boot. # # --- What each hunk fixes ------------------------------------------------- # # 1. loader.c, boot_validated_swap_type() -- THE KEYSTONE. # The NCS NSIB reset-vector disambiguation reads the secondary slot's reset # vector as PLAINTEXT. For an AES-encrypted image it is ciphertext, so the # range check always fails and MCUboot concludes "not intended for any # image", returning BOOT_SWAP_TYPE_NONE *before* sec_slot_mark_assigned() -- # which leaves the slot SEC_SLOT_TOUCHED and gets it erased (hunk 3). # Measured: reset_addr = 0x3ee3fbc2 (ciphertext) vs the valid primary range # [0x28000,0xfc000]; ih_flags = 0x4 (IMAGE_F_ENCRYPTED_AES128) -- MCUboot # already knows the image is encrypted. The plaintext net-core image # (ih_flags 0x100, reset_addr 0x1023291) passes and is ASSIGNED, which is # why only the encrypted app image ever failed. # NOTE: NCS v3.3.1 has NO encryption guard anywhere in this block. # IMPLICATION: SB_CONFIG_SECURE_BOOT_APPCORE + SB_CONFIG_BOOT_ENCRYPTION are # effectively incompatible on nRF5340 in NCS v3.3.1 without this hunk. # # 2. loader.c, boot_prepare_image_for_update() -- skip unwired image 2. # Image 2 is the NSIB s0/s1 self-update slot. Its PRIMARY is # mcuboot_primary_1, a nordic_ram_flash_controller (PCD/RAM-flash) area # backed by the NETWORK CORE, which is held off during boot: the swap-state # read BLOCKS. Measured 10.86 s hang (images 0 and 1 traverse the identical # code in <1 ms) ending in a task_wdt reset that tore the staged slot. # Its SECONDARY is the SHARED mcuboot_secondary, so a staged APP image gives # image 2 "good magic" and triggers this on every staged boot. # # 3. loader.c, sec_slot_cleanup_if_unusable() -- chunk + feed the erase. # Erases the whole external-QSPI secondary in ONE unfed flash_area_erase. # Measured 15.80 s (img 0) and 17.88 s (img 2) for 0xd3000 = 864,256 B -- # each far beyond the 10.5 s application task_wdt that MCUboot inherits # across a warm reset (nRF WDT survives sys_reboot; CRV 0x54000 @32768 Hz). # Note it erases the SHARED area twice, once on behalf of unwired image 2, # destroying a perfectly good staged APP image. # # 4. swap_move.c x3 + bootutil_img_hash.c x1 -- watchdog feeds. # CONFIG_BOOT_WATCHDOG_FEED exists but is called from only three places # upstream, none of them on the swap/validate path, which runs for tens of # seconds over external QSPI. Feeds added to the status scan, the move-up # and sector-swap loops, and the image hash/decrypt chunk loop. # # --- Related findings NOT patched here (for the upstream report) ---------- # * Marking image 0 pending flips image 1's reported state (shared secondary). # * boot_read_image_headers() swallows a failed read (i>0 && !require_all -> # rc 0) and keeps a stale header. # * swap_read_status_bytes() issues ~1000 SINGLE-BYTE flash_area_read calls # over external QSPI (BOOT_MAX_IMG_SECTORS=256); should be batched. # * Application-side: a booted-but-never-advertising image does not self-reset # (task watchdog is fed unconditionally) -- recovery needs an external reset. # # Carried locally as the bench-proven set; not an upstream commit. diff --git a/boot/bootutil/src/bootutil_img_hash.c b/boot/bootutil/src/bootutil_img_hash.c index ae52ffb9..eeeca45f 100644 --- a/boot/bootutil/src/bootutil_img_hash.c +++ b/boot/bootutil/src/bootutil_img_hash.c @@ -143,6 +143,9 @@ bootutil_img_hash(struct boot_loader_state *state, size); #else for (off = 0; off < size; off += blk_sz) { +#ifdef MCUBOOT_WATCHDOG_FEED + MCUBOOT_WATCHDOG_FEED(); +#endif blk_sz = size - off; if (blk_sz > tmp_buf_sz) { blk_sz = tmp_buf_sz; diff --git a/boot/bootutil/src/loader.c b/boot/bootutil/src/loader.c index fcd67afb..83c869f3 100644 --- a/boot/bootutil/src/loader.c +++ b/boot/bootutil/src/loader.c @@ -995,7 +995,29 @@ static void sec_slot_cleanup_if_unusable(void) rc = flash_area_open(flash_area_id_from_multi_image_slot(idx, BOOT_SLOT_SECONDARY), &secondary_fa); if (!rc) { - rc = flash_area_erase(secondary_fa, 0, secondary_fa->fa_size); + /* NOTE: erasing the whole external-QSPI secondary in ONE call + * takes 15.8-17.9 s measured, far beyond an inherited 10.5 s + * application task_wdt, so the erase never completed and the + * staged image was left torn. Chunk it and feed the watchdog. + */ + uint32_t e_off; + + rc = 0; + for (e_off = 0; e_off < secondary_fa->fa_size; + e_off += 0x8000u) { + uint32_t this_sz = 0x8000u; + + if ((e_off + this_sz) > secondary_fa->fa_size) { + this_sz = secondary_fa->fa_size - e_off; + } +#ifdef MCUBOOT_WATCHDOG_FEED + MCUBOOT_WATCHDOG_FEED(); +#endif + rc = flash_area_erase(secondary_fa, e_off, this_sz); + if (rc) { + break; + } + } } BOOT_LOG_ERR("Erase secondary: img %d: %d", idx, rc); @@ -1060,7 +1082,15 @@ boot_validated_swap_type(struct boot_loader_state *state, #ifdef MCUBOOT_IS_SECOND_STAGE #if CONFIG_MCUBOOT_NETWORK_CORE_IMAGE_NUMBER != -1 - if(!(reset_addr >= NETCPU_APP_SLOT_OFFSET && reset_addr < NETCPU_APP_SLOT_END)) + /* NOTE: the reset vector below is read from the secondary slot WITHOUT + * decryption. For an AES-encrypted image it is ciphertext, so the range + * checks always conclude "not intended for any image", return + * BOOT_SWAP_TYPE_NONE before sec_slot_mark_assigned(), and the slot is + * then erased by sec_slot_cleanup_if_unusable(). Skip the + * plaintext-only disambiguation for encrypted images. + */ + if(!(hdr->ih_flags & ENCRYPTIONFLAGS) && + !(reset_addr >= NETCPU_APP_SLOT_OFFSET && reset_addr < NETCPU_APP_SLOT_END)) #endif { const struct flash_area *primary_fa; @@ -1884,6 +1914,17 @@ boot_prepare_image_for_update(struct boot_loader_state *state, int rc; FIH_DECLARE(fih_rc, FIH_FAILURE); + + /* NOTE: image 2 (NSIB s0/s1 self-update) is unwired on nRF5340 designs + * whose image-2 PRIMARY is mcuboot_primary_1, a nordic_ram_flash_controller + * (PCD/RAM-flash) area backed by the network core. The net core is held OFF + * during boot, so reading it BLOCKS (measured 10.86 s) until the watchdog + * fires. Skip all update processing for it. + */ + if (BOOT_CURR_IMG(state) == 2) { + BOOT_SWAP_TYPE(state) = BOOT_SWAP_TYPE_NONE; + return; + } #if defined(MCUBOOT_SERIAL_IMG_GRP_SLOT_INFO) || defined(MCUBOOT_DATA_SHARING) int max_size; #endif diff --git a/boot/bootutil/src/swap_move.c b/boot/bootutil/src/swap_move.c index 49d9d9b2..07ce3d99 100644 --- a/boot/bootutil/src/swap_move.c +++ b/boot/bootutil/src/swap_move.c @@ -161,6 +161,9 @@ swap_read_status_bytes(const struct flash_area *fap, write_sz = BOOT_WRITE_SZ(state); off = boot_status_off(fap); for (i = max_entries; i > 0; i--) { +#ifdef MCUBOOT_WATCHDOG_FEED + MCUBOOT_WATCHDOG_FEED(); +#endif rc = flash_area_read(fap, off + (i - 1) * write_sz, &status, 1); if (rc < 0) { return BOOT_EFLASH; @@ -560,6 +563,9 @@ swap_run(struct boot_loader_state *state, struct boot_status *bs, if (bs->op == BOOT_STATUS_OP_MOVE) { idx = last_idx; while (idx > 0) { +#ifdef MCUBOOT_WATCHDOG_FEED + MCUBOOT_WATCHDOG_FEED(); +#endif if (idx <= (last_idx - bs->idx + 1)) { boot_move_sector_up(idx, sector_sz, state, bs, fap_pri, fap_sec); } @@ -572,6 +578,9 @@ swap_run(struct boot_loader_state *state, struct boot_status *bs, idx = 1; while (idx <= last_idx) { +#ifdef MCUBOOT_WATCHDOG_FEED + MCUBOOT_WATCHDOG_FEED(); +#endif if (idx >= bs->idx) { boot_swap_sectors(idx, sector_sz, state, bs, fap_pri, fap_sec); }