-
Notifications
You must be signed in to change notification settings - Fork 120
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
Breaking PyReadonlyArray #258
Comments
I wonder whether we should use |
Trying this out using our examples here, it looks like even simple code like x = np.array([
[1, 0],
[0, 2],
], dtype=np.float64)
assert sys.getrefcount(x) == 1 will fail with an elevated reference count of 2 instead of 1. (Probably for passing the argument to |
In this example: py_run!(py, array, "array.flags.writeable = True\narray[(0,0)] = 1"); What is the |
On a side note, skimming through |
I think the reason is that it does not really matter for the aliasing problem as you can have multiple pointers to the same "master view" on the Python heap. All of those see
It is
Therefore I think that I fear we might need to bite the bullet and make all functions which produce Or maybe we could kindly ask NumPy to wrap all of their arrays in |
@kngwyu @davidhewitt Do PyO3 or rust-numpy have an already formulated position on the "unsafe-for-all-FFI-or-not" debate? |
One thing I just understood is why this does not affect the other wrappers for native types like |
Sorry it's taken me a few days to have a chance to sit down and read this! Comments below...
Please don't be sorry for this! I think soundness issues are critical and we should be striving for an API that is both safe and sound!
Reading through your example, I can understand this viewpoint, however I was to argue a different angle. I claim there are two issues at play:
I am reading the definition of the
It sounds to me like:
Perhaps we need to reconsider whether the |
I'm a bit unsure exactly what is being asked here. My perspective is that if an API makes it possible for UB to occur (e.g. by an incorrect optimization because invariants were broken), it must be In cases where I'm not certain if an API is sound, I prefer to mark it |
Does this refer to I think we should therefore drop the method and expose raw pointer based array views instead which would be more versatile and more widely applicable.
Indeed, I think we cannot depend on the In my personal opinion, I would prefer dropping it to simplify the API and avoid creating a false sense of safety.
I am referring to the community-wide discussions started by the I think under this approach, one could for example say that if calling a Python function under a given aliasing situation was already wrong, than this isn't changed by that function being implemented in Rust. For example, mutable sharing of NumPy arrays between threads is already UB in a pure-Python setting and a Rust function called by Python can therefore assume that this does not happen. From this point of view, On the other hand, our requirements for creating shared or exclusive references basically do not exist for Python in which case both Personally, I can agree with both perspectives. Especially as I would like rust-numpy to enable scientific users to write fast and safe code. I think having (One more technical issue that enters here is that even a safe |
(Another design point might be to never create |
Having referred to the lack of If am going a bit crazy with this, we could even dynamically borrow check views by following their base objects and testing whether they overlap. Of course, the cost might be prohibitive and we would have to over-approximate when we do not understand a base object type. But I think this might allow be a safe and sound API to access NumPy arrays as reference-based array views. |
Scratch at least that second paragraph: This would not improve things as only Rust code would participate in the checking but Python could still produce overlapping views that the Rust never sees. But then again, could this could which would necessarily be invisible to the Rust compiler still lead to miscompilation? Cross-language LTO of an extension containing Rust and e.g. C++ code? |
Thank you for your very interesting discussion (and sorry for my slow response... I sprained a finger and will be not active for a while 😅 I still don't know which way to go, but let me note some ideas. You nicely summarized two approaches to safety:
And considering that shared mutable references are pretty much everywhere in Python codes, I'm inclined to the latter approach. However, as a lazy programmer, I don't want to overwhelm users with many Detailed comments:
What do you think of as the alternative of
Agreed.
This is a very long-term problem and should be discussed on PyO3... |
Oh, I hope your recuperation goes well!
Actually, I was thinking of going for
as I think we can implement a prototype here which could maybe later be folded into PyO3 if it turns out to be generally useful. My current thinking would be to have safe methods fn as_array<'a>(&'a self) -> PyArrayRef<'a, A, D>;
fn as_array_mut<'a>(&'a self) -> PyArrayRefMut<'a, A, D>; where PyArrayRef<'a, A, D>: Deref<Target=ArrayView<'a, A, D>>
PyArrayRef<'a, A, D>: DerefMut<Target=ArrayViewMut<'a, A, D>> The methods would be safe as we would enforce borrowing check using borrow flags stored like thread_local! {
BORROW_FLAGS: RefCell<HashMap<usize, Box<Cell<isize>>> = Default::default();
} with the key being the address of the NumPy array on the Python heap. This looks rather inefficient at a first glance, but I think modifying the NumPy flags isn't free either. And this would also detect errors like calling #[pyfunction]
fn binop(x: &PyArray1<f64>, x: &PyArray1<f64>, z: &PyArray1<f64>) {
let x = x.readonly().as_array();
let y = y.readonly().as_array();
let z = unsafe { z.as_array_mut() };
...
} as binop(x, y, x) Of course, this would not protect the Rust code from all errors in the Python code or errors in other Python extensions. That would be taking a stance similar to cxx.rs: You need to verify all your unsafe code - i.e. unsafe Rust and e.g. Python, C++, Fortran, etc. - but if that is safe, so is your safe Rust code. (As written above, this could eventually be extended to not just check the identity of the NumPy arrays but traverse the base object chain and also check for overlapping views thereby catching more errors on the Python side of the fence.) |
Note that AFAIU, we are already taking this stance w.r.t. thread safety: While |
Another thing we should probably add is considering the |
@adamreichold By the way - you're probably aware, but just in case:
This was changed in 1.17.0. |
This ^ again, I think (and if I understand correctly), draws a line between
|
We recently change things so that we make data that is owned via
As in the previous discussion, this breaks as soon as we hand that |
Personally, I also advocate for basically the opposite position: Trust Python code (like we do unsafe Rust code) but make sure that safe Rust code upholds borrowing discipline by dynamic borrow checking methods like |
I am sorry for repeatedly opening soundness issues, but I fear that using NumPy's
writeable
is insufficient to protect safe code from mutable aliasing.At least it seems incompatible with a safe
as_cell_slice
as demonstrated by the following test failingwhich implies that
as_cell_slice
cannot be safe. (I am not sure if it adds anything overas_array(_mut)
at all viewed this way. I think a safe method that yields anndarray::RawArraView
might be preferable to handle situations involving aliasing.)Another issue I see is that Python does not need to leave the writeable flag alone, i.e. the test
also fails, but I have to admit that this might be a case of "you're holding it wrong" as I am not sure what guarantees we can expect from the Python code? (After all
PyArray<A,D>: !Send
which Python code does not need to respect either even just trying to access an array and thereby checking its flags from another thread might race with us modifying the flags without atomics.)The text was updated successfully, but these errors were encountered: