Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[BUG]: return_value_policy_override in optional_caster does not use the right types #3330

Closed
3 tasks done
ryancahoon-zoox opened this issue Oct 5, 2021 · 8 comments · Fixed by #3376
Closed
3 tasks done
Labels

Comments

@ryancahoon-zoox
Copy link
Contributor

Required prerequisites

Problem description

return_value_policy_override is not being applied correctly in optional_caster in two ways.

  • The is_lvalue_reference condition references T, which is the optional<T> type parameter from the class, when it should use T_, which is the parameter to the cast function. T_ can potentially be a reference type, but T will never be.
  • The type parameter passed to return_value_policy_override should be T::value_type, not T. This matches the way that the other STL container type casters work.

The result of these issues is that a method/property definition which uses a reference or reference_internal return value policy will create a Python value that's bound by reference to a temporary C++ object, resulting in undefined behavior. For reasons that I was not able to figure out, it seems like this causes problems when using boost::optional, but not with the libstdc++ implementation of std::optional. The issue (that the override to return_value_policy::move is never being applied) is present for all implementations, it just seems like that somehow doesn't result in problems for the libstdc++ implementation.

I believe the following patch is the correct resolution. I would open a PR directly, except I'm not sure how to write a regression test for this case.

diff --git a/include/pybind11/stl.h b/include/pybind11/stl.h
index 2c017b4f..5c0592f8 100644
--- a/include/pybind11/stl.h
+++ b/include/pybind11/stl.h
@@ -252,8 +252,8 @@ template<typename T> struct optional_caster {
     static handle cast(T_ &&src, return_value_policy policy, handle parent) {
         if (!src)
             return none().inc_ref();
-        if (!std::is_lvalue_reference<T>::value) {
-            policy = return_value_policy_override<T>::policy(policy);
+        if (!std::is_lvalue_reference<T_>::value) {
+            policy = return_value_policy_override<typename T::value_type>::policy(policy);
         }
         return value_conv::cast(*std::forward<T_>(src), policy, parent);
     }

Reproducible example code

#include <boost/optional.hpp>
#include <pybind11/pybind11.h>
#include <pybind11/stl.h>

enum class E {
  k0 = 0,
  k1 = 1,
};

struct A {
  boost::optional<E> value = E::k1;
};

namespace pybind11 {
namespace detail {

template <typename T>
struct type_caster<boost::optional<T>> : optional_caster<boost::optional<T>> {};

} // namespace detail
} // namespace pybind11

PYBIND11_MODULE(test_lib, m) {
  pybind11::enum_<E>(m, "E")
      .value("k0", E::k0)
      .value("k1", E::k1);

  pybind11::class_<A>(m, "A")
      .def(pybind11::init<>())
      .def_property_readonly("by_ref",
                             [](A& a) -> boost::optional<E>& { return a.value; })
      .def_property_readonly("by_copy",
                             [](A& a) -> boost::optional<E> { return a.value; });
}




import test_lib


print(int(test_lib.A().by_ref))
print(int(test_lib.A().by_copy))
@ryancahoon-zoox ryancahoon-zoox added the triage New bug, unverified label Oct 5, 2021
@Skylion007 Skylion007 added bug and removed triage New bug, unverified labels Oct 5, 2021
@Skylion007
Copy link
Collaborator

Skylion007 commented Oct 6, 2021

@ryancahoon-zoox For future reference, feel free to just open a PR, we can work on adding regressions tests there if necessary.

@rwgk
Copy link
Collaborator

rwgk commented Oct 6, 2021

Hi @ryancahoon-zoox, is there a chance that you could help by moving your code to a PR?

We already have the ability to conditionally test with boost:

#elif defined(PYBIND11_TEST_BOOST) && (!defined(_MSC_VER) || _MSC_VER >= 1910)

Following that pattern would be a practical way to leverage your existing reproducer.

Please mark the PR as modifiable by maintainers (I think it's the default, a green checkbox when you create the PR). That way we can jump in fixing issues surfaced by the CI.

@henryiii
Copy link
Collaborator

henryiii commented Oct 8, 2021

@ryancahoon-zoox are you planning to open a PR with your boost example as a test?

@ryancahoon-zoox
Copy link
Contributor Author

I've been trying to work on it as I have time, but I'm having trouble reproducing my original issue in the test environment. There's a number of factors that I need to check still, like whether there are some versions of boost that aren't affected by this issue.

@henryiii
Copy link
Collaborator

We have had no problem in reproducing it in #3332, AFAICT, the problem has been compiling the new test on Windows; MSVC doesn't like it at all.

@Skylion007
Copy link
Collaborator

@henryiii I've haven't been able to replicate the segfault behavior in the PR, but I was just going ahead and added the boost::optional tests since they are useful regardless.

@ryancahoon-zoox
Copy link
Contributor Author

I've reproduced the issue after switching to Boost 1.54 (which is what is used in the software environment where I originally observed the bug), whereas the issue does not seem to be reproduced with Boost 1.71. I've not bothered to check intermediate versions, so I'm not sure exactly when boost::optional might have changed.

For the purposes of testing, I'm going to write a simple optional-like type that reproduces the necessary parts of the old boost::optional behavior.

@rwgk
Copy link
Collaborator

rwgk commented Oct 17, 2021 via email

ryancahoon-zoox added a commit to ryancahoon-zoox/pybind11 that referenced this issue Oct 17, 2021
`return_value_policy_override` was not being applied correctly in
`optional_caster` in two ways:
- The `is_lvalue_reference` condition referenced `T`, which was the
`optional<T>` type parameter from the class, when it should have used `T_`,
which was the parameter to the `cast` function. `T_` can potentially be a
reference type, but `T` will never be.
- The type parameter passed to `return_value_policy_override` should be
`T::value_type`, not `T`. This matches the way that the other STL container
type casters work.

The result of these issues was that a method/property definition which used a
`reference` or `reference_internal` return value policy would create a Python
value that's bound by reference to a temporary C++ object, resulting in
undefined behavior. For reasons that I was not able to figure out fully, it
seems like this causes problems when using old versions of `boost::optional`,
but not with recent versions of `boost::optional` or the `libstdc++`
implementation of `std::optional`. The issue (that the override to
`return_value_policy::move` is never being applied) is present for all
implementations, it just seems like that somehow doesn't result in problems for
the some implementation of `optional`. This change includes a regression type
with a custom optional-like type which was able to reproduce the issue.

Part of the issue with using the wrong types may have stemmed from the type
variables `T` and `T_` having very similar names. This also changes the type
variables in `optional_caster` to use slightly more descriptive names, which
also more closely follow the naming convention used by the other STL casters.

Fixes pybind#3330
rwgk pushed a commit that referenced this issue Oct 26, 2021
…3376)

* fix: the types for return_value_policy_override in optional_caster

`return_value_policy_override` was not being applied correctly in
`optional_caster` in two ways:
- The `is_lvalue_reference` condition referenced `T`, which was the
`optional<T>` type parameter from the class, when it should have used `T_`,
which was the parameter to the `cast` function. `T_` can potentially be a
reference type, but `T` will never be.
- The type parameter passed to `return_value_policy_override` should be
`T::value_type`, not `T`. This matches the way that the other STL container
type casters work.

The result of these issues was that a method/property definition which used a
`reference` or `reference_internal` return value policy would create a Python
value that's bound by reference to a temporary C++ object, resulting in
undefined behavior. For reasons that I was not able to figure out fully, it
seems like this causes problems when using old versions of `boost::optional`,
but not with recent versions of `boost::optional` or the `libstdc++`
implementation of `std::optional`. The issue (that the override to
`return_value_policy::move` is never being applied) is present for all
implementations, it just seems like that somehow doesn't result in problems for
the some implementation of `optional`. This change includes a regression type
with a custom optional-like type which was able to reproduce the issue.

Part of the issue with using the wrong types may have stemmed from the type
variables `T` and `T_` having very similar names. This also changes the type
variables in `optional_caster` to use slightly more descriptive names, which
also more closely follow the naming convention used by the other STL casters.

Fixes #3330

* Fix clang-tidy complaints

* Add missing NOLINT

* Apply a couple more fixes

* fix: support GCC 4.8

* tests: avoid warning about unknown compiler for compilers missing C++17

* Remove unneeded test module attribute

* Change test enum to have more unique int values

Co-authored-by: Aaron Gokaslan <skylion.aaron@gmail.com>
Co-authored-by: Henry Schreiner <HenrySchreinerIII@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging a pull request may close this issue.

4 participants