Skip to content

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

Closed as duplicate

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

Activity

  1. added
    type-crashA hard crash of the interpreter, possibly with a core dump
    on Jul 7, 2026
  2. self-assigned this
    on Jul 8, 2026
  3. sobolevn commented on Jul 8, 2026

    @sobolevn
    Member

    Yes, I can reproduce this one as well! Thank you!
    Fix is ready :)

  4. devdanzin commented on Jul 25, 2026

    @devdanzin
    Member

    fusil's --tsan mode independently hit this same class through a different setter: assigning a shared function's __defaults__ (and __kwdefaults__) concurrently. Those setters have the identical Py_XSETREF-with-no-critical-section shape as func_set_qualname, but the fix in gh-153335 covers __name__ / __qualname__ / __code__ and not __defaults__ / __kwdefaults__, so this sibling survives it.

    func_set_defaults (Objects/funcobject.c:776) and func_set_kwdefaults (:819):

    Py_XSETREF(op->func_defaults, Py_XNewRef(value));      // func_set_defaults, no critical section
    ...
    Py_XSETREF(op->func_kwdefaults, Py_XNewRef(value));    // func_set_kwdefaults, no critical section

    Py_XSETREF reads the old func_defaults, stores the new value, and decrefs the old one with no lock, so two threads assigning f.__defaults__ both decref the same old tuple and free it while it is still referenced — the same double-free / use-after-free as the __qualname__ case.

    Reproducer (concurrent __defaults__ assignment; swap __defaults__ → __kwdefaults__ for the kwdefaults variant):

    from threading import Thread
    
    def shared_func(a=0, b=0):
        return a + b
    
    def setter():
        for i in range(20000):
            try:
                shared_func.__defaults__ = (i, i)
            except Exception:
                pass
    
    def reader():
        for _ in range(20000):
            try:
                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()

    On a free-threaded debug build (PYTHON_GIL=0) this aborts ~every run (5/5 here) on the double-decref'd defaults tuple:

    ./Include/refcount.h:520: _Py_NegativeRefcount: Assertion failed: object has negative ref count
    object refcount : 0
    object type name: tuple
    

    __kwdefaults__ (func_set_kwdefaults, :819) reproduces identically. It'd be good to give both setters the same critical-section treatment gh-153335 applies to the others (and it's worth a scan for any remaining Py_XSETREF-without-a-critical-section function-object setters — e.g. func_set_annotate at :857).

    Found with fusil's --tsan mode (its new shared-callable mutation op); reduced and drafted with AI assistance (Claude Code), verified by hand on a free-threaded debug build of main (3.16.0a0, heads/main:a1d580430c8).

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

    Issue actions