Cortex-M hardware stack overflow detection - Page 1
Forum section: electronics / general

author: bson
author_rank: Unknown
timestamp: May 13, 2024, 01:02:02 am
content: I think it should be possible to use debug watchpoints to implement hardware stack overflow detection:Set DHCSR.C_DEBUGEN=0 to prevent debug haltingSet DEMCR.MON_EN=1 to enable the DebugMonitor exceptionMake sure there's an exception handler to properly panic and restart (or whatever action is desired)Set a DWT mask and address to match a block past the stackIf alloca() exists, make sure it detects attempts to allocate past the end of the stack (this unrelated, but without it obviously alloca() could be used to step over the entire watchpoint block)Has anyone experimented with this? Does it work? It would be nice because it requires only trivial setup, has no runtime overhead, and doesn't require code instrumentation.Of course, it could also be made to halt instead of going into the exception handler if a debugger is attached - the debugger can then be used to check exactly what code is overflowing the stack.

author: ataradov
author_rank: Super Contributor
timestamp: May 13, 2024, 01:59:06 am
content: This will have limited utility. The DWT address match can match on a word at most. So, it would detect direct writes just past the stack. But in real life those overflows rarely happen like that, most of the time you write way past.Plus this would interfere with debuggers that often want full control over the debug registers.

author: bson
author_rank: Unknown
timestamp: May 13, 2024, 02:24:41 am
content: Hmm, I thought it could mask the address... but maybe the mask is for data?

author: bson
author_rank: Unknown
timestamp: May 13, 2024, 02:29:03 am
content: Fromhttps://developer.arm.com/documentation/ddi0403/d/Debug-Architecture/ARMv7-M-Debug/The-Data-Watchpoint-and-Trace-unit/The-DWT-comparators?lang=en"For address matching, the comparator can use a mask, so it matches a range of addresses."In fact, the mask is only used for address matching; for cycle or data matching it should be zero...

author: ataradov
author_rank: Super Contributor
timestamp: May 13, 2024, 02:33:23 am
content: You are still limited to blocks aligned at a block size. And you can only match on the unused area between the legitimate variables and the stack end. And this is pointless, you might as well extend the stack to that empty area.If you configure the stack to start at the end of SRAM, then the free stack space is all the RAM that is not used by the variables. The only detection you can do here is a match right at the end of the stack.Plus I would double check how big that mask is in real implementations. ARM documentation is very generic .

author: bson
author_rank: Unknown
timestamp: May 13, 2024, 03:14:28 am
content: Yeah, I understand there is a tradeoff between the size of the guard block and reliability. However, e.g. a 256-byte block might be worth it and sufficiently useful, especially if you suspect the possibility of stack overflows. Functions that need more stack than that, without using alloca(), are pretty rare and can be instrumented to check or modified to use alloca() which is then instrumented to check. This would also catch e.g. nested interrupt stack usage.

author: ataradov
author_rank: Super Contributor
timestamp: May 13, 2024, 03:44:36 am
content: I just checked and it looks like Cortex-M7 DWT implements all 5 bits of the mask register, so it should be possible to reserve a 256 byte gap for this check. It is better than nothing, of course. But I'm still not convinced it is going to be a significant measure.

author: SiliconWizard
author_rank: Super Contributor
timestamp: May 13, 2024, 04:03:28 am
content: It should work for "basic" stack overflows (stack *growing* past its limit), but would not catch all out-of-bounds writes of objects that are supposed to be on the stack.So, it would still cover one potential issue, having not sized the stack properly. And out-of-bounds writes of stack arrays by a few items.

author: ejeffrey
author_rank: Super Contributor
timestamp: May 13, 2024, 06:14:49 am
content: Since the stack grows down, overflowing automatic arrays will overwrite previous stack frames and not trigger this type of detection. Array underflows are a lot less common.It really just protects you from infinite recursion or putting huge arrays on the stack.

author: wek
author_rank: Frequent Contributor
timestamp: May 13, 2024, 10:43:55 am
content: I'd probably employ the MPU for this task (where available of course). I'd very much like to retain debugging facilities for... well... debugging...And what if memory utilization is such that stack almost meets data?JW

author: radiolistener
author_rank: Super Contributor
timestamp: May 13, 2024, 10:50:52 am
content: You can fill the stack memory with 0xff (you can do it on init) and then check the memory state with debugger after test running. You can see how much stack was used for each your thread and in case of needs you can increase stack for the thread which has too small margin till the end.When you see that the stack for some thread don't have 0xff at the end, that's an indication that the stack overflow happens and something bad happens.In that way you can check which thread is okay and which one eats too many stack and is too close to stack border.

author: eutectique
author_rank: Frequent Contributor
timestamp: May 13, 2024, 10:55:06 am
content: In systems without RTOS, I usually place the stack in the beginning of RAM. If stack grows out of proportions and overflows into non-existent address space, the Data Abort handler will catch it. The handler then saves the necessary registers into no-init RAM, and reboots. The bootloader (if any) or the firmware then checks the no-init RAM and reports the crash via CAN, BLE, or whatever means of communication are available.

author: peter-h
author_rank: Super Contributor
timestamp: May 13, 2024, 12:33:34 pm
content: I was just going to post exactly thatIt will pick up overflow of the "general stack" - the one used for interrupts, which uses the SP which is loaded at startup. The RTOS then configures things so it uses its own task stacks, one for each task, when it switches tasks. Interrupts still run on the general stack - there are two SPs. Gosh it's a while since I looked into this...So this way you could do stack overflow due to ISRs, and stack overflow before the RTOS starts up.For overflow of task stacks, FreeRTOS can do that optionally, but in software. RTOS task stack management is actually a much bigger problem; I fill the RTOS space with "s" or some such and have a GUI tool for displaying it. Not ideal really.

author: jnk0le
author_rank: Regular Contributor
timestamp: May 13, 2024, 02:00:43 pm
content: In addition, armv8m has a hardware stack limits.

author: brucehoult
author_rank: Super Contributor
timestamp: May 14, 2024, 04:23:37 am
content: Does Cortex M not have an equivalent to RISC-V's "PMP" (Physical Memory Protection) unit? Pretty much anything with M and U modes will have at least 8 PMP regions. If U mode code accesses outside them then it traps. M mode software can save/restore the PMP settings on thread switch, so every thread can be protected from the others.Combine this with the "stack is at the lowest addresses and grows down, heap is at the upper addresses and grows up" idea and you only need 1 PMP region per thread (at least for data).

author: ataradov
author_rank: Super Contributor
timestamp: May 14, 2024, 04:54:00 am
content: The original post discussed the most basic scenario where there is just Data+BSS at the beginning of SRAM (fixed) and stack at the end of SRAM (grows down).Placing the Data+BSS at the end of SRAM and the stack at the beginning is a great idea. I don't see any downsides doing that.RTOS and protection of individual tasks is entirely different story. You can use MPU for that, but it is still limited with the region sizes and alignment requirements. It is more headache than it is worth.

author: SiliconWizard
author_rank: Super Contributor
timestamp: May 14, 2024, 05:02:03 am
content: I agree with Bruce though, memory protection is increasingly available on even entry-level MCUs and can definitely be put to good use, *even* if you don't write fancy "multi-thread" stuff wtih an RTOS.That's something many of us are not used to yet on MCUs, but I'm definitely getting into that.

author: ataradov
author_rank: Super Contributor
timestamp: May 14, 2024, 05:09:48 am
content: But the issue here is that there is no way to distinguish stack overflow from a legitimate memory access.

author: Berni
author_rank: Super Contributor
timestamp: May 14, 2024, 05:29:37 am
content: Most of the bigger ARM Cortex chips have a MPU in them and that will provide more effective memory protection.Tho the MPU is rarely used since people don't bother to set it up (Unless you have a RTOS that does it for you) and its effectiveness does depend on how your memory is structured. Just stick to good coding practices (like checking array sizes) and if you want to be safe run some static code analysis tools over your source code. These will catch more memory related bugs since a lot of the time the code will garble the existing data in RAM that it should have access to anyway (so the MPU will not detect it)I suppose the most common bug that would eat up the whole stack would be a infinite recursion loop. But that will likely run off the end of the RAM area and then hardfault with a bus exception.I personally only ever used the MPU to disable caching on some areas used for DMA buffers so that i didn't have to constantly be doing the cache flushing dance around those areas.

author: SiliconWizard
author_rank: Super Contributor
timestamp: May 14, 2024, 05:45:31 am
content: Quote from: ataradov on May 14, 2024, 05:09:48 amOne approach is to set up 2 small memory regions with the MPU, one right before the stack, and one right after, both as 'read-only'.This will waste some RAM, but if you place the stack either at the beginning of the RAM, or at the end, you'll waste only 1 small block of RAM, and not 2. The other region will be outside of RAM.Of course, it won't catch write attempts of items supposedly on the stack, but so out-of-bounds that write accesses would lie outside of the protected regions without passing through them, but the most common cases (purely stack overflows and out-of-bounds accesses due to off-by-one errors) will be caught this way, because the CPU will inevitably attempt to write just past the stack in either direction, and fall into one of the protected regions. Not 100% failsafe of course (but that would require much more sophisticated approaches anyway), but still a nice addition for very little cost.

author: peter-h
author_rank: Super Contributor
timestamp: May 14, 2024, 09:14:35 am
content: How about this for another idea...If you have overrun the stack, an ISR will still execute (provided there is physical RAM there, and if there isn't, it will still getenteredprovided that writing to nonexistent RAM doesn't trigger a trap) even if it is corrupting memory.It will also return OK but we aren't necessarily interested in that.So how about checking the SP value at the start of any regular (e.g. 1kHz tick) ISR and if found below the stack, take some action e.g. somehow report it, and then reboot the CPU.In an embedded system, if the stack has overflowed then you are already buggered, there is no meaningful error recovery anyway since RAM is already corrupted, and in most products there is no way to report the error anywayFWIW I have found RTOS stacks to be a far bigger issue than the general stack which is not normally big and just services pre-RTOS-start code, and ISRs. I have 8k for it and only about 1k is ever used. RTOS stacks are a much harder issue because you have one per task and you have to somehow make sure each one is big enough. In most cases the only way is to run the product for a long time and see how far each (custom-prefilled) stack area has been overwritten.

