-
Notifications
You must be signed in to change notification settings - Fork 882
Align the PyCapsule API with the original C API, making it safer #5229
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
base: main
Are you sure you want to change the base?
Conversation
3255558
to
774676a
Compare
4b5a69c
to
48fad84
Compare
Hi @davidhewitt, I apologize for bugging you, does this PR have any chance of being merged? I'm happy to make changes to address any concern with the changes. |
Sorry for the delayed review here. I am somewhat neutral with this PR, I understand the motivation for it and was last discussed in #2485. I noted that So it seemed fine for us to make the same choice as The original design for the capsule C API seemed to come from https://bugs.python.org/issue5630. See also https://discuss.python.org/t/rationale-for-changing-a-capsules-name-or-destructor/48504 where there is the discussion about the fact that a capsule's name could be changed after creation, and what it may or may not mean for safety. One major plus compared to when #2485 was introduced is that I would be open to hearing other maintainers' views on this PR. If we choose to move forward with this, we should introduce new methods and deprecate the existing ones, as I think the existing behaviour is not sufficiently wrong that we need to break all users immediately upon release. |
I think I'm on similar stance here. While not strictly opposed to this change I'm also not really convinced that it significatly improves safety. I think we already are a bit safer because these methods are on a statically typed |
Thank you for your responses and for the additional context!
The motivation for checking the name comes when you receive the PyCapsule from code that is not under your control, not when you construct the capsule yourself. In this case the name of the capsule is the only check that you can do to verify that the opaque pointer in the capsule has the expected type. If the API requires the programmer to provide the name when getting the pointer, it is a strong nudge to check the name of the capsule before working with the pointer. I think that in Rust, it's better to preserve the nudge, rather than going to some lengths to remove it. The particular use case that I had in mind when opening this PR was the Arrow PyCapsule interface, which specifies a PyCapsule name for each object in the Arrow C Interface. For example, the C interface type In practice, the code can look like this: // get the PyCapsule from a Python object that is exportable to an Arrow stream
let py_stream: &Bound<'py, PyAny> = ...;
let stream_capsule = py_stream.call_method0(intern!(py, "__arrow_c_stream__"))?
.downcast_into::<PyCapsule>()?;
// get the pointer from the capsule, checking that the capsule has the expected name.
// note that with the changes proposed in this PR, the programmer is nudged to do the check
let stream_ptr = stream_capsule.pointer(Some(c"arrow_array_stream"))?;
let stream_ptr = stream_ptr.as_ptr() as *mut arrow::ffi_stream::FFI_ArrowArrayStream; I did some sleuthing to find out why pybind11 also bypasses the safety nudge:
So it seems that pybind ended up with bypassing the safety check not by a deliberate choice, but because it was the path of least resistance. I believe that in PyO3, the best lesson that we can learn from pybind11 in this regard is to reintroduce the satefy nudge before it's too late :) |
Thanks for the example. I have a much better idea about how this can be used now. I think in my head I only had the very simple examples about importing a known capsule, not that you could obtain them dynamically like this and still do something useful with them. In this scenario the api makes a lot more sense and an incorrect name should also just be a regular error and not an assertion like I was thinking previously. With this I think this gets a 👍 from me. Alternatively I have this (maybe slightly crazy) idea of using type state for this, but I have no idea if it would be worth it (or even possible within pyo3). Never the less I might as well write it down 🙃 We could have something like |
Hmm, I don't think that using a typestate for the capsule would be very useful for most use cases:
|
CodSpeed Performance ReportMerging #5229 will not alter performanceComparing 🎉 Hooray!
|
@davidhewitt @Icxolu I reworked the PR to add new methods (suffixed with |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks, I think the API proposed here is the right one. Due to the potential soundness issue identified I'm going to push to this PR to adjust the reference
API, merge it, and get on with preparing a new release. Please forgive me for taking over.
Probably when we get to 0.29 and remove the deprecated methods I will rename the _checked
methods to drop the suffix, as I think it'll be good enough without it and read better.
unsafe { &*self.pointer().cast() } | ||
} | ||
|
||
unsafe fn reference_checked<T>(&self, name: Option<&CStr>) -> PyResult<&'py T> { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Staring at this, it looks like a potential use-after-free due to lifetime extension, the 'py
lifetime can potentially be too long.
e.g.
Python::attach(|py| {
let data = PyCapsule::new("foobar".to_string());
let string_ref: &String = unsafe { data.reference() };
drop(data);
// use `string_ref` here for UAF
});
Looks like that is good justification to land this API and probably backport it to 0.26.
We at least get the slight "relief" that .reference()
is already an unsafe fn
with an easily broken invariant on the Python side (user could provide any old data and even modify it while this reference is up), but it's still not great.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
See #5474
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe it will be best to keep only the pointer_checked()
API, and let the user dereference the pointer as they see fit? The reference()
/reference_checked()
method does not provide much in terms of ergonomics (literally saves the user a single *
) and makes strong but quite implicit safety assumptions.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I was wondering about similar, I am very tempted to remove it. If users complain during the deprecation period, we can always reconsider later.
use std::ffi::{CStr, CString}; | ||
use std::ptr::{self, NonNull}; | ||
|
||
/// Represents a Python Capsule |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We should probably add some information here about the name checking, I will do that.
In the PyCapsule C API, the caller has to specify the correct name of the PyCapsule to get the pointer stored in the capsule. This is intended as a safety check: casting
void*
to an arbitrary pointer is inherently quite dangerous and the capsule name provides the only (albeit imperfect) way to check that the pointer has the expected type.However, the Rust API exposed by pyo3 didn't require the capsule name to get the pointer or reference stored in the capsule:
This PR changes these methods to be more in line with the C API and thus harder to misuse:
This is a breaking change, but I believe that the benefit of increased safety is worth the cost of API breakage.