273 lines
12 KiB
C++
273 lines
12 KiB
C++
extern "C" void meshRegulatorCleanReset() __attribute__((weak));
|
|
#include <algorithm>
|
|
#include <cstdint>
|
|
#include <cstdio>
|
|
#include <cstdlib>
|
|
#include <cstring>
|
|
#include <deque>
|
|
#include <vector>
|
|
#if defined(_WIN32)
|
|
#include <windows.h>
|
|
#else
|
|
#include <sys/mman.h>
|
|
#endif
|
|
#include <helpers/ota/OtaManager.h>
|
|
#include <Utils.h>
|
|
|
|
static bool g_fail_next_flash_write = false;
|
|
static uint32_t g_flash_writes = 0;
|
|
extern "C" int flash_nrf5x_write(uint32_t address, const void* data, uint32_t length) {
|
|
++g_flash_writes;
|
|
if (g_fail_next_flash_write) { g_fail_next_flash_write = false; return -1; }
|
|
memcpy(reinterpret_cast<void*>(uintptr_t(address)), data, length);
|
|
return int(length);
|
|
}
|
|
extern "C" void flash_nrf5x_flush(void) {}
|
|
|
|
#include "@STORE_CPP@"
|
|
|
|
#define CHECK(condition) do { if (!(condition)) { \
|
|
fprintf(stderr, "FAIL line %d: %s\n", __LINE__, #condition); exit(1); \
|
|
} } while (0)
|
|
|
|
using namespace mesh::ota;
|
|
@MAPPINGS@
|
|
|
|
namespace mesh { namespace ota {
|
|
bool ota_self_firmware(SelfFwInfo& out) {
|
|
out = SelfFwInfo(); out.valid = true; out.image_len = 0x80000u; return true;
|
|
}
|
|
|
|
struct CountingStore : OtaStoreFlashNrf52 {
|
|
mutable unsigned reads = 0;
|
|
unsigned reopens = 0;
|
|
bool read(uint32_t offset, uint8_t* data, uint32_t length) const override {
|
|
++reads; return OtaStoreFlashNrf52::read(offset, data, length);
|
|
}
|
|
bool reopenFor(const uint8_t* mid, uint32_t target) override {
|
|
++reopens; return OtaStoreFlashNrf52::reopenFor(mid, target);
|
|
}
|
|
};
|
|
|
|
struct Context {
|
|
CountingStore fetch_store;
|
|
OtaManager manager;
|
|
OtaManager sender;
|
|
bool apply_pending = false, bootloader_apply_pending = false;
|
|
uint32_t apply_at = 0x12345678u, apply_hard = 0x23456789u;
|
|
};
|
|
@HELPERS@
|
|
static bool resume_cli(const char* d, char* reply, Context& c) {
|
|
@RESUME@
|
|
return true;
|
|
}
|
|
|
|
// The successful signature/allowlist approval boundary is seeded through the
|
|
// real store API. Only physical reset/reason/register operations are adapted.
|
|
static OtaStoreFlashNrf52* g_nrf52_apply_store = nullptr;
|
|
#if defined(OTA_RAK_AUTO_STORE)
|
|
static uint8_t g_nrf52_external_handoff = GPREGRET2_OTA_STAGE_QSPI;
|
|
#endif
|
|
static uint8_t request_marker = 0, source_marker = 0;
|
|
static unsigned reset_count = 0;
|
|
static bool ota_nrf52_clear_reset_reasons() { return true; }
|
|
static void ota_nrf52_set_reset_handoff(uint8_t request, uint8_t source) {
|
|
request_marker = request; source_marker = source;
|
|
}
|
|
static void NVIC_SystemReset() { ++reset_count; }
|
|
@REBOOT@
|
|
} }
|
|
|
|
struct Packet { OtaManager* destination; std::vector<uint8_t> bytes; };
|
|
static std::deque<Packet> packets;
|
|
static bool send_packet(void* context, const uint8_t* bytes, uint16_t len, bool) {
|
|
packets.push_back({static_cast<OtaManager*>(context), {bytes, bytes + len}});
|
|
return true;
|
|
}
|
|
|
|
static std::vector<uint8_t> container(uint32_t total) {
|
|
const uint32_t blocks = (total - 210u + 1027u) / 1028u;
|
|
const uint32_t payload_size = total - 210u - blocks * 4u;
|
|
CHECK((payload_size + 1023u) / 1024u == blocks);
|
|
std::vector<uint8_t> bytes(total, 0);
|
|
memcpy(bytes.data(), MOTA_MAGIC, sizeof(MOTA_MAGIC));
|
|
wr_u32le(bytes.data() + 4, total);
|
|
bytes[8] = MOTA_APP_FORMAT_VER;
|
|
bytes[8 + 2] = HASH_ALGO_SHA256;
|
|
wr_u32le(bytes.data() + 8 + 3, 0x12345678u);
|
|
wr_u32le(bytes.data() + 8 + 7, 0x01170109u);
|
|
wr_u32le(bytes.data() + 8 + 11, 0x81000u);
|
|
wr_u32le(bytes.data() + 8 + 15, payload_size);
|
|
bytes[8 + 19] = 10;
|
|
bytes[8 + 56] = CODEC_DETOOLS_INPLACE;
|
|
uint8_t* const leaves = bytes.data() + 8u + MOTA_MFL;
|
|
uint8_t* const payload = leaves + blocks * 4u;
|
|
for (uint32_t i = 0; i < payload_size; ++i) payload[i] = uint8_t(i * 37u + 11u);
|
|
for (uint32_t i = 0; i < blocks; ++i)
|
|
merkle_leaf(leaves + i * 4u, payload + i * 1024u,
|
|
std::min(1024u, payload_size - i * 1024u));
|
|
merkle_root(bytes.data() + 8 + 20, leaves, blocks);
|
|
memcpy(bytes.data() + total - 5, MOTA_TRAILER, 5);
|
|
MotaManifest manifest;
|
|
CHECK(mota_parse(bytes.data(), bytes.size(), manifest));
|
|
CHECK(mota_check_root(manifest) && mota_check_payload(manifest));
|
|
return bytes;
|
|
}
|
|
|
|
static void fetch_complete(Context& c, const std::vector<uint8_t>& bytes) {
|
|
OtaManager& sender = c.sender;
|
|
packets.clear();
|
|
sender.begin(0, send_packet, &c.manager);
|
|
c.manager.begin(0x12345678u, send_packet, &sender);
|
|
c.manager.set_apply_codec(CODEC_DETOOLS_INPLACE);
|
|
c.manager.set_fetch_store(&c.fetch_store);
|
|
CHECK(sender.serve(bytes.data(), bytes.size()));
|
|
CHECK(c.manager.pull(bytes.data() + 8 + 20, 0x12345678u) == OtaManager::PULL_STARTED);
|
|
for (uint32_t tick = 0; tick < 200000 && c.manager.fetchState() != OtaManager::COMPLETE; ++tick) {
|
|
const uint32_t now = tick * 100u;
|
|
c.manager.set_clock(now); sender.set_clock(now);
|
|
c.manager.loop(); sender.loop();
|
|
c.manager.serviceEgress(); sender.serviceEgress();
|
|
if (!packets.empty()) {
|
|
auto packet = std::move(packets.front()); packets.pop_front();
|
|
packet.destination->on_message(packet.bytes.data(), packet.bytes.size());
|
|
}
|
|
}
|
|
packets.clear();
|
|
CHECK(c.manager.fetchState() == OtaManager::COMPLETE);
|
|
CHECK(c.manager.blocksHave() == c.manager.blocksTotal());
|
|
CHECK(c.fetch_store.staged_size() == bytes.size());
|
|
// The sender remains owned by Context, so callback context stays live across
|
|
// the CLI resume and subsequent pure-flash verification controls.
|
|
}
|
|
|
|
static void approve(Context& c) {
|
|
uint8_t hash[32];
|
|
for (unsigned i = 0; i < sizeof(hash); ++i) hash[i] = uint8_t(i + 1u);
|
|
CHECK(c.fetch_store.approve_for_application(hash));
|
|
g_nrf52_apply_store = &c.fetch_store;
|
|
}
|
|
|
|
static void arm(Context& c, unsigned flags) {
|
|
c.apply_pending = (flags & 1u) != 0;
|
|
c.bootloader_apply_pending = (flags & 2u) != 0;
|
|
}
|
|
|
|
static void baseline_loss() {
|
|
reset_target_memory();
|
|
Context c;
|
|
const auto bytes = container(MOTA_NRF52_HYBRID_RAM_SIZE + MOTA_NRF52_FLASH_PAGE + 123u);
|
|
fetch_complete(c, bytes);
|
|
CHECK(c.fetch_store.is_hybrid());
|
|
approve(c); arm(c, 1);
|
|
ota_reboot_to_apply();
|
|
CHECK(request_marker == GPREGRET_OTA_APPLY && source_marker == GPREGRET2_OTA_STAGE_HYBRID);
|
|
CHECK(mota_hybrid_auth_valid(reinterpret_cast<const uint8_t*>(MOTA_HYBRID_AUTH_ADDR)));
|
|
char command[32], reply[160];
|
|
char hex[9]; mesh::Utils::toHex(hex, bytes.data() + 8 + 20, 4);
|
|
snprintf(command, sizeof(command), "resume %s", hex);
|
|
const unsigned reopens = c.fetch_store.reopens;
|
|
CHECK(resume_cli(command, reply, c));
|
|
CHECK(strncmp(reply, "ERR resume:", 11) == 0);
|
|
CHECK(c.fetch_store.reopens == reopens + 1);
|
|
CHECK(c.apply_pending && !c.bootloader_apply_pending);
|
|
CHECK(g_nrf52_apply_store == &c.fetch_store);
|
|
CHECK(!c.fetch_store.is_hybrid() && c.fetch_store.staged_size() == 0);
|
|
CHECK(!c.fetch_store.publish_hybrid_handoff());
|
|
ota_reboot_to_apply();
|
|
CHECK(request_marker == GPREGRET_OTA_APPLY && source_marker == GPREGRET2_OTA_STAGE_EXPANDED);
|
|
printf("BASELINE CONFIRMED: real COMPLETE hybrid -> failed resume -> hybrid/descriptor lost; armed pointer retained; reboot source A6 -> ED\n");
|
|
}
|
|
|
|
static unsigned guarded_cases() {
|
|
unsigned cases = 0;
|
|
for (uint32_t total : {3072u, 4097u, MOTA_NRF52_HYBRID_RAM_SIZE + MOTA_NRF52_FLASH_PAGE + 123u})
|
|
for (unsigned flags : {1u, 2u, 3u})
|
|
for (unsigned argument = 0; argument < 5; ++argument) {
|
|
reset_target_memory();
|
|
Context c;
|
|
const auto bytes = container(total);
|
|
fetch_complete(c, bytes); approve(c); arm(c, flags);
|
|
const bool hybrid = c.fetch_store.is_hybrid();
|
|
const uint32_t start = c.fetch_store.write_start(), flash_len = c.fetch_store.flash_len();
|
|
const uint32_t ram_len = c.fetch_store.ram_len(), have = c.manager.blocksHave();
|
|
const uint32_t blocks = c.manager.blocksTotal(), sent = c.manager.packetsSent();
|
|
const unsigned reopens = c.fetch_store.reopens, reads = c.fetch_store.reads, writes = g_flash_writes;
|
|
if (hybrid) CHECK(c.fetch_store.publish_hybrid_handoff());
|
|
std::vector<uint8_t> auth(MOTA_HYBRID_AUTH_LEN);
|
|
memcpy(auth.data(), reinterpret_cast<const void*>(MOTA_HYBRID_AUTH_ADDR), auth.size());
|
|
std::vector<uint8_t> backing(total);
|
|
CHECK(c.fetch_store.OtaStoreFlashNrf52::read(0, backing.data(), total));
|
|
char command[48], reply[160], hex[9];
|
|
mesh::Utils::toHex(hex, bytes.data() + 8 + 20, 4);
|
|
if (argument == 0) snprintf(command, sizeof(command), "resume %s", hex);
|
|
else strcpy(command, argument == 1 ? "resume" : argument == 2 ? "resume malformed" : argument == 3 ? "resume 00112233" : "resume 00112233 trailing");
|
|
CHECK(resume_cli(command, reply, c));
|
|
CHECK(strcmp(reply, "ERR another update is already armed") == 0);
|
|
CHECK(c.fetch_store.reopens == reopens && c.fetch_store.reads == reads && g_flash_writes == writes);
|
|
CHECK(c.apply_pending == bool(flags & 1u) && c.bootloader_apply_pending == bool(flags & 2u));
|
|
CHECK(c.apply_at == 0x12345678u && c.apply_hard == 0x23456789u);
|
|
CHECK(c.manager.fetchState() == OtaManager::COMPLETE && c.manager.blocksHave() == have);
|
|
CHECK(c.manager.blocksTotal() == blocks && c.manager.packetsSent() == sent);
|
|
CHECK(memcmp(c.manager.fetchManifestId(), bytes.data() + 8 + 20, 4) == 0);
|
|
CHECK(c.fetch_store.is_hybrid() == hybrid && c.fetch_store.staged_size() == total);
|
|
CHECK(c.fetch_store.write_start() == start && c.fetch_store.flash_len() == flash_len && c.fetch_store.ram_len() == ram_len);
|
|
CHECK(g_nrf52_apply_store == &c.fetch_store);
|
|
std::vector<uint8_t> after(total);
|
|
CHECK(c.fetch_store.OtaStoreFlashNrf52::read(0, after.data(), total));
|
|
CHECK(after == backing);
|
|
CHECK(memcmp(auth.data(), reinterpret_cast<const void*>(MOTA_HYBRID_AUTH_ADDR), auth.size()) == 0);
|
|
if (hybrid) CHECK(c.fetch_store.publish_hybrid_handoff());
|
|
ota_reboot_to_apply();
|
|
CHECK(request_marker == GPREGRET_OTA_APPLY);
|
|
CHECK(source_marker == (hybrid ? GPREGRET2_OTA_STAGE_HYBRID : GPREGRET2_OTA_STAGE_EXPANDED));
|
|
++cases;
|
|
}
|
|
return cases;
|
|
}
|
|
|
|
static unsigned healthy_cases() {
|
|
unsigned cases = 0;
|
|
for (uint32_t total : {3072u, 4097u})
|
|
for (bool no_argument : {false, true}) {
|
|
reset_target_memory();
|
|
Context c;
|
|
const auto bytes = container(total);
|
|
fetch_complete(c, bytes);
|
|
char command[32], reply[160], hex[9];
|
|
mesh::Utils::toHex(hex, bytes.data() + 8 + 20, 4);
|
|
if (no_argument) strcpy(command, "resume");
|
|
else snprintf(command, sizeof(command), "resume %s", hex);
|
|
const unsigned reopens = c.fetch_store.reopens;
|
|
CHECK(resume_cli(command, reply, c));
|
|
CHECK(c.fetch_store.reopens == reopens + 1);
|
|
if (total <= MOTA_NRF52_FLASH_PAGE) {
|
|
CHECK(strncmp(reply, "OK resume:", 10) == 0);
|
|
CHECK(c.manager.fetchState() == OtaManager::VERIFYING_STAGED);
|
|
for (unsigned guard = 0; guard < 100 && c.manager.fetchState() == OtaManager::VERIFYING_STAGED; ++guard) c.manager.loop();
|
|
CHECK(c.manager.fetchState() == OtaManager::COMPLETE);
|
|
CHECK(c.manager.blocksHave() == c.manager.blocksTotal());
|
|
} else {
|
|
CHECK(strncmp(reply, "ERR resume:", 11) == 0);
|
|
CHECK(!c.fetch_store.is_hybrid() && c.fetch_store.staged_size() == 0);
|
|
}
|
|
CHECK(!c.apply_pending && !c.bootloader_apply_pending);
|
|
++cases;
|
|
}
|
|
for (const char* command : {"resume", "resume malformed", "resume 00112233 trailing"}) {
|
|
reset_target_memory(); Context c;
|
|
c.manager.begin(0x12345678u, nullptr, nullptr); c.manager.set_fetch_store(&c.fetch_store);
|
|
char reply[160]; CHECK(resume_cli(command, reply, c));
|
|
CHECK(strncmp(reply, "ERR usage:", 10) == 0 && c.fetch_store.reopens == 0);
|
|
++cases;
|
|
}
|
|
return cases;
|
|
}
|
|
|
|
int main(int argc, char** argv) {
|
|
map_target_region(FLASH_MAP_START, FLASH_MAP_SIZE);
|
|
map_target_region(RAM_MAP_START, RAM_MAP_SIZE);
|
|
if (argc > 1 && strcmp(argv[1], "baseline") == 0) baseline_loss();
|
|
else printf("PASS: %u armed cases; %u healthy controls\n", guarded_cases(), healthy_cases());
|
|
}
|