Skip to content

Popen.__getattribute__(): don't recurse if __init__() failed - #3000

Open
istoolsfox wants to merge 1 commit into
giampaolo:masterfrom
istoolsfox:fix/popen-getattribute-recursion
Open

istoolsfox wants to merge 1 commit into
giampaolo:masterfrom
istoolsfox:fix/popen-getattribute-recursion

Conversation

@istoolsfox

Copy link
Copy Markdown

Summary

Description

This one has been open since 2017, and Cykooz had the mechanism figured out back then already: when Popen.__init__() fails (say the executable does not exist), the instance never gets its __subproc attribute. The fallback in __getattribute__() then reads it with the usual attribute syntax, which re-enters __getattribute__() and recurses until the stack blows. His last comment in the thread is essentially this fix, it just never turned into a PR, and the recursion is still there on master today.

You can only hit it by touching the half-initialized instance, which is probably why it survived: normal code gets the FileNotFoundError from the constructor and moves on. The 2017 report hit it through raven walking frame locals and calling repr() on the object. The other way in is a subclass that swallows the error and uses the instance later:

class Popen(psutil.Popen):
    def __init__(self, *args, **kwargs):
        try:
            super().__init__(*args, **kwargs)
        except OSError:
            pass  # handle later

p = Popen(["not-an-executable"])
p.poll()  # RecursionError before this patch, AttributeError after

The fix is the direct lookup Cykooz sketched: object.__getattribute__(self, '_Popen__subproc'), with the mangled name spelled out so the fallback no longer re-enters the method.

Nothing else changes on purpose. The constructor still raises the original FileNotFoundError. A working instance delegates exactly like before (the fallback actually saves one dispatch now). Touching a broken instance gives you the AttributeError the existing handler already produces, instead of a stack trace. I saw your note from 2017 about the class-attribute variant surfacing AttributeError where you would expect FileNotFoundError; this patch stays away from that and does not try to decide what these operations should raise on a broken instance, it only stops the stack from blowing. If you would rather see something else raised there, happy to adjust.

TestPopen.test__getattribute__failed_init reproduces the scenario from the issue and fails with RecursionError on master.

If Popen.__init__() raises (e.g. the executable does not exist), the
half-initialized instance has no _Popen__subproc attribute. The
fallback branch of __getattribute__() accessed it via attribute syntax
(self.__subproc), which re-enters __getattribute__() and recurses
until the stack is exhausted. Normal usage never notices, because the
exception from __init__() propagates and the instance is discarded,
but any code introspecting the broken instance (e.g. Sentry-style
frame walking, issue giampaolo#1121) or a subclass swallowing the original
error gets a RecursionError instead of a bounded exception.

Look _Popen__subproc up via object.__getattribute__() instead, which
does not re-enter this method. What __init__() raises is unchanged;
delegation for successfully initialized instances behaves the same,
with one dispatch less in the fallback.
@github-actions github-actions Bot added bug type critical process dies, hangs forever, or memory is corrupt: segfault, deadlock, double free, crash on import labels Sep 15, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug type critical process dies, hangs forever, or memory is corrupt: segfault, deadlock, double free, crash on import

Projects

None yet

Development

Successfully merging this pull request may close these issues.

RecursionError after exception in the psutil.Popen.__init__()

1 participant