Repository navigation
fix(runtime): install the crash handler from the executable's copy only - #241
Merged
Merged
Conversation
The copy of the runtime in a DLL that links it installs its crash handler when the DLL loads. A child that removes the executable's handler, as a program without the runtime has none, then faults while the DLL is loaded gets the backtrace of the DLL's copy. Refs #236
A program that installs a filter of its own over the handler of a DLL's copy keeps it when FreeLibrary unloads the DLL: the child's filter, not the runtime's handler, ends the process, with an exit code of its own. Refs #236
With CT_BACKTRACE set, the copy of the runtime in a DLL installs its crash handler over the filter installed before it, the executable's handler or one of the program's, and nothing put that filter back once FreeLibrary had unloaded the DLL. The copy now keeps the filter it replaced, and puts it back when FreeLibrary unloads the DLL, after the destructors of the DLL's global objects, if its handler is still the process's: a filter installed over it since stays. Nothing changes when the process terminates. Refs #236
The copy of the runtime in a DLL installs its crash handler over the executable's. Once FreeLibrary has unloaded the DLL, whose code is gone, a crash must reach the executable's handler again. Refs #236
A program's own filter, installed before a DLL that links the runtime loads, must handle the crashes again once FreeLibrary has unloaded the DLL, whose copy installed its handler over the filter. Refs #236
Two copies of the DLL that links the runtime, each a module apart, each install their handler over the one before. Unloaded in reverse order, they must leave the executable's handler to report the crash. Refs #236
Each copy of the runtime keeps the filter that its crash handler replaced in a record, in a section .ctcrash of its module, with a magic number and the handler's address: another copy can find the record from the handler alone. Refs #236
Two DLLs that link the runtime, unloaded in load order: the first copy's handler was no longer the process's when it unloaded, so it left the filters as they were, and the second copy, whose handler had replaced the first one's, put that handler back when it unloaded in turn, although its code was gone. A copy whose handler is no longer the process's now walks back from the process's filter through the handlers of the copies that keep a record, to the copy whose handler went over its own, and hands it the filter it replaced. The walk stops at the first filter that is not such a handler. Refs #236
The first copy unloads while the second's handler, installed over its own, is still there. Once the second unloads too, the executable's handler must report the crash, not the first copy's, whose code is gone. Refs #236
A copy of the runtime that unloads while another copy's handler is over its own looked for that copy from the process's filter back, and stopped at the first filter that was not a copy's handler. A filter of the program installed over the other copy's handler, as a scope that puts that handler back afterwards does, hid the copy: once it unloaded in turn, it put back a handler whose code was gone. The copy now looks for the record that holds its handler among the loaded modules, which no thread unloads while FreeLibrary holds the loader lock. The record no longer needs the handler's address. Refs #236
The tests that unload two copies of the DLL that links the runtime load them through one function, for a test to come that unloads them in another way. Refs #236
The first copy unloads while a filter of the program is over the second copy's handler, which had replaced the first one's. Once the program has put the second copy's handler back and that copy has unloaded too, the executable's handler must report the crash, not the first copy's, whose code is gone. Refs #236
The host's filter of the crash tests prints the code of the exception it gets, and the tests expect the fault's: an exception raised while the DLL unloads, such as an access violation, no longer passes for it. Refs #236
A test whose DLL stayed mapped after FreeLibrary would find its copy's handler still there, and pass whatever the runtime does: the children of the crash tests now exit with 2 when a DLL that links the runtime is still mapped once freed. The copies of the DLL that two tests load are named after the child's process, which deletes them once unloaded: two runs that share a temporary directory no longer copy over a DLL that the other has loaded. Refs #236
The crash tests accepted a backtrace from any copy of the runtime. The child now prints the image of the module whose handler must report the crash, the executable's or the DLL's, and the first frame of the backtrace, which the handler takes in itself, must lie in it. Refs #236
ct_is_readable, which the vtable boxes use before they read the RTTI of an object, moves out of the anonymous namespace of windows/ct_runtime_vtable.cpp, declared with the other helpers of the Windows runtime, for the crash handler to use as well. Refs #236
A copy of the runtime whose handler is no longer the process's reads the headers of the loaded modules to find the copy whose handler went over its own. A packer or an anti-tamper check may erase a module's headers, or make their page unreadable: the read then faulted inside DllMain, while FreeLibrary held the loader lock. The copy now checks each part with ct_is_readable, and the DOS and PE signatures, before it reads further, and skips the module otherwise. A record must also lie in a writable section, since the copy updates it. Refs #236
A DLL without the runtime has the page of its headers made unreadable, as a packer or an anti-tamper check can. The DLL that links the runtime, under the program's filter, then unloads on a thread of its own: its copy, looking for the copy above, must skip that module rather than fault while FreeLibrary holds the loader lock. Refs #236
The copy of the runtime that unloads looked for the copy above among the first 1024 modules only. That copy loaded after this one, so it comes late in the list, where a process with more modules cut it off, and it then put back a handler whose code was gone. The copy now asks how many modules the loader lists, and reads them all into a block of the process's heap, which DllMain may allocate. The comment says that a DLL calling exit() runs this without the loader lock. Refs #236
The copy of the runtime that unloads gave the filter it replaced to the first copy found whose record held its handler, and looked for one only when its handler was no longer the process's. A program that puts back a filter it read earlier, over the handlers of copies loaded since, can leave two records holding the handler, or one whose copy's handler is not the process's while the unloading one's is. The copy now hands the filter over to every record that holds its handler, whether its handler was still the process's or not. Refs #236
A program that puts back a filter it read earlier, the first copy's handler, over the second copy's, then loads a third copy, leaves the second and the third copies' records both holding the first one's handler. Once the first copy unloads, neither the third nor the second copy may put that handler back when they unload in turn, whatever the order in which the loader lists their modules. Refs #236
With CT_BACKTRACE, each copy of the runtime installs its crash handler over the filter installed before it. The README says when a DLL's copy puts that filter back, and what becomes of a filter installed over the handler since: the program's own, or another copy's handler. Refs #236
The test of a DLL unloading past unreadable module headers passed without the readability checks too: the access violation that the read raised during FreeLibrary did not reach the program's filter. A vectored handler, which sees every exception before any other handler, now records an access violation in the unreadable page while the DLL unloads, and the child exits with 4 when there was one. Refs #236
A DLL that calls its own C runtime's exit() runs its destructors before the loader detaches it, without the loader lock: the release of its copy's crash handler then walked the modules while other threads could still unload them, although the DLL stays mapped until the process ends. The TLS callback of windows/ct_runtime_shutdown.cpp now records the loader's DLL_PROCESS_DETACH, which comes before the module's entry point and the destructors that the C runtime runs from it, and the release runs only once the loader detaches the DLL. Refs #236
The release of a DLL's copy found the copies whose handlers went over its own by listing the modules into a block of the process's heap: when the allocation or the listing failed, nothing was handed over, and a copy loaded later put back the unloaded copy's handler. Each DLL's copy now links its record, when it installs its handler, to the record of the copy whose handler it went over, found from that handler's address. Both happen with the loader lock held, at the DLL's attach and at FreeLibrary's detach: the release follows the links, takes the copy out of the list below it, and hands the filter it replaced to the copies above, without allocating or listing the modules. The executable's copy links to none. Refs #236
The release of a DLL's copy sets the filter its handler replaced, then puts back the filter it found there when that is not its handler. A filter that another thread installed between those two calls was lost: the second call replaced it. The second call returns that filter instead of the one the first call set: the release now puts it back in turn, and so on, so that no filter installed meanwhile is lost. The thread that installed it still got, as the filter it replaced, the one set by the release for that moment: Windows has no call that reads the process's filter, or sets it only if it is the one expected. Refs #236
A DLL's copy whose handler goes over a filter in a module whose headers cannot be read, as a packer or an anti-tamper check can leave them, cannot tell whether that filter is another copy's handler, nor link its record to it. Installed anyway, it would put that handler back once its DLL had unloaded. The copy now puts the filter back in place and leaves its own handler out, saying so. Refs #236
The README says that a DLL's copy links its record to the copy below when it installs its handler, that it leaves its handler out over a filter whose module it cannot read, what becomes of a filter that another thread installs while it puts its own back, and that a DLL ending the process with exit() changes nothing. Refs #236
A DLL's copy releases its handler only where RtlDllShutdownInProgress tells FreeLibrary apart from the process's termination, from Windows 10 on. Before that, its record would stay linked in the records of other copies once FreeLibrary had unmapped its module, and their release would follow a link into unmapped memory. The copy now links its record only where ct_process_shutdown() can tell. Elsewhere it installs its handler as before this change, without a link. Refs #236
ACopyStaysOffAFilterWhoseModuleItCannotRead made the header page of a loaded DLL PAGE_NOACCESS, then loaded a second DLL: the loader reads that header under its lock while it loads the second, and hung the job for its 45-minute limit on every Windows build. The production path it covered stays, with a comment on why it has no unit test. The copy of the runtime still leaves its handler out when it cannot read the headers of the module of the filter in place. Refs #236
The crash handler of a DLL's copy will no longer be installed, nor put back when FreeLibrary unloads the DLL: the tests of that hand-over go. They checked a copy that cannot read the headers of the module of the filter in place, two copies whose handlers replaced one handler, a DLL that calls exit(), a hand-over for which nothing can be allocated, a filter installed by another thread during the release, and two DLLs unloaded around a filter of the program. Refs #236
Only the dropped tests of a DLL that calls exit() and of a hand-over for which nothing can be allocated loaded it. Refs #236
The test checked that the copy of the runtime in a DLL reports the process's crashes while the DLL is loaded, in a program without a handler of its own. That copy will no longer install its handler: only the executable's copy will, a contract under which such a program gets no crash backtrace from the DLL. Refs #236
With CT_BACKTRACE, the copy of the runtime in a DLL that links it installed its crash handler for the whole process, and had to take it out when FreeLibrary unloaded the DLL. Windows has no call that sets the process's filter only if it still is the one expected: the release put the filter back by exchanges, in a loop that another thread changing its filters could keep going with the loader lock held, and that put back a filter over one that the program had set again to the same address. A filter that the program installed over the handler would call it once its code was gone, too. The copy in a DLL now installs no handler, and leaves the process's filter alone: the executable's copy reports the crashes of the whole process, those of the DLL's code included, and FreeLibrary unloads the DLL as usual. An instrumented DLL that a program without the runtime loads reports no crash. Refs #236
No copy of the runtime in a DLL installs the crash handler any more: nothing remains to put back when FreeLibrary unloads the DLL. The records in the section .ctcrash, their links between copies, the release that put the filter back by exchanges, and the inspection of the headers of the module of the filter in place go. Refs #236
The crash handler records shared it with the vtable diagnostics, its only user once the records are gone: it is local to that file again, as on main. Refs #236
Only the release of a DLL copy's crash handler asked whether the loader, rather than the DLL's own exit(), runs the module's destructors: that release is gone. Refs #236
The DLL exports a function that raises an exception that nothing handles, so that a test can check which handler reports a crash in a DLL whose copy of the runtime installs none. Refs #236
The comments of the crash tests that remain said that a DLL's copy installs its handler over the one before it and hands it over when the DLL unloads. They now say that the copy installs none, that the executable's handler or the program's filter reports the crash once the DLL has unloaded for real, and the copies of the DLL that links the runtime always come from that DLL. Refs #236
The DLL that links the runtime faults in its own code while it is loaded. The executable's copy of the runtime, the only one that installs a crash handler, prints the backtrace: its first frame lies in the executable's image, and a frame lies in the DLL's. Refs #236
In a program that does not link the runtime, which has no handler of its own, the DLL that links the runtime loads and unloads without installing a filter: the process's filter stays null. Such a program gets no crash backtrace from an instrumented DLL, the limit of the contract under which only the executable's copy installs the handler. Refs #236
The test of a filter that the program installs before it loads the DLL that links the runtime checked only the crash after the DLL unloaded. It now checks that the filter stays the process's while the DLL is loaded too, and exits with 3 if the DLL's copy replaced it. Refs #236
The release that put a DLL copy's filter back went on exchanging filters for as long as another thread changed the filter between two of its calls, with the loader lock held. The test points the copy's import of SetUnhandledExceptionFilter at a stand-in after which another thread installs one of two filters in turn, 10000 times at most, and fails with 4 when the copy kept calling for that long. Refs #236
When the release of a DLL copy's filter had set, for a moment, the filter that its handler replaced, and the host installed that same filter again meanwhile, the release could not tell the two apart and put back the filter the host had replaced. The test points the copy's import of SetUnhandledExceptionFilter at a stand-in after whose first call the host installs again the filter that call set, and fails with 3 when the process's filter is not then the one the host installed last. Refs #236
The section on the crash handler on Windows described the hand-over of the filter between the copies of DLLs. It now says that only the executable's copy installs a handler, which reports the crashes in an instrumented DLL's code too, that a DLL's copy leaves the program's filter alone and lets FreeLibrary unload the DLL as any other, and the limit: in a program that does not link the runtime, an instrumented DLL reports no crash. Refs #236
The entry of #236 described the hand-over of a DLL copy's filter when FreeLibrary unloads the DLL. It now says that only the executable's copy installs the crash handler, which reports the crashes in an instrumented DLL's code too, and that an instrumented DLL that a program without the runtime loads reports no crash any more. Refs #236
The test of a crash in the DLL's code combined its two checks with testing::Truly and testing::AllOf, which come from gmock: the runtime unit tests link gtest only, and the file did not build on Windows. The matcher of a crash backtrace printed by a given handler now takes the mark of the image where a frame must lie too. Refs #236
The test of an unload while another thread changes the filter passed on 0111c53, whose release looped for as long as another thread changed the filter between two of its calls. Its stand-in installed its two filters in turn by the parity of the call, so the third call returned the filter that the second had set: the release took it for its own and stopped. The stand-in now installs whichever of its two filters the copy's call did not set, as a thread that keeps changing the filter does. Refs #236
The child of the tests that unload a DLL linking the runtime, then load another, started its watchdog thread just before LoadLibrary, without waiting for it. A thread takes the loader lock to start, to attach to the loaded modules: when the unload waits forever with that lock held before the watchdog has started, the watchdog waits too, and nothing ends the child. startWatchdog() now returns once the watchdog runs, and the child calls it before it loads the DLL. Refs #236
The child of the crash tests started its watchdog thread without waiting for it, then the thread that faults. A thread takes the loader lock to start: should the fault's thread take that lock and wait forever with it held before the watchdog has started, the watchdog would wait too, and nothing would end the child. The child now waits until the watchdog runs before it starts the fault's thread. Refs #236
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Refs #236
Summary
With
CT_BACKTRACEset, each copy of the runtime installed its crash handler withSetUnhandledExceptionFilterwhen its module initialized, over the filter installed before it, the copy in a DLL that links the runtime included. That copy never put the filter back: onceFreeLibraryhad unloaded the DLL, the process's filter was still its handler, whose code was gone, and an exception that nothing handled ended the process without a backtrace.This PR makes only the executable's copy install the crash handler, in
windows/ct_runtime_backtrace.cpp. The copy in a DLL installs none and leaves the process's filter alone: there is nothing to put back whenFreeLibraryunloads the DLL, which it does as for any other DLL.FreeLibraryunloads the DLL for real, and a DLL's copy makes no call on the process's filter, under the loader lock or otherwise.CT_BACKTRACEset: its own filter, if any, or Windows handles the crash. The README's section "Crash handler on Windows" says so.Why not put the filter back
Up to 0111c53, this PR had a DLL's copy put back the filter that its handler had replaced when
FreeLibraryunloaded the DLL, through records that linked the copies. Windows has no call that reads the process's filter, or sets it only if it still is the one expected:SetUnhandledExceptionFilterexchanges it. The review of 0111c53 found two consequences, which no loop of exchanges avoids:FreeLibrary, and the loader with it, waiting.A filter that the program installs over a DLL's handler, and that calls the handler it replaced, would also call code that is gone, which no release can prevent. Of the contracts considered (keep the DLL mapped, restore within a bound, a persistent trampoline, the executable's copy only), the last guarantees the three properties above without keeping the DLL mapped or allocating executable memory, at the cost of the limit above. The rework removes the records, their links, the release and the checks it needed, and brings
ct_is_readableandct_runtime_shutdown.cppback to their state onmain: the only change to the runtime is inct_maybe_install_backtrace.Evidence
The tests ran on
main, on 0111c53 and on this branch, in the four Windows jobs ofLLVM versions(LLVM 19, 20, 22, 23). Each test's child setsCT_BACKTRACE, its executable links the runtime, and "the host's filter" is one that the test installs as a crash reporter would.probe/236-exe-only-red-main, 1d2c88bmainprobe/236-exe-only-red-0111c53, 3893f7cLLVM versionspass, and so doBuild,clang-format,license-complianceand the commit conventionsThe first runs of the test of the review's first finding passed on 0111c53: its stand-in installed its two filters in turn by the parity of the call, so the release got back the filter it had just set and stopped after four calls, a case of the second finding. The stand-in now installs whichever filter the copy's call did not set, and 0111c53 fails it at the 10,000 calls the test allows. The branch's commits a2395d2 to 83c5f32 do not build on Windows: that test then used gmock's
testing::Trulyandtesting::AllOf, and the runtime unit tests link gtest only; d6c3c1b fixed it.Tests
mainACrashInADllThatLinksTheRuntimeIsReportedByTheExecutableOnceADllThatLinksTheRuntimeUnloadsTheExecutableReportsCrashesADllThatLinksTheRuntimeInstallsNoHandlerInAProgramWithoutOneTheHostsFilterInstalledBeforeADllStaysWhileItComesAndGoesADllUnloadedLeavesTheHostsFilterInstalledAfterItOnceTwoDllsUnloadInReverseOrderTheExecutableReportsCrashesOnceTwoDllsUnloadInLoadOrderTheExecutableReportsCrashesADllUnloadsWhileAnotherThreadKeepsChangingTheFilterSetUnhandledExceptionFilter: the copy does not keep calling, and fails the test with 4 at 10,000 calls. The review's first finding.TheFilterTheHostInstallsLastStaysOnceADllUnloadsEach test frees the DLL and fails with 2 when it stays mapped:
FreeLibraryunloads it for real. The last two tests point the DLL's copy's import ofSetUnhandledExceptionFilterat a stand-in, in the test, after loading the DLL: the runtime has no seam for them.The tests of the hand-over go with it: a copy over a filter whose module headers it cannot read, two copies whose handlers replaced one handler, a DLL that calls
exit(), a hand-over for which nothing can be allocated, a filter installed by another thread during the release, two DLLs unloaded around a filter of the program, and a DLL's copy that reports the crashes while its DLL is loaded, which the new contract no longer offers.The watchdogs of two test helpers
On
main,unloadARuntimeLibraryThenLoadAnotherandfaultOnAThreadstart the thread that ends their child with 7 after 20 seconds without waiting for it. A thread takes the loader lock to start, to attach to the loaded modules: when the code under test waits forever with that lock held before the watchdog has started, the watchdog waits too, and nothing ends the child. #242 met this inchurnAndUnload(d19eabf).startWatchdog()now returns once its thread runs, and both helpers call it (b022ebc, 2f8051a).A probe ran each helper in a child on one CPU, its thread above the threads it starts, as on a machine whose other CPUs are busy, while the DLL that links the runtime waited forever in its unload with the loader lock held. A thread started before the helper ended the child with 11 after 60 seconds when the helper's watchdog had not ended it.
unloadARuntimeLibraryThenLoadAnotherfaultOnAThreadprobe/241-watchdog-red, 3b04a49: this branch at 001d887probe/241-watchdog-green, 6f15efa: this branch at 2f8051afaultOnAThreadstarts its watchdog before the thread that faults, so on one CPU the watchdog runs first. Its race needs several CPUs and a watchdog delayed until the fault's thread holds the loader lock, which the probe cannot force without changing the helper: the change there removes a dependence on the scheduler.Limits
CT_BACKTRACEset.dlcloseunloads it.