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
What happened?
In the free-threaded build, the setters for a function's
__qualname__and__name__replace the field withPy_XSETREFand no critical section:cpython/Objects/funcobject.c
Lines 723 to 736 in a74280e
Py_XSETREFreads the oldfunc_qualname, stores the new value, and decrefs the old one, with no lock. Two threads assigningf.__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()readingfunc_qualname), so that dangling access lands on reused memory and the interpreter segfaults.Reproducer:
TSAN Report:
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
__name__and__qualname__to functions on FT builds #153335