Skip to content

dir() can crash with cyclic __bases__ #155452

Open

Description

@BHUVANSH855

Crash report

What happened?

dir() can cause a native stack overflow when an object's __class__
provides a cyclic __bases__ attribute.

Minimal reproducer:

class Fake:
    pass

a = Fake()
a.__bases__ = (a,)

class C:
    @property
    def __class__(self):
        return a

print("entering", flush=True)
dir(C())
print("survived", flush=True)

Observed Result

On CPython 3.16.0a0 built from commit 5107fd700d7:

entering
timeout: the monitored command dumped core
Segmentation fault
rc=139

The crash occurs because object.__dir__() obtains the object's __class__ and merge_class_dict() recursively traverses __bases__ without a recursion guard. A cyclic __bases__ therefore causes unbounded native recursion and eventually a SIGSEGV.

The analogous __bases__ traversal in abstract_issubclass() already uses _Py_EnterRecursiveCall().

I also verified that adding a recursion guard to merge_class_dict() changes the failure from a native crash to a catchable exception:

RecursionError: Stack overflow (used 8120 kB) in __bases__

A regression test covering the cyclic __bases__ case was also added to Lib/test/test_builtin.py and passes with:

./python -m test test_builtin -m test_dir
== Tests result: SUCCESS ==
1 test OK.

CPython versions tested on:

CPython main branch

Operating systems tested on:

Linux

Output from running 'python -VV' on the command line:

Python 3.16.0a0 (heads/investigate-0071-dir-cyclic-bases-dirty:5107fd700d7, Aug 9 2026, 21:10:20) [GCC 13.3.0]

Linked PRs

