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

fix: the types for return_value_policy_override in optional_caster #3376

Merged
merged 9 commits into from
Oct 26, 2021

Conversation

ryancahoon-zoox
Copy link
Contributor

@ryancahoon-zoox ryancahoon-zoox commented Oct 17, 2021

Description

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. Closes #3376.

Suggested changelog entry:

Fixed the potential for dangling references when using properties with std::optional types

`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
@ryancahoon-zoox ryancahoon-zoox changed the title fix: Use the right types for return_value_policy_override in optional_caster fix: the types for return_value_policy_override in optional_caster Oct 17, 2021
@Skylion007 Skylion007 requested review from henryiii and rwgk October 25, 2021 20:28
Copy link
Collaborator

@rwgk rwgk left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good to me. Thanks for the thorough testing!

@@ -245,17 +245,17 @@ template <typename Key, typename Value, typename Hash, typename Equal, typename
: map_caster<std::unordered_map<Key, Value, Hash, Equal, Alloc>, Key, Value> { };

// This type caster is intended to be used for std::optional and std::experimental::optional
template<typename T> struct optional_caster {
using value_conv = make_caster<typename T::value_type>;
template<typename Type, typename Value = typename Type::value_type> struct optional_caster {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not super important, but this would read much nicer with ValueType (and wouldn't even need extra line breaks).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was using the same naming convention for template parameters that's used for list_caster and array_caster. I agree ValueType is a better name, though. Would you prefer that I change it or keep it consistent?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fine as is. Thanks for the explanation!

OptionalProperties() : value(EnumType::k1) {}
~OptionalProperties() {
// Reset value to detect use-after-destruction.
// This is set to a specific value rather than NULL to ensure that
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

rather than NULL
std::nullopt (to be precise)?

The specific value chosen happens to be 0 in memory. Is that sufficiently reliable for detecting use-after-destruction? (Vs. some random non-zero value.)

Copy link
Contributor Author

@ryancahoon-zoox ryancahoon-zoox Oct 26, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done. I used the generic nullopt instead of std::nullopt because the OptionalImpl type may not be std::optional.

#endif

// test_refsensitive_optional
m.attr("has_refsensitive_optional") = true;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this actually needed/used? (copy-paste oversight)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not needed

@henryiii henryiii force-pushed the optional-return_policy_override branch from c50c854 to 2412ad0 Compare October 25, 2021 21:45
Copy link
Collaborator

@henryiii henryiii left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the only test failure is a standard flake. I'm happy once @rwgk's comments are resolved.

@rwgk
Copy link
Collaborator

rwgk commented Oct 26, 2021

The one CI failure is:
Run jwlawson/actions-setup-cmake@v1.11
Error: getaddrinfo ENOTFOUND api.github.com

I've not seen that one before, but it's safe to ignore I think. I'll go ahead merge this now.

@rwgk rwgk merged commit c2d3e22 into pybind:master Oct 26, 2021
@github-actions github-actions bot added the needs changelog Possibly needs a changelog entry label Oct 26, 2021
@rwgk
Copy link
Collaborator

rwgk commented Oct 27, 2021

Hi @ryancahoon-zoox, when testing this PR with the smart_holder branch and PYBIND11_USE_SMART_HOLDER_AS_DEFAULT I'm seeing a test failure (below). I already spent a couple hours trying to understand, but I'm still in the dark. One quick question: do you know what changed in boost::optional between the older version that unmasks the pybind11 bug you fixed here, and the newer version that doesn't?

========================================================================= FAILURES ==========================================================================
_____________________________________________________________ test_reference_sensitive_optional _____________________________________________________________

    def test_reference_sensitive_optional():
...        props = m.OptionalRefSensitiveProperties()
        assert int(props.access_by_ref) == 42
>       assert int(props.access_by_copy) == 42
E       assert 44934346 == 42
E        +  where 44934346 = int(<EnumType.???: 44934346>)
E        +    where <EnumType.???: 44934346> = <pybind11_tests.stl.OptionalRefSensitiveProperties object at 0x7fb5d6517eb0>.access_by_copy

test_stl.py:228: AssertionError
================================================================== short test summary info ==================================================================

@rwgk
Copy link
Collaborator

rwgk commented Oct 27, 2021

@ryancahoon-zoox never mind, I think I just got it, it needs a tweak on the smart_holder branch:
fc5d70d
A bit amazing that this long-standing oversight wasn't covered by any tests before.

@ryancahoon-zoox
Copy link
Contributor Author

Glad my tests are already being useful :-)

I'm not exactly sure what changed - I didn't get so far as reading the Boost source. I just wrote a binding for a class similar to constructor_stats.h that prints when it is constructed/copied/moved/destroyed and tried returning it wrapped in various optional implementations from a return-by-copy property (like access_by_copy from the test cases I provided). I observed that the lifetimes for the object reported with recent std::optional and boost::optional were the same, but they were different from those with the older version of boost::optional.

My very tenuous intuition is that it's either some optimization involving move semantics, or else in the process of standardizing std::optional the standards committee found some edge case that required the implementation to be changed, which was then back-ported to boost::optional. But I have no idea what the actual change was.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

[BUG]: return_value_policy_override in optional_caster does not use the right types
4 participants