Skip to content

Commit 688e743

Browse files
henryiiijturneyrwgk
authored
fix: never run keep_alive for a failed overload or failed return conversion (#6183)
* fix: do not run keep_alive for an overload that failed argument conversion Calling an overloaded function bound with `py::keep_alive<0, N>` segfaults whenever the call is not matched by the first overload tried. .def("make", &Holder::from_int, py::keep_alive<0, 1>()) .def("make", &Holder::from_string, py::keep_alive<0, 1>()); h.make(1) # fine h.make("x") # SIGSEGV The dispatch lambda in `cpp_function::initialize` invokes the post-call hook unconditionally: auto result = call_impl<...>(call, ...); process_attributes<Extra...>::postcall(call, result); but `call_impl` returns `PYBIND11_TRY_NEXT_OVERLOAD` when `load_args` fails, and that sentinel is `((PyObject *) 1)` rather than an object. For a `keep_alive` whose nurse or patient is index 0 the work happens in postcall, so `keep_alive_impl` receives the sentinel as `ret`, hands it to `get_arg(0)` and dereferences it in `_Py_TYPE`. The existing guards do not catch it: the sentinel is neither null nor `Py_None`, so it passes straight through the checks added in #341. Guard inside `keep_alive_impl`, next to those checks. Only the `Nurse == 0 || Patient == 0` specialization does its work in postcall, and `keep_alive` is the only call policy with a non-trivial postcall, so this covers every path that can observe the sentinel. `keep_alive<1, 2>` and friends run in precall against fully populated `call.args` and are unaffected. The regression test crashes the interpreter without the fix. Reaching the second overload is what matters: it is the first overload's failed conversion that produces the sentinel. * fix: never run keep_alive for a failed overload or failed return conversion Move keep_alive activation entirely to postcall, so an argument-to-argument keep_alive no longer fires in precall for an overload that later fails argument conversion. Extend the postcall guard to also skip a null return handle, so a failed return-value conversion raises its own error instead of "Could not activate keep_alive!". Document that postcall can receive the PYBIND11_TRY_NEXT_OVERLOAD sentinel or a null handle. Tests cover the sentinel as nurse and as patient, the argument-to-argument case, and the failed return conversion, and no longer make the test object its own patient. Assisted-by: ClaudeCode:claude-fable-5 Claude-Session: https://claude.ai/code/session_01WRPhwYhrJKvFXU2NWe1p3W * fix: run argument-to-argument keep_alive before the call again Moving keep_alive<N, P> (N, P != 0) to postcall meant a keep_alive error was raised after the function ran, so its side effects stayed. Run the precall hook in call_impl after load_args succeeds instead, so a failed overload still does not trigger it and an error still stops the call. Also release the return value when a postcall keep_alive throws, as the dispatcher drops it. Assisted-by: ClaudeCode:claude-opus-5-5 * test: collect the child before capturing output on PyPy Assisted-by: ClaudeCode:claude-opus-5-5 * fix: skip postcall for an overload that failed argument conversion The dispatcher now runs postcall only for the overload that matched, like precall. Add a test for a rejected sole candidate, and clarify the postcall and keep_alive_impl comments. Assisted-by: ClaudeCode:claude-opus-5-5 * fix: complete call policy conversion and result cleanup Run precall after cast_op and parameter conversion without adding copies or moves, and keep it before guard construction. Protect the owned return throughout postcall so any throwing hook releases it. Extend regression coverage for reference rejection, converting overloads, constructors, return retention, custom hooks and return conversion errors. * refactor: clarify call guard conversion boundaries Remove unused argument-loader invocation overloads, standardize precall naming, and explain the function_ref parameter boundary and copy elision. Document guard ordering and test caster rejection, guarded calls, by-value parameter destruction, and extraction before GIL release. * test: make call guard caster extraction const * test: explain moved-from lifetime logging [skip ci] --------- Co-authored-by: Justin Turney <justin.turney@gmail.com> Co-authored-by: Ralf W. Grosse-Kunstleve <rgrossekunst@nvidia.com>
1 parent 9de3643 commit 688e743

8 files changed

Lines changed: 525 additions & 24 deletions

File tree

‎docs/advanced/functions.rst‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -233,6 +233,12 @@ is equivalent to the following pseudocode:
233233
return foo(args...); // forwarded arguments
234234
});
235235
236+
Argument conversion, including extraction from type casters, finishes before
237+
the guards are constructed. A candidate rejected during argument conversion
238+
does not construct its guards. Call-policy ``precall`` hooks also run before
239+
guard construction, and the guards are destroyed before Python return-value
240+
conversion.
241+
236242
The only requirement is that ``T`` is default-constructible, but otherwise any
237243
scope guard will work. This is very useful in combination with ``gil_scoped_release``.
238244
See :ref:`gil`.

