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.
Agree this is a genuine C-level data race worth the "store the slot atomically" fix, but as a severity note: it looks value-benign on aligned hardware — I couldn't turn it into a crash.
Tried on a free-threaded debug build and a free-threaded ASan build of main: the reader/detacher race (amplified — readers also touch the returned buffer), and separately concurrent detach() on a shared wrapper (barrier-synchronized, to try for a double-Py_CLEAR/double-DECREF), both run clean across many attempts.
That matches the code: _io_TextIOWrapper_detach_impl is @critical_section (so concurrent detach() can't double-free — the second one gets NULL from buffer_access_safe and raises), and it transfers ownership of the buffer to the caller via return buffer rather than Py_CLEAR-ing it — so the LOAD_ATTR_SLOT reader's atomic-load-plus-try-incref never increfs a freed object. What remains is exactly a plain pointer store (self->buffer = NULL) vs an atomic pointer load on the aligned slot: real UB that TSan correctly flags, but the reader only ever observes the old valid pointer or NULL. So the atomic-store fix is right as UB hygiene; there doesn't appear to be a memory-safety issue behind it.
Checked and drafted with AI assistance (Claude Code), verified by hand on free-threaded debug/ASan builds of main.
Activity
devdanzin commented on Jul 23, 2026
Agree this is a genuine C-level data race worth the "store the slot atomically" fix, but as a severity note: it looks value-benign on aligned hardware — I couldn't turn it into a crash.
Tried on a free-threaded debug build and a free-threaded ASan build of main: the reader/detacher race (amplified — readers also touch the returned buffer), and separately concurrent detach() on a shared wrapper (barrier-synchronized, to try for a double-Py_CLEAR/double-DECREF), both run clean across many attempts.
That matches the code: _io_TextIOWrapper_detach_impl is @critical_section (so concurrent detach() can't double-free — the second one gets NULL from buffer_access_safe and raises), and it transfers ownership of the buffer to the caller via return buffer rather than Py_CLEAR-ing it — so the LOAD_ATTR_SLOT reader's atomic-load-plus-try-incref never increfs a freed object. What remains is exactly a plain pointer store (self->buffer = NULL) vs an atomic pointer load on the aligned slot: real UB that TSan correctly flags, but the reader only ever observes the old valid pointer or NULL. So the atomic-store fix is right as UB hygiene; there doesn't appear to be a memory-safety issue behind it.
Checked and drafted with AI assistance (Claude Code), verified by hand on free-threaded debug/ASan builds of main.
kumaraditya303 commented on Jul 30, 2026
The fix should change buffer to be a getset descriptor and use critical section to access it.
gh-154523: Fix data-race in `TextIOWrapper.detach()` (#154565)
pythongh-154523: Fix data-race in `TextIOWrapper.detach()` (python#15…
[3.15] gh-157364: Fix use-after-free in io.TextIOWrapper during reent…
[3.14] gh-157364: Fix use-after-free in io.TextIOWrapper during reent…