Bug report
Bug description:
In the free-threaded build, TextIOWrapper.detach() clears the self->buffer slot with a plain store, while reading tw.buffer goes through a LOAD_ATTR_SLOT specialization that reads the same slot with FT_ATOMIC_LOAD_PTR.
Writer โ _io_TextIOWrapper_detach_impl:
| _io_TextIOWrapper_detach_impl(textio *self) | |
| /*[clinic end generated code: output=7ba3715cd032d5f2 input=c908a3b4ef203b0f]*/ | |
| { | |
| PyObject *buffer; | |
| if (_PyFile_Flush((PyObject *)self) < 0) { | |
| return NULL; | |
| } | |
| /* _PyFile_Flush could detach before returning; raise an exception. */ | |
| buffer = buffer_access_safe(self); | |
| if (buffer == NULL) { | |
| return NULL; | |
| } | |
| self->buffer = NULL; | |
| self->detached = 1; | |
| return buffer; | |
| } |
Reader โ LOAD_ATTR_SLOT reads the slot atomically:
| // _LOAD_ATTR_SLOT | |
| { | |
| uint16_t index = read_u16(&this_instr[4].cache); | |
| PyObject *owner_o = PyStackRef_AsPyObjectBorrow(owner); | |
| PyObject **addr = (PyObject **)((char *)owner_o + index); | |
| PyObject *attr_o = FT_ATOMIC_LOAD_PTR(*addr); | |
| if (attr_o == NULL) { | |
| UPDATE_MISS_STATS(LOAD_ATTR); | |
| assert(_PyOpcode_Deopt[opcode] == (LOAD_ATTR)); | |
| JUMP_TO_PREDICTED(LOAD_ATTR); | |
| } |
So tw.buffer racing tw.detach() is a plain-store-vs-atomic-load data race on the buffer slot. detach() should store the slot atomically.
Reproducer:
import io from threading import Thread def make(): return io.TextIOWrapper(io.BytesIO(b'x' * 16)) slot = [make()] def reader(): for _ in range(20000): try: _ = slot[0].buffer except Exception: pass def detacher(): for _ in range(20000): try: slot[0].detach() except Exception: pass slot[0] = make() threads = [Thread(target=reader) for _ in range(6)] threads += [Thread(target=detacher) for _ in range(2)] for t in threads: t.start() for t in threads: t.join()
TSAN Report:
WARNING: ThreadSanitizer: data race (pid=1522499)
Write of size 8 at 0x7fffd0100110 by thread T7:
#0 _io_TextIOWrapper_detach_impl /cpython/./Modules/_io/textio.c:1642:18
#1 _io_TextIOWrapper_detach /cpython/./Modules/_io/clinic/textio.c.h:793:20
#2 _PyEval_EvalFrameDefault /cpython/Python/generated_cases.c.h:4330:35
Previous atomic read of size 8 at 0x7fffd0100110 by thread T6:
#0 _Py_atomic_load_ptr /cpython/./Include/cpython/pyatomic_gcc.h:300:18
#1 _PyEval_EvalFrameDefault /cpython/Python/generated_cases.c.h:9588:36
#2 _PyEval_EvalFrame /cpython/./Include/internal/pycore_ceval.h:122:16
SUMMARY: ThreadSanitizer: data race /cpython/./Modules/_io/textio.c:1642:18 in _io_TextIOWrapper_detach_impl
==================
CPython versions tested on:
CPython main branch
Operating systems tested on:
macOS, Linux