December 17th, 2021

Why is the stack overflow exception raised before the stack has overflowed?

Consider this program we looked at last time.

#include <stdio.h>

int maxdepth = 0;

int f()
{
  ++maxdepth;
  return f();
}

int main()
{
  __try {
    f();
  } __except (GetExceptionCode() == STATUS_STACK_OVERFLOW ?
              EXCEPTION_EXECUTE_HANDLER :
              EXCEPTION_CONTINUE_SEARCH) {
    printf("Overflow at %u\n", maxdepth);
  }
}

As before, make sure to compile this program with optimizations disabled to ensure that the recursive call occupies stack and doesn’t get tail-call-optimized.

When you run this program, it will break into the debugger at the moment when the stack overflow occurs. And if you look at the stack pointer and the available stack space, you’ll see that the stack overflow exception was raised before the stack actually overflowed.

The exception is raised while the stack still has around 12KB of space left. What’s up with the extra 12KB? “I paid for that extra 12KB of memory, why won’t you let me use it?”

The kernel grows the stack when the stack guard page exception is triggered. (We’ll take a closer look at the stack guard page in a few months,) But when the kernel notices that the stack is nearly depleted, it raises the stack overflow exception. It does this before the stack is depleted so that there is still some stack on which to run the exception handlers.

The kernel is indeed letting you use that last 12KB of stack. It’s letting you use it to handle the stack overflow exception!

In the above program, for example, there is a stack overflow handler that wants to print a message and then break out of the infinite recursion. If the kernel didn’t raise the stack overflow exception until the stack was completely depleted, there wouldn’t be any stack for the stack overflow handler to run on.

Now, you (the person who wants to extract every last drop out of your stack before it overflows) might say, “Well, the kernel could just allocate a special emergency stack for stack overflow exceptions.”

But this would have to be a per-thread emergency stack, since multiple threads could be dealing with stack overflows simultaneously. And there’s already a convenient chunk of per-thread data: The stack itself!

You could say that the kernel does indeed reserve some space for a per-thread emergency stack: It allocates it from the end of the stack. And the stack overflow exception is raised when you reach the emergency stack.

Bonus chatter: Switching to an emergency stack would create a lot of problems. It would mess up stack traces, since the stack trace at the point of a stack overflow would stop when it reached the end of the emergency stack. Split stacks aren’t really a thing in Windows because the kernel needs to know the stack boundaries in order to detect stack buffer overflow attacks: If the chain of stack frames or the chain of exception records leaves the stack, then the kernel assumes that the stack is corrupted. This means that no exception handler from the main stack will be called when the system is running on the emergency stack. Switching to the emergency stack would be pointless, since there won’t be any handlers to run anyway.

I guess the kernel could add extra support for an emergency stack mode where it knows that it’s on an emergency stack and accept stack frames and exception handlers from the main stack. But that would be a lot of work compared to just carving the emergency stack out of the end of the regular stack. (It would also break any code that used Get­Thread­Stack­Limits.)

Topics
Code

Author

Raymond has been involved in the evolution of Windows for more than 30 years. In 2003, he began a Web site known as The Old New Thing which has grown in popularity far beyond his wildest imagination, a development which still gives him the heebie-jeebies. The Web site spawned a book, coincidentally also titled The Old New Thing (Addison Wesley 2007). He occasionally appears on the Windows Dev Docs Twitter account to tell stories which convey no useful information.

7 comments

Discussion is closed. Login to edit/delete existing comments.

  • Henke37

    Doesn’t fibers and such involve messing with the stack layout? Oh well, they are all in cohorts anyway.

  • Antonio Rodríguez

    "Now, you (the person who wants to extract every last drop out of your stack before it overflows) might say, “Well, the kernel could just allocate a special emergency stack for stack overflow exceptions.”"

    Above that, it would be a big contradiction. You want to squeeze those extra 12 KB at the end of your memory-hungry stacks. Well, allocating an emergency stack of at least 12 KB for *every* thread on the system would waste more...

    Read more
  • Joshua Hudson

    On a completely unrelated note, it's not documented anywhere how much stack you're supposed to leave for kernel functions (kernel32.dll, advapi32.dll). This comes up quite a bit when the plan is to allocate hundreds of KB of RAM on the worker thread stack because it's faster than heap allocation and trivially provable that it's leak free. I've been assuming 32KB is enough (including the frames below the start of thread entry -- now why that's...

    Read more
    • Antonio Rodríguez · Edited

      I suppose it's not documented because it's an implementation detail. If they said "32 KB" now, and then needed to grow it later (say, in Windows 12 they introduce Win128, where WOW64 runs under WOW128 :-P ), they would have their hands tied.

      This is one of these cases where if you have to ask where the limit is, you are doing something wrong (like thread number or filesystem handles limits). Stack allocation can be convenient,...

      Read more
      • Joshua Hudson · Edited

        I’m already calling CreateThread() and telling it how much stack to allocate because I know how much that thread’s going to use (and it’s more than the default value). What more do you want?

        Hands are tied anyway; it just without documenting a reserve value they’re tied empirically by whoever took the lowest guess that worked.

      • 紅樓鍮

        Can’t you use VirtualAlloc? I think if you ask the heap manager for some hundred KBs it will pass your request directly to VirtualAlloc anyways (20110930-00/?p=9513).

  • Joshua Hudson

    I do recall one system that had emergency stack, and it worked rather differently. All kernel-mode callbacks were raised on it including stackoverflow. Guess what happened if you overflowed the emergency stack. (I never found out but probably just dead.) This did have an upside of trivial red-zone though. The emergency stack was pretty small; you were supposed to set some flags and potentially call longjmp() to get out of there or exit() to terminate.