From 5f3825b8baface39cb40038a0b81f13fa6d6601b Mon Sep 17 00:00:00 2001 From: Peter Bierma Date: Wed, 11 Jun 2025 19:05:02 -0400 Subject: [PATCH 1/3] Fix thread safety in StringIO. --- Modules/_io/stringio.c | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/Modules/_io/stringio.c b/Modules/_io/stringio.c index 56913fafefba8b..9cf1e0e3e7c278 100644 --- a/Modules/_io/stringio.c +++ b/Modules/_io/stringio.c @@ -79,6 +79,7 @@ static int _io_StringIO___init__(PyObject *self, PyObject *args, PyObject *kwarg static int resize_buffer(stringio *self, size_t size) { + _Py_CRITICAL_SECTION_ASSERT_OBJECT_LOCKED(self); /* Here, unsigned types are used to avoid dealing with signed integer overflow, which is undefined in C. */ size_t alloc = self->buf_size; @@ -131,6 +132,7 @@ resize_buffer(stringio *self, size_t size) static PyObject * make_intermediate(stringio *self) { + _Py_CRITICAL_SECTION_ASSERT_OBJECT_LOCKED(self); PyObject *intermediate = PyUnicodeWriter_Finish(self->writer); self->writer = NULL; self->state = STATE_REALIZED; @@ -153,6 +155,7 @@ make_intermediate(stringio *self) static int realize(stringio *self) { + _Py_CRITICAL_SECTION_ASSERT_OBJECT_LOCKED(self); Py_ssize_t len; PyObject *intermediate; @@ -188,6 +191,7 @@ realize(stringio *self) static Py_ssize_t write_str(stringio *self, PyObject *obj) { + _Py_CRITICAL_SECTION_ASSERT_OBJECT_LOCKED(self); Py_ssize_t len; PyObject *decoded = NULL; @@ -355,6 +359,7 @@ _io_StringIO_read_impl(stringio *self, Py_ssize_t size) static PyObject * _stringio_readline(stringio *self, Py_ssize_t limit) { + _Py_CRITICAL_SECTION_ASSERT_OBJECT_LOCKED(self); Py_UCS4 *start, *end, old_char; Py_ssize_t len, consumed; @@ -404,8 +409,9 @@ _io_StringIO_readline_impl(stringio *self, Py_ssize_t size) } static PyObject * -stringio_iternext(PyObject *op) +stringio_iternext_lock_held(PyObject *op) { + _Py_CRITICAL_SECTION_ASSERT_OBJECT_LOCKED(op); PyObject *line; stringio *self = stringio_CAST(op); @@ -441,6 +447,16 @@ stringio_iternext(PyObject *op) return line; } +static PyObject * +stringio_iternext(PyObject *op) +{ + PyObject *res; + Py_BEGIN_CRITICAL_SECTION(op); + res = stringio_iternext_lock_held(op); + Py_END_CRITICAL_SECTION(); + return res; +} + /*[clinic input] @critical_section _io.StringIO.truncate From 25d2f2723fe5ea711857ab8975b4e7ca5db3052c Mon Sep 17 00:00:00 2001 From: Peter Bierma Date: Wed, 11 Jun 2025 19:05:53 -0400 Subject: [PATCH 2/3] Add blurb. --- .../next/Library/2025-06-11-19-05-49.gh-issue-135410.E89Boi.rst | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 Misc/NEWS.d/next/Library/2025-06-11-19-05-49.gh-issue-135410.E89Boi.rst diff --git a/Misc/NEWS.d/next/Library/2025-06-11-19-05-49.gh-issue-135410.E89Boi.rst b/Misc/NEWS.d/next/Library/2025-06-11-19-05-49.gh-issue-135410.E89Boi.rst new file mode 100644 index 00000000000000..a5917fba3f7bb9 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2025-06-11-19-05-49.gh-issue-135410.E89Boi.rst @@ -0,0 +1,2 @@ +Fix a crash when iterating over :class:`io.StringIO` on the :term:`free +threaded ` build. From 455bfba87b715c60e4a48dd26910823fef34d6be Mon Sep 17 00:00:00 2001 From: Peter Bierma Date: Wed, 11 Jun 2025 19:15:36 -0400 Subject: [PATCH 3/3] Add a test. --- Lib/test/test_memoryio.py | 19 +++++++++++++++++++ Modules/_io/stringio.c | 6 ------ 2 files changed, 19 insertions(+), 6 deletions(-) diff --git a/Lib/test/test_memoryio.py b/Lib/test/test_memoryio.py index 63998a86c45b53..249e0f3ba32f29 100644 --- a/Lib/test/test_memoryio.py +++ b/Lib/test/test_memoryio.py @@ -5,6 +5,7 @@ import unittest from test import support +from test.support import threading_helper import gc import io @@ -12,6 +13,7 @@ import pickle import sys import weakref +import threading class IntLike: def __init__(self, num): @@ -723,6 +725,22 @@ def test_newline_argument(self): for newline in (None, "", "\n", "\r", "\r\n"): self.ioclass(newline=newline) + @unittest.skipUnless(support.Py_GIL_DISABLED, "only meaningful under free-threading") + @threading_helper.requires_working_threading() + def test_concurrent_use(self): + memio = self.ioclass("") + + def use(): + memio.write("x" * 10) + memio.readlines() + + threads = [threading.Thread(target=use) for _ in range(8)] + with threading_helper.catch_threading_exception() as cm: + with threading_helper.start_threads(threads): + pass + + self.assertIsNone(cm.exc_value) + class PyStringIOTest(MemoryTestMixin, MemorySeekTestMixin, TextIOTestMixin, unittest.TestCase): @@ -890,6 +908,7 @@ def test_setstate(self): self.assertRaises(ValueError, memio.__setstate__, ("closed", "", 0, None)) + class CStringIOPickleTest(PyStringIOPickleTest): UnsupportedOperation = io.UnsupportedOperation diff --git a/Modules/_io/stringio.c b/Modules/_io/stringio.c index 9cf1e0e3e7c278..8482c268176c5b 100644 --- a/Modules/_io/stringio.c +++ b/Modules/_io/stringio.c @@ -79,7 +79,6 @@ static int _io_StringIO___init__(PyObject *self, PyObject *args, PyObject *kwarg static int resize_buffer(stringio *self, size_t size) { - _Py_CRITICAL_SECTION_ASSERT_OBJECT_LOCKED(self); /* Here, unsigned types are used to avoid dealing with signed integer overflow, which is undefined in C. */ size_t alloc = self->buf_size; @@ -132,7 +131,6 @@ resize_buffer(stringio *self, size_t size) static PyObject * make_intermediate(stringio *self) { - _Py_CRITICAL_SECTION_ASSERT_OBJECT_LOCKED(self); PyObject *intermediate = PyUnicodeWriter_Finish(self->writer); self->writer = NULL; self->state = STATE_REALIZED; @@ -155,7 +153,6 @@ make_intermediate(stringio *self) static int realize(stringio *self) { - _Py_CRITICAL_SECTION_ASSERT_OBJECT_LOCKED(self); Py_ssize_t len; PyObject *intermediate; @@ -191,7 +188,6 @@ realize(stringio *self) static Py_ssize_t write_str(stringio *self, PyObject *obj) { - _Py_CRITICAL_SECTION_ASSERT_OBJECT_LOCKED(self); Py_ssize_t len; PyObject *decoded = NULL; @@ -359,7 +355,6 @@ _io_StringIO_read_impl(stringio *self, Py_ssize_t size) static PyObject * _stringio_readline(stringio *self, Py_ssize_t limit) { - _Py_CRITICAL_SECTION_ASSERT_OBJECT_LOCKED(self); Py_UCS4 *start, *end, old_char; Py_ssize_t len, consumed; @@ -411,7 +406,6 @@ _io_StringIO_readline_impl(stringio *self, Py_ssize_t size) static PyObject * stringio_iternext_lock_held(PyObject *op) { - _Py_CRITICAL_SECTION_ASSERT_OBJECT_LOCKED(op); PyObject *line; stringio *self = stringio_CAST(op);