Activity

  1. added
    type-crashA hard crash of the interpreter, possibly with a core dump
    on Aug 9, 2026
  2. ZeroIntensity commented on Aug 10, 2026

    @ZeroIntensity
    Member

    I reiterate every single word of what I said here: #155102 (comment)

    This is an absolutely ridiculous case to fix. It's already possible to crash the interpreter with stack overflows and recursion, and I don't think we should give the impression that we want to support reference cycles in __bases__.

  3. added
    pendingThe issue will be closed if no feedback is provided
    on Aug 10, 2026
  4. skirpichev commented on Aug 10, 2026

    @skirpichev
    Member

    This is an absolutely ridiculous case to fix.

    Yet it's a crash (doesn't happen on PyPy and GraalPy). We are trying to make it not possible to segfault the interpreter with pure python code.

    I think we can accept fix, if it's reasonably small. Which seems to be the case.

    @picnixz ?

  5. ZeroIntensity commented on Aug 10, 2026

    @ZeroIntensity
    Member

    We are trying to make it not possible to segfault the interpreter with pure python code.

    Says who? There are plenty of known ways to crash the interpreter with pure Python code. Some are fixable, yes, but it's often not worth it. Practicality beats purity.

    As I said in the other issue, Python has a limited number of reviewers and maintainers. We shouldn't waste it on reviewing (artificial; don't occur in practice) AI-generated issues and PRs, and by accepting this, we're encouraging that. Before dealing with problems like this, let's deal with our other 7 thousand issues that are actually affecting Python users.

  6. picnixz commented on Aug 10, 2026

    @picnixz
    Member

    I'm neutral here but lean towards -0.5. Considering we already have a recursive guard in abstract_issubclass, I don't mind having one here although this case is much more niche. Is the recursive guard guarding the same issue (that is, without that guard, would we crash when doing issubclass()?) If yes, I'm ok with adding a recursive guard when using __bases__ in general. If not, then I don't think it's worth it.

    We try to avoid fixing convoluted cases such as messing with code.__replace__, and I also think messing up with __bases__ is ... somehow similar.

  7. skirpichev commented on Aug 10, 2026

    @skirpichev
    Member

    by accepting this, we're encouraging that.

    I disagree. Each issue should be considered separately.

    Is the recursive guard guarding the same issue (that is, without that guard, would we crash when doing issubclass()?)

    Yes, outcome will be same. For a history: #74755

  8. BHUVANSH855 commented on Aug 10, 2026

    @BHUVANSH855
    ContributorAuthor

    I looked into #74755 / GH-29048. The recursion issue is essentially the same: both paths recursively walk user-controlled __bases__ in C.

    GH-29048 fixed the issubclass() case with _Py_EnterRecursiveCall() / _Py_LeaveRecursiveCall(). merge_class_dict() has the same kind of recursive __bases__ traversal, and without a guard it can overflow the C stack and segfault.

    I tested direct and indirect cycles, as well as finite and empty bases. Cycles now raise RecursionError, while normal hierarchies still work. The full test_builtin suite also passes.

    So this isn't about supporting cyclic __bases__; the guard just prevents an invalid hierarchy from crashing the interpreter. Given the existing issubclass() precedent, I think this is a reasonable small fix.

  9. picnixz commented on Aug 10, 2026

    @picnixz
    Member

    I think this one is legitimate in this case because of that precedent. After thinking a bit more, __bases__ is pretty much accessible in type.__new__ so if someone defines its own __new__ but passes an incorrect 'bases', then it could be easy to have such crash (same with custom __metaclass__).

    I also think people are also changing __bases__ in the wild and it would be nasty to have a stack overflow that kills your program (or even your container) [I forgot to post my comment]

  10. removed
    pendingThe issue will be closed if no feedback is provided
    on Aug 10, 2026
  11. added a commit that references this issue on Aug 10, 2026
  12. ZeroIntensity commented on Aug 10, 2026

    @ZeroIntensity
    Member

    I looked into #74755 / #29048. The recursion issue is essentially the same: both paths recursively walk user-controlled bases in C.

    #74755 is different because it occurred naturally. It did not involve overriding an object's __class__ with an arbitrary object that happens to have __bases__ defined. This was the repro for that issue:

    >>> class Foo(object):
    ...  def __getattr__(self, attr):
    ...   return (self, None)
    ... 
    >>> issubclass(Foo(), int)
    Segmentation fault (core dumped)

    After thinking a bit more, bases is pretty much accessible in type.new so if someone defines its own new but passes an incorrect 'bases', then it could be easy to have such crash (same with custom metaclass).

    How? This issue is only possible if __bases__ manages to reference the type it's defined for -- which makes no sense, because a type cannot be its own base! The only way to achieve this is with weird hacks like the __class__ descriptor shown in this issue.

    Moreover, we explicitly disallow reference cycles in __bases__ for most things:

    >>> class Test:
    ...     pass
    ...     
    >>> Test.__bases__ = (Test,)
    Traceback (most recent call last):
      File "<python-input-1>", line 1, in <module>
        Test.__bases__ = (Test,)
        ^^^^^^^^^^^^^^
    TypeError: a __bases__ item causes an inheritance cycle

    I don't see why dir() should be any different.

  13. BHUVANSH855 commented on Aug 10, 2026

    @BHUVANSH855
    ContributorAuthor

    I think the distinction here is that this does not require a cycle in an actual type's inheritance hierarchy.

    merge_class_dict() doesn't require aclass or the objects returned from __bases__ to be types. object.__dir__() gets self.__class__ dynamically and passes that object to merge_class_dict(), which then gets its __bases__ attribute and recursively processes each item.

    In the reproducer, a is just a normal Python object:

    >>> a = Fake()
    >>> a.__bases__ = (a,)
  14. ZeroIntensity commented on Aug 10, 2026

    @ZeroIntensity
    Member

    merge_class_dict() doesn't require aclass or the objects returned from bases to be types. object.dir() gets self.class dynamically and passes that object to merge_class_dict(), which then gets its bases attribute and recursively processes each item.

    Arguably, that's the bug. __class__ should have to be a type, and so should the items inside __bases__.

  15. picnixz commented on Aug 10, 2026

    @picnixz
    Member

    I'm sorry but defining __class__ is still common:

    I'm not saying it's niche. I'm saying that I would definitely don't want my interpreter AND my laptop to crash because I did something wrong in my tests or if I incorrectly set my __bases__. There are legitimate usages out there for that as well though I agree not much (only one I could find was https://github.com/ethanfurman/aenum/blob/777c33248c0b12458a6059bc4926485b05b2e13c/aenum/_tuple.py#L311-L314, but I'm pretty sure people are anyway hacking their own __bases__ for obscure reasons, whether it's to fake inheritance or for something else. And making a typo is easy and in this case, I don't want my laptop to freeze to death because of stack exhaustion).


    Thta being said, I'd like to say that we did add stack protection so I find it weird not to also protect this kind of issues, namely we protect explicitly against C.__class__ = (C,) but not c.__class__ = (c,). OTOH, I remember #58218 which was... more or less the same though a bit different and we actually usually (and I also did so in the past) decide to ignore those kind of errors. But Mark said that since we now have stack protection we should fix those cases and I don't know if this falls under this.

    cc @markshannon

  16. ZeroIntensity commented on Aug 10, 2026

    @ZeroIntensity
    Member

    All the cases you linked are defining __class__ as returning a type, which is perfectly valid. You can't make a mistake there because the setter for type.__bases__ checks that there aren't cycles. This only ever comes up if you return a non-type object from __class__ that also defines __bases__, which shouldn't ever occur outside of fuzzing.

  17. deadlovelll commented on Aug 13, 2026

    @deadlovelll
    Contributor

    Not about the fix itself, but the topic overlaps very closely

    As i saw in the discussion, there are differing viewpoints on what constitutes a valid type-crash. The devguide describes type-crash only as "for hard crashes of the interpreter, possibly with a core dump".

    Wouldn't it be a better option to describe the scope in triage/triaging.rst (but may be any other file as well)? To specify what falls into this category, what doesn't, and how edge cases are evaluated? Mostly so reviewer can link to it instead of re-deriving it per issue

  18. ZeroIntensity commented on Aug 13, 2026

    @ZeroIntensity
    Member

    It's not that we disagree on whether this is a crash; we disagree on whether it's worth fixing.

  19. skirpichev commented on Sep 3, 2026

    @skirpichev
    Member

    It seems we have no strong support for this issue from core devs. One seems slightly positive and one strongly negative.

    I suggest OP to provide a practical example. Else, lets wait a bit for Mark reply or close issue, if he abstain.

  20. added
    pendingThe issue will be closed if no feedback is provided
    on Sep 3, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    interpreter-core(Objects, Python, Grammar, and Parser dirs)pendingThe issue will be closed if no feedback is providedtype-crashA hard crash of the interpreter, possibly with a core dump

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions