From 9679b358b51d9c8cb6f37d61f9d35bd0e1b68fad Mon Sep 17 00:00:00 2001 From: Itamar Oren Date: Sun, 22 Mar 2026 15:33:03 -0700 Subject: [PATCH 1/2] gh-5991: Fix segfault during finalization related to function_record MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This patch was developed with assistance from Claude Code Opus 4.6 Here's Claude's explanation of the crash mechanism and some reasoning for the difficulty to repro: `tp_dealloc_impl` calls `cpp_function::destruct` which: 1. Calls `std::free()` on function_record string members (`name`, `doc`, `signature`) 2. Calls `arg.value.dec_ref()` on default argument values 3. Calls `delete rec` on the function_record But it never calls `PyObject_Free(self)` or `Py_DECREF(Py_TYPE(self))`, which are required for heap types. During `_Py_Finalize`, final GC collects the heap types (which survive module dict clearing via `tp_mro` self-references). This triggers a massive cascade: `type_dealloc → property_dealloc → meth_dealloc → tp_dealloc_impl → destruct`. At scale (~1,200+ function_records), the volume of `delete`/`free` calls corrupts heap metadata, causing subsequent `std::free()` to receive garbage pointers → SEGV. --- include/pybind11/pybind11.h | 31 +++++++++++++++++++++++++++++-- 1 file changed, 29 insertions(+), 2 deletions(-) diff --git a/include/pybind11/pybind11.h b/include/pybind11/pybind11.h index 76d998a3ba..19b930bf5c 100644 --- a/include/pybind11/pybind11.h +++ b/include/pybind11/pybind11.h @@ -846,8 +846,17 @@ class cpp_function : public function { std::free(const_cast(arg.descr)); } } - for (auto &arg : rec->args) { - arg.value.dec_ref(); + // During finalization, default arg values may already be freed by GC. + // Py_IsFinalizing() is public API since 3.13; before that use the + // private _Py_IsFinalizing(). +#if PY_VERSION_HEX >= 0x030D0000 + if (!Py_IsFinalizing()) { +#else + if (!_Py_IsFinalizing()) { +#endif + for (auto &arg : rec->args) { + arg.value.dec_ref(); + } } if (rec->def) { std::free(const_cast(rec->def->ml_doc)); @@ -1342,9 +1351,27 @@ 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(). + // Py_IsFinalizing() is public API since 3.13; before that use the + // private _Py_IsFinalizing(). +#if PY_VERSION_HEX >= 0x030D0000 + if (Py_IsFinalizing()) { +#else + if (_Py_IsFinalizing()) { +#endif + return; + } + // Save type before PyObject_Free invalidates self. + auto *type = Py_TYPE(self); auto *py_func_rec = reinterpret_cast(self); cpp_function::destruct(py_func_rec->cpp_func_rec); py_func_rec->cpp_func_rec = nullptr; + // PyObject_New increments the heap type refcount and allocates via + // PyObject_Malloc; balance both here + PyObject_Free(self); + Py_DECREF(type); } PYBIND11_NAMESPACE_END(function_record_PyTypeObject_methods) From 36d1a3d09d39cd1df07f8940d09435aae7f7f54d Mon Sep 17 00:00:00 2001 From: "Ralf W. Grosse-Kunstleve" Date: Sun, 22 Mar 2026 16:39:54 -0700 Subject: [PATCH 2/2] Add detail::py_is_finalizing() wrapper to deduplicate version-guarded #ifdef blocks Also fixes clang-tidy readability-implicit-bool-conversion warnings. Made-with: Cursor --- include/pybind11/detail/common.h | 9 +++++++++ include/pybind11/pybind11.h | 16 ++-------------- 2 files changed, 11 insertions(+), 14 deletions(-) diff --git a/include/pybind11/detail/common.h b/include/pybind11/detail/common.h index d1ab6deeb4..5576ad1300 100644 --- a/include/pybind11/detail/common.h +++ b/include/pybind11/detail/common.h @@ -601,6 +601,15 @@ 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 19b930bf5c..454638e299 100644 --- a/include/pybind11/pybind11.h +++ b/include/pybind11/pybind11.h @@ -847,13 +847,7 @@ class cpp_function : public function { } } // During finalization, default arg values may already be freed by GC. - // Py_IsFinalizing() is public API since 3.13; before that use the - // private _Py_IsFinalizing(). -#if PY_VERSION_HEX >= 0x030D0000 - if (!Py_IsFinalizing()) { -#else - if (!_Py_IsFinalizing()) { -#endif + if (!detail::py_is_finalizing()) { for (auto &arg : rec->args) { arg.value.dec_ref(); } @@ -1354,13 +1348,7 @@ 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(). - // Py_IsFinalizing() is public API since 3.13; before that use the - // private _Py_IsFinalizing(). -#if PY_VERSION_HEX >= 0x030D0000 - if (Py_IsFinalizing()) { -#else - if (_Py_IsFinalizing()) { -#endif + if (detail::py_is_finalizing()) { return; } // Save type before PyObject_Free invalidates self.