Skip to content

Commit e7761e3

Browse files
oremanjwjakob
authored andcommitted
Fix potential crash when calling an overloaded function (#1327)
* Fix potential crash when calling an overloaded function The crash would occur if: - dispatcher() uses two-pass logic (because the target is overloaded and some arguments support conversions) - the first pass (with conversions disabled) doesn't find any matching overload - the second pass does find a matching overload, but its return value can't be converted to Python The code for formatting the error message assumed `it` still pointed to the selected overload, but during the second-pass loop `it` was nullptr. Fix by setting `it` correctly if a second-pass call returns a nullptr `handle`. Add a new test that segfaults without this fix. * Make overload iteration const-correct so we don't have to iterate again on second-pass error * Change test_error_after_conversions dependencies to local classes/variables
1 parent 9343e68 commit e7761e3

File tree

5 files changed

+33
-8
lines changed

5 files changed

+33
-8
lines changed

include/pybind11/attr.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -278,7 +278,7 @@ struct type_record {
278278
}
279279
};
280280

281-
inline function_call::function_call(function_record &f, handle p) :
281+
inline function_call::function_call(const function_record &f, handle p) :
282282
func(f), parent(p) {
283283
args.reserve(f.nargs);
284284
args_convert.reserve(f.nargs);

include/pybind11/cast.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1843,7 +1843,7 @@ struct function_record;
18431843

18441844
/// Internal data associated with a single function call
18451845
struct function_call {
1846-
function_call(function_record &f, handle p); // Implementation in attr.h
1846+
function_call(const function_record &f, handle p); // Implementation in attr.h
18471847

18481848
/// The function data:
18491849
const function_record &func;

include/pybind11/pybind11.h

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -420,8 +420,8 @@ class cpp_function : public function {
420420
using namespace detail;
421421

422422
/* Iterator over the list of potentially admissible overloads */
423-
function_record *overloads = (function_record *) PyCapsule_GetPointer(self, nullptr),
424-
*it = overloads;
423+
const function_record *overloads = (function_record *) PyCapsule_GetPointer(self, nullptr),
424+
*it = overloads;
425425

426426
/* Need to know how many arguments + keyword arguments there are to pick the right overload */
427427
const size_t n_args_in = (size_t) PyTuple_GET_SIZE(args_in);
@@ -477,7 +477,7 @@ class cpp_function : public function {
477477
result other than PYBIND11_TRY_NEXT_OVERLOAD.
478478
*/
479479

480-
function_record &func = *it;
480+
const function_record &func = *it;
481481
size_t pos_args = func.nargs; // Number of positional arguments that we need
482482
if (func.has_args) --pos_args; // (but don't count py::args
483483
if (func.has_kwargs) --pos_args; // or py::kwargs)
@@ -509,7 +509,7 @@ class cpp_function : public function {
509509
// 1. Copy any position arguments given.
510510
bool bad_arg = false;
511511
for (; args_copied < args_to_copy; ++args_copied) {
512-
argument_record *arg_rec = args_copied < func.args.size() ? &func.args[args_copied] : nullptr;
512+
const argument_record *arg_rec = args_copied < func.args.size() ? &func.args[args_copied] : nullptr;
513513
if (kwargs_in && arg_rec && arg_rec->name && PyDict_GetItemString(kwargs_in, arg_rec->name)) {
514514
bad_arg = true;
515515
break;
@@ -650,8 +650,13 @@ class cpp_function : public function {
650650
result = PYBIND11_TRY_NEXT_OVERLOAD;
651651
}
652652

653-
if (result.ptr() != PYBIND11_TRY_NEXT_OVERLOAD)
653+
if (result.ptr() != PYBIND11_TRY_NEXT_OVERLOAD) {
654+
// The error reporting logic below expects 'it' to be valid, as it would be
655+
// if we'd encountered this failure in the first-pass loop.
656+
if (!result)
657+
it = &call.func;
654658
break;
659+
}
655660
}
656661
}
657662
} catch (error_already_set &e) {
@@ -703,7 +708,7 @@ class cpp_function : public function {
703708
" arguments. The following argument types are supported:\n";
704709

705710
int ctr = 0;
706-
for (function_record *it2 = overloads; it2 != nullptr; it2 = it2->next) {
711+
for (const function_record *it2 = overloads; it2 != nullptr; it2 = it2->next) {
707712
msg += " "+ std::to_string(++ctr) + ". ";
708713

709714
bool wrote_sig = false;

tests/test_class.cpp

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -341,6 +341,19 @@ TEST_SUBMODULE(class_, m) {
341341
"a"_a, "b"_a, "c"_a);
342342
base.def("g", [](NestBase &, Nested &) {});
343343
base.def("h", []() { return NestBase(); });
344+
345+
// test_error_after_conversion
346+
// The second-pass path through dispatcher() previously didn't
347+
// remember which overload was used, and would crash trying to
348+
// generate a useful error message
349+
350+
struct NotRegistered {};
351+
struct StringWrapper { std::string str; };
352+
m.def("test_error_after_conversions", [](int) {});
353+
m.def("test_error_after_conversions",
354+
[](StringWrapper) -> NotRegistered { return {}; });
355+
py::class_<StringWrapper>(m, "StringWrapper").def(py::init<std::string>());
356+
py::implicitly_convertible<std::string, StringWrapper>();
344357
}
345358

346359
template <int N> class BreaksBase { public: virtual ~BreaksBase() = default; };

tests/test_class.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -266,3 +266,10 @@ def test_reentrant_implicit_conversion_failure(msg):
266266
267267
Invoked with: 0
268268
'''
269+
270+
271+
def test_error_after_conversions():
272+
with pytest.raises(TypeError) as exc_info:
273+
m.test_error_after_conversions("hello")
274+
assert str(exc_info.value).startswith(
275+
"Unable to convert function return value to a Python type!")

0 commit comments

Comments
 (0)