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 exception handling when pybind11::weakref() fails. #3739

Merged
merged 8 commits into from
Feb 18, 2022

Conversation

hawkinsp
Copy link
Contributor

@hawkinsp hawkinsp commented Feb 15, 2022

The weakref() constructor calls pybind11_fail() without clearing any
Python interpreter error state. If a client catches the C++ exception
thrown by pybind11_fail(), the Python interpreter will be left in an
error state.

I found this problem while fixing code that called pybind11::weakref(obj) where obj is not weak-referenceable. In that case, I wanted to fallback to another code path by catching the exception raised by pybind11_fail, e.g.,

try {
  x = py::weakref(obj);
} catch (std::runtime_error& e) {
 ... do something else
}

However, if you do this, then the Python error flag is left set in the catch case and not cleared. This causes later calls into the Python interpreter to fail.

It is possible that this PR should be more aggressive: all calls to pybind11_fail() should clear the error flag. If we are going to throw a C++ exception, then presumably that is replacing any Python error state.

Suggested changelog entry:

* Fix exception handling when ``pybind11::weakref()`` fails

The weakref() constructor calls pybind11_fail() without clearing any
Python interpreter error state. If a client catches the C++ exception
thrown by pybind11_fail(), the Python interpreter will be left in an
error state.
@Skylion007
Copy link
Collaborator

What is the PyErr flag set to? I see two possible alternative solutions:

  1. add a Debug assert that assert(!PyErr_Occurred()); to pybind11_fail.
  2. always use raise_from in pybind11 so more information is passed via the __cause__ exception object. Should be easy to do now that we have dropped Python2 support and I am pretty sure that ensures the error is properly cleared.

I gave a stab of 1. on master in Debug mode, but it didn't trigger any assert failures which tells me that this error isn't caught by our current test suite. @hawkinsp can you verify this is the case and add an appropriate unit test.

@henryiii @rwgk I'd appreciate hearing your input on what the best way to harden pybind11_fail to this would be.

@hawkinsp
Copy link
Contributor Author

hawkinsp commented Feb 16, 2022

@Skylion007 I may not have time to add a test to the PR, but the idea is like this:

In [32]: class C:
    ...:     __slots__ = []
    ...:

In [33]: import weakref

In [34]: c = C()

In [35]: weakref.ref(c)
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-35-6812bda72132> in <module>
----> 1 weakref.ref(c)

TypeError: cannot create weak reference to 'C' object

You can reproduce by simply calling pybind11::weakref from C++ on c instead of weakref.ref() from Python.

@rwgk
Copy link
Collaborator

rwgk commented Feb 16, 2022

@Skylion007 wrote:

always use raise_from in pybind11 so more information is passed via the cause exception object. Should be easy to do now that we have dropped Python2 support and I am pretty sure that ensures the error is properly cleared.

I really like that idea.

Yesterday on chat @hawkinsp had the idea to move the change into the pybind11_fail implementation.

I haven't looked, could use raise_from from there? That would seem like an ideal solution.

@Skylion007
Copy link
Collaborator

I really feel like the proper thing to do here is just remove our pybind11_fail and propogate the typeerror with error_already_set(). Thoughts @henryiii @rwgk ?

It's a very minor breaking change, but one that will match PyBInd11 functionality with Python a bit better. I actually don't like raise_from in this context UNLESS we really have to preserve backwards compatability as the errors will be of different types if it's from PyBind11 vs Python (Runtime vs TypeError).

@henryiii
Copy link
Collaborator

henryiii commented Feb 16, 2022

I'm all for closer matching of pybind11 with Python, personally. We can mention it in the upgrade guide.

@rwgk
Copy link
Collaborator

rwgk commented Feb 16, 2022

It's a very minor breaking change

We're going from 2.9 to 2.10 anyway, that would seem fine.

@Skylion007 Skylion007 changed the title Clear Python error state if pybind11::weakref() fails. Fix exception handling when pybind11::weakref() fails. Feb 16, 2022
@Skylion007 Skylion007 self-requested a review February 16, 2022 19:12
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.

Is it best to test that PyPy doesn't show this behavior (as done now), or skip/xfail the test on PyPy? Guessing this behavior is likely to remain unchanged on PyPy, which would be the assumption in the current implementation, I think that's fine.

@Skylion007
Copy link
Collaborator

@henryiii CPython says weakref is an implementation detail. If PyPy behavior changes when we update PyPy than we can make it an xfail.

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.

Awesome, thanks a lot @Skylion007 for hammering it into such great shape!

# Should raise TypeError on CPython
with pytest.raises(TypeError) if not env.PYPY else contextlib.nullcontext():
if has_callback:
_ = create_weakref(ob, callback)
Copy link
Collaborator

Choose a reason for hiding this comment

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

Is the _ important here? (Guessing yes, but asking JIC it's a leftover from some experiment.)

Copy link
Collaborator

Choose a reason for hiding this comment

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

I just wanted to annotate that there was a return value we were throwing away.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Makes sense! Thanks for the explanation.

@rwgk
Copy link
Collaborator

rwgk commented Feb 17, 2022

I'll run the global testing for this PR as soon as I can. Unfortunately the previous attempt is still stuck in the queue (bad infrastructure day).

@rwgk
Copy link
Collaborator

rwgk commented Feb 18, 2022

Globally tested with the 2022-02-18 00:00 batch. All good!

@Skylion007 Skylion007 merged commit 44596bc into pybind:master Feb 18, 2022
@github-actions github-actions bot added the needs changelog Possibly needs a changelog entry label Feb 18, 2022
@Skylion007
Copy link
Collaborator

Whoops, @henryiii forgot to fix the initial PR to include a changelog. What's the best way to retrofit?

@henryiii
Copy link
Collaborator

? We don't ever include changelogs in the PR. Just make sure it's in the description so that nox -s make_changelog pulls it from GitHub.

@henryiii
Copy link
Collaborator

(done)

@henryiii henryiii removed the needs changelog Possibly needs a changelog entry label Mar 29, 2022
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.

4 participants