Simplify embedded bootloader updates and skip current versions
This commit is contained in:
1 parent
d0d32d7a31
commit
881d8e58a5
12 files changed
+456
-22
No files matched your search
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"]
|
||||
|
||||
@@ -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];
|
||||
|
||||
@@ -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
|
||||
@@ -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) {
|
||||
|
||||
@@ -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()
|
||||
@@ -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(
|
||||
|
||||
@@ -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')
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in new issue
Block a user