BUG: Ensure stable data address in scalar __array_interface__ - #30538
Conversation
Scalar types expose `__array_interface__` by creating a temporary array. To ensure the address remains valid, the temporary array must be kept alive. (See numpy#30354)
__ref value__ref value
__ref valueThere was a problem hiding this comment.
Aren't you decrefing a pointer that has to be NULL here?
There was a problem hiding this comment.
this also indicates this error path isn't tested - might be worth adding a test!
There was a problem hiding this comment.
Aren't you decrefing a pointer that has to be NULL here?
Thank you for catching this. you're absolutely right.
I fixed the incorrect decref.
this also indicates this error path isn't tested - might be worth adding a test!
This if branch seems to be unreachable because the array object always provides __array_interface__.
Instead of adding a direct test for this path, I improved the test coverage by:
- increasing the number of scalar types under test
- adding checks for unconversible types
ngoldbaum
left a comment
There was a problem hiding this comment.
There's some code that is doing questionable things and it only happens to work because scalars are read-only. See comments inline.
| int _flags = flags; | ||
| _flags &= ~(NPY_ARRAY_WRITEBACKIFCOPY | NPY_ARRAY_OWNDATA); | ||
| _flags |= NPY_ARRAY_NOTSWAPPED; |
There was a problem hiding this comment.
This is all a no-op because _flags isn't used below. See my other comment below for speculation about why you defined this variable.
|
|
||
| return Py_BuildValue( | ||
| "NO", PyLong_FromVoidPtr(scalar_value(self, NULL)), | ||
| ((flags & PyBUF_WRITEABLE) == PyBUF_WRITEABLE) ? Py_False : Py_True); |
There was a problem hiding this comment.
Why are you checking PyBUF_WRITEABLE here - that isn't an ndarray flag. PyBUF_WRITEABLE is part of the CPython buffer request API and will only show up in calls directly from CPython for handling the buffer protocol, which has nothing to do with this code as far as I can see.
Looking at the implementation of array_dataptr_get I think you meant to use _flags here and not do the PyBUF_WRITEABLE check at all.
Also I don't think this check for read-only matters at all for scalars because scalars are always read-only:
>>> a = np.int32(5)
>>> a[:] = 7
Traceback (most recent call last):
File "<python-input-3>", line 1, in <module>
a[:] = 7
~^^^
TypeError: 'numpy.int32' object does not support item assignment
>>> a.data
<memory at 0x200012a6e50>
>>> a.data[:] = 7
Traceback (most recent call last):
File "<python-input-5>", line 1, in <module>
a.data[:] = 7
~~~~~~^^^
TypeError: cannot modify read-only memory
Maybe you can avoid all this messing with flags and just set statically that this is read-only.
Also why not set the other items in the __array_interface__ dict like shape etc? Seems like there's no reason not to and it should be trivial to do that for scalars. I'd follow the ndarray implementation in array_interface_get.
There was a problem hiding this comment.
Thank you for taking time to review my PR.
I’d like to briefly explain my original reasoning, and then align with your suggestions.
- Why I do not set read-only flag for scalar array?
As you point out, scalars are always read-only.
However, according to the code below in ctypeslib, if the array is read-only, as_ctypes raise an error. So, I decided to follow the way flags are created in __array_struct__.
numpy/numpy/ctypeslib/_ctypeslib.py
Lines 594 to 595 in 3556042
Maybe, is it better to set flag as writable ? (Or forbid as_ctypes for scalars altogether?)
- use of
PyBUF_WRITEABLEflag
This was a clear mistake on my part. I confused ndarray's flags with buffer protocol's flags. As you pointed out, PyBUF_WRITEABLE is not appropriate here, and I will remove this entirely.
_flagsvariable
You are also right here. I intended to use _flags to check writability, but the current code does not do that correctly.
I am sorry for the confusion.
why not set the other items in the array_interface dict like shape etc? Seems like there's no reason not to and it should be trivial to do that for scalars. I'd follow the ndarray implementation in array_interface_get.
Sure! I will implement it following to array_interface_get.
There was a problem hiding this comment.
I've updated the PR accordingly except for the what kind of flags to set for scalar arrays.
|
|
||
| /* NOTE: sclar array is always readonly, but we need writable flags | ||
| * to use `ctypeslib.as_ctyps` via `__array_interface__`. | ||
| * So, we set the flag borrowing from the temporary array. |
There was a problem hiding this comment.
This doesn't really work out, I think. The content of the scalar is clearly readonly (except for void ones).
It seems that __array_interface__ right now doesn't consider it to be readonly though, which is technically true, because it refers to __ref which is undocumented.
We have two options here:
- Document that
__refthing - Change the flag to return
readonlyhere (maybe always, also for void).
I would be tempted to do the second one but that might backfire in practice and we need to ask a few others.
In the second case, ctypes needs to work around whatever we do here. That may be just ignoring the readonly part or creating a copy explicitly.
Actually, it may make sense to simply error in ctypeslib because ctypeslib was clearly always very broken with a reasonably high chance of generating bad results without an error.
There was a problem hiding this comment.
Thanks for the comment!
I agree with the second option. Under this option, we would not change anything in ctypeslib.as_ctypes.
As a result, scalar arrays would no longer be usable with ctypeslib.as_ctypes, but this behavior seems more robust.
we need to ask a few others.
Is there anyone I should ping about this, or would it be appropriate to bring this topic to the next triage meeting?
There was a problem hiding this comment.
@mtsokol do you have any context about where your initial report about this came from?
There was a problem hiding this comment.
It appeared in our custom compiler:
https://github.com/finch-tensor/finch-tensor-lite/pull/248/files#diff-70a950803da49a25141ebb1d3bd6a6e33d417e2c3e0c42b0a34ed662bd690b2cL211 - within code meant for serializing NumPy scalars to C. We ended up using 0-d arrays here so we won't be impacted if as_ctypes stops supporting scalars.
There was a problem hiding this comment.
Yeah, just to be clear "asking around", I was just thinking of brining it up at the triage meeting and if there is a general consensus to try, try it (but not backport!).
I doubt we'll easily find someone who might actually be affected. The problem is that the main use-case may be something like a Cython function that uses a typed memory view (like double data[::1]) that could use a const (signalling read-only is OK), but doesn't...
And we should maybe think whether such Cython use-cases would hit this (I am not sure), because if they do, than that might be an unfortunately large change.
There was a problem hiding this comment.
🤦 this is __array_interface__ so doesn't matter for Cython of course, i.e. the chance of someone actually caring/noticing is a lot smaller.
There was a problem hiding this comment.
Thanks everyone for the discussion.
It seems reasonable to try this out, but just in case, I’ve added it as a topic for the next triage meeting.
There was a problem hiding this comment.
I've updated the flags in __array_interface__ for scalars, following the discussion in the last triage meeting.
I've also updated the tests accordingly.
I checked repositories under the numba organization to see whether ctypeslib.as_ctypes is used and did not find any usage.
For reference:
https://github.com/search?q=org%3Anumba+%22as_ctypes%28%22&type=code
|
Just a gentle ping. |
|
I think this needs a release note. Can you also update the PR description to match what it actually does? The PR description will show up in the final merge commit. |
ngoldbaum
left a comment
There was a problem hiding this comment.
The C API uses and reference counting all look correct to me.
076e8c7 to
2b58cfc
Compare
|
@ngoldbaum |
seberg
left a comment
There was a problem hiding this comment.
LGTM, one nit is that we should also note the change in __array_interface__ maybe?
I would prefer if we could refactor this so that we have a single function that build the array interface but gets strides, shape, ptr and descr passed in.
(Right now we have twice practically the identical code.)
But considering how long this has been sitting, I am also happy if we don't do that.
| This change was made to avoid the issue `gh-30354 <https://github.com/numpy/numpy/issues/30354>`__ | ||
| and to enforce the readonly nature of scalar types in NumPy. | ||
| Users who need to convert scalar types to ctypes should first convert them to an array | ||
| (e.g., ``numpy.asarray``) before passing them to ``numpy.ctypeslib.as_ctypes``. |
There was a problem hiding this comment.
Might be worth pointing out that this was always shady? :).
Can you add a second note about the change in __array_interface__? (or add both in one.)
| } | ||
| PyList_SET_ITEM(res, 0, dobj); | ||
| return res; | ||
| } |
There was a problem hiding this comment.
It would be cool to refactor these so that we don't repeat them in both files (i.e. by making them take the PyArray_Descr *).
But I can live without doing that at this point :).
There was a problem hiding this comment.
Thanks for pointing this out.
I've refactored the duplicated logic into a common helper function in descriptor.c and updated the relevant call to use it.
I left some functions, such as gentype_shape_get, unchanged since they are also used in other parts of scalartypes.c.src.
If you'd prefer to refactor those as well, I'm happy to follow up.
| This behavior has been removed to avoid the issue `gh-30354 <https://github.com/numpy/numpy/issues/30354>`__. | ||
| Now, scalar types will not have a ``__ref`` entry in their ``__array_interface__``. | ||
| Instead, ``data`` entry in ``__array_interface__`` will directly point to the memory location of the scalar value and | ||
| will be marked as readonly. |
There was a problem hiding this comment.
Thanks for the quick update! I was actually thinking the main thing is that we export it as readonly now.
(But maybe I'll just think about it a bit myself and push an update later)
There was a problem hiding this comment.
Thanks! That makes sense. Let me know if you'd like me to make any adjustments on my side.
There was a problem hiding this comment.
Hmm, thanks for the update, unfortunately PyArray_DescrFromScalar returns a new reference (and actually can in theory error). So we need to do refcounting here unfortunately.
(I was going to just "suggest" it, but it spans the next block also, a goto finish: that also cleans up inter may be nicer.
There was a problem hiding this comment.
Thanks for pointing it out.
I've updated this commit to handle the refcounting for PyArray_DescrFromScalar
in scalartypes.c.src:
5632bf0
In addition, I also refactored the code to use a common helper when creating
__array_interface__ for both arrays and scalars.
Co-authored-by: Sebastian Berg <sebastian@sipsolutions.net>
Co-authored-by: Sebastian Berg <sebastian@sipsolutions.net>
seberg
left a comment
There was a problem hiding this comment.
OK, thanks @riku-sakamoto for all the follow-ups and checking what it might break. Let's get this in. (Small nit/comment: I think the building function would be slightly nicer to take the dtype/descr object to consolidate more of the logic there, but doens't matter if you feel about it, you can follow up).
If anyone finds this PR because the readonly or ctypes changes affect you, please let us know. I can see this affecting downstream, and if it does we have to check what to do about ctypes.
(I know that using ctypes is quite typical, my assumption is that it is not all that typical to use it on scalars and since it was broken memory ownership wise, I am hoping it isn't used much -- but I realize that the e.g. the fact that we cache this type of small temporary arrays may have made this work in practice!)
Review from an earlier iteration of the PR.

Changes
__array_interface__for NumPy scalar types to avoid creating a temporary array.__array_interface__dict for NumPy scalars.As a consequence of this change,
numpy.ctypeslib.as_ctypesnow raisesTypeErrorwhen NumPy scalar types are passed directly.Closes #30354