Skip to content

BUG: avoid uninitialized memory access / NULL-pointer deref in scalar byteswap - #32254

Merged
ngoldbaum merged 3 commits into
numpy:mainfrom
ngoldbaum:fix-scalar-byteswap
Aug 12, 2026
Merged

BUG: avoid uninitialized memory access / NULL-pointer deref in scalar byteswap#32254
ngoldbaum merged 3 commits into
numpy:mainfrom
ngoldbaum:fix-scalar-byteswap

Conversation

@ngoldbaum

@ngoldbaum ngoldbaum commented Aug 11, 2026

Copy link
Copy Markdown
Member

PR summary

Fixes #24694

The current scalar byteswap implementation assumes copyswap doesn't need the last argument for the copyswap function, which is an array object. That is incorrect for flexible descriptors (string, void) and for descriptors that request array initialization like object dtype or void dtypes with embedded object dtypes.

For flexible descriptors this allows access to unitialized heap memory on release builds of NumPy or an assert on a debug build:

>>> np.bytes_(b"1234").byteswap()
np.bytes_(b'\xb0\xf5D\x01')
>>> np.array([b"1234"]).byteswap()
array([b'1234'], dtype='|S4')

The first result is uninitialized heap garbage. With a debug build:

>>> import numpy as np
>>> np.bytes_(b"1234").byteswap()
Assertion failed: (arr != NULL), function STRING_copyswap, file arraytypes.c.src, line 2507.
[1]    84175 abort      spin python

If there are object dtypes involved, this bug can also lead to a segfault:

>>> import numpy as np
... o = object()
... s = np.array([(o,)], dtype=[("x", "O")])[0]
... print(s.byteswap()["x"])
...
[1]    84728 segmentation fault  python

(this is also an assert in VOID_copyswap in debug builds).

I could make this less branchy by forcing all dtypes to go through the object dtype path or making e.g. ints, floats, and other simple data dtypes use the stack-allocated array path. I did find leaving the fast-path, even with all the branches, was still worth it. Scalar byteswaps probably aren't performance critical though so I'm open to simplifying this if anyone wants that.

This was originally noticed by @ikrommyd during review of #32150: #32150 (comment)

AI Disclosure

I used an AI for review and to help identify corner cases.

@ngoldbaum ngoldbaum added 00 - Bug 09 - Backport-Candidate PRs tagged should be backported labels Aug 11, 2026

@mhvk mhvk left a comment

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.

This looks nice, and I would not shorten it. In a way, it is good to see the different paths.

@ikrommyd ikrommyd 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.

Looks good, left only one comment if you wanna avoid heap garbage in an edge case.

}
else {
copyswap(newmem, data, 1, NULL);
char *newmem = PyMem_Malloc(descr->elsize);

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.

Suggested change
char *newmem = PyMem_Malloc(descr->elsize);
char *newmem = PyMem_Calloc(1, descr->elsize);

just to avoid heap garbage in something like this

In [2]: dt = np.dtype([('a', 'i1'), ('b', 'f8')], align=True)
   ...: np.zeros(2, dt)[0].byteswap().tobytes()
Out[2]: b'\x00\x8e\x7f\x0c\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'

vs with calloc

In [1]: dt = np.dtype([('a', 'i1'), ('b', 'f8')], align=True)
   ...: np.zeros(2, dt)[0].byteswap().tobytes()
Out[1]: b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'

@ngoldbaum
ngoldbaum force-pushed the fix-scalar-byteswap branch from a8ff3eb to e14aeca Compare August 12, 2026 15:58
@ngoldbaum

Copy link
Copy Markdown
Member Author

Thanks for the quick review all!

@ngoldbaum
ngoldbaum merged commit 215def6 into numpy:main Aug 12, 2026
91 checks passed
@charris charris removed the 09 - Backport-Candidate PRs tagged should be backported label Aug 14, 2026
charris added a commit that referenced this pull request Aug 14, 2026
BUG: avoid uninitialized memory access / NULL-pointer deref in scalar byteswap (#32254)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

BUG: Possible undefined behavior for numpy.void.byteswap()

4 participants