Why does my program crash when I throw an exception from an APC?


Consider the following code fragment:

class thread_exit_exception
{
};

DWORD CALLBACK WorkerThreadProc(void* parameter)
{
  try {
    while (true) {
      SleepEx(INFINITE, TRUE); // alertable
    }
  } catch (thread_exit_exception& e) {
  }
  return 0;
}

void CALLBACK ExitWorkerThreadApcProc(ULONG_PTR parameter)
{
  throw thread_exit_exception();
}

void MakeTheWorkerThreadExit()
{
    QueueUserAPC(ExitWorkerThreadApcProc, WorkerThreadHandle, 0);
}

In this case, the worker thread just sits around waiting to be told to exit, but in the general case, it would be doing work and check in periodically to see if somebody told it to exit.

The problem is that this sample program crashes. Why?

Well, this is similar to the issue of throwing a C++ exception from a structured exception, because you're throwing an exception from an operating system callback. The C++ compiler isn't expecting that, and it might optimize out the try/catch, in which case the exception you throw from the APC goes unhandled and terminates the process.

But wait, even before you get to this point, you're already in trouble, because you're throwing an exception across frames, even though not all of the frames in between are in on the joke.

So this plan is double-broken.

The correct way to do this is to set a flag from the APC, and the worker thread checks this flag after every alertable wait. If the flag is set, then an APC wants the thread to exit, and the thread can exit on its own terms. This avoids raising exceptions across foreign frames: Instead of raising the exception, you merely set a variable that says "I'd really like to raise an exception, but I can't, so can you pretend that I raised an exception? Thanks."

Alternatively, you could have the alertable wait check the flag, and if the flag is set, then it does a throw thread_exit_exception(). With this design, the variable means "I'd really like to raise an exception, but I can't, so can you raise the exception when it's safe to do so? Thanks."

Comments (10)

  1. kantos says:

    Honestly it’s undefined behavior as you don’t know that the calling code is exception safe, anyone who wants to try is already playing with nasal demons. If anything I’d be tempted to ask the windows SDK team to add a preprocessor flag that marks all callbacks from the OS as noexcept in C++. This would break existing code if you opt in but it would allow you to also tell the compiler that your callbacks should never throw and if they do to just terminate.

  2. xcomcmdr says:

    Exceptions used for flow control, that’s asking for trouble.

    1. Joshua says:

      But I really do have to write try { var fs = new FileStream(pathname, …); /* … */ } catch (FileNotFoundException) { /* code for file doesn’t exist */ }

      1. cheong00 says:

        File.Exists() exists for a reason. I’d rather reserve the use of FileNotFoundException for situations less obvious, such as when loading DLLs (where you can load from multiple places).

        1. Joshua says:

          if (File.Exists(pathanme) { var fs = new FileStream(pathname, …); } is slower and can still throw FileNotFoundException. See https://blogs.msdn.microsoft.com/oldnewthing/20071109-00/?p=24553 for why checking if a file exists before opening it is pointless.

          1. Fred says:

            But a file disappearing between the check and the open() *is* an exceptional condition, for which the exception is legitimate. The File.Exists() check is merely to avoid cluttering the debugger with the cases where one *expects* the file to be missing (like, say, a DLL whose generation at compile-time is optional…)

    2. xcomcmdr says:

      I meant, a thread that terminates is hardly an exceptional situation.

  3. Goran says:

    @kantos: forget exception safety. Calling code is most likely C, so it’s100% oblivious to exceptions. Even if it was C++, it would need to use the same exception implementation as me, which is not a given. And imagine doing it from some other language…

  4. 640k says:

    If the callback code calls any library, all bets are off (there’s no checked exceptions in c++, no guarantees are given). Most, if not all, file system accesses, or even most things dealing with handles, are no-no. And forget logging! The allowed set of language features allowed in these cases is a very small subset of c++, not really c++ or even c. More similar to what’s allowed in DllMain.

  5. Neil says:

    Well, it could have been worse, they could have deliberately dereferenced a null pointer in their APC hoping to catch it with SEH…

Skip to main content