From ba8f2ecc500e4609cb26d656a0e826953d1cbeea Mon Sep 17 00:00:00 2001 From: Jiucheng Zang Date: Tue, 7 Jul 2026 16:29:30 -0400 Subject: [PATCH] Add tests and fix crash on free-threaded debug builds when destroying subinterpreters --- .../test_free_threading/test_interpreters.py | 28 +++++++++++++++++++ ...-07-07-00-00-00.gh-issue-153176.Kx7pQ2.rst | 4 +++ Objects/obmalloc.c | 4 ++- 3 files changed, 35 insertions(+), 1 deletion(-) create mode 100644 Lib/test/test_free_threading/test_interpreters.py create mode 100644 Misc/NEWS.d/next/Core_and_Builtins/2026-07-07-00-00-00.gh-issue-153176.Kx7pQ2.rst diff --git a/Lib/test/test_free_threading/test_interpreters.py b/Lib/test/test_free_threading/test_interpreters.py new file mode 100644 index 00000000000000..cb3e6722aa9807 --- /dev/null +++ b/Lib/test/test_free_threading/test_interpreters.py @@ -0,0 +1,28 @@ +import textwrap +import unittest + +from test.support import import_helper, script_helper + + +# Make sure _testinternalcapi is available before running the test. +import_helper.import_module('_testinternalcapi') + + +class InterpreterTeardownTests(unittest.TestCase): + def test_destroy_subinterpreter_does_not_abort(self): + # gh-153176: On a free-threaded debug build, destroying a + # subinterpreter used to trip an overly-restrictive assertion in + # _PyMem_mi_page_reclaimed and abort the process. Run the + # reproduction in a subprocess so that a regression surfaces as a + # non-zero exit / SIGABRT instead of killing the test runner. + script = textwrap.dedent(""" + import _testinternalcapi + + interpid = _testinternalcapi.create_interpreter() + _testinternalcapi.destroy_interpreter(interpid, basic=True) + """) + script_helper.assert_python_ok('-c', script) + + +if __name__ == "__main__": + unittest.main() diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2026-07-07-00-00-00.gh-issue-153176.Kx7pQ2.rst b/Misc/NEWS.d/next/Core_and_Builtins/2026-07-07-00-00-00.gh-issue-153176.Kx7pQ2.rst new file mode 100644 index 00000000000000..315f0ebc7e27e8 --- /dev/null +++ b/Misc/NEWS.d/next/Core_and_Builtins/2026-07-07-00-00-00.gh-issue-153176.Kx7pQ2.rst @@ -0,0 +1,4 @@ +Fix a crash on free-threaded debug builds when destroying a subinterpreter. +An overly-restrictive assertion in ``_PyMem_mi_page_reclaimed`` wrongly +assumed the reclaimed page's owning thread state always matched the current +thread state, which is not true during stop-the-world events. diff --git a/Objects/obmalloc.c b/Objects/obmalloc.c index 0947d47c8a5558..340503eed3a0b5 100644 --- a/Objects/obmalloc.c +++ b/Objects/obmalloc.c @@ -211,8 +211,10 @@ _PyMem_mi_page_reclaimed(mi_page_t *page) if (page->qsbr_goal != 0) { if (mi_page_all_free(page)) { assert(page->qsbr_node.next == NULL); + // We may be reclaiming a page belonging to a different thread + // during a stop-the-world event. Find the _PyThreadStateImpl for + // the page. _PyThreadStateImpl *tstate = tstate_from_heap(mi_page_heap(page)); - assert(tstate == (_PyThreadStateImpl *)_PyThreadState_GET()); page->retire_expire = 0; llist_insert_tail(&tstate->mimalloc.page_list, &page->qsbr_node); }