# NCS v3.3.1 MCUboot patch set -- secured-line swap-with-revert
#
# Applies to: <ncs>/v3.3.1/bootloader/mcuboot @ 3de5b4df5f9f
#             ("MCUboot v2.3.0-dev-3de5b4df5f9f", NCS v3.3.1-1d7a0b0e49b8)
# Apply with: git -C <mcuboot> 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);
         }
