Skip to content

gh-158364: Don't report other interpreters' threads in sys._current_f… - #158369

Open
Himesh-rupchandani wants to merge 1 commit into
python:mainfrom
Himesh-rupchandani:gh-158364
Open

Himesh-rupchandani wants to merge 1 commit into
python:mainfrom
Himesh-rupchandani:gh-158364

Conversation

@Himesh-rupchandani

@Himesh-rupchandani Himesh-rupchandani commented Sep 28, 2026 •

Copy link
Copy Markdown

…rames()

sys._current_frames() materialized a PyFrameObject for every thread of every interpreter. A frame object created for a thread of another interpreter belongs to that interpreter: it is stored in its _PyInterpreterFrame.frame_obj and deallocated when that interpreter pops the frame, while the calling interpreter holds the reference from the returned dict and drops it at some arbitrary later time. Since PEP 684 gives each interpreter its own obmalloc arenas, the block ends up being freed by a different interpreter than the one that allocated it, which corrupts the heap and typically aborts the process inside free().

sys._current_exceptions() has the same problem: it hands out references to exception objects owned by other interpreters.

Both functions now only report the threads of the calling interpreter. This matches PyUnstable_DumpTracebackThreads() (used by faulthandler), which already only dumps the threads of one interpreter. As a consequence, only the current interpreter's world needs to be stopped.

@read-the-docs-community

read-the-docs-community Bot commented Sep 28, 2026 •

Copy link
Copy Markdown

Documentation build overview

📚 cpython-previews | 🛠️ Build #34818209 | 📁 Comparing e05c195 against main (3330712)

  🔍 Preview build  

2 files changed
± library/sys.html
± whatsnew/changelog.html

Comment thread Doc/library/sys.rst Outdated
Comment on lines +274 to +280
.. versionchanged:: 3.16
Only the threads of the current interpreter are included. Previously
the threads of other interpreters were reported as well, but the frame
objects created for them belonged to those interpreters, which could
corrupt the heap (each interpreter has its own memory arenas) and crash
the process.

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 don't think this is worth documenting; nobody really cares that this used to crash.

Comment thread Doc/library/sys.rst Outdated
Comment on lines +298 to +301
.. versionchanged:: 3.16
Only the threads of the current interpreter are included, as for
:func:`_current_frames`.

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.

Ditto

Comment thread Lib/test/test_sys.py Outdated
Comment on lines +571 to +575
# gh-158364: sys._current_frames() must not report the threads of
# other interpreters. A frame object materialized for such a thread
# belongs to that interpreter, and since each interpreter has its own
# memory arenas, it would be deallocated by a different interpreter
# than the one that allocated it, corrupting the heap.

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 comment is really long. Let's just say something like gh-158364: sys._current_frames() would access frames of another interpreter and crash

Comment thread Lib/test/test_sys.py Outdated
# than the one that allocated it, corrupting the heap.
import threading

# Park a thread of *this* interpreter at a known place.

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.

Not necessary.

Suggested change
# Park a thread of *this* interpreter at a known place.

Comment thread Lib/test/test_sys.py Outdated
Comment on lines +579 to +580
entered_g = threading.Event()
leave_g = threading.Event()

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.

What is the _g suffix? Let's just call these entered and left (leave is present tense).

Comment thread Python/pystate.c Outdated
Comment on lines +2795 to +2797

_Py_EnsureTstateNotNULL(tstate);

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 has a non-zero cost, since it performs an actual NULL check at runtime. It's also not related to this fix, so let's remove it:

Suggested change
_Py_EnsureTstateNotNULL(tstate);

Comment thread Python/pystate.c Outdated
Comment on lines +2811 to +2822
*
* Threads of other interpreters are not included (gh-158364). A frame
* object materialized for such a thread belongs to that interpreter: it
* is stored in its _PyInterpreterFrame.frame_obj and deallocated when
* that interpreter pops the frame, while the caller drops the reference
* held by the returned dict at some arbitrary later time. Since PEP 684
* gives each interpreter its own obmalloc arenas, the block can be freed
* by a different interpreter than the one that allocated it, corrupting
* the heap. Handing out such a frame is unsafe for other reasons too: it
* holds references to the other interpreter's objects, and that
* interpreter may be finalized while the caller still holds the frame.
* Code running in each interpreter sees its own 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.

This comment isn't helpful to anyone reading this in the future. If all we did was delete code, there's no reason to tell them that.

Comment thread Python/pystate.c Outdated
Comment on lines +2885 to +2889
*
* Threads of other interpreters are not included, for the same reason as
* in _PyThread_CurrentFrames(): the returned dict would hold a reference
* to an exception owned by another interpreter, which can then be
* deallocated by whichever interpreter drops it last (gh-158364).

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.

Same thing here. Let's get rid of this.

Comment on lines +1 to +8
Fix heap corruption (typically an abort or crash in the C library's
``free()``) when :func:`sys._current_frames` or
:func:`sys._current_exceptions` is called while another interpreter is
running. Both functions no longer report the threads of other
interpreters: a frame object materialized for such a thread belongs to
that interpreter, and since each interpreter has its own memory arenas it
was being deallocated by a different interpreter than the one that
allocated it.

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 contains a lot of technical information that isn't relevant to changelog readers. We need to say what was fixed, not how:

Suggested change
Fix heap corruption (typically an abort or crash in the C library's
``free()``) when :func:`sys._current_frames` or
:func:`sys._current_exceptions` is called while another interpreter is
running. Both functions no longer report the threads of other
interpreters: a frame object materialized for such a thread belongs to
that interpreter, and since each interpreter has its own memory arenas it
was being deallocated by a different interpreter than the one that
allocated it.
Fix crash when :func:`sys._current_frames` or
:func:`sys._current_exceptions` is called while another interpreter is
running.

Comment thread Lib/test/test_sys.py Outdated
Comment on lines +592 to +593
interp.exec(textwrap.dedent(f'''
import sys

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.

Up to you, but it's usually easier to just do if True: instead of textwrap.dedent.

@Himesh-rupchandani

Copy link
Copy Markdown
Author

Thanks for the review! Force-pushed with all of it addressed:

Both versionchanged blocks are gone — Doc/library/sys.rst is untouched now.
NEWS entry shortened to your wording.
Dropped the _Py_EnsureTstateNotNULL() call and both long comments from
_PyThread_CurrentFrames() / _PyThread_CurrentExceptions(). The loop
comments now just say "the current interpreter's thread states" to match the
new loop.
Deleted test_current_frames_subinterpreter_thread.
Both remaining tests now use threading_helper.start_threads(..., unlock=left.set), wait without timeouts, use entered/left events, and
if True: instead of textwrap.dedent.
The PR is now 3 files, +114/-52.

@Himesh-rupchandani

Copy link
Copy Markdown
Author

A thread only shows up in sys._current_exceptions() with something other
than None while it is actively handling an exception, so the thread has to
be parked inside an except block — otherwise the subinterpreter's call has
nothing of ours to hand out and the test doesn't exercise the broken path.
entered/left just keep it there while the subinterpreter samples. I've
added a one-line comment saying so.

…rent_frames()

sys._current_frames() materialized a PyFrameObject for every thread of
every interpreter.  A frame object created for a thread of another
interpreter belongs to that interpreter: it is stored in its
_PyInterpreterFrame.frame_obj and deallocated when that interpreter pops
the frame, while the calling interpreter holds the reference from the
returned dict and drops it at some arbitrary later time.  Since PEP 684
gives each interpreter its own obmalloc arenas, the block ends up being
freed by a different interpreter than the one that allocated it, which
corrupts the heap and typically aborts the process inside free().

sys._current_exceptions() has the same problem: it hands out references
to exception objects owned by other interpreters.

Both functions now only report the threads of the calling interpreter.
This matches PyUnstable_DumpTracebackThreads() (used by faulthandler),
which already only dumps the threads of one interpreter.  As a
consequence, only the current interpreter's world needs to be stopped.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants