Skip to content

BUG: fix StringDType distinct-allocator bugs and add tests - #31609

Merged
ngoldbaum merged 4 commits into
numpy:mainfrom
ngoldbaum:fix-stringdtype-legacy-api
Jun 20, 2026
Merged

ngoldbaum merged 4 commits into
numpy:mainfrom
ngoldbaum:fix-stringdtype-legacy-api

Conversation

@ngoldbaum

@ngoldbaum ngoldbaum commented Jun 11, 2026 •

Copy link
Copy Markdown
Member

Fixes bugs when multiple distinct stringdtype arrays are passed to np.choose, np.put, np.putmask, np.place, flatiter reads, and a bug that could lead to unsafe views being created for StringDType and object arrays.

Also exposes a new C helper function _is_safe_view_cast which takes view_offset into account and uses it in the Python helper functions for view safety. It only uses this function for StringDType. We could use it more generally but that might lead to breakage, so just fixing StringDType seemed like a practical first pass.

The C changes all work out to fixing the same couple patterns: legacy paths that used copyswap (e.g. #24859, which this might obviate the need for?) or code paths that assumed all input arrays have the same descriptor instance.

Also adds tests for functionality that is fixed here or is poorly tested currently. It makes use of a new helper function that allocates two distinct arrays of strings with allocations that live in the arena buffer to trigger these bugs.

I milestoned this for 2.5.0 and marked it for backport because these are all bugs that can cause segfaults.

I used Claude Fable 5 to work on this, including for the implementation and new tests.

@ngoldbaum ngoldbaum added this to the 2.5.0 Release milestone Jun 11, 2026
@ngoldbaum ngoldbaum added 00 - Bug 09 - Backport-Candidate PRs tagged should be backported component: numpy.strings String dtypes and functions labels Jun 11, 2026
@ngoldbaum

ngoldbaum commented Jun 11, 2026 •

Copy link
Copy Markdown
Member Author

that debug interpreter test crash looks real

@ngoldbaum

ngoldbaum commented Jun 12, 2026 •

Copy link
Copy Markdown
Member Author

I think this is ready for review now. @seberg @MaanasArora @mhvk - hope you don't mind the ping. The C diff isn't so huge, most of the changes are new tests.

* Convert all inputs to arrays of a common type
* Also makes them C-contiguous
*/
mps = PyArray_ConvertToCommonType(op, &n);

@MaanasArora MaanasArora Jun 13, 2026 •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

For the PyArray_Choose fix here, I noticed we actually discussed this bug in #28206 (comment). Is it better to fix the PyArray_ConvertToCommonType function directly to convert all arrays to use an identical descriptor? Then I think we will lose the complexity below, as we only have one descriptor to cast from!

@seberg seberg Jun 13, 2026 •

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

(have to catch up with actual review still; we forgot to open a follow-up issue back then, I guess.)

Theoretically, but it would require copying all the inputs even if they have the right dtypes already basically; and it would be a bit strange to force that (i.e. the point normally is for all arrays to have their own dtype for StringDType).

But without that, each casts needs the identity dtype passed in and that requires one cast per array.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

OK yes, might be a bit strange to copy and then cast separately later, thanks.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

have to catch up with actual review still; we forgot to open a follow-up issue back then, I guess.

Sorry for dumping a bunch of code on you on a Friday. Take your time!

@ngoldbaum
ngoldbaum force-pushed the fix-stringdtype-legacy-api branch from 5915100 to ef88174 Compare June 16, 2026 14:40

@seberg seberg left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Thanks, a few comments. In some places I think you/the bot thought the RefCheck was a pattern to generalize, but it is used in a few places due to bare memcpy use not to avoid copyswap (although it may be fine to do so additionally).

Otherwise a few small nits, many take-it-or-leave it. I do also think the view safety check should be used generally if we create it, there is nothing special about StringDType (and it can short-circuit the ==, I think.

if (j >= nv) {
j = 0;
if (PyDataType_REFCHK(PyArray_DESCR(array))) {
/* dtypes with references may not define copyswap */

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Please change this to an explicit check for ->copyswap being NULL, while it may be true in practice, I don't think the comment really adds up.

NPY_cast_info cast_info;
NPY_ARRAYMETHOD_FLAGS flags;
const npy_intp one = 1;
const npy_intp strides[2] = {chunk, chunk};

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This is better as elsize (zero is also OK, but elsize is probably better in practice to hit the contiguous loops).
chunk just doesn't make sense here at all.

NPY_cast_info_init(&cast_info);
if (PyArray_GetDTypeTransferFunction(
PyArray_ISALIGNED(values) && PyArray_ISALIGNED(array),
chunk, chunk,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Should be identical to strides above, just use:

Suggested change
chunk, chunk,
strides[0], strides[1],

* separate internal state (e.g. StringDType allocators).
*/
NPY_NO_EXPORT PyObject *
_is_view_safe_cast(PyObject *NPY_UNUSED(module), PyObject *args)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Thanks, I really like this helper! I do have a bit of a dislike for PyArg_ParseTuple (it's just silly slow API...), but I don't mind keeping it.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Changed it to METH_FASTCALL.

Comment thread numpy/_core/src/multiarray/getset.c Outdated
swap = PyArray_ISNOTSWAPPED(self) != PyArray_ISNOTSWAPPED(arr);
copyswap = PyDataType_GetArrFuncs(PyArray_DESCR(self))->copyswap;
if (PyDataType_REFCHK(PyArray_DESCR(self))) {
/* dtypes with references may not define copyswap */

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Again, check explicitly for copyswap.

Effectively it means that StringDType shouldn't implement it even if we allowed that (it's far better off with this pattern due to lock handling) but that seems reasonable.

Now... what is completely crazy here is the whole manual PyArray_Item_XDECREF nonsense, which looks like copyswap() would handle it just fine for objects (and structured dtypes).

For structured dtypes the new path may well be better (for larger arrays). So I would be fine with keeping the REFCHCK additionally to copyswap == NULL.

(Another interesting thing, but let's not act on it, I guess... This function should release the GIL for larger inputs but doesn't seem to.)

dest = PyArray_DATA(self);

if (PyDataType_REFCHK(PyArray_DESCR(self))) {
NPY_cast_info cast_info;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Just to note that I am not insane about the copyswap thing above. This path is different, because we assume that memcpy is sufficient (copyswap isn't even used).

(There is a new flag for optimizing structured dtypes that we should possibly check here as well, NOT_TRIVIALLY_COPYABLE but not this PR really... Won't complain if you add it, it's basically an extremely niche fix on structured dtypes.)

dtype,
obj_dtype, 0, &cast_info,
&transfer_flags);
cast_infos = PyMem_New(NPY_cast_info, n);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Could use NPY_ALLOC_WORKSPACE or... just use NPY_MAXARGS the MultiIter is limited to that anyway (although of course it isn't explitily from here).

(I don't care though, I doubt micro-optimizations matter much for choose.)

obj_dtype->elsize,
PyArray_DESCR(mps[i]),
obj_dtype, 0, &cast_infos[i],
&transfer_flags) < 0) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

We could release the GIL here based on transfer_flags (although, I suppose while it can't matter, in principle we should check all transfer flags).

This doesn't matter for objects, but it does matter a bit for strings.

if (y_cast_info.func(
&y_cast_info.context, args, &one,
y_strides, y_cast_info.auxdata) < 0) {
NPY_END_THREADS;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Seems nicer to me to just put it into fail: here (requires moving the THREADS_DEF to the start of the function).

Comment thread numpy/_core/_internal.py Outdated
raise TypeError(
"Cannot view StringDType array data through a different dtype "
"instance; use astype to copy the data to the new dtype "
"instance")

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

If you care for the StringDType error that is fine, but I don't think you need to guard for it here, in theory StringDType isn't special.
Maybe _is_view_safe_cast is not the best name here, but that function can completely replace the newtype == oldtype, I think?

@ngoldbaum

Copy link
Copy Markdown
Member Author

I think I applied all your suggestions in the last commit. If you could give it another look I'd appreciate it.

@seberg seberg left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Thanks, only had a brief look through the new changes, but looks all good. (If you feel like it and haven't done so, I might have thrown an agent once more but don't have it set up here right now.)

Please go ahead and merge!

@ngoldbaum

Copy link
Copy Markdown
Member Author

In it goes, thanks for taking a look and giving me feedback Sebastian!

@ngoldbaum
ngoldbaum merged commit 706b103 into numpy:main Jun 20, 2026
85 of 87 checks passed
@charris charris removed the 09 - Backport-Candidate PRs tagged should be backported label Jun 20, 2026
charris added a commit that referenced this pull request Jun 20, 2026
BUG: fix StringDType distinct-allocator bugs and add tests (#31609)
ngoldbaum added a commit to ngoldbaum/numpy that referenced this pull request Aug 4, 2026
ngoldbaum added a commit to ngoldbaum/numpy that referenced this pull request Sep 8, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

00 - Bug component: numpy.strings String dtypes and functions

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants