From 02bd5c82a16acb0b8677cb007ffbaa37f074f7e9 Mon Sep 17 00:00:00 2001 From: Abhijit1018 Date: Wed, 18 Mar 2026 13:32:29 +0530 Subject: [PATCH] Fix assertion failure in py_clearexc when Python function raises exception (fixes #469) --- include/pybind11/internal/function.h | 12 ++++++- include/pybind11/tests/function.cpp | 53 ++++++++++++++++++++++++++++ 2 files changed, 64 insertions(+), 1 deletion(-) diff --git a/include/pybind11/internal/function.h b/include/pybind11/internal/function.h index 2c84e756..9c677f92 100644 --- a/include/pybind11/internal/function.h +++ b/include/pybind11/internal/function.h @@ -70,6 +70,7 @@ args_proxy interface::operator* () const { template template object interface::operator() (Args&&... args) const { + py_StackRef pc = py_peek(0); // Save checkpoint BEFORE push py_push(ptr()); py_pushnil(); @@ -108,7 +109,16 @@ object interface::operator() (Args&&... args) const { (foreach(std::forward(args)), ...); - raise_call(argc, kwargsc); + // Don't use raise_call here - it saves checkpoint AFTER push, which causes + // an assertion failure in py_clearexc when py_vectorcall fails (because + // py_vectorcall pops 2+argc items on both success and failure). + // Instead, we save checkpoint BEFORE push and handle error explicitly. + if(!py_vectorcall(argc, kwargsc)) { + char* what = py_formatexc(); + object e = object::from_ret(); + py_clearexc(pc); // pc is now at or below current sp, assertion safe + throw python_error(what, std::move(e)); + } return object::from_ret(); } diff --git a/include/pybind11/tests/function.cpp b/include/pybind11/tests/function.cpp index 50da0204..25fbf382 100644 --- a/include/pybind11/tests/function.cpp +++ b/include/pybind11/tests/function.cpp @@ -410,4 +410,57 @@ TEST_F(PYBIND11_TEST, overload_cast) { EXPECT_EVAL_EQ("X().add(1, 2, 3)", 6); } +// Test for GitHub issue: calling Python function that raises exception +// from C++ via py::object::operator() should not crash with assertion failure +// in py_clearexc. The bug was that raise_call saved the stack checkpoint +// AFTER pushing callable + nil + args, but py_vectorcall pops 2+argc items +// on both success and failure, causing the checkpoint to be below the +// current stack pointer when py_clearexc is called. +TEST_F(PYBIND11_TEST, vectorcall_exception) { + auto m = py::module::__main__(); + + // Define a Python function that raises an exception + py::exec(R"( +def raise_error(msg): + raise ValueError(msg) +)"); + + // Get the function object + py::object raise_error = m.attr("raise_error"); + + // Call the function from C++ - this should catch the exception + // without crashing due to assertion failure in py_clearexc + try { + raise_error("test error"); + FAIL() << "Expected py::python_error to be thrown"; + } catch (py::python_error& e) { + // Expected: we should catch the exception + EXPECT_TRUE(e.match(tp_ValueError)); + } + + // Test with arguments + try { + py::str msg("error with arg"); + raise_error(msg); + FAIL() << "Expected py::python_error to be thrown"; + } catch (py::python_error& e) { + // Expected: we should catch the exception + EXPECT_TRUE(e.match(tp_ValueError)); + } + + // Test multiple arguments + py::exec(R"( +def raise_error2(a, b, c): + raise RuntimeError(f'{a}-{b}-{c}') +)"); + py::object raise_error2 = m.attr("raise_error2"); + + try { + raise_error2(1, 2, 3); + FAIL() << "Expected py::python_error to be thrown"; + } catch (py::python_error& e) { + EXPECT_TRUE(e.match(tp_RuntimeError)); + } +} + } // namespace