Skip to content

Segfault by calling repr(SimpleNamespace) with typing.Union attributes in threads on a free-threading build #135878

@devdanzin

Description

@devdanzin
Contributor

Crash report

What happened?

It's possible to segfault or abort the interpreter of a free-threading build by calling repr(SimpleNamespace) in threads when the instance has typing.Union attributes. This seems very similar to #132713.

The MRE will rarely hang and is non-deterministic, but is the best I was able to come up with.

MRE:

from functools import reduce
from operator import or_
from threading import Thread

from types import SimpleNamespace

all_types = []
for x in range(400):
    class Dummy: pass
    all_types.append(Dummy)
big_union = reduce(or_, all_types, int)

for x in range(500):
    s = SimpleNamespace()

    def stress_simplenamespace():
        for x in range(20):
            varying_union = big_union | tuple[int, ...] | list[str, ...] | dict[str, int]
            repr(s)
            attr_name = f"t_attr_{x}"
            setattr(s, attr_name, varying_union)
            repr(s)
            repr(s)
            repr(s)

    alive = [Thread(target=stress_simplenamespace) for x in range(8)]
    for t in alive:
        t.start()
    for t in alive:
        t.join()

Segfault backtrace 1:

Thread 7 "Thread-6 (stres" received signal SIGSEGV, Segmentation fault.
[Switching to Thread 0x7ffff541b640 (LWP 348490)]
0x000055555567cdcd in setup_ga (args=<unknown at remote 0x7ffff5c20640>, origin=<type at remote 0x555555b11d40>, alias=0x2276a0e0140) at Objects/genericaliasobject.c:837
837         if (!PyTuple_Check(args)) {

#0  0x000055555567cdcd in setup_ga (args=<unknown at remote 0x7ffff5c20640>, origin=<type at remote 0x555555b11d40>, alias=0x2276a0e0140)
    at Objects/genericaliasobject.c:837
#1  Py_GenericAlias (origin=<type at remote 0x555555b11d40>, args=<unknown at remote 0x7ffff5c20640>) at Objects/genericaliasobject.c:1015
#2  0x0000555555658dd5 in _PyObject_VectorcallTstate (kwnames=0x0, nargsf=9223372036854775809, args=0x7ffff541a428,
    callable=<built-in method __class_getitem__ of type object at remote 0x555555b11d40>, tstate=0x555555c3c6e0)
    at ./Include/internal/pycore_call.h:169
#3  PyObject_CallOneArg (func=<built-in method __class_getitem__ of type object at remote 0x555555b11d40>,
    arg=arg@entry=<unknown at remote 0x7ffff5c20640>) at Objects/call.c:395
#4  0x0000555555634781 in PyObject_GetItem (o=<type at remote 0x555555b11d40>, key=<unknown at remote 0x7ffff5c20640>) at Objects/abstract.c:193
#5  0x00005555555e2f02 in _PyEval_EvalFrameDefault (tstate=<optimized out>, frame=<optimized out>, throwflag=<optimized out>)
    at Python/generated_cases.c.h:62
#6  0x00005555557dd13e in _PyEval_EvalFrame (throwflag=0, frame=<optimized out>, tstate=0x555555c3c6e0) at ./Include/internal/pycore_ceval.h:119
#7  _PyEval_Vector (tstate=0x555555c3c6e0, func=0x2275e37fd80, locals=0x0, args=0x7ffff541a8a8, argcount=<optimized out>, kwnames=<optimized out>)
    at Python/ceval.c:1975

Segfault backtrace 2:

Thread 3 "Thread-2 (stres" received signal SIGSEGV, Segmentation fault.
[Switching to Thread 0x7ffff742f640 (LWP 343364)]
clear_freelist (dofree=<optimized out>, is_finalization=<optimized out>, freelist=<optimized out>) at Objects/object.c:904
904             dofree(ptr);

#0  clear_freelist (dofree=<optimized out>, is_finalization=<optimized out>, freelist=<optimized out>) at Objects/object.c:904
#1  _PyObject_ClearFreeLists (freelists=0x555555c30948, is_finalization=is_finalization@entry=1) at Objects/object.c:929
#2  0x000055555585c521 in PyThreadState_Clear (tstate=tstate@entry=0x555555c2cf60) at Python/pystate.c:1703
#3  0x0000555555904a10 in thread_run (boot_raw=0x555555c194a0) at ./Modules/_threadmodule.c:390
#4  0x000055555587e27b in pythread_wrapper (arg=<optimized out>) at Python/thread_pthread.h:232
#5  0x00007ffff7d31ac3 in start_thread (arg=<optimized out>) at ./nptl/pthread_create.c:442
#6  0x00007ffff7dc3850 in clone3 () at ../sysdeps/unix/sysv/linux/x86_64/clone3.S:81

A backtrace for a segfault I got with a variant of this code, seems more informative but I cannot repro with the MRE:

Thread 185 "asyncio_4" received signal SIGSEGV, Segmentation fault.
[Switching to Thread 0xffffe31ff060 (LWP 1527140)]
0x0000000000867ce8 in _Py_TYPE (ob=<unknown at remote 0xdddddddddddddddd>) at ./Include/object.h:270
270             return ob->ob_type;

#0  0x0000000000867ce8 in _Py_TYPE (ob=<unknown at remote 0xdddddddddddddddd>) at ./Include/object.h:270
#1  union_repr (self=<unknown at remote 0xffffa425a9d0>) at Objects/unionobject.c:296
#2  0x00000000006e61a8 in PyObject_Repr (v=<unknown at remote 0xffffa425a9d0>) at Objects/object.c:779
#3  0x000000000082ada0 in unicode_fromformat_arg (writer=writer@entry=0xffffe31fb4e0, f=0xd88224 "R", f@entry=0xd88223 "%R", vargs=vargs@entry=0xffffe31fb410)
    at Objects/unicodeobject.c:3101
#4  0x000000000082b33c in unicode_from_format (writer=writer@entry=0xffffe31fb4e0, format=format@entry=0xd88220 "%U=%R", 
    vargs=<error reading variable: Cannot access memory at address 0x1>) at Objects/unicodeobject.c:3220
#5  0x000000000082b68c in PyUnicode_FromFormatV (format=format@entry=0xd88220 "%U=%R", vargs=...) at Objects/unicodeobject.c:3254
#6  0x000000000082b828 in PyUnicode_FromFormat (format=format@entry=0xd88220 "%U=%R") at Objects/unicodeobject.c:3268
#7  0x00000000006e1038 in namespace_repr (ns=<types.SimpleNamespace at remote 0xffffa48e3c60>) at Objects/namespaceobject.c:129
#8  0x000000000075e894 in object_str (self=self@entry=<types.SimpleNamespace at remote 0xffffa48e3c60>) at Objects/typeobject.c:7152
#9  0x0000000000768d18 in wrap_unaryfunc (self=<types.SimpleNamespace at remote 0xffffa48e3c60>, args=<optimized out>, wrapped=0x75e854 <object_str>)
    at Objects/typeobject.c:9536
#10 0x00000000005d8ee0 in wrapperdescr_raw_call (kwds=<optimized out>, args=<optimized out>, self=<optimized out>, descr=<optimized out>) at Objects/descrobject.c:532
#11 wrapper_call (self=<optimized out>, args=<optimized out>, kwds=<optimized out>) at Objects/descrobject.c:1437
#12 0x00000000005ab38c in _PyObject_MakeTpCall (tstate=tstate@entry=0xffff48937210, 
    callable=callable@entry=<method-wrapper '__str__' of types.SimpleNamespace object at 0xffffa48e3c60>, args=args@entry=0xffffe31fbe58, nargs=nargs@entry=0, 
    keywords=keywords@entry=0x0) at Objects/call.c:242
#13 0x00000000005aba68 in _PyObject_VectorcallTstate (tstate=0xffff48937210, callable=<method-wrapper '__str__' of types.SimpleNamespace object at 0xffffa48e3c60>, 
    args=0xffffe31fbe58, nargsf=9223372036854775808, kwnames=0x0) at ./Include/internal/pycore_call.h:167
#14 0x00000000005aba98 in PyObject_Vectorcall (callable=callable@entry=<method-wrapper '__str__' of types.SimpleNamespace object at 0xffffa48e3c60>, 
    args=args@entry=0x0, nargsf=nargsf@entry=9223372036854775808, kwnames=kwnames@entry=0x0) at Objects/call.c:327

Abort backtrace:

*** stack smashing detected ***: terminated

