Most interpreters (including those for bytecode-based languages like Java) are assumed to be exempt of UB at the low-level. For instance, I would never expect NodeJS or the Hotspot JVM to allow blowing the stack in a way that the runtime doesn't handle early, e.g. by throwing an interpreted language-level exception. For this purpose, Java has StackOverflowException
s, while JS engines may throw InternalError
or RangeError
(resp. SpiderMonkey and V8/Node), and maybe others.
As far as I know, there is no perfect way to handle stack overflows at lower level (C, C++, Rust [actually not Rust]) and they are considered UB. If we're lucky enough, we get a segfault, or memory is silently corrupted in the worst case.
I'm wondering how interpreters tend to deal with these issues. There's probably not a single answer to this question, and it may also depend a lot on hardware capabilities (mostly MMU) and OS features. Here are a few ideas I had:
Using a "virtual" stack (reified stack)
That's probably the simplest and obvious solution. Instead of using the host language's native call stack, allocate stack frames manually on a heap-allocated stack structure. Then your overflows turn into out-of-memory/overcomitting (none of which should be exploitable), and it's also easy to add an upper size limit.
However, this is supposedly slow and may start to be tricky to implement when native calls (built-ins or JIT-compiled) are involved. I'm sure that it's possible to play with assembly code and make your stack/frame pointers point to heap memory temporarily, but that would be nontrivial.
OS segfault handler
Similarly to how the Hotspot JVM implements implicit null checks by registering a segfault handler at the OS-level, it might be possible to receive a signal when the stack overflows. However, I'm not sure that this is super reliable nor that all OSes/hardware support it. It can be a nice way to do things when it's available, but would require a fallback.
Guess the stack limit and check
Some OSes may allow dynamically checking the maximum call stack size. You can also expect it to always be larger than a constant value. In these cases, you can implement guards in recursive functions that throw an exception when the limit is reached, with overly conservative slack just to be sure. That is, IIRC, how GNU R checks for stack overflows.
The Wikipedia page on Stack buffer overflows mentions protection schemes, but they seem to be unreliable and more useful as general safety measures in a program that isn't malicious and doesn't evaluate arbitrary code.