Skip to content

Commit 428b0ca

Browse files
[3.14] gh-134908: Protect textiowrapper_iternext with critical section (gh-134910) (gh-135039)
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 7ac4618 commit 428b0ca

File tree

3 files changed

+46
-1
lines changed

3 files changed

+46
-1
lines changed

Lib/test/test_io.py

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

1065+
@threading_helper.requires_working_threading()
1066+
def test_write_readline_races(self):
1067+
# gh-134908: Concurrent iteration over a file caused races
1068+
thread_count = 2
1069+
write_count = 100
1070+
read_count = 100
1071+
1072+
def writer(file, barrier):
1073+
barrier.wait()
1074+
for _ in range(write_count):
1075+
file.write("x")
1076+
1077+
def reader(file, barrier):
1078+
barrier.wait()
1079+
for _ in range(read_count):
1080+
for line in file:
1081+
self.assertEqual(line, "")
1082+
1083+
with self.open(os_helper.TESTFN, "w+") as f:
1084+
barrier = threading.Barrier(thread_count + 1)
1085+
reader = threading.Thread(target=reader, args=(f, barrier))
1086+
writers = [threading.Thread(target=writer, args=(f, barrier))
1087+
for _ in range(thread_count)]
1088+
with threading_helper.catch_threading_exception() as cm:
1089+
with threading_helper.start_threads(writers + [reader]):
1090+
pass
1091+
self.assertIsNone(cm.exc_type)
1092+
1093+
self.assertEqual(os.stat(os_helper.TESTFN).st_size,
1094+
write_count * thread_count)
1095+
10651096

10661097
class CIOTest(IOTest):
10671098

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: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1578,6 +1578,8 @@ _io_TextIOWrapper_detach_impl(textio *self)
15781578
static int
15791579
_textiowrapper_writeflush(textio *self)
15801580
{
1581+
_Py_CRITICAL_SECTION_ASSERT_OBJECT_LOCKED(self);
1582+
15811583
if (self->pending_bytes == NULL)
15821584
return 0;
15831585

@@ -3173,8 +3175,9 @@ _io_TextIOWrapper_close_impl(textio *self)
31733175
}
31743176

31753177
static PyObject *
3176-
textiowrapper_iternext(PyObject *op)
3178+
textiowrapper_iternext_lock_held(PyObject *op)
31773179
{
3180+
_Py_CRITICAL_SECTION_ASSERT_OBJECT_LOCKED(op);
31783181
PyObject *line;
31793182
textio *self = textio_CAST(op);
31803183

@@ -3210,6 +3213,16 @@ textiowrapper_iternext(PyObject *op)
32103213
return line;
32113214
}
32123215

3216+
static PyObject *
3217+
textiowrapper_iternext(PyObject *op)
3218+
{
3219+
PyObject *result;
3220+
Py_BEGIN_CRITICAL_SECTION(op);
3221+
result = textiowrapper_iternext_lock_held(op);
3222+
Py_END_CRITICAL_SECTION();
3223+
return result;
3224+
}
3225+
32133226
/*[clinic input]
32143227
@critical_section
32153228
@getter

0 commit comments

Comments
 (0)