mirror of
https://github.com/mikecarper/MeshCore.git
synced 2026-10-20 22:01:58 +00:00
Keep the deployed SD target and layout, select qualified internal delta storage when no card is usable, and retain exact SD bootloader-update gates. Add storage, capability, transfer, recipe, and release-selection regression coverage. Also preserve mandatory runtime packet logging in canonical XIAO images when verbose build debugging is disabled, fixing the four stopped release-matrix qualification failures.
336 lines
15 KiB
Python
336 lines
15 KiB
Python
"""Check actual nRF52 capability parsing for SD-primary Tower storage."""
|
|
|
|
from pathlib import Path
|
|
import struct
|
|
import subprocess
|
|
import sys
|
|
import tempfile
|
|
import unittest
|
|
|
|
|
|
ROOT = Path(__file__).resolve().parents[1]
|
|
sys.path.insert(0, str(ROOT / "tools/mota"))
|
|
import motalib as ml
|
|
from test_mota import _generic_bootloader_image, _rewrite_boot_image
|
|
|
|
|
|
SOURCE = r'''
|
|
#include <cassert>
|
|
#include <cstring>
|
|
#include <sys/mman.h>
|
|
#include "helpers/ota/OtaBlInfo.h"
|
|
|
|
using namespace mesh::ota;
|
|
|
|
static const uint8_t bundle[32] = {
|
|
'M','O','T','A','R','A','M','A',1,0,72,0,0,0,1,0,
|
|
'M','O','T','A','S','T','O','R',1,0,16,0,2,0,0,0,
|
|
};
|
|
|
|
static void imageFor(uint8_t* image, uint8_t primary, uint8_t optional) {
|
|
memset(image, 0, 256);
|
|
const uint8_t boot[16] = {
|
|
'M','O','T','A','B','L','D','R',3,0,5,0,0,0,0,0,
|
|
};
|
|
memcpy(image, boot, sizeof(boot));
|
|
image[12] = primary;
|
|
if (optional) {
|
|
memcpy(image + 32, bundle, sizeof(bundle));
|
|
image[60] = optional;
|
|
}
|
|
}
|
|
|
|
static void expectViews(const uint8_t* image, uint8_t primary,
|
|
uint8_t application, uint8_t optional) {
|
|
const OtaBlCaps app = ota_bl_app_caps_scan(image, 256);
|
|
assert(app.present && app.storage_flags == application);
|
|
assert(app.optional_app_storage == optional);
|
|
const OtaBlCaps update = ota_bl_update_caps_scan_aligned(image, 256, primary);
|
|
assert(update.present && update.storage_flags == primary);
|
|
assert(update.optional_app_storage == optional);
|
|
}
|
|
|
|
int main() {
|
|
uint8_t image[256];
|
|
imageFor(image, 0x09, 0x02);
|
|
expectViews(image, 0x09, 0x0B, 0x02);
|
|
assert(ota_bootloader_supports_expanded_stage(ota_bl_app_caps_scan(image, 256)));
|
|
assert(ota_nrf52_effective_stage_ceiling(ota_bl_app_caps_scan(image, 256)) == 0xED000);
|
|
|
|
// An ordinary SD loader remains valid for SD and privileged SD upgrades,
|
|
// but its primary marker must never be mistaken for internal support.
|
|
imageFor(image, 0x09, 0);
|
|
expectViews(image, 0x09, 0x09, 0);
|
|
assert(!ota_bootloader_supports_expanded_stage(ota_bl_app_caps_scan(image, 256)));
|
|
assert(ota_nrf52_effective_stage_ceiling(ota_bl_app_caps_scan(image, 256)) == 0xD4000);
|
|
|
|
// RAK keeps the deployed internal privileged contract and optional NOR.
|
|
imageFor(image, 0x0A, 0x14);
|
|
expectViews(image, 0x0A, 0x1E, 0x14);
|
|
imageFor(image, 0x0A, 0);
|
|
expectViews(image, 0x0A, 0x0A, 0);
|
|
|
|
// Optional records are meaningful only under their exact primary profile.
|
|
for (uint8_t primary : {uint8_t(0x09), uint8_t(0x0A), uint8_t(0x0E), uint8_t(0x1E)}) {
|
|
for (uint8_t optional : {uint8_t(0x02), uint8_t(0x14)}) {
|
|
imageFor(image, primary, optional);
|
|
const bool qualified = (primary == 0x09 && optional == 2) ||
|
|
(primary == 0x0A && optional == 0x14);
|
|
expectViews(image, primary, qualified ? primary | optional : primary,
|
|
qualified ? optional : 0);
|
|
}
|
|
}
|
|
|
|
// Every field of the frozen adjacent bundle is enforced. A malformed
|
|
// optional declaration cannot remove the still-valid SD primary profile.
|
|
for (unsigned byte = 0; byte < sizeof(bundle); ++byte) {
|
|
imageFor(image, 0x09, 0x02);
|
|
image[32 + byte] ^= 1;
|
|
expectViews(image, 0x09, 0x09, 0);
|
|
}
|
|
imageFor(image, 0x09, 0x02);
|
|
memcpy(image + 80, bundle, sizeof(bundle));
|
|
expectViews(image, 0x09, 0x09, 0); // duplicate RAM and STOR
|
|
imageFor(image, 0x09, 0x02);
|
|
memcpy(image + 80, bundle, 16);
|
|
expectViews(image, 0x09, 0x09, 0); // duplicate RAM alone
|
|
imageFor(image, 0x09, 0x02);
|
|
memcpy(image + 80, bundle + 16, 16);
|
|
expectViews(image, 0x09, 0x09, 0); // duplicate STOR alone
|
|
imageFor(image, 0x09, 0x02);
|
|
memmove(image + 52, image + 48, 16);
|
|
memset(image + 48, 0, 4);
|
|
expectViews(image, 0x09, 0x09, 0); // separated records
|
|
imageFor(image, 0x09, 0x02);
|
|
memmove(image + 33, image + 32, 32);
|
|
image[32] = 0;
|
|
expectViews(image, 0x09, 0x09, 0); // byte-shifted bundle
|
|
imageFor(image, 0x09, 0x02);
|
|
assert(ota_bl_optional_app_storage(image, 63) == 0);
|
|
assert(ota_bl_optional_app_storage(nullptr, 256) == 0);
|
|
|
|
// A second privileged marker must not be hidden by optional metadata.
|
|
memcpy(image + 128, image, 16);
|
|
assert(!ota_bl_update_caps_scan_aligned(image, 256, 0x09).present);
|
|
|
|
// Exercise the production memory-mapped entry points without defining the
|
|
// retained-RAM application flag: this app retains the full SD RAM layout.
|
|
void* flash = mmap(reinterpret_cast<void*>(uintptr_t(0xF4000)), 0xA000,
|
|
PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS | MAP_FIXED,
|
|
-1, 0);
|
|
assert(flash == reinterpret_cast<void*>(uintptr_t(0xF4000)));
|
|
memset(flash, 0, 0xA000);
|
|
#if defined(OTA_INTERNAL_BOOTLOADER_UPDATE)
|
|
imageFor(static_cast<uint8_t*>(flash), 0x0A, 0x14);
|
|
assert(ota_bootloader_update_storage_flags() == 0x0A);
|
|
assert(ota_bootloader_app_caps().storage_flags == 0x1E);
|
|
assert(ota_bootloader_update_caps().storage_flags == 0x0A);
|
|
#else
|
|
imageFor(static_cast<uint8_t*>(flash), 0x09, 0x02);
|
|
assert(ota_bootloader_update_storage_flags() == 0x09);
|
|
assert(ota_bootloader_app_caps().storage_flags == 0x0B);
|
|
OtaBlCaps update = ota_bootloader_update_caps();
|
|
assert(update.storage_flags == 0x09 && update.optional_app_storage == 2);
|
|
assert(ota_bootloader_self_update_caps_valid(update));
|
|
imageFor(static_cast<uint8_t*>(flash), 0x09, 0);
|
|
update = ota_bootloader_update_caps();
|
|
assert(update.storage_flags == 0x09 && update.optional_app_storage == 0);
|
|
assert(ota_bootloader_self_update_caps_valid(update));
|
|
#endif
|
|
assert(!ota_bootloader_ram_caps().present);
|
|
assert(munmap(flash, 0xA000) == 0);
|
|
}
|
|
'''
|
|
|
|
STREAM_SOURCE = r'''
|
|
#include <cassert>
|
|
#include <cstring>
|
|
#include <fstream>
|
|
#include <iterator>
|
|
#include <string>
|
|
#include <vector>
|
|
#include "helpers/ota/OtaBootloaderUpdate.h"
|
|
using namespace mesh::ota;
|
|
struct Store {
|
|
std::vector<uint8_t> bytes;
|
|
bool fail_read = false;
|
|
bool read(uint32_t off, uint8_t* out, uint32_t len) {
|
|
if (uint64_t(off) + len > bytes.size() || (fail_read && off == 37 + 512)) return false;
|
|
memcpy(out, bytes.data() + off, len);
|
|
return true;
|
|
}
|
|
};
|
|
int main(int argc, char** argv) {
|
|
assert(argc == 6);
|
|
std::ifstream input(argv[1], std::ios::binary);
|
|
Store store;
|
|
store.bytes.resize(37, 0xA5);
|
|
store.bytes.insert(store.bytes.end(), std::istreambuf_iterator<char>(input), {});
|
|
assert(store.bytes.size() == 37 + OTA_BOOT_IMAGE_SIZE);
|
|
store.fail_read = std::stoul(argv[5]) != 0;
|
|
OtaBootloaderIdentity identity;
|
|
OtaBootloaderCapsMarker caps;
|
|
const bool actual = ota_bootloader_external_image_metadata(
|
|
store, 37, uint8_t(std::stoul(argv[2])), identity, caps,
|
|
uint8_t(std::stoul(argv[3])));
|
|
assert(actual == (std::stoul(argv[4]) != 0));
|
|
if (actual) {
|
|
assert(identity.present && identity.crc_ok && identity.continuity_present);
|
|
assert(identity.manifest_offset == OTA_BOOT_CANDIDATE_MANIFEST_OFFSET);
|
|
assert(caps.storage_flags == std::stoul(argv[2]));
|
|
}
|
|
}
|
|
'''
|
|
|
|
UTILS_DECLARATIONS = """#pragma once
|
|
#include <stddef.h>
|
|
#include <stdint.h>
|
|
namespace mesh { struct Utils {
|
|
static void sha256(uint8_t*, size_t, const uint8_t*, int);
|
|
static void sha256(uint8_t*, size_t, const uint8_t*, int, const uint8_t*, int);
|
|
}; }
|
|
"""
|
|
|
|
|
|
class TowerBootloaderCapsTest(unittest.TestCase):
|
|
def test_sd_primary_and_rak_capability_views(self):
|
|
for profile in ("OTA_SD_BOOTLOADER_UPDATE", "OTA_INTERNAL_BOOTLOADER_UPDATE"):
|
|
with self.subTest(profile=profile), tempfile.TemporaryDirectory() as directory:
|
|
path = Path(directory)
|
|
source = path / "caps.cpp"
|
|
source.write_text("#include <initializer_list>\n" + SOURCE, encoding="ascii")
|
|
executable = path / "caps"
|
|
subprocess.run([
|
|
"g++", "-std=c++17", "-Wall", "-Wextra", "-Werror",
|
|
"-DNRF52_PLATFORM=1", f"-D{profile}=1",
|
|
"-DMOTA_NRF52_TEST_LAYOUT_STAGE_CEILING=0xED000u",
|
|
"-I", str(ROOT / "src"), str(source), "-o", str(executable),
|
|
], check=True, timeout=30)
|
|
subprocess.run([str(executable)], check=True, timeout=10)
|
|
|
|
def test_auto_storage_guard_allows_only_sd_primary_tower(self):
|
|
required = ["NRF52_PLATFORM", "HELTEC_TOWER_V2_SDCARD", "OTA_SD_STORE",
|
|
"OTA_FLASH_STORE", "OTA_SD_BOOTLOADER_UPDATE"]
|
|
forbidden = ["OTA_QSPI_STORE", "QSPIFLASH", "OTA_RAK_AUTO_STORE",
|
|
"OTA_INTERNAL_BOOTLOADER_UPDATE", "OTA_QSPI_BOOTLOADER_UPDATE",
|
|
"OTA_HYBRID_RAM_STORE"]
|
|
with tempfile.TemporaryDirectory() as directory:
|
|
path = Path(directory)
|
|
(path / "Utils.h").write_text(UTILS_DECLARATIONS, encoding="ascii")
|
|
source = path / "guard.cpp"
|
|
source.write_text('#include "helpers/ota/OtaBootloaderUpdate.h"\n', encoding="ascii")
|
|
cases = [(required + ["OTA_TOWER_AUTO_STORE"], True),
|
|
([item for item in required if item != "OTA_FLASH_STORE"], True),
|
|
(required, False)]
|
|
cases += [([item for item in required if item != missing]
|
|
+ ["OTA_TOWER_AUTO_STORE"], False) for missing in required]
|
|
cases += [(required + ["OTA_TOWER_AUTO_STORE", item], False) for item in forbidden]
|
|
for flags, expected in cases:
|
|
with self.subTest(flags=flags):
|
|
result = subprocess.run([
|
|
"g++", "-std=c++17", "-fsyntax-only", "-I", str(path),
|
|
"-I", str(ROOT / "src"), *[f"-D{flag}=1" for flag in flags],
|
|
str(source),
|
|
], capture_output=True, text=True, timeout=30)
|
|
self.assertEqual(result.returncode == 0, expected, result.stderr)
|
|
if not expected:
|
|
self.assertIn("#error", result.stderr)
|
|
|
|
def test_streamed_successors_retain_unique_adjacent_optional_storage(self):
|
|
base = _generic_bootloader_image(0x239A0071, "TOWER_V2_OTA", ml.BOOT_STORAGE_SD_UPDATE)
|
|
image = self.with_optional(base, 2)
|
|
ram, optional = image[0x90:0xA0], image[0xA0:0xB0]
|
|
cases = [(base, 9, 0, True, False), (base, 9, 2, False, False),
|
|
(image, 9, 2, True, False), (image, 9, 0, True, False),
|
|
(image, 9, 0x14, False, False), (image, 9, 2, False, True)]
|
|
for offset in (480, 496, 500, 508, 512, 1020):
|
|
def move_pair(data, offset=offset):
|
|
data[0x90:0xB0] = b"\xff" * 32
|
|
data[offset:offset + 32] = ram + optional
|
|
cases.append((_rewrite_boot_image(image, move_pair), 9, 2, True, False))
|
|
mutations = [
|
|
lambda data: data.__setitem__(slice(0x200, 0x210), ram),
|
|
lambda data: data.__setitem__(slice(0x200, 0x210), optional),
|
|
lambda data: data.__setitem__(slice(0x70, 0x80), optional),
|
|
lambda data: data.__setitem__(0xAC, 0x14),
|
|
lambda data: data.__setitem__(0xAD, 1),
|
|
lambda data: data.__setitem__(0x98, 2),
|
|
lambda data: data.__setitem__(slice(0x90, 0xA0), b"\xff" * 16),
|
|
]
|
|
for mutation in mutations:
|
|
candidate = _rewrite_boot_image(image, mutation)
|
|
cases += [(candidate, 9, 2, False, False), (candidate, 9, 0, True, False)]
|
|
rak = self.with_optional(_generic_bootloader_image(), 0x14)
|
|
cases += [(rak, 0x0A, 0x14, True, False), (rak, 0x0A, 2, False, False)]
|
|
with tempfile.TemporaryDirectory() as directory:
|
|
path = Path(directory)
|
|
(path / "Utils.h").write_text(UTILS_DECLARATIONS, encoding="ascii")
|
|
source = path / "stream.cpp"
|
|
source.write_text(STREAM_SOURCE, encoding="ascii")
|
|
executable = path / "stream"
|
|
subprocess.run([
|
|
"g++", "-std=c++17", "-Wall", "-Wextra", "-Werror",
|
|
"-I", str(path), "-I", str(ROOT / "src"), str(source),
|
|
"-o", str(executable),
|
|
], check=True, timeout=30)
|
|
for index, (candidate, primary, required, expected, fail_read) in enumerate(cases):
|
|
with self.subTest(case=index):
|
|
payload = path / "image.bin"
|
|
payload.write_bytes(candidate)
|
|
subprocess.run([str(executable), str(payload), str(primary), str(required),
|
|
str(int(expected)), str(int(fail_read))], check=True, timeout=10)
|
|
|
|
@staticmethod
|
|
def with_optional(image, optional):
|
|
def add_storage(data):
|
|
struct.pack_into("<I", data, 0, 0x20030000)
|
|
data[0x90:0xB0] = (b"MOTARAMA" + struct.pack("<HHI", 1, 72, 65536)
|
|
+ b"MOTASTOR" + struct.pack("<HHB3x", 1, 16, optional))
|
|
return _rewrite_boot_image(image, add_storage)
|
|
|
|
def test_python_admits_exact_tower_and_retains_rak_profiles(self):
|
|
for board, name, primary, optional in (
|
|
(0x239A0071, "TOWER_V2_OTA", ml.BOOT_STORAGE_SD_UPDATE, 2),
|
|
(0x239A0029, "3401_DFU", ml.BOOT_STORAGE_INTERNAL_UPDATE, 0x14),
|
|
(0x239A0029, "4631_DFU", ml.BOOT_STORAGE_INTERNAL_UPDATE, 0x14),
|
|
):
|
|
with self.subTest(name=name):
|
|
base = _generic_bootloader_image(board, name, primary)
|
|
self.assertEqual(ml.bootloader_optional_app_storage(base), 0)
|
|
image = self.with_optional(base, optional)
|
|
self.assertEqual(ml.validate_bootloader_image(image).device_name, name)
|
|
self.assertEqual(ml.bootloader_caps_storage(image), primary)
|
|
self.assertEqual(ml.bootloader_optional_app_storage(image), optional)
|
|
|
|
def test_python_rejects_corrupt_ambiguous_and_cross_profile_records(self):
|
|
base = _generic_bootloader_image(0x239A0071, "TOWER_V2_OTA", ml.BOOT_STORAGE_SD_UPDATE)
|
|
image = self.with_optional(base, 2)
|
|
ram = image[0x90:0xA0]
|
|
optional = image[0xA0:0xB0]
|
|
mutations = [
|
|
lambda data: data.__setitem__(0xAC, 3),
|
|
lambda data: data.__setitem__(0xAD, 1),
|
|
lambda data: data.__setitem__(slice(0xB0, 0xC0), optional),
|
|
lambda data: data.__setitem__(slice(0xB0, 0xC0), ram),
|
|
lambda data: data.__setitem__(slice(0x90, 0xA0), b"\xff" * 16),
|
|
lambda data: struct.pack_into("<I", data, 0, 0x20040000),
|
|
]
|
|
for mutation in mutations:
|
|
with self.subTest(mutation=mutations.index(mutation)):
|
|
with self.assertRaises(ValueError):
|
|
ml.validate_bootloader_image(_rewrite_boot_image(image, mutation))
|
|
for board, name, primary, flags in (
|
|
(0x239A0071, "TOWER_V2_OTA", ml.BOOT_STORAGE_SD_UPDATE, 0x14),
|
|
(0x239A0029, "3401_DFU", ml.BOOT_STORAGE_INTERNAL_UPDATE, 2),
|
|
(0x239A0029, "4631_DFU", ml.BOOT_STORAGE_INTERNAL_UPDATE, 2),
|
|
(0x239A0071, "TOWER_V2_OTA", ml.BOOT_STORAGE_INTERNAL_UPDATE, 2),
|
|
):
|
|
with self.subTest(name=name, primary=primary, flags=flags):
|
|
candidate = self.with_optional(_generic_bootloader_image(board, name, primary), flags)
|
|
with self.assertRaises(ValueError):
|
|
ml.validate_bootloader_image(candidate)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|