Skip to content

Data race: TextIOWrapper.detach() stores self->buffer = NULL non-atomically #154523

Description

@Naserume

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:

cpython/Modules/_io/textio.c

Lines 1630 to 1645 in a2a8466

_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

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions