Skip to content

Commit 1f2998b

Browse files
duanegcolesbury
authored andcommitted
[3.13] 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. (cherry picked from commit 44fb7c3) Co-authored-by: Duane Griffin <duaneg@dghda.com>
1 parent 7ca17ed commit 1f2998b

File tree

3 files changed

+47
-1
lines changed

3 files changed

+47
-1
lines changed

Lib/test/test_io.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1038,6 +1038,37 @@ def flush(self):
10381038
# Silence destructor error
10391039
R.flush = lambda self: None
10401040

1041+
@threading_helper.requires_working_threading()
1042+
def test_write_readline_races(self):
1043+
# gh-134908: Concurrent iteration over a file caused races
1044+
thread_count = 2
1045+
write_count = 100
1046+
read_count = 100
1047+
1048+
def writer(file, barrier):
1049+
barrier.wait()
1050+
for _ in range(write_count):
1051+
file.write("x")
1052+
1053+
def reader(file, barrier):
1054+
barrier.wait()
1055+
for _ in range(read_count):
1056+
for line in file:
1057+
self.assertEqual(line, "")
1058+
1059+
with self.open(os_helper.TESTFN, "w+") as f:
1060+
barrier = threading.Barrier(thread_count + 1)
1061+
reader = threading.Thread(target=reader, args=(f, barrier))
1062+
writers = [threading.Thread(target=writer, args=(f, barrier))
1063+
for _ in range(thread_count)]
1064+
with threading_helper.catch_threading_exception() as cm:
1065+
with threading_helper.start_threads(writers + [reader]):
1066+
pass
1067+
self.assertIsNone(cm.exc_type)
1068+
1069+
self.assertEqual(os.stat(os_helper.TESTFN).st_size,
1070+
write_count * thread_count)
1071+
10411072

10421073
class CIOTest(IOTest):
10431074

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Fix crash when iterating over lines in a text file on the :term:`free threaded <free threading>` build.

Modules/_io/textio.c

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1568,6 +1568,8 @@ _io_TextIOWrapper_detach_impl(textio *self)
15681568
static int
15691569
_textiowrapper_writeflush(textio *self)
15701570
{
1571+
_Py_CRITICAL_SECTION_ASSERT_OBJECT_LOCKED(self);
1572+
15711573
if (self->pending_bytes == NULL)
15721574
return 0;
15731575

@@ -3157,9 +3159,11 @@ _io_TextIOWrapper_close_impl(textio *self)
31573159
}
31583160

31593161
static PyObject *
3160-
textiowrapper_iternext(textio *self)
3162+
textiowrapper_iternext_lock_held(PyObject *op)
31613163
{
3164+
_Py_CRITICAL_SECTION_ASSERT_OBJECT_LOCKED(op);
31623165
PyObject *line;
3166+
textio *self = (textio *)op;
31633167

31643168
CHECK_ATTACHED(self);
31653169

@@ -3194,6 +3198,16 @@ textiowrapper_iternext(textio *self)
31943198
return line;
31953199
}
31963200

3201+
static PyObject *
3202+
textiowrapper_iternext(PyObject *op)
3203+
{
3204+
PyObject *result;
3205+
Py_BEGIN_CRITICAL_SECTION(op);
3206+
result = textiowrapper_iternext_lock_held(op);
3207+
Py_END_CRITICAL_SECTION();
3208+
return result;
3209+
}
3210+
31973211
/*[clinic input]
31983212
@critical_section
31993213
@getter

0 commit comments

Comments
 (0)