Support compatible RAK bootloader updates with adaptive app storage

This commit is contained in:
mikecarper committed 2026-09-29 03:08:53 -07:00
1 parent 793531e16b
commit 346cf2fd1f
12 files changed
+430 -22

No files matched your search

+15 -4
View File
@@ -1257,9 +1257,7 @@ bool ota_prepare_bootloader_update_nrf52(OtaStoreFlashNrf52& store,
}
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)) {
if (!ota_bootloader_self_update_caps_valid(current_caps)) {
strcpy(msg, "installed bootloader cannot safely self-update from internal flash"); return false;
}
@@ -1303,10 +1301,23 @@ bool ota_prepare_bootloader_update_nrf52(OtaStoreFlashNrf52& store,
!ota_bootloader_identity_matches(installed, candidate) ||
!ota_bootloader_caps_from_image(
image, OTA_BOOT_IMAGE_SIZE,
OTA_BL_STORAGE_STAGE_CEILING | OTA_BL_STORAGE_BOOT_UPDATE,
current_caps.storage_flags,
candidate_caps)) {
strcpy(msg, "candidate bootloader identity/capability/CRC mismatch"); return false;
}
if (current_caps.storage_flags == OTA_BL_PROFILE_RAK_AUTO_BOOT_UPDATE &&
(!ota_bootloader_supports_hybrid(ota_ram_caps_scan_aligned(image, OTA_BOOT_IMAGE_SIZE)) ||
ota_boot_rd32(image) > MOTA_NRF52_HYBRID_RAM_START)) {
strcpy(msg, "candidate adaptive bootloader must retain the hybrid RAM arena"); return false;
}
#if defined(OTA_RAK_AUTO_STORE)
const uint8_t optional_storage = ota_bl_optional_app_storage(
(const uint8_t*)(uintptr_t)MOTA_NRF52_BL_START, OTA_BOOT_IMAGE_SIZE);
if (optional_storage != 0 &&
ota_bl_optional_app_storage(image, OTA_BOOT_IMAGE_SIZE) != optional_storage) {
strcpy(msg, "candidate must retain optional RAK application storage"); return false;
}
#endif
const OtaBootloaderContinuityGate continuity = ota_bootloader_continuity_gate(
installed, candidate, m.fw_version, OTA_BOOT_CONTINUITY_FAMILY_S140,
ota_runtime_softdevice_fwid(), mota_nrf52_app_base(),
+39 -2
View File
@@ -28,6 +28,7 @@ struct OtaBlCaps {
uint16_t apply_abi = 0; // max .mota format_ver the bootloader can apply
uint16_t codec_mask = 0; // bit i set => can apply codec_id i (in-place delta = bit 2)
uint8_t storage_flags = 0; // OTA_BL_STORAGE_* capability bits
uint8_t optional_app_storage = 0; // separate MOTASTOR record, never a boot-update backend
};
// Independent marker for the reset-retained hybrid source. Keep this separate
@@ -93,6 +94,8 @@ static const uint8_t OTA_BL_PROFILE_INTERNAL_BOOT_UPDATE =
static const uint8_t OTA_BL_PROFILE_QSPI_BOOT_UPDATE =
OTA_BL_STORAGE_STAGE_CEILING | OTA_BL_STORAGE_QSPI |
OTA_BL_STORAGE_BOOT_UPDATE;
static const uint8_t OTA_BL_PROFILE_RAK_AUTO_BOOT_UPDATE =
OTA_BL_PROFILE_QSPI_BOOT_UPDATE | OTA_BL_STORAGE_HEADER_W25;
// A successor must retain both application update paths used by qualified
// external stores: bit 0 CODEC_FULL and bit 2 CODEC_DETOOLS_INPLACE.
static const uint16_t OTA_BL_REQUIRED_APP_CODEC_MASK = 0x0005u;
@@ -116,10 +119,15 @@ inline uint8_t ota_bootloader_update_storage_flags() {
inline bool ota_bootloader_self_update_caps_valid(const OtaBlCaps& c) {
const uint8_t required = ota_bootloader_update_storage_flags();
const bool profile_ok = c.storage_flags == required
#if defined(OTA_INTERNAL_BOOTLOADER_UPDATE) && defined(OTA_RAK_AUTO_STORE)
|| c.storage_flags == OTA_BL_PROFILE_RAK_AUTO_BOOT_UPDATE
#endif
;
return required != 0 && c.present && c.apply_abi >= 3u &&
(c.codec_mask & OTA_BL_REQUIRED_APP_CODEC_MASK) ==
OTA_BL_REQUIRED_APP_CODEC_MASK &&
c.storage_flags == required;
profile_ok;
}
// Prefer a continuity-capable marker over a numerically newer legacy-looking candidate. Bootloader
@@ -209,8 +217,30 @@ inline OtaBlCaps ota_bl_legacy_app_caps_scan_halfword(const uint8_t* bytes,
return c;
}
// Unified RAK loaders preserve the deployed board's privileged MOTABLDR=0x0A
// contract. A separate record advertises optional APPLICATION storage. It is
// adjacent to the unchanged, unique 64 KiB RAM marker and grants no new
// bootloader staging backend.
inline uint8_t ota_bl_optional_app_storage(const uint8_t* bytes, size_t len) {
static const uint8_t record[32] = {
'M', 'O', 'T', 'A', 'R', 'A', 'M', 'A', 1, 0, 72, 0, 0, 0, 1, 0,
'M', 'O', 'T', 'A', 'S', 'T', 'O', 'R', 1, 0, 16, 0, 0x14, 0, 0, 0,
};
if (!bytes || !ota_bootloader_supports_hybrid(ota_ram_caps_scan_aligned(bytes, len))) return 0;
unsigned matches = 0;
for (size_t off = 0; off + sizeof(record) <= len; off += 4) {
if (memcmp(bytes + off, record, sizeof(record)) == 0 && ++matches > 1) return 0;
}
return matches == 1 ? OTA_BL_STORAGE_QSPI | OTA_BL_STORAGE_HEADER_W25 : 0;
}
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);
OtaBlCaps aligned = ota_bl_caps_scan_aligned(bytes, len, false);
if (aligned.present && aligned.apply_abi >= 3 &&
aligned.storage_flags == OTA_BL_PROFILE_INTERNAL_BOOT_UPDATE) {
aligned.optional_app_storage = ota_bl_optional_app_storage(bytes, len);
aligned.storage_flags |= aligned.optional_app_storage;
}
if (aligned.present) return aligned;
return ota_bl_legacy_app_caps_scan_halfword(bytes, len);
}
@@ -255,8 +285,15 @@ inline OtaBlCaps ota_bootloader_update_caps() {
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;
#if defined(OTA_INTERNAL_BOOTLOADER_UPDATE) && defined(OTA_RAK_AUTO_STORE)
// Unified RAK applications support the existing internal board identity as
// well as the adaptive successor. Both require one privileged marker.
const OtaBlCaps caps = ota_bl_caps_scan_aligned(lo, (size_t)(hi - lo), true);
return ota_bootloader_self_update_caps_valid(caps) ? caps : OtaBlCaps();
#else
return ota_bl_update_caps_scan_aligned(
lo, (size_t)(hi - lo), ota_bootloader_update_storage_flags());
#endif
#else
return OtaBlCaps();
#endif
+2 -2
View File
@@ -360,7 +360,7 @@ struct OtaContext {
const OtaBootloaderIdentity& installed = bootloaderIdentity();
#if defined(OTA_RAK_AUTO_STORE)
if (!fetch_store.usesInternal()) {
strncpy(msg, "bootloader LoRa update needs the internal OTAFIX profile", 96);
strncpy(msg, "bootloader package must be staged in internal flash", 96);
msg[95] = 0; return false;
}
bool ok = ota_prepare_bootloader_update_nrf52(
@@ -704,7 +704,7 @@ struct OtaContext {
(defined(OTA_QSPI_BOOTLOADER_UPDATE) || defined(OTA_INTERNAL_BOOTLOADER_UPDATE) || \
defined(OTA_SD_BOOTLOADER_UPDATE))
#if defined(OTA_RAK_AUTO_STORE)
manager.set_accept_bootloader(fetch_store.usesInternal() &&
manager.set_accept_bootloader(
ota_bootloader_self_update_caps_valid(ota_bootloader_update_caps()));
#else
manager.set_accept_bootloader(true);
+5 -2
View File
@@ -15,11 +15,14 @@ inline RakStorageChoice rak_storage_choice(uint8_t detected,
bool qspi_bootloader,
bool identity_valid,
const char* device_name,
bool rak3401) {
bool rak3401,
bool optional_app_storage = false) {
if (detected > 3u || detected == 3u) return RakStorageChoice::Unsafe;
const char* auto_name = rak3401 ? "3401_AUTO_DFU" : "4631_AUTO_DFU";
const char* board_name = rak3401 ? "3401_DFU" : "4631_DFU";
const bool merged_bootloader = identity_valid && device_name &&
strcmp(device_name, auto_name) == 0;
(strcmp(device_name, auto_name) == 0 ||
(optional_app_storage && strcmp(device_name, board_name) == 0));
if (detected == 0u) return !qspi_bootloader || merged_bootloader
? RakStorageChoice::Internal : RakStorageChoice::Unsafe;
if (!qspi_bootloader) return RakStorageChoice::Internal;
+47 -5
View File
@@ -14,12 +14,14 @@ namespace mesh {
namespace ota {
// One RAK application image supports the board's internal OTA store and its
// explicitly matched external NOR. Selection is fixed for the lifetime of an
// app boot; no staged container is ever moved between backends.
// explicitly matched external NOR. Application placement follows the detected
// hardware. Bootloader packages always use internal flash; select the backend
// before begin(), never move a partially fetched container between stores.
class OtaStoreAdaptiveNrf52 : public OtaStore {
enum Mode : uint8_t { UNSELECTED, INTERNAL_MODE, QSPI_MODE, UNSAFE };
mutable Mode _mode = UNSELECTED;
mutable const char* _reason = "not probed";
bool _bootloader_store = false;
// Only one backend is usable for a given board/bootloader pairing. Sharing
// their two flash-page buffers saves 8 KiB of scarce nRF52840 runtime RAM.
union StoreStorage {
@@ -39,6 +41,26 @@ class OtaStoreAdaptiveNrf52 : public OtaStore {
_mode = QSPI_MODE;
}
bool selectBootloader() {
if (!ota_bootloader_self_update_caps_valid(ota_bootloader_update_caps())) return false;
if (_mode != INTERNAL_MODE) {
if (_mode == QSPI_MODE) _storage.external.~OtaStoreQspiNrf52();
activateInternal();
}
_bootloader_store = true;
_reason = "bootloader package in internal flash";
return true;
}
void selectApplication() {
if (_bootloader_store) {
_storage.internal.~OtaStoreFlashNrf52();
_mode = UNSELECTED;
_bootloader_store = false;
}
select();
}
void select() const {
if (_mode != UNSELECTED) return;
const uint8_t detected = OtaStoreQspiNrf52::autoDetect();
@@ -66,7 +88,8 @@ class OtaStoreAdaptiveNrf52 : public OtaStore {
#endif
const RakStorageChoice choice = rak_storage_choice(
detected, qspi_bootloader, identity_valid,
identity_valid ? identity.device_name : nullptr, rak3401);
identity_valid ? identity.device_name : nullptr, rak3401,
caps.optional_app_storage == (OTA_BL_STORAGE_QSPI | OTA_BL_STORAGE_HEADER_W25));
if (choice == RakStorageChoice::Internal) {
activateInternal();
_reason = detected == 0u ? "no external NOR" :
@@ -138,12 +161,31 @@ public:
}
bool finalize() override { OtaStore* s = active(); return s && s->finalize(); }
void checkpoint() override { OtaStore* s = active(); if (s) s->checkpoint(); }
bool reopen() override { OtaStore* s = active(); return s && s->reopen(); }
bool reopen() override { return reopenFor(nullptr, 0u); }
bool reopenFor(const uint8_t* mid, uint32_t target) override {
OtaStore* s = active(); return s && s->reopenFor(mid, target);
selectApplication();
OtaStore* s = active();
if (s && s->reopenFor(mid, target)) return true;
// Only an explicit MID pull may resume privileged data. Automatic resume
// stays on the application backend. Validate kind before adopting this
// alternative store; the manager repeats all manifest and block checks.
if (!mid || _mode == INTERNAL_MODE || !selectBootloader()) return false;
uint8_t raw[MOTA_MFL];
MotaManifest manifest;
if (_storage.internal.reopenFor(mid, target) &&
_storage.internal.read(8u, raw, sizeof(raw)) &&
mota_parse_manifest(raw, sizeof(raw), manifest) && manifest.is_bootloader())
return true;
selectApplication();
return false;
}
bool plan_layout(bool full, uint32_t image, uint32_t payload_off,
uint32_t payload_size, bool bootloader) override {
if (bootloader) {
if (!selectBootloader()) return false;
} else {
selectApplication();
}
OtaStore* s = active();
return s && s->plan_layout(full, image, payload_off, payload_size, bootloader);
}
+25
View File
@@ -763,6 +763,31 @@ TEST(OtaHybridHandoff, EncodesExactGeometryAndRejectsCorruption) {
MOTA_NRF52_FLASH_PAGE, ram, hash));
}
TEST(OtaBootPackage, OptionalRakApplicationStoragePreservesLegacyBootUpdateContract) {
uint8_t image[128] = {};
const uint8_t boot[16] = {'M','O','T','A','B','L','D','R', 3,0,5,0,0x0A,0,0,0};
const uint8_t bundle[32] = {
'M','O','T','A','R','A','M','A',1,0,72,0,0,0,1,0,
'M','O','T','A','S','T','O','R',1,0,16,0,0x14,0,0,0,
};
memcpy(image, boot, sizeof(boot));
memcpy(image + 32, bundle, sizeof(bundle));
EXPECT_EQ(ota_bl_app_caps_scan(image, sizeof(image)).storage_flags, 0x1Eu);
EXPECT_EQ(ota_bl_app_caps_scan(image, sizeof(image)).optional_app_storage, 0x14u);
EXPECT_EQ(ota_bl_update_caps_scan_aligned(image, sizeof(image), 0x0A).storage_flags, 0x0Au);
for (unsigned byte : {32u, 40u, 42u, 46u, 48u, 56u, 58u, 60u, 63u}) {
image[byte] ^= 1;
EXPECT_EQ(ota_bl_app_caps_scan(image, sizeof(image)).storage_flags, 0x0Au);
image[byte] ^= 1;
}
memcpy(image + 80, bundle, sizeof(bundle));
EXPECT_EQ(ota_bl_optional_app_storage(image, sizeof(image)), 0u);
memset(image + 80, 0, sizeof(bundle));
memset(image + 48, 0, 16); // genuine old board has no optional record
EXPECT_EQ(ota_bl_app_caps_scan(image, sizeof(image)).storage_flags, 0x0Au);
EXPECT_TRUE(ota_bl_update_caps_scan_aligned(image, sizeof(image), 0x0A).present);
}
TEST(OtaBootPackage, LegacyAndCurrentBootloadersHaveSeparateCapabilityViews) {
const uint8_t internal_profile = OTA_BL_PROFILE_INTERNAL_BOOT_UPDATE;
const uint8_t legacy_marker[16] = {
+141
View File
@@ -0,0 +1,141 @@
"""Exercise the actual adaptive store with simulated internal and QSPI media."""
from pathlib import Path
import subprocess
import tempfile
import unittest
ROOT = Path(__file__).resolve().parents[1]
class AdaptiveBootloaderTest(unittest.TestCase):
def test_storage_switch_and_manual_resume(self):
header = (ROOT / "src/helpers/ota/OtaStoreAdaptiveNrf52.h").read_text()
body = header[header.index("class OtaStoreAdaptiveNrf52"):
header.index("} // namespace ota")]
source = r'''
#include <cassert>
#include <new>
#include "src/helpers/ota/OtaStore.h"
#include "src/helpers/ota/OtaBlInfo.h"
#include "src/helpers/ota/OtaRakStoragePolicy.h"
using namespace mesh::ota;
namespace mesh { namespace ota {
static bool capable = true, boot_resume = false, app_resume = false;
static unsigned detected = 2, internal_begins = 0, external_begins = 0;
static unsigned internal_live = 0, external_live = 0;
struct OtaBootloaderIdentity { bool crc_ok = true; const char* device_name = "4631_DFU"; };
bool ota_installed_bootloader_identity(OtaBootloaderIdentity&) { return true; }
struct MotaManifest { bool is_bootloader() const { return boot_resume; } };
bool mota_parse_manifest(const uint8_t*, size_t, MotaManifest&) { return true; }
OtaBlCaps simulatedCaps() {
OtaBlCaps c; c.present = true; c.apply_abi = capable ? 3 : 2;
c.codec_mask = 5; c.storage_flags = capable ? 0x1e : 0x16;
c.optional_app_storage = capable ? 0x14 : 0; return c;
}
OtaBlCaps simulatedBootCaps() {
auto c = simulatedCaps(); c.storage_flags = capable ? 0x0a : 0x02;
c.optional_app_storage = 0; return c;
}
class Store : public OtaStore {
public:
uint32_t size = 0;
bool begin(uint32_t n) override { size = n; return true; }
bool write(uint32_t, const uint8_t*, uint32_t) override { return true; }
bool read(uint32_t, uint8_t*, uint32_t) const override { return true; }
uint32_t capacity() const override { return 65536; }
uint32_t staged_size() const override { return size; }
void clear() override { size = 0; }
};
class OtaStoreFlashNrf52 : public Store {
public:
OtaStoreFlashNrf52() { ++internal_live; assert(external_live == 0); }
~OtaStoreFlashNrf52() { --internal_live; }
bool begin(uint32_t n) override { ++internal_begins; return Store::begin(n); }
bool reopenFor(const uint8_t*, uint32_t) override { return boot_resume; }
};
class OtaStoreQspiNrf52 : public Store {
public:
OtaStoreQspiNrf52() { ++external_live; assert(internal_live == 0); }
~OtaStoreQspiNrf52() { --external_live; }
static uint8_t autoDetect() { return detected; }
static bool headerW25Detected() { return true; }
uint32_t jedec_id() const { return 0xef4015; }
uint8_t status1() const { return 0; }
const char* last_stage() const { return "ok"; }
const char* last_error() const { return "ok"; }
bool begin(uint32_t n) override { ++external_begins; return Store::begin(n); }
bool reopenFor(const uint8_t*, uint32_t) override { return app_resume; }
};
#define ota_bootloader_app_caps simulatedCaps
#define ota_bootloader_update_caps simulatedBootCaps
'''
source += body
source += r'''
} }
int main() {
uint8_t mid[4] = {1, 2, 3, 4};
{
OtaStoreAdaptiveNrf52 store;
assert(store.usesExternal());
assert(store.plan_layout(true, 40960, 365, 40960, true));
assert(store.usesInternal());
assert(store.begin(41330));
assert(internal_begins == 1 && external_begins == 0);
assert(store.plan_layout(true, 100000, 365, 100000, false));
assert(store.usesExternal());
assert(store.begin(100370));
assert(external_begins == 1);
boot_resume = true;
assert(!store.reopenFor(nullptr, 0));
assert(store.usesExternal());
assert(store.reopenFor(mid, 0));
assert(store.usesInternal());
boot_resume = false;
assert(!store.reopenFor(mid, 0));
assert(store.usesExternal());
app_resume = true;
assert(store.reopenFor(mid, 0));
assert(store.usesExternal());
capable = false;
assert(!store.plan_layout(true, 40960, 365, 40960, true));
assert(store.usesExternal());
}
assert(internal_live == 0 && external_live == 0);
capable = true; detected = 0;
{
OtaStoreAdaptiveNrf52 store;
assert(store.usesInternal());
assert(store.plan_layout(true, 40960, 365, 40960, true));
assert(store.begin(41330));
assert(store.plan_layout(false, 40960, 365, 40960, false));
assert(store.usesInternal());
}
assert(internal_live == 0 && external_live == 0);
for (uint8_t flags : {uint8_t(0x0a), uint8_t(0x1e)}) {
OtaBlCaps c = simulatedCaps(); c.storage_flags = flags;
assert(ota_bootloader_self_update_caps_valid(c));
c.apply_abi = 2; assert(!ota_bootloader_self_update_caps_valid(c));
}
for (uint8_t flags : {uint8_t(0x09), uint8_t(0x0e), uint8_t(0x16), uint8_t(0x1f)}) {
OtaBlCaps c = simulatedCaps(); c.storage_flags = flags;
assert(!ota_bootloader_self_update_caps_valid(c));
}
}
'''
source = "#include <initializer_list>\n" + source
with tempfile.TemporaryDirectory() as directory:
cpp = Path(directory) / "adaptive.cpp"
exe = Path(directory) / "adaptive"
cpp.write_text(source)
subprocess.run([
"c++", "-std=c++17", "-Wall", "-Wextra", "-Werror",
"-DOTA_RAK_AUTO_STORE", "-DOTA_INTERNAL_BOOTLOADER_UPDATE",
"-I", str(ROOT), str(cpp), "-o", str(exe),
], check=True)
subprocess.run([str(exe)], check=True)
if __name__ == "__main__":
unittest.main()
+8
View File
@@ -25,6 +25,14 @@ int main() {
const auto Q = RakStorageChoice::Qspi;
const auto U = RakStorageChoice::Unsafe;
for (bool rak3401 : {false, true}) {
const char* board = rak3401 ? "3401_DFU" : "4631_DFU";
assert(rak_storage_choice(0, true, true, board, rak3401, true) == I);
assert(rak_storage_choice(2, true, true, board, rak3401, true) == Q);
assert(rak_storage_choice(2, true, true, board, rak3401, false) == U);
assert(rak_storage_choice(1, true, true, board, rak3401, true) == (rak3401 ? U : Q));
assert(rak_storage_choice(3, true, true, board, rak3401, true) == U);
assert(rak_storage_choice(2, true, true,
rak3401 ? "4631_DFU" : "3401_DFU", rak3401, true) == U);
assert(rak_storage_choice(0, false, false, nullptr, rak3401) == I);
assert(rak_storage_choice(0, true, false, nullptr, rak3401) == U);
assert(rak_storage_choice(0, true, true,
+9 -2
View File
@@ -1089,12 +1089,19 @@ def compatible_mota(info: MotaInfo, target: TargetInfo) -> tuple[bool, str]:
return False, "bootloader self-update requires installed ABI 3 or newer"
if target.bootloader_codecs != 0x5:
return False, "bootloader self-update requires FULL|INPLACE codecs 0x5"
expected_storage = 0x09 if target.nrf_sd else 0x0E if target.nrf_qspi else 0x0A
adaptive_bootloader = target.boot_hw_id in (
"NRF_BL_239A0029_3401_AUTO_DFU", "NRF_BL_239A0029_4631_AUTO_DFU",
)
internal_rak_bootloader = target.boot_hw_id in (
"NRF_BL_239A0029_3401_DFU", "NRF_BL_239A0029_4631_DFU",
)
expected_storage = (0x0A if internal_rak_bootloader else 0x1E if adaptive_bootloader else
0x09 if target.nrf_sd else 0x0E if target.nrf_qspi else 0x0A)
if (
target.boot_storage != expected_storage
or info.bootloader_storage != expected_storage
):
return False, "bootloader package, installed capabilities, and application storage differ"
return False, "bootloader package and installed capabilities differ"
if info.block_size > target.max_block_size:
return False, f"bootloader block exceeds destination maxblk:{target.max_block_size}"
return True, ""
+48
View File
@@ -0,0 +1,48 @@
"""Adaptive bootloader packages use internal flash regardless of app storage."""
from dataclasses import replace
import unittest
import lora_ota as ota
class RakBootloaderCompatibilityTest(unittest.TestCase):
def fixture(self, name, target_id, external):
hw = f"NRF_BL_239A0029_{name}_DFU"
package = ota.MotaInfo(
path=None, blob=b"", flags=ota.MOTA_FLAG_FULL | ota.MOTA_FLAG_BOOTLOADER,
target_id=target_id, fw_version=0x02040A02, image_size=40960,
payload_size=40960, block_size=1024, merkle_root=b"\0" * 4,
image_hash=b"\0" * 8, codec_id=0, hw_id=hw,
base_hash=b"\0" * 8, payload_offset=365, bootloader_storage=0x0A,
)
target = ota.TargetInfo(
name=name, target_id=1, base_hash=b"\0" * 8, platform="nrf52",
nrf_sd=False, nrf_qspi=external, hw_id=name,
bootloader_version="OTAFIX2.4.10-preview.1", bootloader_abi=3,
bootloader_codecs=5, status="", self_status="",
boot_target_id=target_id, boot_hw_id=hw, boot_storage=0x0A,
)
return package, target
def test_internal_boot_update_with_either_application_backend(self):
for name, target_id in (("3401", 0x23818A80), ("4631", 0x2D0DF000)):
for external in (False, True):
with self.subTest(board=name, external=external):
package, target = self.fixture(name, target_id, external)
self.assertTrue(ota.compatible_mota(package, target)[0])
for flags in (0x1E, 0x0E, 0x16):
self.assertFalse(ota.compatible_mota(
replace(package, bootloader_storage=flags), target)[0])
self.assertFalse(ota.compatible_mota(
package, replace(target, boot_storage=flags))[0])
self.assertFalse(ota.compatible_mota(
package, replace(target, bootloader_abi=2))[0])
self.assertFalse(ota.compatible_mota(
package, replace(target, boot_hw_id="NRF_BL_239A0029_4631_AUTO_DFU"))[0])
self.assertFalse(ota.compatible_mota(
package, replace(target, boot_target_id=target_id ^ 1))[0])
if __name__ == "__main__":
unittest.main()
+36 -3
View File
@@ -108,11 +108,17 @@ BOOT_STORAGE_SD = 0x01
BOOT_STORAGE_STAGE_CEILING = 0x02
XIAO_BOOT_STORAGE_QSPI = 0x04
XIAO_BOOT_STORAGE_UPDATE = 0x08
BOOT_STORAGE_KNOWN = 0x0F
BOOT_STORAGE_HEADER_W25 = 0x10
BOOT_STORAGE_KNOWN = 0x1F
BOOT_STORAGE_SD_UPDATE = BOOT_STORAGE_SD | XIAO_BOOT_STORAGE_UPDATE
BOOT_STORAGE_QSPI_UPDATE = (BOOT_STORAGE_STAGE_CEILING | XIAO_BOOT_STORAGE_QSPI |
XIAO_BOOT_STORAGE_UPDATE)
BOOT_STORAGE_INTERNAL_UPDATE = BOOT_STORAGE_STAGE_CEILING | XIAO_BOOT_STORAGE_UPDATE
BOOT_STORAGE_RAK_AUTO_UPDATE = BOOT_STORAGE_QSPI_UPDATE | BOOT_STORAGE_HEADER_W25
RAK_AUTO_BOOTLOADER_IDENTITIES = (
(0x239A0029, "3401_AUTO_DFU"),
(0x239A0029, "4631_AUTO_DFU"),
)
BOOT_REQUIRED_APP_CODEC_MASK = ((1 << CODEC_FULL) |
(1 << CODEC_DETOOLS_INPLACE))
@@ -696,7 +702,7 @@ def audit_bootloader_target_inventory(application_target_ids=()) -> dict:
identities = (
(XIAO_BOOT_BOARD_ID_BASE, "XIAO_DFU"),
(XIAO_BOOT_BOARD_ID_SENSE, "XIAO_DFU"),
) + INTERNAL_BOOTLOADER_IDENTITIES
) + INTERNAL_BOOTLOADER_IDENTITIES + RAK_AUTO_BOOTLOADER_IDENTITIES
app_ids = set(application_target_ids)
targets = {}
for identity in identities:
@@ -714,6 +720,7 @@ def audit_bootloader_target_inventory(application_target_ids=()) -> dict:
def bootloader_identity_is_buildable(board_id: int, device_name: str) -> bool:
return ((board_id, device_name) in INTERNAL_BOOTLOADER_IDENTITIES or
(board_id, device_name) in RAK_AUTO_BOOTLOADER_IDENTITIES or
(board_id, device_name) in (
(XIAO_BOOT_BOARD_ID_BASE, "XIAO_DFU"),
(XIAO_BOOT_BOARD_ID_SENSE, "XIAO_DFU")))
@@ -734,6 +741,8 @@ def bootloader_qualified_platform_profile(board_id: int, device_name: str):
def bootloader_qualified_storage_profiles(board_id: int, device_name: str):
"""Return the exact allowed capability-marker profiles for one identity."""
identity = (board_id, device_name)
if identity in RAK_AUTO_BOOTLOADER_IDENTITIES:
return (BOOT_STORAGE_RAK_AUTO_UPDATE,)
if identity in ((XIAO_BOOT_BOARD_ID_BASE, "XIAO_DFU"),
(XIAO_BOOT_BOARD_ID_SENSE, "XIAO_DFU")):
return (BOOT_STORAGE_QSPI_UPDATE,)
@@ -836,7 +845,7 @@ def bootloader_caps_storage(image: bytes) -> Optional[int]:
if valid_count != 1:
return None
if storage in (BOOT_STORAGE_SD_UPDATE, BOOT_STORAGE_QSPI_UPDATE,
BOOT_STORAGE_INTERNAL_UPDATE):
BOOT_STORAGE_INTERNAL_UPDATE, BOOT_STORAGE_RAK_AUTO_UPDATE):
found = storage
return found if valid_count == 1 else None
@@ -845,6 +854,19 @@ def xiao_bootloader_caps_ok(image: bytes) -> bool:
return bootloader_caps_storage(image) == BOOT_STORAGE_QSPI_UPDATE
def bootloader_optional_app_storage(image: bytes) -> int:
record = (b"MOTARAMA" + struct.pack("<HHI", 1, 72, 65536) +
b"MOTASTOR" + struct.pack("<HHBBBB", 1, 16, 0x14, 0, 0, 0))
offsets = [off for off in range(0, len(image) - 7, 4)
if image[off:off + 8] == b"MOTASTOR"]
if not offsets:
return 0
if (len(offsets) != 1 or offsets[0] < 16 or
image[offsets[0] - 16:offsets[0] + 16] != record):
raise ValueError("optional RAK application storage marker is invalid or ambiguous")
return 0x14
def validate_bootloader_image(image: bytes, target_id: Optional[int] = None,
signed_hw_id: Optional[bytes] = None) -> BootloaderIdentity:
if len(image) != XIAO_BOOT_IMAGE_SIZE:
@@ -876,6 +898,17 @@ def validate_bootloader_image(image: bytes, target_id: Optional[int] = None,
if actual_storage not in expected_storage:
expected = "/".join(f"0x{value:02X}" for value in expected_storage)
raise ValueError(f"bootloader lacks exact ABI 3 self-update capabilities {expected}")
optional_storage = bootloader_optional_app_storage(image)
if optional_storage and (actual_storage != BOOT_STORAGE_INTERNAL_UPDATE or
identity.board_id != 0x239A0029 or
identity.device_name not in ("3401_DFU", "4631_DFU")):
raise ValueError("optional RAK application storage requires the deployed board identity")
if actual_storage == BOOT_STORAGE_RAK_AUTO_UPDATE or optional_storage:
ram_marker = b"MOTARAMA" + struct.pack("<HHI", 1, 72, 65536)
count = sum(image[off:off + 16] == ram_marker
for off in range(0, len(image) - 15, 4))
if count != 1 or struct.unpack_from("<I", image)[0] > 0x20030000:
raise ValueError("adaptive bootloader must retain its 64 KiB hybrid RAM arena")
expected_platform = bootloader_qualified_platform_profile(
identity.board_id, identity.device_name)
if expected_platform is not None:
+55 -2
View File
@@ -1500,7 +1500,7 @@ def test_xiao_bootloader_caps_rejects_malformed_or_unaligned_markers():
malformed_decoy = bytearray(caps())
struct.pack_into("<8sHHB3s", malformed_decoy, 24,
ml.XIAO_BOOT_CAPS_MAGIC, ml.BOOT_FORMAT_VER,
ml.BOOT_REQUIRED_APP_CODEC_MASK, 0x1C, b"\0\0\0")
ml.BOOT_REQUIRED_APP_CODEC_MASK, 0x2C, b"\0\0\0")
assert ml.bootloader_caps_storage(bytes(malformed_decoy)) == \
ml.BOOT_STORAGE_QSPI_UPDATE
@@ -1672,7 +1672,9 @@ def test_bootloader_build_inventory_is_unique_and_disjoint_from_app_targets():
for value in re.findall(r"\{ 0x([0-9a-fA-F]{8}),", target_header)
}
audited = ml.audit_bootloader_target_inventory(app_targets)
assert len(audited) == len(expected) + 2 # generic internal plus two legacy XIAOs
assert len(audited) == len(expected) + 4 # internal, two XIAOs, two adaptive RAKs
assert audited[0xD04AB3AB] == (0x239A0029, "3401_AUTO_DFU")
assert audited[0xFEEAFD1B] == (0x239A0029, "4631_AUTO_DFU")
# Generic parsing remains useful for inspecting a future candidate, but a
# signing/build call fails closed until its exact identity is qualified.
@@ -1762,6 +1764,57 @@ def test_qualified_bootloader_platform_and_storage_profiles_are_exact():
pass
def test_adaptive_bootloader_preserves_update_and_hybrid_capabilities():
for name in ("3401_AUTO_DFU", "4631_AUTO_DFU"):
image = _generic_bootloader_image(
device_name=name, storage=ml.BOOT_STORAGE_RAK_AUTO_UPDATE)
marker = b"MOTARAMA" + struct.pack("<HHI", 1, 72, 65536)
def add_ram(data):
struct.pack_into("<I", data, 0, 0x20030000)
data[0x90:0xA0] = marker
image = _rewrite_boot_image(image, add_ram)
assert ml.validate_bootloader_image(image).device_name == name
assert ml.bootloader_caps_storage(image) == 0x1E
mutations = (
lambda data: data.__setitem__(slice(0x90, 0xA0), b"\xff" * 16),
lambda data: data.__setitem__(slice(0xA0, 0xB0), marker),
lambda data: struct.pack_into("<I", data, 0, 0x20040000),
lambda data: data.__setitem__(0x8C, 0x16),
lambda data: data.__setitem__(0x88, 2),
)
for mutate in mutations:
try:
ml.validate_bootloader_image(_rewrite_boot_image(image, mutate))
assert False, "invalid adaptive successor was accepted"
except ValueError:
pass
def test_compatible_rak_keeps_legacy_contract_and_optional_application_storage():
marker = b"MOTARAMA" + struct.pack("<HHI", 1, 72, 65536)
optional = b"MOTASTOR" + struct.pack("<HHB3x", 1, 16, 0x14)
for name in ("3401_DFU", "4631_DFU"):
def add_storage(data):
struct.pack_into("<I", data, 0, 0x20030000)
data[0x90:0xB0] = marker + optional
image = _rewrite_boot_image(_generic_bootloader_image(device_name=name), add_storage)
assert ml.validate_bootloader_image(image).device_name == name
assert ml.bootloader_caps_storage(image) == 0x0A
assert ml.bootloader_optional_app_storage(image) == 0x14
for mutate in (
lambda raw: raw.__setitem__(0xAC, 0x04),
lambda raw: raw.__setitem__(0xAD, 1),
lambda raw: raw.__setitem__(slice(0xB0, 0xC0), optional),
lambda raw: raw.__setitem__(slice(0x90, 0xA0), b"\xff" * 16),
lambda raw: struct.pack_into("<I", raw, 0, 0x20040000),
):
try:
ml.validate_bootloader_image(_rewrite_boot_image(image, mutate))
assert False, "malformed optional application storage accepted"
except ValueError:
pass
def test_bootloader_builder_rejects_wrong_identity_geometry_and_continuity():
priv = Ed25519PrivateKey.generate()
image = _xiao_bootloader_image()