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

Reply via email to