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
Bug report
Bug description:
In the free-threaded build,
TextIOWrapper.detach()clears theself->bufferslot with a plain store, while readingtw.buffergoes through aLOAD_ATTR_SLOTspecialization that reads the same slot withFT_ATOMIC_LOAD_PTR.Writer —
_io_TextIOWrapper_detach_impl:cpython/Modules/_io/textio.c
Lines 1630 to 1645 in a2a8466
Reader —
LOAD_ATTR_SLOTreads the slot atomically:cpython/Python/generated_cases.c.h
Lines 9493 to 9503 in a2a8466
So
tw.bufferracingtw.detach()is a plain-store-vs-atomic-load data race on thebufferslot.detach()should store the slot atomically.Reproducer:
TSAN Report:
CPython versions tested on:
CPython main branch
Operating systems tested on:
macOS, Linux