Skip to content

BUG: fix crash on 32 bit systems using abi3t - #31771

Merged
ngoldbaum merged 19 commits into
numpy:mainfrom
kumaraditya303:32bit-crash
Jul 8, 2026
Merged

BUG: fix crash on 32 bit systems using abi3t#31771
ngoldbaum merged 19 commits into
numpy:mainfrom
kumaraditya303:32bit-crash

Conversation

@kumaraditya303

@kumaraditya303 kumaraditya303 commented Jun 26, 2026

Copy link
Copy Markdown
Contributor

PR summary

Fixes #31762

This PR fixes the crash on 32 bit systems when using abi3t ABI. In this PR, the first member of the PyArrayDescr is now aligned to 8 bytes so that regardless of the size of the actual PyObject, the field accesses remain correct. 8 bytes is used instead of alignof(max_align_t) to preserve backwards compatibility with exisiting abi3 extensions where sizeof(PyObject) is a multiple of 8 thereby the added alignment is no-op and does not changes the offset for such extensions.

AI Disclosure

AI was used to create the test.

@charris charris added 00 - Bug 09 - Backport-Candidate PRs tagged should be backported labels Jun 27, 2026
@charris

charris commented Jun 27, 2026

Copy link
Copy Markdown
Member

The lint problem is unrelated and fixed in main.

@seberg seberg added the 63 - C API Changes or additions to the C API. Mailing list should usually be notified. label Jun 28, 2026
* object.
*/
PyTypeObject *typeobj;
NPY_DECL_ALIGNED(8) PyTypeObject *typeobj;

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 a neat idea! Inspired I had a bit of a brainstorming search:

How about we embed the "public" fields here as an anonymous struct? I feel like that may be even cleaner (it doesn't encode the actual alignment guarantees yet, see below).
That is C11 but major compilers support it earlier, that might be somewhat annoying (I hope not, but...). However, NPY_DECL_ALIGNED is also not defined in a fully compatible way, currently (i.e. also would need fixing).

With an anonymous struct, but also in general, I think it might be good to do this for all our structs maybe. And it would be nice to add static asserts that would tell us in case we ever break our assumptions.
(I think a static_assert(alignof(object_struct) <= 8/16) would do the trick. It is a bit more strict than needed, but that could be addressed if it ever breaks...)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

How about we embed the "public" fields here as an anonymous struct? I feel like that may be even cleaner (it doesn't encode the actual alignment guarantees yet, see below).
That is C11 but major compilers support it earlier, that might be somewhat annoying (I hope not, but...).

I think we have considered anonymous struct before but avoided that because it is incompatible with C++ and requires C11. See #31091 (comment) where it was discussed previously.

However, NPY_DECL_ALIGNED is also not defined in a fully compatible way, currently (i.e. also would need fixing).

Yeah, I'll fix it, thanks!

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I've added the asserts

@seberg seberg Jun 29, 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.

Thanks, I forgot about that and we do need C++ of course.

Three points, mostly because I felt they are worth noting/considering once:

  1. _Alignas is also C11, so either we require C11 for downstream for compilers other than gcc/clang/msvc, or we'd have to omit it and stop compiling for free-thread and opaque builds.
    I am not exactly sure how unreasonable C11 + fun compiler is?!
  2. Reducing alignment is a one way road for public fields (i.e. the asserts I asked for). We aren't reducing it on 32bit systems but are doing so on 64bit systems.
    I don't know that there is a realistic need for 16 byte aligned types public types but if we do this we can only add them by telling the compiler to not only align them to 8 bytes.
  3. As this is an ABI choice, it also (potentially) affects structs that are public but not exposed on opaque builds.
    I have not checked, I suspect this is only the scalars and I think that is fine. So this may mostly be about diffusing knowledge: Adding this alignment is vital for all new types we might want to expose on the opaque API (sure for many it does nothing but that isn't obvious).
    EDIT: I think we can make such structs public actually, but it is limited to future Python versions (i.e. right now we can for 3.15+ which is all there is anyway for opaque builds).

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

_Alignas is also C11, so either we require C11 for downstream for compilers other than gcc/clang/msvc, or we'd have to omit it and stop compiling for free-thread and opaque builds.
I am not exactly sure how unreasonable C11 + fun compiler is?!

I think it is reasonable to use C11 _Alignas because CPython itself require it for defining PyObject layout. https://github.com/python/cpython/blob/2670cb062c9ec31cd6df7be645f929a8398601c7/Include/pymacro.h#L65-L86

Reducing alignment is a one way road for public fields (i.e. the asserts I asked for). We aren't reducing it on 32bit systems but are doing so on 64bit systems.

I don't think we are actually reducing it on 64 bits, as noted in the CPython implementation, alignment can only be increased and the macros becomes no-op when it will reduce the alignment.

@seberg seberg Jun 29, 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.

I think it is reasonable to use C11 _Alignas because CPython itself require it for defining PyObject layout.

An nice, shouldn't we match CPython and add that #ifndef to allow compilers that don't do this right? (Not that I expect this is actually used.)

I don't think we are actually reducing it on 64 bits, as noted in the CPython implementation, alignment can only be increased and the macros becomes no-op when it will reduce the alignment.

This assumes that Python promises that on 64bit systems the size of PyObject_HEAD will always remain a multiple of 16. Unless that is a promise that Python is willing to make officially, I don't think we should rely on it.
(Sure, we could just add a static_assert in our code with a comment "if this fails we have to think about ABI" in practice.)

Sorry if I seem pedantic, but I feel we need to be very clear about what ABI decisions we are making here that we cannot change easily.

EDIT: I'll note that I think there are one or two more issues to think about. But I need a pause from quick iteration and probably Nathan will find them as well anyway.

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.

Sure, we could just add a static_assert in our code with a comment "if this fails we have to think about ABI" in practice.

IMO this is the most practical thing to do. There is overlap between the CPython team and the NumPy team (me and Kumar) and IMO we would be able to handle that static assert failing if it ever does come up in the future.

@kumaraditya303
kumaraditya303 marked this pull request as ready for review June 28, 2026 11:45
@kumaraditya303
kumaraditya303 requested a review from seberg June 28, 2026 11:46
@kumaraditya303

kumaraditya303 commented Jul 2, 2026

Copy link
Copy Markdown
Contributor Author

I've changed the code to only align the first field for 3.15+ as we discussed in community meeting. I tested it with https://github.com/kumaraditya303/np-test against 3.12 stable ABI extensions and 3.14t extensions and it passes on all of them.
See https://github.com/kumaraditya303/np-test/actions/runs/28570376628/job/84706582608 for the results.

Now since the alignment only affects 3.15+, we can also do 16 bytes alignment specifically for 64 bit versions for future proofing as @seberg suggested earlier, I have tested that at https://github.com/kumaraditya303/np-test/actions/runs/28571408746/job/84709836846 as that works fine as well. Ignore this part, I messed up the testing.

@seberg

seberg commented Jul 2, 2026

Copy link
Copy Markdown
Member

Now since the alignment only affects 3.15+, we can also do 16 bytes alignment specifically for 64 bit versions for future proofing as @seberg suggested earlier

Sorry, but this is winding me up: Can you explain what changed now?

About it: I was OK with just adding an assert to defer the decision to the future poor sods if it ever happens (hopefully us still, but who knows). However, this is not the assert we have.
Of course I would prefer to hear opinions from the specialists (your, maybe @mattip), because for new objects I would be tempted to just use alignof(max_align_t) to align with PEP 697, but it is correct that in practice it will probably never make a difference.
And because it'll probably never happen, I thought we agreed on just adding a static assert with a comment (not the one we have).

FWIW, I am wondering if we can't add a bit more docs around the core problem we have to ensure:

offsetof(struct_with_object_head.first_field) % alignof(struct_without_object_head) == 0

so that others don't have to think backwards from the bug condition. I am thinking there are likely cargo-culters and we spend enough time on this to leave more breadcrumbs for them/us (e.g. point out that for new objects sizeof(max_align_t) might make sense).

From there we can explain all of our choices:

  • historic constraints.
  • 64bit choice of 8 bytes over 16 bytes being "deferred" (or not). Because alignof(struct) is (a) none of our structs (except the longdouble scalar) need an alignment of more than 8 and (b) we added an assert to re-consider if sizeof(PyObject) changes and 16 would actually be different.
  • Ideally a brief note why possibly indicating an over-alignment of 8 seems unproblematic in the case that alignof(max_align_t) < 8. I expect it is because I don't think a malloc alignment smaller 8 is a thing and even if this happens we would have to change the padding strategy, the padding itself is OK.
    (The only slight "too bad" I can think of is that the padding may differ from that of PEP 967 on those builds, but we already have no choice but to differ on many 32bit platforms anyway...)

@kumaraditya303

kumaraditya303 commented Jul 2, 2026

Copy link
Copy Markdown
Contributor Author

Sorry, but this is winding me up: Can you explain what changed now?

Sorry for the noise, that comment is not correct. I messed up the testing ignore that part.

About it: I was OK with just adding an assert to defer the decision to the future poor sods if it ever happens (hopefully us still, but who knows). However, this is not the assert we have.
Of course I would prefer to hear opinions from the specialists (your, maybe @mattip), because for new objects I would be tempted to just use alignof(max_align_t) to align with PEP 697, but it is correct that in practice it will probably never make a difference.

Honestly given how difficult it is to get this right, I would prefer if for new objects, we do not expose it the way it is done here. For new objects you are correct and we should use max_align_t as you suggest but also I would suggest to not expose the struct at all in the ABI instead add functions to access the fields. We should also define a separate fields struct and object struct which embed the fields structs to be standard compliant, we couldn't do it for existing types because we did not want break extensions which access fields directly.

@kumaraditya303

Copy link
Copy Markdown
Contributor Author

Of course I would prefer to hear opinions from the specialists (your, maybe @mattip), because for new objects I would be tempted to just use alignof(max_align_t) to align with PEP 697, but it is correct that in practice it will probably never make a difference.

I mean something like this for new types:

struct NewNumpyStructFields {
    ... // Define the fields of the new struct here
};

struct NewNumpyStruct {
    PyObject_HEAD
    alignas(max_align_t) struct NewNumpyStructFields fields;
};

@seberg

seberg commented Jul 6, 2026

Copy link
Copy Markdown
Member

Review wise, there are still two things mentioned earlier that I am not sure we aligned on (maybe, maybe not); I may also be forgetting something, but hopefully nothing big:

  • Do we want a guard so we can support non-C11 niche compilers like Python does?
  • Maybe not for here, but previously we didn't have to worry about wanting to expose more structs. Now we do. If we don't add the padding now, it means abi3t builds that include a "future" struct . This may be nothing and maybe Nathan and you had already thought through this part? But if not, I would appreciate looking at a list (IMO minus scalars, I am not worried about them) to avoid forgetting something now that we may regret.
  • My changes need to be checked of course!

But, I guess I'll just let you and Nathan mop that up unless you wonder e.g. if I think a struct should get the padding.

@ngoldbaum

Copy link
Copy Markdown
Member

Thanks @kumaraditya303!

@ngoldbaum
ngoldbaum merged commit 009c609 into numpy:main Jul 8, 2026
87 checks passed
@kumaraditya303
kumaraditya303 deleted the 32bit-crash branch July 8, 2026 19:05
@charris charris removed the 09 - Backport-Candidate PRs tagged should be backported label Jul 10, 2026
charris pushed a commit that referenced this pull request Jul 10, 2026
Co-authored-by: Sebastian Berg <sebastianb@nvidia.com>
Co-authored-by: Nathan Goldbaum <nathan.goldbaum@gmail.com>
charris added a commit that referenced this pull request Jul 10, 2026
BUG: fix crash on 32 bit systems using abi3t (#31771)
Icxolu added a commit to Icxolu/rust-numpy that referenced this pull request Jul 10, 2026
Icxolu added a commit to Icxolu/rust-numpy that referenced this pull request Jul 11, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

00 - Bug 63 - C API Changes or additions to the C API. Mailing list should usually be notified.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

BUG: descriptor accessors should account for struct padding on 32bit abi3t builds

4 participants