Protect nRF SD OTA authorization RAM from source-only stores
This commit is contained in:
1 parent
9c3df94f99
commit
5ee9ceeb20
14 files changed
+556
-9
No files matched your search
+91
@@ -0,0 +1,91 @@
|
||||
#include <assert.h>
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include <sys/mman.h>
|
||||
#include "helpers/BorrowableFrameBuffer.h"
|
||||
#include "helpers/ota/OtaSdAuthRecord.h"
|
||||
|
||||
#define STRINGIFY_INNER(value) #value
|
||||
#define STRINGIFY(value) STRINGIFY_INNER(value)
|
||||
#ifdef TEST_AUTH_START
|
||||
asm(".global __mota_sd_auth_start__\n.set __mota_sd_auth_start__, " STRINGIFY(TEST_AUTH_START));
|
||||
asm(".global __mota_sd_auth_end__\n.set __mota_sd_auth_end__, " STRINGIFY(TEST_AUTH_END));
|
||||
#endif
|
||||
// Simulate an ordinary linker with a large .svc_data/.fs_data prefix. Its
|
||||
// initialized .data starts after the auth slot, but the slot is still owned by
|
||||
// the application. A __data_start__ check alone would incorrectly allow writes.
|
||||
asm(".global __data_start__\n.set __data_start__, 0x20006100");
|
||||
|
||||
static unsigned barriers;
|
||||
[[maybe_unused]] static void __DMB() { ++barriers; }
|
||||
[[maybe_unused]] static void __DSB() { ++barriers; }
|
||||
|
||||
namespace mesh {
|
||||
namespace ota {
|
||||
class OtaStoreSdNrf52 {
|
||||
public:
|
||||
OtaStoreSdNrf52();
|
||||
};
|
||||
#include "production_auth.inc"
|
||||
} // namespace ota
|
||||
} // namespace mesh
|
||||
|
||||
struct Frame { uint8_t bytes[96]; };
|
||||
struct Workspace {
|
||||
mesh::ota::OtaStoreSdNrf52 store;
|
||||
uint32_t payload = 0x12345678;
|
||||
};
|
||||
|
||||
int main() {
|
||||
void* const mapped = mmap(reinterpret_cast<void*>(0x20006000), 4096,
|
||||
PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS | MAP_FIXED_NOREPLACE, -1, 0);
|
||||
assert(mapped == reinterpret_cast<void*>(0x20006000));
|
||||
uint8_t* const page = static_cast<uint8_t*>(mapped);
|
||||
uint8_t before[4096];
|
||||
memset(page, 0xA7, 4096);
|
||||
memcpy(before, page, sizeof(before));
|
||||
mesh::BorrowableFrameBuffer<Frame, 256, 128, Workspace> queue;
|
||||
int head = 39;
|
||||
for (size_t index = 0; index < queue.capacity(); ++index)
|
||||
memset(queue.at(index).bytes, static_cast<int>(index), sizeof(Frame));
|
||||
for (unsigned cycle = 0; cycle != 64; ++cycle) {
|
||||
Workspace* const workspace = queue.acquire(97, head);
|
||||
assert(workspace && workspace->payload == 0x12345678 && head == 0);
|
||||
assert(queue.acquire(97, head) == workspace);
|
||||
assert(queue.capacity() == 128);
|
||||
for (size_t index = 0; index < 97; ++index) {
|
||||
const uint8_t expected = static_cast<uint8_t>(index + 39);
|
||||
for (uint8_t byte : queue.at(index).bytes) assert(byte == expected);
|
||||
}
|
||||
queue.release(head);
|
||||
queue.release(head);
|
||||
assert(queue.capacity() == 256);
|
||||
}
|
||||
if (EXPECT_OWNS) {
|
||||
for (unsigned index = 8; index < 80; ++index) assert(page[index] == 0);
|
||||
assert(barriers == 128);
|
||||
} else {
|
||||
assert(memcmp(page, before, sizeof(before)) == 0);
|
||||
assert(barriers == 0);
|
||||
}
|
||||
#if !defined(OTA_SEEDER_ONLY)
|
||||
uint8_t record[mesh::ota::MOTA_SD_AUTH_LEN];
|
||||
for (size_t index = 0; index < sizeof(record); ++index)
|
||||
record[index] = static_cast<uint8_t>(index + 1);
|
||||
memcpy(before, page, sizeof(before));
|
||||
assert(mesh::ota::publish_sd_auth_record(record) == static_cast<bool>(EXPECT_OWNS));
|
||||
if (EXPECT_OWNS) {
|
||||
assert(memcmp(page + 8, record, sizeof(record)) == 0);
|
||||
mesh::ota::clear_sd_auth_record();
|
||||
for (unsigned index = 8; index < 80; ++index) assert(page[index] == 0);
|
||||
} else {
|
||||
assert(memcmp(page, before, sizeof(before)) == 0);
|
||||
}
|
||||
#endif
|
||||
// Neither successful receivers nor failed/absent ownership touch either side.
|
||||
for (unsigned index = 0; index < 8; ++index) assert(page[index] == 0xA7);
|
||||
for (unsigned index = 80; index < 4096; ++index) assert(page[index] == 0xA7);
|
||||
assert(munmap(mapped, 4096) == 0);
|
||||
puts("SD auth ownership and 64 queue lifetimes passed");
|
||||
}
|
||||
@@ -263,6 +263,7 @@ class FirmwareCapabilityCheckerTest(unittest.TestCase):
|
||||
(0, "internal_flash", ["in_place_delta"]),
|
||||
(4, "external_qspi", ["full", "in_place_delta"]),
|
||||
(48, "adaptive_internal_or_external_qspi", ["full", "in_place_delta"]),
|
||||
(32, "adaptive_internal_or_external_qspi", ["full", "in_place_delta"]),
|
||||
(1, "external_sd", ["full", "in_place_delta"])]:
|
||||
result, manifest = self.run_checker(
|
||||
b"dfu invalid in-place patch geometry", "--platform", "NRF52_PLATFORM",
|
||||
@@ -279,6 +280,10 @@ class FirmwareCapabilityCheckerTest(unittest.TestCase):
|
||||
self.assertIn("adaptive OTAFIX", details["bootloader"])
|
||||
self.assertTrue(any("*_AUTO_DFU" in note for note in details["notes"]))
|
||||
self.assertFalse(any("self-update is unavailable" in note for note in details["notes"]))
|
||||
if flags == 32:
|
||||
self.assertTrue(any("flash-only" in note for note in details["notes"]))
|
||||
self.assertTrue(any("self-update are not compiled" in note for note in details["notes"]))
|
||||
self.assertFalse(any("needs the retained-RAM" in note for note in details["notes"]))
|
||||
|
||||
def test_nrf52_full_companion_cannot_claim_lora_self_update(self):
|
||||
result, manifest = self.run_checker(
|
||||
|
||||
@@ -9,6 +9,7 @@ from pathlib import Path
|
||||
import sys
|
||||
import tempfile
|
||||
import unittest
|
||||
from unittest import mock
|
||||
|
||||
from intelhex import IntelHex
|
||||
|
||||
@@ -73,6 +74,68 @@ def stamp(action, path, env):
|
||||
|
||||
|
||||
class Nrf52EndfHexTests(unittest.TestCase):
|
||||
def test_explicit_legacy_rak_sd_stamps_actual_flash_only_adaptive_capability(self):
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
env = BuildEnvironment(ml.NRF52_APP_BASE_S140_V6)
|
||||
env["CPPDEFINES"].extend(["RAK_4631", "OTA_SD_STORE", "OTA_RAK15002_SD_STORE",
|
||||
"OTA_RAK_AUTO_STORE", "OTA_QSPI_STORE", "OTA_FLASH_STORE"])
|
||||
env.linker = str(ROOT / "boards/nrf52840_s140_v6_sd_ota.ld")
|
||||
env["LDSCRIPT_PATH"] = env.linker
|
||||
load_post_script(env)
|
||||
path = Path(directory) / "legacy.hex"
|
||||
image = IntelHex()
|
||||
image.frombytes(bytes(range(256)), offset=env.app_base)
|
||||
image.write_hex_file(str(path))
|
||||
with mock.patch.dict(os.environ, {"MESHCORE_NRF52_LEGACY_FLASH_OTA": "1"}):
|
||||
stamp(env.post_actions["$BUILD_DIR/${PROGNAME}.hex"], path, env)
|
||||
layout = ml.parse_nrf52_layout(read_application(path))
|
||||
self.assertEqual(layout, ml.Nrf52Layout(
|
||||
env.app_base, ml.NRF52_APP_END, ml.NRF52_APP_END, ml.NRF52_LAYOUT_FLAG_AUTO_STORE))
|
||||
self.assertTrue(layout.auto_store)
|
||||
self.assertFalse(layout.hybrid_ram)
|
||||
self.assertFalse(layout.external_backed)
|
||||
|
||||
def test_legacy_adaptive_exception_rejects_wrong_flags_maps_and_flash_geometry(self):
|
||||
base_flags = ["RAK_4631", "OTA_SD_STORE", "OTA_RAK15002_SD_STORE",
|
||||
"OTA_RAK_AUTO_STORE", "OTA_QSPI_STORE", "OTA_FLASH_STORE"]
|
||||
cases = (
|
||||
("not_explicit", {}, {}, "RAK adaptive OTA requires"),
|
||||
("missing_qspi", {"remove": "OTA_QSPI_STORE"}, {"MESHCORE_NRF52_LEGACY_FLASH_OTA": "1"}, "both stores"),
|
||||
("missing_flash", {"remove": "OTA_FLASH_STORE"}, {"MESHCORE_NRF52_LEGACY_FLASH_OTA": "1"}, "both stores"),
|
||||
("seeder", {"add": ["OTA_SEEDER_ONLY"]}, {"MESHCORE_NRF52_LEGACY_FLASH_OTA": "1"}, "requires a receiver"),
|
||||
("wrong_active", {"active": "boards/nrf52840_s140_v6.ld"}, {"MESHCORE_NRF52_LEGACY_FLASH_OTA": "1"}, "active linker"),
|
||||
("wrong_configured", {"configured": "boards/nrf52840_s140_v6.ld"}, {"MESHCORE_NRF52_LEGACY_FLASH_OTA": "1"}, "configured linker"),
|
||||
("wrong_softdevice", {"base": ml.NRF52_APP_BASE_S140_V7}, {"MESHCORE_NRF52_LEGACY_FLASH_OTA": "1"}, "exact S140 v6"),
|
||||
("wrong_flash_end", {"end": ml.NRF52_EXTRAFS_START}, {"MESHCORE_NRF52_LEGACY_FLASH_OTA": "1"}, "exact S140 v6"),
|
||||
("hybrid_only", {"add": ["OTA_HYBRID_RAM_STORE"]}, {"MESHCORE_NRF52_LEGACY_FLASH_OTA": "1"}, "enabled together"),
|
||||
("hybrid_pair", {"add": ["OTA_HYBRID_RAM_STORE", "OTA_INTERNAL_BOOTLOADER_UPDATE"],
|
||||
"configured": str(ROOT / "boards/nrf52840_s140_v6_mota64.ld"),
|
||||
"active": str(ROOT / "boards/nrf52840_s140_v6_mota64.ld")},
|
||||
{"MESHCORE_NRF52_LEGACY_FLASH_OTA": "1"}, "without hybrid RAM"),
|
||||
("unmarked_sd_role", {"remove": "OTA_RAK15002_SD_STORE"}, {"MESHCORE_NRF52_LEGACY_FLASH_OTA": "1"}, "both SD and QSPI"),
|
||||
)
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
path = Path(directory) / "legacy.hex"
|
||||
for name, changes, process_env, error in cases:
|
||||
with self.subTest(case=name):
|
||||
env = BuildEnvironment(changes.get("base", ml.NRF52_APP_BASE_S140_V6))
|
||||
env["CPPDEFINES"].extend(base_flags)
|
||||
if "remove" in changes:
|
||||
env["CPPDEFINES"].remove(changes["remove"])
|
||||
env["CPPDEFINES"].extend(changes.get("add", []))
|
||||
env.maximum_size = changes.get("end", ml.NRF52_APP_END) - env.app_base
|
||||
env.linker = changes.get("configured", str(ROOT / "boards/nrf52840_s140_v6_sd_ota.ld"))
|
||||
env["LDSCRIPT_PATH"] = changes.get("active", str(ROOT / "boards/nrf52840_s140_v6_sd_ota.ld"))
|
||||
load_post_script(env)
|
||||
image = IntelHex()
|
||||
image.frombytes(bytes(range(256)), offset=env.app_base)
|
||||
image.write_hex_file(str(path))
|
||||
before = path.read_bytes()
|
||||
with mock.patch.dict(os.environ, process_env, clear=True):
|
||||
with self.assertRaisesRegex(RuntimeError, error):
|
||||
stamp(env.post_actions["$BUILD_DIR/${PROGNAME}.hex"], path, env)
|
||||
self.assertEqual(path.read_bytes(), before)
|
||||
|
||||
def test_hex_hook_appends_valid_identity_and_preserves_other_segments(self):
|
||||
for app_base in (ml.NRF52_APP_BASE_S140_V6, ml.NRF52_APP_BASE_S140_V7):
|
||||
with self.subTest(app_base=hex(app_base)), tempfile.TemporaryDirectory() as tmp:
|
||||
|
||||
@@ -8,6 +8,9 @@ import os
|
||||
from pathlib import Path
|
||||
import re
|
||||
import runpy
|
||||
import shutil
|
||||
import subprocess
|
||||
import tempfile
|
||||
import unittest
|
||||
from unittest import mock
|
||||
|
||||
@@ -44,6 +47,9 @@ class FakeEnvironment:
|
||||
def __getitem__(self, key):
|
||||
return self.values[key]
|
||||
|
||||
def get(self, key, default=None):
|
||||
return self.values.get(key, default)
|
||||
|
||||
def BoardConfig(self):
|
||||
return self.board
|
||||
|
||||
@@ -62,9 +68,11 @@ class FakeEnvironment:
|
||||
raise AssertionError(f"unexpected fake-SCons call: {values!r}")
|
||||
|
||||
|
||||
def run_selector(*, pioenv, board_values, project=ROOT, requested=False, legacy_flash=None):
|
||||
def run_selector(*, pioenv, board_values, project=ROOT, requested=False, legacy_flash=None,
|
||||
flags=(), cppdefines=()):
|
||||
board = FakeBoardConfig(board_values)
|
||||
environment = FakeEnvironment(project, pioenv, board)
|
||||
environment.values.update(BUILD_FLAGS=flags, CPPDEFINES=cppdefines)
|
||||
process_environment = (
|
||||
{"MESHCORE_NRF52_INTERNAL_BOOTLOADER_UPDATE": "1"}
|
||||
if requested else {}
|
||||
@@ -136,6 +144,42 @@ def c_integer(path, name):
|
||||
|
||||
|
||||
class Nrf52InternalBootloaderSelectorTest(unittest.TestCase):
|
||||
def test_legacy_sd_receiver_reserves_auth_without_hybrid_features(self):
|
||||
for flags, cppdefines in (("-D OTA_SD_STORE=1", ()),
|
||||
((), [("OTA_SD_STORE", 1)]),
|
||||
("-DOTA_SD_STORE -DOTA_SEEDER_ONLY -UOTA_SEEDER_ONLY", ())):
|
||||
with self.subTest(flags=flags, cppdefines=cppdefines):
|
||||
environment, output = run_selector(
|
||||
pioenv="RAK_4631_repeater_lora_ota_no_external_sensors",
|
||||
legacy_flash="1", flags=flags, cppdefines=cppdefines,
|
||||
board_values={"build.mcu": "nrf52840",
|
||||
"build.ldscript": "boards/nrf52840_s140_v6.ld"},
|
||||
)
|
||||
expected = str(ROOT / "boards/nrf52840_s140_v6_sd_ota.ld")
|
||||
self.assertEqual(environment.cppdefines, [])
|
||||
self.assertEqual(environment.board.updates, [("build.ldscript", expected)])
|
||||
self.assertEqual(environment.replacements, {"LDSCRIPT_PATH": expected})
|
||||
self.assertIn("reserved SD authorization", output)
|
||||
|
||||
def test_legacy_source_only_or_undefined_sd_keeps_ordinary_linker(self):
|
||||
for flags in ("-DOTA_SD_STORE -DOTA_SEEDER_ONLY=1",
|
||||
"-DOTA_SD_STORE -UOTA_SD_STORE",
|
||||
"-DOTA_SD_STORE -DOTA_SEEDER_ONLY=0"):
|
||||
with self.subTest(flags=flags):
|
||||
environment, _ = run_selector(
|
||||
pioenv=V6_TARGET, legacy_flash="1", flags=flags,
|
||||
board_values={"build.mcu": "nrf52840",
|
||||
"build.ldscript": "nrf52840_s140_v6.ld"},
|
||||
)
|
||||
self.assertEqual(environment.board.updates, [])
|
||||
self.assertEqual(environment.replacements, {})
|
||||
|
||||
def test_legacy_sd_unknown_reservation_map_fails_closed(self):
|
||||
with self.assertRaisesRegex(RuntimeError, "missing legacy SD authorization linker"):
|
||||
run_selector(pioenv=V7_TARGET, legacy_flash="1", flags="-DOTA_SD_STORE=1",
|
||||
board_values={"build.mcu": "nrf52840",
|
||||
"build.ldscript": "nrf52840_s140_v7.ld"})
|
||||
|
||||
def test_legacy_flash_preserves_linker_for_direct_and_release_builds(self):
|
||||
for requested in (False, True):
|
||||
with self.subTest(requested=requested):
|
||||
@@ -262,6 +306,79 @@ class Nrf52InternalBootloaderSelectorTest(unittest.TestCase):
|
||||
|
||||
|
||||
class Nrf52HybridLinkerContractTest(unittest.TestCase):
|
||||
def test_real_linker_markers_do_not_confuse_late_data_with_reservation(self):
|
||||
compiler, linker, nm = (shutil.which(name) for name in ("cc", "ld", "nm"))
|
||||
if not all((compiler, linker, nm)):
|
||||
self.skipTest("native cc, ld and nm are required for linker fixtures")
|
||||
# A large framework registration prefix moves .data beyond the auth
|
||||
# ABI even in an unreserved map. Only the explicit region markers can
|
||||
# distinguish it from a genuinely reserved receiver layout.
|
||||
assembly = """
|
||||
.section .persistent_magic,"aw"
|
||||
.long 0
|
||||
.section .persistent_data,"aw"
|
||||
.long 0
|
||||
.section .svc_data,"aw"
|
||||
.space 128
|
||||
.section .fs_data,"aw"
|
||||
.space 32
|
||||
.section .data,"aw"
|
||||
.long 0x3f800000
|
||||
"""
|
||||
common = """
|
||||
SECTIONS {
|
||||
.data : { __data_start__ = .; *(.data) } > RAM
|
||||
}
|
||||
"""
|
||||
with tempfile.TemporaryDirectory(prefix="meshcore-sd-auth-linker-") as directory:
|
||||
work = Path(directory)
|
||||
(work / "fixture.S").write_text(assembly, encoding="ascii")
|
||||
(work / "nrf52_common.ld").write_text(common, encoding="ascii")
|
||||
subprocess.run([compiler, "-c", str(work / "fixture.S"), "-o", str(work / "fixture.o")],
|
||||
check=True, capture_output=True, text=True, timeout=30)
|
||||
for suffix in ("v6", "v7", "v6_sd_ota", "v6_mota64", "v7_mota64"):
|
||||
with self.subTest(suffix=suffix):
|
||||
source = (ROOT / "boards" / f"nrf52840_s140_{suffix}.ld").read_text()
|
||||
# Native tools suffice: omit only the ARM C library group;
|
||||
# every production MEMORY/SECTIONS/ASSERT is kept verbatim.
|
||||
source = re.sub(r"^GROUP\([^\n]*\)\s*$", "", source, flags=re.M)
|
||||
(work / "fixture.ld").write_text(source, encoding="ascii")
|
||||
linked = subprocess.run([linker, "-T", "fixture.ld", "fixture.o", "-o", "fixture.elf"],
|
||||
cwd=work, text=True, capture_output=True, timeout=30)
|
||||
self.assertEqual(linked.returncode, 0, linked.stdout + linked.stderr)
|
||||
output = subprocess.check_output([nm, str(work / "fixture.elf")], text=True)
|
||||
symbols = {name: int(value, 16) for value, _kind, name in
|
||||
(line.split() for line in output.splitlines())}
|
||||
self.assertGreaterEqual(symbols["__data_start__"], 0x20006050)
|
||||
if suffix in ("v6", "v7"):
|
||||
self.assertNotIn("__mota_sd_auth_start__", symbols)
|
||||
self.assertNotIn("__mota_sd_auth_end__", symbols)
|
||||
else:
|
||||
self.assertEqual(symbols["__mota_sd_auth_start__"], 0x20006008)
|
||||
self.assertEqual(symbols["__mota_sd_auth_end__"], 0x20006050)
|
||||
|
||||
def test_sd_authorization_has_explicit_reserved_boundaries(self):
|
||||
for suffix in ("v6_sd_ota", "v6_mota64", "v7_mota64"):
|
||||
with self.subTest(suffix=suffix):
|
||||
path = ROOT / "boards" / f"nrf52840_s140_{suffix}.ld"
|
||||
regions = memory_regions(path)
|
||||
source = path.read_text(encoding="utf-8")
|
||||
self.assertEqual(regions["RAM"][0], 0x20006050)
|
||||
self.assertEqual(regions["PERSISTENT_RAM"], (0x20006000, 80))
|
||||
self.assertIn("__mota_sd_auth_start__ = ", source)
|
||||
self.assertIn("__mota_sd_auth_end__ = ", source)
|
||||
self.assertIn("ASSERT(ORIGIN(RAM) >= __mota_sd_auth_end__,", source)
|
||||
if suffix == "v6_sd_ota":
|
||||
self.assertEqual(regions["RAM"][1], 0x20040000 - 0x20006050)
|
||||
self.assertIn("ASSERT(__mota_sd_auth_start__ == 0x20006008,", source)
|
||||
self.assertIn("ASSERT(__mota_sd_auth_end__ == 0x20006050,", source)
|
||||
else:
|
||||
self.assertIn("__mota_sd_auth_start__ = __mota_hybrid_handoff_start__;", source)
|
||||
self.assertIn("__mota_sd_auth_end__ = __mota_hybrid_handoff_end__;", source)
|
||||
for version in ("v6", "v7"):
|
||||
source = (ROOT / "boards" / f"nrf52840_s140_{version}.ld").read_text()
|
||||
self.assertNotIn("__mota_sd_auth_", source)
|
||||
|
||||
def test_v6_and_v7_maps_match_runtime_handoff_abi(self):
|
||||
handoff = ROOT / "src" / "helpers" / "ota" / "OtaHybridHandoff.h"
|
||||
layout = ROOT / "src" / "helpers" / "ota" / "OtaFlashLayout_nrf52.h"
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Execute the production SD auth helpers and constructor with RAM canaries."""
|
||||
|
||||
from pathlib import Path
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import unittest
|
||||
|
||||
from test_replay_reset_integration import extract_braced
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
SOURCE = ROOT / "src/helpers/ota/OtaStoreSdNrf52.cpp"
|
||||
|
||||
|
||||
class OtaSdAuthOwnershipTest(unittest.TestCase):
|
||||
def test_source_and_receiver_ownership_and_queue_lifetimes(self):
|
||||
if not sys.platform.startswith("linux"):
|
||||
self.skipTest("fixed-address RAM canaries require Linux mmap")
|
||||
compiler = shutil.which("g++") or shutil.which("clang++")
|
||||
if compiler is None:
|
||||
self.skipTest("a host C++ compiler is required")
|
||||
source = SOURCE.read_text(encoding="utf-8")
|
||||
helpers = source.split("namespace {", 1)[1].split("} // namespace", 1)[0]
|
||||
constructor = extract_braced(source, "OtaStoreSdNrf52::OtaStoreSdNrf52()")
|
||||
cases = (
|
||||
("source_unreserved", True, None, None, False),
|
||||
("source_reserved", True, 0x20006008, 0x20006050, False),
|
||||
("receiver_unreserved", False, None, None, False),
|
||||
("receiver_wrong_start", False, 0x20006000, 0x20006050, False),
|
||||
("receiver_wrong_end", False, 0x20006008, 0x20006058, False),
|
||||
("receiver_reserved", False, 0x20006008, 0x20006050, True),
|
||||
)
|
||||
with tempfile.TemporaryDirectory(prefix="meshcore-sd-auth-owner-") as directory:
|
||||
work = Path(directory)
|
||||
generated = work / "production_auth.inc"
|
||||
generated.write_text(helpers + "\n" + constructor + "\n", encoding="ascii")
|
||||
for name, seeder, start, end, owns in cases:
|
||||
with self.subTest(case=name):
|
||||
binary = work / name
|
||||
flags = ["-DOTA_RAK15002_SD_STORE=1", "-DEXPECT_OWNS=" + str(int(owns))]
|
||||
if seeder:
|
||||
flags += ["-DOTA_SEEDER_ONLY=1"]
|
||||
if start is not None:
|
||||
flags += [f"-DTEST_AUTH_START={start}", f"-DTEST_AUTH_END={end}"]
|
||||
built = subprocess.run([
|
||||
compiler, "-std=c++17", "-Wall", "-Wextra", "-Werror",
|
||||
"-fsanitize=address,undefined", "-fno-sanitize-recover=all",
|
||||
"-fno-pie", "-no-pie", "-g", *flags,
|
||||
"-I", str(ROOT / "src"), "-I", str(work),
|
||||
str(ROOT / "test/fixtures/ota_sd_auth_ownership.cpp"),
|
||||
"-o", str(binary),
|
||||
], text=True, capture_output=True, timeout=60)
|
||||
self.assertEqual(built.returncode, 0, built.stdout + built.stderr)
|
||||
checked = subprocess.run([str(binary)], text=True, capture_output=True, timeout=10)
|
||||
self.assertEqual(checked.returncode, 0, checked.stdout + checked.stderr)
|
||||
self.assertIn("SD auth ownership and 64 queue lifetimes passed", checked.stdout)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in new issue
Block a user