Repository navigation
Conversation
…<T>> arguments optional_caster::load() used a local inner caster, so for T with a by-value caster (Eigen matrices, STL containers, ...) the loaded reference_wrapper pointed into a destroyed caster. Keep the inner caster as a member so it lives as long as the optional caster. Fixes pybind#5637
There was a problem hiding this comment.
codex GPT-6.1-Sol ultra:
Found two regressions at stl.h:565:
-
[P2] Preserve lazy construction for
None. The member constructs the inner caster before checking forNone. Forstd::optional<py::array_t<double>>, this imports NumPy and allocates an array. Without NumPy installed,f(None)now raisesModuleNotFoundError; the baseline accepts it. Reproducer: repro.cpp below. -
[P2] Preserve support for nonmovable custom casters. If the inner caster contains nonmovable state, such as
std::mutex, the optional caster now becomes nonmovable too. Previously validobj.cast<std::optional<Item>>()fails compiling atload_type()’sreturn conv, even whenItemis movable. Confirmed against baseline and PR headers. Reproducer: nonmovable_full.cpp below.
Lazy initialization through movable ownership could address both while preserving the lifetime fix.
… movable Hold the inner caster of optional_caster through std::unique_ptr, created only for non-None values. This keeps the pybind#5637 lifetime fix while restoring lazy construction for None (e.g. std::optional<py::array_t<T>> no longer needs NumPy for None) and movability of the optional caster when the inner caster is not movable (needed by cast<std::optional<T>>()).
|
Thanks @rwgk, both regressions reproduced with the attached files ( Fixed in f116885: the inner caster is now held by |
|
Here's a Fable 5.1 review too: 🤖 AI text below 🤖
Items 1, 2 and 4 are the ones that affect the design of the fix. |
Description
Fixes #5637.
Passing a NumPy array to a function that takes
std::optional<std::reference_wrapper<Eigen::MatrixXd>>segfaults. The same happens for anyTthat is converted by value (Eigen types, STL containers, strings, custom casters), not only Eigen.optional_caster::load()loads into a localvalue_conv inner_casterand then storescast_op<Value &&>(std::move(inner_caster))in the optional. For areference_wrapper<T>that result points at theTowned by the inner caster, which is destroyed whenload()returns. The bound function then reads a freed object.With master, the reproducer from the issue under AddressSanitizer:
The fix makes the inner caster a member of
optional_caster, so it lives as long as the outer caster, i.e. for the whole call, the same waytype_caster<std::reference_wrapper<T>>keeps itssubcaster. Behaviour for otherstd::optional<T>arguments does not change.Testing:
test_optional_reference_wrapper_to_converted_valueintest_stl. It uses a small by-value caster whose type records which instances are alive, so the check is deterministic and does not rely on reading freed memory. It fails on master ('dangling' == 7) and passes with the fix.test_stl.py(C++17): 41 passed, 2 skipped.-fsanitize=address: heap-use-after-free on master, clean with the fix.variant_caster::load_alternativeuses the same local-caster pattern, sostd::variant<std::reference_wrapper<T>, ...>probably has the same issue. I left it out to keep this PR small.Suggested changelog entry:
std::optional<std::reference_wrapper<T>>argument refers to a type converted by value (e.g. Eigen matrices).