Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 0 additions & 9 deletions include/pybind11/detail/common.h
Original file line number Diff line number Diff line change
Expand Up @@ -606,15 +606,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.
Expand Down
35 changes: 24 additions & 11 deletions include/pybind11/pybind11.h
Original file line number Diff line number Diff line change
Expand Up @@ -936,11 +936,8 @@ class cpp_function : public function {
std::free(const_cast<char *>(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<char *>(rec->def->ml_doc));
Expand Down Expand Up @@ -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<function_record_PyObject *>(self);
Expand Down Expand Up @@ -2687,19 +2678,41 @@ class class_ : public detail::generic_type {
if (rec_fget) {
char *doc_prev = rec_fget->doc; /* 'extra' field may include a property-specific
documentation string */
auto args_before = rec_fget->args.size();
detail::process_attributes<Extra...>::init(extra..., rec_fget);
if (rec_fget->doc && rec_fget->doc != doc_prev) {
std::free(doc_prev);
rec_fget->doc = PYBIND11_COMPAT_STRDUP(rec_fget->doc);
}
// Args added by process_attributes (e.g. "self" via is_method + pos_only/kw_only)
// need their strings strdup'd: initialize_generic's strdup loop already ran during
// cpp_function construction, so it won't process these late additions. Without this,
// destruct() would call free() on string literals. See gh-5976.
for (auto i = args_before; i < rec_fget->args.size(); ++i) {
if (rec_fget->args[i].name) {
rec_fget->args[i].name = PYBIND11_COMPAT_STRDUP(rec_fget->args[i].name);
}
if (rec_fget->args[i].descr) {
rec_fget->args[i].descr = PYBIND11_COMPAT_STRDUP(rec_fget->args[i].descr);
}
}
}
if (rec_fset) {
char *doc_prev = rec_fset->doc;
auto args_before = rec_fset->args.size();
detail::process_attributes<Extra...>::init(extra..., rec_fset);
if (rec_fset->doc && rec_fset->doc != doc_prev) {
std::free(doc_prev);
rec_fset->doc = PYBIND11_COMPAT_STRDUP(rec_fset->doc);
}
for (auto i = args_before; i < rec_fset->args.size(); ++i) {
if (rec_fset->args[i].name) {
rec_fset->args[i].name = PYBIND11_COMPAT_STRDUP(rec_fset->args[i].name);
}
if (rec_fset->args[i].descr) {
rec_fset->args[i].descr = PYBIND11_COMPAT_STRDUP(rec_fset->args[i].descr);
}
}
if (!rec_active) {
rec_active = rec_fset;
}
Expand Down
2 changes: 2 additions & 0 deletions tests/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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"
Expand Down
13 changes: 13 additions & 0 deletions tests/standalone_enum_module.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
// Copyright (c) 2026 The pybind Community.

#include <pybind11/pybind11.h>

namespace standalone_enum_module_ns {
enum SomeEnum {};
} // namespace standalone_enum_module_ns

using namespace standalone_enum_module_ns;

PYBIND11_MODULE(standalone_enum_module, m) { // Added in PR #6015
pybind11::enum_<SomeEnum> some_enum_wrapper(m, "SomeEnum");
}
18 changes: 18 additions & 0 deletions tests/test_standalone_enum_module.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
from __future__ import annotations

import os

import env


def test_enum_import_exit_no_crash():
# Added in PR #6015. 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,
)
5 changes: 5 additions & 0 deletions tests/test_with_catch/catch_skip.h
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,18 @@

#pragma once

#include <pybind11/detail/pybind11_namespace_macros.h>

#include <catch.hpp>

#define PYBIND11_CATCH2_SKIP_IF(condition, reason) \
do { \
PYBIND11_WARNING_PUSH \
PYBIND11_WARNING_DISABLE_MSVC(4127) \
if (condition) { \
Catch::cout() << "[ SKIPPED ] " << (reason) << '\n'; \
Catch::cout().flush(); \
return; \
} \
PYBIND11_WARNING_POP \
} while (0)
28 changes: 28 additions & 0 deletions tests/test_with_catch/test_interpreter.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
// catch 2.0.1; this should be fixed in the next catch release after 2.0.1).
PYBIND11_WARNING_DISABLE_MSVC(4996)

#include "catch_skip.h"

#include <catch.hpp>
#include <cstdlib>
#include <fstream>
Expand Down Expand Up @@ -84,6 +86,14 @@ PYBIND11_EMBEDDED_MODULE(trampoline_module, m) {
.def("func", &test_override_cache_helper::func);
}

enum class SomeEnum { value1, value2 }; // Added in PR #6015

PYBIND11_EMBEDDED_MODULE(enum_module, m, py::multiple_interpreters::per_interpreter_gil()) {
py::enum_<SomeEnum>(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, ) {
Expand Down Expand Up @@ -343,6 +353,24 @@ TEST_CASE("Restart the interpreter") {
REQUIRE(py_widget.attr("the_message").cast<std::string>() == "Hello after restart");
}

TEST_CASE("Enum module survives restart") { // Added in PR #6015
// 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.
PYBIND11_CATCH2_SKIP_IF(PY_MAJOR_VERSION == 3 && PY_MINOR_VERSION == 12,
"Pre-existing crash in enum cleanup during finalize on Python 3.12");

auto enum_mod = py::module_::import("enum_module");
REQUIRE(enum_mod.attr("SomeEnum").attr("value1").attr("name").cast<std::string>() == "value1");

py::finalize_interpreter();
py::initialize_interpreter();

enum_mod = py::module_::import("enum_module");
REQUIRE(enum_mod.attr("SomeEnum").attr("value2").attr("name").cast<std::string>() == "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__`.
Expand Down
Loading