Fix nRF52 loop stack allocation under LTO

This commit is contained in:
mikecarper committed 2026-10-04 01:54:01 -07:00
1 parent 63f4642252
commit 8586ef5f51
4 files changed
+157 -1

No files matched your search

+97
View File
@@ -0,0 +1,97 @@
"""Reproduce the LTO wrapper bypass and execute the patched core task policy."""
import importlib.util
from pathlib import Path
import shutil
import subprocess
import tempfile
import unittest
ROOT = Path(__file__).resolve().parents[1]
SPEC = importlib.util.spec_from_file_location(
"nrf52_loop_stack_fix", ROOT / "scripts/nrf52_loop_stack_fix.py")
PATCH = importlib.util.module_from_spec(SPEC)
SPEC.loader.exec_module(PATCH)
RTOS = """#pragma once
#include <cstdint>
using BaseType_t = int;
using UBaseType_t = unsigned;
using TaskHandle_t = void*;
using TaskFunction_t = void (*)(void*);
using configSTACK_DEPTH_TYPE = uint16_t;
constexpr BaseType_t pdPASS = 1;
extern "C" BaseType_t xTaskCreate(TaskFunction_t, const char*,
configSTACK_DEPTH_TYPE, void*, UBaseType_t, TaskHandle_t*);
"""
CORE = """#include "Arduino.h"
#include <cassert>
#include <cstddef>
#define LOOP_STACK_SZ (256*4)
static TaskHandle_t _loopHandle;
static void loop_task(void*) {}
static unsigned last_depth;
constexpr unsigned TASK_PRIO_LOW = 1;
extern "C" BaseType_t xTaskCreate(TaskFunction_t, const char*,
configSTACK_DEPTH_TYPE depth, void*, UBaseType_t, TaskHandle_t*) {
last_depth = depth;
return pdPASS;
}
int main() {
xTaskCreate(loop_task, "loop", LOOP_STACK_SZ, NULL, TASK_PRIO_LOW, &_loopHandle);
assert(last_depth == EXPECTED_LOOP_WORDS);
xTaskCreate(nullptr, "usbd", 200, nullptr, 3, nullptr);
assert(last_depth == 200);
xTaskCreate(nullptr, "callback", 768, nullptr, 1, nullptr);
assert(last_depth == 768);
}
"""
class Nrf52LoopStackFixTest(unittest.TestCase):
def test_patch_is_idempotent_and_rejects_changed_or_partial_core(self):
patched = PATCH.patched_source(CORE.replace("\n", "\r\n"))
self.assertEqual(PATCH.patched_source(patched), patched)
for source in (
CORE.replace("LOOP_STACK_SZ (256*4)", "LOOP_STACK_SZ 4096"),
CORE.replace(PATCH.OLD_LOOP_CREATE, PATCH.LOOP_CREATE),
CORE.replace(PATCH.LOOP_STACK, PATCH.DECLARATION + PATCH.LOOP_STACK),
CORE + "\n" + PATCH.OLD_LOOP_CREATE,
):
with self.subTest(source=source), self.assertRaises(RuntimeError):
PATCH.patched_source(source)
@unittest.skipUnless(shutil.which("c++"), "native C++ compiler unavailable")
def test_lto_bypass_is_reproduced_then_fixed_without_resizing_other_tasks(self):
with tempfile.TemporaryDirectory(prefix="meshcore-loop-stack-") as temp:
temp = Path(temp)
(temp / "Arduino.h").write_text(RTOS)
for name in ("FreeRTOS.h", "task.h"):
(temp / name).write_text('#include "Arduino.h"\n')
core, binary = temp / "main.cpp", temp / "core"
for patched, configured_words in ((False, 2048), (True, 2048), (True, 3072)):
with self.subTest(patched=patched, configured_words=configured_words):
core.write_text(PATCH.patched_source(CORE) if patched else CORE)
expected = configured_words if patched else 1024
built = subprocess.run([
"c++", "-std=c++17", "-O2", "-flto", "-flto-partition=one",
"-fno-semantic-interposition", "-DNRF52_PLATFORM=1",
f"-DMESH_NRF52_LOOP_STACK_WORDS={configured_words}",
f"-DEXPECTED_LOOP_WORDS={expected}", "-I" + str(temp),
str(core), str(ROOT / "src/Nrf52LoopStack.cpp"),
"-Wl,--wrap=xTaskCreate", "-o", str(binary),
], text=True, capture_output=True)
self.assertEqual(built.returncode, 0, built.stderr)
result = subprocess.run([str(binary)], text=True, capture_output=True)
self.assertEqual(result.returncode, 0, result.stderr)
def test_every_nrf52_build_uses_private_core_patch(self):
config = (ROOT / "platformio.ini").read_text()
nrf = config.split("[nrf52_base]", 1)[1].split("\n[", 1)[0]
self.assertIn("pre:scripts/nrf52_loop_stack_fix.py", nrf)
self.assertEqual(config.count("pre:scripts/nrf52_loop_stack_fix.py"), 1)
if __name__ == "__main__":
unittest.main()