Add shared-slot nRF52 bootloader updates
This commit is contained in:
1 parent
b7c9e37d09
commit
3de5fddf47
43 files changed
+1647
-211
No files matched your search
+2
-2
@@ -70,8 +70,8 @@ does not reflect the GoogleTest count -- run the built binary directly
|
||||
| `test_user_gpio` | `src/helpers/UserGpio.cpp`, `UserGpioReplyTracker.h` | board-approved pins, get/set/reset, timed nonblocking transitions, duplicate suppression, rollover, and completion-reply routing |
|
||||
| `test_utf8_helpers` | `src/helpers/UTF8Helpers.h` | byte-limit truncation at complete code-point boundaries and rejection of malformed or truncated UTF-8 |
|
||||
| `test_wifi_ota_seeder_policy` | `src/helpers/WiFiOtaSeederPolicy.h`, `WiFiOtaSeederStatus.h` | listener state versus network availability, serial/TCP folder ownership, detach detection, and bounded status formatting |
|
||||
| `test_ota` | `src/helpers/ota/` | v2 application/v3 bootloader parser separation; XIAO identity, vector, capability, explicit-confirmation and no-autofetch gates; container and EndF integrity; protocol codecs; transfer, resume, and layered apply safety; adaptive 2-to-4 block-request window growth and stall contraction; active-transfer priority classification |
|
||||
| `test_trace_retry` | `src/Mesh.cpp` retry and relay policy | opaque OTA relay behavior during TempRadio; background discovery priority; immediate primary transfer relay, receive-delay bypass, fast CAD retry, and no generic flood retry; trace and non-OTA flood retry timing |
|
||||
| `test_ota` | `src/helpers/ota/` | v2 application/v3 bootloader parser separation; legacy XIAO and generic embedded identity, vector, capability, explicit-confirmation, codec-isolation, shared-slot placement/no-EndF erase gates, and no-autofetch gates; container and EndF integrity; protocol codecs; transfer, resume, and layered apply safety; adaptive 2-to-4 block-request window growth and stall contraction; active-transfer priority classification |
|
||||
| `test_trace_retry` | `src/Mesh.cpp` retry and relay policy | app-v2 and boot-v3 traffic sharing `PAYLOAD_TYPE_OTA=0x0C` and the TempRadio suspend policy; opaque OTA relay behavior; background discovery priority; immediate primary transfer relay, receive-delay bypass, fast CAD retry, and no generic flood retry; trace and non-OTA flood retry timing |
|
||||
| `test_utils` | `src/Utils.cpp` | `Utils::toHex` (upstream) |
|
||||
|
||||
## Conventions (and how to add a suite)
|
||||
|
||||
@@ -24,6 +24,8 @@ extern "C" {
|
||||
|
||||
using namespace mesh::ota;
|
||||
|
||||
static std::vector<uint8_t> boot_manifest_bytes();
|
||||
|
||||
TEST(OtaBootResult, AcceptsOnlyOtafixApplyDiagnostics) {
|
||||
EXPECT_EQ(ota_nrf52_boot_result_or_zero(0x90), 0x90);
|
||||
EXPECT_EQ(ota_nrf52_boot_result_or_zero(0x9F), 0x9F);
|
||||
@@ -41,9 +43,77 @@ TEST(OtaBootResult, AcceptsOnlyOtafixApplyDiagnostics) {
|
||||
EXPECT_EQ(ota_nrf52_boot_result_or_zero(0x51), 0x00); // QSPI handoff
|
||||
EXPECT_EQ(ota_nrf52_boot_result_or_zero(0xD4), 0x00); // legacy handoff
|
||||
EXPECT_EQ(ota_nrf52_boot_result_or_zero(0xED), 0x00); // expanded handoff
|
||||
EXPECT_EQ(ota_nrf52_boot_result_or_zero(0xE0), 0x00); // retired handoff remains non-result
|
||||
EXPECT_EQ(ota_nrf52_boot_result_or_zero(0xBD), 0x00);
|
||||
}
|
||||
|
||||
TEST(OtaBootPackage, GenericIdentityUsesBoardAndNameForCollisionSafeTarget) {
|
||||
const uint32_t shared_board_id = 0x239A0029u;
|
||||
const uint8_t rak3401_name[16] = {'3','4','0','1','_','D','F','U',0,0,0,0,0,0,0,0};
|
||||
const uint8_t rak4631_name[16] = {'4','6','3','1','_','D','F','U',0,0,0,0,0,0,0,0};
|
||||
uint8_t hw3401[32], hw4631[32];
|
||||
ASSERT_TRUE(ota_bootloader_hw_id(shared_board_id, rak3401_name, hw3401));
|
||||
ASSERT_TRUE(ota_bootloader_hw_id(shared_board_id, rak4631_name, hw4631));
|
||||
EXPECT_EQ(0, memcmp(hw3401, "NRF_BL_239A0029_3401_DFU", 24));
|
||||
EXPECT_EQ(0, memcmp(hw4631, "NRF_BL_239A0029_4631_DFU", 24));
|
||||
EXPECT_NE(0, memcmp(hw3401, hw4631, sizeof(hw3401)));
|
||||
EXPECT_EQ(ota_bootloader_target_id(shared_board_id, rak3401_name), 0x23818A80u);
|
||||
EXPECT_EQ(ota_bootloader_target_id(shared_board_id, rak4631_name), 0x2D0DF000u);
|
||||
|
||||
uint8_t invalid[16] = {'B','A','D',0,'T','A','I','L',0,0,0,0,0,0,0,0};
|
||||
EXPECT_FALSE(ota_bootloader_device_name_valid(shared_board_id, invalid));
|
||||
memset(invalid, 'A', sizeof(invalid));
|
||||
EXPECT_FALSE(ota_bootloader_device_name_valid(shared_board_id, invalid));
|
||||
EXPECT_FALSE(ota_bootloader_board_id_valid(0));
|
||||
EXPECT_FALSE(ota_bootloader_board_id_valid(UINT32_MAX));
|
||||
|
||||
auto raw = boot_manifest_bytes();
|
||||
wr_u32le(raw.data() + 3, ota_bootloader_target_id(shared_board_id, rak3401_name));
|
||||
memcpy(raw.data() + 57, hw3401, sizeof(hw3401));
|
||||
MotaManifest manifest;
|
||||
ASSERT_TRUE(mota_parse_manifest(raw.data(), raw.size(), manifest));
|
||||
OtaBootloaderIdentity installed;
|
||||
installed.present = installed.crc_ok = true;
|
||||
installed.image_start = OTA_BOOT_IMAGE_START;
|
||||
installed.image_size = OTA_BOOT_IMAGE_SIZE;
|
||||
installed.board_id = shared_board_id;
|
||||
memcpy(installed.device_name, rak3401_name, sizeof(rak3401_name));
|
||||
uint8_t mid[4] = {0x11,0x22,0x33,0x44};
|
||||
uint8_t hash8[8]; memcpy(hash8, manifest.image_hash, sizeof(hash8));
|
||||
EXPECT_EQ(ota_bootloader_confirmation_gate(manifest, installed, mid, mid, hash8),
|
||||
OTA_BOOT_CONFIRM_OK);
|
||||
wr_u32le(raw.data() + 3, shared_board_id); // raw USB ID is not the generic wire target
|
||||
ASSERT_TRUE(mota_parse_manifest(raw.data(), raw.size(), manifest));
|
||||
EXPECT_EQ(ota_bootloader_confirmation_gate(manifest, installed, mid, mid, hash8),
|
||||
OTA_BOOT_CONFIRM_TARGET);
|
||||
|
||||
struct KnownIdentity { uint32_t board_id; const char* name; uint32_t target_id; };
|
||||
const KnownIdentity known[] = {
|
||||
{0x239A0071u, "TOWER_V2_OTA", 0x1150F50Eu},
|
||||
{0x239A0071u, "T096_DFU", 0x42354C85u},
|
||||
{0x239A0071u, "T1_DFU", 0xFC556FFCu},
|
||||
{0x239A0071u, "T114_DFU", 0x0C3F2902u},
|
||||
{0x239A0071u, "MESH_POCKET_OTA",0x059277F4u},
|
||||
{0x239A00B3u, "KeepteenLT1_OTA",0xDB2E7B51u},
|
||||
{0x239A0029u, "MX25_DFU", 0x026AA982u},
|
||||
{0x239A00B3u, "PROM_DFU", 0xAF79E8CCu},
|
||||
{0x28860057u, "T1KE_DFU", 0xE6F5F03Fu},
|
||||
{0x239A00DAu, "TNM3_DFU", 0x0CA41DB2u},
|
||||
{0x239A0029u, "3401_DFU", 0x23818A80u},
|
||||
{0x239A0029u, "4631_DFU", 0x2D0DF000u},
|
||||
{0x239A0029u, "RTAG_DFU", 0xC72E9C9Cu},
|
||||
};
|
||||
uint32_t targets[sizeof(known) / sizeof(known[0])] = {0};
|
||||
for (size_t i = 0; i < sizeof(known) / sizeof(known[0]); i++) {
|
||||
uint8_t name[16] = {0};
|
||||
ASSERT_LT(strlen(known[i].name), sizeof(name));
|
||||
memcpy(name, known[i].name, strlen(known[i].name));
|
||||
targets[i] = ota_bootloader_target_id(known[i].board_id, name);
|
||||
EXPECT_EQ(targets[i], known[i].target_id);
|
||||
for (size_t j = 0; j < i; j++) EXPECT_NE(targets[i], targets[j]);
|
||||
}
|
||||
}
|
||||
|
||||
static std::vector<uint8_t> boot_manifest_bytes() {
|
||||
std::vector<uint8_t> m(MOTA_MFL, 0);
|
||||
m[0] = MOTA_BOOT_FORMAT_VER;
|
||||
@@ -146,6 +216,27 @@ TEST(OtaBootPackage, EmbeddedIdentityVectorCapsAndExplicitConfirmationGate) {
|
||||
marker[15] = 1;
|
||||
EXPECT_FALSE(ota_bootloader_caps_marker_parse(marker, caps));
|
||||
|
||||
std::vector<uint8_t> internal_caps_image(64, 0xFF);
|
||||
const uint8_t internal_marker[16] = {
|
||||
'M','O','T','A','B','L','D','R', 3,0, 1,0,
|
||||
(uint8_t)(OTA_BL_STORAGE_STAGE_CEILING | OTA_BL_STORAGE_BOOT_UPDATE), 0,0,0};
|
||||
memcpy(internal_caps_image.data() + 4, internal_marker, sizeof(internal_marker));
|
||||
EXPECT_TRUE(ota_bootloader_caps_from_image(
|
||||
internal_caps_image.data(), internal_caps_image.size(),
|
||||
OTA_BL_STORAGE_STAGE_CEILING | OTA_BL_STORAGE_BOOT_UPDATE,
|
||||
caps));
|
||||
EXPECT_FALSE(ota_bootloader_caps_from_image(
|
||||
internal_caps_image.data(), internal_caps_image.size(),
|
||||
OTA_BL_STORAGE_STAGE_CEILING | OTA_BL_STORAGE_QSPI | OTA_BL_STORAGE_BOOT_UPDATE,
|
||||
caps));
|
||||
memcpy(internal_caps_image.data() + 24, internal_marker, sizeof(internal_marker));
|
||||
internal_caps_image[24 + 12] =
|
||||
OTA_BL_STORAGE_STAGE_CEILING | OTA_BL_STORAGE_QSPI | OTA_BL_STORAGE_BOOT_UPDATE;
|
||||
EXPECT_FALSE(ota_bootloader_caps_from_image(
|
||||
internal_caps_image.data(), internal_caps_image.size(),
|
||||
OTA_BL_STORAGE_STAGE_CEILING | OTA_BL_STORAGE_BOOT_UPDATE,
|
||||
caps));
|
||||
|
||||
OtaBlCaps selected;
|
||||
selected.present = true;
|
||||
selected.apply_abi = 3;
|
||||
@@ -176,20 +267,38 @@ TEST(OtaBootPackage, EmbeddedIdentityVectorCapsAndExplicitConfirmationGate) {
|
||||
}
|
||||
|
||||
TEST(OtaBootPackage, CapabilityScannerRejectsAnOtherwiseValidUnalignedMarker) {
|
||||
const uint8_t qspi_profile =
|
||||
OTA_BL_STORAGE_STAGE_CEILING | OTA_BL_STORAGE_QSPI |
|
||||
OTA_BL_STORAGE_BOOT_UPDATE;
|
||||
const uint8_t marker[16] = {'M','O','T','A','B','L','D','R', 3,0, 1,0,
|
||||
(uint8_t)(OTA_BL_STORAGE_QSPI | OTA_BL_STORAGE_BOOT_UPDATE), 0,0,0};
|
||||
qspi_profile, 0,0,0};
|
||||
uint8_t image[64];
|
||||
memset(image, 0xFF, sizeof(image));
|
||||
memcpy(image + 1, marker, sizeof(marker));
|
||||
EXPECT_FALSE(ota_bl_caps_scan_aligned(image, sizeof(image), true).present);
|
||||
EXPECT_FALSE(ota_bl_caps_scan_aligned(image, sizeof(image), true, qspi_profile).present);
|
||||
|
||||
memset(image, 0xFF, sizeof(image));
|
||||
memcpy(image + 4, marker, sizeof(marker));
|
||||
const OtaBlCaps caps = ota_bl_caps_scan_aligned(image, sizeof(image), true);
|
||||
const OtaBlCaps caps = ota_bl_caps_scan_aligned(image, sizeof(image), true, qspi_profile);
|
||||
ASSERT_TRUE(caps.present);
|
||||
EXPECT_EQ(caps.apply_abi, 3u);
|
||||
EXPECT_EQ(caps.storage_flags,
|
||||
(uint8_t)(OTA_BL_STORAGE_QSPI | OTA_BL_STORAGE_BOOT_UPDATE));
|
||||
EXPECT_EQ(caps.storage_flags, qspi_profile);
|
||||
|
||||
// A malformed aligned magic decoy is ignored, but a second fully valid
|
||||
// privileged marker makes the installed capability identity ambiguous.
|
||||
memcpy(image + 24, marker, sizeof(marker));
|
||||
EXPECT_FALSE(ota_bl_caps_scan_aligned(image, sizeof(image), true, qspi_profile).present);
|
||||
memset(image + 24, 0xFF, sizeof(marker));
|
||||
memcpy(image + 24, marker, sizeof(marker));
|
||||
image[24 + 13] = 1;
|
||||
EXPECT_TRUE(ota_bl_caps_scan_aligned(image, sizeof(image), true, qspi_profile).present);
|
||||
|
||||
// A structurally valid marker for the other storage profile is not an
|
||||
// installed-profile candidate and cannot shadow the exact marker.
|
||||
memcpy(image + 24, marker, sizeof(marker));
|
||||
image[24 + 12] = OTA_BL_STORAGE_STAGE_CEILING |
|
||||
OTA_BL_STORAGE_BOOT_UPDATE;
|
||||
EXPECT_TRUE(ota_bl_caps_scan_aligned(image, sizeof(image), true, qspi_profile).present);
|
||||
}
|
||||
|
||||
class FakeMotaSeederStream : public Stream {
|
||||
@@ -1979,7 +2088,7 @@ TEST(OtaTransfer, BootloaderPackageRequiresCapableExplicitMidPull) {
|
||||
OtaStoreRam<45000> capable_store;
|
||||
capable.begin(OTA_XIAO_BOARD_ID_BASE, nullptr, nullptr);
|
||||
capable.set_fetch_store(&capable_store);
|
||||
capable.set_accept_full(true);
|
||||
capable.set_accept_full(false); // internal nRF52 app FULL remains forbidden
|
||||
capable.set_accept_bootloader(true);
|
||||
capable.set_autofetch(OtaManager::AUTOFETCH_ANY);
|
||||
capable.on_message(
|
||||
@@ -1988,6 +2097,15 @@ TEST(OtaTransfer, BootloaderPackageRequiresCapableExplicitMidPull) {
|
||||
EXPECT_EQ(capable.fetchState(), OtaManager::IDLE);
|
||||
EXPECT_EQ(capable_store.staged_size(), 0u);
|
||||
|
||||
// Enabling the privileged bootloader path must not broaden the ordinary
|
||||
// single-slot application codec policy or turn autofetch on for FULL apps.
|
||||
const uint8_t app_mid[4] = {0x21, 0x22, 0x23, 0x24};
|
||||
capable.on_message(
|
||||
wire, make_have1(wire, sizeof(wire), app_mid, OTA_XIAO_BOARD_ID_BASE, 2,
|
||||
CODEC_FULL, MFLAG_FULL));
|
||||
EXPECT_EQ(capable.fetchState(), OtaManager::IDLE);
|
||||
EXPECT_EQ(capable_store.staged_size(), 0u);
|
||||
|
||||
// An exact explicit pull admits the strict v3 manifest and opens the store for the 40 KiB payload.
|
||||
EXPECT_EQ(capable.pull(mid, OTA_XIAO_BOARD_ID_BASE), OtaManager::PULL_STARTED);
|
||||
deliver_manifest_fragment(capable, mid, 0, manifest.data(), OTA_MF_FRAG);
|
||||
|
||||
@@ -18,6 +18,7 @@ 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 LEGACY = MOTA_NRF52_STAGE_CEILING_LEGACY;
|
||||
static constexpr uint32_t EXPANDED = MOTA_NRF52_STAGE_CEILING_EXPANDED;
|
||||
static constexpr uint32_t SHARED_BOOT_START = MOTA_NRF52_SHARED_BOOT_STAGE_START;
|
||||
static constexpr uint32_t APP_IMAGE_SIZE = 520u * 1024u;
|
||||
static constexpr uint32_t APP_END_V6 = APP_V6 + APP_IMAGE_SIZE;
|
||||
static constexpr uint32_t APP_END_V7 = APP_V7 + APP_IMAGE_SIZE;
|
||||
@@ -190,10 +191,134 @@ TEST(OtaFlashPlan, RejectsAppOutsideSelectedRegion) {
|
||||
|
||||
TEST(OtaFlashPlan, RejectsUnknownCeiling) {
|
||||
uint32_t start = 0x1234ABCD;
|
||||
EXPECT_FALSE(mota_nrf52_stage_plan(4096, APP_V6, APP_V6, 0xE0000u, start));
|
||||
EXPECT_FALSE(mota_nrf52_stage_plan(4096, APP_V6, APP_V6, 0xE1000u, start));
|
||||
EXPECT_EQ(start, 0x1234ABCDu);
|
||||
}
|
||||
|
||||
TEST(OtaFlashPlan, BootPackageUsesOneDynamicSharedInternalSlot) {
|
||||
// v3 = header + fixed manifest + forty leaves + 40 KiB image + trailer.
|
||||
const uint32_t total = 8u + 197u + 40u * 4u + 40u * 1024u + 5u;
|
||||
ASSERT_EQ(total, MOTA_NRF52_BOOT_CONTAINER_SIZE);
|
||||
uint32_t start = 0;
|
||||
ASSERT_TRUE(mota_nrf52_shared_boot_stage_plan(
|
||||
total, APP_V6, true, SHARED_BOOT_START, start));
|
||||
EXPECT_EQ(start, SHARED_BOOT_START);
|
||||
EXPECT_EQ(start, (EXPANDED - total) & ~(MOTA_NRF52_FLASH_PAGE - 1u));
|
||||
|
||||
// OTAFIX compacts payload offset +365 forward inside these same eleven
|
||||
// pages; no independent scratch bank participates in the internal path.
|
||||
const uint32_t payload_offset = 8u + 197u + 40u * 4u;
|
||||
EXPECT_EQ(payload_offset, 365u);
|
||||
EXPECT_LE(start + payload_offset + 40u * 1024u, EXPANDED);
|
||||
EXPECT_LE(start + 40u * 1024u, EXPANDED);
|
||||
|
||||
// Runtime headroom, not a special linker, is authoritative.
|
||||
EXPECT_FALSE(mota_nrf52_shared_boot_stage_plan(
|
||||
total, APP_V6, true, SHARED_BOOT_START + 1u, start));
|
||||
EXPECT_FALSE(mota_nrf52_shared_boot_stage_plan(
|
||||
total - 1u, APP_V6, true, APP_V6, start));
|
||||
EXPECT_FALSE(mota_nrf52_shared_boot_stage_plan(
|
||||
total + 1u, APP_V6, true, APP_V6, start));
|
||||
}
|
||||
|
||||
TEST(OtaFlashPlan, SharedInternalStoreKeepsNormalApplicationCeilingAndHandoff) {
|
||||
EXPECT_TRUE(mota_nrf52_target_image_fits(
|
||||
APP_V6, EXPANDED - APP_V6, EXPANDED));
|
||||
EXPECT_FALSE(mota_nrf52_target_image_fits(
|
||||
APP_V6, EXPANDED - APP_V6 + 1u, EXPANDED));
|
||||
EXPECT_EQ(mota_nrf52_flash_stage_handoff(EXPANDED),
|
||||
GPREGRET2_OTA_STAGE_EXPANDED);
|
||||
EXPECT_EQ(mota_nrf52_flash_stage_handoff(LEGACY),
|
||||
GPREGRET2_OTA_STAGE_LEGACY);
|
||||
}
|
||||
|
||||
TEST(OtaFlashPlan, InternalOrdinaryDeltaUsesTheSameEd000Store) {
|
||||
const uint32_t running_end = APP_V6 + 500u * 1024u;
|
||||
const uint32_t delta_container = 28u * 1024u;
|
||||
uint32_t staged = 0;
|
||||
ASSERT_TRUE(mota_nrf52_stage_plan(delta_container, APP_V6, running_end,
|
||||
EXPANDED, staged));
|
||||
EXPECT_GE(staged, running_end);
|
||||
EXPECT_LE(staged + delta_container, EXPANDED);
|
||||
EXPECT_EQ(mota_nrf52_flash_stage_handoff(EXPANDED), 0xEDu);
|
||||
}
|
||||
|
||||
TEST(OtaFlashPlan, LargeInternalOrdinaryDeltaMayStageBelowSharedBootStart) {
|
||||
// An ordinary delta container can exceed the boot package's eleven pages.
|
||||
// It bottom-aligns below E2000 and remains valid when detools'
|
||||
// encoded workspace ends before the actual container start.
|
||||
const uint32_t running_end = APP_V6 + 600u * 1024u;
|
||||
const uint32_t delta_container = 80u * 1024u;
|
||||
uint32_t staged = 0;
|
||||
ASSERT_TRUE(mota_nrf52_stage_plan(delta_container, APP_V6, running_end,
|
||||
EXPANDED, staged));
|
||||
EXPECT_LT(staged, SHARED_BOOT_START);
|
||||
EXPECT_GE(staged, running_end);
|
||||
|
||||
const uint32_t workspace = staged - APP_V6;
|
||||
const uint32_t target_size = workspace;
|
||||
EXPECT_TRUE(mota_nrf52_internal_patch_workspace_valid(
|
||||
workspace, APP_V6, staged, target_size, EXPANDED));
|
||||
EXPECT_FALSE(mota_nrf52_internal_patch_workspace_valid(
|
||||
workspace + 1u, APP_V6, staged, target_size, EXPANDED));
|
||||
EXPECT_FALSE(mota_nrf52_internal_patch_workspace_valid(
|
||||
workspace, APP_V6, staged, EXPANDED - APP_V6 + 1u, EXPANDED));
|
||||
}
|
||||
|
||||
TEST(OtaFlashPlan, MissingEndfRejectsEverySharedInternalPackageBeforeErase) {
|
||||
uint32_t protected_end = 0;
|
||||
ASSERT_TRUE(mota_nrf52_protected_app_end(
|
||||
APP_V6, EXPANDED, false, 0, false, protected_end));
|
||||
EXPECT_EQ(protected_end, APP_V6 + MOTA_NRF52_FALLBACK_INPLACE_MEMORY);
|
||||
|
||||
// A shared-internal-update build is linked through ED000, so the old 608 KiB
|
||||
// estimate is not a safe erase floor for either package kind. Missing EndF
|
||||
// disables all internal staging before begin()/reopen can erase a page.
|
||||
EXPECT_FALSE(mota_nrf52_protected_app_end(
|
||||
APP_V6, EXPANDED, false, 0, true, protected_end));
|
||||
|
||||
// The old rescue estimate would also admit this ordinary 80 KiB delta at
|
||||
// D9000 even though a legal ED000-linked image can have live bytes there.
|
||||
// A privileged shared-internal build therefore never calls stage_plan with
|
||||
// that estimate: protected_app_end() above returns false/capacity zero.
|
||||
const uint32_t ordinary_container = 80u * 1024u;
|
||||
uint32_t unsafe_start = 0;
|
||||
const uint32_t legacy_estimate =
|
||||
APP_V6 + MOTA_NRF52_FALLBACK_INPLACE_MEMORY;
|
||||
ASSERT_TRUE(mota_nrf52_stage_plan(
|
||||
ordinary_container, APP_V6, legacy_estimate, EXPANDED, unsafe_start));
|
||||
const uint32_t legal_live_tail = unsafe_start + MOTA_NRF52_FLASH_PAGE;
|
||||
ASSERT_GT(legal_live_tail, legacy_estimate);
|
||||
ASSERT_LT(legal_live_tail, EXPANDED);
|
||||
EXPECT_FALSE(mota_nrf52_stage_plan(
|
||||
ordinary_container, APP_V6, legal_live_tail, EXPANDED, protected_end));
|
||||
|
||||
// Model a legal linked image whose tail extends beyond the old 608 KiB
|
||||
// estimate. The privileged planner rejects missing EndF even if the generic
|
||||
// rescue fallback would appear to leave enough room.
|
||||
const uint32_t real_tail = APP_V6 + MOTA_NRF52_FALLBACK_INPLACE_MEMORY + 0x8000u;
|
||||
ASSERT_LT(real_tail, SHARED_BOOT_START);
|
||||
uint32_t staged = 0;
|
||||
EXPECT_FALSE(mota_nrf52_shared_boot_stage_plan(
|
||||
MOTA_NRF52_BOOT_CONTAINER_SIZE, APP_V6, false, protected_end, staged));
|
||||
ASSERT_TRUE(mota_nrf52_shared_boot_stage_plan(
|
||||
MOTA_NRF52_BOOT_CONTAINER_SIZE, APP_V6, true, real_tail, staged));
|
||||
EXPECT_EQ(staged, SHARED_BOOT_START);
|
||||
}
|
||||
|
||||
TEST(OtaFlashPlan, ReopenBoundsUntrustedTotalBeforeManifestRead) {
|
||||
const uint32_t start = SHARED_BOOT_START;
|
||||
EXPECT_TRUE(mota_nrf52_container_span_valid(
|
||||
start, EXPANDED, MOTA_NRF52_BOOT_CONTAINER_SIZE, 8u + 197u + 5u));
|
||||
EXPECT_FALSE(mota_nrf52_container_span_valid(
|
||||
start, EXPANDED, UINT32_MAX, 8u + 197u + 5u));
|
||||
EXPECT_FALSE(mota_nrf52_container_span_valid(
|
||||
start, EXPANDED, 8u + 197u + 4u, 8u + 197u + 5u));
|
||||
EXPECT_FALSE(mota_nrf52_container_span_valid(
|
||||
EXPANDED + 1u, EXPANDED, MOTA_NRF52_BOOT_CONTAINER_SIZE,
|
||||
8u + 197u + 5u));
|
||||
}
|
||||
|
||||
// out_start is only written on success - a rejected plan must not clobber the caller's variable.
|
||||
TEST(OtaFlashPlan, LeavesOutputUntouchedOnReject) {
|
||||
uint32_t start = 0x1234ABCD;
|
||||
|
||||
@@ -164,6 +164,21 @@ static mesh::Packet makeFloodPacket(uint8_t payload_type) {
|
||||
return packet;
|
||||
}
|
||||
|
||||
static mesh::Packet makeOtaManifestFragment(uint8_t format_version) {
|
||||
mesh::Packet packet = makeFloodPacket(PAYLOAD_TYPE_OTA);
|
||||
packet.payload_len = 12;
|
||||
packet.payload[0] = mesh::ota::OTA_MANIFEST;
|
||||
packet.payload[1] = 0x11;
|
||||
packet.payload[2] = 0x22;
|
||||
packet.payload[3] = 0x33;
|
||||
packet.payload[4] = 0x44;
|
||||
packet.payload[5] = 0; // fragment index
|
||||
packet.payload[6] = 2; // fragment count
|
||||
memcpy(packet.payload + 7, mesh::ota::MOTA_MAGIC, sizeof(mesh::ota::MOTA_MAGIC));
|
||||
packet.payload[11] = format_version;
|
||||
return packet;
|
||||
}
|
||||
|
||||
TEST(RepeaterTransport, UnknownFloodPayloadIsRelayedWhenForwardingAllowsIt) {
|
||||
TraceTestClock clock;
|
||||
TraceTestRTC rtc;
|
||||
@@ -233,6 +248,41 @@ TEST(RepeaterTransport, OtaDiscoveryRelaysInBackgroundOnlyDuringTempRadio) {
|
||||
EXPECT_FALSE(node.canTransmit(&packet)); // queued-near-expiry packets cannot leak onto the normal channel
|
||||
}
|
||||
|
||||
TEST(RepeaterTransport, AppV2AndBootV3ShareOta0cTempRadioRelayPolicy) {
|
||||
// The package format is metadata inside an OTA_MANIFEST fragment. It must
|
||||
// never select a second mesh payload type or bypass the repeater default
|
||||
// `flood.filter.1 0x0C all suspend=tempradio` policy.
|
||||
ASSERT_EQ(0x0C, PAYLOAD_TYPE_OTA);
|
||||
const uint8_t formats[] = {
|
||||
mesh::ota::MOTA_APP_FORMAT_VER,
|
||||
mesh::ota::MOTA_BOOT_FORMAT_VER,
|
||||
};
|
||||
|
||||
for (uint8_t format : formats) {
|
||||
TraceTestClock clock;
|
||||
TraceTestRTC rtc;
|
||||
TraceTestRNG rng;
|
||||
TraceTestRadio radio;
|
||||
ForwardingTestTables tables;
|
||||
StaticPoolPacketManager manager(12);
|
||||
TraceTestMesh node(radio, clock, rng, rtc, manager, tables);
|
||||
node.forwardFloods = true;
|
||||
|
||||
mesh::Packet packet = makeOtaManifestFragment(format);
|
||||
ASSERT_EQ(PAYLOAD_TYPE_OTA, packet.getPayloadType());
|
||||
ASSERT_EQ(format, packet.payload[11]);
|
||||
EXPECT_EQ(ACTION_RELEASE, node.receivePacket(&packet));
|
||||
EXPECT_EQ(0, tables.mark_seen_calls);
|
||||
|
||||
node.tempRadioActive = true;
|
||||
mesh::DispatcherAction action = node.receivePacket(&packet);
|
||||
EXPECT_NE(ACTION_RELEASE, action);
|
||||
EXPECT_EQ(OTA_TRANSFER_TX_PRIORITY, (action >> 24) - 1);
|
||||
EXPECT_EQ(1, tables.mark_seen_calls);
|
||||
EXPECT_EQ(1, packet.getPathHashCount());
|
||||
}
|
||||
}
|
||||
|
||||
TEST(RepeaterTransport, OtaTransferRelaysAsPrimaryTrafficWithoutOtaManager) {
|
||||
TraceTestClock clock;
|
||||
TraceTestRTC rtc;
|
||||
|
||||
Reference in new issue
Block a user