Skip to content

jit: instrument COFF add-on load (diagnose Windows exit-127)#2098

Open
jcelerier wants to merge 39 commits into
masterfrom
jit-coff-load-diag
Open

jit: instrument COFF add-on load (diagnose Windows exit-127)#2098
jcelerier wants to merge 39 commits into
masterfrom
jit-coff-load-diag

Conversation

@jcelerier

Copy link
Copy Markdown
Member

Temporary diagnostics to pinpoint the silent Windows JIT-load failure (exit 127 after the add-on compiles). Surfaces ORC errors to stderr + brackets each load step / eager lookup. Once merged, continuous rebuilds and the template JIT-Windows test will print what fails instead of exit 127, so the follow-up fix is targeted. Will be reverted with that fix.

🤖 Generated with Claude Code

The Windows JIT compiles the add-on but the *load* dies silently (exit 127, no
message) -- because the ORC error reporter only appends to m_errors (never
prints) and the eager-linking loop consumeError-swallows failures. Make both
visible (flushed to stderr) and bracket each load step + per-symbol eager lookup,
so the next Windows run shows whether it's a clean JITLink error (and which
symbol) or a hard crash, and where. Temporary diagnostic; reverted with the fix.
@jcelerier jcelerier force-pushed the jit-coff-load-diag branch from 05caa7e to 1232859 Compare June 25, 2026 00:28
jcelerier and others added 28 commits June 24, 2026 23:01
The instrumented Windows run pinpointed the crash: the JIT link fully succeeds
(all add-on symbols resolve) but m_jit->initialize() segfaults (SIGSEGV/139)
running static initializers -- and boost.asio thread_locals are among the
resolved symbols. Fall back to emulated TLS on COFF (host emutls runtime),
keeping native TLS on ELF/MachO, to test whether native COFF TLS is the cause.
Also skip llvm.* pseudo-globals (e.g. llvm.global_ctors) in the eager loop.
- Revert the emulated-TLS-on-COFF experiment (it's worse: host has no
  __emutls_get_address, so the add-on fails to materialize). Back to native TLS.
- Add SCORE_JIT_SKIP_INIT env toggle: skip m_jit->initialize() to test whether the
  Windows crash is purely in running static initializers.
- Disable all non-Windows CI on this diag branch and trim win-builds.yaml to the
  artifact-producing win32 job only, for faster iteration.
A tiny standalone exe (no Qt/score/boost/clang-frontend) that JIT-loads a
pre-compiled .ll through the *same* MinGWCOFFPlatform + initialize() path the
score JIT uses, so we can isolate the Windows COFF static-init crash with 5-line
cases instead of the full generated plugin. Builds in minutes.

Cases isolate: trivial / global-ctor / host-call-in-ctor / thread_local-use /
tls-in-ctor / exception. The jitmin.yaml workflow builds it against sdk37's
LLVM, compiles each case with the SDK clang, and runs jitmin on each -- the step
logs pinpoint exactly which construct faults.
c00_trivial (no static init) hard-crashes in initialize() -> the bug is the
COFF init dispatch itself, not add-on ctors. Add SKIP_INIT to confirm the JIT
otherwise works, run every case (set +e), and log rt_pushInitializers entry to
see whether init even reaches our wrapper before crashing.
…OFF dlopen)

Root cause (via jitmin): our LLVM-side init dispatch completes cleanly, then
orc_rt's COFF dlopen init path segfaults -- it runs .CRT$XC* ctors through the
MSVC CRT (_initterm), which a VC-runtime-free MinGW session doesn't provide.

Fix: collect each add-on JITDylib's .CRT$XI*/.CRT$XC* ctor addresses at
materialization (registerObjectPlatformSections) and run them directly via
runAsVoidFunction in a custom MinGWCOFFPlatformSupport::initialize() -- exactly
like the bootstrap already does and which we proved works. orc_rt's dlopen init
is no longer used on MinGW. (deinitialize is a no-op for now -- teardown dtors
are a follow-up.) jitmin reordered to materialize before initialize().
LLJIT's IR layer lowers llvm.global_ctors into a synthetic init function set as
the module's init symbol -- no .CRT$XC* section is emitted, so collecting ctor
section edges finds nothing. Record MR.getInitializerSymbol() per-JD and run it
directly by name lookup in runJDInitializers().

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RJk9KBVYSsZdLhNv2q3mfi
…symbol)

The scraped $.<module>.__inits.0 symbol is MaterializationSideEffectsOnly and
cannot be looked up after the module materialized via entry. Instead extract the
init function's address directly from the link graph at post-fixup and run it.
Also test a native-TLS (-fno-emulated-tls) variant for the TLS cases.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RJk9KBVYSsZdLhNv2q3mfi
…ls_get_address

The IR scraper makes the init symbol side-effects-only with no graph body; the
runnable init code is clang's _GLOBAL__sub_I_<tu> (or the lone
__cxx_global_var_init). Extract its address from the link graph. Also bind
__emutls_get_address (windows-gnu always uses emulated TLS; compiler-rt has it
but the host exe doesn't export it).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RJk9KBVYSsZdLhNv2q3mfi
Real add-ons failed to JIT-link with '.pdata: relocation target out of range of
Pointer32 fixup': Windows x64 SEH unwind tables use 32-bit image-relative RVAs,
but the default InProcessMemoryManager scatters segments so __ImageBase and the
code land >2GB apart. Reserve one 1GiB contiguous slab via
MapperJITLinkMemoryManager + InProcessMemoryMapper instead, keeping every segment
and unwind section within RVA range. Applied to both the score JIT plugin and the
jitmin harness.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RJk9KBVYSsZdLhNv2q3mfi
…ract only as fallback

Real add-ons have a populated .ctors section AND clang's __cxx_global_var_init
symbols, so the section-edge collection and the name-based init-extract were both
collecting the same 6 initializers -> constructors ran twice -> crash. Fold the
name-based extraction into registerObjectPlatformSections as a fallback that only
fires when no .ctors/.CRT$XC*/.init_array edges were found (the pre-scraped .ll
case). Real add-ons now run each constructor exactly once.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RJk9KBVYSsZdLhNv2q3mfi
…en't truncated

After the slab + double-run fixes the real add-on compiles, links and runs its
constructors, but crashes in score::registerPlugin -> __dynamic_cast jumping to a
half-truncated pointer (0xffffff16_25ffffff). The small (default) code model
emits 32-bit absolute relocations for data pointers (vtables, type_info); at the
high slab load address those truncate and corrupt RTTI. Set CodeModel::Large on
COFF (score plugin + jitmin) for 64-bit references throughout -- expected to fix
cross-module dynamic_cast and the c05 exception-typeinfo crash too.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RJk9KBVYSsZdLhNv2q3mfi
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