Skip to content

fix(test-build): MinGW test allocator uses FLS init instead of loader hooks - #2850

Open
DeusData wants to merge 2 commits into
mainfrom
exp/r32-mimalloc-win-test-init
Open

DeusData wants to merge 2 commits into
mainfrom
exp/r32-mimalloc-win-test-init

Conversation

@DeusData

@DeusData DeusData commented Oct 30, 2026 •

Copy link
Copy Markdown
Owner

What this is

The fix for the nondeterministic Windows test-leg crash that came with the
mimalloc 3.5.3 update (#2731). It changes how the vendored mimalloc is compiled
for the Windows (MinGW) TEST build only. It started as a CI experiment; the
experiment concluded (see "Result" below), and the decision was to land it as
the test-build fix and to measure the shipped binary separately in #2865.

The crash

  • Job: test / test-windows (windows-latest, CLANG64, x86_64) (ASan+UBSan
    test-runner, 4 suites in parallel).
  • Signature: 1-3 suites die in their FIRST test with exit 0xC0000005
    (pass=0, secs=0, no sanitizer report), while the other ~9,600 tests pass.
  • Suites seen dying: pipeline, doc_mentions_c, doc_mentions_rs, diagrams,
    repro_reference_precision, openapi. Which suites die varies between runs.
  • Counts: 2 of 6 Windows legs on trees that contain fix(mem): vendor mimalloc 3.5.3 so free(NULL) before init is safe (supersedes #2562) #2731 (vendored mimalloc
    3.5.3, merge beddaaa), and 0 of 35 legs on
    trees without it. The legs ran on the same runner image.

Hypothesis

mimalloc 3.5.3 is the first version whose Windows loader hooks are live in a MinGW
build. In 3.3.2 the hook tables were placed with MSVC-only #pragma const_seg /
data_seg, which clang ignores for MinGW targets. That left them as plain data
the loader never called. 3.5.3 adds a __MINGW32__ branch
(vendored/mimalloc/src/prim/windows/prim.c, around lines 902-907) that puts
real entries in .CRT$XLB, .CRT$XLY and .CRT$XIB. Since then:

  • mi_tls_attach runs _mi_auto_process_init from a TLS callback, under the
    loader lock, before the CRT is initialised;
  • mi_tls_detach runs _mi_thread_done on every thread detach, also under the
    loader lock, next to our own .CRT$XLB thread-detach callback
    (src/foundation/compat_thread.c).

The hypothesis is that these hooks cause the first-test 0xC0000005.

What this PR changes

On MinGW only, MIMALLOC_CFLAGS_TEST gains -DMI_WIN_INIT_USE_FLS=1 -DMI_DEBUG=1.

  • MI_WIN_INIT_USE_FLS=1: the test allocator carries no .CRT$XLB,
    .CRT$XLY or .CRT$XIB hooks. Process init runs once, from the
    priority-101 constructor in vendored/mimalloc/src/prim/prim.c, so the
    double-init / blank-stderr-line problem described at MI_MINGW_UCRT64 does
    not come back. Thread-done runs through a fiber-local (FLS) destructor.
  • MI_DEBUG=1: FLS cannot run without it. The first thread init
    (src/init.c, mi_thread_init inside mi_process_init_once) associates the
    main theap with FLS before the FLS key is allocated a few lines later. That
    trips the internal assertion mi_fls_key != (DWORD)(-1) in prim.c and
    would abort every MI_DEBUG=2 process at startup. Level 1 drops only the
    internal assertions and the other MI_DEBUG>1 blocks. It keeps the
    0xD0/0xDF fill patterns, padding checks, double-free checks and statistics.

A cross-compile of the test allocator object for x86_64-w64-mingw32 (the
repository's pinned llvm-mingw image) shows the difference:

  • main has the three .CRT$ hook sections, holding mi_tls_attach,
    mi_tls_detach and mi_crt_init;
  • this branch has none of them, and carries .ctors.65434 / .dtors.65434
    (the constructor) plus the FLS imports;
  • with FLS but MI_DEBUG=2, _mi_prim_thread_associate_default_theap calls
    _mi_assert_fail; with MI_DEBUG=1 it does not.

What this PR does NOT change

  • The production allocator. MIMALLOC_CFLAGS is unchanged on every platform,
    so the shipped binary and its --version output are unaffected.
  • macOS and Linux. Their expanded MIMALLOC_CFLAGS and MIMALLOC_CFLAGS_TEST
    are byte-identical to main, and so are the compile commands from
    make -n -B for the test, TSan and production mimalloc objects.
  • Any vendored source, workflow or test.

Experiment protocol

  1. Rerun this PR's Windows test leg until it has 9 completed legs.
  2. A leg counts as a crash if it shows the signature above (a suite dies in its
    first test with 0xC0000005 and no sanitizer report).
  3. Any crash in the 9 legs rejects the hypothesis.
  4. 0 crashes in 9 legs supports it. If the change made no difference and the base
    rate is 1 in 3, 9 clean legs in a row would happen with probability
    (2/3)^9 = ~2.6%.
  5. A different failure that is not the signature does not count toward either
    outcome. A deterministic one, such as a mimalloc assertion at startup on
    every leg, means the experiment build itself is broken, and the run stops
    there.

Caveat: this run compares the FLS configuration (no loader hooks, internal
assertions off) with main as a whole. A clean result points at the loader
hooks, but on its own it cannot rule out the MI_DEBUG change. The follow-up
decides how far to take that.

Result

9 of 9 Windows test legs were clean (each 9722 passed, 0 failed, no
rc=3221225477; run 41147605534, attempts 1-9). Before the change, about 1 in 3
legs on trees with #2731 crashed; 9 clean legs in a row by chance would be about
2.6% likely. The shipped binary is unchanged by this PR; whether it is affected
by the 3.5.3 loader hooks is tracked in #2865.

…ent)

The Windows test leg (CLANG64 x86_64, ASan+UBSan) has a nondeterministic
crash that only shows on trees with mimalloc 3.5.3: 1-3 suites die in their
first test with 0xC0000005 and no sanitizer report. mimalloc 3.5.3 is the
first version whose loader hooks are live on MinGW: TLS callbacks in
.CRT$XLB/.CRT$XLY run _mi_auto_process_init under the loader lock and
_mi_thread_done on every thread detach.

As an experiment, compile the MinGW TEST allocator with
MI_WIN_INIT_USE_FLS=1. That build carries none of those hooks: process init
runs once from the priority-101 constructor and thread-done goes through a
fiber-local destructor. FLS also needs MI_DEBUG=1, because its internal
assertion that the FLS key exists fires on the first thread init, which runs
before the key is allocated, and that would abort every MI_DEBUG=2 process at
startup. Level 1 keeps the fill patterns, padding and double-free checks.

The production allocator flags (MIMALLOC_CFLAGS) are unchanged on every
platform, and on macOS and Linux MIMALLOC_CFLAGS_TEST is byte-identical.

Signed-off-by: Martin Vogel <[email protected]>
@DeusData DeusData changed the title test(windows): try FLS init for the MinGW test allocator (R32 experiment, do not merge yet) fix(test-build): MinGW test allocator uses FLS init instead of loader hooks Oct 31, 2026

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant