Fix nRF52 OTA compatibility with legacy bootloaders

This commit is contained in:
mikecarper
2026-08-28 20:10:28 -07:00
parent 90de3fee6f
commit fd98bc90f7
6 changed files with 157 additions and 34 deletions
+4 -4
View File
@@ -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)) {
+64 -18
View File
@@ -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
+17 -8
View File
@@ -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 <stdio.h>
@@ -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 &&
+11 -2
View File
@@ -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() {
+3 -2
View File
@@ -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;
+58
View File
@@ -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;