Skip to content

Windows: sentry_init() crashes when called before C++ dynamic initializers (regression) #1930

Description

@triplef

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

  • During build
  • During run-time
  • When capturing a hard crash

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).

Metadata

Metadata

Assignees

No one assigned

    Labels

    Bugsomething isn't working as it shouldLogsNativeplatform label

    Projects

    Status
    No status

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions