fix(core): handle stack overflow on stm32f4 properly
What changed, and why it matters
This commit fixes a bug in the Trezor hardware wallet's low-level crash handler for STM32F4 devices. The handler decides whether a memory fault is a stack overflow by checking the wrong stack pointer due to an inverted branch condition. Before the fix, the device could fail to recognize or recover from a stack overflow on the main stack, potentially leaving it in a crashed or unresponsive state instead of resetting safely.
Treat as a reliability and potential security fix. Review related fault handlers for similar inverted-condition errors, ensure stack overflow recovery is tested on STM32F4 hardware, and consider whether the bug could be triggered by maliciously deep call stacks or crafted messages to induce denial-of-service.
Security signals we found
Stack overflow handling bypass due to inverted branch condition
Memory fault (MemManage) exception handler logic error
Potential denial-of-service or unsafe crash state on stack exhaustion
Embedded firmware / bare-metal ARM Cortex-M fault handler
No changelog entry and co-authored by GitHub Copilot
Evidence from the diff
The MemManage_Handler in core/embed/sys/task/stm32/systask.c is a naked assembly exception handler. It uses the EXC_RETURN bit 2 (LR & 0x4) to determine which stack was active (PSP=1, MSP=0). The original code used BEQ after TST LR,#0x4, which branches when the bit is clear, i.e., when MSP was active. The label 1f is the ‘skip stack pointer checking’ path, so the branch was taken for MSP, meaning the intended stack-overflow check for the main stack was skipped. The fix changes BEQ to BNE so the check is performed when MSP is active (the bit is clear, so Z=1, BNE not taken? Wait: TST sets Z if (LR & 0x4)==0. BNE branches if Z==0, i.e., if bit is set (PSP). That would skip for PSP. Hmm, need to re-examine. Actually the comment says ‘Skip stack ptr checking for PSP’. So label 1f is skip. We want to skip when PSP (bit set). TST LR,#0x4: if bit set, Z=0, so BNE branches -> skip. If bit clear (MSP), Z=1, BNE does not branch -> continue to check. So BNE is correct for skipping PSP. Original BEQ would skip for MSP and check for PSP. The code then compares R0 (stack pointer) against sstack. R0 was presumably loaded with the active stack pointer earlier. The comment was also updated from ‘Check if PSP is below the stack’ to ‘Check if MSP is below the stack’. This confirms the intent: detect stack overflow on the main stack (MSP) and reset MSP to the top of stack (estack). The bug meant stack overflow on MSP was not detected/rehandled, likely causing the handler to fall through to fault or infinite loop instead of recovering.
Changed components
core/embed/sys/task/stm32/systask.cMemManage_HandlerSTM32F4 Trezor devicesMain stack (MSP) overflow recovery pathInspect captured patch +2 / −2
diff --git a/core/embed/sys/task/stm32/systask.c b/core/embed/sys/task/stm32/systask.c
index 23644a8d..690b1392 100644
--- a/core/embed/sys/task/stm32/systask.c
+++ b/core/embed/sys/task/stm32/systask.c
@@ -790,9 +790,9 @@ __attribute__((naked, no_stack_protector)) void MemManage_Handler(void) {
"MOV R1, LR \n" // R1 = EXC_RETURN code
#if !(defined(__ARM_ARCH_8M_MAIN__) || defined(__ARM_ARCH_8M_BASE__))
"TST LR, #0x4 \n" // Return stack (1=>PSP, 0=>MSP)
- "BEQ 1f \n" // Skip stack ptr checking for PSP
+ "BNE 1f \n" // Skip stack ptr checking for PSP
"LDR R2, =%[sstack] \n"
- "CMP R0, R2 \n" // Check if PSP is below the stack
+ "CMP R0, R2 \n" // Check if MSP is below the stack
"ITT LO \n" // base
"LDRLO R2, =%[estack] \n"
"MSRLO MSP, R2 \n" // Reset MSP
Why this scored 59/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.