mirror of
https://github.com/vk496/MeshCore.git
synced 2026-10-24 08:22:26 +00:00
ota: nRF52 mOTA flash-layout safety net + tests
Route both begin() and reopen() through a pure mota_nrf52_stage_plan() that bottom-aligns a received .mota below the filesystem region (trailer ending at FS_START, where the bootloader scans) and refuses any size that would overrun that ceiling or overlap the running image — the single place the FS/prefs-safe bounds are enforced. Add compile-time static_asserts pinning the flash-layout ordering (app < staging ceiling < bootloader; in-place workspace ends at/below the ceiling) so an inconsistent constant fails the build instead of silently corrupting user prefs. Cover the pure planner in test/test_ota/test_ota_flashplan.cpp.
This commit is contained in:
1 parent
49820b57aa
commit
4a601486ac
3 files changed
+120
-7
No files matched your search
@@ -35,5 +35,33 @@ static const uint32_t MOTA_NRF52_INPLACE_MEMORY = 0x00098000u; // 608 KB (APP_B
|
||||
static const uint32_t MOTA_NRF52_BL_START = 0x000F4000u;
|
||||
static const uint32_t MOTA_NRF52_BL_END = 0x000FE000u;
|
||||
|
||||
// Compile-time layout-ordering invariants. If a constant above is edited inconsistently these fail the
|
||||
// BUILD rather than silently letting a stage/apply corrupt the filesystem (user prefs) or the app.
|
||||
static_assert((MOTA_NRF52_APP_BASE % MOTA_NRF52_FLASH_PAGE) == 0, "APP_BASE must be page-aligned");
|
||||
static_assert((MOTA_NRF52_FS_START % MOTA_NRF52_FLASH_PAGE) == 0, "FS_START must be page-aligned");
|
||||
static_assert(MOTA_NRF52_APP_BASE < MOTA_NRF52_FS_START, "app must precede the staging ceiling");
|
||||
static_assert(MOTA_NRF52_FS_START < MOTA_NRF52_BL_START, "staging (+FS) must end below the bootloader");
|
||||
static_assert(MOTA_NRF52_BL_START < MOTA_NRF52_BL_END, "bootloader region must be non-empty");
|
||||
// The in-place apply workspace [APP_BASE, APP_BASE+INPLACE_MEMORY) must end at/below the staging ceiling,
|
||||
// so an in-place apply never writes into ExtraFS/InternalFS (where user prefs live).
|
||||
static_assert(MOTA_NRF52_APP_BASE + MOTA_NRF52_INPLACE_MEMORY <= MOTA_NRF52_FS_START,
|
||||
"in-place apply workspace must end at or below the staging ceiling");
|
||||
|
||||
// Plan where to stage a received `.mota` of `total_size` bytes. It is placed bottom-aligned so its
|
||||
// 5-byte trailer ends exactly at FS_START (the bootloader scans downward from there), and it must sit
|
||||
// ENTIRELY within [app_end, FS_START): above the running image (`app_end` = APP_BASE + its EndF image_len)
|
||||
// and below the filesystem region (ExtraFS/InternalFS — where user prefs live, assumed immutable).
|
||||
// Returns false (and leaves out_start untouched) if it does not fit. This is the SINGLE place the FS
|
||||
// ceiling + app-collision bounds are enforced; begin()/reopen() both go through it. Pure — no flash I/O —
|
||||
// so it is unit-tested natively in test/test_ota/test_ota_flashplan.cpp.
|
||||
inline bool mota_nrf52_stage_plan(uint32_t total_size, uint32_t app_end, uint32_t& out_start) {
|
||||
const uint32_t capacity = MOTA_NRF52_FS_START - MOTA_NRF52_APP_BASE;
|
||||
if (total_size < 13 || total_size > capacity) return false; // 13 = header(8)+trailer(5); must fit below FS
|
||||
uint32_t start = (MOTA_NRF52_FS_START - total_size) & ~(MOTA_NRF52_FLASH_PAGE - 1); // bottom-align down
|
||||
if (start < app_end) return false; // would overlap the running image
|
||||
out_start = start;
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace ota
|
||||
} // namespace mesh
|
||||
@@ -56,16 +56,16 @@ uint8_t* OtaStoreFlashNrf52::write_slot(uint32_t pos) {
|
||||
|
||||
bool OtaStoreFlashNrf52::begin(uint32_t total_size) {
|
||||
clear();
|
||||
if (total_size < 13 || total_size > capacity()) return false; // 13 = header(8) + trailer(5)
|
||||
|
||||
// bottom-align against FS_START so the trailer ends exactly at FS_START (bootloader scans for it)
|
||||
uint32_t start = align_down(MOTA_NRF52_FS_START - total_size, PG);
|
||||
|
||||
// never collide with the running application image (its extent comes from its EndF trailer)
|
||||
uint32_t app_end = MOTA_NRF52_APP_BASE;
|
||||
SelfFwInfo fi;
|
||||
if (ota_self_firmware(fi) && fi.valid) app_end = MOTA_NRF52_APP_BASE + fi.image_len;
|
||||
if (start < app_end) return false;
|
||||
|
||||
// bottom-align below FS_START + reject if it won't fit above the running image (the FS/prefs-safe
|
||||
// bounds check; pure + unit-tested in test/test_ota/test_ota_flashplan.cpp)
|
||||
uint32_t start;
|
||||
if (!mota_nrf52_stage_plan(total_size, app_end, start)) return false;
|
||||
|
||||
_write_start = start;
|
||||
_total = total_size;
|
||||
@@ -139,8 +139,8 @@ bool OtaStoreFlashNrf52::reopen() {
|
||||
const uint8_t* p = (const uint8_t*)(uintptr_t)start;
|
||||
if (memcmp(p, MOTA_MAGIC, 4) != 0) continue;
|
||||
uint32_t total = rd_u32le(p + 4);
|
||||
if (total < 13 || total > capacity()) continue;
|
||||
if (align_down(MOTA_NRF52_FS_START - total, PG) != start) continue; // must match begin()'s placement
|
||||
uint32_t want; // must be valid + placed exactly where begin() would have staged it (same bounds fn)
|
||||
if (!mota_nrf52_stage_plan(total, app_end, want) || want != start) continue;
|
||||
_write_start = start;
|
||||
_total = total;
|
||||
memcpy(_meta_page, p, PG); // load page 0 (header+manifest+leaves) into RAM to continue
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include "helpers/ota/OtaFlashLayout_nrf52.h"
|
||||
|
||||
using namespace mesh::ota;
|
||||
|
||||
// These lock down the nRF52 (RAK4631) 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
|
||||
|
||||
// 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 start = 0xDEADBEEF;
|
||||
ASSERT_TRUE(mota_nrf52_stage_plan(64u * 1024u, app_end, start));
|
||||
EXPECT_GE(start, app_end); // never overlaps the running image
|
||||
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) {
|
||||
uint32_t start = 0;
|
||||
uint32_t total = 100000;
|
||||
ASSERT_TRUE(mota_nrf52_stage_plan(total, MOTA_NRF52_APP_BASE, 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.
|
||||
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));
|
||||
}
|
||||
|
||||
// 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
|
||||
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
|
||||
}
|
||||
|
||||
// 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));
|
||||
}
|
||||
|
||||
// The user-preferences filesystems (ExtraFS @ 0xD4000, InternalFS @ 0xED000) are entirely ABOVE any
|
||||
// staged container AND above the in-place apply workspace — the immutability the shared-firmware hashes
|
||||
// depend on. Hard-code the FS addresses here (independent of the layout header) so a future edit to
|
||||
// FS_START that drifts into a filesystem is caught.
|
||||
TEST(OtaFlashPlan, PrefsRegionNeverStaged) {
|
||||
const uint32_t EXTRAFS_START = 0xD4000u; // companion ExtraFS (CustomLFS(0xD4000, 0x19000))
|
||||
const uint32_t INTERNALFS_START = 0xED000u; // primary LittleFS (holds /com_prefs)
|
||||
EXPECT_LE(MOTA_NRF52_FS_START, EXTRAFS_START); // staging ceiling at/below the first filesystem
|
||||
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);
|
||||
// and the in-place apply workspace ends below the filesystem too
|
||||
EXPECT_LE(MOTA_NRF52_APP_BASE + MOTA_NRF52_INPLACE_MEMORY, EXTRAFS_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_EQ(start, 0x1234ABCDu);
|
||||
}
|
||||
Reference in new issue
Block a user