Merge remote-tracking branch 'origin/keymindCascade' into keymindCascade

This commit is contained in:
mikecarper committed 2026-10-23 18:08:25 -07:00
commit 170c1761d0
21 files changed
+1658 -41

No files matched your search

+2
View File
@@ -280,6 +280,8 @@ jobs:
python3 -B tools/mota/test_bootloader_migration.py -v
python3 -B tools/mota/test_bootloader_migration_runtime.py -v
python3 -B test/test_bootloader_update_cli.py -v
python3 -B test/test_bootloader_migration_role_persistence.py -v
python3 -B test/test_bootloader_migration_store.py -v
- name: Verify old-firmware compatibility in both OTA launchers
run: python3 -B -m unittest discover -s tools/lora_ota -p test_lora_ota_bootloader_version.py -v
+32 -5
View File
@@ -6,9 +6,12 @@
An opt-in Repeater or Room Server build can carry its application firmware and
the exact board's [OTAFIX 2.4.12 bootloader](https://github.com/mikecarper/Adafruit_nRF52_Bootloader_OTAFIX/releases/tag/v0.11.0-OTAFIX2.4.12)
in one application image. Install that application first, then activate the
bootloader replacement with an authenticated admin command. Booting the image
does not flash the bootloader automatically.
in one application image. After installing that application, it checks the
bootloader once at 70 seconds after startup and updates it when needed. The
check waits for idle radio and OTA activity, and skips a valid matching
bootloader at the same or newer version. Recovery and unrecognized bootloaders
are excluded from automatic installation. Ordinary firmware has no automatic
bootloader update.
This follows the internal-flash bridge in
[MeshEnvy firmware PR #2](https://github.com/MeshEnvy/meshcore-firmware/pull/2).
@@ -74,8 +77,10 @@ P1 image, not a recovery image.
updater. An internal-only legacy receiver needs a delta from its exact
installed application; this image does not give a stock bootloader its first
LoRa update capability.
2. After the new application is running, use the local CLI or authenticated
remote admin CLI:
2. Leave the new application running for at least 70 seconds. It checks once
when radio and OTA activity are idle, then waits for the usual 1.5-second
grace and persistence checks before replacement and reboot. To start sooner,
use the local CLI or authenticated remote admin CLI:
```text
ota bootloader update
@@ -92,6 +97,28 @@ P1 image, not a recovery image.
`ota bootloader embedded install HASH16` command remains available with the
confirmation shown by `status`.
Automatic installation is limited to one attempt per firmware image. A saved
40-byte record in `/bl_auto` contains the full SHA-256 of the running image,
including its version trailer. It survives reboots and power cycles. Flashing
a different firmware image permits a new check; recovery bootloaders remain
excluded. Older recovery images without a recognizable normal bootloader label
also skip automatic installation.
The record is committed before inspecting the bootloader, so an interrupted or
failed check also requires manual retry or different firmware. Missing persistent
storage, corrupt records, or an unfinished file transaction block automatic updates.
Manual `ota bootloader update` remains available under its existing identity,
version and persistence checks. The first lock write changes the filesystem
audit hash; it does not change the application or SoftDevice.
The retained reboot guard still covers the initial checks and copy. A guard
from the previous firmware does not prevent a verified different image from
qualifying again. Status reports `auto=locked for this firmware`, `auto=recovery`,
`auto=unrecognized`, `auto=storage unavailable`, or `auto=blocked` when relevant.
These guards prevent repeated update attempts; they cannot repair a bootloader
that fails before the application starts. Erasing the filesystem also erases
the saved firmware lock.
The bridge validates the live application, candidate hash, installed identity,
chip/layout, MBR addresses, watchdog and flash protection before copying. A
protected region is refused; the bridge does not unlock it. Existing staged
+6
View File
@@ -1323,6 +1323,12 @@ public:
bool handleHostCliSerialReply(const char* command, char* reply);
#endif
void loop();
#if defined(OTA_BOOTLOADER_MIGRATION)
bool prepareForOtaReboot() override {
// Let the normal ACL save finish, including its failure backoff.
return dirty_contacts_expiry == 0;
}
#endif
bool canRecoverUsbLogging() const {
return dirty_contacts_expiry == 0 && !hasOutbound() && !isAnyTempRadioActive()
&& !hasPendingOtaApply() && !saved_radio_apply_pending
+6
View File
@@ -19,6 +19,9 @@
#endif
#include "MyMesh.h"
#if defined(OTA_BOOTLOADER_MIGRATION)
#include <helpers/ota/BootloaderMigration.h>
#endif
#if defined(MESH_SOAK_DIAGNOSTICS)
#include "../../tools/hil/S3SoakDiagnostics.h"
#endif
@@ -377,6 +380,9 @@ void setup() {
mesh::hilStartupTrace("mesh_begin");
the_mesh.begin(fs);
mesh::hilStartupTrace("mesh_ready");
#if defined(OTA_BOOTLOADER_MIGRATION)
mesh::ota::bootloader_migration_begin(volatile_primary_fs ? nullptr : fs);
#endif
mesh::hilStartupTrace("watchdog_begin");
#if defined(NRF52_PLATFORM)
+6
View File
@@ -654,6 +654,12 @@ public:
return mesh::wireless::WiFi;
}
void loop();
#if defined(OTA_BOOTLOADER_MIGRATION)
bool prepareForOtaReboot() override {
// Let the normal ACL save finish, including its failure backoff.
return dirty_contacts_expiry == 0;
}
#endif
bool canRecoverUsbLogging() const {
return dirty_contacts_expiry == 0 && !hasOutbound() && !isAnyTempRadioActive()
&& !hasPendingOtaApply() && !saved_radio_apply_pending
+6
View File
@@ -18,6 +18,9 @@
#endif
#include "MyMesh.h"
#if defined(OTA_BOOTLOADER_MIGRATION)
#include <helpers/ota/BootloaderMigration.h>
#endif
#if defined(ESP32_PLATFORM)
#include <helpers/ESP32TrueRandom.h>
@@ -253,6 +256,9 @@ void setup() {
sensors.begin();
the_mesh.begin(fs);
#if defined(OTA_BOOTLOADER_MIGRATION)
mesh::ota::bootloader_migration_begin(volatile_primary_fs ? nullptr : fs);
#endif
#if defined(NRF52_PLATFORM)
mesh::loadUsbLoggingWatchdog(fs, !volatile_primary_fs,
@@ -52,6 +52,7 @@ def qualify(directory, manifest_path):
"legacy_sha256": metadata.get("legacy_sha256"),
"app_offset": offset, "image_bytes": BOOT_SIZE,
"activation": "ota bootloader update",
"automatic_delay_seconds": 70,
}
manifest_path.write_text(json.dumps(manifest, indent=2, sort_keys=True) + "\n")
return manifest["embedded_bootloader"]
+3 -2
View File
@@ -481,13 +481,14 @@ void __attribute__((noinline)) Mesh::serviceLoopMaintenance() {
}
#if defined(ENABLE_OTA)
#if defined(OTA_BOOTLOADER_MIGRATION)
if (ota::bootloader_migration_pending()) {
if (ota::bootloader_migration_service_due()) {
// This bridge owns no OTA workspace. Service it even after a dynamic
// context has been released, and never allocate one just to poll it.
const ota::OtaContext* context = ota::ota_context_if_active();
const bool ota_idle = !context || context->embeddedBootloaderIdle();
const bool outbound_idle = !hasPendingRadioWork();
const bool ready = outbound_idle && ota_idle && prepareForOtaReboot();
const bool ready = outbound_idle && ota_idle &&
(!ota::bootloader_migration_pending() || prepareForOtaReboot());
ota::bootloader_migration_tick(ready, ota_idle);
}
#endif
+156 -9
View File
@@ -24,10 +24,20 @@ static_assert(migration::ACL_READ_DENIED == ACL_ACL_PERM_READ_Msk, "ACL read per
static_assert(migration::ACL_WRITE_DENIED == ACL_ACL_PERM_WRITE_Msk, "ACL write permission bit");
namespace {
bool pending = false;
bool automatic_attempted = false;
bool automatic_blocked = false;
constexpr uint32_t AUTOMATIC_DELAY_MS = 70000u;
constexpr uint8_t ATTEMPT_MARKER = 0xD0;
uint32_t armed_at = 0;
uint8_t last_result = 0;
bool result_is_current = false;
bool already_installed = false;
FILESYSTEM *state_fs = nullptr;
uint8_t firmware_hash[32] = {};
MigrationAutoLock automatic_lock = MigrationAutoLock::Unavailable;
bool firmware_identified = false;
migration::InstalledVariant automatic_variant = migration::InstalledVariant::Unknown;
bool loader_excluded = false;
// The pinned framework's historical SVC wrapper has no memory clobber or IPA
// barrier. The MBR reads this pointed-to command after exception entry, which
@@ -77,19 +87,67 @@ bool installed_up_to_date() {
migration_softdevice_fwid);
}
uint8_t retained_result() {
if (result_is_current) return last_result;
bool installed_allows_automatic() {
const uint8_t *boot = reinterpret_cast<const uint8_t *>(migration::BOOT_BASE);
OtaBootloaderIdentity installed;
const bool valid = ota_bootloader_identity_from_image(boot, migration::BOOT_SIZE, installed);
automatic_variant = migration::installed_variant(boot, migration::BOOT_SIZE,
valid ? installed.boot_version : 0);
if (automatic_variant == migration::InstalledVariant::Recovery) return false;
if (valid && automatic_variant == migration::InstalledVariant::Normal &&
migration::installed_matches_resource(installed, migration_board_id, migration_device_name,
mota_nrf52_app_base(), migration_softdevice_fwid))
return true;
// Only the explicitly pinned legacy bridge may omit modern identity/text.
if (!migration_has_legacy) return false;
uint8_t digest[32];
hash_region(migration::BOOT_BASE, migration::BOOT_SIZE, digest);
return memcmp(digest, migration_legacy_sha256, sizeof(digest)) == 0;
}
bool lock_automatic_for_firmware() {
if (!firmware_identified || !bootloader_migration_lock_auto(state_fs, firmware_hash)) {
automatic_lock = MigrationAutoLock::Unavailable;
return false;
}
automatic_lock = MigrationAutoLock::Locked;
return true;
}
bool read_reboot_marker(uint32_t &result) {
uint8_t enabled = 0;
uint32_t result = 0;
if (sd_softdevice_is_enabled(&enabled) != NRF_SUCCESS) return 0;
if (sd_softdevice_is_enabled(&enabled) != NRF_SUCCESS) return false;
if (enabled) {
if (sd_power_gpregret_get(0, &result) != NRF_SUCCESS) return 0;
if (sd_power_gpregret_get(0, &result) != NRF_SUCCESS) return false;
} else {
result = NRF_POWER->GPREGRET;
}
return true;
}
uint8_t retained_result() {
if (result_is_current) return last_result;
uint32_t result = 0;
if (!read_reboot_marker(result)) return 0;
return result >= 0xC0 && result <= 0xCF ? result & 15 : last_result;
}
bool write_reboot_marker(uint8_t marker) {
uint8_t enabled = 0;
if (sd_softdevice_is_enabled(&enabled) != NRF_SUCCESS) return false;
if (enabled) {
uint32_t current = 0;
if (sd_power_gpregret_get(0, &current) != NRF_SUCCESS) return false;
// Arming after the automatic image check must preserve its existing
// guard, rather than creating an unguarded gap between clear and set.
if (current == marker) return true;
if (sd_power_gpregret_clr(0, 0xFF) != NRF_SUCCESS) return false;
return marker == 0 || sd_power_gpregret_set(0, marker) == NRF_SUCCESS;
}
NRF_POWER->GPREGRET = marker;
return NRF_POWER->GPREGRET == marker;
}
// Called only after the operator reply has drained and SoftDevice is stopped.
// GPREGRET (not GPREGRET2) survives this legacy loader's diagnostic writes.
void fail_and_reset(uint8_t result) {
@@ -174,7 +232,10 @@ void install() {
if (NRF_WDT->RREN & (1u << i)) NRF_WDT->RR[i] = 0x6E524635u;
}
}
NRF_POWER->GPREGRET = 0;
// The installed loader preserves our non-DFU marker. If COPY_BL or its
// preflight is interrupted, the next application boot will stay available
// instead of automatically trying the same operation again.
NRF_POWER->GPREGRET = ATTEMPT_MARKER;
NRF_POWER->GPREGRET2 = 0;
sd_mbr_command_t command = {};
command.command = SD_MBR_COMMAND_COPY_BL;
@@ -187,16 +248,40 @@ void install() {
}
} // namespace
void bootloader_migration_begin(FILESYSTEM *fs) {
state_fs = fs;
firmware_identified = false;
automatic_lock = MigrationAutoLock::Unavailable;
SelfFwInfo app;
if (fs && ota_self_firmware(app) && app.valid) {
hash_region(mota_nrf52_app_base(), app.image_len, firmware_hash);
firmware_identified = true;
automatic_lock = bootloader_migration_auto_lock(fs, firmware_hash);
}
automatic_attempted = automatic_lock == MigrationAutoLock::Locked ||
automatic_lock == MigrationAutoLock::Unavailable;
automatic_blocked = automatic_attempted;
}
bool bootloader_migration_pending() {
return pending;
}
bool bootloader_migration_service_due() {
return pending || (!automatic_attempted && millis() >= AUTOMATIC_DELAY_MS);
}
void bootloader_migration_status(char *reply, size_t capacity) {
char hash[17];
hex8(migration_candidate_sha256, hash);
snprintf(reply, capacity, "BL embedded 2.4.12: 40960 B; hash=%s; %s; last=%u", hash,
snprintf(reply, capacity, "BL embedded 2.4.12: 40960 B; hash=%s; %s; last=%u; auto=%s", hash,
pending ? "armed" : already_installed ? "already installed" :
installed_up_to_date() ? "up to date" : "idle", retained_result());
installed_up_to_date() ? "up to date" : "idle", retained_result(),
loader_excluded ? automatic_variant == migration::InstalledVariant::Recovery ?
"recovery" : "unrecognized" :
automatic_lock == MigrationAutoLock::Locked ? "locked for this firmware" :
automatic_lock == MigrationAutoLock::Unavailable ? "storage unavailable" :
automatic_blocked ? "blocked" : automatic_attempted ? "checked" : "70s after boot");
}
bool bootloader_migration_request(const char *confirmation, char *reply, size_t capacity) {
@@ -206,6 +291,10 @@ bool bootloader_migration_request(const char *confirmation, char *reply, size_t
snprintf(reply, capacity, "ERR confirm exact image: ota bootloader embedded install %s", hash);
return false;
}
// A manual attempt also satisfies the startup check, including a cancelled
// or failed attempt. Only another explicit command may retry it this boot.
automatic_attempted = true;
automatic_blocked = false;
if (pending) {
snprintf(reply, capacity, "OK bootloader update already armed");
return true;
@@ -229,6 +318,18 @@ bool bootloader_migration_request(const char *confirmation, char *reply, size_t
return true;
}
already_installed = false;
if (!lock_automatic_for_firmware()) {
last_result = 18;
result_is_current = true;
snprintf(reply, capacity, "ERR cannot save bootloader automatic-update lock");
return false;
}
if (!write_reboot_marker(ATTEMPT_MARKER)) {
last_result = 17;
result_is_current = true;
snprintf(reply, capacity, "ERR bootloader reboot guard unavailable");
return false;
}
pending = true;
armed_at = millis();
last_result = 0;
@@ -258,7 +359,53 @@ void bootloader_migration_audit(char *reply, size_t capacity) {
}
void bootloader_migration_tick(bool outbound_idle, bool ota_idle) {
if (!pending) return;
if (!pending) {
if (!bootloader_migration_service_due() || !outbound_idle || !ota_idle) return;
// A failed MBR/preflight reset leaves this marker. Do not create a loop
// that retries and resets every 70 seconds; the CLI can retry explicitly.
uint32_t marker = 0;
if (!read_reboot_marker(marker) ||
(automatic_lock != MigrationAutoLock::PreviousImage &&
(marker == ATTEMPT_MARKER || (marker > 0xC0 && marker <= 0xCF)))) {
automatic_attempted = true;
automatic_blocked = true;
return;
}
automatic_attempted = true;
// Mark before reading either image too, so a reset in the version/hash
// check cannot turn into a recurring 70-second reboot loop.
if (!write_reboot_marker(ATTEMPT_MARKER)) {
last_result = 17;
result_is_current = true;
automatic_blocked = true;
return;
}
// Save the current image before inspecting the bootloader. A new firmware
// may ignore its predecessor's retained marker, so GPREGRET alone cannot
// protect a reset during that first check.
if (!lock_automatic_for_firmware()) {
last_result = 18;
result_is_current = true;
automatic_blocked = true;
return;
}
if (!installed_allows_automatic()) {
loader_excluded = true;
automatic_blocked = true;
if (!write_reboot_marker(0)) {
last_result = 17;
result_is_current = true;
}
return;
}
char reply[160];
bootloader_migration_request(nullptr, reply, sizeof(reply));
if (!pending && last_result == 0 && !write_reboot_marker(0)) {
last_result = 17;
result_is_current = true;
}
return;
}
const uint32_t elapsed = millis() - armed_at;
// An incoming advert may have started autofetch since the admin command.
// Never disable SoftDevice while another OTA session owns the flash store.
+3
View File
@@ -2,10 +2,13 @@
#if defined(OTA_BOOTLOADER_MIGRATION)
#include <stddef.h>
#include "BootloaderMigrationStore.h"
namespace mesh {
namespace ota {
void bootloader_migration_begin(FILESYSTEM *fs);
bool bootloader_migration_pending();
bool bootloader_migration_service_due();
// nullptr selects the pinned embedded image; a supplied hash must match exactly.
bool bootloader_migration_request(const char *confirmation, char *reply, size_t capacity);
void bootloader_migration_status(char *reply, size_t capacity);
+60 -4
View File
@@ -2,6 +2,7 @@
#include <stddef.h>
#include <stdint.h>
#include <stdio.h>
#include "OtaBootloaderUpdate.h"
namespace mesh {
@@ -16,6 +17,55 @@ static const uint32_t SETTINGS_BASE = 0xFF000u;
static const uint32_t ACL_READ_DENIED = 1u << 2;
static const uint32_t ACL_WRITE_DENIED = 1u << 1;
enum class InstalledVariant { Unknown, Normal, Recovery };
inline bool boot_text_matches(const uint8_t *text, size_t available,
const char *expected, size_t length) {
return available >= length && memcmp(text, expected, length) == 0;
}
// Recovery builds deliberately retain the normal board identity and BLM2
// flags. Their on-device version text is the only published discriminator.
// Older recovery images can omit that text entirely, so automatic installation
// also requires one complete, recognized normal INFO_UF2 version record.
// The caller must separately verify the image CRC, identity and platform.
inline InstalledVariant installed_variant(const uint8_t *image, size_t size,
uint32_t version) {
if (!image || size != BOOT_SIZE || !ota_bootloader_version_valid(version))
return InstalledVariant::Unknown;
static const char header[] = "UF2 Bootloader ";
static const char recovery[] = "R_0x";
static const char old_recovery[] = "-recovery-allow-all";
static const char recovery_header[] = "UF2 Bootloader R_";
for (size_t i = 0; i < size; ++i) {
const size_t left = size - i;
if (boot_text_matches(image + i, left, recovery, sizeof(recovery) - 1) ||
boot_text_matches(image + i, left, old_recovery, sizeof(old_recovery) - 1) ||
boot_text_matches(image + i, left, recovery_header, sizeof(recovery_header) - 1))
return InstalledVariant::Recovery;
}
char normal[48], packed[16];
const unsigned preview = version & 0xFFu;
snprintf(normal, sizeof(normal),
preview == 0xFFu ? "0.11.0-OTAFIX%u.%u.%u\r\n" :
"0.11.0-OTAFIX%u.%u.%u-preview.%u\r\n",
unsigned(version >> 24), unsigned((version >> 16) & 0xFFu),
unsigned((version >> 8) & 0xFFu), preview);
snprintf(packed, sizeof(packed), "BL_0x%08lX\r\n", (unsigned long)version);
unsigned records = 0;
for (size_t i = 0; i + sizeof(header) - 1 < size; ++i) {
if (!boot_text_matches(image + i, size - i, header, sizeof(header) - 1)) continue;
const uint8_t *label = image + i + sizeof(header) - 1;
size_t left = size - i - (sizeof(header) - 1);
bool matches = boot_text_matches(label, left, packed, strlen(packed));
if (*label == 'v') { ++label; --left; }
if (!matches && !boot_text_matches(label, left, normal, strlen(normal)))
return InstalledVariant::Unknown;
if (++records > 1) return InstalledVariant::Unknown;
}
return records == 1 ? InstalledVariant::Normal : InstalledVariant::Unknown;
}
struct AclRegion {
uint32_t address;
uint32_t size;
@@ -101,13 +151,19 @@ inline bool installed_can_migrate(const OtaBootloaderIdentity &installed,
140, fwid, app_base, 1) == OTA_BOOT_CONTINUITY_OK;
}
inline bool installed_up_to_date(const OtaBootloaderIdentity &installed,
uint32_t board_id, const char name[16],
uint32_t version, uint32_t app_base, uint16_t fwid) {
inline bool installed_matches_resource(const OtaBootloaderIdentity &installed,
uint32_t board_id, const char name[16],
uint32_t app_base, uint16_t fwid) {
return installed.present && installed.crc_ok &&
installed.image_start == BOOT_BASE && installed.image_size == BOOT_SIZE &&
installed.board_id == board_id && memcmp(installed.device_name, name, 16) == 0 &&
ota_bootloader_continuity_matches_runtime(installed, 140, fwid, app_base, 1) &&
ota_bootloader_continuity_matches_runtime(installed, 140, fwid, app_base, 1);
}
inline bool installed_up_to_date(const OtaBootloaderIdentity &installed,
uint32_t board_id, const char name[16],
uint32_t version, uint32_t app_base, uint16_t fwid) {
return installed_matches_resource(installed, board_id, name, app_base, fwid) &&
installed.boot_version >= version;
}
@@ -0,0 +1,57 @@
#if defined(OTA_BOOTLOADER_MIGRATION)
#include "BootloaderMigrationStore.h"
#include <helpers/AtomicFileWriter.h>
#include <helpers/FilePresence.h>
#include <helpers/PersistentStoreFormat.h>
namespace mesh { namespace ota {
namespace {
constexpr size_t RecordSize = 40;
const char* const Path = "/bl_auto";
const char* const Temp = "/bl_auto.tmp";
const uint8_t Magic[4] = {'B', 'A', 'L', 1};
bool readRecord(FILESYSTEM* fs, uint8_t record[RecordSize]) {
File file(*fs);
if (!file.open(Path, FILE_O_READ)) return false;
const bool complete = !file.isDirectory() && file.size() == RecordSize
&& file.read(record, RecordSize) == static_cast<int>(RecordSize);
file.close();
return complete
&& memcmp(record, Magic, sizeof(Magic)) == 0
&& storage::readLE32(record + RecordSize - 4)
== storage::updateCRC32(0xffffffffU, record, RecordSize - 4);
}
}
MigrationAutoLock bootloader_migration_auto_lock(
FILESYSTEM* fs, const uint8_t image_hash[32]) {
bool present = false;
if (!fs || !image_hash || !filePresence(fs, Temp, present) || present)
return MigrationAutoLock::Unavailable;
if (!filePresence(fs, Path, present)) return MigrationAutoLock::Unavailable;
if (!present) return MigrationAutoLock::Eligible;
uint8_t record[RecordSize];
if (!readRecord(fs, record)) return MigrationAutoLock::Unavailable;
return memcmp(record + sizeof(Magic), image_hash, 32) == 0
? MigrationAutoLock::Locked : MigrationAutoLock::PreviousImage;
}
bool bootloader_migration_lock_auto(FILESYSTEM* fs, const uint8_t image_hash[32]) {
const auto state = bootloader_migration_auto_lock(fs, image_hash);
if (state == MigrationAutoLock::Locked) return true;
if (state == MigrationAutoLock::Unavailable) return false;
uint8_t record[RecordSize], verify[RecordSize];
memcpy(record, Magic, sizeof(Magic));
memcpy(record + sizeof(Magic), image_hash, 32);
storage::writeLE32(record + RecordSize - 4,
storage::updateCRC32(0xffffffffU, record, RecordSize - 4));
AtomicFileWriter writer(fs, Path);
return writer && writer.write(record, sizeof(record)) == sizeof(record)
&& writer.commit() && readRecord(fs, verify)
&& memcmp(record, verify, sizeof(record)) == 0;
}
} } // namespace mesh::ota
#endif
@@ -0,0 +1,20 @@
#pragma once
#if defined(OTA_BOOTLOADER_MIGRATION)
#include <helpers/IdentityStore.h>
namespace mesh { namespace ota {
enum class MigrationAutoLock : uint8_t { Eligible, PreviousImage, Locked, Unavailable };
// image_hash is SHA-256 of the complete, verified running application. Pass
// nullptr as the filesystem when the role is using volatile fallback storage.
MigrationAutoLock bootloader_migration_auto_lock(
FILESYSTEM* fs, const uint8_t image_hash[32]);
// Record an attempt before inspecting the bootloader. A matching durable record is
// already sufficient; a different image may atomically replace its predecessor.
bool bootloader_migration_lock_auto(FILESYSTEM* fs, const uint8_t image_hash[32]);
} } // namespace mesh::ota
#endif
@@ -0,0 +1,211 @@
"""Exercise embedded migration gates against the roles' normal ACL saves."""
import os
from pathlib import Path
import re
import shutil
import subprocess
import tempfile
import unittest
from cpp_source import body
ROOT = Path(__file__).resolve().parents[1]
SOURCE = r'''
#include <cassert>
#include <cstdint>
#include <type_traits>
#include "helpers/LazyPersistence.h"
#define LAZY_CONTACTS_WRITE_DELAY 5000
#define MESH_DEBUG_PRINTLN(...) ((void)0)
static uint32_t now = 70000;
static bool millisHasNowPassed(unsigned long deadline) {
return int32_t(now - uint32_t(deadline)) > 0;
}
static unsigned long futureMillis(uint32_t delay) { return uint32_t(now + delay); }
struct Base { virtual bool prepareForOtaReboot() { return true; } };
struct Role : Base {
using MyMesh = Role;
unsigned long dirty_contacts_expiry = 75000;
uint8_t contacts_save_failures = 0;
static bool saveFilter(int*) { return true; }
void* _fs = nullptr;
struct Acl {
bool succeeds = false;
unsigned saves = 0;
bool save(void*, bool (*)(int*) = nullptr) { saves++; return succeeds; }
} acl;
@GATE@
void servicePersistence() {
@SAVE@
}
};
int main() {
Role role;
#ifdef OTA_BOOTLOADER_MIGRATION
static_assert(std::is_same<decltype(&Role::prepareForOtaReboot), bool (Role::*)()>::value,
"embedded role must override the reboot gate");
assert(!role.prepareForOtaReboot());
role.servicePersistence();
assert(role.acl.saves == 0 && !role.prepareForOtaReboot());
now = 75001;
role.servicePersistence();
assert(role.acl.saves == 1 && role.contacts_save_failures == 1);
assert(!role.prepareForOtaReboot());
// Repeated migration polls must not bypass the failed save's backoff.
now = 80000;
for (unsigned poll = 0; poll < 100; poll++) {
assert(!role.prepareForOtaReboot());
role.servicePersistence();
}
assert(role.acl.saves == 1);
role.acl.succeeds = true;
now = 80002;
role.servicePersistence();
assert(role.acl.saves == 2 && role.contacts_save_failures == 0);
assert(role.dirty_contacts_expiry == 0 && role.prepareForOtaReboot());
#else
// Ordinary firmware retains the base implementation without an override.
assert(role.prepareForOtaReboot());
static_assert(std::is_same<decltype(&Role::prepareForOtaReboot), bool (Base::*)()>::value,
"ordinary role must retain its base reboot gate");
#endif
}
'''
MESH_SOURCE = r'''
#include <cassert>
namespace ota {
static bool due = false, pending = false;
static unsigned context_polls = 0, ticks = 0;
static bool last_ready = false, last_idle = false;
struct OtaContext {
bool idle = true;
bool embeddedBootloaderIdle() const { return idle; }
};
static OtaContext* active = nullptr;
static bool bootloader_migration_service_due() { return due; }
static bool bootloader_migration_pending() { return pending; }
static const OtaContext* ota_context_if_active() { context_polls++; return active; }
static void bootloader_migration_tick(bool ready, bool idle) {
ticks++; last_ready = ready; last_idle = idle;
}
}
struct Mesh {
bool radio_busy = false, persistence_ready = false;
unsigned persistence_checks = 0;
bool hasPendingRadioWork() const { return radio_busy; }
bool prepareForOtaReboot() { persistence_checks++; return persistence_ready; }
void service() { @SERVICE@ }
};
int main() {
Mesh mesh;
mesh.service();
assert(ota::ticks == 0 && ota::context_polls == 0);
ota::due = true;
mesh.service();
// Startup checks work with a released workspace and no persistence flush.
assert(ota::ticks == 1 && ota::context_polls == 1);
assert(ota::last_ready && ota::last_idle && mesh.persistence_checks == 0);
ota::pending = true;
mesh.service();
assert(!ota::last_ready && ota::last_idle && mesh.persistence_checks == 1);
mesh.persistence_ready = true;
mesh.service();
assert(ota::last_ready && ota::last_idle && mesh.persistence_checks == 2);
mesh.radio_busy = true;
mesh.service();
assert(!ota::last_ready && ota::last_idle && mesh.persistence_checks == 2);
mesh.radio_busy = false;
ota::OtaContext context;
context.idle = false;
ota::active = &context;
mesh.service();
assert(!ota::last_ready && !ota::last_idle && mesh.persistence_checks == 2);
ota::pending = false;
context.idle = true;
mesh.service();
assert(ota::last_ready && ota::last_idle && mesh.persistence_checks == 2);
}
'''
class BootloaderMigrationRolePersistenceTest(unittest.TestCase):
def test_both_roles_initialize_the_lock_after_mesh_with_durable_storage_only(self):
for role in ("simple_repeater", "simple_room_server"):
with self.subTest(role=role):
source = (ROOT / "examples" / role / "main.cpp").read_text(encoding="utf-8")
setup = body(source, "void setup()")
startup = re.search(
r"#if defined\(OTA_BOOTLOADER_MIGRATION\)\n"
r"\s*mesh::ota::bootloader_migration_begin\("
r"volatile_primary_fs \? nullptr : fs\);\n#endif", setup)
self.assertIsNotNone(startup)
self.assertEqual(setup.count("bootloader_migration_begin("), 1)
self.assertLess(setup.index("the_mesh.begin(fs);"), startup.start())
self.assertLess(setup.index("internalPrimaryFilesystemReady("), startup.start())
self.assertLess(setup.index("volatile_primary_fs = true;"), startup.start())
def test_mesh_services_startup_checks_without_allocating_an_ota_workspace(self):
source = (ROOT / "src/Mesh.cpp").read_text(encoding="utf-8")
maintenance = body(source, "void __attribute__((noinline)) Mesh::serviceLoopMaintenance()")
service = body(maintenance, "if (ota::bootloader_migration_service_due())")
dynamic_context = maintenance.index("#if OTA_DYNAMIC_CONTEXT")
self.assertLess(maintenance.index(service) + len(service), dynamic_context)
released = body(maintenance[dynamic_context:], "if (!ota::ota_context_if_active())")
self.assertIn("return;", released)
# Allocating ota_ctx() merely to poll would defeat dynamic-context RAM savings.
self.assertNotRegex(service, r"\bota_ctx\s*\(")
compiler = shutil.which("g++") or shutil.which("clang++")
if compiler is None:
self.skipTest("a host C++17 compiler is required")
sanitize = [] if os.name == "nt" else [
"-fsanitize=address,undefined", "-fno-sanitize-recover=all", "-fno-pie", "-no-pie"]
with tempfile.TemporaryDirectory(prefix="migration-mesh-service-") as directory:
unit = Path(directory) / "service.cpp"
binary = Path(directory) / ("service.exe" if os.name == "nt" else "service")
unit.write_text(MESH_SOURCE.replace("@SERVICE@", service), encoding="ascii")
built = subprocess.run([
compiler, "-std=c++17", "-O1", "-g", "-Wall", "-Wextra", "-Werror",
*sanitize, str(unit), "-o", str(binary),
], capture_output=True, text=True, timeout=60)
self.assertEqual(built.returncode, 0, built.stdout + built.stderr)
tested = subprocess.run([str(binary)], capture_output=True, text=True, timeout=30)
self.assertEqual(tested.returncode, 0, tested.stdout + tested.stderr)
def test_repeater_and_room_wait_for_the_normal_acl_save(self):
compiler = shutil.which("g++") or shutil.which("clang++")
if compiler is None:
self.skipTest("a host C++17 compiler is required")
sanitize = [] if os.name == "nt" else [
"-fsanitize=address,undefined", "-fno-sanitize-recover=all", "-fno-pie", "-no-pie"]
with tempfile.TemporaryDirectory(prefix="migration-role-persistence-") as directory:
for role in ("simple_repeater", "simple_room_server"):
header = (ROOT / "examples" / role / "MyMesh.h").read_text(encoding="utf-8")
source = (ROOT / "examples" / role / "MyMesh.cpp").read_text(encoding="utf-8")
gate = re.search(
r"#if defined\(OTA_BOOTLOADER_MIGRATION\)\n"
r"\s*bool prepareForOtaReboot\(\) override \{.*?\n\s*\}\n#endif",
header, re.DOTALL)
self.assertIsNotNone(gate, role)
save = body(source, "if (dirty_contacts_expiry && millisHasNowPassed(dirty_contacts_expiry))")
generated = SOURCE.replace("@GATE@", gate.group()).replace("@SAVE@", save)
for embedded in (False, True):
with self.subTest(role=role, embedded=embedded):
unit = Path(directory) / (role + ".cpp")
binary = Path(directory) / (role + (".exe" if os.name == "nt" else ""))
unit.write_text(generated, encoding="ascii")
built = subprocess.run([
compiler, "-std=c++17", "-O1", "-g", "-Wall", "-Wextra", "-Werror",
"-Wno-unused-function", *sanitize,
*(["-DOTA_BOOTLOADER_MIGRATION=1"] if embedded else []),
"-I", str(ROOT / "src"), str(unit), "-o", str(binary),
], capture_output=True, text=True, timeout=60)
self.assertEqual(built.returncode, 0, built.stdout + built.stderr)
tested = subprocess.run([str(binary)], capture_output=True, text=True, timeout=30)
self.assertEqual(tested.returncode, 0, tested.stdout + tested.stderr)
if __name__ == "__main__":
unittest.main()
+343
View File
@@ -0,0 +1,343 @@
"""Exercise the production per-image bootloader lock with filesystem faults."""
import os
from pathlib import Path
import shutil
import subprocess
import tempfile
import unittest
ROOT = Path(__file__).resolve().parents[1]
FILESYSTEM = r'''
#pragma once
#include <algorithm>
#include <cstdint>
#include <cstring>
#include <limits>
#include <map>
#include <set>
#include <string>
#include <vector>
struct lfs_info {};
static const int LFS_ERR_NOENT = -2;
static const uint8_t FILE_O_READ = 0, FILE_O_WRITE = 1;
class FakeFilesystem;
class File {
FakeFilesystem* fs;
std::string path;
size_t position = 0;
bool opened = false, writing = false;
public:
explicit File(FakeFilesystem& filesystem) : fs(&filesystem) {}
bool open(const char*, uint8_t);
size_t write(const uint8_t*, size_t);
int read(void*, uint16_t);
uint32_t size() const;
bool isDirectory() const;
void flush() {}
void close() { opened = false; }
explicit operator bool() const { return opened; }
};
class FakeFilesystem {
public:
std::map<std::string, std::vector<uint8_t>> files;
std::set<std::string> directories;
std::string stat_error;
bool fail_read = false, fail_write_open = false, fail_rename = false;
bool false_success_rename = false, error_after_publish = false;
bool corrupt_after_publish = false, fail_reads_after_publish = false;
size_t max_write = std::numeric_limits<size_t>::max();
size_t max_read = std::numeric_limits<size_t>::max();
unsigned writes = 0, renames = 0;
bool exists(const char* path) const {
return files.count(path) || directories.count(path);
}
bool remove(const char* path) { return files.erase(path) != 0; }
bool rename(const char* from, const char* to) {
++renames;
if (fail_rename) return false;
if (false_success_rename) return true;
auto found = files.find(from);
if (found == files.end() || directories.count(to)) return false;
files[to] = found->second;
files.erase(found);
if (corrupt_after_publish && !files[to].empty()) files[to][0] ^= 1;
if (fail_reads_after_publish) fail_read = true;
return !error_after_publish;
}
void _lockFS() {}
void _unlockFS() {}
FakeFilesystem* _getFS() { return this; }
};
inline int lfs_stat(FakeFilesystem* fs, const char* path, lfs_info*) {
if (fs->stat_error == path) return -5;
return fs->exists(path) ? 0 : LFS_ERR_NOENT;
}
inline bool File::open(const char* filename, uint8_t mode) {
path = filename;
writing = mode == FILE_O_WRITE;
if (fs->directories.count(path)) return false;
if (writing) {
if (fs->fail_write_open) return false;
position = fs->files[path].size();
} else {
if (fs->fail_read || !fs->files.count(path)) return false;
position = 0;
}
opened = true;
return true;
}
inline size_t File::write(const uint8_t* data, size_t size) {
if (!opened || !writing) return 0;
++fs->writes;
const size_t count = std::min(size, fs->max_write);
auto& contents = fs->files[path];
contents.insert(contents.end(), data, data + count);
position += count;
return count;
}
inline int File::read(void* data, uint16_t size) {
if (!opened || writing || fs->fail_read) return -1;
const auto& contents = fs->files[path];
const size_t count = std::min<size_t>(
std::min<size_t>(size, fs->max_read),
contents.size() - std::min(position, contents.size()));
if (count) memcpy(data, contents.data() + position, count);
position += count;
return static_cast<int>(count);
}
inline uint32_t File::size() const {
auto found = fs->files.find(path);
return found == fs->files.end() ? 0 : found->second.size();
}
inline bool File::isDirectory() const { return fs->directories.count(path); }
#define FILESYSTEM FakeFilesystem
'''
SOURCE = r'''
#include <cassert>
#include <helpers/IdentityStore.h>
#define ATOMIC_FILE_WRITER_IMPLEMENTATION
#include <helpers/AtomicFileWriter.h>
#include <helpers/ota/BootloaderMigrationStore.h>
#include <helpers/PersistentStoreFormat.h>
using namespace mesh::ota;
static const char* Path = "/bl_auto";
static const char* Temp = "/bl_auto.tmp";
int main(int argc, char** argv) {
assert(argc == 2);
const std::string scenario = argv[1];
uint8_t a[32], b[32];
memset(a, 0x31, sizeof(a));
memcpy(b, a, sizeof(b));
b[31] ^= 1; // Deliberately preserve the 8-byte EndF hash prefix.
FakeFilesystem fs;
if (scenario == "fresh") {
assert(bootloader_migration_auto_lock(&fs, a) == MigrationAutoLock::Eligible);
assert(bootloader_migration_auto_lock(nullptr, a) == MigrationAutoLock::Unavailable);
assert(bootloader_migration_auto_lock(&fs, nullptr) == MigrationAutoLock::Unavailable);
assert(!bootloader_migration_lock_auto(nullptr, a));
assert(!bootloader_migration_lock_auto(&fs, nullptr));
} else if (scenario == "persist") {
assert(bootloader_migration_lock_auto(&fs, a));
assert(fs.files.at(Path).size() == 40 && !fs.exists(Temp));
FakeFilesystem rebooted;
rebooted.files = fs.files;
assert(bootloader_migration_auto_lock(&rebooted, a) == MigrationAutoLock::Locked);
assert(bootloader_migration_auto_lock(&rebooted, b) == MigrationAutoLock::PreviousImage);
assert(bootloader_migration_lock_auto(&rebooted, b));
assert(bootloader_migration_auto_lock(&rebooted, b) == MigrationAutoLock::Locked);
assert(bootloader_migration_auto_lock(&rebooted, a) == MigrationAutoLock::PreviousImage);
} else if (scenario == "locked") {
assert(bootloader_migration_lock_auto(&fs, a));
const auto original = fs.files;
const unsigned writes = fs.writes, renames = fs.renames;
fs.max_write = 0;
fs.fail_rename = true;
assert(bootloader_migration_lock_auto(&fs, a));
assert(fs.files == original && fs.writes == writes && fs.renames == renames);
} else if (scenario == "malformed") {
assert(bootloader_migration_lock_auto(&fs, a));
const auto good = fs.files.at(Path);
for (unsigned kind = 0; kind < 4; ++kind) {
fs.files[Path] = good;
if (kind == 0) fs.files[Path].clear();
if (kind == 1) fs.files[Path].push_back(0);
if (kind == 2) {
fs.files[Path][0] ^= 1;
mesh::storage::writeLE32(fs.files[Path].data() + 36,
mesh::storage::updateCRC32(0xffffffffU, fs.files[Path].data(), 36));
}
if (kind == 3) fs.files[Path][39] ^= 1;
const auto before = fs.files;
assert(bootloader_migration_auto_lock(&fs, a) == MigrationAutoLock::Unavailable);
assert(!bootloader_migration_lock_auto(&fs, b));
assert(fs.files == before);
}
} else if (scenario == "directory") {
fs.directories.insert(Path);
assert(bootloader_migration_auto_lock(&fs, a) == MigrationAutoLock::Unavailable);
assert(!bootloader_migration_lock_auto(&fs, a));
} else if (scenario == "stat") {
for (const auto path : {Path, Temp}) {
fs.stat_error = path;
assert(bootloader_migration_auto_lock(&fs, a) == MigrationAutoLock::Unavailable);
assert(!bootloader_migration_lock_auto(&fs, a));
assert(fs.writes == 0 && fs.renames == 0);
}
} else if (scenario == "read") {
assert(bootloader_migration_lock_auto(&fs, a));
const auto before = fs.files;
fs.fail_read = true;
assert(bootloader_migration_auto_lock(&fs, a) == MigrationAutoLock::Unavailable);
assert(!bootloader_migration_lock_auto(&fs, b));
fs.fail_read = false;
fs.max_read = 7;
assert(bootloader_migration_auto_lock(&fs, a) == MigrationAutoLock::Unavailable);
assert(!bootloader_migration_lock_auto(&fs, b));
assert(fs.files == before);
} else if (scenario == "temporary") {
assert(bootloader_migration_lock_auto(&fs, a));
const auto complete = fs.files.at(Path);
for (bool primary : {false, true}) {
fs = FakeFilesystem();
if (primary) assert(bootloader_migration_lock_auto(&fs, a));
for (size_t size : {size_t(0), size_t(7), size_t(40)}) {
fs.files[Temp] = std::vector<uint8_t>(size, 0x31);
const auto before = fs.files;
assert(bootloader_migration_auto_lock(&fs, a) == MigrationAutoLock::Unavailable);
assert(!bootloader_migration_lock_auto(&fs, b));
assert(fs.files == before);
}
fs.files[Temp] = complete;
const auto before = fs.files;
assert(bootloader_migration_auto_lock(&fs, a) == MigrationAutoLock::Unavailable);
assert(bootloader_migration_auto_lock(&fs, b) == MigrationAutoLock::Unavailable);
assert(!bootloader_migration_lock_auto(&fs, b));
assert(fs.files == before);
}
} else if (scenario == "short_write" || scenario == "write_open") {
assert(bootloader_migration_lock_auto(&fs, a));
const auto before = fs.files;
if (scenario == "short_write") fs.max_write = 7;
else fs.fail_write_open = true;
assert(!bootloader_migration_lock_auto(&fs, b));
assert(fs.files == before);
assert(bootloader_migration_auto_lock(&fs, a) == MigrationAutoLock::Locked);
} else if (scenario == "rename_fail" || scenario == "rename_false_success") {
assert(bootloader_migration_lock_auto(&fs, a));
const auto before = fs.files;
fs.fail_rename = scenario == "rename_fail";
fs.false_success_rename = scenario == "rename_false_success";
assert(!bootloader_migration_lock_auto(&fs, b));
assert(fs.files == before);
assert(bootloader_migration_auto_lock(&fs, a) == MigrationAutoLock::Locked);
} else if (scenario == "rename_late_error") {
fs.error_after_publish = true;
assert(bootloader_migration_lock_auto(&fs, a));
assert(bootloader_migration_auto_lock(&fs, a) == MigrationAutoLock::Locked);
} else if (scenario == "publish_corrupt") {
fs.corrupt_after_publish = true;
assert(!bootloader_migration_lock_auto(&fs, a));
assert(bootloader_migration_auto_lock(&fs, a) == MigrationAutoLock::Unavailable);
} else if (scenario == "publish_unreadable") {
fs.fail_reads_after_publish = true;
assert(!bootloader_migration_lock_auto(&fs, a));
assert(bootloader_migration_auto_lock(&fs, a) == MigrationAutoLock::Unavailable);
fs.fail_read = false;
assert(bootloader_migration_auto_lock(&fs, a) == MigrationAutoLock::Locked);
} else if (scenario == "settings") {
fs.files["/node_prefs"] = {1, 2, 3, 4};
fs.files["/_main"] = {9, 8, 7};
const auto before = fs.files;
assert(bootloader_migration_lock_auto(&fs, a));
for (const auto& file : before) assert(fs.files.at(file.first) == file.second);
assert(fs.files.size() == before.size() + 1);
} else assert(false);
}
'''
class BootloaderMigrationStoreTest(unittest.TestCase):
@classmethod
def setUpClass(cls):
compiler = shutil.which("g++") or shutil.which("clang++")
if compiler is None:
raise unittest.SkipTest("a host C++17 compiler is required")
cls.temporary = tempfile.TemporaryDirectory(prefix="bootloader-lock-store-")
cls.addClassCleanup(cls.temporary.cleanup)
directory = Path(cls.temporary.name)
headers = directory / "helpers"
headers.mkdir()
(headers / "IdentityStore.h").write_text(FILESYSTEM, encoding="ascii")
source = directory / "test.cpp"
source.write_text(SOURCE, encoding="ascii")
cls.binary = directory / ("test.exe" if os.name == "nt" else "test")
sanitize = [] if os.name == "nt" else [
"-fsanitize=address,undefined", "-fno-sanitize-recover=all", "-fno-pie", "-no-pie"]
built = subprocess.run([
compiler, "-std=c++17", "-O1", "-g", "-Wall", "-Wextra", "-Werror",
"-DNRF52_PLATFORM=1", "-DOTA_BOOTLOADER_MIGRATION=1", *sanitize,
"-I", str(directory), "-I", str(ROOT / "src"), str(source),
str(ROOT / "src/helpers/ota/BootloaderMigrationStore.cpp"),
"-o", str(cls.binary),
], capture_output=True, text=True, timeout=60)
if built.returncode:
raise AssertionError(built.stdout + built.stderr)
def scenario(self, name):
tested = subprocess.run([str(self.binary), name], capture_output=True, text=True, timeout=30)
self.assertEqual(tested.returncode, 0, tested.stdout + tested.stderr)
def test_missing_store_and_volatile_storage(self):
self.scenario("fresh")
def test_power_cycle_and_new_firmware_compare_all_32_hash_bytes(self):
self.scenario("persist")
def test_matching_image_never_rewrites_its_lock(self):
self.scenario("locked")
def test_malformed_records_are_preserved_and_block_install(self):
self.scenario("malformed")
def test_directory_cannot_authorize_install(self):
self.scenario("directory")
def test_metadata_errors_are_not_treated_as_absence(self):
self.scenario("stat")
def test_unreadable_and_partial_records_are_preserved(self):
self.scenario("read")
def test_interrupted_transaction_never_authorizes_another_attempt(self):
self.scenario("temporary")
def test_partial_write_cannot_authorize_install(self):
self.scenario("short_write")
def test_write_open_failure_cannot_authorize_install(self):
self.scenario("write_open")
def test_failed_rename_preserves_previous_firmware_lock(self):
self.scenario("rename_fail")
def test_false_success_rename_cannot_authorize_install(self):
self.scenario("rename_false_success")
def test_verified_public_record_survives_late_rename_error(self):
self.scenario("rename_late_error")
def test_corruption_after_publication_fails_closed(self):
self.scenario("publish_corrupt")
def test_unreadable_publication_fails_closed_and_later_remains_locked(self):
self.scenario("publish_unreadable")
def test_settings_and_identity_are_untouched(self):
self.scenario("settings")
if __name__ == "__main__":
unittest.main()
+1
View File
@@ -180,6 +180,7 @@ class PackagedApplicationTests(unittest.TestCase):
self.assertEqual(result["profile"], self.profile["id"])
self.assertEqual(result["candidate_sha256"], hashlib.sha256(self.candidate).hexdigest())
self.assertEqual(result["activation"], "ota bootloader update")
self.assertEqual(result["automatic_delay_seconds"], 70)
self.patches["select_profile"].assert_called_once_with(
{"profiles": [self.profile]}, TARGET)
self.patches["validate_candidate_zip"].assert_called_once_with(
+91 -1
View File
@@ -47,9 +47,73 @@ static Snapshot good() {
s.app_image_size=s.app_body_size+ENDF_LEN;
return s;
}
static std::array<uint8_t, BOOT_SIZE> labelled(const char *text, size_t offset=100) {
std::array<uint8_t, BOOT_SIZE> bytes;
bytes.fill(0xFF);
assert(offset + std::strlen(text) + 1 <= bytes.size());
std::memcpy(bytes.data() + offset, text, std::strlen(text) + 1);
return bytes;
}
static InstalledVariant variant(const std::array<uint8_t, BOOT_SIZE> &image,
uint32_t version=0x02040CFF) {
return installed_variant(image.data(), image.size(), version);
}
int main(int argc,char **argv) {
assert(argc==2);
if (!std::strcmp(argv[1],"geometry")) {
if (!std::strcmp(argv[1],"normal-labels")) {
assert(variant(labelled("UF2 Bootloader v0.11.0-OTAFIX2.4.12\r\nModel: test\r\n")) == InstalledVariant::Normal);
assert(variant(labelled("UF2 Bootloader 0.11.0-OTAFIX2.4.12\r\n")) == InstalledVariant::Normal);
assert(variant(labelled("UF2 Bootloader BL_0x02040CFF\r\n")) == InstalledVariant::Normal);
assert(variant(labelled("UF2 Bootloader v0.11.0-OTAFIX2.4.11\r\n"),0x02040BFF) == InstalledVariant::Normal);
assert(variant(labelled("UF2 Bootloader v0.11.0-OTAFIX2.4.12-preview.16\r\n"),0x02040C10) == InstalledVariant::Normal);
assert(variant(labelled("UF2 Bootloader BL_0x02040C10\r\n"),0x02040C10) == InstalledVariant::Normal);
assert(variant(labelled("UF2 Bootloader v0.11.0-OTAFIX255.255.254-preview.254\r\n"),0xFFFFFEFE) == InstalledVariant::Normal);
} else if (!std::strcmp(argv[1],"recovery-labels")) {
for (const char *text: {"R_0x02040CFF", "UF2 Bootloader R_0x02040CFF\r\n",
"UF2 Bootloader R_v0.11.0-OTAFIX2.4.12\r\n",
"0.11.0-OTAFIX2.4.7-preview.2-recovery-allow-all s140 6.1.1"}) {
auto bytes=labelled("UF2 Bootloader v0.11.0-OTAFIX2.4.12\r\n");
std::memcpy(bytes.data()+500,text,std::strlen(text)+1);
assert(variant(bytes)==InstalledVariant::Recovery); // Recovery wins over a normal decoy.
assert(variant(labelled(text))==InstalledVariant::Recovery);
}
// Historical compact recovery images have neither version label nor flag.
assert(variant(labelled(""))==InstalledVariant::Unknown);
auto bytes=labelled("UF2 Bootloader v0.11.0-OTAFIX2.4.12\r\n");
std::memcpy(bytes.data()+BOOT_SIZE-4,"R_0x",4);
assert(variant(bytes)==InstalledVariant::Recovery);
} else if (!std::strcmp(argv[1],"unknown-labels")) {
for (const char *text: {"", "v0.11.0-OTAFIX2.4.12", "UF2 Bootloader ",
"UF2 Bootloader v0.11.0-OTAFIX2.4.11\r\n",
"UF2 Bootloader v0.11.0-OTAFIX2.4.12-preview.1\r\n",
"UF2 Bootloader v0.11.0-OTAFIX2.4.12-dirty\r\n",
"UF2 Bootloader v0.11.0-OTAFIX2.4.12\n",
"UF2 Bootloader v0.11.0-OTAFIX2.4.12",
"UF2 Bootloader vBL_0x02040CFF\r\n",
"UF2 Bootloader BL_0x02040DFF\r\n"})
assert(variant(labelled(text))==InstalledVariant::Unknown);
auto bytes=labelled("UF2 Bootloader v0.11.0-OTAFIX2.4.12\r\n");
assert(installed_variant(nullptr,BOOT_SIZE,0x02040CFF)==InstalledVariant::Unknown);
assert(installed_variant(bytes.data(),BOOT_SIZE-1,0x02040CFF)==InstalledVariant::Unknown);
assert(variant(bytes,0)==InstalledVariant::Unknown);
assert(variant(bytes,UINT32_MAX)==InstalledVariant::Unknown);
assert(variant(bytes,0x02040C00)==InstalledVariant::Unknown);
const char *duplicate="UF2 Bootloader BL_0x02040CFF\r\n";
std::memcpy(bytes.data()+500,duplicate,std::strlen(duplicate)+1);
assert(variant(bytes)==InstalledVariant::Unknown);
bytes=labelled("UF2 Bootloader v0.11.0-OTAFIX2.4.12\r\n");
const char *conflict="UF2 Bootloader v0.11.0-OTAFIX2.4.11\r\n";
std::memcpy(bytes.data()+500,conflict,std::strlen(conflict)+1);
assert(variant(bytes)==InstalledVariant::Unknown);
for (size_t tail=1;tail<40;++tail) {
bytes.fill(0xFF);
const char *label="UF2 Bootloader v0.11.0-OTAFIX2.4.12\r\n";
size_t count=std::strlen(label);
if (tail>=count) continue;
std::memcpy(bytes.data()+BOOT_SIZE-tail,label,tail);
assert(variant(bytes)==InstalledVariant::Unknown);
}
} else if (!std::strcmp(argv[1],"geometry")) {
assert(preflight(good())==Gate::Ok);
Snapshot s=good(); s.part=0x52833; assert(preflight(s)==Gate::Chip);
s=good(); s.flash_page_size=2048; assert(preflight(s)==Gate::Chip);
@@ -101,6 +165,7 @@ int main(int argc,char **argv) {
assert(candidate_matches_resource(v7,0x239A0029,name,0x02040CFF,0x27000,0x123));
assert(installed_can_migrate(installed,candidate,false,false,0x26000,0xB6));
if (!std::strcmp(argv[1],"up-to-date")) {
assert(installed_matches_resource(installed,0x239A0029,name,0x26000,0xB6));
assert(!installed_up_to_date(installed,0x239A0029,name,0x02040CFF,0x26000,0xB6));
installed.boot_version=0x02040CFE; // Preview precedes the stable release.
assert(!installed_up_to_date(installed,0x239A0029,name,0x02040CFF,0x26000,0xB6));
@@ -189,6 +254,15 @@ class MigrationPolicyTests(unittest.TestCase):
def test_actual_v6_v7_geometry_and_linked_source_bounds(self):
self.scenario('geometry')
def test_automatic_variant_recognizes_complete_normal_release_labels(self):
self.scenario('normal-labels')
def test_recovery_markers_override_normal_labels_and_missing_text_fails_closed(self):
self.scenario('recovery-labels')
def test_automatic_variant_rejects_unknown_ambiguous_or_truncated_labels(self):
self.scenario('unknown-labels')
def test_all_acl_slots_protect_source_bootloader_and_parameter_page(self):
self.scenario('acl')
@@ -219,6 +293,22 @@ class MigrationPolicyTests(unittest.TestCase):
self.assertNotEqual(result.returncode, 0)
self.assertIn('Assertion', result.stderr)
def test_negative_control_cannot_ignore_recovery_version_marker(self):
original = (ROOT / 'src/helpers/ota/BootloaderMigrationPolicy.h').read_text()
changed = original.replace(
'boot_text_matches(image + i, left, recovery, sizeof(recovery) - 1)', 'false')
self.assertNotEqual(original, changed)
with tempfile.TemporaryDirectory(prefix='migration-recovery-negative-') as directory:
directory = Path(directory)
header = directory / 'helpers/ota/BootloaderMigrationPolicy.h'
header.parent.mkdir(parents=True)
header.write_text(changed.replace('#include "OtaBootloaderUpdate.h"',
'#include "helpers/ota/OtaBootloaderUpdate.h"'))
binary = self.compile(directory, HARNESS, directory)
result = subprocess.run([str(binary), 'recovery-labels'], capture_output=True, text=True)
self.assertNotEqual(result.returncode, 0)
self.assertIn('Assertion', result.stderr)
if __name__ == '__main__':
unittest.main()
+100 -1
View File
@@ -5,7 +5,8 @@ The ARM SVC adapter is replaced by a failing trap, and the EndF stub refuses
installation before hardware access. Real CRC-bound installed manifests test
the version check; a controlled digest exercises the exact candidate no-op.
This tests scheduling and cancellation, not cryptography, MBR copying or
power-loss recovery; those require separate qualification.
power-loss recovery; those require separate qualification. The durable-store
boundary is modeled here and tested against real file faults separately.
"""
import os
from pathlib import Path
@@ -40,6 +41,84 @@ class MigrationRuntimeTests(unittest.TestCase):
def run_scenario(self, scenario):
subprocess.run([str(self.binary), scenario], check=True)
def test_recovery_unknown_or_damaged_bootloaders_require_an_explicit_command(self):
for scenario in ('automatic-recovery-prefix', 'automatic-recovery-suffix',
'automatic-unlabelled-loader', 'automatic-ambiguous-loader',
'automatic-foreign-loader', 'automatic-damaged-loader'):
with self.subTest(scenario=scenario):
self.run_scenario(scenario)
def test_same_firmware_remains_locked_after_a_cold_boot(self):
for scenario in ('durable-same-firmware-cold-boot',
'durable-same-firmware-old-loader'):
with self.subTest(scenario=scenario):
self.run_scenario(scenario)
def test_new_image_classification_interruption_is_locked_before_reboot(self):
self.run_scenario('previous-image-interrupted-classification')
def test_a_different_full_firmware_hash_allows_a_new_automatic_check(self):
for scenario in ('durable-different-firmware',
'durable-different-firmware-failure-marker'):
with self.subTest(scenario=scenario):
self.run_scenario(scenario)
def test_missing_or_failed_durable_state_refuses_a_genuine_update(self):
for scenario in ('automatic-store-unavailable', 'automatic-store-null',
'automatic-invalid-application', 'automatic-store-write-failure'):
with self.subTest(scenario=scenario):
self.run_scenario(scenario)
def test_automatic_update_waits_70_seconds_then_observes_install_grace(self):
for scenario in ('automatic-delay', 'automatic-arm-enabled'):
with self.subTest(scenario=scenario):
self.run_scenario(scenario)
def test_automatic_update_defers_until_radio_and_ota_are_idle(self):
self.run_scenario('automatic-busy')
def test_automatic_update_skips_same_or_newer_version_and_exact_image(self):
for scenario in ('automatic-same-version', 'automatic-same-version-enabled',
'automatic-newer-version',
'automatic-exact-image'):
with self.subTest(scenario=scenario):
self.run_scenario(scenario)
def test_retained_failure_prevents_automatic_retry_but_allows_manual_retry(self):
self.run_scenario('automatic-retained-failure')
def test_manual_attempt_prevents_a_second_automatic_attempt(self):
for scenario in ('manual-cancel-consumes-automatic',
'manual-failure-consumes-automatic'):
with self.subTest(scenario=scenario):
self.run_scenario(scenario)
def test_cancelled_automatic_update_is_not_rearmed(self):
self.run_scenario('automatic-cancel')
def test_invalid_confirmation_does_not_cancel_the_automatic_update(self):
self.run_scenario('invalid-confirmation-preserves-automatic')
def test_reboot_guard_is_written_through_softdevice_before_arming(self):
self.run_scenario('guard-softdevice-enabled')
def test_reboot_guard_read_errors_block_the_automatic_attempt(self):
for scenario in ('automatic-status-read-failure', 'automatic-marker-read-failure'):
with self.subTest(scenario=scenario):
self.run_scenario(scenario)
def test_reboot_guard_api_failures_refuse_to_arm(self):
for scenario in ('guard-softdevice-status-failure', 'guard-marker-read-failure',
'guard-clear-failure',
'guard-set-failure'):
with self.subTest(scenario=scenario):
self.run_scenario(scenario)
def test_reset_during_check_or_armed_attempt_cannot_create_an_update_loop(self):
for scenario in ('automatic-interrupted-check', 'automatic-interrupted-arm'):
with self.subTest(scenario=scenario):
self.run_scenario(scenario)
def test_wrong_confirmation_cannot_arm(self):
self.run_scenario('wrong-confirmation')
@@ -95,6 +174,26 @@ class MigrationRuntimeTests(unittest.TestCase):
self.assertNotEqual(result.returncode, 0)
self.assertIn('!bootloader_migration_pending()', result.stderr)
def test_negative_control_automatic_check_must_lock_before_classification(self):
driver = (ROOT / 'src/helpers/ota/BootloaderMigration.cpp').read_text()
start = driver.index('void bootloader_migration_tick(')
changed_tick = driver[start:].replace(
'if (!lock_automatic_for_firmware()) {', 'if (false) {', 1)
self.assertNotEqual(driver[start:], changed_tick)
changed = driver[:start] + changed_tick
harness = (STUBS / 'runtime.cpp').read_text().replace(
'#include "helpers/ota/BootloaderMigration.cpp"', changed)
with tempfile.TemporaryDirectory(prefix='migration-auto-lock-negative-') as directory:
directory = Path(directory)
source = directory / 'runtime.cpp'
source.write_text(harness)
binary = self.compile(directory, source)
result = subprocess.run(
[str(binary), 'previous-image-interrupted-classification'],
capture_output=True, text=True)
self.assertNotEqual(result.returncode, 0)
self.assertIn('durable_lock_present && store_writes == 1', result.stderr)
def test_grace_and_radio_drain_are_both_required(self):
self.run_scenario('grace-and-drain')
@@ -0,0 +1,7 @@
#pragma once
#include <stdint.h>
// Only the filesystem handle crosses the migration driver's store boundary.
// The real persistence implementation is exercised by its separate fault tests.
class MigrationTestFilesystem {};
#define FILESYSTEM MigrationTestFilesystem
@@ -2,3 +2,5 @@
#include <stdint.h>
uint32_t sd_power_gpregret_get(uint32_t index, uint32_t *result);
uint32_t sd_power_gpregret_clr(uint32_t index, uint32_t mask);
uint32_t sd_power_gpregret_set(uint32_t index, uint32_t mask);
+545 -19
View File
@@ -30,7 +30,28 @@ unsigned softdevice_checks = 0;
unsigned disables = 0;
unsigned resets = 0;
unsigned copies = 0;
unsigned retained_reads = 0;
unsigned retained_clears = 0;
unsigned retained_sets = 0;
bool candidate_installed = false;
bool interrupt_hash = false;
bool interrupt_classification = false;
unsigned classification_checks = 0;
bool softdevice_enabled = false;
bool startup_metadata = false;
bool startup_valid = true;
bool store_available = true;
bool store_write_ok = true;
bool durable_lock_present = false;
uint8_t application_hash[32] = {1};
uint8_t durable_lock_hash[32] = {};
unsigned store_reads = 0;
unsigned store_writes = 0;
unsigned store_lock_calls = 0;
uint32_t softdevice_status = NRF_SUCCESS;
uint32_t retained_get_status = NRF_SUCCESS;
uint32_t retained_clear_status = NRF_SUCCESS;
uint32_t retained_set_status = NRF_SUCCESS;
void mbr_command() {
++copies;
@@ -38,14 +59,29 @@ void mbr_command() {
}
} // namespace migration_test
#include "helpers/ota/BootloaderMigrationPolicy.h"
namespace mesh { namespace ota { namespace migration {
InstalledVariant installed_variant_for_runtime(const uint8_t *image, size_t size,
uint32_t version) {
++migration_test::classification_checks;
if (migration_test::interrupt_classification)
throw std::runtime_error("interrupted bootloader classification");
return installed_variant(image, size, version);
}
} } }
// Include the actual driver. Only its target-specific naked SVC adapter is
// replaced: function-like 'volatile(...)' captures the extended assembly, but
// leaves ordinary 'volatile uint8_t *' memory reads unchanged. The adapter is
// an explicit failing trap, never a successful simulated flash operation.
// A classification boundary permits a reset injection while still using the
// real classifier for every completed check.
#define __attribute__(...)
#define __asm
#define volatile(...) migration_test::mbr_command()
#define installed_variant installed_variant_for_runtime
#include "helpers/ota/BootloaderMigration.cpp"
#undef installed_variant
#undef volatile
#undef __asm
#undef __attribute__
@@ -83,8 +119,8 @@ void NVIC_SystemReset() {
uint32_t sd_softdevice_is_enabled(uint8_t *enabled) {
++migration_test::softdevice_checks;
*enabled = 0;
return NRF_SUCCESS;
*enabled = migration_test::softdevice_enabled;
return migration_test::softdevice_status;
}
uint32_t sd_softdevice_disable() {
@@ -92,12 +128,34 @@ uint32_t sd_softdevice_disable() {
throw std::runtime_error("unexpected SoftDevice disable in state-machine test");
}
uint32_t sd_power_gpregret_get(uint32_t, uint32_t *result) {
*result = 0;
return NRF_SUCCESS;
uint32_t sd_power_gpregret_get(uint32_t index, uint32_t *result) {
assert(index == 0);
++migration_test::retained_reads;
*result = power_registers.GPREGRET;
return migration_test::retained_get_status;
}
uint32_t sd_power_gpregret_clr(uint32_t index, uint32_t mask) {
assert(index == 0 && mask == 0xFF);
assert(!mesh::ota::bootloader_migration_pending());
++migration_test::retained_clears;
if (migration_test::retained_clear_status == NRF_SUCCESS)
power_registers.GPREGRET &= ~mask;
return migration_test::retained_clear_status;
}
uint32_t sd_power_gpregret_set(uint32_t index, uint32_t mask) {
assert(index == 0 && mask == mesh::ota::ATTEMPT_MARKER);
assert(!mesh::ota::bootloader_migration_pending());
++migration_test::retained_sets;
if (migration_test::retained_set_status == NRF_SUCCESS)
power_registers.GPREGRET |= mask;
return migration_test::retained_set_status;
}
void SHA256::reset() {
if (migration_test::interrupt_hash)
throw std::runtime_error("interrupted bootloader check");
}
void SHA256::update(const void *, size_t) {
}
@@ -106,16 +164,47 @@ void SHA256::finalize(void *out, size_t size) {
// Scheduling tests exercise the full memory-read loop; cryptography itself
// is the production SHA256 library's contract, not this hardware stub.
std::memset(out, 0, size);
if (migration_test::candidate_installed)
if (migration_test::startup_metadata)
std::memcpy(out, migration_test::application_hash, size);
else if (migration_test::candidate_installed)
std::memcpy(out, migration_candidate_sha256, size);
}
namespace mesh {
namespace ota {
bool ota_self_firmware(SelfFwInfo &) {
bool ota_self_firmware(SelfFwInfo &out) {
++migration_test::endf_checks;
if (migration_test::startup_metadata && migration_test::startup_valid) {
out.valid = true;
out.body_len = 256 - ENDF_LEN;
out.image_len = 256;
return true;
}
return false; // Stop at the first install preflight, before hardware access.
}
MigrationAutoLock bootloader_migration_auto_lock(FILESYSTEM *fs, const uint8_t hash[32]) {
++migration_test::store_reads;
assert(std::memcmp(hash, migration_test::application_hash, 32) == 0);
if (!fs || !migration_test::store_available) return MigrationAutoLock::Unavailable;
if (!migration_test::durable_lock_present) return MigrationAutoLock::Eligible;
return std::memcmp(hash, migration_test::durable_lock_hash, 32) == 0
? MigrationAutoLock::Locked : MigrationAutoLock::PreviousImage;
}
bool bootloader_migration_lock_auto(FILESYSTEM *fs, const uint8_t hash[32]) {
++migration_test::store_lock_calls;
assert(!bootloader_migration_pending());
assert(std::memcmp(hash, migration_test::application_hash, 32) == 0);
if (!fs || !migration_test::store_available) return false;
if (migration_test::durable_lock_present &&
std::memcmp(migration_test::durable_lock_hash, hash, 32) == 0) return true;
if (!migration_test::store_write_ok) return false;
++migration_test::store_writes;
std::memcpy(migration_test::durable_lock_hash, hash, 32);
migration_test::durable_lock_present = true;
return true;
}
} // namespace ota
} // namespace mesh
@@ -123,6 +212,7 @@ namespace {
using namespace mesh::ota;
const char confirmation[] = "0123456789ABCDEF";
char reply[160];
FILESYSTEM test_filesystem;
void wr16(uint8_t *p, uint16_t value) {
p[0] = value;
@@ -137,6 +227,8 @@ void wr32(uint8_t *p, uint32_t value) {
uint8_t *installed_manifest(uint32_t version) {
uint8_t *boot = reinterpret_cast<uint8_t *>(migration::BOOT_BASE);
std::memset(boot, 0xFF, migration::BOOT_SIZE);
std::snprintf(reinterpret_cast<char *>(boot + 0x100), 64,
"UF2 Bootloader BL_0x%08X\r\n", version);
uint8_t *p = boot + OTA_BOOT_CANDIDATE_MANIFEST_OFFSET;
wr32(p, OTA_BOOT_MANIFEST_MAGIC0);
wr32(p + 4, OTA_BOOT_MANIFEST_MAGIC1);
@@ -166,15 +258,26 @@ void seal_manifest(uint8_t *p) {
OTA_BOOT_CANDIDATE_MANIFEST_OFFSET + 40));
}
void replace_boot_label(const char *label) {
auto *boot = reinterpret_cast<uint8_t *>(migration::BOOT_BASE);
std::memset(boot + 0x100, 0xFF, 256);
std::strcpy(reinterpret_cast<char *>(boot + 0x100), label);
seal_manifest(boot + OTA_BOOT_CANDIDATE_MANIFEST_OFFSET);
}
void arm() {
assert(bootloader_migration_request(confirmation, reply, sizeof(reply)));
assert(bootloader_migration_pending());
assert(std::strstr(reply, "OK migration armed"));
assert(power_registers.GPREGRET == mesh::ota::ATTEMPT_MARKER);
assert(migration_test::durable_lock_present);
assert(std::memcmp(migration_test::durable_lock_hash,
migration_test::application_hash, 32) == 0);
}
void no_install() {
void no_install(unsigned softdevice_reads = 0) {
assert(migration_test::endf_checks == 0);
assert(migration_test::softdevice_checks == 0);
assert(migration_test::softdevice_checks == softdevice_reads);
assert(migration_test::disables == 0);
assert(migration_test::resets == 0);
assert(migration_test::copies == 0);
@@ -186,6 +289,33 @@ void status_result(unsigned result) {
std::snprintf(expected, sizeof(expected), "last=%u", result);
assert(std::strstr(reply, expected));
}
void begin_application(FILESYSTEM *fs = &test_filesystem) {
migration_test::startup_metadata = true;
bootloader_migration_begin(fs);
migration_test::startup_metadata = false;
migration_test::endf_checks = 0;
}
void restart_application(FILESYSTEM *fs = &test_filesystem) {
// Recreate zero-initialized driver state while retaining GPREGRET, as a
// hardware reset would. Test counters deliberately survive for assertions.
mesh::ota::pending = false;
mesh::ota::automatic_attempted = false;
mesh::ota::automatic_blocked = false;
mesh::ota::armed_at = 0;
mesh::ota::last_result = 0;
mesh::ota::result_is_current = false;
mesh::ota::already_installed = false;
mesh::ota::state_fs = nullptr;
std::memset(mesh::ota::firmware_hash, 0, sizeof(mesh::ota::firmware_hash));
mesh::ota::automatic_lock = MigrationAutoLock::Unavailable;
mesh::ota::firmware_identified = false;
mesh::ota::automatic_variant = migration::InstalledVariant::Unknown;
mesh::ota::loader_excluded = false;
migration_test::now = 0;
begin_application(fs);
}
} // namespace
int main(int argc, char **argv) {
@@ -198,8 +328,401 @@ int main(int argc, char **argv) {
assert(mmap(reinterpret_cast<void *>(0xF0000), 0x10000, PROT_READ | PROT_WRITE,
MAP_PRIVATE | MAP_ANONYMOUS | MAP_FIXED, -1, 0) != MAP_FAILED);
#endif
#if defined(_WIN32)
assert(VirtualAlloc(reinterpret_cast<void *>(0x20000), 0x10000,
MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE) != nullptr);
#else
assert(mmap(reinterpret_cast<void *>(0x26000), 4096, PROT_READ | PROT_WRITE,
MAP_PRIVATE | MAP_ANONYMOUS | MAP_FIXED, -1, 0) != MAP_FAILED);
#endif
seal_manifest(installed_manifest(0x02040BFF));
begin_application();
const char *scenario = argv[1];
if (std::strcmp(scenario, "already-installed") == 0) {
if (std::strcmp(scenario, "automatic-recovery-prefix") == 0 ||
std::strcmp(scenario, "automatic-recovery-suffix") == 0 ||
std::strcmp(scenario, "automatic-unlabelled-loader") == 0 ||
std::strcmp(scenario, "automatic-ambiguous-loader") == 0 ||
std::strcmp(scenario, "automatic-foreign-loader") == 0 ||
std::strcmp(scenario, "automatic-damaged-loader") == 0) {
if (std::strcmp(scenario, "automatic-recovery-prefix") == 0)
replace_boot_label("UF2 Bootloader R_0x02040BFF\r\n");
else if (std::strcmp(scenario, "automatic-recovery-suffix") == 0)
replace_boot_label("UF2 Bootloader v0.11.0-OTAFIX2.4.11-recovery-allow-all\r\n");
else if (std::strcmp(scenario, "automatic-unlabelled-loader") == 0)
replace_boot_label("");
else if (std::strcmp(scenario, "automatic-ambiguous-loader") == 0)
replace_boot_label("UF2 Bootloader BL_0x02040BFF\r\nUF2 Bootloader BL_0x02040BFF\r\n");
else {
auto *p = reinterpret_cast<uint8_t *>(migration::BOOT_BASE) + OTA_BOOT_CANDIDATE_MANIFEST_OFFSET;
if (std::strcmp(scenario, "automatic-foreign-loader") == 0) {
wr32(p + 20, migration_board_id ^ 1);
seal_manifest(p);
} else {
p[40] ^= 1;
}
}
now = 70000;
bootloader_migration_tick(true, true);
assert(!bootloader_migration_pending());
assert(!bootloader_migration_service_due());
assert(durable_lock_present && store_writes == 1);
no_install(softdevice_checks);
bootloader_migration_status(reply, sizeof(reply));
const bool recovery = std::strstr(scenario, "recovery") != nullptr;
assert(std::strstr(reply, recovery ? "auto=recovery" : "auto=unrecognized"));
now = 140000;
bootloader_migration_tick(true, true);
no_install(softdevice_checks);
arm(); // Recovery/unknown loaders remain an explicit operator decision.
assert(durable_lock_present && store_writes == 1);
now = 141500;
bootloader_migration_tick(true, true);
assert(endf_checks == 1);
} else if (std::strcmp(scenario, "previous-image-interrupted-classification") == 0) {
durable_lock_present = true;
std::memcpy(durable_lock_hash, application_hash, 32);
durable_lock_hash[31] ^= 1;
power_registers.GPREGRET = mesh::ota::ATTEMPT_MARKER;
restart_application();
now = 70000;
interrupt_classification = true;
bool interrupted = false;
try {
bootloader_migration_tick(true, true);
} catch (const std::runtime_error &error) {
assert(std::strcmp(error.what(), "interrupted bootloader classification") == 0);
interrupted = true;
}
assert(interrupted && classification_checks == 1);
assert(!bootloader_migration_pending());
assert(durable_lock_present && store_writes == 1);
assert(std::memcmp(durable_lock_hash, application_hash, 32) == 0);
assert(power_registers.GPREGRET == mesh::ota::ATTEMPT_MARKER);
const unsigned reads_before_reset = softdevice_checks;
interrupt_classification = false;
restart_application();
now = 70000;
assert(!bootloader_migration_service_due());
bootloader_migration_tick(true, true);
assert(!bootloader_migration_pending());
assert(classification_checks == 1 && store_writes == 1);
no_install(reads_before_reset);
} else if (std::strcmp(scenario, "durable-same-firmware-cold-boot") == 0 ||
std::strcmp(scenario, "durable-same-firmware-old-loader") == 0) {
now = 70000;
bootloader_migration_tick(true, true);
assert(bootloader_migration_pending());
assert(durable_lock_present && store_writes == 1);
const bool installed = std::strcmp(scenario, "durable-same-firmware-cold-boot") == 0;
if (installed) seal_manifest(installed_manifest(migration_boot_version));
power_registers.GPREGRET = 0; // A power cycle loses the retained register.
restart_application();
const unsigned reads_before = softdevice_checks;
now = 70000;
assert(!bootloader_migration_service_due());
bootloader_migration_tick(true, true);
assert(!bootloader_migration_pending());
assert(store_writes == 1);
no_install(reads_before);
assert(bootloader_migration_request(nullptr, reply, sizeof(reply)));
assert(bootloader_migration_pending() == !installed);
assert(store_writes == 1); // An explicit retry reuses this image's lock.
} else if (std::strcmp(scenario, "durable-different-firmware") == 0 ||
std::strcmp(scenario, "durable-different-firmware-failure-marker") == 0) {
now = 70000;
bootloader_migration_tick(true, true);
assert(bootloader_migration_pending());
assert(durable_lock_present && store_writes == 1);
assert(power_registers.GPREGRET == mesh::ota::ATTEMPT_MARKER);
if (std::strcmp(scenario, "durable-different-firmware-failure-marker") == 0)
power_registers.GPREGRET = 0xCC;
application_hash[31] ^= 1; // The full 32-byte identity must distinguish images.
restart_application();
now = 70000;
assert(bootloader_migration_service_due());
bootloader_migration_tick(true, true);
assert(bootloader_migration_pending()); // Old retained guards belong to the old image.
assert(store_writes == 2);
assert(std::memcmp(durable_lock_hash, application_hash, 32) == 0);
assert(endf_checks == 0);
} else if (std::strcmp(scenario, "automatic-store-unavailable") == 0 ||
std::strcmp(scenario, "automatic-store-null") == 0 ||
std::strcmp(scenario, "automatic-invalid-application") == 0 ||
std::strcmp(scenario, "automatic-store-write-failure") == 0) {
if (std::strcmp(scenario, "automatic-store-unavailable") == 0)
store_available = false;
else if (std::strcmp(scenario, "automatic-store-null") == 0) {
// The role has no durable filesystem handle at this boot.
}
else if (std::strcmp(scenario, "automatic-invalid-application") == 0)
startup_valid = false;
else
store_write_ok = false;
restart_application(std::strcmp(scenario, "automatic-store-null") == 0
? nullptr : &test_filesystem);
now = 70000;
bootloader_migration_tick(true, true);
assert(!bootloader_migration_pending());
assert(!bootloader_migration_service_due());
assert(!durable_lock_present && store_writes == 0);
no_install(softdevice_checks);
assert(!bootloader_migration_request(nullptr, reply, sizeof(reply)));
assert(!bootloader_migration_pending());
no_install(softdevice_checks);
seal_manifest(installed_manifest(migration_boot_version));
assert(bootloader_migration_request(nullptr, reply, sizeof(reply)));
assert(!bootloader_migration_pending()); // A version no-op needs no store write.
} else if (std::strcmp(scenario, "automatic-delay") == 0 ||
std::strcmp(scenario, "automatic-arm-enabled") == 0) {
softdevice_enabled = std::strcmp(scenario, "automatic-arm-enabled") == 0;
assert(!bootloader_migration_service_due());
now = 69999;
assert(!bootloader_migration_service_due());
bootloader_migration_tick(true, true);
assert(!bootloader_migration_pending());
no_install();
now = 70000;
assert(bootloader_migration_service_due());
bootloader_migration_tick(true, true);
assert(bootloader_migration_pending());
assert(bootloader_migration_service_due());
no_install(3);
assert(durable_lock_present && store_writes == 1);
assert(power_registers.GPREGRET == mesh::ota::ATTEMPT_MARKER);
assert(retained_reads == (softdevice_enabled ? 3u : 0u));
assert(retained_clears == (softdevice_enabled ? 1u : 0u));
assert(retained_sets == (softdevice_enabled ? 1u : 0u));
now = 71499;
bootloader_migration_tick(true, true);
assert(bootloader_migration_pending());
no_install(3);
now = 71500;
bootloader_migration_tick(true, true);
assert(endf_checks == 1);
assert(!bootloader_migration_pending());
assert(!bootloader_migration_service_due());
now = 140000;
bootloader_migration_tick(true, true);
assert(endf_checks == 1); // A failed preflight must not be retried automatically.
assert(softdevice_checks == 3);
status_result(1);
} else if (std::strcmp(scenario, "automatic-busy") == 0) {
now = 70000;
bootloader_migration_tick(false, true);
assert(!bootloader_migration_pending());
assert(bootloader_migration_service_due());
no_install();
now = 72000;
bootloader_migration_tick(true, false);
assert(!bootloader_migration_pending());
assert(bootloader_migration_service_due());
no_install();
now = 74000;
bootloader_migration_tick(false, false);
assert(!bootloader_migration_pending());
assert(bootloader_migration_service_due());
no_install();
now = 76000;
bootloader_migration_tick(true, true);
assert(bootloader_migration_pending());
no_install(3);
now = 77499;
bootloader_migration_tick(true, true);
no_install(3);
now = 77500;
bootloader_migration_tick(true, true);
assert(endf_checks == 1); // Grace starts when the deferred request is armed.
} else if (std::strcmp(scenario, "automatic-same-version") == 0 ||
std::strcmp(scenario, "automatic-same-version-enabled") == 0 ||
std::strcmp(scenario, "automatic-newer-version") == 0 ||
std::strcmp(scenario, "automatic-exact-image") == 0) {
softdevice_enabled = std::strcmp(scenario, "automatic-same-version-enabled") == 0;
if (std::strcmp(scenario, "automatic-exact-image") == 0) {
candidate_installed = true;
} else {
seal_manifest(installed_manifest(
(std::strcmp(scenario, "automatic-same-version") == 0 || softdevice_enabled)
? migration_boot_version : 0x02040D01));
}
now = 70000;
bootloader_migration_tick(true, true);
assert(!bootloader_migration_pending());
assert(!bootloader_migration_service_due());
no_install(3);
assert(power_registers.GPREGRET == 0); // Clear the provisional check marker.
assert(retained_reads == (softdevice_enabled ? 3u : 0u));
assert(retained_clears == (softdevice_enabled ? 2u : 0u));
assert(retained_sets == (softdevice_enabled ? 1u : 0u));
now = 140000;
bootloader_migration_tick(true, true);
assert(!bootloader_migration_pending());
assert(!bootloader_migration_service_due());
no_install(3);
bootloader_migration_status(reply, sizeof(reply));
assert(std::strstr(reply, candidate_installed ? "already installed" : "up to date"));
} else if (std::strcmp(scenario, "automatic-retained-failure") == 0) {
power_registers.GPREGRET = 0xCC;
now = 70000;
bootloader_migration_tick(true, true);
assert(!bootloader_migration_pending());
assert(!bootloader_migration_service_due());
no_install(1);
now = 140000;
bootloader_migration_tick(true, true);
no_install(1); // Retained failures must not create a warm-reset retry loop.
bootloader_migration_status(reply, sizeof(reply));
assert(std::strstr(reply, "auto=blocked"));
assert(bootloader_migration_request(nullptr, reply, sizeof(reply)));
assert(bootloader_migration_pending());
assert(std::strstr(reply, "OK migration armed"));
now = 141500;
bootloader_migration_tick(true, true);
assert(endf_checks == 1); // An explicit operator request may still retry.
assert(!bootloader_migration_pending());
} else if (std::strcmp(scenario, "manual-cancel-consumes-automatic") == 0 ||
std::strcmp(scenario, "manual-failure-consumes-automatic") == 0) {
arm();
now = 1500;
const bool cancel = std::strcmp(scenario, "manual-cancel-consumes-automatic") == 0;
bootloader_migration_tick(true, !cancel);
assert(!bootloader_migration_pending());
assert(endf_checks == (cancel ? 0u : 1u));
now = 70000;
assert(!bootloader_migration_service_due());
bootloader_migration_tick(true, true);
assert(!bootloader_migration_pending());
assert(endf_checks == (cancel ? 0u : 1u));
assert(softdevice_checks == 1);
} else if (std::strcmp(scenario, "automatic-cancel") == 0) {
now = 70000;
bootloader_migration_tick(true, true);
assert(bootloader_migration_pending());
now = 70100;
bootloader_migration_tick(true, false);
assert(!bootloader_migration_pending());
assert(!bootloader_migration_service_due());
no_install(3);
now = 140000;
bootloader_migration_tick(true, true);
assert(!bootloader_migration_pending());
assert(!bootloader_migration_service_due());
no_install(3);
status_result(16);
} else if (std::strcmp(scenario, "invalid-confirmation-preserves-automatic") == 0) {
now = 1000;
assert(!bootloader_migration_request("0123456789ABCDE0", reply, sizeof(reply)));
assert(!bootloader_migration_pending());
now = 70000;
assert(bootloader_migration_service_due());
bootloader_migration_tick(true, true);
assert(bootloader_migration_pending());
no_install(3);
now = 71500;
bootloader_migration_tick(true, true);
assert(endf_checks == 1);
assert(!bootloader_migration_pending());
assert(!bootloader_migration_service_due());
} else if (std::strcmp(scenario, "automatic-status-read-failure") == 0 ||
std::strcmp(scenario, "automatic-marker-read-failure") == 0) {
power_registers.GPREGRET = mesh::ota::ATTEMPT_MARKER;
if (std::strcmp(scenario, "automatic-status-read-failure") == 0) {
softdevice_status = 1;
} else {
softdevice_enabled = true;
retained_get_status = 1;
}
now = 70000;
bootloader_migration_tick(true, true);
assert(!bootloader_migration_pending());
assert(!bootloader_migration_service_due());
no_install(1);
assert(retained_reads == (softdevice_enabled ? 1u : 0u));
assert(retained_clears == 0 && retained_sets == 0);
assert(power_registers.GPREGRET == mesh::ota::ATTEMPT_MARKER);
bootloader_migration_status(reply, sizeof(reply));
assert(std::strstr(reply, "auto=blocked"));
} else if (std::strcmp(scenario, "guard-softdevice-enabled") == 0) {
softdevice_enabled = true;
power_registers.GPREGRET = 0xAA;
arm();
assert(retained_clears == 1 && retained_sets == 1);
no_install(1);
assert(bootloader_migration_request(nullptr, reply, sizeof(reply)));
assert(retained_clears == 1 && retained_sets == 1); // Duplicates do no work.
now = 1500;
bootloader_migration_tick(true, true);
assert(endf_checks == 1);
assert(power_registers.GPREGRET == mesh::ota::ATTEMPT_MARKER);
} else if (std::strcmp(scenario, "guard-softdevice-status-failure") == 0 ||
std::strcmp(scenario, "guard-marker-read-failure") == 0 ||
std::strcmp(scenario, "guard-clear-failure") == 0 ||
std::strcmp(scenario, "guard-set-failure") == 0) {
power_registers.GPREGRET = 0x37;
if (std::strcmp(scenario, "guard-softdevice-status-failure") == 0) {
softdevice_status = 1;
} else {
softdevice_enabled = true;
if (std::strcmp(scenario, "guard-marker-read-failure") == 0)
retained_get_status = 1;
else if (std::strcmp(scenario, "guard-clear-failure") == 0)
retained_clear_status = 1;
else
retained_set_status = 1;
}
assert(!bootloader_migration_request(nullptr, reply, sizeof(reply)));
assert(std::strstr(reply, "ERR bootloader reboot guard unavailable"));
assert(!bootloader_migration_pending());
no_install(1);
assert(retained_clears == (softdevice_enabled && !retained_get_status ? 1u : 0u));
assert(retained_sets == (retained_set_status ? 1u : 0u));
assert(power_registers.GPREGRET == (retained_set_status ? 0u : 0x37u));
now = 70000;
assert(!bootloader_migration_service_due());
bootloader_migration_tick(true, true);
no_install(1);
status_result(17);
} else if (std::strcmp(scenario, "automatic-interrupted-check") == 0 ||
std::strcmp(scenario, "automatic-interrupted-arm") == 0) {
const bool during_check = std::strcmp(scenario, "automatic-interrupted-check") == 0;
now = 70000;
interrupt_hash = during_check;
bool interrupted = false;
try {
bootloader_migration_tick(true, true);
} catch (const std::runtime_error &error) {
assert(during_check);
assert(std::strcmp(error.what(), "interrupted bootloader check") == 0);
interrupted = true;
}
assert(interrupted == during_check);
assert(bootloader_migration_pending() == !during_check);
assert(power_registers.GPREGRET == mesh::ota::ATTEMPT_MARKER);
assert(durable_lock_present);
assert(endf_checks == 0);
const unsigned initial_reads = softdevice_checks;
interrupt_hash = false;
restart_application();
now = 69999;
assert(!bootloader_migration_service_due());
now = 70000;
assert(!bootloader_migration_service_due());
bootloader_migration_tick(true, true);
assert(!bootloader_migration_pending());
assert(!bootloader_migration_service_due());
assert(power_registers.GPREGRET == mesh::ota::ATTEMPT_MARKER);
no_install(initial_reads);
now = 140000;
bootloader_migration_tick(true, true);
no_install(initial_reads); // A second reset-time check must not repeat.
bootloader_migration_status(reply, sizeof(reply));
assert(std::strstr(reply, "auto=locked for this firmware"));
arm(); // An operator can retry after correcting an interruption's cause.
now = 141500;
bootloader_migration_tick(true, true);
assert(endf_checks == 1);
assert(!bootloader_migration_pending());
} else if (std::strcmp(scenario, "already-installed") == 0) {
power_registers.GPREGRET = 0x37;
candidate_installed = true;
assert(bootloader_migration_request(confirmation, reply, sizeof(reply)));
assert(!bootloader_migration_pending());
@@ -207,10 +730,12 @@ int main(int argc, char **argv) {
now = 2000;
bootloader_migration_tick(true, true);
no_install();
assert(power_registers.GPREGRET == 0x37);
bootloader_migration_status(reply, sizeof(reply));
assert(std::strstr(reply, "already installed"));
} else if (std::strcmp(scenario, "same-version") == 0 ||
std::strcmp(scenario, "newer-version") == 0) {
power_registers.GPREGRET = 0x37;
const uint32_t version = std::strcmp(scenario, "same-version") == 0
? migration_boot_version : 0x02040D01;
seal_manifest(installed_manifest(version));
@@ -229,6 +754,7 @@ int main(int argc, char **argv) {
now += 2000;
bootloader_migration_tick(true, true);
no_install();
assert(power_registers.GPREGRET == 0x37);
}
} else if (std::strcmp(scenario, "easy-update") == 0 ||
std::strcmp(scenario, "older-preview") == 0) {
@@ -239,7 +765,7 @@ int main(int argc, char **argv) {
assert(std::strstr(reply, "OK migration armed"));
now = 1499;
bootloader_migration_tick(true, true);
no_install();
no_install(1);
++now;
bootloader_migration_tick(true, true);
assert(endf_checks == 1);
@@ -279,13 +805,13 @@ int main(int argc, char **argv) {
arm();
now = 1499;
bootloader_migration_tick(true, true);
no_install();
no_install(1);
now = 1500;
bootloader_migration_tick(false, true);
no_install();
no_install(1);
now = 29999;
bootloader_migration_tick(false, true);
no_install();
no_install(1);
assert(bootloader_migration_pending());
bootloader_migration_tick(true, true);
assert(endf_checks == 1);
@@ -309,7 +835,7 @@ int main(int argc, char **argv) {
now = 30000;
bootloader_migration_tick(true, true);
assert(!bootloader_migration_pending());
no_install();
no_install(1);
status_result(15);
bootloader_migration_tick(true, true);
assert(endf_checks == 0);
@@ -318,7 +844,7 @@ int main(int argc, char **argv) {
now = 100;
bootloader_migration_tick(false, false);
assert(!bootloader_migration_pending());
no_install();
no_install(1);
status_result(16);
now = 2000;
bootloader_migration_tick(true, true);
@@ -335,7 +861,7 @@ int main(int argc, char **argv) {
now = 1500;
bootloader_migration_tick(true, false);
assert(!bootloader_migration_pending());
no_install();
no_install(1);
status_result(16);
} else if (std::strcmp(scenario, "stale-retained-result") == 0) {
power_registers.GPREGRET = 0xCC;
@@ -352,14 +878,14 @@ int main(int argc, char **argv) {
bootloader_migration_tick(true, false);
assert(!bootloader_migration_pending());
status_result(16);
assert(power_registers.GPREGRET == 0xCC);
assert(power_registers.GPREGRET == mesh::ota::ATTEMPT_MARKER);
assert(endf_checks == 0);
} else if (std::strcmp(scenario, "clock-wrap") == 0) {
now = UINT32_MAX - 999u;
arm();
now += 1499u;
bootloader_migration_tick(true, true);
no_install();
no_install(1);
++now;
bootloader_migration_tick(true, true);
assert(endf_checks == 1);