Skip to content

Segfault with data race / double-decref in func_set_qualname under free-threading #153297

Description

@Naserume

What happened?

In the free-threaded build, the setters for a function's __qualname__ and __name__ replace the field with Py_XSETREF and no critical section:

func_set_qualname(PyObject *self, PyObject *value, void *Py_UNUSED(ignored))
{
PyFunctionObject *op = _PyFunction_CAST(self);
/* Not legal to del f.__qualname__ or to set it to anything
* other than a string object. */
if (value == NULL || !PyUnicode_Check(value)) {
PyErr_SetString(PyExc_TypeError,
"__qualname__ must be set to a string object");
return -1;
}
handle_func_event(PyFunction_EVENT_MODIFY_QUALNAME, (PyFunctionObject *) op, value);
Py_XSETREF(op->func_qualname, Py_NewRef(value));
return 0;
}

Py_XSETREF reads the old func_qualname, stores the new value, and decrefs the old one, with no lock. Two threads assigning f.__qualname__ can therefore both decref the same old string and free it while it is still referenced, a use-after-free.

The freed memory is then reused by a new allocation while another thread is still dereferencing the old pointer (e.g. repr() reading func_qualname), so that dangling access lands on reused memory and the interpreter segfaults.

Reproducer:

from threading import Thread

def shared_func():
    return 1

def setter():
    for i in range(20000):
        try:
            shared_func.__qualname__ = "q_%d_%d" % (id(i), i & 15)
        except Exception:
            pass

def reader():
    for _ in range(20000):
        try:
            _ = shared_func.__qualname__
            repr(shared_func)
        except Exception:
            pass

threads  = [Thread(target=setter) for _ in range(6)]
threads += [Thread(target=reader) for _ in range(4)]
for t in threads: t.start()
for t in threads: t.join()

TSAN Report:

==================
WARNING: ThreadSanitizer: data race (pid=46190)
  Read of size 8 at 0x7e6f48678c78 by thread T2:
    #0 func_set_qualname /cpython/Objects/funcobject.c:734:5 
    #1 getset_set /cpython/Objects/descrobject.c:250:16 
    #2 _PyObject_GenericSetAttrWithDict /cpython/Objects/object.c:2049:19 
    #3 PyObject_GenericSetAttr /cpython/Objects/object.c:2120:12
    #4 PyObject_SetAttr /cpython/Objects/object.c:1533:15 
    #5 _PyEval_EvalFrameDefault /cpython/Python/generated_cases.c.h:12146:27 

  Previous write of size 8 at 0x7e6f48678c78 by thread T1:
    #0 func_set_qualname /cpython/Objects/funcobject.c:734:5 
    #1 getset_set /cpython/Objects/descrobject.c:250:16 
    #2 _PyObject_GenericSetAttrWithDict /cpython/Objects/object.c:2049:19 
    #3 PyObject_GenericSetAttr /cpython/Objects/object.c:2120:12  
    #4 PyObject_SetAttr /cpython/Objects/object.c:1533:15 
    #5 _PyEval_EvalFrameDefault /cpython/Python/generated_cases.c.h:12146:27 

SUMMARY: ThreadSanitizer: data race /cpython/Objects/funcobject.c:734:5 in func_set_qualname
==================
...
==================
WARNING: ThreadSanitizer: data race (pid=46190)
  Read of size 4 at 0x7e6f4a07019c by thread T4:
    #0 _Py_ExplicitMergeRefcount /cpython/Objects/object.c:477:40 
    #1 _Py_brc_queue_object /cpython/Python/brc.c:73:31 
    #2 _Py_DecRefSharedIsDead /cpython/Objects/object.c:413:9 
    #3 _Py_DecRefSharedDebug /cpython/Objects/object.c:425:9 
    #4 _Py_DecRefShared /cpython/Objects/object.c:433:5 
    #5 Py_DECREF /cpython/./Include/refcount.h:385:9  
    #6 Py_XDECREF /cpython/./Include/refcount.h:520:9 
    #7 func_set_qualname /cpython/Objects/funcobject.c:734:5 
    #8 getset_set /cpython/Objects/descrobject.c:250:16 
    #9 _PyObject_GenericSetAttrWithDict /cpython/Objects/object.c:2049:19 
    #10 PyObject_GenericSetAttr /cpython/Objects/object.c:2120:12 
    #11 PyObject_SetAttr /cpython/Objects/object.c:1533:15 
    #12 _PyEval_EvalFrameDefault /cpython/Python/generated_cases.c.h:12146:27  

  Previous atomic write of size 4 at 0x7e6f4a07019c by thread T1:
    #0 _Py_atomic_store_uint32_relaxed /cpython/./Include/cpython/pyatomic_gcc.h:493:3 
    #1 new_reference /cpython/Objects/object.c:2743:5 
    #2 _Py_NewReferenceNoTotal /cpython/Objects/object.c:2770:5 
    #3 _PyUnicode_ResizeCompact /cpython/Objects/unicodeobject.c:1117:5 
    #4 _PyUnicodeWriter_Finish /cpython/Objects/unicode_writer.c:620:16 
    #5 PyUnicode_Format /cpython/Objects/unicode_format.c:1102:12 
    #6 unicode_mod /cpython/Objects/unicodeobject.c:13721:12 
    #7 binary_op1 /cpython/Objects/abstract.c:974:13 
    #8 binary_op /cpython/Objects/abstract.c:1013:24 
    #9 PyNumber_Remainder /cpython/Objects/abstract.c:1197:1 
    #10 _PyEval_EvalFrameDefault /cpython/Python/generated_cases.c.h:67:35 

