Preserve SoftDevice memory side effects under nRF52 LTO

This commit is contained in:
mikecarper committed 2026-09-30 23:01:37 -07:00
1 parent d5853b9ca2
commit 91db37489a
3 files changed
+110

No files matched your search

+8
View File
@@ -1,5 +1,13 @@
Import("env")
# Apply the SoftDevice ABI guard to framework code as well as the application.
# A normal application include cannot protect Bluefruit's pairing and GATT
# calls, which are compiled in separate framework translation units.
env.AppendUnique(CCFLAGS=[
"-include",
env.subst("$PROJECT_DIR/src/helpers/nrf52/SoftDeviceSvcCompat.h"),
])
# This script is loaded as a ``pre:`` extra script. The Nordic platform's
# builder selects binutils ar/ranlib later while it creates the library build
# actions. Those tools do not generate the plugin index required by GNU LTO,
+28
View File
@@ -0,0 +1,28 @@
#pragma once
#include <stdint.h>
// Nordic's SDK SVC wrappers contain assembly without a memory clobber. With
// whole-program optimization, GCC can infer that the wrappers neither read
// nor write the structures passed to the SoftDevice. That assumption is false
// for GAP pairing keys, GATT notifications, and many other supervisor calls.
// Keep each SVC opaque to its callers and declare its memory side effects.
// This header is force-included in nRF52 C and C++ compilation, including the
// framework, before nrf_svc.h defines SVCALL.
#if defined(NRF52_PLATFORM) && defined(__GNUC__) \
&& !defined(SVCALL_AS_NORMAL_FUNCTION) && !defined(SVCALL)
#if __GNUC__ >= 9
#define MESHCORE_SVC_ATTRIBUTES __attribute__((naked, unused, noipa))
#else
#define MESHCORE_SVC_ATTRIBUTES __attribute__((naked, unused, noinline))
#endif
#define SVCALL(number, return_type, signature) \
_Pragma("GCC diagnostic push") \
_Pragma("GCC diagnostic ignored \"-Wreturn-type\"") \
MESHCORE_SVC_ATTRIBUTES static return_type signature { \
__asm volatile ("svc %0\n" "bx r14" : : "I" ((uint16_t)(number)) \
: "r0", "memory"); \
} \
_Pragma("GCC diagnostic pop")
#endif
+74
View File
@@ -0,0 +1,74 @@
"""Regression for pointer side effects hidden by Nordic's SVC assembly."""
from pathlib import Path
import os
import shutil
import subprocess
import tempfile
import unittest
ROOT = Path(__file__).resolve().parents[1]
HEADER = ROOT / "src/helpers/nrf52/SoftDeviceSvcCompat.h"
CORE = Path(os.environ.get("PLATFORMIO_CORE_DIR", Path.home() / ".platformio"))
BIN = CORE / "packages/toolchain-gccarmnoneeabi/bin"
PROBE = r'''
#include <stdint.h>
SVCALL(0x80, uint32_t, system_call(uint32_t *value));
__attribute__((used, noinline)) uint32_t read_output(void) {
uint32_t value = 0;
system_call(&value);
return value;
}
__attribute__((used, noinline)) uint32_t write_input(void) {
uint32_t value = 0x12345678;
return system_call(&value);
}
'''
class Nrf52SvcCompatTest(unittest.TestCase):
def test_framework_and_application_receive_the_guard(self):
script = (ROOT / "scripts/nrf52_lto_toolchain.py").read_text()
self.assertIn('env.AppendUnique(CCFLAGS=[', script)
self.assertIn('"-include"', script)
self.assertIn('SoftDeviceSvcCompat.h', script)
self.assertIn('pre:scripts/nrf52_lto_toolchain.py',
(ROOT / "platformio.ini").read_text())
def test_lto_preserves_supervisor_input_and_output_memory(self):
cc = shutil.which("arm-none-eabi-gcc") or str(BIN / "arm-none-eabi-gcc")
dump = shutil.which("arm-none-eabi-objdump") or str(BIN / "arm-none-eabi-objdump")
if not Path(cc).is_file() or not Path(dump).is_file():
self.skipTest("ARM compiler required for the LTO regression")
with tempfile.TemporaryDirectory(prefix="meshcore-svc-lto-") as temp:
folder = Path(temp)
probe = folder / "probe.c"
probe.write_text(PROBE)
# Reproduce the SDK wrapper's missing IPA and memory protections.
broken = folder / "broken.h"
broken.write_text(HEADER.read_text().replace(", noipa", "")
.replace(', "memory"', ""))
assemblies = {}
for name, header in (("fixed", HEADER), ("broken", broken)):
elf = folder / (name + ".elf")
result = subprocess.run([
cc, "-DNRF52_PLATFORM", "-include", str(header),
"-mcpu=cortex-m4", "-mthumb", "-Os", "-flto",
"-fipa-pta", "-fmerge-all-constants", "-nostdlib",
"-Wl,-e,read_output", str(probe), "-o", str(elf),
], capture_output=True, text=True)
self.assertEqual(result.returncode, 0, result.stderr)
assemblies[name] = subprocess.check_output([
dump, "-d", "--disassemble=read_output", str(elf),
], text=True)
# The broken wrapper returns a compile-time zero after the SVC;
# the fixed wrapper reloads the value written by the supervisor.
self.assertRegex(assemblies["fixed"], r"\bldr\s+r0,\s*\[sp")
self.assertNotRegex(assemblies["broken"], r"\bldr\s+r0,\s*\[sp")
self.assertRegex(assemblies["fixed"], r"\bstr\s+r\d+,\s*\[sp")
self.assertNotRegex(assemblies["broken"], r"\bstr\s+r\d+,\s*\[sp")
if __name__ == "__main__":
unittest.main()