[Mlir-commits] [mlir] [mlir][python] Fix segfault at interpreter shutdown with entered contexts (PR #203826)
llvmlistbot at llvm.org
llvmlistbot at llvm.org
Sun Jun 14 23:04:58 PDT 2026
llvmorg-github-actions[bot] wrote:
<!--LLVM PR SUMMARY COMMENT-->
@llvm/pr-subscribers-mlir
Author: Maksim Levental (makslevental)
<details>
<summary>Changes</summary>
# TL;DR:
The `static thread_local std::vector<PyThreadContextEntry>` holds `nb::object` references to Python Context/Location/InsertionPoint objects. When a Context is entered (pushed onto the stack) but never exited before interpreter shutdown, the thread-local storage destructor runs after `Py_Finalize()` on the main thread, attempting `Py_DECREF` through the dead runtime → SIGSEGV.
Fix: Register an `atexit` handler that clears the stack while the interpreter is still alive, releasing all held Python references before finalization.
The checked in tests will SIGSEGV on a commit that doesn't have this fix (try it!).
# Full explanation
Per
> Destructors for initialized objects with thread storage duration within a given thread are called as a result of returning from the initial function of that thread and as a result of that thread calling `std::exit`.[^1]
For the main thread, the "initial function" is CPython's `main()` of course. So thread_local destructors fire after `main()` returns. But CPython calls `Py_FinalizeEx()` explicitly before returning:
```
// CPython's Modules/main.c (simplified)
int main() {
Py_Initialize();
run_script();
Py_FinalizeEx(); // ← tears down interpreter (calls Python atexit, GC, etc.)
return 0; // ← thread_local destructors fire AFTER this
}
```
So the sequence is:
1. `Py_FinalizeEx()` runs — calls Python `atexit` callbacks (interpreter alive)
2. `Py_FinalizeEx()` continues — GC collects, clears modules, tears down interpreter
3. `Py_FinalizeEx()` returns; `main()` returns
4. Thread_local destructors fire (per [basic.start.term]) ← **the `vector<PyThreadContextEntry>` dies here**
5. `nb::object` destructors call `Py_DECREF` → **interpreter is already dead → segfault**
The `atexit.register()` fix works because Python's `atexit` callbacks execute at step 1 — the very first thing inside `Py_FinalizeEx()` — while the interpreter is still fully functional.
Note: Python's `atexit` module is distinct from C's `atexit()`. Python's runs inside `Py_FinalizeEx()` (before main returns). C's runs after main returns (interleaved with static destructors).
Assisted by Claude
[^1]: https://timsong-cpp.github.io/cppwp/n3337/basic.start.term
---
Full diff: https://github.com/llvm/llvm-project/pull/203826.diff
2 Files Affected:
- (modified) mlir/lib/Bindings/Python/IRCore.cpp (+9)
- (added) mlir/test/python/context_shutdown.py (+26)
``````````diff
diff --git a/mlir/lib/Bindings/Python/IRCore.cpp b/mlir/lib/Bindings/Python/IRCore.cpp
index 92e9ecf3f2c20..fc69ef8884466 100644
--- a/mlir/lib/Bindings/Python/IRCore.cpp
+++ b/mlir/lib/Bindings/Python/IRCore.cpp
@@ -5292,6 +5292,15 @@ void populateIRCore(nb::module_ &m) {
// MLIRError exception.
MLIRError::bind(m);
+
+ // Register an atexit handler to clear the thread-local context stack.
+ // The stack holds nb::object references that prevent Python GC of Contexts.
+ // At interpreter shutdown, thread_local storage outlives Py_Finalize() on
+ // the main thread. When the thread_local vector destructs, its nb::object
+ // members call Py_DECREF through the dead runtime, causing a segfault.
+ // Clearing the stack in atexit releases references while alive.
+ nb::module_::import_("atexit").attr("register")(
+ nb::cpp_function([]() { PyThreadContextEntry::getStack().clear(); }));
}
} // namespace MLIR_BINDINGS_PYTHON_DOMAIN
} // namespace python
diff --git a/mlir/test/python/context_shutdown.py b/mlir/test/python/context_shutdown.py
new file mode 100644
index 0000000000000..cb7b12cd5a4d6
--- /dev/null
+++ b/mlir/test/python/context_shutdown.py
@@ -0,0 +1,26 @@
+# RUN: %PYTHON %s
+# Regression test: entering a Context (or Location/InsertionPoint) without
+# exiting before interpreter shutdown used to segfault. The thread-local
+# context stack holds nb::object references; if not cleared before
+# Py_Finalize(), the thread_local destructor calls Py_DECREF through the
+# dead runtime.
+
+from mlir.ir import *
+
+# Case 1: Single context entered, not exited.
+ctx = Context()
+ctx.__enter__()
+ctx.enable_multithreading(False)
+with Location.unknown():
+ m = Module.parse("func.func @f() { return }")
+
+# Case 2: Multiple contexts entered, not exited.
+ctx2 = Context()
+ctx2.__enter__()
+
+# Case 3: Location entered, not exited (also uses the same thread-local stack).
+loc = Location.unknown()
+loc.__enter__()
+
+# Interpreter shutdown proceeds with contexts/locations still on the
+# thread-local stack. Before the fix, this would segfault (exit code 139).
``````````
</details>
https://github.com/llvm/llvm-project/pull/203826
More information about the Mlir-commits
mailing list