From 1a3cc76c41383629a84416fd5d895ab56ecbf350 Mon Sep 17 00:00:00 2001 From: Vyron Vasileiadis Date: Sat, 26 Sep 2026 00:05:50 -0400 Subject: [PATCH] gh-158213: Don't return a str subclass from PyUnicodeWriter_Finish() Since GH-157861, every writer uses the read-only optimization of _PyUnicodeWriter_WriteStr(), so the first write of a str subclass instance into an empty writer kept that object as the buffer, and PyUnicodeWriter_Finish() returned it. io.StringIO.getvalue() then returned the written object itself, and the next write re-read it through its __str__() method, which changed the contents and could make read() read past the end of the buffer. Only use the read-only optimization for exact str objects. A subclass is copied into a new buffer, as before GH-157861. --- Lib/test/test_capi/test_unicode.py | 12 ++++++++++++ Lib/test/test_io/test_memoryio.py | 6 +++++- Objects/unicode_writer.c | 2 +- 3 files changed, 18 insertions(+), 2 deletions(-) diff --git a/Lib/test/test_capi/test_unicode.py b/Lib/test/test_capi/test_unicode.py index f4bd961017b0ede..032b910a280083b 100644 --- a/Lib/test/test_capi/test_unicode.py +++ b/Lib/test/test_capi/test_unicode.py @@ -1910,6 +1910,18 @@ def test_create(self): self.assertGreater(writer.get_buffer()[0], len(s)) self.assertEqual(writer.finish(), s) + def test_str_subclass(self): + # The read-only optimization must not return a str subclass + class MyStr(str): + def __str__(self): + return self + + writer = self.create_writer(0) + writer.write_str(MyStr('abc')) + result = writer.finish() + self.assertEqual(result, 'abc') + self.assertIs(type(result), str) + def test_repr_null(self): writer = self.create_writer(0) writer.write_utf8(b'var=', -1) diff --git a/Lib/test/test_io/test_memoryio.py b/Lib/test/test_io/test_memoryio.py index b378505aa8f7db2..b6f3aa93e7aa43f 100644 --- a/Lib/test/test_io/test_memoryio.py +++ b/Lib/test/test_io/test_memoryio.py @@ -1118,7 +1118,11 @@ def __str__(self): s = MyStr("correct") memio = self.ioclass() memio.write(s) - self.assertEqual(memio.getvalue(), "correct") + value = memio.getvalue() + self.assertEqual(value, "correct") + self.assertIs(type(value), str) + memio.write("!") + self.assertEqual(memio.getvalue(), "correct!") # Also test the fast path where pos == string_size (STATE_ACCUMULATING) memio2 = self.ioclass() diff --git a/Objects/unicode_writer.c b/Objects/unicode_writer.c index d6564ce84ed54eb..c1a2af4d9ac1fe9 100644 --- a/Objects/unicode_writer.c +++ b/Objects/unicode_writer.c @@ -313,7 +313,7 @@ _PyUnicodeWriter_WriteStr(_PyUnicodeWriter *writer, PyObject *str) Py_UCS4 maxchar = PyUnicode_MAX_CHAR_VALUE(str); if (maxchar > writer->maxchar || len > writer->size - writer->pos) { - if (writer->buffer == NULL) { + if (writer->buffer == NULL && PyUnicode_CheckExact(str)) { assert(_PyUnicode_CheckConsistency(str, 1)); writer->readonly = 1; writer->buffer = Py_NewRef(str);