mirror of
https://github.com/mikecarper/MeshCore.git
synced 2026-10-23 20:22:26 +00:00
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
@@ -194,10 +194,13 @@ jobs:
|
||||
- name: Verify nRF52 SoftDevice LTO and persistent BLE subscriptions
|
||||
env:
|
||||
MESHCORE_REQUIRE_ARM_COMPILER: '1'
|
||||
MESHCORE_REQUIRE_DFU_EMULATOR: '1'
|
||||
run: |
|
||||
pio pkg install --global --tool 'platformio/toolchain-gccarmnoneeabi@1.140201.0'
|
||||
python3 -B test/test_nrf52_svc_compat.py -v
|
||||
python3 -B test/test_nrf52_ble_cccd_fix.py -v
|
||||
pip install unicorn==2.1.4
|
||||
python3 -B test/test_nrf52_ble_dfu_fix.py -v
|
||||
|
||||
- name: Verify WiFi, ESP-NOW and combined wireless controls
|
||||
run: |
|
||||
|
||||
@@ -156,10 +156,12 @@ extra_scripts = create-uf2.py
|
||||
pre:scripts/nrf52_usb_power_fix.py
|
||||
pre:scripts/nrf52_internal_flash_fix.py
|
||||
pre:scripts/nrf52_ble_cccd_fix.py
|
||||
pre:scripts/nrf52_ble_dfu_fix.py
|
||||
pre:scripts/nrf52_internal_bootloader_link.py
|
||||
; Nordic's builder otherwise overwrites the LTO-aware archive tools.
|
||||
pre:scripts/nrf52_lto_toolchain.py
|
||||
post:scripts/check_firmware_ram.py
|
||||
post:scripts/check_nrf52_ble_dfu_handoff.py
|
||||
; Nordic's framework defaults to -Ofast. Prefer the size-oriented profile for
|
||||
; nRF52 images, where a smaller binary preserves OTA and filesystem headroom.
|
||||
build_unflags =
|
||||
|
||||
@@ -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")
|
||||
@@ -0,0 +1,244 @@
|
||||
"""Compile and execute the Bluetooth DFU stack-transition regression."""
|
||||
|
||||
from importlib.util import module_from_spec, spec_from_file_location
|
||||
import os
|
||||
from pathlib import Path
|
||||
import shutil
|
||||
import struct
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import unittest
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
sys.path.insert(0, str(ROOT / "scripts"))
|
||||
from check_nrf52_ble_dfu_handoff import (HELPER_CODE, HELPER_NAME, check_firmware,
|
||||
writes_control)
|
||||
from firmware_elf import FirmwareElf
|
||||
|
||||
spec = spec_from_file_location("nrf52_ble_dfu_fix", ROOT / "scripts/nrf52_ble_dfu_fix.py")
|
||||
patch = module_from_spec(spec)
|
||||
spec.loader.exec_module(patch)
|
||||
|
||||
try:
|
||||
import unicorn
|
||||
from unicorn import arm_const
|
||||
except ImportError:
|
||||
unicorn = None
|
||||
|
||||
|
||||
FIXTURE_BODY = r'''
|
||||
extern "C" void bledfu_control_wr_authorize_cb_fixture(uint32_t address) {
|
||||
volatile uint32_t frame[26];
|
||||
// The real callback saves these registers while preparing peer/bond data.
|
||||
// Keep that C epilogue so the old path reproduces its invalid MSP pop.
|
||||
__asm volatile ("" ::: "r4", "r5", "r6", "r7", "r8");
|
||||
frame[0] = address;
|
||||
uint32_t bootloader = frame[0];
|
||||
HANDOFF
|
||||
}
|
||||
'''
|
||||
BROKEN_HELPER = r'''
|
||||
extern "C" __attribute__((noinline, used))
|
||||
void bootloader_util_app_start(uint32_t address) {
|
||||
__asm volatile ("ldr r1, [%0]\n"
|
||||
"msr msp, r1\n"
|
||||
"ldr r1, [%0, #4]\n"
|
||||
"bx r1\n" :: "r" (address) : "r1");
|
||||
}
|
||||
'''
|
||||
|
||||
|
||||
def arm_compiler():
|
||||
core = Path(os.environ.get("PLATFORMIO_CORE_DIR", Path.home() / ".platformio"))
|
||||
candidates = [os.environ.get("MESHCORE_ARM_GXX", ""),
|
||||
str(core / "packages/toolchain-gccarmnoneeabi/bin/arm-none-eabi-g++"),
|
||||
str(Path.home() / ".local/opt/arm-gnu-toolchain-14.2.rel1-x86_64-arm-none-eabi/bin/arm-none-eabi-g++"),
|
||||
shutil.which("arm-none-eabi-g++") or ""]
|
||||
return next((candidate for candidate in candidates if candidate and Path(candidate).is_file()), None)
|
||||
|
||||
|
||||
class DfuSourcePatchTest(unittest.TestCase):
|
||||
def fixture(self):
|
||||
return (patch.OLD_HELPER + "// retained peer preparation and B1 stay here\n"
|
||||
+ patch.OLD_INTERRUPTS
|
||||
+ " VERIFY_STATUS( sd_softdevice_vector_table_base_set(NRF_UICR->NRFFW[0]), );\n"
|
||||
+ patch.OLD_JUMP)
|
||||
|
||||
def test_patch_is_idempotent_and_preserves_handoff_protocol(self):
|
||||
fixed = patch.patched_source(self.fixture())
|
||||
self.assertEqual(patch.patched_source(fixed), fixed)
|
||||
self.assertIn("retained peer preparation and B1 stay here", fixed)
|
||||
self.assertNotIn("__set_CONTROL(0)", fixed)
|
||||
self.assertIn("sd_softdevice_vector_table_base_set", fixed)
|
||||
self.assertLess(fixed.index("NVIC->ICER[1]"),
|
||||
fixed.index("sd_softdevice_vector_table_base_set"))
|
||||
self.assertLess(fixed.index("SCB_ICSR_PENDSVCLR_Msk"),
|
||||
fixed.index("sd_softdevice_vector_table_base_set"))
|
||||
self.assertLess(fixed.index("sd_softdevice_vector_table_base_set"),
|
||||
fixed.index("mesh_nrf52_dfu_jump(NRF_UICR"))
|
||||
|
||||
def test_unknown_or_partially_patched_framework_fails_closed(self):
|
||||
for original in ("unrecognized framework", self.fixture().replace(
|
||||
patch.OLD_JUMP, patch.JUMP), self.fixture().replace(
|
||||
"NVIC->ICPR[0]=0xFFFFFFFF;", "NVIC->ICPR[0]=0;")):
|
||||
with self.subTest(original=original), self.assertRaises(RuntimeError):
|
||||
patch.patched_source(original)
|
||||
|
||||
def test_every_nrf52_build_uses_patch_and_machine_gate(self):
|
||||
ini = (ROOT / "platformio.ini").read_text()
|
||||
base = ini.split("[nrf52_base]", 1)[1].split("\n[", 1)[0]
|
||||
self.assertIn("pre:scripts/nrf52_ble_dfu_fix.py", base)
|
||||
self.assertIn("post:scripts/check_nrf52_ble_dfu_handoff.py", base)
|
||||
|
||||
|
||||
class DfuCompiledHandoffTest(unittest.TestCase):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
cls.compiler = arm_compiler()
|
||||
if cls.compiler is None:
|
||||
if os.environ.get("MESHCORE_REQUIRE_ARM_COMPILER") == "1":
|
||||
raise RuntimeError("ARM compiler required for the DFU regression")
|
||||
raise unittest.SkipTest("ARM compiler required for the DFU regression")
|
||||
cls.folder = tempfile.TemporaryDirectory(prefix="meshcore-dfu-handoff-")
|
||||
cls.path = Path(cls.folder.name)
|
||||
cls.images = {}
|
||||
for optimize, lto in (("-Os", False), ("-Oz", False), ("-Oz", True)):
|
||||
for fixed in (False, True):
|
||||
name = f"{'fixed' if fixed else 'broken'}-{optimize[1:]}-{'lto' if lto else 'plain'}"
|
||||
source, elf = cls.path / (name + ".cpp"), cls.path / (name + ".elf")
|
||||
handoff = ("mesh_nrf52_dfu_jump(bootloader);" if fixed else
|
||||
'__asm volatile ("movs r1, #0\\nmsr control, r1\\n" ::: "r1");\n'
|
||||
" bootloader_util_app_start(bootloader);")
|
||||
source.write_text("#include <stdint.h>\n" + (patch.HELPER if fixed else BROKEN_HELPER)
|
||||
+ FIXTURE_BODY.replace("HANDOFF", handoff))
|
||||
command = [cls.compiler, "-mcpu=cortex-m4", "-mthumb", optimize,
|
||||
"-ffunction-sections", "-fdata-sections", "-nostdlib",
|
||||
"-Wl,--entry=bledfu_control_wr_authorize_cb_fixture,-Ttext=0x26000",
|
||||
str(source), "-o", str(elf)]
|
||||
if lto:
|
||||
command[1:1] = ["-flto", "-fipa-pta", "-fno-inline-small-functions"]
|
||||
subprocess.run(command, check=True, capture_output=True, text=True)
|
||||
cls.images[(optimize, lto, fixed)] = elf
|
||||
|
||||
@classmethod
|
||||
def tearDownClass(cls):
|
||||
cls.folder.cleanup()
|
||||
|
||||
def test_machine_gate_accepts_fixed_and_rejects_old_release_sequence(self):
|
||||
for key, path in self.images.items():
|
||||
with self.subTest(build=key):
|
||||
if key[2]:
|
||||
self.assertTrue(check_firmware(path))
|
||||
else:
|
||||
with self.assertRaisesRegex(ValueError, "safe stack-transition helper"):
|
||||
check_firmware(path)
|
||||
elf = FirmwareElf(path)
|
||||
address, size, _ = elf.symbols["bledfu_control_wr_authorize_cb_fixture"]
|
||||
self.assertTrue(writes_control(elf.read(address & ~1, size)))
|
||||
|
||||
def test_machine_gate_rejects_helper_prologue_or_modified_instruction(self):
|
||||
path = self.images[("-Oz", True, True)]
|
||||
original = path.read_bytes()
|
||||
self.assertEqual(original.count(HELPER_CODE), 1)
|
||||
damaged = self.path / "changed-helper.elf"
|
||||
damaged.write_bytes(original.replace(HELPER_CODE, b"\x00\xb5" + HELPER_CODE[2:]))
|
||||
with self.assertRaisesRegex(ValueError, "qualified assembly"):
|
||||
check_firmware(damaged)
|
||||
|
||||
def test_machine_gate_rejects_control_write_in_callback_with_valid_helper(self):
|
||||
path = self.images[("-Oz", True, True)]
|
||||
elf = FirmwareElf(path)
|
||||
address, size, _ = elf.symbols["bledfu_control_wr_authorize_cb_fixture"]
|
||||
code = elf.read(address & ~1, size)
|
||||
original = path.read_bytes()
|
||||
self.assertEqual(original.count(code), 1)
|
||||
damaged = self.path / "changed-callback.elf"
|
||||
damaged.write_bytes(original.replace(code, b"\x80\xf3\x14\x88" + code[4:]))
|
||||
with self.assertRaisesRegex(ValueError, "inside a C stack frame"):
|
||||
check_firmware(damaged)
|
||||
|
||||
def test_machine_gate_rejects_callback_which_bypasses_valid_helper(self):
|
||||
path = self.images[("-Oz", True, True)]
|
||||
elf = FirmwareElf(path)
|
||||
address, size, _ = elf.symbols["bledfu_control_wr_authorize_cb_fixture"]
|
||||
code = elf.read(address & ~1, size)
|
||||
original = path.read_bytes()
|
||||
self.assertEqual(original.count(code), 1)
|
||||
damaged = self.path / "bypassed-helper.elf"
|
||||
# BL to the following instruction replaces the final nonreturning call.
|
||||
damaged.write_bytes(original.replace(code, code[:-4] + b"\x00\xf0\x00\xf8"))
|
||||
with self.assertRaisesRegex(ValueError, "does not branch"):
|
||||
check_firmware(damaged)
|
||||
|
||||
def emulate(self, path, psp=True, handler=False):
|
||||
if unicorn is None:
|
||||
if os.environ.get("MESHCORE_REQUIRE_DFU_EMULATOR") == "1":
|
||||
self.fail("Unicorn required for the Cortex-M DFU regression")
|
||||
self.skipTest("Unicorn required for the Cortex-M DFU regression")
|
||||
elf = FirmwareElf(path)
|
||||
machine = unicorn.Uc(unicorn.UC_ARCH_ARM, unicorn.UC_MODE_THUMB | unicorn.UC_MODE_MCLASS)
|
||||
machine.mem_map(0x20000, 0xE0000)
|
||||
machine.mem_map(0x20000000, 0x40000)
|
||||
for section in elf.sections:
|
||||
if section[1] != 8 and section[2] & 4 and section[5]:
|
||||
machine.mem_write(section[3], elf.section_bytes(section))
|
||||
machine.mem_write(0xF4000, struct.pack("<II", 0x20030000, 0xF4101))
|
||||
peer = bytes(range(62))
|
||||
machine.mem_write(0x20007F80, peer)
|
||||
reg = arm_const
|
||||
machine.reg_write(reg.UC_ARM_REG_MSP, 0x20040000)
|
||||
machine.reg_write(reg.UC_ARM_REG_PSP, 0x20002000)
|
||||
machine.reg_write(reg.UC_ARM_REG_CONTROL, 2 if psp else 0)
|
||||
machine.reg_write(reg.UC_ARM_REG_BASEPRI, 0x80)
|
||||
machine.reg_write(reg.UC_ARM_REG_FAULTMASK, 1)
|
||||
machine.reg_write(reg.UC_ARM_REG_R0, 0xF4000)
|
||||
if handler:
|
||||
machine.reg_write(reg.UC_ARM_REG_IPSR, 16)
|
||||
entry = elf.address(HELPER_NAME)
|
||||
else:
|
||||
entry = elf.address("bledfu_control_wr_authorize_cb_fixture")
|
||||
stop = ((elf.address(HELPER_NAME) & ~1) + len(HELPER_CODE) - 2
|
||||
if handler else 0xF4100)
|
||||
def stop_at_branch(uc, address, size, context):
|
||||
if address == stop:
|
||||
uc.emu_stop()
|
||||
machine.hook_add(unicorn.UC_HOOK_CODE, stop_at_branch)
|
||||
machine.emu_start(entry | 1, 0, count=100)
|
||||
self.assertEqual(machine.reg_read(reg.UC_ARM_REG_PC), stop)
|
||||
self.assertEqual(machine.reg_read(reg.UC_ARM_REG_CONTROL), 0)
|
||||
self.assertEqual(machine.reg_read(reg.UC_ARM_REG_PRIMASK), 0)
|
||||
self.assertEqual(machine.reg_read(reg.UC_ARM_REG_BASEPRI), 0)
|
||||
self.assertEqual(machine.reg_read(reg.UC_ARM_REG_FAULTMASK), 0)
|
||||
self.assertEqual(bytes(machine.mem_read(0x20007F80, 62)), peer)
|
||||
return machine
|
||||
|
||||
def test_freertos_psp_and_msp_callers_reach_bootloader_without_fault(self):
|
||||
for key, path in self.images.items():
|
||||
if not key[2]:
|
||||
continue
|
||||
for psp in (False, True):
|
||||
with self.subTest(build=key, psp=psp):
|
||||
machine = self.emulate(path, psp=psp)
|
||||
self.assertEqual(machine.reg_read(arm_const.UC_ARM_REG_MSP), 0x20030000)
|
||||
|
||||
def test_original_callback_faults_outside_sram_before_bootloader(self):
|
||||
if unicorn is None:
|
||||
self.skipTest("Unicorn required for the Cortex-M DFU regression")
|
||||
for key, path in self.images.items():
|
||||
if not key[2]:
|
||||
with self.subTest(build=key), self.assertRaises(unicorn.UcError):
|
||||
self.emulate(path)
|
||||
|
||||
def test_handler_path_constructs_valid_exception_return_frame(self):
|
||||
machine = self.emulate(self.images[("-Oz", True, True)], handler=True)
|
||||
reg = arm_const
|
||||
self.assertEqual(machine.reg_read(reg.UC_ARM_REG_MSP), 0x20030000 - 32)
|
||||
self.assertEqual(machine.reg_read(reg.UC_ARM_REG_LR), 0xFFFFFFF9)
|
||||
self.assertEqual(struct.unpack("<8I", machine.mem_read(0x20030000 - 32, 32)),
|
||||
(0, 0, 0, 0, 0, 0xFFFFFFFF, 0xF4101, 0x21000000))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in new issue
Block a user