Simplify embedded bootloader updates and skip current versions

This commit is contained in:
mikecarper committed 2026-10-23 17:16:32 -07:00
1 parent d0d32d7a31
commit 881d8e58a5
12 files changed
+456 -22

No files matched your search

+1
View File
@@ -279,6 +279,7 @@ jobs:
python3 -B test/test_sensecap_bootloader_runtime.py -v
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
- 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
+11 -9
View File
@@ -78,17 +78,19 @@ P1 image, not a recovery image.
remote admin CLI:
```text
ota bootloader embedded status
ota bootloader embedded audit
ota bootloader embedded install HASH16
ota bootloader update
```
Replace `HASH16` with the image confirmation shown by `status`. Save the audit
before installation. The acknowledgment means armed; replacement waits for
the reply and queued/in-flight radio transmissions to drain.
3. After reboot, check `get bootloader.ver` and repeat the audit. Confirm 2.4.12
and compare the application, filesystem, settings and SoftDevice hashes.
Check status before repeating a command whose reply was lost.
The command checks the installed bootloader and skips the update if a valid,
matching bootloader is already 2.4.12 or newer. Repeating the command while
it is armed is harmless. An `armed` reply means the update is queued until
the CLI reply and queued/in-flight radio transmissions drain.
3. After reboot, check `get bootloader.ver`. Optional diagnostics are
`ota bootloader embedded status` and `ota bootloader embedded audit`; save an
audit before installation to compare the application, filesystem, settings
and SoftDevice hashes afterward. The older
`ota bootloader embedded install HASH16` command remains available with the
confirmation shown by `status`.
The bridge validates the live application, candidate hash, installed identity,
chip/layout, MBR addresses, watchdog and flash protection before copying. A
+1 -1
View File
@@ -51,7 +51,7 @@ def qualify(directory, manifest_path):
"manifest_sha256": MANIFEST_SHA256,
"legacy_sha256": metadata.get("legacy_sha256"),
"app_offset": offset, "image_bytes": BOOT_SIZE,
"activation": "ota bootloader embedded install HASH16",
"activation": "ota bootloader update",
}
manifest_path.write_text(json.dumps(manifest, indent=2, sort_keys=True) + "\n")
return manifest["embedded_bootloader"]
+23 -6
View File
@@ -68,6 +68,15 @@ void hash_region(uint32_t address, uint32_t size, uint8_t output[32]) {
hash.finalize(output, 32);
}
bool installed_up_to_date() {
OtaBootloaderIdentity installed;
return ota_bootloader_identity_from_image(
reinterpret_cast<const uint8_t *>(migration::BOOT_BASE), migration::BOOT_SIZE, installed) &&
migration::installed_up_to_date(installed, migration_board_id, migration_device_name,
migration_boot_version, mota_nrf52_app_base(),
migration_softdevice_fwid);
}
uint8_t retained_result() {
if (result_is_current) return last_result;
uint8_t enabled = 0;
@@ -186,20 +195,28 @@ 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,
pending ? "armed" : already_installed ? "already installed" : "idle", retained_result());
pending ? "armed" : already_installed ? "already installed" :
installed_up_to_date() ? "up to date" : "idle", retained_result());
}
bool bootloader_migration_request(const char *confirmation, char *reply, size_t capacity) {
char hash[17];
hex8(migration_candidate_sha256, hash);
if (pending) {
snprintf(reply, capacity, "ERR bootloader migration is already armed");
return false;
}
if (strcmp(confirmation, hash) != 0) {
if (confirmation && strcmp(confirmation, hash) != 0) {
snprintf(reply, capacity, "ERR confirm exact image: ota bootloader embedded install %s", hash);
return false;
}
if (pending) {
snprintf(reply, capacity, "OK bootloader update already armed");
return true;
}
if (installed_up_to_date()) {
already_installed = false;
last_result = 0;
result_is_current = true;
snprintf(reply, capacity, "OK bootloader already up to date");
return true;
}
// Bootloader diagnostics already read this memory-mapped region while
// SoftDevice runs. Detect a no-op before stopping it so BLE stays usable.
uint8_t digest[32];
+1
View File
@@ -6,6 +6,7 @@
namespace mesh {
namespace ota {
bool bootloader_migration_pending();
// 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);
void bootloader_migration_audit(char *reply, size_t capacity);
@@ -101,6 +101,16 @@ 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) {
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) &&
installed.boot_version >= version;
}
} // namespace migration
} // namespace ota
} // namespace mesh
+11
View File
@@ -1031,6 +1031,17 @@ static __attribute__((noinline)) bool handle_control_command(
} else if (is_cmd(a, "bootloader|blupdate", &rest)) {
#if defined(OTA_BOOTLOADER_MIGRATION)
const char* update_args = nullptr;
if (is_cmd(rest, "update", &update_args)) {
if (*update_args) {
strcpy(reply, "ERR usage: ota bootloader update");
} else if (!c.embeddedBootloaderIdle()) {
strcpy(reply, "ERR another OTA operation is active");
} else {
bootloader_migration_request(nullptr, reply, 160);
}
return true;
}
const char* embedded = nullptr;
if (is_cmd(rest, "embedded", &embedded)) {
if (*embedded == 0 || strcmp(embedded, "status") == 0) {
+209
View File
@@ -0,0 +1,209 @@
"""Run the production bootloader CLI branch with and without an embedded image."""
import os
from pathlib import Path
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 <cstdio>
#include <cstring>
#include <string>
namespace mesh { struct Utils {
static bool fromHex(uint8_t* out, size_t size, const char* text) {
for (size_t i = 0; i < size; i++) {
unsigned value;
if (sscanf(text + i * 2, "%2x", &value) != 1) return false;
out[i] = value;
}
return true;
}
static void toHex(char* out, const uint8_t* bytes, size_t size) {
for (size_t i = 0; i < size; i++) sprintf(out + i * 2, "%02X", bytes[i]);
}
}; }
struct OtaBootloaderIdentity {
bool present = true, crc_ok = true;
uint32_t board_id = 0x12345678, crc32 = 0;
const char* device_name = "TEST_DFU";
};
struct OtaBlCaps { unsigned apply_abi = 3, storage_flags = 1; };
static uint32_t ota_bootloader_target_id(const OtaBootloaderIdentity& id) { return id.board_id; }
static bool ota_bootloader_self_update_caps_valid(const OtaBlCaps&) { return true; }
struct MotaManifest {
uint8_t image_hash[8] = {};
bool is_bootloader() const { return true; }
};
struct OtaManager {
enum FetchState { IDLE, WANT_MANIFEST, FETCHING, COMPLETE, FAILED, PAUSED };
FetchState state = IDLE;
bool serve_jobs = false, manifest_jobs = false;
uint8_t mid[4] = {0x12, 0x34, 0x56, 0x78};
FetchState fetchState() const { return state; }
bool pendingServeJobs() const { return serve_jobs; }
bool pendingManifestJobs() const { return manifest_jobs; }
const uint8_t* fetchManifestId() const { return mid; }
bool fetched_is_bootloader() const { return true; }
};
struct OtaContext {
OtaManager manager;
struct Store { unsigned staged_size() const { return 100; } } fetch_store;
bool apply_pending = false, bootloader_apply_pending = false, fetch_to_folder = false;
bool sd_cache_fetching = false;
unsigned staged_installs = 0;
OtaBootloaderIdentity identity;
OtaBlCaps caps;
bool sdCacheFetching() const { return sd_cache_fetching; }
@IDLE@
const OtaBootloaderIdentity& bootloaderIdentity() const { return identity; }
const OtaBlCaps& bootloaderUpdateCaps() const { return caps; }
bool apply_fetched_bootloader(const uint8_t* mid, const uint8_t* hash, char* reply) {
assert(mid[0] == 0x12 && mid[3] == 0x78 && hash[0] == 0x01 && hash[7] == 0xEF);
staged_installs++;
strcpy(reply, "staged install");
return true;
}
};
static bool staged_manifest(OtaContext&, MotaManifest&) { return true; }
static unsigned requests = 0, statuses = 0, audits = 0;
static bool automatic = false;
static std::string confirmation;
static void bootloader_migration_status(char* out, size_t) { statuses++; strcpy(out, "STATUS"); }
static void bootloader_migration_audit(char* out, size_t) { audits++; strcpy(out, "AUDIT"); }
static bool bootloader_migration_request(const char* hash, char* out, size_t) {
requests++;
automatic = hash == nullptr;
confirmation = hash ? hash : "";
strcpy(out, automatic ? "AUTO" : "HASH");
return true;
}
@HELPERS@
static bool command(const char* a, char* reply, OtaContext& c) {
const char* rest = nullptr;
@COMMAND@
else { return false; }
return true;
}
static void expect(const char* text, const char* expected, OtaContext& c) {
char reply[160] = {};
assert(command(text, reply, c));
if (strcmp(reply, expected)) {
fprintf(stderr, "%s: expected '%s', got '%s'\n", text, expected, reply);
assert(false);
}
}
int main() {
OtaContext c;
#if defined(OTA_BOOTLOADER_MIGRATION)
expect("bootloader update ignored", "ERR usage: ota bootloader update", c);
assert(requests == 0);
expect("bootloader update", "AUTO", c);
assert(requests == 1 && automatic);
expect("blupdate update ", "AUTO", c);
assert(requests == 2 && automatic);
for (auto state : {OtaManager::IDLE, OtaManager::COMPLETE, OtaManager::FAILED}) {
c.manager.state = state;
expect("bootloader update", "AUTO", c);
}
for (auto state : {OtaManager::FETCHING, OtaManager::WANT_MANIFEST, OtaManager::PAUSED}) {
c.manager.state = state;
expect("bootloader update", "ERR another OTA operation is active", c);
}
c.manager.state = OtaManager::IDLE;
for (bool* flag : {&c.apply_pending, &c.bootloader_apply_pending, &c.manager.serve_jobs,
&c.manager.manifest_jobs, &c.sd_cache_fetching}) {
*flag = true;
expect("bootloader update", "ERR another OTA operation is active", c);
expect("bootloader embedded install 0123456789ABCDEF",
"ERR another OTA operation is active", c);
*flag = false;
}
assert(requests == 5);
expect("bootloader embedded", "STATUS", c);
expect("bootloader embedded status", "STATUS", c);
expect("bootloader embedded audit", "AUDIT", c);
assert(statuses == 2 && audits == 1 && requests == 5);
expect("bootloader embedded install 0123456789ABCDEF", "HASH", c);
assert(requests == 6 && !automatic && confirmation == "0123456789ABCDEF");
expect("bootloader embedded install", "HASH", c);
assert(requests == 7 && !automatic && confirmation.empty());
expect("bootloader embedded install 0123456789ABCDEF extra", "HASH", c);
assert(requests == 8 && !automatic && confirmation == "0123456789ABCDEF extra");
#else
// Ordinary images keep their existing staged-update command surface.
#if defined(NRF52_PLATFORM) && !defined(OTA_SEEDER_ONLY) && defined(OTA_INTERNAL_BOOTLOADER_UPDATE)
expect("bootloader update", "ERR usage: ota bootloader install <MID8> <HASH16>", c);
expect("bootloader update ignored", "ERR usage: ota bootloader install <MID8> <HASH16>", c);
#else
expect("bootloader update", "ERR this build cannot update its bootloader over LoRa", c);
#endif
assert(requests == 0);
#endif
#if defined(NRF52_PLATFORM) && !defined(OTA_SEEDER_ONLY) && defined(OTA_INTERNAL_BOOTLOADER_UPDATE)
expect("bootloader updatejunk", "ERR usage: ota bootloader install <MID8> <HASH16>", c);
expect("bootloader install", "ERR usage: ota bootloader install <MID8> <HASH16>", c);
c.manager.state = OtaManager::COMPLETE;
expect("bootloader install 12345678 0123456789ABCDEF", "OK | staged install", c);
assert(c.staged_installs == 1);
expect("bootloader install 12345678 0123456789ABCDEF extra",
"ERR usage: ota bootloader install <MID8> <HASH16>", c);
assert(c.staged_installs == 1);
#else
expect("bootloader install 12345678 0123456789ABCDEF",
"ERR this build cannot update its bootloader over LoRa", c);
assert(c.staged_installs == 0);
#endif
}
'''
def harness():
cli = (ROOT / "src/helpers/ota/OtaCli.cpp").read_text(encoding="utf-8")
context = (ROOT / "src/helpers/ota/OtaContext.h").read_text(encoding="utf-8")
return (SOURCE.replace("@IDLE@", body(context, "bool embeddedBootloaderIdle() const"))
.replace("@HELPERS@", "\n".join(body(cli, signature) for signature in (
"static bool is_cmd(", "static bool parse_hex_exact(")))
.replace("@COMMAND@", body(cli, 'if (is_cmd(a, "bootloader|blupdate", &rest))')))
class BootloaderUpdateCliTest(unittest.TestCase):
def test_production_dispatch_keeps_embedded_and_staged_updates_separate(self):
compiler = shutil.which("g++") or shutil.which("clang++")
if compiler is None:
self.skipTest("a host C++17 compiler is required")
variants = {
"embedded": ("OTA_BOOTLOADER_MIGRATION", "NRF52_PLATFORM",
"OTA_INTERNAL_BOOTLOADER_UPDATE", "OTA_SD_STORE"),
"staged": ("NRF52_PLATFORM", "OTA_INTERNAL_BOOTLOADER_UPDATE"),
"unsupported": (),
"seeder": ("NRF52_PLATFORM", "OTA_INTERNAL_BOOTLOADER_UPDATE", "OTA_SEEDER_ONLY"),
}
sanitize = [] if os.name == "nt" else [
"-fsanitize=address,undefined", "-fno-sanitize-recover=all", "-fno-pie", "-no-pie"]
with tempfile.TemporaryDirectory(prefix="bootloader-update-cli-") as directory:
source = Path(directory) / "cli.cpp"
source.write_text(harness(), encoding="ascii")
for name, macros in variants.items():
with self.subTest(variant=name):
binary = Path(directory) / (name + (".exe" if os.name == "nt" else ""))
built = subprocess.run([
compiler, "-std=c++17", "-O1", "-g", "-Wall", "-Wextra", "-Werror",
"-Wno-unused-function", "-Wno-unused-parameter", *sanitize,
*["-D" + macro + "=1" for macro in macros], str(source), "-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()
+1 -1
View File
@@ -179,7 +179,7 @@ class PackagedApplicationTests(unittest.TestCase):
self.assertEqual(result["release"], "2.4.12")
self.assertEqual(result["profile"], self.profile["id"])
self.assertEqual(result["candidate_sha256"], hashlib.sha256(self.candidate).hexdigest())
self.assertEqual(result["activation"], "ota bootloader embedded install HASH16")
self.assertEqual(result["activation"], "ota bootloader update")
self.patches["select_profile"].assert_called_once_with(
{"profiles": [self.profile]}, TARGET)
self.patches["validate_candidate_zip"].assert_called_once_with(
+40 -1
View File
@@ -100,7 +100,43 @@ int main(int argc,char **argv) {
auto v7=identity(0x02040CFF,0x27000,0x123);
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],"monotonic")) {
if (!std::strcmp(argv[1],"up-to-date")) {
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));
installed.boot_version=0x02040CFF;
assert(installed_up_to_date(installed,0x239A0029,name,0x02040CFF,0x26000,0xB6));
installed.boot_version=0x02040D01; // Even a later preview must not be downgraded.
assert(installed_up_to_date(installed,0x239A0029,name,0x02040CFF,0x26000,0xB6));
bad=installed; bad.present=false;
assert(!installed_up_to_date(bad,0x239A0029,name,0x02040CFF,0x26000,0xB6));
bad=installed; bad.crc_ok=false;
assert(!installed_up_to_date(bad,0x239A0029,name,0x02040CFF,0x26000,0xB6));
bad=installed; bad.board_id^=1;
assert(!installed_up_to_date(bad,0x239A0029,name,0x02040CFF,0x26000,0xB6));
bad=installed; bad.device_name[0]='X';
assert(!installed_up_to_date(bad,0x239A0029,name,0x02040CFF,0x26000,0xB6));
bad=installed; bad.image_start+=4096;
assert(!installed_up_to_date(bad,0x239A0029,name,0x02040CFF,0x26000,0xB6));
bad=installed; --bad.image_size;
assert(!installed_up_to_date(bad,0x239A0029,name,0x02040CFF,0x26000,0xB6));
bad=installed; bad.continuity_present=false;
assert(!installed_up_to_date(bad,0x239A0029,name,0x02040CFF,0x26000,0xB6));
bad=installed; bad.softdevice_family=132;
assert(!installed_up_to_date(bad,0x239A0029,name,0x02040CFF,0x26000,0xB6));
bad=installed; bad.softdevice_fwid=0x123;
assert(!installed_up_to_date(bad,0x239A0029,name,0x02040CFF,0x26000,0xB6));
bad=installed; bad.app_base=0x27000;
assert(!installed_up_to_date(bad,0x239A0029,name,0x02040CFF,0x26000,0xB6));
bad=installed; bad.layout_abi=2;
assert(!installed_up_to_date(bad,0x239A0029,name,0x02040CFF,0x26000,0xB6));
bad=installed; bad.compat_flags=1;
assert(!installed_up_to_date(bad,0x239A0029,name,0x02040CFF,0x26000,0xB6));
bad=installed; bad.boot_version=0;
assert(!installed_up_to_date(bad,0x239A0029,name,0x02040CFF,0x26000,0xB6));
bad=installed; bad.boot_version=UINT32_MAX;
assert(!installed_up_to_date(bad,0x239A0029,name,0x02040CFF,0x26000,0xB6));
} else if (!std::strcmp(argv[1],"monotonic")) {
installed.boot_version=candidate.boot_version;
assert(!installed_can_migrate(installed,candidate,true,true,0x26000,0xB6));
++installed.boot_version;
@@ -159,6 +195,9 @@ class MigrationPolicyTests(unittest.TestCase):
def test_modern_loader_requires_strictly_newer_candidate(self):
self.scenario('monotonic')
def test_installed_version_noop_requires_exact_identity_and_platform(self):
self.scenario('up-to-date')
def test_crc_exact_identity_and_runtime_platform(self):
self.scenario('identity')
@@ -2,9 +2,10 @@
"""Exercise the production migration arm/tick state machine with host stubs.
The ARM SVC adapter is replaced by a failing trap, and the EndF stub refuses
installation before hardware access. 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.
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.
"""
import os
from pathlib import Path
@@ -29,6 +30,7 @@ class MigrationRuntimeTests(unittest.TestCase):
subprocess.run([
os.environ.get('CXX', 'g++'), '-std=c++17', '-O2', '-Wall', '-Wextra',
'-DOTA_BOOTLOADER_MIGRATION', '-DNRF52_PLATFORM', '-DENABLE_OTA',
'-DMOTA_NRF52_TEST_APP_BASE=0x26000',
'-fsanitize=address,undefined', '-fno-sanitize-recover=all', '-fno-pie', '-no-pie',
'-I', str(STUBS), '-I', str(ROOT / 'src'), '-I', str(ROOT / 'src/helpers/ota'),
str(source), '-o', str(binary),
@@ -44,6 +46,24 @@ class MigrationRuntimeTests(unittest.TestCase):
def test_already_installed_preserves_running_application(self):
self.run_scenario('already-installed')
def test_easy_update_needs_no_hash_and_preserves_install_preflight(self):
self.run_scenario('easy-update')
def test_same_version_is_idle_before_and_after_repeated_requests(self):
self.run_scenario('same-version')
def test_newer_preview_is_idle_and_cannot_be_downgraded(self):
self.run_scenario('newer-version')
def test_older_preview_is_still_eligible_for_the_stable_update(self):
self.run_scenario('older-preview')
def test_version_noop_requires_valid_matching_identity_and_continuity(self):
for mutation in ('board', 'name', 'family', 'fwid', 'app', 'abi',
'continuity', 'crc'):
with self.subTest(mutation=mutation):
self.run_scenario('invalid-version-' + mutation)
def test_negative_control_noop_must_not_arm_or_stop_softdevice(self):
driver = (ROOT / 'src/helpers/ota/BootloaderMigration.cpp').read_text()
changed = driver.replace('if (memcmp(digest, migration_candidate_sha256, sizeof(digest)) == 0) {',
@@ -60,6 +80,21 @@ class MigrationRuntimeTests(unittest.TestCase):
self.assertNotEqual(result.returncode, 0)
self.assertIn('!bootloader_migration_pending()', result.stderr)
def test_negative_control_version_noop_must_not_queue_an_install(self):
driver = (ROOT / 'src/helpers/ota/BootloaderMigration.cpp').read_text()
changed = driver.replace('if (installed_up_to_date()) {', 'if (false) {')
self.assertNotEqual(driver, changed)
harness = (STUBS / 'runtime.cpp').read_text().replace(
'#include "helpers/ota/BootloaderMigration.cpp"', changed)
with tempfile.TemporaryDirectory(prefix='migration-version-negative-') as directory:
directory = Path(directory)
source = directory / 'runtime.cpp'
source.write_text(harness)
binary = self.compile(directory, source)
result = subprocess.run([str(binary), 'same-version'], capture_output=True, text=True)
self.assertNotEqual(result.returncode, 0)
self.assertIn('!bootloader_migration_pending()', result.stderr)
def test_grace_and_radio_drain_are_both_required(self):
self.run_scenario('grace-and-drain')
+110 -1
View File
@@ -1,6 +1,11 @@
#include "Arduino.h"
#include "BootloaderMigrationResource.h"
#include "SHA256.h"
// The test app-base override bypasses the target linker's declarations.
namespace mesh { namespace ota {
extern "C" uint32_t __flash_arduino_end[];
extern "C" const uint8_t g_meshcore_internal_extrafs __attribute__((weak));
} }
#include "helpers/ota/OtaSelf.h"
#include "nrf.h"
#include "nrf_mbr.h"
@@ -119,6 +124,48 @@ using namespace mesh::ota;
const char confirmation[] = "0123456789ABCDEF";
char reply[160];
void wr16(uint8_t *p, uint16_t value) {
p[0] = value;
p[1] = value >> 8;
}
void wr32(uint8_t *p, uint32_t value) {
wr16(p, value);
wr16(p + 2, value >> 16);
}
uint8_t *installed_manifest(uint32_t version) {
uint8_t *boot = reinterpret_cast<uint8_t *>(migration::BOOT_BASE);
std::memset(boot, 0xFF, migration::BOOT_SIZE);
uint8_t *p = boot + OTA_BOOT_CANDIDATE_MANIFEST_OFFSET;
wr32(p, OTA_BOOT_MANIFEST_MAGIC0);
wr32(p + 4, OTA_BOOT_MANIFEST_MAGIC1);
wr16(p + 8, 1);
wr16(p + 10, 44);
wr32(p + 12, migration::BOOT_BASE);
wr32(p + 16, migration::BOOT_SIZE);
wr32(p + 20, migration_board_id);
std::memcpy(p + 24, migration_device_name, 16);
wr32(p + 44, OTA_BOOT_CONTINUITY_MAGIC0);
wr32(p + 48, OTA_BOOT_CONTINUITY_MAGIC1);
wr16(p + 52, 2);
wr16(p + 54, 32);
wr32(p + 56, version);
wr16(p + 60, 140);
wr16(p + 62, migration_softdevice_fwid);
wr32(p + 64, migration_app_base);
wr16(p + 68, 1);
wr16(p + 70, 0);
wr32(p + 72, 0);
return p;
}
void seal_manifest(uint8_t *p) {
const uint8_t *boot = reinterpret_cast<const uint8_t *>(migration::BOOT_BASE);
wr32(p + 40, ota_boot_image_crc32(boot, migration::BOOT_SIZE,
OTA_BOOT_CANDIDATE_MANIFEST_OFFSET + 40));
}
void arm() {
assert(bootloader_migration_request(confirmation, reply, sizeof(reply)));
assert(bootloader_migration_pending());
@@ -162,6 +209,64 @@ int main(int argc, char **argv) {
no_install();
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) {
const uint32_t version = std::strcmp(scenario, "same-version") == 0
? migration_boot_version : 0x02040D01;
seal_manifest(installed_manifest(version));
// Live status has to detect this before any request, as it would after reboot.
bootloader_migration_status(reply, sizeof(reply));
assert(std::strstr(reply, "up to date"));
assert(!bootloader_migration_pending());
assert(!bootloader_migration_request("0123456789ABCDE0", reply, sizeof(reply)));
assert(!bootloader_migration_pending());
softdevice_checks = 0; // Status may read the retained diagnostic result.
const char *tokens[] = {nullptr, confirmation};
for (const char *token : tokens) {
assert(bootloader_migration_request(token, reply, sizeof(reply)));
assert(!bootloader_migration_pending());
assert(std::strstr(reply, "up to date"));
now += 2000;
bootloader_migration_tick(true, true);
no_install();
}
} else if (std::strcmp(scenario, "easy-update") == 0 ||
std::strcmp(scenario, "older-preview") == 0) {
if (std::strcmp(scenario, "older-preview") == 0)
seal_manifest(installed_manifest(0x02040CFE));
assert(bootloader_migration_request(nullptr, reply, sizeof(reply)));
assert(bootloader_migration_pending());
assert(std::strstr(reply, "OK migration armed"));
now = 1499;
bootloader_migration_tick(true, true);
no_install();
++now;
bootloader_migration_tick(true, true);
assert(endf_checks == 1);
assert(!bootloader_migration_pending());
} else if (std::strncmp(scenario, "invalid-version-", 16) == 0) {
uint8_t *p = installed_manifest(0x02040DFF);
const char *mutation = scenario + 16;
if (std::strcmp(mutation, "board") == 0) wr32(p + 20, migration_board_id ^ 1);
else if (std::strcmp(mutation, "name") == 0) p[24] = 'X';
else if (std::strcmp(mutation, "family") == 0) wr16(p + 60, 132);
else if (std::strcmp(mutation, "fwid") == 0) wr16(p + 62, 0x123);
else if (std::strcmp(mutation, "app") == 0) wr32(p + 64, 0x27000);
else if (std::strcmp(mutation, "abi") == 0) wr16(p + 68, 2);
else if (std::strcmp(mutation, "continuity") == 0) std::memset(p + 44, 0xFF, 32);
else assert(std::strcmp(mutation, "crc") == 0);
seal_manifest(p);
if (std::strcmp(mutation, "crc") == 0) p[40] ^= 1;
bootloader_migration_status(reply, sizeof(reply));
assert(!std::strstr(reply, "up to date"));
assert(bootloader_migration_request(nullptr, reply, sizeof(reply)));
assert(bootloader_migration_pending());
assert(!std::strstr(reply, "up to date"));
now = 1500;
bootloader_migration_tick(true, true);
// The production install preflight still runs, and the EndF stub refuses it.
assert(endf_checks == 1);
assert(!bootloader_migration_pending());
} else if (std::strcmp(scenario, "wrong-confirmation") == 0) {
assert(!bootloader_migration_request("0123456789ABCDE0", reply, sizeof(reply)));
assert(!bootloader_migration_request("0123456789abcdef", reply, sizeof(reply)));
@@ -190,7 +295,11 @@ int main(int argc, char **argv) {
} else if (std::strcmp(scenario, "duplicate-arm") == 0) {
arm();
now = 1499;
assert(!bootloader_migration_request(confirmation, reply, sizeof(reply)));
assert(!bootloader_migration_request("0123456789ABCDE0", reply, sizeof(reply)));
assert(bootloader_migration_pending());
assert(bootloader_migration_request(nullptr, reply, sizeof(reply)));
assert(std::strstr(reply, "OK bootloader update already armed"));
assert(bootloader_migration_request(confirmation, reply, sizeof(reply)));
assert(std::strstr(reply, "already armed"));
now = 1500;
bootloader_migration_tick(true, true);