Harden LoRa OTA and enable unattended targets
This commit is contained in:
1 parent
8599cf0acb
commit
11aa4bc191
17 files changed
+204
-86
No files matched your search
@@ -808,7 +808,7 @@ static uint16_t make_have1(uint8_t* buf, uint16_t cap, const uint8_t mid[4],
|
||||
}
|
||||
|
||||
// A node must not fetch firmware it can't apply: a catalog row whose codec the platform can't decode is
|
||||
// not fetched. FULL + the platform's delta codec(s) are accepted.
|
||||
// not fetched. Full-image acceptance is platform-selectable (nRF52 single-slot disables it).
|
||||
TEST(OtaTransfer, RejectsIncompatibleCodec) {
|
||||
g_q.clear();
|
||||
OtaManager client; OtaStoreRam<4096> store;
|
||||
@@ -816,7 +816,8 @@ TEST(OtaTransfer, RejectsIncompatibleCodec) {
|
||||
client.begin(SIM_TARGET_ID, sim_send, &to_server);
|
||||
client.set_fetch_store(&store);
|
||||
client.set_autofetch(OtaManager::AUTOFETCH_ANY);
|
||||
client.set_apply_codec(CODEC_DETOOLS_INPLACE); // nRF52-style: accepts only full + in-place
|
||||
client.set_apply_codec(CODEC_DETOOLS_INPLACE);
|
||||
client.set_accept_full(false); // nRF52-style: in-place delta only
|
||||
uint8_t b[64];
|
||||
|
||||
// a SEQUENTIAL delta for our target -> incompatible -> not fetched (stays IDLE)
|
||||
@@ -824,6 +825,11 @@ TEST(OtaTransfer, RejectsIncompatibleCodec) {
|
||||
client.on_message(b, make_have1(b, sizeof(b), midA, SIM_TARGET_ID, 0x01000000, CODEC_DETOOLS_SEQUENTIAL, 0));
|
||||
EXPECT_EQ(client.fetchState(), OtaManager::IDLE);
|
||||
|
||||
// a FULL image cannot be installed in an nRF52 single slot -> do not spend hours fetching it
|
||||
uint8_t midFull[4] = {2,3,4,5};
|
||||
client.on_message(b, make_have1(b, sizeof(b), midFull, SIM_TARGET_ID, 0x01000000, CODEC_FULL, MFLAG_FULL));
|
||||
EXPECT_EQ(client.fetchState(), OtaManager::IDLE);
|
||||
|
||||
// an IN-PLACE delta for our target -> compatible -> begins fetching (requests the manifest)
|
||||
uint8_t midB[4] = {5,6,7,8};
|
||||
client.on_message(b, make_have1(b, sizeof(b), midB, SIM_TARGET_ID, 0x01000000, CODEC_DETOOLS_INPLACE, 0));
|
||||
|
||||
@@ -4,60 +4,61 @@
|
||||
|
||||
using namespace mesh::ota;
|
||||
|
||||
// These lock down the nRF52 (RAK4631) single-slot staging geometry that OtaStoreFlashNrf52::begin()/
|
||||
// These lock down the nRF52 single-slot staging geometry that OtaStoreFlashNrf52::begin()/
|
||||
// reopen() rely on. A received `.mota` is placed bottom-aligned below the filesystem region — ExtraFS
|
||||
// (0xD4000) / InternalFS (0xED000), where the node's user preferences live — and above the running image.
|
||||
// The prefs region is assumed IMMUTABLE (its bytes are outside the served/hashed self-image), so staging
|
||||
// or an in-place apply must never reach into it. If a layout constant or the placement math is edited
|
||||
// inconsistently, these fail here instead of silently corrupting prefs / the app on real hardware.
|
||||
|
||||
static constexpr uint32_t CAP = MOTA_NRF52_FS_START - MOTA_NRF52_APP_BASE; // 0xAE000, 696 KB
|
||||
static constexpr uint32_t APP_V6 = MOTA_NRF52_APP_BASE_S140_V6;
|
||||
static constexpr uint32_t APP_V7 = MOTA_NRF52_APP_BASE_S140_V7;
|
||||
static constexpr uint32_t CAP_V6 = MOTA_NRF52_FS_START - (APP_V6 + MOTA_NRF52_INPLACE_MEMORY); // 88 KB
|
||||
static constexpr uint32_t CAP_V7 = MOTA_NRF52_FS_START - (APP_V7 + MOTA_NRF52_INPLACE_MEMORY); // 84 KB
|
||||
|
||||
// A typical running image (~520 KB) leaves room; the container lands strictly within (app_end, FS_START].
|
||||
TEST(OtaFlashPlan, StagesBelowFilesystemAndAboveApp) {
|
||||
uint32_t app_end = MOTA_NRF52_APP_BASE + 520u * 1024u;
|
||||
uint32_t app_end = APP_V6 + 520u * 1024u;
|
||||
uint32_t start = 0xDEADBEEF;
|
||||
ASSERT_TRUE(mota_nrf52_stage_plan(64u * 1024u, app_end, start));
|
||||
ASSERT_TRUE(mota_nrf52_stage_plan(64u * 1024u, APP_V6, app_end, start));
|
||||
EXPECT_GE(start, app_end); // never overlaps the running image
|
||||
EXPECT_GE(start, APP_V6 + MOTA_NRF52_INPLACE_MEMORY); // never overlaps detools workspace
|
||||
EXPECT_LE(start + 64u * 1024u, MOTA_NRF52_FS_START); // never reaches into ExtraFS/InternalFS/prefs
|
||||
EXPECT_EQ(start % MOTA_NRF52_FLASH_PAGE, 0u); // page-aligned (the flash erase unit)
|
||||
}
|
||||
|
||||
// Bottom-aligned: the container's trailer ends AT FS_START (the bootloader scans downward from there),
|
||||
// so start is the page-aligned FS_START - total_size and sits within one page of the ceiling.
|
||||
TEST(OtaFlashPlan, BottomAlignedTrailerEndsAtCeiling) {
|
||||
// Bottom-aligned: start is the page-aligned FS_START - total_size, so the trailer sits within the
|
||||
// highest page below the ceiling where the bootloader's downward scan finds it.
|
||||
TEST(OtaFlashPlan, BottomAlignedBelowCeiling) {
|
||||
uint32_t start = 0;
|
||||
uint32_t total = 100000;
|
||||
ASSERT_TRUE(mota_nrf52_stage_plan(total, MOTA_NRF52_APP_BASE, start));
|
||||
uint32_t total = 60000;
|
||||
ASSERT_TRUE(mota_nrf52_stage_plan(total, APP_V6, APP_V6, start));
|
||||
EXPECT_EQ(start, (MOTA_NRF52_FS_START - total) & ~(MOTA_NRF52_FLASH_PAGE - 1));
|
||||
EXPECT_LE(start + total, MOTA_NRF52_FS_START);
|
||||
EXPECT_GT(start + total, MOTA_NRF52_FS_START - MOTA_NRF52_FLASH_PAGE); // within one page of the ceiling
|
||||
}
|
||||
|
||||
// An exactly-capacity container (no app below it) fills the whole staging region; one byte more never fits.
|
||||
// An exactly-capacity container fills the region above the decoder workspace; one byte more never fits.
|
||||
TEST(OtaFlashPlan, RejectsOversizedContainer) {
|
||||
uint32_t start = 0;
|
||||
ASSERT_TRUE(mota_nrf52_stage_plan(CAP, MOTA_NRF52_APP_BASE, start));
|
||||
EXPECT_EQ(start, MOTA_NRF52_APP_BASE); // exactly fills [APP_BASE, FS_START)
|
||||
EXPECT_EQ(start + CAP, MOTA_NRF52_FS_START);
|
||||
EXPECT_FALSE(mota_nrf52_stage_plan(CAP + 1, MOTA_NRF52_APP_BASE, start));
|
||||
ASSERT_TRUE(mota_nrf52_stage_plan(CAP_V6, APP_V6, APP_V6, start));
|
||||
EXPECT_EQ(start, APP_V6 + MOTA_NRF52_INPLACE_MEMORY);
|
||||
EXPECT_EQ(start + CAP_V6, MOTA_NRF52_FS_START);
|
||||
EXPECT_FALSE(mota_nrf52_stage_plan(CAP_V6 + 1, APP_V6, APP_V6, start));
|
||||
}
|
||||
|
||||
// A container that would fit below FS_START on its own but overlaps the running image is refused —
|
||||
// the pull fails cleanly rather than corrupting the app that is currently executing.
|
||||
TEST(OtaFlashPlan, RejectsCollisionWithRunningImage) {
|
||||
uint32_t app_end = MOTA_NRF52_FS_START - 8u * 1024u; // only 8 KB free below the ceiling
|
||||
// A running image that exceeds detools' fixed in-place memory can never be a valid delta base.
|
||||
TEST(OtaFlashPlan, RejectsAppLargerThanWorkspace) {
|
||||
uint32_t app_end = APP_V6 + MOTA_NRF52_INPLACE_MEMORY + 1;
|
||||
uint32_t start = 0;
|
||||
EXPECT_TRUE(mota_nrf52_stage_plan(4u * 1024u, app_end, start)); // 4 KB fits in the 8 KB gap
|
||||
EXPECT_GE(start, app_end);
|
||||
EXPECT_FALSE(mota_nrf52_stage_plan(16u * 1024u, app_end, start)); // 16 KB does not
|
||||
EXPECT_FALSE(mota_nrf52_stage_plan(4u * 1024u, APP_V6, app_end, start));
|
||||
}
|
||||
|
||||
// Minimum container is header(8)+trailer(5)=13 bytes; anything smaller is not a container.
|
||||
TEST(OtaFlashPlan, RejectsUndersizedContainer) {
|
||||
uint32_t start = 0;
|
||||
EXPECT_FALSE(mota_nrf52_stage_plan(12, MOTA_NRF52_APP_BASE, start));
|
||||
EXPECT_TRUE(mota_nrf52_stage_plan(13, MOTA_NRF52_APP_BASE, start));
|
||||
EXPECT_FALSE(mota_nrf52_stage_plan(12, APP_V6, APP_V6, start));
|
||||
EXPECT_TRUE(mota_nrf52_stage_plan(13, APP_V6, APP_V6, start));
|
||||
}
|
||||
|
||||
// The user-preferences filesystems (ExtraFS @ 0xD4000, InternalFS @ 0xED000) are entirely ABOVE any
|
||||
@@ -71,15 +72,26 @@ TEST(OtaFlashPlan, PrefsRegionNeverStaged) {
|
||||
EXPECT_LT(EXTRAFS_START, INTERNALFS_START);
|
||||
// the largest possible staged container still ends at the ceiling, never into a filesystem
|
||||
uint32_t start = 0;
|
||||
ASSERT_TRUE(mota_nrf52_stage_plan(CAP, MOTA_NRF52_APP_BASE, start));
|
||||
EXPECT_LE(start + CAP, EXTRAFS_START);
|
||||
ASSERT_TRUE(mota_nrf52_stage_plan(CAP_V6, APP_V6, APP_V6, start));
|
||||
EXPECT_LE(start + CAP_V6, EXTRAFS_START);
|
||||
// and the in-place apply workspace ends below the filesystem too
|
||||
EXPECT_LE(MOTA_NRF52_APP_BASE + MOTA_NRF52_INPLACE_MEMORY, EXTRAFS_START);
|
||||
EXPECT_LE(APP_V6 + MOTA_NRF52_INPLACE_MEMORY, EXTRAFS_START);
|
||||
}
|
||||
|
||||
// S140 v7 moves the app start by one page. Runtime linker-base discovery must leave a correspondingly
|
||||
// smaller but still safe staging region rather than scanning the v6 address and missing EndF.
|
||||
TEST(OtaFlashPlan, SupportsS140V7RuntimeBase) {
|
||||
EXPECT_TRUE(mota_nrf52_layout_valid(APP_V7));
|
||||
EXPECT_EQ(mota_nrf52_stage_capacity(APP_V7), CAP_V7);
|
||||
uint32_t start = 0;
|
||||
ASSERT_TRUE(mota_nrf52_stage_plan(CAP_V7, APP_V7, APP_V7 + 520u * 1024u, start));
|
||||
EXPECT_EQ(start, APP_V7 + MOTA_NRF52_INPLACE_MEMORY);
|
||||
EXPECT_EQ(start + CAP_V7, MOTA_NRF52_FS_START);
|
||||
}
|
||||
|
||||
// out_start is only written on success — a rejected plan must not clobber the caller's variable.
|
||||
TEST(OtaFlashPlan, LeavesOutputUntouchedOnReject) {
|
||||
uint32_t start = 0x1234ABCD;
|
||||
EXPECT_FALSE(mota_nrf52_stage_plan(CAP + 1, MOTA_NRF52_APP_BASE, start));
|
||||
EXPECT_FALSE(mota_nrf52_stage_plan(CAP_V6 + 1, APP_V6, APP_V6, start));
|
||||
EXPECT_EQ(start, 0x1234ABCDu);
|
||||
}
|
||||
Reference in new issue
Block a user