This is an automated email from the ASF dual-hosted git repository.

tqchen pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/tvm-ffi.git


The following commit(s) were added to refs/heads/main by this push:
     new 059f8af  fix(cython): Segfault in free-threaded Py_DecRef Cleanup 
(#529)
059f8af is described below

commit 059f8af3436b4b1fc629ba6d2c87f6be72513aeb
Author: Junru Shao <[email protected]>
AuthorDate: Fri Apr 10 06:03:30 2026 -0700

    fix(cython): Segfault in free-threaded Py_DecRef Cleanup (#529)
    
    Free-threaded CPython still requires an attached PyThreadState before
    using the Python C API. tvm-ffi skipped that setup under
    Py_GIL_DISABLED, so native cleanup paths could call Py_DecRef without
    thread state and crash.
    
    Always attach thread state around native decref paths, and add a
    free-threaded regression test that drops the last ref from a
    detached-thread-state region. The test is skipped on non-free- threaded
    builds and runs in the existing 3.14t CI job.
---
 python/tvm_ffi/cython/function.pxi                | 20 +++++++++++
 python/tvm_ffi/cython/tvm_ffi_python_helpers.h    | 41 +++++++++++------------
 tests/python/test_free_threaded_python_helpers.py | 32 ++++++++++++++++++
 3 files changed, 71 insertions(+), 22 deletions(-)

diff --git a/python/tvm_ffi/cython/function.pxi 
b/python/tvm_ffi/cython/function.pxi
index 913c698..aadd4ee 100644
--- a/python/tvm_ffi/cython/function.pxi
+++ b/python/tvm_ffi/cython/function.pxi
@@ -1180,6 +1180,26 @@ def _convert_to_opaque_object(object pyobject: Any) -> 
OpaquePyObject:
     return ret
 
 
+cdef extern from *:
+    """
+    static void TVMFFITestingCallDeleterWithoutThreadState(void* py_obj) {
+      PyThreadState* thread_state = PyEval_SaveThread();
+      TVMFFIPyObjectDeleter(py_obj);
+      PyEval_RestoreThread(thread_state);
+    }
+    """
+    void TVMFFITestingCallDeleterWithoutThreadState(void* py_obj)
+
+
+def _testing_drop_last_ref_without_thread_state() -> None:
+    """Drop the last Python ref from a detached-thread-state region."""
+    cdef object pyobject = {}
+    cdef PyObject* py_obj = <PyObject*>pyobject
+    Py_INCREF(pyobject)
+    pyobject = None
+    TVMFFITestingCallDeleterWithoutThreadState(<void*>py_obj)
+
+
 def _print_debug_info() -> None:
     """Get the size of the dispatch map"""
     cdef size_t size = TVMFFIPyGetDispatchMapSize()
diff --git a/python/tvm_ffi/cython/tvm_ffi_python_helpers.h 
b/python/tvm_ffi/cython/tvm_ffi_python_helpers.h
index 2015215..9c923af 100644
--- a/python/tvm_ffi/cython/tvm_ffi_python_helpers.h
+++ b/python/tvm_ffi/cython/tvm_ffi_python_helpers.h
@@ -47,6 +47,22 @@
 //  prefixed with TVMFFIPy so they can be easily invoked through Cython.
 
///--------------------------------------------------------------------------------
 
+//------------------------------------------------------------------------------------
+// Helpers for Python thread-state attachment
+//------------------------------------------------------------------------------------
+//
+// On classic builds, PyGILState_Ensure attaches the current thread and 
acquires the GIL.
+// On free-threaded builds, there is no process-wide GIL to acquire, but 
CPython still
+// requires an attached thread state before manipulating Python refcounts.
+class TVMFFIPyWithAttachedThreadState {
+ public:
+  TVMFFIPyWithAttachedThreadState() noexcept { gstate_ = PyGILState_Ensure(); }
+  ~TVMFFIPyWithAttachedThreadState() { PyGILState_Release(gstate_); }
+
+ private:
+  PyGILState_STATE gstate_;
+};
+
 /*!
  * \brief Thread-local call stack used by TVMFFIPyCallContext.
  */
@@ -124,6 +140,7 @@ class TVMFFIPyCallContext {
   }
 
   ~TVMFFIPyCallContext() {
+    TVMFFIPyWithAttachedThreadState thread_state;
     try {
       // recycle the temporary arguments if any
       for (int i = 0; i < this->num_temp_ffi_objects; ++i) {
@@ -664,6 +681,7 @@ class TVMFFIPyMLIRPackedSafeCall {
   }
 
   ~TVMFFIPyMLIRPackedSafeCall() {
+    TVMFFIPyWithAttachedThreadState thread_state;
     if (keep_alive_object_) {
       Py_DecRef(keep_alive_object_);
     }
@@ -717,33 +735,12 @@ void TVMFFIPyMLIRPackedSafeCallDeleter(void* self) {
   return TVMFFIPyMLIRPackedSafeCall::Deleter(self);
 }
 
-//------------------------------------------------------------------------------------
-// Helpers for free-threaded python
-//------------------------------------------------------------------------------------
-#if defined(Py_GIL_DISABLED)
-// NOGIL case
-class TVMFFIPyWithGILIfNotFreeThreaded {
- public:
-  TVMFFIPyWithGILIfNotFreeThreaded() = default;
-};
-#else
-// GIL case, need to ensure/release the GIL
-class TVMFFIPyWithGILIfNotFreeThreaded {
- public:
-  TVMFFIPyWithGILIfNotFreeThreaded() noexcept { gstate_ = PyGILState_Ensure(); 
}
-  ~TVMFFIPyWithGILIfNotFreeThreaded() { PyGILState_Release(gstate_); }
-
- private:
-  PyGILState_STATE gstate_;
-};
-#endif
-
 /*!
  * \brief Deleter for Python objects
  * \param py_obj The Python object to delete
  */
 extern "C" void TVMFFIPyObjectDeleter(void* py_obj) noexcept {
-  TVMFFIPyWithGILIfNotFreeThreaded gil_state;
+  TVMFFIPyWithAttachedThreadState thread_state;
   Py_DecRef(static_cast<PyObject*>(py_obj));
 }
 
diff --git a/tests/python/test_free_threaded_python_helpers.py 
b/tests/python/test_free_threaded_python_helpers.py
new file mode 100644
index 0000000..c6483b3
--- /dev/null
+++ b/tests/python/test_free_threaded_python_helpers.py
@@ -0,0 +1,32 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+from __future__ import annotations
+
+import sys
+
+import pytest
+import tvm_ffi
+
+
+def _is_free_threaded_python() -> bool:
+    return hasattr(sys, "_is_gil_enabled") and not sys._is_gil_enabled()
+
+
[email protected](not _is_free_threaded_python(), reason="requires 
free-threaded Python")
+def test_pyobject_deleter_handles_last_ref() -> None:
+    drop_last_ref = getattr(tvm_ffi.core, 
"_testing_drop_last_ref_without_thread_state")
+    drop_last_ref()

Reply via email to