mirror of
https://github.com/mikecarper/MeshCore.git
synced 2026-10-20 12:41:58 +00:00
Revert adaptive RAK app-owned bootloader recovery after ACL test
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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 &&
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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
@@ -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
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user