Fun with exceptions

03-21-2025

The Bug

Yesterday I was chatting with a colleague who was looking at a bizarre application crash. There was a thread pool thread that was throwing an exception after the callback completed without reverting the impersonation level.

The code was roughly this:

#include <wil/result.h>
void WorkFunction(HANDLE token)
{
	THROW_IF_WIN32_BOOL_FALSE(ImpersonateLoggedOnUser(token));
	auto revert = wil::scope_exit([]
	{
		RevertToSelf();
	});
	THROW_IF_FAILED(OtherWorkFunction());
}

For those of you who know what is wrong here. Nice. For those who don't come along for some learning.

This function is leveraging the Windows Implementation Library (WIL). This is a header only library to help make interactions with Windows align more with a C++ style. The header wil/result.h provides some RAII wrappers and macros for handling some Windows error patterns. For example,

BOOL ImpersonateLoggedOnUser(
  [in] HANDLE hToken
);

ImpersonateLoggedOnUser returns a BOOL and sets the last-error code on the current threads thread execution block (TEB). Callers should check this value using GetLastError. Meaning good error handling for this function would look like:

if (!ImpersonateLoggedOnUser(token))
{
	DWORD error = GetLastError();
	// Do something about the error
}

Will allows us to turn this into a single line with the macro RETURN_IF_WIN32_BOOL_FALSE which will return the last error code, or throw an exception THROW_IF_WIN32_BOOL_FALSE.

The next item to discuss is the RAII wrapper wil::scope_exit. This is a utility function to execute a supplied lambda when the created object leaves the scope. For example:

    auto outerscope = wil::scope_exit([]
        { std::cout << "Outer" << std::endl;
        });
    {
        auto innerscope = wil::scope_exit([]
            { std::cout << "Inner" << std::endl;
        });
    }
    std::cout << "Some text" << std::endl;

    return 0;
/* prints
Inner
Some text
Outer
*/

Now that we have some context. Let's get back to the original problem.

#include <wil/result.h>
void WorkFunction(HANDLE token)
{
	THROW_IF_WIN32_BOOL_FALSE(ImpersonateLoggedOnUser(token));
	auto revert = wil::scope_exit([]
	{
		RevertToSelf();
	});
	THROW_IF_FAILED(OtherWorkFunction());
}

When the thread pool thread returns from the callback it is still impersonating and that is bad. The question is, why didn't the wil::scope_exit destruct?

This is a pretty simple set of code, can we write a minimal repro?

#include <stdio.h>

struct Object
{
    Object() { printf("Constructor\n"); }
    ~Object() { printf("Destructor\n"); }
};

void foo()
{
    Object o{};
    throw "bar";
}


int main()
{
    try
    {
        foo();
    }
    catch(...)
    {
        printf("Exception caught\n");
    }
}
/* Outputs
Constructor
Destructor
Exception caught
*/

Okay... Not what we are seeing in the bug. Why?

Looking at the compiler-explorer output:

Source

We can clearly see from the assembly that the destructor is being called as a part of the stack unwinding. Why?

Digging into this further, I did some reading on stack unwinding and the example they use aligns with what I saw in my local testing.

If that is the case, how could the revert not be happening???? After briefly chatting with another colleague who really knows his C++, he mentioned.

Off the top of my head, there are three major ways in which an exception can fail to trigger the appropriate scope destruction. The compiler was not passed an exception handling specification--usually /EHsc. The compiler will still generate throws and catches for functions directly containing them, but intervening code doesn't get the correct scope unwinders. A function is incorrectly marked as noexcept, driving the compiler to omit unwinding support. This can be diagnosed using the /EHr option that adds guards to noexcept contexts and will crash your program if a C++ exception escapes from such a context. You're using structured exceptions instead of C++ exceptions. Using catch triggers unwinding, but __except generally will not.

Okay now we have something interesting. I take a look at the build options for the library in question and I don't find anything for exception handling? But it gives me something to look into. How do the exception handling options work?

Going directly to the source /EH (Exception handling model) I find:

The compiler always generates code that supports asynchronous structured exception handling (SEH). By default (that is, if no /EHsc, /EHs, or /EHa option is specified), the compiler supports SEH handlers in the native C++ catch(...) clause. However, it also generates code that only partially supports C++ exceptions. The default exception unwinding code doesn't destroy automatic C++ objects outside of try blocks that go out of scope because of an exception. Resource leaks and undefined behavior may result when a C++ exception is thrown.

