]> git.ipfire.org Git - thirdparty/Python/cpython.git/commitdiff
gh-134908: Protect `textiowrapper_iternext` with critical section (gh-134910)
authorDuane Griffin <duaneg@dghda.com>
Mon, 2 Jun 2025 17:22:41 +0000 (05:22 +1200)
committerGitHub <noreply@github.com>
Mon, 2 Jun 2025 17:22:41 +0000 (17:22 +0000)
The `textiowrapper_iternext` function called `_textiowrapper_writeflush`, but did not
use a critical section, making it racy in free-threaded builds.

Lib/test/test_io.py
Misc/NEWS.d/next/Core_and_Builtins/2025-05-30-15-56-19.gh-issue-134908.3a7PxM.rst [new file with mode: 0644]
Modules/_io/textio.c

index 168e66c5a3f0e0346c22df3e69db03f9cdc77893..0c921ffbc2576aa4c4d989d0b0735d3609ba5ef1 100644 (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):
 
diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2025-05-30-15-56-19.gh-issue-134908.3a7PxM.rst b/Misc/NEWS.d/next/Core_and_Builtins/2025-05-30-15-56-19.gh-issue-134908.3a7PxM.rst
new file mode 100644 (file)
index 0000000..3178f0a
--- /dev/null
@@ -0,0 +1 @@
+Fix crash when iterating over lines in a text file on the :term:`free threaded <free threading>` build.
index 86328e46a7b13117d0d5351c7a655b9d5040333a..3808ecdceb9b7048e0872434703f221204e3aa11 100644 (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