Thread 7 "Thread-6 (stres" received signal SIGABRT, Aborted.
[Switching to Thread 0x7ffff541b640 (LWP 351627)]
__pthread_kill_implementation (no_tid=0, signo=6, threadid=0) at ./nptl/pthread_kill.c:44

#0  __pthread_kill_implementation (no_tid=0, signo=6, threadid=0) at ./nptl/pthread_kill.c:44
#1  __pthread_kill_internal (signo=6, threadid=0) at ./nptl/pthread_kill.c:78
#2  __GI___pthread_kill (threadid=0, signo=signo@entry=6) at ./nptl/pthread_kill.c:89
#3  0x00007ffff7cdf476 in __GI_raise (sig=sig@entry=6) at ../sysdeps/posix/raise.c:26
#4  0x00007ffff7cc57f3 in __GI_abort () at ./stdlib/abort.c:79
#5  0x00007ffff7d26677 in __libc_message (action=action@entry=do_abort, fmt=fmt@entry=0x7ffff7e7892e "*** %s ***: terminated\n")
    at ../sysdeps/posix/libc_fatal.c:156
#6  0x00007ffff7dd359a in __GI___fortify_fail (msg=msg@entry=0x7ffff7e78916 "stack smashing detected") at ./debug/fortify_fail.c:26
#7  0x00007ffff7dd3566 in __stack_chk_fail () at ./debug/stack_chk_fail.c:24
#8  0x000055555572321d in _Py_type_getattro_impl (type=<optimized out>, name=<optimized out>,
    suppress_missing_attribute=suppress_missing_attribute@entry=0x7ffff541a194) at Objects/typeobject.c:6355
#9  0x00005555556db432 in PyObject_GetOptionalAttr (v=v@entry=<type at remote 0x3b2b2e06800>, name=<optimized out>,
    result=result@entry=0x7ffff541a1d0) at Objects/object.c:1345
#10 0x000055555572ea38 in _Py_typing_type_repr (writer=writer@entry=0x3b2be090e80, p=<type at remote 0x3b2b2e06800>) at Objects/typevarobject.c:295
#11 0x000055555577ad6e in union_repr (self=<typing.Union at remote 0x3b2c20e0820>) at Objects/unionobject.c:297
#12 0x00005555556d80db in PyObject_Repr (v=<typing.Union at remote 0x3b2c20e0820>) at ./Include/object.h:277
#13 PyObject_Repr (v=<typing.Union at remote 0x3b2c20e0820>) at Objects/object.c:754
#14 0x0000555555765835 in unicode_fromformat_arg (vargs=0x7ffff541a278, f=0x555555937b12 "R", writer=0x7ffff541a300)
    at Objects/unicodeobject.c:3101
#15 unicode_from_format (writer=writer@entry=0x7ffff541a300, format=format@entry=0x555555937b0e "%U=%R", vargs=vargs@entry=0x7ffff541a340)
    at Objects/unicodeobject.c:3220
#16 0x000055555576fadf in PyUnicode_FromFormatV (vargs=0x7ffff541a340, format=0x555555937b0e "%U=%R") at Objects/unicodeobject.c:3254
#17 PyUnicode_FromFormat (format=format@entry=0x555555937b0e "%U=%R") at Objects/unicodeobject.c:3268
#18 0x00005555556d5802 in namespace_repr (ns=<types.SimpleNamespace at remote 0x3b2b2152310>) at Objects/namespaceobject.c:129
#19 0x00005555556d80db in PyObject_Repr (v=<types.SimpleNamespace at remote 0x3b2b2152310>) at ./Include/object.h:277
#20 PyObject_Repr (v=<types.SimpleNamespace at remote 0x3b2b2152310>) at Objects/object.c:754
#21 0x00005555555e4883 in _PyEval_EvalFrameDefault (tstate=<optimized out>, frame=<optimized out>, throwflag=<optimized out>)
    at Python/generated_cases.c.h:2487
#22 0x00005555557dd13e in _PyEval_EvalFrame (throwflag=0, frame=<optimized out>, tstate=0x555555c3c6e0) at ./Include/internal/pycore_ceval.h:119
#23 _PyEval_Vector (tstate=0x555555c3c6e0, func=0x3b2b237fd80, locals=0x0, args=0x7ffff541a8a8, argcount=<optimized out>, kwnames=<optimized out>)
    at Python/ceval.c:1975

Found using fusil by @vstinner.

CPython versions tested on:

CPython main branch

Operating systems tested on:

Linux

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

Python 3.15.0a0 experimental free-threading build (heads/main:b706ff003c5, Jun 11 2025, 19:34:04) [GCC 11.4.0]

Linked PRs

Activity

added
type-crashA hard crash of the interpreter, possibly with a core dump
on Jun 24, 2025
self-assigned this
on Jun 24, 2025
ZeroIntensity

ZeroIntensity commented on Jun 24, 2025

@ZeroIntensity
Member

There's an evil borrowed reference in namespace_repr:

value = PyDict_GetItemWithError(d, key);

I think that's probably the culprit.

sobolevn

sobolevn commented on Jun 24, 2025

@sobolevn
Member

Yes, @ZeroIntensity, you are right! I've fixed this already by using PyDict_GetItemRef, but I can't find out the simplest test for this.

ZeroIntensity

ZeroIntensity commented on Jun 24, 2025

@ZeroIntensity
Member

I think it's a combination of borrowed references here. I was able to shorten the repro down to this:

from functools import reduce
from operator import or_
from threading import Thread

from types import SimpleNamespace

all_types = []
for x in range(100):
    class Dummy: pass
    all_types.append(Dummy)
big_union = reduce(or_, all_types)

s = SimpleNamespace()

def stress_simplenamespace():
    repr(s)
    s.x = big_union | tuple[int]

alive = [Thread(target=stress_simplenamespace) for _ in range(4)]
for t in alive:
    t.start()
for t in alive:
    t.join()
sobolevn

sobolevn commented on Jun 24, 2025

@sobolevn
Member

I think it's a combination of borrowed references here.

Can you please elaborate? I didn't see a single crash after this fix:

diff --git Objects/namespaceobject.c Objects/namespaceobject.c
index caebe6bf543..871063b7a04 100644
--- Objects/namespaceobject.c
+++ Objects/namespaceobject.c
@@ -124,8 +124,7 @@ namespace_repr(PyObject *ns)
         if (PyUnicode_Check(key) && PyUnicode_GET_LENGTH(key) > 0) {
             PyObject *value, *item;
 
-            value = PyDict_GetItemWithError(d, key);
-            if (value != NULL) {
+            if (PyDict_GetItemRef(d, key, &value) == 1) {
                 item = PyUnicode_FromFormat("%U=%R", key, value);
                 if (item == NULL) {
                     loop_error = 1;
@@ -135,7 +134,7 @@ namespace_repr(PyObject *ns)
                     Py_DECREF(item);
                 }
             }
-            else if (PyErr_Occurred()) {
+            else {
                 loop_error = 1;
             }
         }
ZeroIntensity

ZeroIntensity commented on Jun 24, 2025

@ZeroIntensity
Member

Union has some borrowed references too, but they're protected because they're immutable. That fix looks correct, but I think most repros you could come up with won't work because something else is keeping a reference.

Anyways, there's been some pushback on recent PRs for adding FT tests, because they might be a bit too specific to catch any future regressions. If coming up with a repro that works in a test here is too difficult, it's probably fine to not have one.

added a commit that references this issue on Jun 24, 2025
a9a99c7
added a commit that references this issue on Jun 24, 2025
b3ab94a
added 2 commits that reference this issue on Jun 24, 2025
sobolevn

sobolevn commented on Jun 24, 2025

@sobolevn
Member

@devdanzin can you please verify that this no longer happens?

added 2 commits that reference this issue on Jun 24, 2025
88c5552
469f69d
kumaraditya303

kumaraditya303 commented on Jun 30, 2025

@kumaraditya303
Contributor

I think this is fixed now, closing

added a commit that references this issue on Jul 11, 2025
added a commit that references this issue on Jul 12, 2025
added a commit that references this issue on Aug 4, 2025
7fba813
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

interpreter-core(Objects, Python, Grammar, and Parser dirs)topic-free-threadingtype-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

      Participants

      @JelleZijlstra@sobolevn@picnixz@ZeroIntensity@kumaraditya303

      Issue actions

        Segfault by calling `repr(SimpleNamespace)` with `typing.Union` attributes in threads on a free-threading build · Issue #135878 · python/cpython