Interesting... In Visual Studio (where I wrote my repro app), I found that by default the C++ Exception setting is /EHsc (C++ stack unwinding).

Let's try adjusting our options to see how things work in the test app after disabling C++ exceptions. I'll also need to replace the C++ try/catch with the structed exception handling __try/__except.

Source

Now this is what we are seeing in the failure condition, but let's take it a step further and make this app a little more robust. Let's see if we can repro something at least closer to the failure case.

// tp.cpp : This file contains the 'main' function. Program execution begins and ends there.
//
#include <iostream>
#include <Windows.h>
#include "wil/result.h"
#include "wil/resource.h"

wil::critical_section g_cs;

void ExceptionalFunction()
{
    auto lock = g_cs.lock();
    throw "Exceptional thing!";

}

DWORD WINAPI WorkThread(LPVOID data)
{
    __try
    {
        ExceptionalFunction();
    }
    __except(EXCEPTION_EXECUTE_HANDLER)
    {
        std::cout << GetCurrentThreadId() << " Caught an exception" << std::endl;
    }
    std::cout << GetCurrentThreadId() << " thread exiting" << std::endl;
    return ERROR_SUCCESS;
}


int main()
{
    HANDLE t1 = CreateThread(
        nullptr,
        0,
        WorkThread,
        nullptr,
        0,
        nullptr);
    RETURN_IF_NULL_ALLOC(t1);
    WaitForSingleObject(t1, INFINITE);
    CloseHandle(t1);

    HANDLE t2 = CreateThread(
        nullptr,
        0,
        WorkThread,
        nullptr,
        0,
        nullptr);
    RETURN_IF_NULL_ALLOC(t2);
    WaitForSingleObject(t2, INFINITE);
    CloseHandle(t2);
    std::cout << "Process exiting" << std::endl;
    return 0;
}
/* Output
5028 Caught an exception
5028 thread exiting
... Never completes
*/

And just as I suspected, we have a deadlock. Now that we know what happens and how, I want to dig more into the why. I am aware I could just say "yeah I know this because that is the way it is" but I want to take this as an opportunity to put some learnings about exceptions into one place.

Exceptions and their handling

On Windows there are a few types of exceptions. C++ exceptions, software exceptions and hardware exceptions. Sofware and hardware exceptions are often grouped into the category of structured exceptions.

They are similar but not the same especially when it comes to their behavior.

Both can be recovered from, however when it comes to SEH you may be opening a can of worms. Structured exceptions can be either continuable or noncontinuable. A noncontinuable exception can happen when the hardware indicates that it shouldn't continue, or it doesn't make any sense to continue. But noncontinuable exceptions do not terminate the application upon creation, meaning that you could (but really should) attempt to handle that exception.

This could be something like stack corruption, or something more serious. Windows trusts that you are not going to shoot yourself in the foot if you handle these exceptions.

By comparison, C++ exceptions are exceptional but are usually scoped within a clearly defined behavior. A developer decided that "this this happening is bad, and we should address it ASAP", going through typical error handling will not cut it.

Although, as I write this, I am thinking that structured exceptions are not a force of nature. A developer somewhere decided that the hardware should raise an exception.

When considering their differences the largest one may be down to their language limitations. SEH is available in C whereas C++ exceptions are available in... well C++ duh.

As for their handling, let's break down each of them.

C++ Handling behavior

  1. We enter the try block
  2. Assuming no exception is thrown, we proceed through the instructions in the block until leaving the scope
  3. Upon leaving the scope continue executing. If there is a finally block, execute that after leaving the try block
  4. If an exception is thrown in the try block, an execution object is created and the code looks for a catch block that can handle the specified exception.
  5. I fan catch handler is found, the unwinding of the stack begins

Structed Exception Handling

Unless I'm missing something, the rules for SEH are not as clearly defined as for C++ exceptions. But

  1. The guarded __try block section is executed
  2. If no exceptions occur, execution continues after the __except block
  3. If an exception occurs, the __except block is evaluated with three potential values
  4. If there is a __finally block, that is execute after either the __try block or the __except block. - It is often used for code clean up

Take aways

Exceptions are tricky. This deep(ish) dive has re-enforced that unless I'm operating in C#, I'm going to avoid using exceptions where I can. The potential for footgun is high, and while there is clear value from this error handling model, I'm not sure if it outweighs the potential footguns for me.