mirror of
https://github.com/mikecarper/MeshCore.git
synced 2026-11-04 16:47:15 +00:00
949 lines
39 KiB
C++
949 lines
39 KiB
C++
#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"
|
|
#include "nrf_sdm.h"
|
|
#include "nrf_soc.h"
|
|
|
|
#include <cassert>
|
|
#include <cstdint>
|
|
#include <cstdlib>
|
|
#include <cstring>
|
|
#include <stdexcept>
|
|
#if defined(_WIN32)
|
|
#include <windows.h>
|
|
#else
|
|
#include <sys/mman.h>
|
|
#endif
|
|
|
|
namespace migration_test {
|
|
uint32_t now = 0;
|
|
unsigned endf_checks = 0;
|
|
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 stock_installed = false;
|
|
bool stock_hash_damaged = 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;
|
|
throw std::runtime_error("unexpected MBR command in state-machine test");
|
|
}
|
|
} // 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__
|
|
|
|
const uint8_t migration_candidate[40960] = {};
|
|
const uint8_t migration_candidate_sha256[32] = { 0x01, 0x23, 0x45, 0x67, 0x89, 0xAB, 0xCD, 0xEF };
|
|
const uint8_t migration_legacy_sha256[32] = {};
|
|
// The real registry's stock RAK 0.4.3 image identity; the SHA stub below
|
|
// controls whether that digest is observed, not the cryptographic primitive.
|
|
const uint8_t migration_stock_sha256[1][32] = {{
|
|
0x6c,0xbf,0xa5,0x69,0xa0,0x5c,0xbd,0x23,0x2d,0xab,0xe4,0xa6,0x54,0xab,0xe9,0xc0,
|
|
0x4b,0x7a,0x03,0xab,0x45,0x76,0x15,0x02,0x8d,0x28,0xf2,0xd1,0x32,0x9e,0x58,0xd2}};
|
|
const uint32_t migration_stock_count = 1;
|
|
const bool migration_has_legacy = false;
|
|
const uint32_t migration_app_base = 0x26000, migration_boot_version = 0x02040CFF,
|
|
migration_board_id = 0x239A0029;
|
|
const uint16_t migration_softdevice_fwid = 0xB6;
|
|
const char migration_device_name[16] = "4631_DFU";
|
|
namespace mesh { namespace ota {
|
|
extern "C" {
|
|
uint32_t __flash_arduino_start[1] = {};
|
|
uint32_t __flash_arduino_end[1] = {};
|
|
}
|
|
} }
|
|
|
|
MigrationTestPower power_registers = {};
|
|
MigrationTestPower *NRF_POWER = &power_registers;
|
|
MigrationTestFicr *NRF_FICR = nullptr;
|
|
MigrationTestAcl *NRF_ACL = nullptr;
|
|
MigrationTestUicr *NRF_UICR = nullptr;
|
|
MigrationTestWdt *NRF_WDT = nullptr;
|
|
|
|
uint32_t millis() {
|
|
return migration_test::now;
|
|
}
|
|
|
|
void NVIC_SystemReset() {
|
|
++migration_test::resets;
|
|
throw std::runtime_error("unexpected reset in state-machine test");
|
|
}
|
|
|
|
uint32_t sd_softdevice_is_enabled(uint8_t *enabled) {
|
|
++migration_test::softdevice_checks;
|
|
*enabled = migration_test::softdevice_enabled;
|
|
return migration_test::softdevice_status;
|
|
}
|
|
|
|
uint32_t sd_softdevice_disable() {
|
|
++migration_test::disables;
|
|
throw std::runtime_error("unexpected SoftDevice disable in state-machine test");
|
|
}
|
|
|
|
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) {
|
|
}
|
|
void SHA256::finalize(void *out, size_t size) {
|
|
assert(size == 32);
|
|
// 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::startup_metadata)
|
|
std::memcpy(out, migration_test::application_hash, size);
|
|
else if (migration_test::candidate_installed)
|
|
std::memcpy(out, migration_candidate_sha256, size);
|
|
else if (migration_test::stock_installed) {
|
|
std::memcpy(out, migration_stock_sha256[0], size);
|
|
if (migration_test::stock_hash_damaged) static_cast<uint8_t *>(out)[31] ^= 1;
|
|
}
|
|
}
|
|
|
|
namespace mesh {
|
|
namespace ota {
|
|
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
|
|
|
|
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;
|
|
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);
|
|
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);
|
|
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 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(unsigned softdevice_reads = 0) {
|
|
assert(migration_test::endf_checks == 0);
|
|
assert(migration_test::softdevice_checks == softdevice_reads);
|
|
assert(migration_test::disables == 0);
|
|
assert(migration_test::resets == 0);
|
|
assert(migration_test::copies == 0);
|
|
}
|
|
|
|
void status_result(unsigned result) {
|
|
bootloader_migration_status(reply, sizeof(reply));
|
|
char expected[24];
|
|
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) {
|
|
assert(argc == 2);
|
|
using namespace migration_test;
|
|
#if defined(_WIN32)
|
|
assert(VirtualAlloc(reinterpret_cast<void *>(0xF0000), 0x10000,
|
|
MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE) != nullptr);
|
|
#else
|
|
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, "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);
|
|
assert(!bootloader_migration_request(nullptr, reply, sizeof(reply)));
|
|
assert(!bootloader_migration_pending());
|
|
assert(std::strstr(reply, "not a reviewed exact compatible image"));
|
|
assert(durable_lock_present && store_writes == 1);
|
|
now = 141500;
|
|
bootloader_migration_tick(true, true);
|
|
assert(endf_checks == 0); // Explicit commands cannot bypass source identity.
|
|
} else if (std::strncmp(scenario, "stock-", 6) == 0) {
|
|
auto *boot = reinterpret_cast<uint8_t *>(migration::BOOT_BASE);
|
|
std::memset(boot, 0xFF, migration::BOOT_SIZE);
|
|
std::strcpy(reinterpret_cast<char *>(boot + 0x100), "UF2 Bootloader 0.4.3\r\n");
|
|
stock_installed = true;
|
|
const bool automatic = std::strcmp(scenario, "stock-automatic") == 0;
|
|
const bool manual = std::strcmp(scenario, "stock-manual") == 0;
|
|
if (std::strcmp(scenario, "stock-damaged-hash") == 0) stock_hash_damaged = true;
|
|
else if (std::strcmp(scenario, "stock-recovery") == 0)
|
|
std::strcpy(reinterpret_cast<char *>(boot + 0x100), "UF2 Bootloader R_0x02040BFF\r\n");
|
|
else if (std::strcmp(scenario, "stock-modern-mismatch") == 0) {
|
|
auto *p = installed_manifest(0x02040BFF);
|
|
p[24] = 'X'; seal_manifest(p);
|
|
} else assert(automatic || manual);
|
|
if (automatic) {
|
|
now = 69999; bootloader_migration_tick(true, true);
|
|
assert(!bootloader_migration_pending());
|
|
now = 70000; bootloader_migration_tick(true, true);
|
|
} else {
|
|
const bool accepted = bootloader_migration_request(nullptr, reply, sizeof(reply));
|
|
assert(accepted == manual);
|
|
}
|
|
assert(bootloader_migration_pending() == (automatic || manual));
|
|
if (automatic || manual) {
|
|
assert(durable_lock_present && store_writes == 1);
|
|
assert(power_registers.GPREGRET == mesh::ota::ATTEMPT_MARKER);
|
|
now += 1499; bootloader_migration_tick(true, true);
|
|
assert(bootloader_migration_pending() && endf_checks == 0);
|
|
++now; bootloader_migration_tick(true, true);
|
|
assert(!bootloader_migration_pending() && endf_checks == 1);
|
|
} else {
|
|
assert(!durable_lock_present && store_writes == 0);
|
|
assert(std::strstr(reply, "not a reviewed exact compatible image"));
|
|
now = 70000; bootloader_migration_tick(true, true);
|
|
assert(!bootloader_migration_pending());
|
|
assert(endf_checks == 0);
|
|
}
|
|
} 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());
|
|
assert(std::strstr(reply, "already installed"));
|
|
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));
|
|
// 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();
|
|
assert(power_registers.GPREGRET == 0x37);
|
|
}
|
|
} 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(1);
|
|
++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, "not a reviewed exact compatible image"));
|
|
assert(!std::strstr(reply, "up to date"));
|
|
now = 1500;
|
|
bootloader_migration_tick(true, true);
|
|
// Source rejection happens while the application/SoftDevice stays running.
|
|
assert(endf_checks == 0);
|
|
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)));
|
|
assert(!bootloader_migration_request("0123456789ABCDEF ", reply, sizeof(reply)));
|
|
assert(!bootloader_migration_pending());
|
|
now = 2000;
|
|
bootloader_migration_tick(true, true);
|
|
no_install();
|
|
} else if (std::strcmp(scenario, "grace-and-drain") == 0) {
|
|
arm();
|
|
now = 1499;
|
|
bootloader_migration_tick(true, true);
|
|
no_install(1);
|
|
now = 1500;
|
|
bootloader_migration_tick(false, true);
|
|
no_install(1);
|
|
now = 29999;
|
|
bootloader_migration_tick(false, true);
|
|
no_install(1);
|
|
assert(bootloader_migration_pending());
|
|
bootloader_migration_tick(true, true);
|
|
assert(endf_checks == 1);
|
|
assert(!bootloader_migration_pending());
|
|
assert(disables == 0 && resets == 0 && copies == 0);
|
|
status_result(1);
|
|
} else if (std::strcmp(scenario, "duplicate-arm") == 0) {
|
|
arm();
|
|
now = 1499;
|
|
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);
|
|
assert(endf_checks == 1); // A duplicate must not extend the grace period.
|
|
} else if (std::strcmp(scenario, "timeout") == 0) {
|
|
arm();
|
|
now = 30000;
|
|
bootloader_migration_tick(true, true);
|
|
assert(!bootloader_migration_pending());
|
|
no_install(1);
|
|
status_result(15);
|
|
bootloader_migration_tick(true, true);
|
|
assert(endf_checks == 0);
|
|
} else if (std::strcmp(scenario, "ota-cancel-rearm") == 0) {
|
|
arm();
|
|
now = 100;
|
|
bootloader_migration_tick(false, false);
|
|
assert(!bootloader_migration_pending());
|
|
no_install(1);
|
|
status_result(16);
|
|
now = 2000;
|
|
bootloader_migration_tick(true, true);
|
|
assert(endf_checks == 0); // Becoming idle must not revive cancelled work.
|
|
arm();
|
|
now = 3499;
|
|
bootloader_migration_tick(true, true);
|
|
assert(endf_checks == 0);
|
|
now = 3500;
|
|
bootloader_migration_tick(true, true);
|
|
assert(endf_checks == 1);
|
|
} else if (std::strcmp(scenario, "ota-cancel-at-install") == 0) {
|
|
arm();
|
|
now = 1500;
|
|
bootloader_migration_tick(true, false);
|
|
assert(!bootloader_migration_pending());
|
|
no_install(1);
|
|
status_result(16);
|
|
} else if (std::strcmp(scenario, "stale-retained-result") == 0) {
|
|
power_registers.GPREGRET = 0xCC;
|
|
status_result(12);
|
|
arm();
|
|
status_result(0);
|
|
now = 30000;
|
|
bootloader_migration_tick(true, true);
|
|
assert(!bootloader_migration_pending());
|
|
status_result(15);
|
|
arm();
|
|
status_result(0);
|
|
++now;
|
|
bootloader_migration_tick(true, false);
|
|
assert(!bootloader_migration_pending());
|
|
status_result(16);
|
|
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(1);
|
|
++now;
|
|
bootloader_migration_tick(true, true);
|
|
assert(endf_checks == 1);
|
|
} else {
|
|
throw std::runtime_error("unknown scenario");
|
|
}
|
|
assert(disables == 0 && resets == 0 && copies == 0);
|
|
}
|