Skip to content

gh-134144: Fix use-after-free in zapthreads() #134145

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 5 commits into
base: main
Choose a base branch
from

Conversation

b-pass
Copy link

@b-pass b-pass commented May 17, 2025

Fixes #134144. This problem was introduced by #127077 which added _Py_FOR_EACH_TSTATE_UNLOCKED.

_Py_FOR_EACH_TSTATE_UNLOCKED reads the tstate at the end of each loop iteration to get the next value. When the loop body frees the tstate, the result is always a use-after-free. It doesn't always crash (seems rare, actually), I guess it just depends what the allocator does with those pages during the rest of the tstate deletion.

Can't use the _Py_FOR_EACH_TSTATE_UNLOCKED macro because it will reference the just-deleted tstate in order to get the next value.
@b-pass b-pass requested a review from ericsnowcurrently as a code owner May 17, 2025 15:02
@python-cla-bot
Copy link

python-cla-bot bot commented May 17, 2025

All commit authors signed the Contributor License Agreement.

CLA signed

@bedevere-app
Copy link

bedevere-app bot commented May 17, 2025

Most changes to Python require a NEWS entry. Add one using the blurb_it web app or the blurb command-line tool.

If this change has little impact on Python users, wait for a maintainer to apply the skip news label instead.

@bedevere-app
Copy link

bedevere-app bot commented May 17, 2025

Most changes to Python require a NEWS entry. Add one using the blurb_it web app or the blurb command-line tool.

If this change has little impact on Python users, wait for a maintainer to apply the skip news label instead.

@ericsnowcurrently ericsnowcurrently added the needs backport to 3.14 bugs and security fixes label May 17, 2025
Copy link
Member

@ZeroIntensity ZeroIntensity left a comment

Choose a reason for hiding this comment

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

I'm pretty sure we can drop zapthreads entirely, because right now the assumption is that the interpreter doesn't have any threads left. Let's hold off on this until we come to a decision on what the actual behavior should be for dangling threads.

Copy link
Member

@ericsnowcurrently ericsnowcurrently left a comment

Choose a reason for hiding this comment

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

LGTM

We just need a NEWS entry.

@ZeroIntensity, regarding gh-128640, I expect we'll do that too since it solves an additional problem. However, this PR is a good idea regardless.

@ZeroIntensity
Copy link
Member

ZeroIntensity commented May 17, 2025

We need a test as well, but I'm more worried about the use-case as given in the issue. zapthreads will arbitrarily destroy a thread state (one created by PyGILState_Ensure in the repro), which is a recipe for crashes if the thread is still running.

@b-pass
Copy link
Author

b-pass commented May 17, 2025

We need a test as well, but I'm more worried about the use-case as given in the issue. zapthreads will arbitrarily destroy a thread state (one created by PyGILState_Ensure in the repro), which is a recipe for crashes if the thread is still running.

Sorry, the issue description isn't great. The thread wasn't alive, but Py_EndInterpreter takes a PyThreadState* as the argument (not a PyInterpreterState*), so there must always been one on the interpreter. But the fact is this code ALWAYS does a use-after-free (even if there was only ever one thread state). Here is some pseudo code of what is going on:

for (PyThreadState *tstate = interp->threads.head; 
       tstate != NULL; 
       tstate = tstate->next)
{
   free(tstate);
}

@ZeroIntensity
Copy link
Member

Right, I've noticed this before. In most cases, we get lucky because the initial thread is statically allocated on the interpreter state, so it doesn't actually cause UAF. It becomes an issue with something like PyGILState_Ensure because we have more than one thread state, but the problem is that's unsupported anyway.

@b-pass
Copy link
Author

b-pass commented May 17, 2025

It becomes an issue with something like PyGILState_Ensure because we have more than one thread state, but the problem is that's unsupported anyway.

It happens without the Ensure. I've updated the linked issue with a complete program (without an Ensure). So are you saying it's not supported to use a different PyThreadState* to end the interpreter than the one returned from its creation?

@ZeroIntensity
Copy link
Member

ZeroIntensity commented May 17, 2025

So are you saying it's not supported to use a different PyThreadState* to end the interpreter than the one returned from its creation?

Yes, and you can't have any additional threads when you call Py_EndInterpreter.

I'm fine with this fix, but my point is that it's not enough. Let's add a test and then I'll approve.

@ericsnowcurrently
Copy link
Member

Yeah, a test would be nice.

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.

Cannot safely Py_EndInterpreter in 3.14b1
4 participants