gh-134908: Protect textiowrapper_iternext with critical section (gh-134910)

The `textiowrapper_iternext` function called `_textiowrapper_writeflush`, but did not
use a critical section, making it racy in free-threaded builds.
This commit is contained in:
Duane Griffin 2025-06-03 05:22:41 +12:00 committed by GitHub
parent 055827528f
commit 44fb7c361c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 46 additions and 1 deletions

View file

@ -1062,6 +1062,37 @@ class IOTest(unittest.TestCase):
# Silence destructor error
R.flush = lambda self: None
@threading_helper.requires_working_threading()
def test_write_readline_races(self):
# gh-134908: Concurrent iteration over a file caused races
thread_count = 2
write_count = 100
read_count = 100
def writer(file, barrier):
barrier.wait()
for _ in range(write_count):
file.write("x")
def reader(file, barrier):
barrier.wait()
for _ in range(read_count):
for line in file:
self.assertEqual(line, "")
with self.open(os_helper.TESTFN, "w+") as f:
barrier = threading.Barrier(thread_count + 1)
reader = threading.Thread(target=reader, args=(f, barrier))
writers = [threading.Thread(target=writer, args=(f, barrier))
for _ in range(thread_count)]
with threading_helper.catch_threading_exception() as cm:
with threading_helper.start_threads(writers + [reader]):
pass
self.assertIsNone(cm.exc_type)
self.assertEqual(os.stat(os_helper.TESTFN).st_size,
write_count * thread_count)
class CIOTest(IOTest):

View file

@ -0,0 +1 @@
Fix crash when iterating over lines in a text file on the :term:`free threaded <free threading>` build.

View file

@ -1578,6 +1578,8 @@ _io_TextIOWrapper_detach_impl(textio *self)
static int
_textiowrapper_writeflush(textio *self)
{
_Py_CRITICAL_SECTION_ASSERT_OBJECT_LOCKED(self);
if (self->pending_bytes == NULL)
return 0;
@ -3173,8 +3175,9 @@ _io_TextIOWrapper_close_impl(textio *self)
}
static PyObject *
textiowrapper_iternext(PyObject *op)
textiowrapper_iternext_lock_held(PyObject *op)
{
_Py_CRITICAL_SECTION_ASSERT_OBJECT_LOCKED(op);
PyObject *line;
textio *self = textio_CAST(op);
@ -3210,6 +3213,16 @@ textiowrapper_iternext(PyObject *op)
return line;
}
static PyObject *
textiowrapper_iternext(PyObject *op)
{
PyObject *result;
Py_BEGIN_CRITICAL_SECTION(op);
result = textiowrapper_iternext_lock_held(op);
Py_END_CRITICAL_SECTION();
return result;
}
/*[clinic input]
@critical_section
@getter