https://github.com/vgvassilev created https://github.com/llvm/llvm-project/pull/196874
`LLJIT::deinitialize` (via `GenericLLVMIRPlatformSupport`) collects each JITDylib's deinit symbols (`__lljit_run_atexits` plus per-module dtors) through `Platform::lookupInitSymbols`, which triggers fresh JIT materialization for symbols that were emitted but never reached. When deinitialize runs from a static destructor -- as happens for embedders that hold the JIT in a function-local static (e.g. CppInterOp) -- that compile pipeline can reach into LLVM function-local statics already destroyed by their own static destructors (`OptBisect.cpp`'s `OptBisector`/`OptDisabler`, `TargetLibraryInfoImpl::Strings`, ...). The result is a SEGFAULT in SelectionDAG codegen, or a use-of- uninitialized-value report under MSan, with a stack rooted at `~Interpreter` -> `OrcIncrementalExecutor::cleanUp` -> `lookupInitSymbols` -> `IRTransformLayer::emit` -> `LowerEmuTLS::runOnModule` -> `getGlobalPassGate`. Add `Platform::lookupResolvedInitSymbols`: walks each JITDylib's symbol table under the session lock, keeps only entries already at `>= SymbolState::Resolved`, defers to the existing `lookupInitSymbols` on the filtered set (now no-op materialization-wise, since every remaining symbol is already resolved). Switch `GenericLLVMIRPlatformSupport::getDeinitializers` to use it; replace its "every JD has at least __lljit_run_atexits" assertion with a `continue`, since the new filter can legitimately drop a JD whose run-atexits thunk was never materialized. A symbol that never materialized never ran its constructor either, so skipping its destructor is correct behavior, not a leak. The init path (`getInitializers`) keeps the existing materializing lookup -- program startup wants ctors to run. Two regression tests are included: a unit test in `CoreAPIsTest` pinning the no-materialization contract of the new helper (with `lookupInitSymbols` as baseline), and an `EXPECT_EXIT`-based clang-repl test in `InterpreterTest` that holds two Interpreters in function-local statics and `std::exit`s -- it SEGFAULTs without this patch and exits cleanly with it. >From 0f961cf2a6f82680e08c25949152a5b4f7e526b5 Mon Sep 17 00:00:00 2001 From: Vassil Vassilev <[email protected]> Date: Sun, 10 May 2026 19:15:59 +0000 Subject: [PATCH] [ORC] Skip lazy materialization in JIT deinit symbol lookup. `LLJIT::deinitialize` (via `GenericLLVMIRPlatformSupport`) collects each JITDylib's deinit symbols (`__lljit_run_atexits` plus per-module dtors) through `Platform::lookupInitSymbols`, which triggers fresh JIT materialization for symbols that were emitted but never reached. When deinitialize runs from a static destructor -- as happens for embedders that hold the JIT in a function-local static (e.g. CppInterOp) -- that compile pipeline can reach into LLVM function-local statics already destroyed by their own static destructors (`OptBisect.cpp`'s `OptBisector`/`OptDisabler`, `TargetLibraryInfoImpl::Strings`, ...). The result is a SEGFAULT in SelectionDAG codegen, or a use-of- uninitialized-value report under MSan, with a stack rooted at `~Interpreter` -> `OrcIncrementalExecutor::cleanUp` -> `lookupInitSymbols` -> `IRTransformLayer::emit` -> `LowerEmuTLS::runOnModule` -> `getGlobalPassGate`. Add `Platform::lookupResolvedInitSymbols`: walks each JITDylib's symbol table under the session lock, keeps only entries already at `>= SymbolState::Resolved`, defers to the existing `lookupInitSymbols` on the filtered set (now no-op materialization-wise, since every remaining symbol is already resolved). Switch `GenericLLVMIRPlatformSupport::getDeinitializers` to use it; replace its "every JD has at least __lljit_run_atexits" assertion with a `continue`, since the new filter can legitimately drop a JD whose run-atexits thunk was never materialized. A symbol that never materialized never ran its constructor either, so skipping its destructor is correct behavior, not a leak. The init path (`getInitializers`) keeps the existing materializing lookup -- program startup wants ctors to run. Two regression tests are included: a unit test in `CoreAPIsTest` pinning the no-materialization contract of the new helper (with `lookupInitSymbols` as baseline), and an `EXPECT_EXIT`-based clang-repl test in `InterpreterTest` that holds two Interpreters in function-local statics and `std::exit`s -- it SEGFAULTs without this patch and exits cleanly with it. --- .../unittests/Interpreter/InterpreterTest.cpp | 26 ++++++++++ llvm/include/llvm/ExecutionEngine/Orc/Core.h | 10 ++++ llvm/lib/ExecutionEngine/Orc/Core.cpp | 23 +++++++++ llvm/lib/ExecutionEngine/Orc/LLJIT.cpp | 8 +-- .../ExecutionEngine/Orc/CoreAPIsTest.cpp | 51 +++++++++++++++++++ 5 files changed, 115 insertions(+), 3 deletions(-) diff --git a/clang/unittests/Interpreter/InterpreterTest.cpp b/clang/unittests/Interpreter/InterpreterTest.cpp index 9ff9092524d21..52261c573ffae 100644 --- a/clang/unittests/Interpreter/InterpreterTest.cpp +++ b/clang/unittests/Interpreter/InterpreterTest.cpp @@ -443,4 +443,30 @@ TEST_F(InterpreterTest, TranslationUnit_CanonicalDecl) { sema.getASTContext().getTranslationUnitDecl()->getCanonicalDecl()); } +TEST_F(InterpreterTest, ShutdownDoesNotMaterializeAgainstDestroyedGlobals) { +#ifdef CLANG_INTERPRETER_PLATFORM_CANNOT_CREATE_LLJIT + GTEST_SKIP() << "Platform cannot create LLJIT"; +#endif + EXPECT_EXIT( + { + // Function-local statics so the dtors run during process exit, + // not when the lambda returns. + static auto I1 = createInterpreter(); + static auto I2 = createInterpreter(); + + // Static initializers in JIT'd code emit init + deinit symbols + // that the JIT lazily materializes on demand. Two interpreters + // is the minimal repro for the original embedder pattern. + cantFail(I1->ParseAndExecute( + "struct S1 { S1() {} ~S1() {} }; static S1 s1;")); + cantFail(I2->ParseAndExecute( + "struct S2 { S2() {} ~S2() {} }; static S2 s2;")); + + // Trigger the C++ static-dtor phase explicitly (death-test + // _exit otherwise skips it). + std::exit(0); + }, + ::testing::ExitedWithCode(0), ""); +} + } // end anonymous namespace diff --git a/llvm/include/llvm/ExecutionEngine/Orc/Core.h b/llvm/include/llvm/ExecutionEngine/Orc/Core.h index 2c0b17de5fa22..28adf566633c6 100644 --- a/llvm/include/llvm/ExecutionEngine/Orc/Core.h +++ b/llvm/include/llvm/ExecutionEngine/Orc/Core.h @@ -1316,6 +1316,16 @@ class LLVM_ABI Platform { lookupInitSymbolsAsync(unique_function<void(Error)> OnComplete, ExecutionSession &ES, const DenseMap<JITDylib *, SymbolLookupSet> &InitSyms); + + /// Like lookupInitSymbols but does not trigger materialization: + /// symbols whose state is < SymbolState::Resolved are dropped from + /// the result. Intended for deinit paths -- a symbol that never + /// materialized never ran its initializer, so skipping its + /// deinitializer is correct. + LLVM_ABI static Expected<DenseMap<JITDylib *, SymbolMap>> + lookupResolvedInitSymbols( + ExecutionSession &ES, + const DenseMap<JITDylib *, SymbolLookupSet> &InitSyms); }; /// A materialization task. diff --git a/llvm/lib/ExecutionEngine/Orc/Core.cpp b/llvm/lib/ExecutionEngine/Orc/Core.cpp index ac780dc82ae5a..82aba560e3ca2 100644 --- a/llvm/lib/ExecutionEngine/Orc/Core.cpp +++ b/llvm/lib/ExecutionEngine/Orc/Core.cpp @@ -1512,6 +1512,29 @@ Expected<DenseMap<JITDylib *, SymbolMap>> Platform::lookupInitSymbols( return std::move(CompoundResult); } +Expected<DenseMap<JITDylib *, SymbolMap>> Platform::lookupResolvedInitSymbols( + ExecutionSession &ES, + const DenseMap<JITDylib *, SymbolLookupSet> &InitSyms) { + // Filter to already-resolved symbols, then defer to lookupInitSymbols + // -- the filtered lookup triggers no fresh materialization. + DenseMap<JITDylib *, SymbolLookupSet> Filtered; + ES.runSessionLocked([&]() { + for (const auto &KV : InitSyms) { + JITDylib *JD = KV.first; + SymbolLookupSet Keep; + for (const auto &[Name, Flags] : KV.second) { + auto SymI = JD->Symbols.find(Name); + if (SymI != JD->Symbols.end() && + SymI->second.getState() >= SymbolState::Resolved) + Keep.add(Name, Flags); + } + if (!Keep.empty()) + Filtered.try_emplace(JD, std::move(Keep)); + } + }); + return lookupInitSymbols(ES, Filtered); +} + void Platform::lookupInitSymbolsAsync( unique_function<void(Error)> OnComplete, ExecutionSession &ES, const DenseMap<JITDylib *, SymbolLookupSet> &InitSyms) { diff --git a/llvm/lib/ExecutionEngine/Orc/LLJIT.cpp b/llvm/lib/ExecutionEngine/Orc/LLJIT.cpp index 18db7d24a8262..edc07062d4525 100644 --- a/llvm/lib/ExecutionEngine/Orc/LLJIT.cpp +++ b/llvm/lib/ExecutionEngine/Orc/LLJIT.cpp @@ -372,7 +372,7 @@ class GenericLLVMIRPlatformSupport : public LLJIT::PlatformSupport { dbgs() << " \"" << KV.first->getName() << "\": " << KV.second << "\n"; }); - auto LookupResult = Platform::lookupInitSymbols(ES, LookupSymbols); + auto LookupResult = Platform::lookupResolvedInitSymbols(ES, LookupSymbols); if (!LookupResult) return LookupResult.takeError(); @@ -380,8 +380,10 @@ class GenericLLVMIRPlatformSupport : public LLJIT::PlatformSupport { std::vector<ExecutorAddr> DeInitializers; for (auto &NextJD : DFSLinkOrder) { auto DeInitsItr = LookupResult->find(NextJD.get()); - assert(DeInitsItr != LookupResult->end() && - "Every JD should have at least __lljit_run_atexits"); + // lookupResolvedInitSymbols may legitimately drop a JD whose + // deinit funcs (and __lljit_run_atexits) are all unmaterialized. + if (DeInitsItr == LookupResult->end()) + continue; auto RunAtExitsItr = DeInitsItr->second.find(LLJITRunAtExits); if (RunAtExitsItr != DeInitsItr->second.end()) diff --git a/llvm/unittests/ExecutionEngine/Orc/CoreAPIsTest.cpp b/llvm/unittests/ExecutionEngine/Orc/CoreAPIsTest.cpp index c8fcdfbb72759..b428973d11048 100644 --- a/llvm/unittests/ExecutionEngine/Orc/CoreAPIsTest.cpp +++ b/llvm/unittests/ExecutionEngine/Orc/CoreAPIsTest.cpp @@ -1967,4 +1967,55 @@ TEST(BootstrapJITDylibTest, EmptyBootstrapSymbols) { cantFail(ES.endSession()); } +TEST_F(CoreAPIsStandardTest, LookupResolvedInitSymbolsSkipsUnmaterialized) { + // Symbol whose materialize() must never run: represents an emitted- + // but-never-materialized deinit thunk. + std::atomic<unsigned> MaterializeCount{0}; + auto MU = std::make_unique<SimpleMaterializationUnit>( + SymbolFlagsMap({{Foo, JITSymbolFlags::Exported}}), + [this, + &MaterializeCount](std::unique_ptr<MaterializationResponsibility> R) { + ++MaterializeCount; + cantFail(R->notifyResolved( + {{Foo, ExecutorSymbolDef(ExecutorAddr(0xfeed), + JITSymbolFlags::Exported)}})); + cantFail(R->notifyEmitted({})); + }); + cantFail(JD.define(std::move(MU))); + + DenseMap<JITDylib *, SymbolLookupSet> InitSyms; + InitSyms[&JD].add(Foo, SymbolLookupFlags::WeaklyReferencedSymbol); + + auto ResolvedResult = Platform::lookupResolvedInitSymbols(ES, InitSyms); + ASSERT_THAT_EXPECTED(ResolvedResult, Succeeded()); + EXPECT_EQ(MaterializeCount.load(), 0u) + << "lookupResolvedInitSymbols must not materialize"; + auto It = ResolvedResult->find(&JD); + EXPECT_TRUE(It == ResolvedResult->end() || It->second.empty()); + + // Baseline: lookupInitSymbols DOES trigger materialization. + auto LegacyResult = Platform::lookupInitSymbols(ES, InitSyms); + ASSERT_THAT_EXPECTED(LegacyResult, Succeeded()); + EXPECT_EQ(MaterializeCount.load(), 1u); +} + +TEST_F(CoreAPIsStandardTest, LookupResolvedInitSymbolsReturnsResolved) { + cantFail(JD.define( + absoluteSymbols({{Foo, ExecutorSymbolDef(ExecutorAddr(0xbeef), + JITSymbolFlags::Exported)}}))); + + // Drive Foo to Ready (absoluteSymbols defines it but a real lookup is + // required to reach materialized state). + cantFail(ES.lookup({&JD}, Foo)); + + DenseMap<JITDylib *, SymbolLookupSet> InitSyms; + InitSyms[&JD].add(Foo); + + auto Result = Platform::lookupResolvedInitSymbols(ES, InitSyms); + ASSERT_THAT_EXPECTED(Result, Succeeded()); + ASSERT_EQ(Result->count(&JD), 1u); + ASSERT_EQ((*Result)[&JD].count(Foo), 1u); + EXPECT_EQ((*Result)[&JD][Foo].getAddress().getValue(), 0xbeefULL); +} + } // namespace _______________________________________________ cfe-commits mailing list [email protected] https://lists.llvm.org/cgi-bin/mailman/listinfo/cfe-commits
