From c99660763fb6963031bacbf33b0268cfd7594719 Mon Sep 17 00:00:00 2001 From: harjoth Date: Fri, 6 Nov 2026 21:40:19 -0800 Subject: [PATCH] gh-161518: Reject a non-bytes encoder result unless it is the ASCII fast path TextIOWrapper.write() accepted any encoder result that was the same object as its input. That is only safe for ASCII text, so a custom encoder returning a non-ASCII str wrote corrupt data. Co-Authored-By: Claude Sonnet 5.5 --- Lib/test/test_io/test_textio.py | 25 +++++++++++++++++++ ...-11-06-21-50-00.gh-issue-161518.Tx9Kq2.rst | 2 ++ Modules/_io/textio.c | 2 +- 3 files changed, 28 insertions(+), 1 deletion(-) create mode 100644 Misc/NEWS.d/next/Library/2026-11-06-21-50-00.gh-issue-161518.Tx9Kq2.rst diff --git a/Lib/test/test_io/test_textio.py b/Lib/test/test_io/test_textio.py index 6e649240f08d5bf..9d3d5d120cc87b6 100644 --- a/Lib/test/test_io/test_textio.py +++ b/Lib/test/test_io/test_textio.py @@ -1097,6 +1097,31 @@ def test_illegal_encoder(self): t = self.TextIOWrapper(self.BytesIO(b'foo'), encoding="rot13") self.assertRaises(TypeError, t.write, 'bar') + def test_encoder_returns_str(self): + # gh-161518: an encoder returning its non-ASCII input str must be + # rejected, not have its internal storage written as bytes. + class Encoder(codecs.IncrementalEncoder): + def encode(self, input, final=False): + return input + + info = codecs.CodecInfo( + None, None, incrementalencoder=Encoder, + incrementaldecoder=codecs.getincrementaldecoder('utf-8'), + name='test_str_encoder') + search = lambda name: info if name == 'test_str_encoder' else None + codecs.register(search) + self.addCleanup(codecs.unregister, search) + + t = self.TextIOWrapper(self.BytesIO(), encoding='test_str_encoder') + self.assertRaises(TypeError, t.write, 'caf\xe9') + + # The ASCII fast path still works. + raw = self.BytesIO() + t = self.TextIOWrapper(raw, encoding='utf-8') + t.write('cafe') + t.flush() + self.assertEqual(raw.getvalue(), b'cafe') + def test_illegal_decoder(self): # Issue #17106 # Bypass the early encoding check added in issue 20404 diff --git a/Misc/NEWS.d/next/Library/2026-11-06-21-50-00.gh-issue-161518.Tx9Kq2.rst b/Misc/NEWS.d/next/Library/2026-11-06-21-50-00.gh-issue-161518.Tx9Kq2.rst new file mode 100644 index 000000000000000..435d58216fa118c --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-11-06-21-50-00.gh-issue-161518.Tx9Kq2.rst @@ -0,0 +1,2 @@ +:class:`io.TextIOWrapper` now raises :exc:`TypeError`, instead of writing +corrupt output, when an encoder returns its non-ASCII input string. diff --git a/Modules/_io/textio.c b/Modules/_io/textio.c index b6f3f4f17119289..7dd28ace8c370fa 100644 --- a/Modules/_io/textio.c +++ b/Modules/_io/textio.c @@ -1831,7 +1831,7 @@ _io_TextIOWrapper_write_impl(textio *self, PyObject *text) Py_DECREF(text); if (b == NULL) return NULL; - if (b != text && !PyBytes_Check(b)) { + if (!PyBytes_Check(b) && !(b == text && PyUnicode_IS_ASCII(text))) { PyErr_Format(PyExc_TypeError, "encoder should return a bytes object, not '%.200s'", Py_TYPE(b)->tp_name);