Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
59 commits
Select commit Hold shift + click to select a range
14a5b6a
test(runtime): check that a DLL's copy reports crashes while loaded
SizzleUnrlsd Nov 1, 2026
7bfa995
test(runtime): check that an unloaded DLL leaves a later host filter
SizzleUnrlsd Nov 1, 2026
ff1d359
fix(runtime): put back the filter a DLL's crash handler replaced
SizzleUnrlsd Nov 1, 2026
7d0cb32
test(runtime): check the executable reports crashes after a DLL unloads
SizzleUnrlsd Nov 1, 2026
dd5148e
test(runtime): check a host filter installed before a DLL comes back
SizzleUnrlsd Nov 1, 2026
8a8a49c
test(runtime): check crashes after two DLLs unload in reverse order
SizzleUnrlsd Nov 1, 2026
f959ea5
refactor(runtime): publish the filter a crash handler replaced
SizzleUnrlsd Nov 1, 2026
ebebfae
fix(runtime): hand a DLL's replaced filter to the copy above it
SizzleUnrlsd Nov 1, 2026
eb4fa5e
test(runtime): check crashes after two DLLs unload in load order
SizzleUnrlsd Nov 1, 2026
169d3c6
fix(runtime): find the copy above among the loaded modules
SizzleUnrlsd Nov 1, 2026
1ecf68c
test(runtime): load the copies of the runtime library in a helper
SizzleUnrlsd Nov 1, 2026
a6deed2
test(runtime): check two DLLs unloaded around a host's filter
SizzleUnrlsd Nov 1, 2026
8f51424
test(runtime): print the exception that the host's filter sees
SizzleUnrlsd Nov 1, 2026
2ed205a
test(runtime): check that the crash tests' DLLs unload
SizzleUnrlsd Nov 1, 2026
d0f4c5f
test(runtime): check which copy's handler prints the backtrace
SizzleUnrlsd Nov 1, 2026
4be3a0d
refactor(runtime): share the readability check of the Windows runtime
SizzleUnrlsd Nov 1, 2026
258bce4
fix(runtime): read a module's headers only once found readable
SizzleUnrlsd Nov 1, 2026
0a0da66
test(runtime): check a DLL unloading past unreadable module headers
SizzleUnrlsd Nov 1, 2026
da5949e
fix(runtime): look through every module the loader lists
SizzleUnrlsd Nov 1, 2026
37c5c38
fix(runtime): hand the filter over to every copy that holds the handler
SizzleUnrlsd Nov 1, 2026
b0b9a7f
test(runtime): check two copies whose handlers replaced one handler
SizzleUnrlsd Nov 1, 2026
b592e53
docs(readme): describe the crash handler of a DLL's copy
SizzleUnrlsd Nov 1, 2026
6064c2d
docs(changelog): note the crash handler left by an unloaded DLL
SizzleUnrlsd Nov 1, 2026
779dcd8
test(runtime): watch for a read of the unreadable headers
SizzleUnrlsd Nov 1, 2026
a765c3d
fix(runtime): leave the crash filter alone while a DLL's exit() runs
SizzleUnrlsd Nov 1, 2026
7be3ce3
fix(runtime): link the crash handler records when a DLL's copy installs
SizzleUnrlsd Nov 1, 2026
73a4181
fix(runtime): put back a filter installed during a handler's release
SizzleUnrlsd Nov 1, 2026
3de9d42
fix(runtime): keep a DLL's handler off a filter it cannot inspect
SizzleUnrlsd Nov 1, 2026
a9d9aa0
test(runtime): check that a DLL calling exit() leaves its handler alone
SizzleUnrlsd Nov 1, 2026
9b4a28a
test(runtime): check a hand-over for which nothing can be allocated
SizzleUnrlsd Nov 1, 2026
f2bdb9a
test(runtime): check a copy over a filter whose module it cannot read
SizzleUnrlsd Nov 1, 2026
e0c265c
test(runtime): check a filter installed while a copy releases its own
SizzleUnrlsd Nov 1, 2026
353f5ff
test(runtime): describe the latest crash handler tests in the header
SizzleUnrlsd Nov 1, 2026
fb7c43d
docs(readme): describe how a DLL's copy hands its filter over
SizzleUnrlsd Nov 1, 2026
4acdc2e
docs(changelog): note a DLL's handler left out over an unreadable module
SizzleUnrlsd Nov 1, 2026
a017476
fix(runtime): link a DLL copy's record only where its release runs
SizzleUnrlsd Nov 1, 2026
88b3db9
docs(readme): say what a DLL's copy does before Windows 10
SizzleUnrlsd Nov 1, 2026
0111c53
test(runtime): drop the test that hangs the Windows loader
SizzleUnrlsd Nov 1, 2026
5597a99
test(runtime): drop the tests of a DLL copy's crash handler hand-over
SizzleUnrlsd Nov 2, 2026
43b5cbb
test(runtime): stop giving the leak DLL to the runtime unit tests
SizzleUnrlsd Nov 2, 2026
3406bec
test(runtime): drop the test of a DLL copy's own crash handler
SizzleUnrlsd Nov 2, 2026
01bfd36
fix(runtime): install the crash handler from the executable's copy only
SizzleUnrlsd Nov 2, 2026
7419a31
refactor(runtime): drop the crash handler records and their release
SizzleUnrlsd Nov 2, 2026
214aaca
refactor(runtime): keep the readability check in the Windows vtable file
SizzleUnrlsd Nov 2, 2026
02c7192
refactor(runtime): drop the check that the loader detaches the module
SizzleUnrlsd Nov 2, 2026
ac7b837
test(runtime): let the DLL that links the runtime fault in its own code
SizzleUnrlsd Nov 2, 2026
d27a397
test(runtime): describe the crash tests under the executable's handler
SizzleUnrlsd Nov 2, 2026
a2395d2
test(runtime): check that the executable reports a crash in a DLL
SizzleUnrlsd Nov 2, 2026
76506ab
test(runtime): check that a DLL's copy installs no crash handler
SizzleUnrlsd Nov 2, 2026
d4a9757
test(runtime): check the host's filter while a DLL is loaded
SizzleUnrlsd Nov 2, 2026
009745b
test(runtime): check an unload while another thread changes the filter
SizzleUnrlsd Nov 2, 2026
83c5f32
test(runtime): check that the host's last filter stays after an unload
SizzleUnrlsd Nov 2, 2026
cb53508
test(runtime): describe the crash handler tests in the file's header
SizzleUnrlsd Nov 2, 2026
99dea85
docs(readme): say that only the executable's copy installs the handler
SizzleUnrlsd Nov 2, 2026
681e632
docs(changelog): note that only the executable installs the handler
SizzleUnrlsd Nov 2, 2026
d6c3c1b
test(runtime): match a crash in a DLL with gtest's matchers alone
SizzleUnrlsd Nov 2, 2026
001d887
test(runtime): make the other thread's filter differ from the one set
SizzleUnrlsd Nov 3, 2026
b022ebc
test(runtime): start the unload test's watchdog before the DLL loads
SizzleUnrlsd Nov 4, 2026
2f8051a
test(runtime): start the crash tests' watchdog before the fault's thread
SizzleUnrlsd Nov 4, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,14 @@ pull request.
allocated, and each load of the DLL built another one: 488 bytes per load and unload in the
measurement of #227, whose DLL decoded two names. The copy now frees the cache when `FreeLibrary`
unloads the DLL, after the DLL's destructors, whose boxes still name types (#227).
- **The crash handler of an unloaded DLL, on Windows.** With `CT_BACKTRACE`, the copy of the
runtime in a DLL that links it installed its crash handler over the filter installed before it,
and nothing put that filter back. Once `FreeLibrary` had unloaded the DLL, an exception that
nothing handled ended the process without a backtrace: neither the executable's handler nor a
filter that the program had installed before the DLL ran. Only the executable's copy installs the
crash handler now: it reports the crashes in an instrumented DLL's code too, and a DLL's copy
leaves the process's filter alone. An instrumented DLL that a program without the runtime loads
reports no crash any more (README, "Crash handler on Windows") (#236).

### Tests and CI

Expand Down
19 changes: 19 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -284,6 +284,25 @@ runtime as follows:
have unloaded. Once that copy has stopped following the loader, when the module that holds it
detaches, the handler prints every frame bare.

## Crash handler on Windows

With `CT_BACKTRACE` set, only the executable's copy of the runtime installs a crash handler for the
whole process, with `SetUnhandledExceptionFilter`, when the executable initializes. The copy in a
DLL that links the runtime installs none and leaves the process's filter alone, while the DLL is
loaded and when it unloads; `FreeLibrary` unloads the DLL as any other (#236).

- In an executable that links the runtime, its handler reports the crashes of the whole process,
those in the code of an instrumented DLL included, while the DLL is loaded and once it has
unloaded.
- A filter that the program installs, before or after it loads such a DLL, stays the process's.
- **Limit.** In a program that does not link the runtime, an instrumented DLL reports no crash,
even with `CT_BACKTRACE` set: the program's own filter, if any, or Windows handles it.

Windows has no call that sets the process's filter only if it still is the one expected. A DLL's
copy that installed its handler could not take it out when the DLL unloads without overwriting a
filter that the program or another thread installs meanwhile, and a filter installed over the
handler would call it once its code is gone.

## LLVM versions

CoreTrace Compiler builds and instruments with LLVM 16 to 23. Most behaviour is the same on
Expand Down
15 changes: 14 additions & 1 deletion src/runtime/windows/ct_runtime_backtrace.cpp
Original file line number Diff line number Diff line change
@@ -1,4 +1,12 @@
// SPDX-License-Identifier: Apache-2.0
//
// The crash handler of the runtime, which CT_BACKTRACE installs for the whole process from the
// executable's copy of the runtime only. A DLL that links the runtime has a copy of its own, which
// installs none: Windows has no call that puts the process's filter back only if it still is the
// one expected, so a DLL's copy could not take its handler out when FreeLibrary unloads the DLL
// without overwriting a filter that the program installs meanwhile, and a filter installed over the
// handler would call it once its code is gone. A crash in the DLL is reported by the executable's
// handler; an instrumented DLL that a program without the runtime loads reports none (#236).
#include "ct_runtime_internal.h"
#include "ct_runtime_helpers.h"

Expand All @@ -14,6 +22,9 @@

#pragma comment(lib, "Dbghelp.lib")

// The module that holds this copy of the runtime.
extern "C" IMAGE_DOS_HEADER __ImageBase;

namespace
{
std::atomic<int> ct_backtrace_installed{0};
Expand Down Expand Up @@ -84,7 +95,9 @@ namespace

CT_NOINSTR void ct_maybe_install_backtrace(void)
{
if (std::getenv("CT_BACKTRACE") == nullptr)
// A DLL's copy leaves the process's filter alone: see the top of this file.
if (std::getenv("CT_BACKTRACE") == nullptr ||
reinterpret_cast<HMODULE>(&__ImageBase) != GetModuleHandleW(nullptr))
{
return;
}
Expand Down
21 changes: 20 additions & 1 deletion test/unit/reload/ct_runtime_library.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,14 @@
// A DLL that links the runtime, as an instrumented DLL does, and with it the runtime's watch of
// the loader, its DbgHelp session and the DbgHelp lock of the process:
// runtime_vtable_replace_test.cpp makes its copy of the runtime name a virtual call's target, or
// not, then unloads it, when its destructors name one, and loads other DLLs.
// not, then unloads it, when its destructors name one, and loads other DLLs, or has it fault.
#define CT_RUNTIME_LIBRARY_EXPORT extern "C" __declspec(dllexport)

#ifndef NOMINMAX
#define NOMINMAX
#endif
#include <windows.h>

extern "C" void __ct_vcall_trace(void* this_ptr, void* target, const char* site,
const char* static_type);

Expand Down Expand Up @@ -49,3 +54,17 @@ CT_RUNTIME_LIBRARY_EXPORT void ct_runtime_library_trace()
__ct_vcall_trace(&gadget, reinterpret_cast<void*>(&ct_runtime_library_measure),
"runtime_library:1", "<unknown>");
}

namespace
{
volatile int faulted = 0;
} // namespace

// Raises an exception that nothing handles, in the DLL's own code: the handler of the executable's
// copy of the runtime reports it, the DLL's copy installing none (#236). The store after the call
// keeps this frame on the stack, which a tail call would leave out.
CT_RUNTIME_LIBRARY_EXPORT void ct_runtime_library_fault()
{
RaiseException(0xE0000225, EXCEPTION_NONCONTINUABLE, 0, nullptr);
faulted = 1;
}
Loading
Loading