‎include/pybind11/attr.h‎

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -409,7 +409,9 @@ struct process_attribute_default {
409409
/// Default implementation: do nothing
410410
static void init(const T &, function_record *) {}
411411
static void init(const T &, type_record *) {}
412+
/// Runs after argument conversion succeeded, before the call.
412413
static void precall(function_call &) {}
414+
/// Runs after the call succeeded. The handle is null if return-value conversion failed.
413415
static void postcall(function_call &, handle) {}
414416
};
415417

@@ -650,8 +652,8 @@ struct process_attribute<call_guard<Ts...>> : process_attribute_default<call_gua
650652

651653
/**
652654
* Process a keep_alive call policy -- invokes keep_alive_impl during the
653-
* pre-call handler if both Nurse, Patient != 0 and use the post-call handler
654-
* otherwise
655+
* pre-call handler (after argument conversion succeeded) if both Nurse,
656+
* Patient != 0 and use the post-call handler otherwise
655657
*/
656658
template <size_t Nurse, size_t Patient>
657659
struct process_attribute<keep_alive<Nurse, Patient>>

‎include/pybind11/cast.h‎

Lines changed: 14 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2162,17 +2162,17 @@ class argument_loader {
21622162

21632163
bool load_args(function_call &call) { return load_impl_sequence(call, indices{}); }
21642164

2165-
template <typename Return, typename Guard, typename Func>
2165+
template <typename Return, typename Guard, typename Func, typename Precall>
21662166
// NOLINTNEXTLINE(readability-const-return-type)
2167-
enable_if_t<!std::is_void<Return>::value, Return> call(Func &&f) && {
2168-
return std::move(*this).template call_impl<remove_cv_t<Return>>(
2169-
std::forward<Func>(f), indices{}, Guard{});
2167+
enable_if_t<!std::is_void<Return>::value, Return> call(Func &&f, Precall &&precall) && {
2168+
return std::move(*this).template call_impl<remove_cv_t<Return>, Guard>(
2169+
std::forward<Func>(f), std::forward<Precall>(precall), indices{});
21702170
}
21712171

2172-
template <typename Return, typename Guard, typename Func>
2173-
enable_if_t<std::is_void<Return>::value, void_type> call(Func &&f) && {
2174-
std::move(*this).template call_impl<remove_cv_t<Return>>(
2175-
std::forward<Func>(f), indices{}, Guard{});
2172+
template <typename Return, typename Guard, typename Func, typename Precall>
2173+
enable_if_t<std::is_void<Return>::value, void_type> call(Func &&f, Precall &&precall) && {
2174+
std::move(*this).template call_impl<remove_cv_t<Return>, Guard>(
2175+
std::forward<Func>(f), std::forward<Precall>(precall), indices{});
21762176
return void_type();
21772177
}
21782178

@@ -2201,9 +2201,12 @@ class argument_loader {
22012201
return true;
22022202
}
22032203

2204-
template <typename Return, typename Func, size_t... Is, typename Guard>
2205-
Return call_impl(Func &&f, index_sequence<Is...>, Guard &&) && {
2206-
return std::forward<Func>(f)(cast_op<Args>(std::move(std::get<Is>(argcasters)))...);
2204+
template <typename Return, typename Guard, typename Func, typename Precall, size_t... Is>
2205+
Return call_impl(Func &&f, Precall &&precall, index_sequence<Is...>) && {
2206+
// Func provides function_ref::invoke_with_guard, whose typed parameters finish conversion
2207+
// before precall. Direct casts and returns preserve copy elision (see issue #6142).
2208+
return std::forward<Func>(f).template invoke_with_guard<Guard>(
2209+
std::forward<Precall>(precall), cast_op<Args>(std::move(std::get<Is>(argcasters)))...);
22072210
}
22082211

22092212
std::tuple<make_caster<Args>...> argcasters;

‎include/pybind11/detail/function_ref.h‎

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,17 @@ class function_ref<Ret(Params...)> {
104104
return callback(callable, std::forward<Params>(params)...);
105105
}
106106

107+
template <typename Guard, typename Precall>
108+
// NOLINTNEXTLINE(performance-unnecessary-value-param)
109+
Ret invoke_with_guard(Precall &&precall, Params... params) const {
110+
// Keep the same parameter boundary as operator() so prvalue arguments can be elided.
111+
// Argument conversion must finish before the hook, which runs before the guard.
112+
std::forward<Precall>(precall)();
113+
Guard guard{};
114+
(void) guard;
115+
return callback(callable, std::forward<Params>(params)...);
116+
}
117+
107118
explicit operator bool() const { return callback; }
108119

109120
bool operator==(const function_ref<Ret(Params...)> &Other) const {

‎include/pybind11/pybind11-inl.h‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -309,6 +309,14 @@ PYBIND11_NOINLINE_ATTR PYBIND11_INLINE void keep_alive_impl(handle nurse, handle
309309

310310
PYBIND11_NOINLINE_ATTR PYBIND11_INLINE void
311311
keep_alive_impl(size_t Nurse, size_t Patient, function_call &call, handle ret) {
312+
// With index 0, this runs in postcall, where a null `ret` means the return-value conversion
313+
// failed with the real error already set. Report that error, not "Could not activate
314+
// keep_alive!". Without index 0, this runs in precall, where `ret` is always null.
315+
const bool uses_ret = Nurse == 0 || Patient == 0;
316+
if (uses_ret && !ret) {
317+
return;
318+
}
319+
312320
auto get_arg = [&](size_t n) {
313321
if (n == 0) {
314322
return ret;

‎include/pybind11/pybind11.h‎

Lines changed: 21 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -319,7 +319,9 @@ class cpp_function : public function {
319319
// actual function lambda so that we can get code reuse for
320320
// functions with the same Return, Args, and Guard.
321321
template <typename Return, typename Guard, typename ArgsConverter, typename... Args>
322-
static handle call_impl(detail::function_call &call, detail::function_ref<Return(Args...)> f) {
322+
static handle call_impl(detail::function_call &call,
323+
detail::function_ref<Return(Args...)> f,
324+
void (*precall)(detail::function_call &)) {
323325
using namespace detail;
324326
// Static assertion: function_ref must be trivially copyable to ensure safe pass-by-value.
325327
// Lifetime safety: The function_ref is created from cap->f which lives in the capture
@@ -335,18 +337,24 @@ class cpp_function : public function {
335337
return PYBIND11_TRY_NEXT_OVERLOAD;
336338
}
337339

340+
// cast_op can reject an argument after load_args succeeds. Run the hook after those
341+
// conversions, but before constructing the guard, which may release the GIL.
342+
auto precall_hook = [&] { precall(call); };
343+
338344
/* Override policy for rvalues -- usually to enforce rvp::move on an rvalue */
339345
return_value_policy policy
340346
= return_value_policy_override<Return>::policy(call.func.policy);
341347

342348
/* Perform the function call */
343349
handle result;
344350
if (call.func.is_setter) {
345-
(void) std::move(args_converter).template call<Return, Guard>(f);
351+
(void) std::move(args_converter).template call<Return, Guard>(f, precall_hook);
346352
result = none().release();
347353
} else {
348354
result = cast_out::cast(
349-
std::move(args_converter).template call<Return, Guard>(f), policy, call.parent);
355+
std::move(args_converter).template call<Return, Guard>(f, precall_hook),
356+
policy,
357+
call.parent);
350358
}
351359

352360
return result;
@@ -415,9 +423,6 @@ class cpp_function : public function {
415423

416424
/* Dispatch code which converts function arguments and performs the actual function call */
417425
rec->impl = [](function_call &call) -> handle {
418-
/* Invoke call policy pre-call hook */
419-
process_attributes<Extra...>::precall(call);
420-
421426
/* Get a pointer to the capture object */
422427
const auto *data = (sizeof(capture) <= sizeof(call.func.data) ? &call.func.data
423428
: call.func.data[0]);
@@ -427,12 +432,18 @@ class cpp_function : public function {
427432
/* Function scope guard -- defaults to the compile-to-nothing
428433
`void_type` */
429434
extract_guard_t<Extra...>,
430-
cast_in>(call, detail::function_ref<Return(Args...)>(cap->f));
435+
cast_in>(call,
436+
detail::function_ref<Return(Args...)>(cap->f),
437+
&process_attributes<Extra...>::precall);
431438

432-
/* Invoke call policy post-call hook */
433-
process_attributes<Extra...>::postcall(call, result);
439+
if (result.ptr() == PYBIND11_TRY_NEXT_OVERLOAD) {
440+
return result;
441+
}
434442

435-
return result;
443+
// Own the result while running postcall so any throwing hook releases it.
444+
auto result_guard = reinterpret_steal<object>(result);
445+
process_attributes<Extra...>::postcall(call, result_guard);
446+
return result_guard.release();
436447
};
437448

438449
rec->nargs_pos = cast_in::args_pos >= 0

0 commit comments

Comments
 (0)