Avoid flash load image for the empty contact-path cache

This commit is contained in:
mikecarper committed 2026-10-17 11:57:05 -07:00
1 parent dcabcb8cb2
commit 53835c0793
4 files changed
+73

No files matched your search

+34
View File
@@ -401,6 +401,40 @@ App CLI replies remain bounded and supply a continuation command.
The physical RAK passed all 21 chapters, ranges, app-style continuation replies,
a paused USB reader exceeding the 4 KiB queue, and disconnect cancellation.
### Empty contact-path cache initialization
The contact-path pool now has an explicit runtime constructor. Its existing
member defaults still initialize every empty handle and resident entry, and
`Slot()` still resets released handles. This moves the local static pool from
initialized `.data` to zero-initialized `.bss`: the vtable pointer and repeated
`NONE`/255 slot markers no longer require a 5,780-byte flash load image. The
pool is constructed on first use by the existing main-loop contact owner.
Its layout, 390 handle slots, 16 resident paths, reference/snapshot behavior
and persistent contact records are unchanged.
The RAK4631 Full release recipe at `c0672b90`, with the full
`v1.17.1.10-halo-keymind-cascade-dev` version and commit suffix, measured:
| Measurement | Before | Runtime pool construction |
| --- | ---: | ---: |
| Linked initialized flash bytes | 716,448 | 710,908 |
| Application image including EndF | 716,528 | 710,988 |
| Linked available heap bytes | 79,388 | 79,388 |
| Required runtime heap bytes | 74,576 | 74,576 |
The net flash saving is 5,540 bytes after constructor/guard code. The image
passes the existing 712,704-byte application ceiling, capability checks, RAM
budget and DFU handoff checks. Its padded UF2 ends at `0xD3A00`, below the
unchanged `0xD4000` boundary. Sensors, BLE/USB/Ethernet, SD, logging, Meshtext,
OTA, Reader data and the 8 KiB loop stack remain enabled.
The contact-cache test compiles a real static pool and checks its zero-initialized
storage, alongside the existing sanitized eviction, snapshot, migration and
transaction tests. The compiled ARM Companion check also rejects a pool in an
initialized ELF section, so compiler promotion cannot silently restore the
flash image. Its normal linked crypto vectors and Reader retention checks
remain required.
## Release evidence and regression tests
Full Companions without PSRAM also use
+4
View File
@@ -45,6 +45,9 @@ def check(path):
reader = "_ZN4mesh5bible9generatedL10readerDataE"
if reader not in elf.symbols:
raise ValueError("Companion Reader corpus is missing")
path_cache = elf.symbols.get("_ZZN4mesh18contactPathStorageEvE4pool")
if path_cache and elf.sections[path_cache[2]][1] != 8: # SHT_NOBITS
raise ValueError("Contact path cache has a flash load image; expected zero-initialized storage")
uc = Uc(UC_ARCH_ARM, UC_MODE_THUMB | UC_MODE_MCLASS)
uc.mem_map(0, 0x100000)
uc.mem_map(0x20000000, 0x40000)
@@ -100,6 +103,7 @@ def check(path):
"passed": True, "elf_sha256": hashlib.sha256(elf.data).hexdigest(),
"base_table_bytes": elf.symbols["base"][1],
"reader_data_bytes": elf.symbols[reader][1], "vectors": rows,
"zero_initialized_path_cache_bytes": path_cache[1] if path_cache else 0,
"stack_limit_bytes": 2048,
"scope": "Crypto function frames only; excludes caller/ISR/task stacks and physical timing.",
}
+5
View File
@@ -170,6 +170,11 @@ class ContactPathPool : public ContactPathStorage {
}
public:
// Construct the empty cache on first use instead of storing a flash load
// image of every slot's NONE/255 sentinels and the vtable pointer. Keep the
// member defaults for both initial construction and Slot() resets.
ContactPathPool() {}
void attach(ContactPathBackend* backend) override { _backend = backend; }
uint16_t bind(uint16_t location, const uint8_t path[64]) override {
if (location == NONE) return NONE;
+30
View File
@@ -1,6 +1,7 @@
#!/usr/bin/env python3
"""Exercise real contact handles, cache policy and persistence under sanitizers."""
from pathlib import Path
import re
import subprocess
import tempfile
import unittest
@@ -22,6 +23,35 @@ def crypto_objects(temp):
class ContactCacheTest(unittest.TestCase):
def test_empty_path_pool_has_no_flash_load_image(self):
# The 390-handle pool must occupy zero-initialized storage, not a
# multi-KiB initialized-data image containing repeated empty markers.
# Compile a real static instance; source-shape checks cannot establish
# whether the compiler emits a flash initializer for it.
with tempfile.TemporaryDirectory(prefix="mesh-path-pool-image-") as directory:
source = Path(directory) / "pool.cpp"
obj = Path(directory) / "pool.o"
source.write_text('''#include <helpers/ContactPathCache.h>
mesh::ContactPathStorage& testPathStorage() {
static mesh::ContactPathPool<390> pool;
return pool;
}
''', encoding="ascii")
compiled = subprocess.run([
"c++", "-std=c++17", "-Os", "-fno-threadsafe-statics",
"-fno-rtti", "-fno-exceptions", "-I", str(ROOT / "src"),
"-c", str(source), "-o", str(obj),
], capture_output=True, text=True)
self.assertEqual(compiled.returncode, 0, compiled.stdout + compiled.stderr)
symbols = subprocess.run(["nm", "-S", "-C", "--defined-only", str(obj)],
capture_output=True, text=True)
self.assertEqual(symbols.returncode, 0, symbols.stdout + symbols.stderr)
match = re.search(r"^\S+\s+([0-9a-fA-F]+)\s+(\w) testPathStorage\(\)::pool$",
symbols.stdout, re.MULTILINE)
self.assertIsNotNone(match, symbols.stdout)
self.assertGreater(int(match[1], 16), 5000)
self.assertEqual(match[2].lower(), "b", symbols.stdout)
def test_default_policy_covers_high_contact_nrf52_without_full(self):
cases = (
({"NRF52_PLATFORM": 1, "MAX_CONTACTS": 350}, 1),