Fix nRF52 Bluetooth DFU stack handoff
This commit is contained in:
1 parent
3e3c4bdd4e
commit
65a8cfd721
5 files changed
+483
No files matched your search
@@ -0,0 +1,115 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Reject nRF52 ELFs with an unsafe compiled buttonless Bluetooth DFU handoff."""
|
||||
|
||||
import argparse
|
||||
from pathlib import Path
|
||||
import struct
|
||||
import sys
|
||||
|
||||
|
||||
SCRIPT_DIR = Path(__file__).resolve().parent if "__file__" in globals() else Path("scripts").resolve()
|
||||
sys.path.insert(0, str(SCRIPT_DIR))
|
||||
from firmware_elf import FirmwareElf
|
||||
|
||||
|
||||
HELPER_NAME = "mesh_nrf52_dfu_jump"
|
||||
# Assembled Cortex-M4/Thumb helper. These instructions are entirely local: no
|
||||
# relocation or compiler-generated C stack access can change with image layout.
|
||||
# The only post-transition stack writes construct the explicit handler-mode
|
||||
# exception frame. The normal FreeRTOS thread path performs no stack access.
|
||||
HELPER_CODE = bytes.fromhex(
|
||||
"72b6eff305830168426881f30888002080f3148880f3118880f31388"
|
||||
"bff34f8fbff36f8f002b03d16ff0000e62b6104788b0009001900290"
|
||||
"039004906ff00000059006924ff0045007906ff0060e62b67047"
|
||||
)
|
||||
|
||||
|
||||
def branch_targets(code, start):
|
||||
"""Decode direct Thumb-2 BL/B.W targets without depending on objdump."""
|
||||
for offset in range(0, len(code) - 3, 2):
|
||||
first, second = struct.unpack_from("<HH", code, offset)
|
||||
if first & 0xF800 != 0xF000 or second & 0xD000 not in (0x9000, 0xD000):
|
||||
continue
|
||||
sign = (first >> 10) & 1
|
||||
i1 = 1 ^ ((second >> 13) & 1) ^ sign
|
||||
i2 = 1 ^ ((second >> 11) & 1) ^ sign
|
||||
immediate = (sign << 24) | (i1 << 23) | (i2 << 22)
|
||||
immediate |= (first & 0x3FF) << 12 | (second & 0x7FF) << 1
|
||||
if sign:
|
||||
immediate -= 1 << 25
|
||||
yield start + offset + 4 + immediate
|
||||
|
||||
|
||||
def writes_control(code):
|
||||
for offset in range(0, len(code) - 3, 2):
|
||||
first, second = struct.unpack_from("<HH", code, offset)
|
||||
if first & 0xFFF0 == 0xF380 and second == 0x8814:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def check_firmware(path):
|
||||
elf = FirmwareElf(path)
|
||||
callbacks = [name for name in elf.symbols
|
||||
if "bledfu_control_wr_authorize_cb" in name]
|
||||
if not callbacks and HELPER_NAME not in elf.symbols:
|
||||
return False # A firmware without the DFU service has no handoff to gate.
|
||||
if HELPER_NAME not in elf.symbols:
|
||||
raise ValueError("Bluetooth DFU callback lacks the safe stack-transition helper")
|
||||
address, size, _ = elf.symbols[HELPER_NAME]
|
||||
address &= ~1 # ELF Thumb function values carry the low bit.
|
||||
if size != len(HELPER_CODE) or elf.read(address, size) != HELPER_CODE:
|
||||
raise ValueError("Bluetooth DFU helper differs from the qualified assembly")
|
||||
if not callbacks:
|
||||
raise ValueError("Bluetooth DFU helper is present without its control callback")
|
||||
for name in callbacks:
|
||||
start, size, _ = elf.symbols[name]
|
||||
start &= ~1
|
||||
if not size:
|
||||
raise ValueError("Bluetooth DFU callback has no inspectable machine code")
|
||||
code = elf.read(start, size)
|
||||
if writes_control(code):
|
||||
raise ValueError("Bluetooth DFU callback changes CONTROL inside a C stack frame")
|
||||
if address not in branch_targets(code, start):
|
||||
raise ValueError("Bluetooth DFU callback does not branch to the safe helper")
|
||||
return True
|
||||
|
||||
|
||||
def register_platformio(env):
|
||||
def check(source, target, env):
|
||||
path = Path(env.subst("$BUILD_DIR/${PROGNAME}.elf"))
|
||||
try:
|
||||
enabled = check_firmware(path)
|
||||
except (OSError, ValueError, KeyError, struct.error) as error:
|
||||
print(f"nRF52 Bluetooth DFU qualification failed: {error}", file=sys.stderr)
|
||||
return 2
|
||||
print("nRF52 Bluetooth DFU: " + ("safe compiled handoff PASS" if enabled
|
||||
else "service not linked"))
|
||||
return 0
|
||||
|
||||
env.AddPostAction("$BUILD_DIR/${PROGNAME}.elf", check)
|
||||
for alias in ("checkprogsize", "upload"):
|
||||
env.AddPreAction(env.Alias(alias), check)
|
||||
for suffix in ("bin", "hex", "uf2", "zip"):
|
||||
env.AddPreAction("$BUILD_DIR/${PROGNAME}." + suffix, check)
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("elf", type=Path)
|
||||
args = parser.parse_args()
|
||||
try:
|
||||
enabled = check_firmware(args.elf)
|
||||
except (OSError, ValueError, KeyError, struct.error) as error:
|
||||
parser.exit(1, f"nRF52 Bluetooth DFU qualification failed: {error}\n")
|
||||
print("nRF52 Bluetooth DFU: " + ("safe compiled handoff PASS" if enabled
|
||||
else "service not linked"))
|
||||
|
||||
|
||||
try:
|
||||
Import("env") # noqa: F821 -- PlatformIO/SCons
|
||||
except NameError:
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
else:
|
||||
register_platformio(env) # noqa: F821
|
||||
@@ -0,0 +1,119 @@
|
||||
"""Give the pinned Bluefruit DFU service a safe FreeRTOS bootloader handoff.
|
||||
|
||||
The original callback selects MSP while its C frame is still on PSP. GCC 14
|
||||
then emits a stack restore on MSP before reaching the bootloader jump, outside
|
||||
the nRF52840 SRAM boundary. Keep the legacy B1/retained-peer protocol, but move
|
||||
the stack transition into a naked, nonreturning assembly helper. Patch a private
|
||||
build copy; never modify PlatformIO's shared framework cache.
|
||||
"""
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
OLD_HELPER = 'extern "C" void bootloader_util_app_start(uint32_t start_addr);\n'
|
||||
HELPER_NAME = "mesh_nrf52_dfu_jump"
|
||||
HELPER = r'''// Keep all stack operations inside this assembly-only transition. In particular,
|
||||
// the callback must return neither through its PSP frame nor through a C helper
|
||||
// after CONTROL selects MSP. Preserve handler-mode entry supported by BLEDfu.
|
||||
extern "C" __attribute__((naked, noinline, noreturn, used))
|
||||
void mesh_nrf52_dfu_jump(uint32_t start_addr)
|
||||
{
|
||||
__asm volatile (
|
||||
"cpsid i\n"
|
||||
"mrs r3, ipsr\n"
|
||||
"ldr r1, [r0]\n"
|
||||
"ldr r2, [r0, #4]\n"
|
||||
"msr msp, r1\n"
|
||||
"movs r0, #0\n"
|
||||
"msr control, r0\n"
|
||||
"msr basepri, r0\n"
|
||||
"msr faultmask, r0\n"
|
||||
"dsb\n"
|
||||
"isb\n"
|
||||
"cmp r3, #0\n"
|
||||
"bne 1f\n"
|
||||
"mvn lr, #0\n"
|
||||
"cpsie i\n"
|
||||
"bx r2\n"
|
||||
"1:\n"
|
||||
"sub sp, sp, #32\n"
|
||||
"str r0, [sp, #0]\n"
|
||||
"str r0, [sp, #4]\n"
|
||||
"str r0, [sp, #8]\n"
|
||||
"str r0, [sp, #12]\n"
|
||||
"str r0, [sp, #16]\n"
|
||||
"mvn r0, #0\n"
|
||||
"str r0, [sp, #20]\n"
|
||||
"str r2, [sp, #24]\n"
|
||||
"mov r0, #0x21000000\n"
|
||||
"str r0, [sp, #28]\n"
|
||||
"mvn lr, #6\n"
|
||||
"cpsie i\n"
|
||||
"bx lr\n"
|
||||
);
|
||||
}
|
||||
'''
|
||||
|
||||
OLD_INTERRUPTS = ''' // Disable all interrupts
|
||||
NVIC->ICER[0]=0xFFFFFFFF;
|
||||
NVIC->ICPR[0]=0xFFFFFFFF;
|
||||
#if defined(__NRF_NVIC_ISER_COUNT) && __NRF_NVIC_ISER_COUNT == 2
|
||||
NVIC->ICER[1]=0xFFFFFFFF;
|
||||
NVIC->ICPR[1]=0xFFFFFFFF;
|
||||
#endif
|
||||
'''
|
||||
INTERRUPTS = ''' // nRF52 has external IRQs in both banks. FreeRTOS system exceptions
|
||||
// need separate cleanup: they are not covered by NVIC->ICER/ICPR.
|
||||
SysTick->CTRL = 0;
|
||||
SysTick->LOAD = 0;
|
||||
SysTick->VAL = 0;
|
||||
SCB->ICSR = SCB_ICSR_PENDSTCLR_Msk | SCB_ICSR_PENDSVCLR_Msk;
|
||||
NVIC->ICER[0] = 0xFFFFFFFF;
|
||||
NVIC->ICER[1] = 0xFFFFFFFF;
|
||||
NVIC->ICPR[0] = 0xFFFFFFFF;
|
||||
NVIC->ICPR[1] = 0xFFFFFFFF;
|
||||
__DSB();
|
||||
__ISB();
|
||||
'''
|
||||
OLD_JUMP = ''' __set_CONTROL(0); // switch to MSP, required if using FreeRTOS
|
||||
bootloader_util_app_start(NRF_UICR->NRFFW[0]);
|
||||
'''
|
||||
JUMP = ''' // The vector-table SVC above must complete before the helper masks
|
||||
// interrupts. B1 and the retained peer record preserve legacy DFU entry.
|
||||
mesh_nrf52_dfu_jump(NRF_UICR->NRFFW[0]);
|
||||
'''
|
||||
|
||||
|
||||
def patched_source(source):
|
||||
source = source.replace("\r\n", "\n")
|
||||
replacements = ((OLD_HELPER, HELPER), (OLD_INTERRUPTS, INTERRUPTS),
|
||||
(OLD_JUMP, JUMP))
|
||||
if all(new in source and old not in source for old, new in replacements):
|
||||
return source
|
||||
if any(source.count(old) != 1 for old, _ in replacements):
|
||||
raise RuntimeError(
|
||||
"nRF52 BLE DFU handoff: unrecognized framework source; "
|
||||
"review the framework update before building"
|
||||
)
|
||||
for old, new in replacements:
|
||||
source = source.replace(old, new)
|
||||
return source
|
||||
|
||||
|
||||
def replace_framework_source(build_env, node):
|
||||
source = Path(node.srcnode().get_abspath())
|
||||
if source.name != "BLEDfu.cpp" or source.parent.name != "services":
|
||||
return node
|
||||
patched = patched_source(source.read_text(encoding="utf-8"))
|
||||
build_env.AppendUnique(CPPPATH=[str(source.parent), str(source.parent.parent)])
|
||||
destination = Path(build_env.subst("$BUILD_DIR")) / "patched-nrf52-ble" / source.name
|
||||
destination.parent.mkdir(parents=True, exist_ok=True)
|
||||
if not destination.exists() or destination.read_text(encoding="utf-8") != patched:
|
||||
destination.write_text(patched, encoding="utf-8")
|
||||
print("nRF52 BLE: private FreeRTOS-safe DFU handoff")
|
||||
return build_env.File(str(destination))
|
||||
|
||||
|
||||
if "Import" in globals():
|
||||
Import("env")
|
||||
env.AddBuildMiddleware(replace_framework_source, "*BLEDfu.cpp")
|
||||
Reference in new issue
Block a user