diff --git a/src/helpers/ota/OtaApply.cpp b/src/helpers/ota/OtaApply.cpp index bafa51fb..d17dde3d 100644 --- a/src/helpers/ota/OtaApply.cpp +++ b/src/helpers/ota/OtaApply.cpp @@ -528,7 +528,7 @@ static bool ota_apply_mota_nrf52_impl(const uint8_t* buf, uint32_t len, // 0) THIS device's bootloader must be able to apply this .mota - otherwise staging + approving + rebooting // just bounces back unchanged (a legacy/stock/older-OTAFIX bootloader). Refuse here, before any reboot. { - OtaBlCaps bl = ota_bootloader_caps(); + OtaBlCaps bl = ota_bootloader_app_caps(); if (!bl.present) { strcpy(msg, "this bootloader has no OTA-apply support - update the bootloader first"); return false; } if (bl.apply_abi < m.format_ver || !(bl.codec_mask & (1u << m.codec_id))) { snprintf(msg, 159, "bootloader too old to apply this update (bl abi=%u codecs=0x%x; need fmt>=%u codec=%u) - update the bootloader", @@ -699,7 +699,7 @@ static bool ota_apply_mota_nrf52_external(Store& store, const SignerAllowlist& a memcpy(st.image_hash, m.image_hash, sizeof(st.image_hash)); st.manifest_ok = true; - OtaBlCaps bl = ota_bootloader_caps(); + OtaBlCaps bl = ota_bootloader_app_caps(); if (!bl.present || !(bl.storage_flags & storage_flag)) { snprintf(msg, NRF52_APPLY_MSG_CAP, "this bootloader has no %s OTA support - update the bootloader first", storage_name); @@ -860,7 +860,7 @@ static bool ota_prepare_bootloader_update_external(Store& store, case OTA_BOOT_CONFIRM_IMAGE_HASH: strcpy(msg, "bootloader image-hash confirmation mismatch"); return false; } - const OtaBlCaps current_caps = ota_bootloader_caps(); + const OtaBlCaps current_caps = ota_bootloader_update_caps(); if (!ota_bootloader_self_update_caps_valid(current_caps)) { snprintf(msg, CAP, "installed bootloader cannot safely self-update from %s", storage_name); return false; @@ -1018,7 +1018,7 @@ bool ota_prepare_bootloader_update_nrf52(OtaStoreFlashNrf52& store, case OTA_BOOT_CONFIRM_IMAGE_HASH: strcpy(msg, "bootloader image-hash confirmation mismatch"); return false; } - const OtaBlCaps current_caps = ota_bootloader_caps(); + const OtaBlCaps current_caps = ota_bootloader_update_caps(); if (!ota_bootloader_self_update_caps_valid(current_caps) || current_caps.storage_flags != (OTA_BL_STORAGE_STAGE_CEILING | OTA_BL_STORAGE_BOOT_UPDATE)) { diff --git a/src/helpers/ota/OtaBlInfo.h b/src/helpers/ota/OtaBlInfo.h index 998d3c5f..0d6cf35e 100644 --- a/src/helpers/ota/OtaBlInfo.h +++ b/src/helpers/ota/OtaBlInfo.h @@ -133,19 +133,68 @@ inline OtaBlCaps ota_bl_caps_scan_aligned(const uint8_t* bytes, size_t len, return c; } -// Scan the bootloader flash region for the marker. Returns {present=false} if not found / non-nRF52. -inline OtaBlCaps ota_bootloader_caps() { +// Preview 5 declared its ABI-2 marker without an explicit alignment attribute. +// GCC therefore emitted the naturally halfword-aligned const object at address +// 2 mod 4 in the released RAK4631 bootloader. Keep the broader modern-marker +// scan word-aligned, but recognize this one exact legacy shape at the other +// valid uint16_t alignment. This does not relax the privileged self-update +// scan below. +inline OtaBlCaps ota_bl_legacy_app_caps_scan_halfword(const uint8_t* bytes, + size_t len) { + OtaBlCaps c; + if (!bytes) return c; + for (size_t off = 2u; off + 16u <= len; off += 4u) { + const uint8_t* p = bytes + off; + if (p[0] != OTA_BL_MAGIC[0] || memcmp(p, OTA_BL_MAGIC, 8) != 0) continue; + if (p[8] == 2u && p[9] == 0u && p[10] == (1u << 2) && p[11] == 0u && + p[12] == 0u && p[13] == 0u && p[14] == 0u && p[15] == 0u) { + c.present = true; + c.apply_abi = 2u; + c.codec_mask = (1u << 2); + return c; + } + } + return c; +} + +inline OtaBlCaps ota_bl_app_caps_scan(const uint8_t* bytes, size_t len) { + const OtaBlCaps aligned = ota_bl_caps_scan_aligned(bytes, len, false); + if (aligned.present) return aligned; + return ota_bl_legacy_app_caps_scan_halfword(bytes, len); +} + +inline OtaBlCaps ota_bl_update_caps_scan_aligned(const uint8_t* bytes, size_t len, + uint8_t exact_storage_flags) { + if (exact_storage_flags == 0) return OtaBlCaps(); + return ota_bl_caps_scan_aligned(bytes, len, true, exact_storage_flags); +} + +// Scan the bootloader flash region for ordinary application-update capabilities. +// This deliberately accepts both the legacy ABI-2 marker (codec 2, no storage +// flags) and the newer ABI-3 marker. Compiling bootloader self-update support +// into the application must not make an otherwise compatible older bootloader +// disappear from the ordinary application-OTA path. +inline OtaBlCaps ota_bootloader_app_caps() { #if defined(NRF52_PLATFORM) const uint8_t* lo = (const uint8_t*)(uintptr_t)MOTA_NRF52_BL_START; const uint8_t* hi = (const uint8_t*)(uintptr_t)MOTA_NRF52_BL_END; - return ota_bl_caps_scan_aligned(lo, (size_t)(hi - lo), -#if defined(OTA_QSPI_BOOTLOADER_UPDATE) || defined(OTA_INTERNAL_BOOTLOADER_UPDATE) || \ - defined(OTA_SD_BOOTLOADER_UPDATE) - true, ota_bootloader_update_storage_flags() + return ota_bl_app_caps_scan(lo, (size_t)(hi - lo)); #else - false + return OtaBlCaps(); #endif - ); +} + +// Scan the same region using the stricter, fail-closed rules required before +// replacing the bootloader itself. Legacy bootloaders remain valid for normal +// application deltas, but can never enter this privileged path. +inline OtaBlCaps ota_bootloader_update_caps() { +#if defined(NRF52_PLATFORM) && \ + (defined(OTA_QSPI_BOOTLOADER_UPDATE) || defined(OTA_INTERNAL_BOOTLOADER_UPDATE) || \ + defined(OTA_SD_BOOTLOADER_UPDATE)) + const uint8_t* lo = (const uint8_t*)(uintptr_t)MOTA_NRF52_BL_START; + const uint8_t* hi = (const uint8_t*)(uintptr_t)MOTA_NRF52_BL_END; + return ota_bl_update_caps_scan_aligned( + lo, (size_t)(hi - lo), ota_bootloader_update_storage_flags()); #else return OtaBlCaps(); #endif @@ -153,32 +202,29 @@ inline OtaBlCaps ota_bootloader_caps() { // True if this device's bootloader can apply a .mota of the given format_ver + codec_id. inline bool ota_bootloader_can_apply(uint8_t format_ver, uint8_t codec_id) { - OtaBlCaps c = ota_bootloader_caps(); + OtaBlCaps c = ota_bootloader_app_caps(); return c.present && c.apply_abi >= format_ver && (c.codec_mask & (1u << codec_id)) != 0; } +inline bool ota_bootloader_supports_expanded_stage(const OtaBlCaps& c) { + return c.present && (c.storage_flags & OTA_BL_STORAGE_STAGE_CEILING) != 0; +} + #if defined(NRF52_PLATFORM) // Use the layout's larger ceiling only when the installed bootloader advertises the matching // GPREGRET2 handoff. New applications remain filesystem-safe with older OTAFIX bootloaders by retaining // the legacy 0xD4000 ceiling. Packages sized for the expanded window still require the newer bootloader. inline uint32_t ota_nrf52_effective_stage_ceiling(const OtaBlCaps& c) { const uint32_t desired = mota_nrf52_layout_stage_ceiling(); -#if defined(OTA_INTERNAL_BOOTLOADER_UPDATE) - if (desired != MOTA_NRF52_STAGE_CEILING_EXPANDED || - !ota_bootloader_self_update_caps_valid(c)) - return MOTA_NRF52_STAGE_CEILING_LEGACY; - return desired; -#else if (desired == MOTA_NRF52_STAGE_CEILING_EXPANDED) { - if (!c.present || !(c.storage_flags & OTA_BL_STORAGE_STAGE_CEILING)) + if (!ota_bootloader_supports_expanded_stage(c)) return MOTA_NRF52_STAGE_CEILING_LEGACY; } return desired; -#endif } inline uint32_t ota_nrf52_effective_stage_ceiling() { - return ota_nrf52_effective_stage_ceiling(ota_bootloader_caps()); + return ota_nrf52_effective_stage_ceiling(ota_bootloader_app_caps()); } #endif diff --git a/src/helpers/ota/OtaCli.cpp b/src/helpers/ota/OtaCli.cpp index b5d66785..6e4544ff 100644 --- a/src/helpers/ota/OtaCli.cpp +++ b/src/helpers/ota/OtaCli.cpp @@ -5,7 +5,7 @@ #include "OtaSelf.h" #include "OtaTargets.h" // ota_target_env_name(): human-readable name for a target_id (no string on the wire) #if defined(NRF52_PLATFORM) - #include "OtaBlInfo.h" // ota_bootloader_caps(): can this device's bootloader apply a .mota? + #include "OtaBlInfo.h" // installed bootloader application/update capability views #endif #include "Utils.h" #include @@ -240,7 +240,7 @@ bool handle_ota_command(const char* command, char* reply, mesh::MainBoard& board #if defined(NRF52_PLATFORM) // nRF52 applies via the bootloader - show (cached) whether it can, so `ota get`/`install` won't surprise. // blrc = the bootloader's last apply code (diagnostic; 0xB8=success, see ota_delta.c). - const OtaBlCaps& bl = c.bootloaderCaps(); + const OtaBlCaps& bl = c.bootloaderAppCaps(); #if defined(OTA_QSPI_STORE) const char* bl_state = !bl.present ? "NONE" : (bl.storage_flags & OTA_BL_STORAGE_QSPI) ? "QSPI" : "NO-QSPI"; @@ -333,7 +333,11 @@ bool handle_ota_command(const char* command, char* reply, mesh::MainBoard& board const uint8_t* cur = (fs != OtaManager::IDLE) ? c.manager.fetchManifestId() : nullptr; uint32_t myt = c.manager.target(); // effective target (EndF identity if present, else build flag) #if defined(NRF52_PLATFORM) - const OtaBlCaps& list_bl = c.bootloaderCaps(); + const OtaBlCaps& list_bl = c.bootloaderAppCaps(); +#if defined(OTA_QSPI_BOOTLOADER_UPDATE) || defined(OTA_INTERNAL_BOOTLOADER_UPDATE) || \ + defined(OTA_SD_BOOTLOADER_UPDATE) + const OtaBlCaps& list_bl_update = c.bootloaderUpdateCaps(); +#endif #endif #if defined(NRF52_PLATFORM) && defined(OTA_FLASH_STORE) && !defined(OTA_SD_STORE) && \ !defined(OTA_QSPI_STORE) @@ -382,7 +386,7 @@ bool handle_ota_command(const char* command, char* reply, mesh::MainBoard& board installable = h->flags == (MFLAG_FULL | MFLAG_SIGNED | MFLAG_BOOTLOADER) && h->codec == CODEC_FULL && bid.present && bid.crc_ok && h->target_id == ota_bootloader_target_id(bid) && - ota_bootloader_self_update_caps_valid(list_bl); + ota_bootloader_self_update_caps_valid(list_bl_update); #if defined(OTA_SD_BOOTLOADER_UPDATE) installable = installable && list_sd_headroom; #endif @@ -517,7 +521,7 @@ bool handle_ota_command(const char* command, char* reply, mesh::MainBoard& board (defined(OTA_QSPI_BOOTLOADER_UPDATE) || defined(OTA_INTERNAL_BOOTLOADER_UPDATE) || \ defined(OTA_SD_BOOTLOADER_UPDATE)) const OtaBootloaderIdentity& bid = c.bootloaderIdentity(); - const OtaBlCaps& bl = c.bootloaderCaps(); + const OtaBlCaps& bl = c.bootloaderUpdateCaps(); if (selflags != (MFLAG_FULL | MFLAG_SIGNED | MFLAG_BOOTLOADER) || selcodec != CODEC_FULL) { strcpy(reply, "ERR malformed bootloader catalog row; capture it to folder for inspection"); @@ -555,7 +559,8 @@ bool handle_ota_command(const char* command, char* reply, mesh::MainBoard& board return true; } #if defined(NRF52_PLATFORM) - const OtaBlCaps& bl = c.bootloaderCaps(); + const OtaBlCaps& bl = selboot ? c.bootloaderUpdateCaps() + : c.bootloaderAppCaps(); if (!bl.present) { strcpy(reply, "ERR bootloader has no mOTA apply support; update it over USB first"); return true; @@ -692,7 +697,7 @@ bool handle_ota_command(const char* command, char* reply, mesh::MainBoard& board int n = snprintf(reply, 160, "self body=%u image=%u base_hash=%s", (unsigned)fi.body_len, (unsigned)fi.image_len, hx); #if defined(NRF52_PLATFORM) // nRF52 applies via the bootloader, so surface whether THIS device's bootloader can install this store. - const OtaBlCaps& bl = c.bootloaderCaps(); // cached (flash scanned once) + const OtaBlCaps& bl = c.bootloaderAppCaps(); // cached (flash scanned once) #if defined(OTA_QSPI_STORE) uint32_t qspi_capacity = c.fetch_store.capacity(); n += snprintf(reply + n, 160 - n, " | QSPI store:%s%uK", @@ -758,12 +763,16 @@ bool handle_ota_command(const char* command, char* reply, mesh::MainBoard& board (defined(OTA_QSPI_BOOTLOADER_UPDATE) || defined(OTA_INTERNAL_BOOTLOADER_UPDATE) || \ defined(OTA_SD_BOOTLOADER_UPDATE)) const OtaBootloaderIdentity& bid = c.bootloaderIdentity(); - const OtaBlCaps& bl = c.bootloaderCaps(); + const OtaBlCaps& bl = c.bootloaderUpdateCaps(); if (*rest == 0 || strcmp(rest, "status") == 0) { if (!bid.present || !bid.crc_ok) { strcpy(reply, "Bootloader update unavailable: installed embedded manifest/CRC is invalid"); return true; } + if (!ota_bootloader_self_update_caps_valid(bl)) { + strcpy(reply, "Bootloader update unavailable: installed bootloader supports application OTA only"); + return true; + } MotaManifest staged; char midhx[9] = "-", hashhx[17] = "-"; const bool ready = c.manager.fetchState() == OtaManager::COMPLETE && diff --git a/src/helpers/ota/OtaContext.h b/src/helpers/ota/OtaContext.h index f5c51171..f6a23d45 100644 --- a/src/helpers/ota/OtaContext.h +++ b/src/helpers/ota/OtaContext.h @@ -296,10 +296,19 @@ struct OtaContext { // ONCE (the scan is ~40 KB) and cached in RAM. On other platforms present=false (apply is in-app). OtaBlCaps _bl_caps; bool _bl_caps_read = false; - const OtaBlCaps& bootloaderCaps() { - if (!_bl_caps_read) { _bl_caps = ota_bootloader_caps(); _bl_caps_read = true; } + const OtaBlCaps& bootloaderAppCaps() { + if (!_bl_caps_read) { _bl_caps = ota_bootloader_app_caps(); _bl_caps_read = true; } return _bl_caps; } + OtaBlCaps _bl_update_caps; + bool _bl_update_caps_read = false; + const OtaBlCaps& bootloaderUpdateCaps() { + if (!_bl_update_caps_read) { + _bl_update_caps = ota_bootloader_update_caps(); + _bl_update_caps_read = true; + } + return _bl_update_caps; + } OtaBootloaderIdentity _bl_identity; bool _bl_identity_read = false; const OtaBootloaderIdentity& bootloaderIdentity() { diff --git a/src/helpers/ota/OtaStoreFlashNrf52.cpp b/src/helpers/ota/OtaStoreFlashNrf52.cpp index 8c5665be..72818fe3 100644 --- a/src/helpers/ota/OtaStoreFlashNrf52.cpp +++ b/src/helpers/ota/OtaStoreFlashNrf52.cpp @@ -41,7 +41,7 @@ bool OtaStoreFlashNrf52::plan_layout(bool is_full, uint32_t image_size, const uint64_t total64 = (uint64_t)payload_off + payload_size + 5u; if (!is_full || image_size != 40u * 1024u || payload_size != 40u * 1024u || payload_off != 365u || total64 != MOTA_NRF52_BOOT_CONTAINER_SIZE || - !ota_bootloader_self_update_caps_valid(ota_bootloader_caps())) + !ota_bootloader_self_update_caps_valid(ota_bootloader_update_caps())) return false; // This is the release-blocking no-EndF gate: package kind is known before @@ -279,7 +279,8 @@ bool OtaStoreFlashNrf52::reopen() { if (bootloader) { #if defined(OTA_INTERNAL_BOOTLOADER_UPDATE) SelfFwInfo fi; - if (!ota_self_firmware(fi) || !fi.valid || fi.image_len > UINT32_MAX - app_base || + if (!ota_bootloader_self_update_caps_valid(ota_bootloader_update_caps()) || + !ota_self_firmware(fi) || !fi.valid || fi.image_len > UINT32_MAX - app_base || !mota_nrf52_shared_boot_stage_plan( total, app_base, true, app_base + fi.image_len, want) || want != start) continue; diff --git a/test/test_ota/test_ota_core.cpp b/test/test_ota/test_ota_core.cpp index 61564e7f..422e6e83 100644 --- a/test/test_ota/test_ota_core.cpp +++ b/test/test_ota/test_ota_core.cpp @@ -655,6 +655,64 @@ TEST(OtaBootPackage, CapabilityScannerRejectsAnOtherwiseValidUnalignedMarker) { EXPECT_FALSE(ota_bl_caps_scan_aligned(image, sizeof(image), true, qspi_profile).present); } +TEST(OtaBootPackage, LegacyAndCurrentBootloadersHaveSeparateCapabilityViews) { + const uint8_t internal_profile = OTA_BL_PROFILE_INTERNAL_BOOT_UPDATE; + const uint8_t legacy_marker[16] = { + 'M','O','T','A','B','L','D','R', 2,0, 4,0, 0,0,0,0}; + const uint8_t current_marker[16] = { + 'M','O','T','A','B','L','D','R', 3,0, 5,0, + internal_profile, 0,0,0}; + uint8_t image[64]; + + // The released OTAFIX Preview 5 marker is naturally halfword-aligned at + // address 2 mod 4. It can still install ABI-2, codec-2 application deltas, + // but cannot replace itself and must retain the legacy staging ceiling. + memset(image, 0xFF, sizeof(image)); + memcpy(image + 2, legacy_marker, sizeof(legacy_marker)); + OtaBlCaps app = ota_bl_app_caps_scan(image, sizeof(image)); + OtaBlCaps update = ota_bl_update_caps_scan_aligned( + image, sizeof(image), internal_profile); + ASSERT_TRUE(app.present); + EXPECT_EQ(app.apply_abi, 2u); + EXPECT_EQ(app.codec_mask, 1u << 2); + EXPECT_FALSE(update.present); + EXPECT_FALSE(ota_bootloader_supports_expanded_stage(app)); + + // Do not turn the compatibility exception into a bytewise magic scan. + memset(image, 0xFF, sizeof(image)); + memcpy(image + 1, legacy_marker, sizeof(legacy_marker)); + EXPECT_FALSE(ota_bl_app_caps_scan(image, sizeof(image)).present); + + // The current marker enables both views and advertises the expanded-stage + // handoff independently of the privileged self-update decision. + memset(image, 0xFF, sizeof(image)); + memcpy(image + 4, current_marker, sizeof(current_marker)); + app = ota_bl_app_caps_scan(image, sizeof(image)); + update = ota_bl_update_caps_scan_aligned(image, sizeof(image), internal_profile); + ASSERT_TRUE(app.present); + ASSERT_TRUE(update.present); + EXPECT_EQ(update.storage_flags, internal_profile); + EXPECT_TRUE(ota_bootloader_supports_expanded_stage(app)); + + // Ambiguous privileged markers fail closed only for self-update. Ordinary + // application OTA remains available through the unprivileged view. + memcpy(image + 24, current_marker, sizeof(current_marker)); + app = ota_bl_app_caps_scan(image, sizeof(image)); + update = ota_bl_update_caps_scan_aligned(image, sizeof(image), internal_profile); + EXPECT_TRUE(app.present); + EXPECT_FALSE(update.present); + + // A valid marker for another storage profile is visible diagnostically but + // cannot authorize an internal-flash bootloader replacement. + memset(image, 0xFF, sizeof(image)); + memcpy(image + 4, current_marker, sizeof(current_marker)); + image[4 + 12] = OTA_BL_PROFILE_QSPI_BOOT_UPDATE; + app = ota_bl_app_caps_scan(image, sizeof(image)); + update = ota_bl_update_caps_scan_aligned(image, sizeof(image), internal_profile); + EXPECT_TRUE(app.present); + EXPECT_FALSE(update.present); +} + class FakeMotaSeederStream : public Stream { public: using Stream::write;