Skip to content

Commit 0a45af2

Browse files
itamarorwgk
andauthored
gh-5991: Fix segfault during finalization related to function_record (#6010)
* gh-5991: Fix segfault during finalization related to function_record 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. * Add detail::py_is_finalizing() wrapper to deduplicate version-guarded #ifdef blocks Also fixes clang-tidy readability-implicit-bool-conversion warnings. Made-with: Cursor --------- Co-authored-by: Ralf W. Grosse-Kunstleve <rgrossekunst@nvidia.com>
1 parent 1c72409 commit 0a45af2

2 files changed

Lines changed: 26 additions & 2 deletions

File tree

include/pybind11/detail/common.h

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -601,6 +601,15 @@ enum class return_value_policy : uint8_t {
601601

602602
PYBIND11_NAMESPACE_BEGIN(detail)
603603

604+
// Py_IsFinalizing() is a public API since 3.13; before that use _Py_IsFinalizing().
605+
inline bool py_is_finalizing() {
606+
#if PY_VERSION_HEX >= 0x030D0000
607+
return Py_IsFinalizing() != 0;
608+
#else
609+
return _Py_IsFinalizing() != 0;
610+
#endif
611+
}
612+
604613
static constexpr int log2(size_t n, int k = 0) { return (n <= 1) ? k : log2(n >> 1, k + 1); }
605614

606615
// Returns the size as a multiple of sizeof(void *), rounded up.

include/pybind11/pybind11.h

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -846,8 +846,11 @@ class cpp_function : public function {
846846
std::free(const_cast<char *>(arg.descr));
847847
}
848848
}
849-
for (auto &arg : rec->args) {
850-
arg.value.dec_ref();
849+
// During finalization, default arg values may already be freed by GC.
850+
if (!detail::py_is_finalizing()) {
851+
for (auto &arg : rec->args) {
852+
arg.value.dec_ref();
853+
}
851854
}
852855
if (rec->def) {
853856
std::free(const_cast<char *>(rec->def->ml_doc));
@@ -1342,9 +1345,21 @@ PYBIND11_NAMESPACE_BEGIN(function_record_PyTypeObject_methods)
13421345

13431346
// This implementation needs the definition of `class cpp_function`.
13441347
inline void tp_dealloc_impl(PyObject *self) {
1348+
// Skip dealloc during finalization — GC may have already freed objects
1349+
// reachable from the function record (e.g. default arg values), causing
1350+
// use-after-free in destruct().
1351+
if (detail::py_is_finalizing()) {
1352+
return;
1353+
}
1354+
// Save type before PyObject_Free invalidates self.
1355+
auto *type = Py_TYPE(self);
13451356
auto *py_func_rec = reinterpret_cast<function_record_PyObject *>(self);
13461357
cpp_function::destruct(py_func_rec->cpp_func_rec);
13471358
py_func_rec->cpp_func_rec = nullptr;
1359+
// PyObject_New increments the heap type refcount and allocates via
1360+
// PyObject_Malloc; balance both here
1361+
PyObject_Free(self);
1362+
Py_DECREF(type);
13481363
}
13491364

13501365
PYBIND11_NAMESPACE_END(function_record_PyTypeObject_methods)

0 commit comments

Comments
 (0)