From 528c0c2713e39c2091ecf73ca9a3ec15ee83fc41 Mon Sep 17 00:00:00 2001 From: "Ralf W. Grosse-Kunstleve" Date: Fri, 27 Mar 2026 11:07:25 -0700 Subject: [PATCH 1/6] test: add embedding test for py::enum_ across interpreter restart (gh-5976) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit py::enum_ is the primary trigger for gh-5976 because its constructor creates properties via def_property_static / def_property_readonly_static, which call process_attributes::init on already-initialized function records. Yet none of the existing embedding tests used py::enum_ at all. Add an PYBIND11_EMBEDDED_MODULE with py::enum_ and a test case that imports it, finalize/reinitializes the interpreter, and re-imports it. This exercises the def_property_static code path that was fixed in the preceding commit. Note: on Python 3.14.2 (and likely 3.12+), tp_dealloc_impl is not called during Py_FinalizeEx for function record PyObjects — they simply leak because types are effectively immortalized. As a result, this test cannot trigger the original free()-on-string-literal crash on this Python version. However, it remains valuable as a regression guard: on Python builds where finalization does clean up function records (or if CPython changes this behavior), the test would catch the crash. It also verifies that py::enum_ survives interpreter restart correctly, which was previously untested. Made-with: Cursor --- tests/test_with_catch/test_interpreter.cpp | 23 ++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/tests/test_with_catch/test_interpreter.cpp b/tests/test_with_catch/test_interpreter.cpp index d227eecddc..35682445fa 100644 --- a/tests/test_with_catch/test_interpreter.cpp +++ b/tests/test_with_catch/test_interpreter.cpp @@ -84,6 +84,14 @@ PYBIND11_EMBEDDED_MODULE(trampoline_module, m) { .def("func", &test_override_cache_helper::func); } +enum class SomeEnum { value1, value2 }; + +PYBIND11_EMBEDDED_MODULE(enum_module, m, py::multiple_interpreters::per_interpreter_gil()) { + py::enum_(m, "SomeEnum") + .value("value1", SomeEnum::value1) + .value("value2", SomeEnum::value2); +} + PYBIND11_EMBEDDED_MODULE(throw_exception, ) { throw std::runtime_error("C++ Error"); } PYBIND11_EMBEDDED_MODULE(throw_error_already_set, ) { @@ -343,6 +351,21 @@ TEST_CASE("Restart the interpreter") { REQUIRE(py_widget.attr("the_message").cast() == "Hello after restart"); } +TEST_CASE("Enum module survives restart") { + // Regression test for gh-5976: py::enum_ uses def_property_static, which + // calls process_attributes::init after initialize_generic's strdup loop, + // leaving arg names as string literals. Without the fix, destruct() would + // call free() on those literals during interpreter finalization. + auto enum_mod = py::module_::import("enum_module"); + REQUIRE(enum_mod.attr("SomeEnum").attr("value1").attr("name").cast() == "value1"); + + py::finalize_interpreter(); + py::initialize_interpreter(); + + enum_mod = py::module_::import("enum_module"); + REQUIRE(enum_mod.attr("SomeEnum").attr("value2").attr("name").cast() == "value2"); +} + TEST_CASE("Execution frame") { // When the interpreter is embedded, there is no execution frame, but `py::exec` // should still function by using reasonable globals: `__main__.__dict__`. From 29a103cca39feface28ad006a5c6a715f9ef07b7 Mon Sep 17 00:00:00 2001 From: "Ralf W. Grosse-Kunstleve" Date: Fri, 27 Mar 2026 10:38:48 -0700 Subject: [PATCH 2/6] Partially revert gh-6010: remove py_is_finalizing() workarounds Now that the root cause (free of string literals in def_property_static, gh-5976) is fixed in the previous commit, the py_is_finalizing() guards introduced in gh-6010 are no longer needed: - tp_dealloc_impl: remove early return during finalization (was leaking all function records instead of properly destroying them) - destruct(): remove guard around arg.value.dec_ref() - common.h: remove py_is_finalizing() helper (no remaining callers) The genuine fix from gh-6010 (PyObject_Free + Py_DECREF ordering in tp_dealloc_impl) is retained. Made-with: Cursor --- include/pybind11/detail/common.h | 9 --------- include/pybind11/pybind11.h | 13 ++----------- 2 files changed, 2 insertions(+), 20 deletions(-) diff --git a/include/pybind11/detail/common.h b/include/pybind11/detail/common.h index ebffe579a3..d57f590e2f 100644 --- a/include/pybind11/detail/common.h +++ b/include/pybind11/detail/common.h @@ -601,15 +601,6 @@ enum class return_value_policy : uint8_t { PYBIND11_NAMESPACE_BEGIN(detail) -// Py_IsFinalizing() is a public API since 3.13; before that use _Py_IsFinalizing(). -inline bool py_is_finalizing() { -#if PY_VERSION_HEX >= 0x030D0000 - return Py_IsFinalizing() != 0; -#else - return _Py_IsFinalizing() != 0; -#endif -} - static constexpr int log2(size_t n, int k = 0) { return (n <= 1) ? k : log2(n >> 1, k + 1); } // Returns the size as a multiple of sizeof(void *), rounded up. diff --git a/include/pybind11/pybind11.h b/include/pybind11/pybind11.h index 6c269adbe1..06ca41db0a 100644 --- a/include/pybind11/pybind11.h +++ b/include/pybind11/pybind11.h @@ -936,11 +936,8 @@ class cpp_function : public function { std::free(const_cast(arg.descr)); } } - // During finalization, default arg values may already be freed by GC. - if (!detail::py_is_finalizing()) { - for (auto &arg : rec->args) { - arg.value.dec_ref(); - } + for (auto &arg : rec->args) { + arg.value.dec_ref(); } if (rec->def) { std::free(const_cast(rec->def->ml_doc)); @@ -1435,12 +1432,6 @@ PYBIND11_NAMESPACE_BEGIN(function_record_PyTypeObject_methods) // This implementation needs the definition of `class cpp_function`. inline void tp_dealloc_impl(PyObject *self) { - // Skip dealloc during finalization — GC may have already freed objects - // reachable from the function record (e.g. default arg values), causing - // use-after-free in destruct(). - if (detail::py_is_finalizing()) { - return; - } // Save type before PyObject_Free invalidates self. auto *type = Py_TYPE(self); auto *py_func_rec = reinterpret_cast(self); From 3b2d426bcc2bf43908f12079bd8aabff555970f5 Mon Sep 17 00:00:00 2001 From: "Ralf W. Grosse-Kunstleve" Date: Fri, 27 Mar 2026 14:03:52 -0700 Subject: [PATCH 3/6] test: skip enum restart test on Python 3.12 (pre-existing crash) Made-with: Cursor --- tests/test_with_catch/test_interpreter.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tests/test_with_catch/test_interpreter.cpp b/tests/test_with_catch/test_interpreter.cpp index 35682445fa..32068cbc0f 100644 --- a/tests/test_with_catch/test_interpreter.cpp +++ b/tests/test_with_catch/test_interpreter.cpp @@ -356,6 +356,9 @@ TEST_CASE("Enum module survives restart") { // calls process_attributes::init after initialize_generic's strdup loop, // leaving arg names as string literals. Without the fix, destruct() would // call free() on those literals during interpreter finalization. +#if PY_VERSION_HEX >= 0x030C0000 && PY_VERSION_HEX < 0x030D0000 + WARN("Skipping: pre-existing crash in enum cleanup during finalize on Python 3.12"); +#else auto enum_mod = py::module_::import("enum_module"); REQUIRE(enum_mod.attr("SomeEnum").attr("value1").attr("name").cast() == "value1"); @@ -364,6 +367,7 @@ TEST_CASE("Enum module survives restart") { enum_mod = py::module_::import("enum_module"); REQUIRE(enum_mod.attr("SomeEnum").attr("value2").attr("name").cast() == "value2"); +#endif } TEST_CASE("Execution frame") { From 1c11abc5f2478a46f627f256944a347b36db1d32 Mon Sep 17 00:00:00 2001 From: "Ralf W. Grosse-Kunstleve" Date: Fri, 27 Mar 2026 15:21:56 -0700 Subject: [PATCH 4/6] Add test_standalone_enum_module.py, standalone_enum_module.cpp --- tests/CMakeLists.txt | 2 ++ tests/standalone_enum_module.cpp | 15 +++++++++++++++ tests/test_standalone_enum_module.py | 19 +++++++++++++++++++ 3 files changed, 36 insertions(+) create mode 100644 tests/standalone_enum_module.cpp create mode 100644 tests/test_standalone_enum_module.py diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 9a35052daa..e87b1e93b3 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -172,6 +172,7 @@ set(PYBIND11_TEST_FILES test_scoped_critical_section test_sequences_and_iterators test_smart_ptr + test_standalone_enum_module.py test_stl test_stl_binders test_tagbased_polymorphic @@ -249,6 +250,7 @@ tests_extra_targets("test_exceptions.py" "cross_module_interleaved_error_already tests_extra_targets("test_gil_scoped.py" "cross_module_gil_utils") tests_extra_targets("test_cpp_conduit.py" "exo_planet_pybind11;exo_planet_c_api;home_planet_very_lonely_traveler") +tests_extra_targets("test_standalone_enum_module.py" "standalone_enum_module") set(PYBIND11_EIGEN_REPO "https://gitlab.com/libeigen/eigen.git" diff --git a/tests/standalone_enum_module.cpp b/tests/standalone_enum_module.cpp new file mode 100644 index 0000000000..370f1a5d05 --- /dev/null +++ b/tests/standalone_enum_module.cpp @@ -0,0 +1,15 @@ +// Copyright (c) 2026 The pybind Community. + +#include + +namespace standalone_enum_module_ns { +enum class SomeEnum { value1, value2 }; +} + +using namespace standalone_enum_module_ns; + +PYBIND11_MODULE(standalone_enum_module, m) { + pybind11::enum_(m, "SomeEnum") + .value("value1", SomeEnum::value1) + .value("value2", SomeEnum::value2); +} diff --git a/tests/test_standalone_enum_module.py b/tests/test_standalone_enum_module.py new file mode 100644 index 0000000000..ba2c347d44 --- /dev/null +++ b/tests/test_standalone_enum_module.py @@ -0,0 +1,19 @@ +from __future__ import annotations + +import os + +import env + + +def test_enum_import_exit_no_crash(): + # Modeled after reproducer under issue #5976 + env.check_script_success_in_subprocess( + f""" + import sys + sys.path.insert(0, {os.path.dirname(env.__file__)!r}) + import standalone_enum_module as m + assert int(m.SomeEnum.value1) == 0 + assert int(m.SomeEnum.value2) == 1 + """, + rerun=1, + ) From 58115d5382409cc324a88e26400c262bfc6b1a9a Mon Sep 17 00:00:00 2001 From: "Ralf W. Grosse-Kunstleve" Date: Fri, 27 Mar 2026 17:16:13 -0700 Subject: [PATCH 5/6] Make standalone_enum_module.cpp more similar to #5976 reproducer. Also fix clang-tidy error. --- tests/standalone_enum_module.cpp | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/tests/standalone_enum_module.cpp b/tests/standalone_enum_module.cpp index 370f1a5d05..7d913ab5d5 100644 --- a/tests/standalone_enum_module.cpp +++ b/tests/standalone_enum_module.cpp @@ -4,12 +4,11 @@ namespace standalone_enum_module_ns { enum class SomeEnum { value1, value2 }; -} +} // namespace standalone_enum_module_ns using namespace standalone_enum_module_ns; PYBIND11_MODULE(standalone_enum_module, m) { - pybind11::enum_(m, "SomeEnum") - .value("value1", SomeEnum::value1) - .value("value2", SomeEnum::value2); + pybind11::enum_ some_enum_type(m, "SomeEnum"); + some_enum_type.value("value1", SomeEnum::value1).value("value2", SomeEnum::value2); } From a3be775664b9975ca8ab3392fcc7fecc1dcf87fc Mon Sep 17 00:00:00 2001 From: "Ralf W. Grosse-Kunstleve" Date: Fri, 27 Mar 2026 18:06:33 -0700 Subject: [PATCH 6/6] This crashes when testing locally: ( cd /wrk/forked/pybind11/tests && PYTHONPATH=/wrk/bld/pybind11_gcc_v3.14.2_df793163d58_default/lib /wrk/bld/pybind11_gcc_v3.14.2_df793163d58_default/TestVenv/bin/python3 -m pytest test_standalone_enum_module.py ) ============================= test session starts ============================== platform linux -- Python 3.14.2, pytest-9.0.2, pluggy-1.6.0 installed packages of interest: build==1.4.2 numpy==2.4.3 scipy==1.17.1 C++ Info: 13.3.0 C++20 __pybind11_internals_v12_system_libstdcpp_gxx_abi_1xxx_use_cxx11_abi_1__ PYBIND11_SIMPLE_GIL_MANAGEMENT=False rootdir: /wrk/forked/pybind11/tests configfile: pytest.ini plugins: timeout-2.4.0, xdist-3.8.0 collected 1 item test_standalone_enum_module.py F [100%] =================================== FAILURES =================================== ________________________ test_enum_import_exit_no_crash ________________________ def test_enum_import_exit_no_crash(): # Modeled after reproducer under issue #5976 > env.check_script_success_in_subprocess( f""" import sys sys.path.insert(0, {os.path.dirname(env.__file__)!r}) import standalone_enum_module as m assert m.SomeEnum.__class__.__name__ == "pybind11_type" """, rerun=1, ) test_standalone_enum_module.py:10: _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ code = 'import sys\nsys.path.insert(0, \'/wrk/forked/pybind11/tests\')\nimport standalone_enum_module as m\nassert m.SomeEnum.__class__.__name__ == "pybind11_type"' def check_script_success_in_subprocess(code: str, *, rerun: int = 8) -> None: """Runs the given code in a subprocess.""" import os import subprocess import sys import textwrap if ANDROID or IOS or sys.platform.startswith("emscripten"): pytest.skip("Requires subprocess support") code = textwrap.dedent(code).strip() try: for _ in range(rerun): # run flakily failing test multiple times subprocess.check_output( [sys.executable, "-c", code], cwd=os.getcwd(), stderr=subprocess.STDOUT, text=True, ) except subprocess.CalledProcessError as ex: > raise RuntimeError( f"Subprocess failed with exit code {ex.returncode}.\n\n" f"Code:\n" f"```python\n" f"{code}\n" f"```\n\n" f"Output:\n" f"{ex.output}" ) from None E RuntimeError: Subprocess failed with exit code -6. E E Code: E ```python E import sys E sys.path.insert(0, '/wrk/forked/pybind11/tests') E import standalone_enum_module as m E assert m.SomeEnum.__class__.__name__ == "pybind11_type" E ``` E E Output: E munmap_chunk(): invalid pointer _ = 0 code = 'import sys\nsys.path.insert(0, \'/wrk/forked/pybind11/tests\')\nimport standalone_enum_module as m\nassert m.SomeEnum.__class__.__name__ == "pybind11_type"' os = rerun = 1 subprocess = sys = textwrap = env.py:68: RuntimeError =========================== short test summary info ============================ FAILED test_standalone_enum_module.py::test_enum_import_exit_no_crash - Runti... ============================== 1 failed in 0.23s =============================== ERROR: completed_process.returncode=1 --- tests/standalone_enum_module.cpp | 5 ++--- tests/test_standalone_enum_module.py | 3 +-- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/tests/standalone_enum_module.cpp b/tests/standalone_enum_module.cpp index 7d913ab5d5..2a5ee72938 100644 --- a/tests/standalone_enum_module.cpp +++ b/tests/standalone_enum_module.cpp @@ -3,12 +3,11 @@ #include namespace standalone_enum_module_ns { -enum class SomeEnum { value1, value2 }; +enum SomeEnum {}; } // namespace standalone_enum_module_ns using namespace standalone_enum_module_ns; PYBIND11_MODULE(standalone_enum_module, m) { - pybind11::enum_ some_enum_type(m, "SomeEnum"); - some_enum_type.value("value1", SomeEnum::value1).value("value2", SomeEnum::value2); + pybind11::enum_ some_enum_wrapper(m, "SomeEnum"); } diff --git a/tests/test_standalone_enum_module.py b/tests/test_standalone_enum_module.py index ba2c347d44..9dd35ff86d 100644 --- a/tests/test_standalone_enum_module.py +++ b/tests/test_standalone_enum_module.py @@ -12,8 +12,7 @@ def test_enum_import_exit_no_crash(): import sys sys.path.insert(0, {os.path.dirname(env.__file__)!r}) import standalone_enum_module as m - assert int(m.SomeEnum.value1) == 0 - assert int(m.SomeEnum.value2) == 1 + assert m.SomeEnum.__class__.__name__ == "pybind11_type" """, rerun=1, )