Merge remote-tracking branch 'origin/keymindCascade' into keymindCascade
This commit is contained in:
commit
170c1761d0
21 files changed
+1658
-41
No files matched your search
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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, ¤t) != 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.
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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(
|
||||
|
||||
@@ -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()
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
|
||||
Reference in new issue
Block a user