Crash report
What happened?
On a free-threaded build, constructing a PEP 661 sentinel() in one thread while another thread reassigns the enclosing function's __module__ causes a heap use-after-free and SIGSEGV.
Objects/sentinelobject.c's caller() reads func->func_module as a borrowed reference and then Py_NewRefs it, with no protection against a concurrent reassignment freeing that module in the window between the read and the incref. The freed pointer is baked into the new sentinel's self->module; when the sentinel is later collected, sentinel_clear's Py_CLEAR(self->module) decrefs the dangling pointer and _Py_Dealloc reads a freed type slot → crash.
Reproducer
import sys, threading, time
class M:
__slots__ = ("payload",)
def __init__(self):
self.payload = bytearray(128)
def make():
return sentinel("X") # sentinel() -> caller() reads make.__module__
make.__module__ = M()
stop = False
def churn(): # reassign make.__module__ rapidly
while not stop:
make.__module__ = M()
def build(): # construct + drop sentinels
while not stop:
s = make(); del s
ts = [threading.Thread(target=churn) for _ in range(4)]
ts += [threading.Thread(target=build) for _ in range(8)]
for t in ts: t.start()
time.sleep(5); stop = True
for t in ts: t.join()
print("survived (no crash this run)")
Crashes reliably, only on a free-threaded build.
Root cause
Objects/sentinelobject.c, caller() (≈ lines 41–46):
PyFunctionObject *func = _PyFrame_GetFunction(f);
assert(PyFunction_Check(func));
PyObject *r = PyFunction_GetModule((PyObject *)func); /* BORROWED reference */
if (!r) {
assert(!PyErr_Occurred());
Py_RETURN_NONE;
}
return Py_NewRef(r); /* increfs the borrowed ref */
PyFunction_GetModule returns a borrowed reference to func->func_module. Between that read and Py_NewRef(r), a concurrent func.__module__ = ... can drop the previous module object's last reference to 0 and free it; Py_NewRef(r) then operates on freed memory.
The result is stored into the sentinel by sentinel_new_impl → sentinel_new_with_module (≈ line 59):
self->module = Py_NewRef(module); /* dangling pointer baked into the sentinel */
and later released by sentinel_clear (line 129):
Py_CLEAR(self->module); /* Py_DECREF on the dangling pointer -> UAF */
Backtrace
Debug free-threaded build, faulting thread:
#0 _Py_Dealloc (op=<freed module>) at ./Include/internal/pycore_pystate.h:339
#1 Py_DECREF (op=<freed module>) at ./Include/refcount.h:363
#2 sentinel_clear (op=<sentinel>) at Objects/sentinelobject.c:129
#3 sentinel_dealloc (op=<freed module>) at Objects/sentinelobject.c:138
Release free-threaded build (same repro), faulting thread lands one step later, in the allocator freeing the corrupted object:
#0 mi_free at Objects/mimalloc/alloc.c:576
#1 _PyObject_MiFree at Objects/obmalloc.c:381
#2 subtype_dealloc at Objects/typeobject.c:2876
#3 _Py_Dealloc at Objects/object.c:3319
Related
Same borrowed-reference-under-free-threading class as #153297 ("double-decref in func_set_qualname under free-threading") and the broader funcobject.c getset FT-unsafety. The shared root is that func->func_module (and the other function fields) can be reassigned concurrently, so any consumer that reads them borrowed and then increfs is racy on the free-threaded build. Here the unsafe consumer is caller() in sentinelobject.c rather than a funcobject.c getset.
Found, investigated, and drafted with assistance from Claude Code (Opus 4.8).
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 (heads/main:bcf98ddbc40, Jul 4 2026, 15:09:31) [Clang 21.1.8 (6ubuntu1)]
Crash report
What happened?
On a free-threaded build, constructing a PEP 661
sentinel()in one thread while another thread reassigns the enclosing function's__module__causes a heap use-after-free and SIGSEGV.Objects/sentinelobject.c'scaller()readsfunc->func_moduleas a borrowed reference and thenPy_NewRefs it, with no protection against a concurrent reassignment freeing that module in the window between the read and the incref. The freed pointer is baked into the new sentinel'sself->module; when the sentinel is later collected,sentinel_clear'sPy_CLEAR(self->module)decrefs the dangling pointer and_Py_Deallocreads a freed type slot → crash.Reproducer
Crashes reliably, only on a free-threaded build.
Root cause
Objects/sentinelobject.c,caller()(≈ lines 41–46):PyFunction_GetModulereturns a borrowed reference tofunc->func_module. Between that read andPy_NewRef(r), a concurrentfunc.__module__ = ...can drop the previous module object's last reference to 0 and free it;Py_NewRef(r)then operates on freed memory.The result is stored into the sentinel by
sentinel_new_impl→sentinel_new_with_module(≈ line 59):and later released by
sentinel_clear(line 129):Backtrace
Debug free-threaded build, faulting thread:
Release free-threaded build (same repro), faulting thread lands one step later, in the allocator freeing the corrupted object:
Related
Same borrowed-reference-under-free-threading class as #153297 ("double-decref in
func_set_qualnameunder free-threading") and the broaderfuncobject.cgetset FT-unsafety. The shared root is thatfunc->func_module(and the other function fields) can be reassigned concurrently, so any consumer that reads them borrowed and then increfs is racy on the free-threaded build. Here the unsafe consumer iscaller()insentinelobject.crather than afuncobject.cgetset.Found, investigated, and drafted with assistance from Claude Code (Opus 4.8).
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 (heads/main:bcf98ddbc40, Jul 4 2026, 15:09:31) [Clang 21.1.8 (6ubuntu1)]