SUMMARY: ThreadSanitizer: data race /cpython/Objects/object.c:477:40 in _Py_ExplicitMergeRefcount
==================
...
==================
WARNING: ThreadSanitizer: data race (pid=46190)
  Read of size 8 at 0x7e6f52070160 by thread T7:
    #0 unicode_fromformat_write_str /cpython/Objects/unicodeobject.c:2527:14 
    #1 unicode_fromformat_arg /cpython/Objects/unicodeobject.c 
    #2 unicode_from_format /cpython/Objects/unicodeobject.c:3081:17 
    #3 PyUnicode_FromFormatV /cpython/Objects/unicodeobject.c:3115:9 
    #4 PyUnicode_FromFormat /cpython/Objects/unicodeobject.c:3129:11
    #5 func_repr /cpython/Objects/funcobject.c:1150:12 
    #6 PyObject_Repr /cpython/Objects/object.c:784:11 
    #7 builtin_repr /cpython/Python/bltinmodule.c:2677:12 
    #8 cfunction_vectorcall_O /cpython/Objects/methodobject.c:536:24 
    #9 _PyObject_VectorcallTstate /cpython/./Include/internal/pycore_call.h:144:11 
    #10 PyObject_Vectorcall /cpython/Objects/call.c:327:12 
    #11 _Py_VectorCallInstrumentation_StackRefSteal /cpython/Python/ceval.c:768:11 
    #12 _PyEval_EvalFrameDefault /cpython/Python/generated_cases.c.h:1906:35 

  Previous write of size 8 at 0x7e6f52070160 by thread T3:
    #0 __tsan_memcpy <null> (python3.16t+0xf514f) 
    #1 _mi_memcpy /cpython/./Include/internal/mimalloc/mimalloc/internal.h:929:3 
    #2 _PyObject_MiRealloc /cpython/Objects/obmalloc.c:363:9 
    #3 PyObject_Realloc /cpython/Objects/obmalloc.c:1731:12
    #4 _PyUnicode_ResizeCompact /cpython/Objects/unicodeobject.c:1110:31 
    #5 _PyUnicodeWriter_Finish /cpython/Objects/unicode_writer.c:620:16 
    #6 PyUnicode_Format /cpython/Objects/unicode_format.c:1102:12 
    #7 unicode_mod /cpython/Objects/unicodeobject.c:13721:12 
    #8 binary_op1 /cpython/Objects/abstract.c:974:13 
    #9 binary_op /cpython/Objects/abstract.c:1013:24 
    #10 PyNumber_Remainder /cpython/Objects/abstract.c:1197:1 
    #11 _PyEval_EvalFrameDefault /cpython/Python/generated_cases.c.h:67:35 

SUMMARY: ThreadSanitizer: data race /cpython/Objects/unicodeobject.c:2527:14 in unicode_fromformat_write_str
==================
...
==================
WARNING: ThreadSanitizer: data race (pid=46190)
  Read of size 8 at 0x7e6f5a074ed8 by thread T8:
    #0 _Py_Dealloc /cpython/Objects/object.c:3292:26 
    #1 _Py_brc_queue_object /cpython/Python/brc.c 
    #2 _Py_DecRefSharedIsDead /cpython/Objects/object.c:413:9 
    #3 _Py_DecRefSharedDebug /cpython/Objects/object.c:425:9 
    #4 _Py_DecRefShared /cpython/Objects/object.c:433:5 
    #5 Py_DECREF /cpython/./Include/refcount.h:385:9
    #6 PyStackRef_XCLOSE /cpython/./Include/internal/pycore_stackref.h:726:9 
    #7 _PyEval_EvalFrameDefault /cpython/Python/generated_cases.c.h:12475:17 

  Previous write of size 8 at 0x7e6f5a074ed8 by thread T5:
    #0 __tsan_memcpy <null> (python3.16t+0xf514f) 
    #1 _mi_memcpy /cpython/./Include/internal/mimalloc/mimalloc/internal.h:929:3 
    #2 _PyObject_MiRealloc /cpython/Objects/obmalloc.c:363:9 
    #3 PyObject_Realloc /cpython/Objects/obmalloc.c:1731:12 
    #4 _PyUnicode_ResizeCompact /cpython/Objects/unicodeobject.c:1110:31 
    #5 _PyUnicodeWriter_Finish /cpython/Objects/unicode_writer.c:620:16 
    #6 PyUnicode_Format /cpython/Objects/unicode_format.c:1102:12 
    #7 unicode_mod /cpython/Objects/unicodeobject.c:13721:12 
    #8 binary_op1 /cpython/Objects/abstract.c:974:13 
    #9 binary_op /cpython/Objects/abstract.c:1013:24 
    #10 PyNumber_Remainder /cpython/Objects/abstract.c:1197:1 
    #11 _PyEval_EvalFrameDefault /cpython/Python/generated_cases.c.h:67:35 

SUMMARY: ThreadSanitizer: data race /cpython/Objects/object.c:3292:26 in _Py_Dealloc
==================
ThreadSanitizer:DEADLYSIGNAL
==46190==ERROR: ThreadSanitizer: SEGV on unknown address (pc 0x56b51dd549ac bp 0x7e6f3f9fc8b0 sp 0x7e6f3f9fc858 T46198)
==46190==The signal is caused by a READ memory access.
==46190==Hint: this fault was caused by a dereference of a high value address (see register values below).  Disassemble the provided pc to learn which register was used.
ThreadSanitizer:DEADLYSIGNAL
ThreadSanitizer: nested bug in the same thread, aborting.

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 free-threading build (tags/v3.15.0b1-614-g7928a8b730b:7928a8b730b, Jul 7 2026, 18:49:27) [Clang 18.1.3 (1ubuntu1)]

Linked PRs

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

Fields

No fields configured for issues without a type.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions