Skip to content

BUG: Refactor error handling in array_setstate to prevent typecode leak - #31701

Merged
seberg merged 3 commits into
numpy:mainfrom
whyvineet:fix-setstate-typecode-leak
Jun 23, 2026
Merged

seberg merged 3 commits into
numpy:mainfrom
whyvineet:fix-setstate-typecode-leak

Conversation

@whyvineet

Copy link
Copy Markdown
Contributor

PR summary

This fixes the reference-count imbalance reported in #31678

AI Disclosure

No AI tools used

@da-woods da-woods 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 is a fairly complicated function so it isn't too easy to follow the flow and all the ownership. But I find it easier to reason about if you have fewer exit points and fewer paths through.

That typically means that an owned reference should either be NULL or set. References shouldn't change between being owned or unowned (like rawdata does).

So you'd have something like

PyObject *result = NULL, *typecode = NULL, *rawdata = NULL;

// ... code goes here ...

result = final_thing_that_creates_the_result();
end:
Py_XDECREF(typecode);  // either NULL, or owned, XDECREF is happy both ways
Py_XDECREF(rawdata);
return result; // either NULL or set...

and then you have one exit point and everything goes through that. Every return should go through the end (except possibly right at the start of the function before anything is owned).

This probably requires taking ownership of both typecode and rawdata with a Py_INCREF immediately after PyArg_ParseTuple.

Comment thread numpy/_core/src/multiarray/methods.c Outdated

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 seems like it might still be leaking typecode

Comment thread numpy/_core/src/multiarray/methods.c Outdated

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.

and here?

Comment thread numpy/_core/src/multiarray/methods.c Outdated

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.

and here?

@whyvineet

Copy link
Copy Markdown
Contributor Author

I agree @da-woods; the current goto chain does not handle the shared failure paths very well. Using a single exit point for cleanup would make ownership easier to understand.

From what I understand, the goal is to keep local references consistent, either owned or NULL, and send all error paths through a shared cleanup block with Py_XDECREF. This avoids switching between borrowed and owned references in the function.

I'll take a look at restructuring the ownership and cleanup logic along those lines.

@da-woods da-woods 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.

A few small comments (only one of which is a real error I think) but to me that looks much more likely to work right.

Comment thread numpy/_core/src/multiarray/methods.c Outdated
Comment on lines 2254 to 2260
* Direct pointer path: PyArray_SetBaseObject steals the rawdata
* reference. NULL rawdata afterward so Py_XDECREF at the end
* label does not double-decref a stolen ref.
*/
fa->mem_handler = NULL;
fa->data = datastr;
if (PyArray_SetBaseObject(self, rawdata) < 0) {

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.

Most reference steeling functions (including PyArray_SetBaseObject it looks like) steal a reference on both success and failure to be consistent

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’ll recheck the ownership contract here... and adjust the cleanup accordingly

Comment thread numpy/_core/src/multiarray/methods.c Outdated
Comment on lines +2227 to +2228
Py_DECREF(fa->descr);
fa->descr = swapped;

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.

You might be better doing Py_SETREF(fa->descr, swapped) (because can in principle cause arbitrary code to execute so you're leaving it in an potentially invalid state while that happens).

I don't think it can actually happen here, but I think it's a good idea anyway

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.

Okay... I’ll switch this assignment.

Comment thread numpy/_core/src/multiarray/methods.c Outdated
Comment on lines +2242 to +2243
Py_DECREF(fa->descr);
fa->descr = swapped;

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.

And here with Py_SETREF

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.

Here as well...

@whyvineet

Copy link
Copy Markdown
Contributor Author

I looked at how PyArray_SetBaseObject() is implemented in arrayobject.c. This function takes ownership of the reference and decreases it if there is a failure, so the local cleanup does not need to handle that reference after the call.

I changed the code to transfer ownership before calling PyArray_SetBaseObject() by clearing the local rawdata reference. This keeps the single-exit cleanup safe.

Could you take another look, @da-woods? Does this match what you had in mind?

@da-woods

Copy link
Copy Markdown
Contributor

I changed the code to transfer ownership before calling PyArray_SetBaseObject() by clearing the local rawdata reference. This keeps the single-exit cleanup safe.

Could you take another look, @da-woods? Does this match what you had in mind?

Yes that looks right to me.

@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 @whyvineet and especially also @da-woods for reviewing. This is a bit of a dark corner, the fix is I think in a sense meaningless (you would need a bad pickle to hit it and at that point anything is possible).

(We use finish: more often than end: but I don't feel like style nitpicking, let's get it in.)

@seberg
seberg merged commit 123678e into numpy:main Jun 23, 2026
84 of 87 checks passed
@charris charris added the 09 - Backport-Candidate PRs tagged should be backported label Jun 23, 2026
@charris charris changed the title FIX: Refactor error handling in array_setstate to prevent typecode leak BUG: Refactor error handling in array_setstate to prevent typecode leak Jun 23, 2026
@charris charris removed the 09 - Backport-Candidate PRs tagged should be backported label Jul 2, 2026
charris added a commit that referenced this pull request Jul 3, 2026
FIX: Refactor error handling in array_setstate to prevent typecode leak (#31701)
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.

4 participants