http://bugs.winehq.org/show_bug.cgi?id=60243 Bug ID: 60243 Summary: Uncaught C++ exception in a DLL static initializer is only reported when the throwing frame has no cleanup landing pad Product: Wine Version: 11.15 Hardware: x86-64 OS: Linux Status: UNCONFIRMED Severity: normal Priority: P2 Component: ntdll Assignee: wine-bugs@list.winehq.org Reporter: awsmadi@amazon.com Target Milestone: --- Distribution: --- When a C++ exception escapes a DLL's static initializers, Wine's loader normally reports the failing module by name from loader_init() (dlls/ntdll/loader.c): err:module:loader_init "throwdll.dll" failed to initialize, aborting err:module:loader_init Initializing dlls for L"...\A.exe" failed, status 20474343 That only happens when there are no cleanup frames between the throw and the loader. Add a single local object with a destructor to the throwing initializer and the diagnostic disappears completely: the process exits with status 5 (STATUS_ACCESS_VIOLATION truncated to its low byte) having printed nothing at all on either stdout or stderr. This matters in practice because essentially every real static initializer constructs an object with a destructor, so the silent path is the common one and the reported path is the rare one. STEPS TO REPRODUCE Two ~20-line cases that differ only by one local object. Built with mingw-w64 GCC 15.3.0 for x86_64-w64-mingw32, -O0 -g, shared libstdc++. Case A, no cleanup frame: #include <stdexcept> struct Boom { Boom() { throw std::runtime_error("boom from a DLL static initializer"); } }; static Boom boom; extern "C" __declspec(dllexport) int touch(void) { return 1; } Case D, identical plus one local object whose destructor is out-of-line so it cannot be elided: #include <stdexcept> struct Noisy { ~Noisy(); }; Noisy::~Noisy() {} struct Boom { Boom() { Noisy n; throw std::runtime_error("boom from a DLL static initializer"); } }; static Boom boom; extern "C" __declspec(dllexport) int touch(void) { return 1; } Each is linked into an executable that calls touch(), so the DLL loads at process start and the initializer runs during PROCESS_ATTACH. Run each under wine. ACTUAL RESULTS case cleanup frame 11.0 11.15 output A no rc=67 rc=67 err:module:loader_init "throwdll.dll" failed to initialize D yes rc=5 rc=5 nothing at all For case D, WINEDEBUG=+seh ends: trace:seh:RtlUnwindEx code=20474343 flags=7 end_frame=0000000000000000 target_ip=0000000000000000 err:seh:RtlUnwindEx invalid frame 00007FFFFE300008 (00007FFFFE102000-00007FFFFE300000) trace:seh:RtlRestoreContext returning to 0000000000000000 stack 00007FFFFE300008 trace:seh:dispatch_exception code=c0000005 (EXCEPTION_ACCESS_VIOLATION) addr=0000000000000000 err:seh:call_seh_handlers invalid frame 00007FFFFE300008 (00007FFFFE102000-00007FFFFE300000) err:seh:NtRaiseException Exception frame is not in stack limits => unable to dispatch exception. The establisher frame is StackBase + 8, eight bytes past the top of the stack, so is_valid_frame() (dlls/ntdll/signal_x86_64.c:259 and :735) rejects it and sets EXCEPTION_STACK_INVALID. Control then transfers to RIP 0, and the resulting access violation cannot be dispatched either. The process is killed at dlls/ntdll/unix/thread.c:1660 with rec->ExceptionCode. EXPECTED RESULTS Case D reports the failing module by name, as case A does. RELATIONSHIP TO BUG 59850 Bug 59850 ("Exit unwind is broken") is fixed in 11.15 and the fix does engage here, but it is not sufficient. Instrumenting the TEB-frame walk shows "calling TEB handler" going from 1 occurrence on 11.0 to 2 on 11.15, so the previously-skipped handler now runs, while every observable outcome is unchanged: no err:module:loader_init, the same two "invalid frame" errors, the same exit 5. For reference, 11.0 has the pre-fix loop and 11.15 has the fixed one: /* 11.0, dlls/ntdll/signal_x86_64.c:779-781. On an exit unwind end_frame is null, so teb_frame < 0 is never true and the body never runs. */ while (is_valid_frame( (ULONG_PTR)teb_frame ) && (ULONG64)teb_frame < new_context.Rsp && (ULONG64)teb_frame < (ULONG64)end_frame) /* 11.15, dlls/ntdll/signal_x86_64.c:778-781 */ ULONG_PTR last_frame = new_context.Rsp; if (end_frame && (ULONG_PTR)end_frame < last_frame) last_frame = (ULONG_PTR)end_frame; while (is_valid_frame( (ULONG_PTR)teb_frame ) && (ULONG_PTR)teb_frame < last_frame) WHY THE NULL end_frame IS CORRECT ON THE CALLER'S SIDE libgcc writes the unwind target into private_[1]/private_[2] only inside the _URC_HANDLER_FOUND branch of _GCC_specific_handler (libgcc/unwind-seh.c), and zeroes them at throw time. With no C++ handler anywhere on the stack, which is the situation before main(), that branch never runs, so _Unwind_Resume passes zeros to RtlUnwindEx. Requesting an exit unwind with a null target frame is documented and correct; the problem is in honouring the request. The number of target-less exit unwinds equals the number of cleanup frames, so one cleanup frame is enough to trigger this. DOWNSTREAM CONTEXT Found while porting Nix to Windows. It made a cross-compiled test binary exit 5 with zero bytes on both streams, which three of Nix's five unit-test suites did. Write-up: https://github.com/NixOS/nix/issues/16356 Tested with nixpkgs' wineWow64Packages.stable (11.0) and .unstable (11.15) on NixOS, x86_64. -- Do not reply to this email, post in Bugzilla using the above URL to reply. You are receiving this mail because: You are watching all bug changes.