Revert adaptive RAK app-owned bootloader recovery after ACL test

This commit is contained in:
mikecarper
2026-09-27 15:07:29 -07:00
parent a007346561
commit ea40663f09
17 changed files with 49 additions and 521 deletions
+8 -38
View File
@@ -16,14 +16,14 @@ be replaced with the exact ABI-3 self-update-capable OTAFIX build over USB/BLE
DFU or SWD. MeshTower SD also requires the BLM2 retained-auth version to be
provisioned locally before either application or bootloader OTA.
The adaptive RAK3401 and RAK4631 loaders are an exception: their 2.4.8
application updater has ABI 2 and capability flags `0x16`, with no bootloader
self-update code. The two `*_repeater_unified_lora_ota` MeshCore builds now
include an application-owned recovery installer. Install that application
through its normal exact-base application mOTA path first. It can then fetch
an exact-board signed format-3 bootloader package from internal flash or a
matched external NOR and invoke Nordic's MBR itself. A node that cannot run
MeshCore and receive LoRa cannot use this path; use local UF2/DFU or SWD.
The adaptive RAK3401 and RAK4631 OTAFIX 2.4.8 profiles support application
full and delta updates, but not bootloader self-update. Their running
bootloader protects `0xF4000..0x100000` with the nRF52840 ACL before starting
MeshCore. This includes the MBR parameter page at `0xFE000`, so an application
cannot initiate `COPY_BL` even after verifying a signed bootloader package.
Hardware testing on a RAK3401 confirmed the MBR SVC enters a bus fault and
leaves the installed bootloader unchanged. Migrate these profiles with their
exact-board UF2 or local DFU package; do not offer a bootloader mOTA for them.
## Storage layouts
@@ -32,36 +32,6 @@ MeshCore and receive LoRa cannot use this path; use local UF2/DFU or SWD.
| XIAO-module raw QSPI | below `0xE0000` | external QSPI offset 0 | dedicated internal `0xE0000..0xEA000` scratch | source `0x51`, flags `0x0E` |
| Qualified internal-flash target | normal `0xED000` flash limit; top 64 KiB SRAM reserved for application-delta staging | shared internal slot, exact start `0xE2000` | the same eleven-page slot; no second flash reservation | source `0xED`, flags `0x0A` |
| MeshTower V2 microSD | normal `0xED000` limit | contiguous `/meshcore-ota.mota` | dynamic internal `0xE0000..0xEA000` scratch; live image must end by `0xE0000` | source `0x53`, flags `0x09` |
| Adaptive RAK3401/RAK4631 recovery application | normal `0xED000` flash limit; live EndF and bank CRC must stop by `0xE2000` | internal `0xE2000` slot or exact matched NOR | internal `0xE2000..0xEC000` raw copy | application calls MBR; installed loader flags `0x16` |
For adaptive RAK recovery, `ota bootloader status` reports the installed
`3401_AUTO_DFU` or `4631_AUTO_DFU` identity. The host must build a signed
format-3 package from a *newer* exact-board OTAFIX image with the same `0x16`
capability marker and BLM2 continuity metadata. The package is full, so it
does not depend on the installed bootloader bytes. Its target IDs are
`D04AB3AB` and `FEEAFD1B`, respectively. The application requires a trusted
signer, matching board name/ID and SoftDevice layout, complete Merkle and
SHA-256 verification, explicit MID and image-hash confirmation, and a
hash-valid live EndF plus safe bank settings before writing scratch. It
copies each page with readback, hashes the final source again after the LoRa
reply drains, then calls `SD_MBR_COMMAND_COPY_BL`. The old bootloader remains
untouched until the MBR call. An installed 2.4.8 BLM2 loader rejects an equal
or older version; a 2.4.8 image is therefore not a recovery upgrade for it.
If scratch verification or the MBR call fails, the application resets without
the bootloader-update trigger and reports `blup:CE` on its next OTA status.
Build a candidate only from the matching **newer** OTAFIX bootloader HEX:
```bash
motatool build-bootloader --fw <rak3401-auto-bootloader.hex> \
--board wiscore_rak3401_auto --sign <trusted-private-key> \
--out <rak3401-recovery.mota>
# For RAK4631 use --board wiscore_rak4631_auto and its own exact-board HEX.
```
Verify the signed file with the corresponding public key, serve it over LoRa,
pull its MID manually, and run the same `ota bootloader install <MID8> <HASH16>`
confirmation shown below. A temporary or untrusted test signing key cannot
install on a node whose allowlist does not contain its public key.
The exact SD target is
`Heltec_tower_v2_sdcard_repeater_lora_ota_no_external_sensors`. Its normal
-1
View File
@@ -271,7 +271,6 @@ build_flags =
[nrf52_wisblock_auto_ota]
build_flags =
-D OTA_RAK_AUTO_STORE=1
-D OTA_RAK_BOOTLOADER_RECOVERY=1
-D OTA_QSPI_STORE=1
-D OTA_FLASH_STORE=1
-D OTA_QSPI_SHARED_WISBLOCK_SPI=1
-183
View File
@@ -18,7 +18,6 @@
#include "OtaSelf.h" // SelfFwInfo / ota_self_firmware (running-image base_hash gate)
#endif
#elif defined(NRF52_PLATFORM)
#include <SHA256.h>
#include "OtaVerify.h"
#include "OtaSelf.h"
#include "OtaFlashLayout_nrf52.h"
@@ -27,9 +26,7 @@
#include "nrf.h"
#include "nrf_soc.h"
#include "nrf_sdm.h"
#include "nrf_mbr.h"
#include "../nrf52/SoftDeviceState.h"
#include <stdlib.h>
#if defined(OTA_SD_STORE)
#include "OtaStoreSdNrf52.h"
#endif
@@ -471,31 +468,6 @@ static OtaStoreFlashNrf52* g_nrf52_apply_store = nullptr;
#if defined(OTA_QSPI_STORE)
static uint8_t g_nrf52_qspi_handoff = GPREGRET2_OTA_STAGE_QSPI;
#endif
#if defined(OTA_RAK_BOOTLOADER_RECOVERY)
// Set only after the exact-board signed image has been copied to the internal
// source and independently hashed there. Mesh drains the LoRa reply first.
static bool g_rak_recovery_ready = false;
static uint8_t g_rak_recovery_hash[32] = {0};
static bool ota_rak_recovery_source_hash_matches() {
SHA256 sha;
const uint8_t* image = (const uint8_t*)(uintptr_t)MOTA_NRF52_SHARED_BOOT_STAGE_START;
for (uint32_t off = 0; off < OTA_BOOT_IMAGE_SIZE; off += 512u)
sha.update(image + off, 512u);
uint8_t hash[32];
sha.finalize(hash, sizeof(hash));
return memcmp(hash, g_rak_recovery_hash, sizeof(hash)) == 0;
}
static bool ota_rak_mbr_addresses_ready() {
const uint32_t mbr_boot = *(const volatile uint32_t*)(uintptr_t)MBR_BOOTLOADER_ADDR;
const uint32_t mbr_params = *(const volatile uint32_t*)(uintptr_t)MBR_PARAM_PAGE_ADDR;
const uint32_t boot = mbr_boot == UINT32_MAX ? NRF_UICR->NRFFW[0] : mbr_boot;
const uint32_t params = mbr_params == UINT32_MAX ? NRF_UICR->NRFFW[1] : mbr_params;
return boot == OTA_BOOT_IMAGE_START &&
params == OTA_BOOT_IMAGE_START + OTA_BOOT_IMAGE_SIZE;
}
#endif
static void ota_nrf52_set_reset_handoff(uint8_t request, uint8_t source) {
uint8_t sd_en = 0;
@@ -568,40 +540,6 @@ void ota_reboot_to_apply() { // public: set the apply magic +
}
void ota_reboot_to_bootloader_update() {
#if defined(OTA_RAK_BOOTLOADER_RECOVERY)
if (g_rak_recovery_ready) {
// COPY_BL succeeds by resetting directly into the new bootloader. Never
// hand 0x6B to an older adaptive loader: it cannot consume this package.
if (!ota_rak_mbr_addresses_ready() ||
!ota_rak_recovery_source_hash_matches()) {
g_rak_recovery_ready = false;
ota_nrf52_set_reset_handoff(0u, GPREGRET2_RAK_RECOVERY_FAILED);
NVIC_SystemReset();
return;
}
uint8_t sd_enabled = 0;
if (mesh_nrf52::softdeviceIsEnabled(sd_enabled) != NRF_SUCCESS ||
(sd_enabled && sd_softdevice_disable() != NRF_SUCCESS)) {
g_rak_recovery_ready = false;
ota_nrf52_set_reset_handoff(0u, GPREGRET2_RAK_RECOVERY_FAILED);
NVIC_SystemReset();
return;
}
sd_mbr_command_t command = {};
command.command = SD_MBR_COMMAND_COPY_BL;
command.params.copy_bl.bl_src =
(uint32_t*)(uintptr_t)MOTA_NRF52_SHARED_BOOT_STAGE_START;
command.params.copy_bl.bl_len = OTA_BOOT_IMAGE_SIZE / sizeof(uint32_t);
NRF_POWER->GPREGRET = 0u;
NRF_POWER->GPREGRET2 = 0u;
(void)sd_mbr_command(&command); // success never returns
g_rak_recovery_ready = false;
NRF_POWER->GPREGRET = 0u;
NRF_POWER->GPREGRET2 = GPREGRET2_RAK_RECOVERY_FAILED;
NVIC_SystemReset();
return;
}
#endif
uint8_t source = GPREGRET2_OTA_STAGE_QSPI;
#if defined(OTA_INTERNAL_BOOTLOADER_UPDATE)
source = GPREGRET2_OTA_STAGE_EXPANDED;
@@ -1076,127 +1014,6 @@ static const char* ota_bootloader_continuity_error(OtaBootloaderContinuityGate g
}
}
#if defined(OTA_RAK_BOOTLOADER_RECOVERY)
bool ota_prepare_rak_bootloader_recovery(OtaStore& store, bool internal,
const SignerAllowlist& allow,
const OtaBootloaderIdentity& installed,
const uint8_t actual_mid[4],
const uint8_t operator_mid[4],
const uint8_t operator_hash8[8],
ApplyState& st, char* msg) {
st = ApplyState();
g_rak_recovery_ready = false;
const uint32_t total = store.staged_size();
uint8_t header[8], manifest[MOTA_MFL];
MotaManifest m;
if (total != MOTA_NRF52_BOOT_CONTAINER_SIZE ||
!store.read(0, header, sizeof(header)) ||
memcmp(header, MOTA_MAGIC, sizeof(MOTA_MAGIC)) != 0 ||
rd_u32le(header + 4) != total ||
!store.read(8, manifest, sizeof(manifest)) ||
!mota_parse_manifest(manifest, sizeof(manifest), m)) {
strcpy(msg, "recovery bootloader container is invalid"); return false;
}
if (ota_bootloader_confirmation_gate(m, installed, actual_mid, operator_mid,
operator_hash8) != OTA_BOOT_CONFIRM_OK) {
strcpy(msg, "recovery bootloader identity/MID/hash confirmation failed"); return false;
}
const OtaBlCaps current = ota_bootloader_app_caps();
const uint8_t rak_flags = OTA_BL_PROFILE_RAK_AUTO;
if (!current.present || current.storage_flags != rak_flags ||
current.apply_abi < MOTA_APP_FORMAT_VER ||
(current.codec_mask & OTA_BL_REQUIRED_APP_CODEC_MASK) !=
OTA_BL_REQUIRED_APP_CODEC_MASK) {
strcpy(msg, "installed bootloader is not the adaptive RAK OTAFIX profile"); return false;
}
SelfFwInfo fi;
const uint32_t app_base = mota_nrf52_app_base();
if (!ota_self_firmware(fi) ||
!ota_bootloader_scratch_headroom_valid(
fi.valid, app_base, fi.image_len, MOTA_NRF52_SHARED_BOOT_STAGE_START) ||
!ota_bootloader_live_bank_preserves_scratch(
app_base, fi.image_len, MOTA_NRF52_SHARED_BOOT_STAGE_START) ||
ota_nrf52_effective_stage_ceiling() != MOTA_NRF52_APP_END ||
!ota_rak_mbr_addresses_ready()) {
strcpy(msg, "running app/settings or MBR do not preserve bootloader scratch"); return false;
}
if (internal) {
OtaStoreFlashNrf52& flash = static_cast<OtaStoreFlashNrf52&>(store);
if (flash.write_start() != MOTA_NRF52_SHARED_BOOT_STAGE_START ||
!flash.data() || flash.is_hybrid()) {
strcpy(msg, "internal recovery container placement is invalid"); return false;
}
}
const uint32_t payload_off = 8u + MOTA_MFL + (uint32_t)m.block_count * 4u;
if (payload_off != 365u || payload_off + OTA_BOOT_IMAGE_SIZE + 5u != total) {
strcpy(msg, "recovery payload layout is invalid"); return false;
}
mesh::Identity signer(m.signer_pubkey);
if (!signer.verify(m.signature, m.manifest_start, (int)m.signed_len) ||
!allow.contains(m.signer_pubkey)) {
strcpy(msg, "recovery bootloader signer is not trusted"); return false;
}
const VerifyResult verified = ota_verify(store, allow, manifest);
st.manifest_ok = verified.parsed;
st.sig_ok = verified.sig_ok;
st.trusted = verified.trusted;
st.image_size = m.image_size;
memcpy(st.image_hash, m.image_hash, sizeof(st.image_hash));
if (!verified.auto_appliable() || !verified.container_hash_ok) {
strcpy(msg, "recovery bootloader package failed full verification"); return false;
}
uint8_t vectors[8];
OtaBootloaderIdentity candidate;
OtaBootloaderCapsMarker candidate_caps;
if (!store.read(payload_off, vectors, sizeof(vectors)) ||
!ota_bootloader_vector_sane(vectors) ||
!ota_bootloader_external_image_metadata(
store, payload_off, rak_flags, candidate, candidate_caps, false) ||
!ota_bootloader_identity_matches(installed, candidate) ||
candidate_caps.apply_abi < current.apply_abi ||
(candidate_caps.codec_mask & current.codec_mask) != current.codec_mask) {
strcpy(msg, "recovery candidate identity/capabilities/CRC mismatch"); return false;
}
const OtaBootloaderContinuityGate continuity = ota_bootloader_continuity_gate(
installed, candidate, m.fw_version, OTA_BOOT_CONTINUITY_FAMILY_S140,
ota_runtime_softdevice_fwid(), app_base, OTA_BOOT_CONTINUITY_LAYOUT_ABI);
if (continuity != OTA_BOOT_CONTINUITY_OK) {
strncpy(msg, ota_bootloader_continuity_error(continuity), 96);
msg[95] = 0; return false;
}
uint8_t* page = static_cast<uint8_t*>(malloc(MOTA_NRF52_FLASH_PAGE));
if (!page) { strcpy(msg, "not enough RAM for recovery copy"); return false; }
bool copied = true;
for (uint32_t off = 0; off < OTA_BOOT_IMAGE_SIZE; off += MOTA_NRF52_FLASH_PAGE) {
const uint32_t dest = MOTA_NRF52_SHARED_BOOT_STAGE_START + off;
// Internal packages overlap the destination by 365 bytes. Read the next
// complete source page before erasing the lower destination page.
if (!store.read(payload_off + off, page, MOTA_NRF52_FLASH_PAGE) ||
flash_nrf5x_write(dest, page, MOTA_NRF52_FLASH_PAGE) < 0) {
copied = false; break;
}
flash_nrf5x_flush();
if (memcmp((const void*)(uintptr_t)dest, page, MOTA_NRF52_FLASH_PAGE) != 0) {
copied = false; break;
}
}
free(page);
if (!copied) {
strcpy(msg, "recovery scratch copy/readback failed; bootloader unchanged"); return false;
}
memcpy(g_rak_recovery_hash, m.image_hash, sizeof(g_rak_recovery_hash));
if (!ota_rak_recovery_source_hash_matches()) {
strcpy(msg, "recovery scratch hash mismatch; bootloader unchanged"); return false;
}
st.slot_ok = true;
g_rak_recovery_ready = true;
strcpy(msg, "signed bootloader copied and verified; MBR install follows this reply");
return true;
}
#endif
#if defined(OTA_SD_BOOTLOADER_UPDATE)
static bool external_bootloader_approve(OtaStoreSdNrf52& store,
const uint8_t image_hash[32],
-15
View File
@@ -15,8 +15,6 @@
namespace mesh {
namespace ota {
class OtaStore;
struct ApplyState {
bool manifest_ok = false;
bool sig_ok = false;
@@ -133,19 +131,6 @@ bool ota_prepare_bootloader_update_nrf52(OtaStoreFlashNrf52& store,
ApplyState& st, char* msg);
#endif
#if defined(OTA_RAK_BOOTLOADER_RECOVERY)
// Application-owned recovery for adaptive RAK loaders lacking BOOT_UPDATE.
// Copies a verified full bootloader from either active store into internal
// scratch; deferred commit invokes Nordic's MBR after the LoRa reply drains.
bool ota_prepare_rak_bootloader_recovery(OtaStore& store, bool internal,
const SignerAllowlist& allow,
const OtaBootloaderIdentity& installed,
const uint8_t actual_mid[4],
const uint8_t operator_mid[4],
const uint8_t operator_hash8[8],
ApplyState& st, char* msg);
#endif
// Commit the (already approved/armed) update and reboot into it - does NOT return. Call this only after
// a successful ota_apply_* AND after the confirmation reply has been delivered, so the operator knows
// the apply started (over LoRa the device then goes silent while the bootloader applies). nRF52: set
-3
View File
@@ -93,9 +93,6 @@ 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 =
OTA_BL_STORAGE_STAGE_CEILING | OTA_BL_STORAGE_QSPI |
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;
+6 -21
View File
@@ -32,17 +32,6 @@
#endif
#endif
#if defined(OTA_RAK_BOOTLOADER_RECOVERY)
#if !defined(NRF52_PLATFORM) || !defined(OTA_RAK_AUTO_STORE) || \
!defined(OTA_INTERNAL_BOOTLOADER_UPDATE) || !defined(OTA_FLASH_STORE) || \
!defined(OTA_QSPI_STORE)
#error "RAK application recovery requires the adaptive nRF52 shared-slot profile"
#endif
#if !defined(RAK_3401) && !defined(RAK_4631)
#error "RAK application recovery is restricted to the two qualified RAK profiles"
#endif
#endif
#if defined(OTA_SD_BOOTLOADER_UPDATE)
#if !defined(NRF52_PLATFORM) || !defined(OTA_SD_STORE)
#error "OTA_SD_BOOTLOADER_UPDATE requires nRF52 SD staging"
@@ -523,19 +512,17 @@ inline bool ota_bootloader_caps_marker_parse(const uint8_t raw[16],
inline bool ota_bootloader_caps_from_image(const uint8_t* image, size_t image_size,
uint8_t exact_storage_flags,
OtaBootloaderCapsMarker& out,
bool require_boot_update = true) {
OtaBootloaderCapsMarker& out) {
out = OtaBootloaderCapsMarker();
if (!image || exact_storage_flags == 0) return false;
uint8_t valid_count = 0;
for (size_t off = 0; off + 16u <= image_size; off += 4u) {
OtaBootloaderCapsMarker parsed;
if (!ota_bootloader_caps_marker_parse(image + off, parsed) ||
parsed.apply_abi < (require_boot_update ? MOTA_BOOT_FORMAT_VER : MOTA_APP_FORMAT_VER) ||
parsed.apply_abi < MOTA_BOOT_FORMAT_VER ||
(parsed.codec_mask & OTA_BL_REQUIRED_APP_CODEC_MASK) !=
OTA_BL_REQUIRED_APP_CODEC_MASK ||
((parsed.storage_flags & OTA_BL_STORAGE_BOOT_UPDATE) != 0u) !=
require_boot_update) continue;
(parsed.storage_flags & OTA_BL_STORAGE_BOOT_UPDATE) == 0) continue;
if (++valid_count != 1u) return false; // privileged marker identity must be unambiguous
if (parsed.storage_flags != exact_storage_flags) continue;
out = parsed;
@@ -569,8 +556,7 @@ inline bool ota_bootloader_external_crc_ok(
template <typename Store>
inline bool ota_bootloader_external_image_metadata(
Store& store, uint32_t payload_off, uint8_t exact_storage_flags,
OtaBootloaderIdentity& candidate, OtaBootloaderCapsMarker& caps,
bool require_boot_update = true) {
OtaBootloaderIdentity& candidate, OtaBootloaderCapsMarker& caps) {
candidate = OtaBootloaderIdentity();
caps = OtaBootloaderCapsMarker();
if (exact_storage_flags == 0u) return false;
@@ -590,11 +576,10 @@ inline bool ota_bootloader_external_image_metadata(
if ((absolute & 3u) == 0u && local + 16u <= len) {
OtaBootloaderCapsMarker parsed;
if (ota_bootloader_caps_marker_parse(buf + local, parsed) &&
parsed.apply_abi >= (require_boot_update ? MOTA_BOOT_FORMAT_VER : MOTA_APP_FORMAT_VER) &&
parsed.apply_abi >= MOTA_BOOT_FORMAT_VER &&
(parsed.codec_mask & OTA_BL_REQUIRED_APP_CODEC_MASK) ==
OTA_BL_REQUIRED_APP_CODEC_MASK &&
((parsed.storage_flags & OTA_BL_STORAGE_BOOT_UPDATE) != 0u) ==
require_boot_update) {
(parsed.storage_flags & OTA_BL_STORAGE_BOOT_UPDATE) != 0u) {
if (++valid_caps != 1u) return false;
if (parsed.storage_flags == exact_storage_flags) caps = parsed;
}
+2 -38
View File
@@ -400,17 +400,11 @@ bool handle_ota_command(const char* command, char* reply, mesh::MainBoard& board
const OtaBootloaderIdentity& bid = c.bootloaderIdentity();
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);
#if defined(OTA_RAK_BOOTLOADER_RECOVERY)
installable = installable &&
list_bl.storage_flags == OTA_BL_PROFILE_RAK_AUTO &&
(c.fetch_store.usesExternal() || c.fetch_store.usesInternal());
#else
installable = installable && ota_bootloader_self_update_caps_valid(list_bl_update);
h->target_id == ota_bootloader_target_id(bid) &&
ota_bootloader_self_update_caps_valid(list_bl_update);
#if defined(OTA_SD_BOOTLOADER_UPDATE)
installable = installable && list_sd_headroom;
#endif
#endif
#endif
fit = installable ? "yours" : "bootloader unsupported";
} else if (myt && h->target_id == myt) {
@@ -547,11 +541,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();
#if defined(OTA_RAK_BOOTLOADER_RECOVERY)
const OtaBlCaps& bl = c.bootloaderAppCaps();
#else
const OtaBlCaps& bl = c.bootloaderUpdateCaps();
#endif
if (selflags != (MFLAG_FULL | MFLAG_SIGNED | MFLAG_BOOTLOADER) ||
selcodec != CODEC_FULL) {
strcpy(reply, "ERR malformed bootloader catalog row; capture it to folder for inspection");
@@ -561,17 +551,10 @@ bool handle_ota_command(const char* command, char* reply, mesh::MainBoard& board
strcpy(reply, "ERR bootloader package does not match this installed bootloader identity");
return true;
}
#if defined(OTA_RAK_BOOTLOADER_RECOVERY)
if (bl.storage_flags != OTA_BL_PROFILE_RAK_AUTO) {
strcpy(reply, "ERR installed bootloader is not the adaptive RAK profile");
return true;
}
#else
if (!ota_bootloader_self_update_caps_valid(bl)) {
strcpy(reply, "ERR installed bootloader lacks safe LoRa bootloader-update support");
return true;
}
#endif
#if defined(OTA_SD_BOOTLOADER_UPDATE)
SelfFwInfo sd_self;
if (!ota_self_firmware(sd_self) ||
@@ -596,21 +579,13 @@ bool handle_ota_command(const char* command, char* reply, mesh::MainBoard& board
return true;
}
#if defined(NRF52_PLATFORM)
#if defined(OTA_RAK_BOOTLOADER_RECOVERY)
const OtaBlCaps& bl = c.bootloaderAppCaps();
#else
const OtaBlCaps& bl = selboot ? c.bootloaderUpdateCaps()
: c.bootloaderAppCaps();
#endif
if (!bl.present) {
strcpy(reply, "ERR bootloader has no mOTA apply support; update it over USB first");
return true;
}
#if defined(OTA_RAK_BOOTLOADER_RECOVERY)
const uint8_t need_abi = MOTA_APP_FORMAT_VER;
#else
const uint8_t need_abi = selboot ? MOTA_BOOT_FORMAT_VER : MOTA_APP_FORMAT_VER;
#endif
if (bl.apply_abi < need_abi || selcodec >= 16 || !(bl.codec_mask & (1u << selcodec))) {
snprintf(reply, 160, "ERR bootloader cannot apply mOTA ABI %u codec %u (has abi=%u codecs=0x%x)",
need_abi, (unsigned)selcodec, bl.apply_abi, bl.codec_mask);
@@ -883,27 +858,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();
#if defined(OTA_RAK_BOOTLOADER_RECOVERY)
const OtaBlCaps& bl = c.bootloaderAppCaps();
#else
const OtaBlCaps& bl = c.bootloaderUpdateCaps();
#endif
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 defined(OTA_RAK_BOOTLOADER_RECOVERY)
if (bl.storage_flags != OTA_BL_PROFILE_RAK_AUTO) {
strcpy(reply, "Bootloader recovery unavailable: installed loader is not adaptive RAK OTAFIX");
return true;
}
#else
if (!ota_bootloader_self_update_caps_valid(bl)) {
strcpy(reply, "Bootloader update unavailable: installed bootloader supports application OTA only");
return true;
}
#endif
MotaManifest staged;
char midhx[9] = "-", hashhx[17] = "-";
const bool ready = c.manager.fetchState() == OtaManager::COMPLETE &&
-21
View File
@@ -359,19 +359,6 @@ struct OtaContext {
}
const OtaBootloaderIdentity& installed = bootloaderIdentity();
#if defined(OTA_RAK_AUTO_STORE)
#if defined(OTA_RAK_BOOTLOADER_RECOVERY)
if (!fetch_store.usesInternal() && !fetch_store.usesExternal()) {
strncpy(msg, fetch_store.selectionReason(), 96);
msg[95] = 0; return false;
}
const bool internal = fetch_store.usesInternal();
OtaStore& recovery_store = internal
? static_cast<OtaStore&>(fetch_store.internalStore())
: static_cast<OtaStore&>(fetch_store.externalStore());
bool ok = ota_prepare_rak_bootloader_recovery(
recovery_store, internal, allow, installed,
manager.fetchManifestId(), operator_mid, operator_hash8, apply_st, msg);
#else
if (!fetch_store.usesInternal()) {
strncpy(msg, "bootloader LoRa update needs the internal OTAFIX profile", 96);
msg[95] = 0; return false;
@@ -379,7 +366,6 @@ struct OtaContext {
bool ok = ota_prepare_bootloader_update_nrf52(
fetch_store.internalStore(), allow, installed, manager.fetchManifestId(), operator_mid,
operator_hash8, apply_st, msg);
#endif
#else
bool ok = ota_prepare_bootloader_update_nrf52(
fetch_store, allow, installed, manager.fetchManifestId(), operator_mid,
@@ -718,15 +704,8 @@ struct OtaContext {
(defined(OTA_QSPI_BOOTLOADER_UPDATE) || defined(OTA_INTERNAL_BOOTLOADER_UPDATE) || \
defined(OTA_SD_BOOTLOADER_UPDATE))
#if defined(OTA_RAK_AUTO_STORE)
#if defined(OTA_RAK_BOOTLOADER_RECOVERY)
manager.set_accept_bootloader(
(fetch_store.usesInternal() || fetch_store.usesExternal()) &&
bootloaderIdentity().crc_ok &&
bootloaderAppCaps().storage_flags == OTA_BL_PROFILE_RAK_AUTO);
#else
manager.set_accept_bootloader(fetch_store.usesInternal() &&
ota_bootloader_self_update_caps_valid(ota_bootloader_update_caps()));
#endif
#else
manager.set_accept_bootloader(true);
#endif
-1
View File
@@ -52,7 +52,6 @@ static const uint32_t MOTA_NRF52_FLASH_PAGE = 4096u;
static const uint32_t MOTA_NRF52_CONTAINER_MIN_SIZE = 8u + 197u + 5u;
static const uint8_t GPREGRET_OTA_APPLY = 0x6Au; // distinct from DFU magics 0x57/0x4E/0xA8
static const uint8_t GPREGRET_OTA_BOOTLOADER_UPDATE = 0x6Bu;
static const uint8_t GPREGRET2_RAK_RECOVERY_FAILED = 0xCEu;
static const uint8_t GPREGRET2_OTA_STAGE_LEGACY = 0xD4u;
static const uint8_t GPREGRET2_OTA_STAGE_EXPANDED = 0xEDu;
static const uint8_t GPREGRET2_OTA_STAGE_QSPI = 0x51u;
+5 -29
View File
@@ -4,7 +4,6 @@
#include "OtaSelf.h"
#include "OtaBlInfo.h"
#include "OtaBootloaderUpdate.h"
#include "MotaContainer.h"
#include "OtaDebug.h"
#include "OtaByteIO.h" // align_down / rd_u32le (flash-page geometry + header read)
@@ -155,11 +154,7 @@ bool OtaStoreFlashNrf52::plan_layout(bool is_full, uint32_t image_size,
#if defined(OTA_INTERNAL_BOOTLOADER_UPDATE)
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_update_caps())
#if defined(OTA_RAK_BOOTLOADER_RECOVERY)
|| ota_bootloader_app_caps().storage_flags == OTA_BL_PROFILE_RAK_AUTO
#endif
))
!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
@@ -170,12 +165,7 @@ bool OtaStoreFlashNrf52::plan_layout(bool is_full, uint32_t image_size,
const uint32_t app_base = mota_nrf52_app_base();
if (!ota_self_firmware(fi) || !fi.valid || fi.image_len > UINT32_MAX - app_base ||
!mota_nrf52_shared_boot_stage_plan(
(uint32_t)total64, app_base, true, app_base + fi.image_len, start)
#if defined(OTA_RAK_BOOTLOADER_RECOVERY)
|| !ota_bootloader_live_bank_preserves_scratch(
app_base, fi.image_len, MOTA_NRF52_SHARED_BOOT_STAGE_START)
#endif
)
(uint32_t)total64, app_base, true, app_base + fi.image_len, start))
return false;
_planned_bootloader = true;
_planned_total = (uint32_t)total64;
@@ -355,12 +345,7 @@ bool OtaStoreFlashNrf52::begin(uint32_t total_size) {
!ota_self_firmware(fi) || !fi.valid || fi.image_len > UINT32_MAX - app_base ||
!mota_nrf52_shared_boot_stage_plan(
total_size, app_base, true, app_base + fi.image_len, checked_start) ||
checked_start != planned_start
#if defined(OTA_RAK_BOOTLOADER_RECOVERY)
|| !ota_bootloader_live_bank_preserves_scratch(
app_base, fi.image_len, MOTA_NRF52_SHARED_BOOT_STAGE_START)
#endif
)
checked_start != planned_start)
return false;
app_end = app_base + fi.image_len;
#else
@@ -612,19 +597,10 @@ bool OtaStoreFlashNrf52::reopenFor(const uint8_t* want_mid, uint32_t expected_ta
if (bootloader) {
#if defined(OTA_INTERNAL_BOOTLOADER_UPDATE)
SelfFwInfo fi;
if (!(ota_bootloader_self_update_caps_valid(ota_bootloader_update_caps())
#if defined(OTA_RAK_BOOTLOADER_RECOVERY)
|| ota_bootloader_app_caps().storage_flags == OTA_BL_PROFILE_RAK_AUTO
#endif
) ||
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
#if defined(OTA_RAK_BOOTLOADER_RECOVERY)
|| !ota_bootloader_live_bank_preserves_scratch(
app_base, fi.image_len, MOTA_NRF52_SHARED_BOOT_STAGE_START)
#endif
)
total, app_base, true, app_base + fi.image_len, want) || want != start)
continue;
#else
continue;
+2 -26
View File
@@ -4,10 +4,6 @@
#include "OtaByteIO.h"
#include "OtaFlashLayout_nrf52.h"
#if defined(OTA_RAK_BOOTLOADER_RECOVERY)
#include "OtaBootloaderUpdate.h"
#include "OtaSelf.h"
#endif
#include "hal/nrf_gpio.h"
#include "hal/nrf_qspi.h"
#include "nrf.h"
@@ -889,9 +885,8 @@ uint32_t OtaStoreQspiNrf52::capacity() const {
return result;
}
bool OtaStoreQspiNrf52::plan_layout(bool is_full, uint32_t image_size,
uint32_t payload_off, uint32_t payload_size,
bool is_bootloader) {
bool OtaStoreQspiNrf52::plan_layout(bool, uint32_t image_size, uint32_t,
uint32_t payload_size, bool) {
// This is the start of a new manifest admission attempt. Clear diagnostics
// left by the normal empty-store reopen probe, then latch the first error
// from this attempt until the operator reads it or starts another attempt.
@@ -900,25 +895,6 @@ bool OtaStoreQspiNrf52::plan_layout(bool is_full, uint32_t image_size,
_stage = OtaQspiStage::IDLE;
const uint32_t app_base = mota_nrf52_app_base();
const uint32_t app_ceiling = mota_nrf52_application_ceiling();
#if defined(OTA_RAK_BOOTLOADER_RECOVERY)
if (is_bootloader) {
SelfFwInfo fi;
if (!is_full || image_size != OTA_BOOT_IMAGE_SIZE ||
payload_size != OTA_BOOT_IMAGE_SIZE || payload_off != 365u ||
ota_nrf52_effective_stage_ceiling() != MOTA_NRF52_APP_END ||
!ota_self_firmware(fi) ||
!ota_bootloader_scratch_headroom_valid(
fi.valid, app_base, fi.image_len, MOTA_NRF52_SHARED_BOOT_STAGE_START) ||
!ota_bootloader_live_bank_preserves_scratch(
app_base, fi.image_len, MOTA_NRF52_SHARED_BOOT_STAGE_START)) {
fail("recovery scratch is unavailable");
releaseFlash();
return false;
}
}
#else
(void)is_full; (void)payload_off; (void)is_bootloader;
#endif
if (image_size == 0 || payload_size == 0 || app_base >= app_ceiling ||
image_size > app_ceiling - app_base) {
fail("image exceeds nRF52 application region");
-34
View File
@@ -278,26 +278,6 @@ TEST(OtaBootPackage, CandidateContinuityEnvelopeHasOneCanonicalFinalOffset) {
store, 0, OTA_BL_PROFILE_SD_BOOT_UPDATE, identity, caps));
EXPECT_EQ(identity.manifest_offset, OTA_BOOT_CANDIDATE_MANIFEST_OFFSET);
// Adaptive RAK recovery is authenticated by the application, so its
// candidate retains ABI 2 and the exact 0x16 application capability marker.
auto rak_recovery = canonical;
rak_recovery[0x400u + 8u] = MOTA_APP_FORMAT_VER;
rak_recovery[0x400u + 12u] = OTA_BL_STORAGE_STAGE_CEILING |
OTA_BL_STORAGE_QSPI |
OTA_BL_STORAGE_HEADER_W25;
wr_u32le(rak_recovery.data() + OTA_BOOT_CANDIDATE_MANIFEST_OFFSET + 40u,
0u);
wr_u32le(rak_recovery.data() + OTA_BOOT_CANDIDATE_MANIFEST_OFFSET + 40u,
ota_boot_image_crc32(rak_recovery.data(), rak_recovery.size(),
OTA_BOOT_CANDIDATE_MANIFEST_OFFSET + 40u));
ASSERT_TRUE(store.begin((uint32_t)rak_recovery.size()));
ASSERT_TRUE(store.write(0, rak_recovery.data(), (uint32_t)rak_recovery.size()));
EXPECT_TRUE(ota_bootloader_external_image_metadata(
store, 0, rak_recovery[0x400u + 12u], identity, caps, false));
EXPECT_EQ(caps.apply_abi, MOTA_APP_FORMAT_VER);
EXPECT_FALSE(ota_bootloader_external_image_metadata(
store, 0, rak_recovery[0x400u + 12u], identity, caps));
// A second CRC-valid base identity still counts when its adjacent BLM2
// extension is only half present. These coupled values are the fixed point
// for this deterministic image. Counting continuity first would wrongly
@@ -830,20 +810,6 @@ TEST(OtaBootPackage, LegacyAndCurrentBootloadersHaveSeparateCapabilityViews) {
EXPECT_TRUE(app.present);
EXPECT_FALSE(update.present);
// Adaptive RAK ABI-2 advertises application OTA and the recovery storage
// layout, while its bootloader still cannot self-update.
const uint8_t adaptive_marker[16] = {
'M','O','T','A','B','L','D','R', 2,0, 5,0,
OTA_BL_PROFILE_RAK_AUTO, 0,0,0};
memset(image, 0xFF, sizeof(image));
memcpy(image + 4, adaptive_marker, sizeof(adaptive_marker));
app = ota_bl_app_caps_scan(image, sizeof(image));
update = ota_bl_update_caps_scan_aligned(
image, sizeof(image), OTA_BL_PROFILE_RAK_AUTO);
ASSERT_TRUE(app.present);
EXPECT_EQ(app.storage_flags, OTA_BL_PROFILE_RAK_AUTO);
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));
+6 -12
View File
@@ -149,7 +149,7 @@ MIN_MESHCLI_VERSION = (1, 6, 0)
# Tested format-3 inspect/verify/serve support. Keep automatic repairs pinned;
# neither a package nor a moving branch may choose code to install on the host.
MOTATOOL_REPAIR_REPOSITORY = "https://github.com/mikecarper/motatool.git"
MOTATOOL_REPAIR_REVISION = "beff192aec341077f6d9d4b4938e47323775b9eb"
MOTATOOL_REPAIR_REVISION = "8c38369e7d35ad50cf74261869676d52dd24adf7"
MOTATOOL_REPAIR_TIMEOUT_SECONDS = 60 * 60
# The pinned lockfile, not motatool's older package-level rust-version: zeroize
# needs edition 2024 (1.85), and idna_adapter/ICU 2.2 require Rust 1.86.
@@ -1085,15 +1085,11 @@ def compatible_mota(info: MotaInfo, target: TargetInfo) -> tuple[bool, str]:
f"bootloader target {info.target_id:08X} hw={info.hw_id}, need "
f"{target.boot_target_id:08X} hw={target.boot_hw_id}"
)
recovery = (target.boot_storage == 0x16 and
info.bootloader_storage == 0x16)
if target.bootloader_abi is None or target.bootloader_abi < (2 if recovery else 3):
return False, "bootloader install requires a capable installed OTAFIX profile"
if target.bootloader_abi is None or target.bootloader_abi < 3:
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 = (0x16 if recovery else
0x09 if target.nrf_sd else
0x0E if target.nrf_qspi else 0x0A)
expected_storage = 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
@@ -2769,7 +2765,7 @@ def query_bootloader_target(
if match is None:
raise OtaError(
f"destination cannot stage a bootloader update: {reply}; "
"requires an exact-board update or adaptive RAK recovery application"
"requires exact-board ABI-3 self-update bootloader and capable application"
)
board, target_id, name, _crc, abi, caps = match.groups()
library = bootloader_library()
@@ -2783,9 +2779,7 @@ def query_bootloader_target(
raise OtaError(f"invalid destination bootloader identity: {exc}") from exc
if derived_target != int(target_id, 16):
raise OtaError("destination bootloader board/name does not match its target ID")
recovery = int(caps, 16) == 0x16 and (int(board, 16), name) in \
library.RAK_AUTO_RECOVERY_IDENTITIES
if int(abi) < (2 if recovery else 3) or int(abi) != target.bootloader_abi:
if int(abi) < 3 or int(abi) != target.bootloader_abi:
raise OtaError("destination reports inconsistent or unsupported bootloader ABI")
return replace(
target, boot_target_id=derived_target, boot_hw_id=hw_id,
+4 -28
View File
@@ -21,7 +21,7 @@ import lora_ota as ota
from test_lora_ota import firmware, mota_blob, prepare_args, target, VERSION_NEW
def boot_blob(*, storage=0x0A, version=0x0117010D, rak_name="3401_AUTO_DFU"):
def boot_blob(*, storage=0x0A, version=0x0117010D):
"""Build a signed test-only candidate using the canonical reference builder."""
ml = ota.bootloader_library()
board, name, app_base = 0x239A0029, "3401_DFU", ml.NRF52_APP_BASE_S140_V6
@@ -30,12 +30,9 @@ def boot_blob(*, storage=0x0A, version=0x0117010D, rak_name="3401_AUTO_DFU"):
name = ml.XIAO_BOOT_DEVICE_NAME.rstrip(b"\0").decode("ascii")
elif storage == 0x09:
board, name = 0x239A0071, "TOWER_V2_OTA"
elif storage == 0x16:
board, name = 0x239A0029, rak_name
image = bytearray(b"\xff" * ml.XIAO_BOOT_IMAGE_SIZE)
struct.pack_into("<II", image, 0, 0x20040000, ml.XIAO_BOOT_IMAGE_START + 0x101)
struct.pack_into("<8sHHB3x", image, 0x80, ml.XIAO_BOOT_CAPS_MAGIC,
2 if storage == 0x16 else 3, 5, storage)
struct.pack_into("<8sHHB3x", image, 0x80, ml.XIAO_BOOT_CAPS_MAGIC, 3, 5, storage)
offset = ml.BOOT_CANDIDATE_MANIFEST_OFFSET
struct.pack_into(
"<8sHHIII16sI", image, offset, ml.XIAO_BOOT_MANIFEST_MAGIC,
@@ -62,8 +59,7 @@ def boot_blob(*, storage=0x0A, version=0x0117010D, rak_name="3401_AUTO_DFU"):
def boot_target(package):
return replace(
target(platform="nrf52", boot_codecs=5),
bootloader_abi=2 if package.bootloader_storage == 0x16 else 3,
target(platform="nrf52", boot_codecs=5), bootloader_abi=3,
boot_target_id=package.target_id, boot_hw_id=package.hw_id,
boot_storage=package.bootloader_storage,
nrf_sd=package.bootloader_storage == 9,
@@ -86,31 +82,11 @@ class BootloaderStagingTests(unittest.TestCase):
self.assertEqual(package.payload_size, 40960)
def test_all_supported_storage_profiles(self):
for storage in (10, 14, 9, 22):
for storage in (10, 14, 9):
with self.subTest(storage=storage):
package = ota.parse_mota(boot_blob(storage=storage))
self.assertEqual(ota.compatible_mota(package, boot_target(package)), (True, ""))
def test_adaptive_rak_recovery_accepts_internal_and_nor_storage(self):
for name in ("3401_AUTO_DFU", "4631_AUTO_DFU"):
package = ota.parse_mota(boot_blob(storage=0x16, rak_name=name))
for external in (False, True):
live = replace(boot_target(package), nrf_qspi=external)
self.assertEqual(ota.compatible_mota(package, live), (True, ""))
live = replace(boot_target(package), bootloader_abi=1)
self.assertFalse(ota.compatible_mota(package, live)[0])
def test_adaptive_rak_bootloader_status_uses_app_owned_recovery(self):
controller = mock.Mock()
controller.remote_command.return_value = (
"BL board=239A0029 target=D04AB3AB name=3401_AUTO_DFU "
"crc=12345678 abi=2 caps=16 | staged:none mid=- hash=-"
)
live = replace(target(platform="nrf52", boot_codecs=5), bootloader_abi=2)
result = ota.query_bootloader_target(controller, live)
self.assertEqual(result.boot_target_id, 0xD04AB3AB)
self.assertEqual(result.boot_storage, 0x16)
def test_malformed_bootloaders_fail_closed(self):
for offset in (9, 10, 11, 27, 28, 105, 137, 201, 205, 600):
with self.subTest(offset=offset):
+4 -7
View File
@@ -63,11 +63,9 @@ targeting a deployed receiver that predates the extended transport descriptor.
Passing `bootloader=True` is a deliberately narrow reference-only path: it
requires a nonzero version, a signed exact 40 KiB OTAFIX region, 1024-byte
blocks, full codec, zero base hash, a sane vector table, an exact embedded
manifest/CRC identity, and a marker retaining both application codecs
(`FULL|INPLACE`, mask `0x0005`). Self-update profiles require ABI 3 and the
exact boot-update storage capability. The two adaptive RAK recovery profiles
use ABI 2 and exact `0x16` application capabilities; their MeshCore
application verifies and copies the image through the MBR. Deployed XIAO packages retain their
manifest/CRC identity, and an ABI-3 marker retaining both application codecs
(`FULL|INPLACE`, mask `0x0005`), the selected storage, and boot-update
capabilities. Deployed XIAO packages retain their
`XIAO_BL_28860044`/`XIAO_BL_28860045` IDs. Generic packages derive the padded
`NRF_BL_<BOARD_ID>_<DEVICE_NAME>` ID and collision-checked wire target from the
full embedded manifest pair. Parsing and `verify()` repeat those gates; magic
@@ -78,8 +76,7 @@ builder accepts only identities in the qualified inventory. That inventory
currently covers the shared-internal nRF52840 targets, the two deployed XIAO
raw-QSPI identities, and the exact MeshTower V2 identity whose candidate
marker may select either its internal (`0x0A`) or microSD (`0x09`) application
layout, plus the two exact adaptive RAK identities (`3401_AUTO_DFU` and
`4631_AUTO_DFU`). The test suite verifies those boot target IDs are unique and disjoint
layout. The test suite verifies those boot target IDs are unique and disjoint
from the generated application target table.
This library does not authorize a device update. A capable node will only arm
+8 -28
View File
@@ -108,15 +108,11 @@ BOOT_STORAGE_SD = 0x01
BOOT_STORAGE_STAGE_CEILING = 0x02
XIAO_BOOT_STORAGE_QSPI = 0x04
XIAO_BOOT_STORAGE_UPDATE = 0x08
BOOT_STORAGE_KNOWN = 0x1F
BOOT_STORAGE_HEADER_W25 = 0x10
BOOT_STORAGE_KNOWN = 0x0F
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_RECOVERY = (BOOT_STORAGE_STAGE_CEILING |
XIAO_BOOT_STORAGE_QSPI |
BOOT_STORAGE_HEADER_W25)
BOOT_REQUIRED_APP_CODEC_MASK = ((1 << CODEC_FULL) |
(1 << CODEC_DETOOLS_INPLACE))
@@ -142,13 +138,6 @@ INTERNAL_BOOTLOADER_IDENTITIES = (
(0x239A0029, "RTAG_DFU"),
)
# These adaptive loaders do not self-update. Their exact MeshCore application
# authenticates a format-3 package and uses the MBR to replace the bootloader.
RAK_AUTO_RECOVERY_IDENTITIES = (
(0x239A0029, "3401_AUTO_DFU"),
(0x239A0029, "4631_AUTO_DFU"),
)
# MeshTower V2 has both a lean internal-store application and an exact SD-store
# application. They intentionally retain the same embedded bootloader identity
# and signed wire target; the candidate capability marker selects the matching
@@ -707,7 +696,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 + RAK_AUTO_RECOVERY_IDENTITIES
) + INTERNAL_BOOTLOADER_IDENTITIES
app_ids = set(application_target_ids)
targets = {}
for identity in identities:
@@ -725,7 +714,6 @@ 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_RECOVERY_IDENTITIES or
(board_id, device_name) in (
(XIAO_BOOT_BOARD_ID_BASE, "XIAO_DFU"),
(XIAO_BOOT_BOARD_ID_SENSE, "XIAO_DFU")))
@@ -755,8 +743,6 @@ def bootloader_qualified_storage_profiles(board_id: int, device_name: str):
return (BOOT_STORAGE_INTERNAL_UPDATE, BOOT_STORAGE_SD_UPDATE)
if identity in INTERNAL_BOOTLOADER_IDENTITIES:
return (BOOT_STORAGE_INTERNAL_UPDATE,)
if identity in RAK_AUTO_RECOVERY_IDENTITIES:
return (BOOT_STORAGE_RAK_AUTO_RECOVERY,)
return None
@@ -833,7 +819,7 @@ def parse_xiao_bootloader_identity(image: bytes) -> Optional[XiaoBootloaderIdent
def bootloader_caps_storage(image: bytes) -> Optional[int]:
"""Return one exact qualified self-update or app-owned recovery marker."""
"""Return the one exact supported self-update storage marker, or None."""
found = None
valid_count = 0
for off in range(0, len(image) - 16 + 1, 4):
@@ -841,22 +827,16 @@ def bootloader_caps_storage(image: bytes) -> Optional[int]:
continue
abi, codecs = struct.unpack_from("<HH", image, off + 8)
storage = image[off + 12]
if (abi < (APP_FORMAT_VER if storage == BOOT_STORAGE_RAK_AUTO_RECOVERY
else BOOT_FORMAT_VER) or abi == 0xFFFF or
if (abi < BOOT_FORMAT_VER or abi == 0xFFFF or
codecs & BOOT_REQUIRED_APP_CODEC_MASK != BOOT_REQUIRED_APP_CODEC_MASK or
storage & ~BOOT_STORAGE_KNOWN or
(storage & BOOT_STORAGE_HEADER_W25 and
storage != BOOT_STORAGE_RAK_AUTO_RECOVERY) or
image[off + 13:off + 16] != b"\0\0\0" or
(not storage & XIAO_BOOT_STORAGE_UPDATE and
storage != BOOT_STORAGE_RAK_AUTO_RECOVERY)):
storage & ~BOOT_STORAGE_KNOWN or image[off + 13:off + 16] != b"\0\0\0" or
not storage & XIAO_BOOT_STORAGE_UPDATE):
continue
valid_count += 1
if valid_count != 1:
return None
if storage in (BOOT_STORAGE_SD_UPDATE, BOOT_STORAGE_QSPI_UPDATE,
BOOT_STORAGE_INTERNAL_UPDATE,
BOOT_STORAGE_RAK_AUTO_RECOVERY):
BOOT_STORAGE_INTERNAL_UPDATE):
found = storage
return found if valid_count == 1 else None
@@ -895,7 +875,7 @@ def validate_bootloader_image(image: bytes, target_id: Optional[int] = None,
actual_storage = bootloader_caps_storage(image)
if actual_storage not in expected_storage:
expected = "/".join(f"0x{value:02X}" for value in expected_storage)
raise ValueError(f"bootloader lacks exact qualified update/recovery capabilities {expected}")
raise ValueError(f"bootloader lacks exact ABI 3 self-update capabilities {expected}")
expected_platform = bootloader_qualified_platform_profile(
identity.board_id, identity.device_name)
if expected_platform is not None:
+4 -36
View File
@@ -1328,12 +1328,12 @@ def _xiao_bootloader_image(board_id=ml.XIAO_BOOT_BOARD_ID_BASE,
def _generic_bootloader_image(board_id=0x239A0029, device_name="3401_DFU",
storage=ml.BOOT_STORAGE_INTERNAL_UPDATE,
boot_version=0x0117010D, apply_abi=ml.BOOT_FORMAT_VER):
boot_version=0x0117010D):
import zlib
image = bytearray(b"\xff" * ml.XIAO_BOOT_IMAGE_SIZE)
struct.pack_into("<II", image, 0, 0x20040000, ml.XIAO_BOOT_IMAGE_START + 0x101)
struct.pack_into("<8sHHB3x", image, 0x80, ml.XIAO_BOOT_CAPS_MAGIC,
apply_abi, ml.BOOT_REQUIRED_APP_CODEC_MASK,
ml.BOOT_FORMAT_VER, ml.BOOT_REQUIRED_APP_CODEC_MASK,
storage)
name = device_name.encode("ascii").ljust(16, b"\0")
off = ml.BOOT_CANDIDATE_MANIFEST_OFFSET
@@ -1575,36 +1575,6 @@ def test_generic_internal_bootloader_build_parse_and_strict_contract():
pass
def test_adaptive_rak_recovery_bootloader_packages_are_exact():
priv = Ed25519PrivateKey.generate()
for board_id, name in ml.RAK_AUTO_RECOVERY_IDENTITIES:
image = _generic_bootloader_image(
board_id=board_id, device_name=name,
storage=ml.BOOT_STORAGE_RAK_AUTO_RECOVERY,
apply_abi=ml.APP_FORMAT_VER)
identity = ml.validate_bootloader_image(image)
target = ml.bootloader_target_id(board_id, name)
manifest = ml.build_manifest(
target_id=target, fw_version=identity.boot_version,
image_size=len(image), payload=image, block_size=1024,
image_hash=ml.mh32(image), codec_id=ml.CODEC_FULL,
is_full=True, sign_priv=priv, bootloader=True)
parsed = ml.parse_container(ml.build_container(manifest, image))
assert ml.verify(parsed, expect_pub=ml.ed25519_public_bytes(priv)) == []
assert parsed.manifest.target_id == target
assert ml.bootloader_caps_storage(image) == ml.BOOT_STORAGE_RAK_AUTO_RECOVERY
for wrong in (ml.BOOT_STORAGE_INTERNAL_UPDATE,
ml.BOOT_STORAGE_QSPI_UPDATE):
invalid = _generic_bootloader_image(
board_id=board_id, device_name=name, storage=wrong)
try:
ml.validate_bootloader_image(invalid)
assert False, "adaptive recovery accepted wrong loader capabilities"
except ValueError:
pass
def test_gat562_internal_bootloader_profile_is_exact():
priv = Ed25519PrivateKey.generate()
image = _generic_bootloader_image(device_name="GAT562_DFU")
@@ -1702,9 +1672,7 @@ 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) + 4 # two adaptive RAKs and two XIAOs
assert audited[0xD04AB3AB] == (0x239A0029, "3401_AUTO_DFU")
assert audited[0xFEEAFD1B] == (0x239A0029, "4631_AUTO_DFU")
assert len(audited) == len(expected) + 2 # generic internal plus two legacy XIAOs
# Generic parsing remains useful for inspecting a future candidate, but a
# signing/build call fails closed until its exact identity is qualified.
@@ -1726,7 +1694,7 @@ def test_qualified_bootloader_platform_and_storage_profiles_are_exact():
qualified = (
(ml.XIAO_BOOT_BOARD_ID_BASE, "XIAO_DFU"),
(ml.XIAO_BOOT_BOARD_ID_SENSE, "XIAO_DFU"),
) + ml.INTERNAL_BOOTLOADER_IDENTITIES + ml.RAK_AUTO_RECOVERY_IDENTITIES
) + ml.INTERNAL_BOOTLOADER_IDENTITIES
for board_id, device_name in qualified:
family, fwid, app_base, layout_abi = \
ml.bootloader_qualified_platform_profile(board_id, device_name)