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:
1#include <wil/result.h>
2void WorkFunction(HANDLE token)
3{
4 THROW_IF_WIN32_BOOL_FALSE(ImpersonateLoggedOnUser(token));
5 auto revert = wil::scope_exit([]
6 {
7 RevertToSelf();
8 });
9 THROW_IF_FAILED(OtherWorkFunction());
10}
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,
1BOOL ImpersonateLoggedOnUser(
2 [in] HANDLE hToken
3);
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:
1if (!ImpersonateLoggedOnUser(token))
2{
3 DWORD error = GetLastError();
4 // Do something about the error
5}
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:
1 auto outerscope = wil::scope_exit([]
2 { std::cout << "Outer" << std::endl;
3 });
4 {
5 auto innerscope = wil::scope_exit([]
6 { std::cout << "Inner" << std::endl;
7 });
8 }
9 std::cout << "Some text" << std::endl;
10
11 return 0;
12/* prints
13Inner
14Some text
15Outer
16*/
Now that we have some context. Let's get back to the original problem.
1#include <wil/result.h>
2void WorkFunction(HANDLE token)
3{
4 THROW_IF_WIN32_BOOL_FALSE(ImpersonateLoggedOnUser(token));
5 auto revert = wil::scope_exit([]
6 {
7 RevertToSelf();
8 });
9 THROW_IF_FAILED(OtherWorkFunction());
10}
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?
1#include <stdio.h>
2
3struct Object
4{
5 Object() { printf("Constructor\n"); }
6 ~Object() { printf("Destructor\n"); }
7};
8
9void foo()
10{
11 Object o{};
12 throw "bar";
13}
14
15
16int main()
17{
18 try
19 {
20 foo();
21 }
22 catch(...)
23 {
24 printf("Exception caught\n");
25 }
26}
27/* Outputs
28Constructor
29Destructor
30Exception caught
31*/
Okay... Not what we are seeing in the bug. Why?
Looking at the compiler-explorer output:
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
.
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.
1// tp.cpp : This file contains the 'main' function. Program execution begins and ends there.
2//
3#include <iostream>
4#include <Windows.h>
5#include "wil/result.h"
6#include "wil/resource.h"
7
8wil::critical_section g_cs;
9
10void ExceptionalFunction()
11{
12 auto lock = g_cs.lock();
13 throw "Exceptional thing!";
14
15}
16
17DWORD WINAPI WorkThread(LPVOID data)
18{
19 __try
20 {
21 ExceptionalFunction();
22 }
23 __except(EXCEPTION_EXECUTE_HANDLER)
24 {
25 std::cout << GetCurrentThreadId() << " Caught an exception" << std::endl;
26 }
27 std::cout << GetCurrentThreadId() << " thread exiting" << std::endl;
28 return ERROR_SUCCESS;
29}
30
31
32int main()
33{
34 HANDLE t1 = CreateThread(
35 nullptr,
36 0,
37 WorkThread,
38 nullptr,
39 0,
40 nullptr);
41 RETURN_IF_NULL_ALLOC(t1);
42 WaitForSingleObject(t1, INFINITE);
43 CloseHandle(t1);
44
45 HANDLE t2 = CreateThread(
46 nullptr,
47 0,
48 WorkThread,
49 nullptr,
50 0,
51 nullptr);
52 RETURN_IF_NULL_ALLOC(t2);
53 WaitForSingleObject(t2, INFINITE);
54 CloseHandle(t2);
55 std::cout << "Process exiting" << std::endl;
56 return 0;
57}
58/* Output
595028 Caught an exception
605028 thread exiting
61... Never completes
62*/
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.
- You can throw C++ exceptions using the
throw
keyword - You can raise structured exceptions with something like
RaiseException
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 #
- We enter the
try
block - Assuming no exception is thrown, we proceed through the instructions in the block until leaving the scope
- Upon leaving the scope continue executing. If there is a
finally
block, execute that after leaving the try block - If an exception is thrown in the
try
block, an execution object is created and the code looks for acatch
block that can handle the specified exception.- The search is done in reverse from the point of the exception
- If no handler is found, it is evaluated from the previous try block
- If still no handler is found, terminate is called.
- If an exception occurs after the exception is thrown but before it unwinds, terminate is called.
- I fan catch handler is found, the unwinding of the stack begins
- Destruction of objects that were fully constructed but not yet destructed
- This includes every object between the
try
block and the associatedcatch
handler - Execution then proceeds from the point of the
catch
handler
Structed Exception Handling #
Unless I'm missing something, the rules for SEH are not as clearly defined as for C++ exceptions. But
- The guarded
__try
block section is executed - If no exceptions occur, execution continues after the
__except
block - If an exception occurs, the
__except
block is evaluated with three potential valuesEXCEPTION_CONTINUE_EXECUTION
: Dismiss the exception, continue from where the exception was thrownEXCEPTION_CONTINUE_SEARCH
: The exception isn't recognized, continue searching up the stack for a handlerEXCEPTION_EXECUTE_HANDLER
: Exception is recognized, let the__except
block handle the exception then continue after the handling__except
block- The exception handler is a filter that allows you to compare against the exception code or potentially the exception information.
- The exception code is an
unsigned int
- The exception code is an
- The exception information is
_EXCEPTION_POINTERS*
that you retrieve fromGetExceptionInformation
- 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.
More Links #
- https://learn.microsoft.com/en-us/windows/win32/debug/exception-handling
- https://learn.microsoft.com/en-us/cpp/cpp/unhandled-cpp-exceptions?view=msvc-170
- https://learn.microsoft.com/en-us/cpp/cpp/try-throw-and-catch-statements-cpp?view=msvc-170
- https://learn.microsoft.com/en-us/cpp/cpp/structured-exception-handling-c-cpp?view=msvc-170
- https://learn.microsoft.com/en-us/cpp/cpp/mixing-c-structured-and-cpp-exceptions?view=msvc-170