author: S. Petrukhin
author_rank: Super Contributor
timestamp: May 14, 2024, 02:32:04 pm
content: A little abstract...It is convenient to use CCM for stack.This will not save you from overflowing, but...

author: peter-h
author_rank: Super Contributor
timestamp: May 15, 2024, 06:40:11 am
content: Indeed; the CCM is handy for this; also handy for the block of RAM for FreeRTOS (if 64k is enough for the RTOS task stacks which usually it is; in my box I have > 20 tasks and it all sits in the CCM) and handy for the RAM block for MbedTLS too. You just can't execute code from the CCM (in the 32F4) or put the vector table in there.

author: bson
author_rank: Unknown
timestamp: May 15, 2024, 10:03:21 pm
content: Quote from: radiolistener on May 13, 2024, 10:50:52 amIf you allocate all your dynamic memory on the thread stack, then a bug will easily cause a stack overflow. Detecting it after the fact is trivial, as you outline. Finding out when and where exactly it happens so you can track down the bug is the problem actually addressed here and is often NOT trivial. An Debug exception on the first overflowing access is a very good start to tracking down such obscure bugs. Because of the nature of stack allocation, when you overflow the stack frame chain contains the allocation history.

author: westfw
author_rank: Super Contributor
timestamp: May 16, 2024, 12:23:01 am
content: QuoteARMv7m (M3, M4, and higher?) have a "Memory Protection Unit" (MPU) that supports the ARM "Protected Memory System Archiecture."On M0 and M0+ it's optional.No mapping or address translation - just "protection", so protecting a stack still means "wasting" at least 32 bytes of memory.(The idea of putting the "bottom" of your stack at 0 is temping, but there tends to be other things that are also nice to put there. (a relocated vector table has relatively nasty alignment requirements, for instance.))

author: brucehoult
author_rank: Super Contributor
timestamp: May 16, 2024, 01:27:43 am
content: Wasting in what sense?

author: ataradov
author_rank: Super Contributor
timestamp: May 16, 2024, 01:37:09 am
content: You need to setup the protected region that would trigger a fault when a write happens. In general in MCUs all SRAM is technically "used" and there is no way to tell what is stack and what is not. With MMU you could map those regions far apart in the address space and have a way easier time detecting issues.And I think the smallest MPU region size is 256 bytes, so you will be wasting even more.

author: brucehoult
author_rank: Super Contributor
timestamp: May 16, 2024, 02:28:41 am
content: Ahhh .. in RISC-V "no access" is the default, and you'd just enable R/W access to the stack and data/bss/heap region(s) and R/X access to the code.

author: ataradov
author_rank: Super Contributor
timestamp: May 16, 2024, 02:35:36 am
content: But there are no regions generally. There is no way any system can know if a write at some address is a legitimate write to the variable at that address or a stack overflow into that address.Obviously if you are not using all the SRAM, you can define the middle section as unused in a platform specific way. But the question is why? If you have some memory left after the variables are allocated, you might as well allocate it for the heap/stack.And this is where you need to decide how much of that memory in the middle you are willing to waste. Obviously if you don't have a lot of data and you can limit your stack to a very small size, you can declare most of the SRAM as RO, but this is not a typical scenario.

author: ejeffrey
author_rank: Super Contributor
timestamp: May 16, 2024, 03:07:37 am
content: If you use your compilers call graph analysis tool and find the largest stack frame of any single function and you can afford to waste that much memory, then you should be able to mostly protect against stack / heap collisions by marking that much space read only. If on activation of a function the storing the return address doesn't fault, then the local variables of that function should not be able to touch the heap. You have to add a check to alloca and avoid or check variable size arrays and the like if you use them.This will only keep the stack allocated space from colliding with the heap. It won't stop out of bounds indexing of automatic arrays from doing pretty much anything. However I still think it's a win. Bounds checking is a local problem and amenable to local solutions. Stack overflow is a global problem, so a series of individually correct functions can have incorrect behavior. Also, more often buffer overflows are positive overflow which won't impact the heap in a conventional layout but will be just as damaging by overwriting the rest of the stack.

author: westfw
author_rank: Super Contributor
timestamp: May 16, 2024, 07:42:40 am
content: QuoteI don't see how that's different. You're "wasting" any RAM set for "no access."(now we can argue about whether it makes any sense to call "not used RAM" "wasted." These days it is relatively easy to just pick a chip with more ram than you need.)

author: brucehoult
author_rank: Super Contributor
timestamp: May 16, 2024, 08:03:31 am
content: Quote from: westfw on May 16, 2024, 07:42:40 amIt's not unused, it's used by other threads.If you're allocating all of RAM for your [ stack : data : bss : heap ] then you just let it trap with "unmapped address" (or whatever it's called) if the stack overflows the RAM area downwards or the brk upwards and don't need to bother with the protection mechanism.