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
@@ -253,9 +253,15 @@ bool handle_ota_command(const char* command, char* reply, mesh::MainBoard& board
|
||||
OtaStore* store; const char* dname;
|
||||
if (strncmp(dst, "flash", 5) == 0) {
|
||||
store = &c.fetch_store; c.fetch_store.clear(); dname = "flash"; validate = false; // seed lives in the folder
|
||||
#if defined(NRF52_PLATFORM)
|
||||
c.manager.set_accept_full(false); // nRF52 flash can install only in-place deltas
|
||||
#endif
|
||||
} else if (strncmp(dst, "folder", 6) == 0) {
|
||||
if (!c.folder_dest) { strcpy(reply, "ERR no folder connected (run motatool serve --tcp/--serial)"); return true; }
|
||||
c.folder_dest->set_mid(selmid); store = c.folder_dest; dname = validate ? "folder+validate" : "folder";
|
||||
#if defined(NRF52_PLATFORM)
|
||||
c.manager.set_accept_full(true); // capture can store a full image; it is not applied
|
||||
#endif
|
||||
} else { strcpy(reply, "ERR destination must be `flash` or `folder`"); return true; }
|
||||
c.manager.reset_session();
|
||||
c.manager.set_fetch_store(store); // stage this pull to the chosen destination
|
||||
@@ -270,6 +276,9 @@ bool handle_ota_command(const char* command, char* reply, mesh::MainBoard& board
|
||||
if (fs != OtaManager::IDLE) mesh::Utils::toHex(midhx, c.manager.fetchManifestId(), 4);
|
||||
c.manager.reset_session(); c.manager.want(0); c.manager.want_mid(nullptr);
|
||||
c.manager.set_fetch_store(&c.fetch_store); // revert to the default flash store (a folder pull switched it)
|
||||
#if defined(NRF52_PLATFORM)
|
||||
c.manager.set_accept_full(false);
|
||||
#endif
|
||||
c.fetch_store.clear(); c.serving = false; c.serve_expected = 0; c.session_started_ms = 0;
|
||||
snprintf(reply, 160, "OK dropped session (was %c mid=%s); slot free for a new pull", fstate_char(fs), midhx);
|
||||
|
||||
|
||||
@@ -40,7 +40,13 @@ namespace ota {
|
||||
class FolderMotaStore; // pull destination over the seeder link (full type only where instantiated/used)
|
||||
|
||||
#ifndef OTA_SERVE_BUF_SIZE
|
||||
#define OTA_SERVE_BUF_SIZE 16384
|
||||
// nRF52 self-serving streams from flash; this buffer is only for the manual `ota dev stage` helper.
|
||||
// Keep it to one flash page so the OTA singleton does not consume another 16 KB of scarce SRAM.
|
||||
#if defined(NRF52_PLATFORM) && defined(OTA_FLASH_STORE)
|
||||
#define OTA_SERVE_BUF_SIZE 4096
|
||||
#else
|
||||
#define OTA_SERVE_BUF_SIZE 16384
|
||||
#endif
|
||||
#endif
|
||||
#ifndef OTA_FETCH_BUF_SIZE
|
||||
#define OTA_FETCH_BUF_SIZE 16384
|
||||
@@ -191,8 +197,10 @@ struct OtaContext {
|
||||
if (hw) { strncpy(hw_id, hw, sizeof(hw_id) - 1); hw_id[sizeof(hw_id) - 1] = 0; }
|
||||
// a node only fetches firmware it can apply: ESP32 A/B -> sequential, nRF52 single-slot -> in-place
|
||||
#if defined(NRF52_PLATFORM)
|
||||
manager.set_accept_full(false); // single-slot bootloader applies deltas only
|
||||
manager.set_apply_codec(CODEC_DETOOLS_INPLACE);
|
||||
#elif defined(ESP32_PLATFORM)
|
||||
manager.set_accept_full(true);
|
||||
manager.set_apply_codec(CODEC_DETOOLS_SEQUENTIAL); // preferred (streams straight to the slot)
|
||||
manager.set_apply_codec2(CODEC_DETOOLS_INPLACE); // also accepted -> a single in-place .mota fits both
|
||||
#endif
|
||||
|
||||
@@ -1,33 +1,41 @@
|
||||
#pragma once
|
||||
|
||||
// Shared OTA flash-layout constants for the nRF52840 (RAK4631) single-slot delta-apply path.
|
||||
// SINGLE SOURCE OF TRUTH — keep byte-identical with the bootloader's src/ota_layout.h.
|
||||
// Shared OTA flash-layout constants for the nRF52840 single-slot delta-apply path.
|
||||
//
|
||||
// The running app occupies [APP_BASE, app_end]; the primary LittleFS (InternalFS) starts at FS_START.
|
||||
// MeshCore stages a verified+approved `.mota` in the free flash below FS_START (bottom-aligned), then
|
||||
// sets GPREGRET_OTA_APPLY and resets; the bootloader scans [APP_BASE, FS_START) for it and applies it
|
||||
// in place. These must match the bootloader and the running SoftDevice's app base.
|
||||
// in place. APP_BASE is obtained from the linker so S140 v6 (0x26000) and v7 (0x27000) both work. The
|
||||
// bootloader independently uses DFU_BANK_0_REGION_START, which resolves to the same address.
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
namespace mesh {
|
||||
namespace ota {
|
||||
|
||||
static const uint32_t MOTA_NRF52_APP_BASE = 0x00026000u; // S140 end (== CODE_REGION_1_START)
|
||||
// Staging ceiling: the lowest filesystem region above the app. RAK4631 companion builds use the
|
||||
// extrafs ldscript with ExtraFS at 0xD4000..0xED000 (and InternalFS at 0xED000), while the repeater
|
||||
// uses the default ldscript (InternalFS at 0xED000, 0xD4000..0xED000 free). 0xD4000 is the safe
|
||||
// universal ceiling for ALL RAK4631 roles: staging below it never touches ExtraFS or InternalFS, and
|
||||
// the app (~520 KB) sits well below 0xD4000 either way.
|
||||
static const uint32_t MOTA_NRF52_APP_BASE_S140_V6 = 0x00026000u;
|
||||
static const uint32_t MOTA_NRF52_APP_BASE_S140_V7 = 0x00027000u;
|
||||
#if defined(NRF52_PLATFORM)
|
||||
extern "C" uint32_t __flash_arduino_start[]; // nrf52_common.ld: ORIGIN(FLASH)
|
||||
inline uint32_t mota_nrf52_app_base() {
|
||||
return (uint32_t)(uintptr_t)__flash_arduino_start;
|
||||
}
|
||||
#else
|
||||
// Native geometry tests have no linker script; default their runtime helper to the v6 layout.
|
||||
inline uint32_t mota_nrf52_app_base() { return MOTA_NRF52_APP_BASE_S140_V6; }
|
||||
#endif
|
||||
// Staging ceiling: the lowest filesystem region above the app. nRF52840 ExtraFS linker scripts place
|
||||
// ExtraFS at 0xD4000..0xED000 (and InternalFS at 0xED000), while default scripts leave that range free.
|
||||
// Staging below 0xD4000 therefore never touches either filesystem.
|
||||
static const uint32_t MOTA_NRF52_FS_START = 0x000D4000u; // ExtraFS start (universal staging ceiling)
|
||||
static const uint32_t MOTA_NRF52_FLASH_PAGE = 4096u;
|
||||
static const uint8_t GPREGRET_OTA_APPLY = 0x6Au; // distinct from DFU magics 0x57/0x4E/0xA8
|
||||
|
||||
// In-place patches are built with --inplace-memory = this (the apply workspace, from APP_BASE up).
|
||||
// It must hold the new image (~520 KB) yet leave the staged mota room below FS_START: workspace ends
|
||||
// at APP_BASE+this = 0xBE000, leaving 0xBE000..0xD4000 (~88 KB) for the staged delta. The bootloader
|
||||
// also bounds writes to < the (scanned) mota start, so a mis-sized memory still fails safe.
|
||||
static const uint32_t MOTA_NRF52_INPLACE_MEMORY = 0x00098000u; // 608 KB (APP_BASE .. 0xBE000)
|
||||
// at APP_BASE+this = 0xBE000 (S140 v6) or 0xBF000 (v7), leaving 88/84 KB for the staged delta. The
|
||||
// bootloader also bounds writes to < the (scanned) mota start, so a mis-sized memory still fails safe.
|
||||
static const uint32_t MOTA_NRF52_INPLACE_MEMORY = 0x00098000u; // 608 KB
|
||||
|
||||
// Bootloader flash region (nRF52840: 39 KB ending just below the CF2/MBR-params pages). The app scans
|
||||
// this for the bootloader capability marker (OtaBlInfo.h) to know whether THIS device's bootloader can
|
||||
@@ -37,28 +45,43 @@ 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_APP_BASE_S140_V6 % MOTA_NRF52_FLASH_PAGE) == 0, "S140 v6 base must be page-aligned");
|
||||
static_assert((MOTA_NRF52_APP_BASE_S140_V7 % MOTA_NRF52_FLASH_PAGE) == 0, "S140 v7 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,
|
||||
static_assert(MOTA_NRF52_APP_BASE_S140_V7 + 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).
|
||||
inline bool mota_nrf52_layout_valid(uint32_t app_base) {
|
||||
return (app_base % MOTA_NRF52_FLASH_PAGE) == 0 && app_base < MOTA_NRF52_FS_START &&
|
||||
(uint64_t)app_base + MOTA_NRF52_INPLACE_MEMORY <= MOTA_NRF52_FS_START;
|
||||
}
|
||||
|
||||
inline uint32_t mota_nrf52_stage_capacity(uint32_t app_base) {
|
||||
return mota_nrf52_layout_valid(app_base)
|
||||
? MOTA_NRF52_FS_START - (app_base + MOTA_NRF52_INPLACE_MEMORY) : 0;
|
||||
}
|
||||
|
||||
// Plan where to stage a received `.mota` of `total_size` bytes. It is placed bottom-aligned within the
|
||||
// highest flash page below FS_START (the bootloader scans downward from there), and it must sit
|
||||
// ENTIRELY above both the running image and the in-place decoder workspace, and below the filesystem
|
||||
// region (ExtraFS/InternalFS — where user prefs live, assumed immutable). Reserving the full workspace
|
||||
// here prevents accepting a large container that the bootloader would later reject as overlapping it.
|
||||
// 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;
|
||||
inline bool mota_nrf52_stage_plan(uint32_t total_size, uint32_t app_base, uint32_t app_end,
|
||||
uint32_t& out_start) {
|
||||
if (!mota_nrf52_layout_valid(app_base) || app_end < app_base ||
|
||||
app_end > app_base + MOTA_NRF52_INPLACE_MEMORY) return false;
|
||||
const uint32_t workspace_end = app_base + MOTA_NRF52_INPLACE_MEMORY;
|
||||
const uint32_t capacity = MOTA_NRF52_FS_START - workspace_end;
|
||||
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
|
||||
if (start < app_end || start < workspace_end) return false; // would overlap app / decoder workspace
|
||||
out_start = start;
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -217,14 +217,17 @@ public:
|
||||
// Coarse clock for source/catalog ages + LRU (the Mesh adapter feeds millis; 0 in host tests is fine).
|
||||
void set_clock(uint32_t ms) { _now_ms = ms; }
|
||||
|
||||
// Codec compatibility: a node only fetches/accepts fw it can actually apply. CODEC_FULL is always
|
||||
// acceptable; the platform's single delta codec is set here (ESP32 A/B -> sequential, nRF52 single-
|
||||
// slot -> in-place). A mismatching `.mota` is rejected at OTA_ADV time, before fetching anything.
|
||||
// Codec compatibility: a node only fetches/accepts firmware it can actually apply. ESP32 A/B accepts
|
||||
// full images; nRF52 single-slot does not and disables them (it requires an in-place delta). A manual
|
||||
// pull to an external folder may temporarily allow full images because it is capture, not install.
|
||||
void set_apply_codec(uint8_t c) { _apply_codec = c; }
|
||||
// A platform may apply MORE than one delta codec (ESP32 does both sequential AND in-place, so a single
|
||||
// in-place `.mota` can target both ESP32 and nRF52). 0xFF = unset.
|
||||
void set_apply_codec2(uint8_t c) { _apply_codec2 = c; }
|
||||
bool codecOk(uint8_t c) const { return c == CODEC_FULL || c == _apply_codec || c == _apply_codec2; }
|
||||
void set_accept_full(bool on) { _accept_full = on; }
|
||||
bool codecOk(uint8_t c) const {
|
||||
return (c == CODEC_FULL && _accept_full) || c == _apply_codec || c == _apply_codec2;
|
||||
}
|
||||
|
||||
// Auto-fetch policy (manual `ota pull` always works regardless): 0=off (discover only), 1=any
|
||||
// compatible own-target advert, 2=only signed adverts. Conservative default = off.
|
||||
@@ -351,6 +354,7 @@ private:
|
||||
bool _have_desired_mid = false;
|
||||
uint8_t _apply_codec = CODEC_DETOOLS_SEQUENTIAL; // platform's delta codec (OtaContext sets it)
|
||||
uint8_t _apply_codec2 = 0xFF; // optional 2nd accepted delta codec (ESP32: in-place)
|
||||
bool _accept_full = true; // false on nRF52 flash (single-slot cannot apply full)
|
||||
uint8_t _seeder_id[4] = {0,0,0,0}; // our node id (pubkey[0:4]) for advert seeder counting
|
||||
uint8_t _autofetch = AUTOFETCH_OFF; // auto-fetch policy (persisted in NodePrefs)
|
||||
uint16_t _checkpoint_blocks = OTA_CHECKPOINT_BLOCKS; // resume checkpoint cadence (persisted)
|
||||
|
||||
@@ -67,8 +67,10 @@ bool ota_self_firmware(SelfFwInfo& out) {
|
||||
// APP_BASE; find_self_firmware() picks the EndF whose stored body_len equals its offset (the running
|
||||
// firmware's own trailer), ignoring any staged `.mota` (which carries its own embedded EndF) higher up.
|
||||
bool ota_self_firmware(SelfFwInfo& out) {
|
||||
const uint8_t* region = (const uint8_t*)(uintptr_t)MOTA_NRF52_APP_BASE;
|
||||
uint32_t region_len = MOTA_NRF52_FS_START - MOTA_NRF52_APP_BASE;
|
||||
const uint32_t app_base = mota_nrf52_app_base();
|
||||
if (!mota_nrf52_layout_valid(app_base)) { out = SelfFwInfo(); return false; }
|
||||
const uint8_t* region = (const uint8_t*)(uintptr_t)app_base;
|
||||
uint32_t region_len = MOTA_NRF52_FS_START - app_base;
|
||||
return find_self_firmware(region, region_len, out, /*verify_body=*/true);
|
||||
}
|
||||
#else
|
||||
@@ -86,8 +88,9 @@ bool ota_self_read(uint32_t off, uint8_t* buf, uint32_t len) {
|
||||
}
|
||||
#elif defined(NRF52_PLATFORM)
|
||||
bool ota_self_read(uint32_t off, uint8_t* buf, uint32_t len) {
|
||||
if ((uint64_t)MOTA_NRF52_APP_BASE + off + len > MOTA_NRF52_FS_START) return false;
|
||||
memcpy(buf, (const uint8_t*)(uintptr_t)(MOTA_NRF52_APP_BASE + off), len);
|
||||
const uint32_t app_base = mota_nrf52_app_base();
|
||||
if (!mota_nrf52_layout_valid(app_base) || (uint64_t)app_base + off + len > MOTA_NRF52_FS_START) return false;
|
||||
memcpy(buf, (const uint8_t*)(uintptr_t)(app_base + off), len);
|
||||
return true;
|
||||
}
|
||||
#else
|
||||
|
||||
@@ -73,14 +73,19 @@ bool OtaStoreFlashNrf52::begin(uint32_t total_size) {
|
||||
clear();
|
||||
|
||||
// never collide with the running application image (its extent comes from its EndF trailer)
|
||||
uint32_t app_end = MOTA_NRF52_APP_BASE;
|
||||
const uint32_t app_base = mota_nrf52_app_base();
|
||||
if (!mota_nrf52_layout_valid(app_base)) return false;
|
||||
uint32_t app_end = app_base;
|
||||
SelfFwInfo fi;
|
||||
if (ota_self_firmware(fi) && fi.valid) app_end = MOTA_NRF52_APP_BASE + fi.image_len;
|
||||
if (ota_self_firmware(fi) && fi.valid) {
|
||||
if ((uint64_t)app_base + fi.image_len > MOTA_NRF52_FS_START) return false;
|
||||
app_end = app_base + fi.image_len;
|
||||
}
|
||||
|
||||
// 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)
|
||||
// Bottom-align below FS_START and reject unless it sits above the running image AND the full detools
|
||||
// workspace (the FS/prefs-safe bounds check; pure + unit-tested in the native OTA suite).
|
||||
uint32_t start;
|
||||
if (!mota_nrf52_stage_plan(total_size, app_end, start)) return false;
|
||||
if (!mota_nrf52_stage_plan(total_size, app_base, app_end, start)) return false;
|
||||
|
||||
_write_start = start;
|
||||
_total = total_size;
|
||||
@@ -156,15 +161,20 @@ void OtaStoreFlashNrf52::checkpoint() {
|
||||
// the first match (highest address = most recent for the common single-container case). The manager then
|
||||
// parses the loaded manifest and validates geometry/root, so a stale leftover is rejected there.
|
||||
bool OtaStoreFlashNrf52::reopen() {
|
||||
uint32_t app_end = MOTA_NRF52_APP_BASE;
|
||||
const uint32_t app_base = mota_nrf52_app_base();
|
||||
if (!mota_nrf52_layout_valid(app_base)) return false;
|
||||
uint32_t app_end = app_base;
|
||||
SelfFwInfo fi;
|
||||
if (ota_self_firmware(fi) && fi.valid) app_end = MOTA_NRF52_APP_BASE + fi.image_len;
|
||||
if (ota_self_firmware(fi) && fi.valid) {
|
||||
if ((uint64_t)app_base + fi.image_len > MOTA_NRF52_FS_START) return false;
|
||||
app_end = app_base + fi.image_len;
|
||||
}
|
||||
for (uint32_t start = align_down(MOTA_NRF52_FS_START - PG, PG); start >= app_end; start -= PG) {
|
||||
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);
|
||||
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;
|
||||
if (!mota_nrf52_stage_plan(total, app_base, 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
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
#include "OtaStore.h"
|
||||
#include "OtaFlashLayout_nrf52.h"
|
||||
|
||||
// Persistent flash-backed OtaStore for nRF52 (RAK4631). Stages the received `.mota` in the free flash
|
||||
// Persistent flash-backed OtaStore for nRF52840. Stages the received `.mota` in the free flash
|
||||
// below the primary LittleFS (FS_START), bottom-aligned so its trailer ends at FS_START and the
|
||||
// bootloader can scan for it. Survives reboot — the whole point — so the bootloader can apply the
|
||||
// staged delta on the next boot.
|
||||
@@ -58,7 +58,7 @@ public:
|
||||
bool begin(uint32_t total_size) override;
|
||||
bool write(uint32_t offset, const uint8_t* data, uint32_t len) override;
|
||||
bool read(uint32_t offset, uint8_t* buf, uint32_t len) const override;
|
||||
uint32_t capacity() const override { return MOTA_NRF52_FS_START - MOTA_NRF52_APP_BASE; }
|
||||
uint32_t capacity() const override { return mota_nrf52_stage_capacity(mota_nrf52_app_base()); }
|
||||
uint32_t staged_size() const override { return _total; }
|
||||
void clear() override { _total = 0; _pay_idx = 0; _flushed = false; _io_ok = true; }
|
||||
bool set_meta_size(uint32_t meta_bytes) override { return meta_bytes <= PG; } // leaves must fit page 0
|
||||
|
||||
Reference in new issue
Block a user