You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
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]
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__.
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.
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.
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.
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]
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:
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:
>>> classTest:
... 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 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:
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__.
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.
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.
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
Activity
ZeroIntensity commentedon Aug 10, 2026
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__.skirpichev commentedon Aug 10, 2026
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 ?
ZeroIntensity commentedon Aug 10, 2026
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.
picnixz commentedon Aug 10, 2026
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.skirpichev commentedon Aug 10, 2026
I disagree. Each issue should be considered separately.
Yes, outcome will be same. For a history: #74755
BHUVANSH855 commentedon Aug 10, 2026
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 fulltest_builtinsuite also passes.So this isn't about supporting cyclic
__bases__; the guard just prevents an invalid hierarchy from crashing the interpreter. Given the existingissubclass()precedent, I think this is a reasonable small fix.picnixz commentedon Aug 10, 2026
I think this one is legitimate in this case because of that precedent. After thinking a bit more,
__bases__is pretty much accessible intype.__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]pythongh-155452: Skip dir recursion test on limited stacks
ZeroIntensity commentedon Aug 10, 2026
#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: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:I don't see why
dir()should be any different.BHUVANSH855 commentedon Aug 10, 2026
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 requireaclassor the objects returned from__bases__to be types.object.__dir__()getsself.__class__dynamically and passes that object tomerge_class_dict(), which then gets its__bases__attribute and recursively processes each item.In the reproducer,
ais just a normal Python object:ZeroIntensity commentedon Aug 10, 2026
Arguably, that's the bug.
__class__should have to be a type, and so should the items inside__bases__.picnixz commentedon Aug 10, 2026
I'm sorry but defining
__class__is still common:cpython/Lib/unittest/mock.py
Lines 601 to 605 in 63bf95b
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 notc.__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
ZeroIntensity commentedon Aug 10, 2026
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 fortype.__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.deadlovelll commentedon Aug 13, 2026
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 describestype-crashonly 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 issueZeroIntensity commentedon Aug 13, 2026
It's not that we disagree on whether this is a crash; we disagree on whether it's worth fixing.
skirpichev commentedon Sep 3, 2026
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.