Bug report
Bug description:
In the free-threaded build, rlock_repr reads the recursive lock's owner with plain loads to build the repr string, while acquire/release/_acquire_restore write the owner with atomic stores.
| static PyObject * | |
| rlock_repr(PyObject *op) | |
| { | |
| rlockobject *self = rlockobject_CAST(op); | |
| PyThread_ident_t owner = self->lock.thread; | |
| int locked = rlock_locked_impl(self); |
The reader side is not atomic, so a repr() concurrent with acquire() is a data race on the owner field.
Reproducer:
import _thread from threading import Thread r = _thread.RLock() def thread1(): for _ in range(20000): try: repr(r) except Exception: pass def thread2(): for _ in range(20000): r.acquire() r.release() threads = [Thread(target=thread1) for _ in range(6)] threads += [Thread(target=thread2) for _ in range(2)] for t in threads: t.start() for t in threads: t.join()
TSAN Report:
==================
WARNING: ThreadSanitizer: data race (pid=40273)
Read of size 8 at 0x7fffb6600f38 by thread T1:
#0 rlock_repr /cpython/./Modules/_threadmodule.c:1291:41
#1 PyObject_Repr /cpython/Objects/object.c:784:11
#2 builtin_repr /cpython/Python/bltinmodule.c:2677:12
#3 _PyEval_EvalFrameDefault /cpython/Python/generated_cases.c.h:2712:35
...
Previous atomic write of size 8 at 0x7fffb6600f38 by thread T7:
#0 _Py_atomic_store_ullong_relaxed /cpython/./Include/cpython/pyatomic_gcc.h:518:3
#1 _PyRecursiveMutex_LockTimed /cpython/Python/lock.c:439:9
#2 _thread_RLock_acquire_impl /cpython/./Modules/_threadmodule.c:1102:22
#3 _thread_RLock_acquire /cpython/./Modules/clinic/_threadmodule.c.h:418:20
#4 _PyCallMethodDescriptorFastWithKeywords_StackRef /cpython/Python/ceval.c:883:11
#5 _PyEval_EvalFrameDefault /cpython/Python/generated_cases.c.h:4205:35
...
SUMMARY: ThreadSanitizer: data race /cpython/./Modules/_threadmodule.c:1291:41 in rlock_repr
==================
CPython versions tested on:
CPython main branch
Operating systems tested on:
Linux