mirror of
https://github.com/mikecarper/MeshCore.git
synced 2026-09-09 12:15:34 +00:00
Fix nRF52 OTA compatibility with legacy bootloaders
This commit is contained in:
@@ -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
@@ -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
|
||||
|
||||
|
||||
@@ -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 &&
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user