Description
On Windows, sentry_init() crashes with an access violation if it is called from a C++ static initializer that runs before the compiler-generated dynamic initializers in .CRT$XCU — e.g. an initializer placed in .CRT$XCL via #pragma init_seg(lib), or in .CRT$XCA* via a section attribute.
The crashpad backend calls logging::InitLogging(), which acquires a lock stored in a namespace-scope global with a non-trivial constructor. If that constructor has not run yet, the underlying CRITICAL_SECTION is still zero-filled causing an access violation.
We hit this because we initialize crash reporting from a .CRT$XCA* initializer in order to cover crashes during static initialization and Objective-C +load methods (which are initialized from .CRT$XCLz by the GNUstep ObjC runtime).
It worked on 0.12.0 and started crashing after updating to 0.15.4.
When does the problem happen
Environment
- OS: Windows 11, x64 and arm64
- Compiler: Clang, MSVC
- CMake version and config: 3.29.3,
SENTRY_BUILD_SHARED_LIBS=OFF, SENTRY_INTEGRATION_QT=ON, RelWithDebInfo
Steps To Reproduce
repro.cpp:
// crashpad_handler.exe must sit next to the executable (the CMakeLists copies it there).
#include <sentry.h>
#include <stdio.h>
static void init_sentry(void)
{
fprintf(stderr, "[repro] calling sentry_init()\n");
fflush(stderr);
sentry_options_t *options = sentry_options_new();
sentry_options_set_dsn(options, "https://example.com");
sentry_options_set_database_path(options, "sentry-repro-db");
sentry_options_set_debug(options, 1);
sentry_options_set_logger_level(options, SENTRY_LEVEL_DEBUG);
int rv = sentry_init(options);
fprintf(stderr, "[repro] sentry_init() returned %d\n", rv);
fflush(stderr);
}
// ".CRT$XCL" sorts before ".CRT$XCU", where the compiler emits dynamic initializers, so this
// constructor runs before mini_chromium's g_log_file (logging.cc) is constructed.
#pragma init_seg(lib)
struct EarlyInit {
EarlyInit() { init_sentry(); }
};
static EarlyInit g_earlyInit;
int main(void)
{
fprintf(stderr, "[repro] main() reached\n");
fflush(stderr);
sentry_close();
return 0;
}
CMakeLists.txt:
cmake_minimum_required(VERSION 3.20)
project(sentry-early-init-repro CXX)
find_package(sentry REQUIRED)
add_executable(repro repro.cpp)
target_link_libraries(repro PRIVATE sentry::sentry)
# sentry looks for the crashpad handler next to the executable
add_custom_command(TARGET repro POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy_if_different
"$<TARGET_FILE:sentry_crashpad::crashpad_handler>"
"$<TARGET_FILE_DIR:repro>"
)
cmake -S . -B build -G "Visual Studio 17 2022" -A x64 -Dsentry_DIR=<sentry-install>/lib/cmake/sentry
cmake --build build --config RelWithDebInfo
Expected behaviour
Either sentry_init() is safe to call at any point after CRT initialization (including before C++ dynamic initializers), or the constraint is documented (maybe it is but I didn’t find it?).
Actual behaviour
The process dies during static initialization, before main() is reached. The last log line is the one emitted immediately before InitLogging():
[repro] calling sentry_init()
[sentry] INFO using database path "...\sentry-repro-db"
[sentry] DEBUG starting transport
[sentry] DEBUG starting background worker thread
[sentry] DEBUG starting backend
[sentry] DEBUG background worker thread started
[sentry] DEBUG starting crashpad backend with handler "...\crashpad_handler.exe"
<process dies here — neither "sentry_init() returned" nor "main() reached" is printed>
Stack trace:
(4d7c.3dec): Access violation - code c0000005 (first chance)
ntdll!RtlInitializeResource+0x4e3
ntdll!RtlSleepConditionVariableCS+0x43f
ntdll!RtlEnterCriticalSection+0xf2
repro!base::Lock::Acquire+0x8 (Inline Function)
repro!base::AutoLock::{ctor}+0x14 (Inline Function)
repro!logging::InitLogging+0x2a
repro!crashpad_backend_startup+0x842
repro!sentry_init+0x226
repro!init_sentry+0x7c
ucrtbase!initterm+0x36
repro!__scrt_common_main_seh+0x81
KERNEL32!BaseThreadInitThunk+0x17
=== EXCEPTION RECORD ===
ExceptionAddress: 00007ff9c940f6a3 (ntdll!RtlInitializeResource+0x00000000000004e3)
ExceptionCode: c0000005 (Access violation)
ExceptionFlags: 00000000
NumberParameters: 2
Parameter[0]: 0000000000000001
Parameter[1]: 0000000000000024
Attempt to write to address 0000000000000024
Moving the same init_sentry() call from the static initializer into main() initializes and shuts down cleanly.
Root cause
- In mini_chromium’s logging.cc:
g_log_file is a global with a non-trivial constructor, so it is initialized by a dynamic initializer emitted into .CRT$XCU.
InitLogging() is called from sentry_init() and acquires it, causing the crash if the constructor has not been run.
This seems to be specific to the Sentry fork of mini_chromium, as upstream InitLogging() is a plain atomic store with no lock.
Suggested fix
Make mini_chromium's logging state constant-initializable so it does not depend on dynamic initialization (e.g. using an SRWLOCK with SRWLOCK_INIT and making the remaining LogFile members constant-initializable too).
Description
On Windows,
sentry_init()crashes with an access violation if it is called from a C++ static initializer that runs before the compiler-generated dynamic initializers in.CRT$XCU— e.g. an initializer placed in.CRT$XCLvia#pragma init_seg(lib), or in.CRT$XCA*via a section attribute.The crashpad backend calls
logging::InitLogging(), which acquires a lock stored in a namespace-scope global with a non-trivial constructor. If that constructor has not run yet, the underlyingCRITICAL_SECTIONis still zero-filled causing an access violation.We hit this because we initialize crash reporting from a
.CRT$XCA*initializer in order to cover crashes during static initialization and Objective-C +load methods (which are initialized from.CRT$XCLzby the GNUstep ObjC runtime).It worked on 0.12.0 and started crashing after updating to 0.15.4.
When does the problem happen
Environment
SENTRY_BUILD_SHARED_LIBS=OFF,SENTRY_INTEGRATION_QT=ON, RelWithDebInfoSteps To Reproduce
repro.cpp:CMakeLists.txt:Expected behaviour
Either
sentry_init()is safe to call at any point after CRT initialization (including before C++ dynamic initializers), or the constraint is documented (maybe it is but I didn’t find it?).Actual behaviour
The process dies during static initialization, before
main()is reached. The last log line is the one emitted immediately beforeInitLogging():Stack trace:
Moving the same
init_sentry()call from the static initializer intomain()initializes and shuts down cleanly.Root cause
g_log_fileis a global with a non-trivial constructor, so it is initialized by a dynamic initializer emitted into.CRT$XCU.InitLogging()is called fromsentry_init()and acquires it, causing the crash if the constructor has not been run.This seems to be specific to the Sentry fork of mini_chromium, as upstream
InitLogging()is a plain atomic store with no lock.Suggested fix
Make mini_chromium's logging state constant-initializable so it does not depend on dynamic initialization (e.g. using an
SRWLOCKwithSRWLOCK_INITand making the remainingLogFilemembers constant-initializable too).