Skip to content

Commit 0262ca1

Browse files
miss-islingtonjelle-openaiJelleZijlstra
authored
[3.15] gh-159739: Keep text I/O state valid across reentrant callbacks (GH-160398) (#160623)
A raw read can reinitialize a TextIOWrapper as write-only, and codec callbacks or finalizers can release an encoder, decoder, or decoded chunk while a read or write still needs it. This can crash the interpreter. Check that a decoder is still available after reading, keep codecs alive across calls, and read the chunk length before transferring ownership. Also recheck StringIO after converting the truncate size because __index__() can close the stream. (cherry picked from commit 665bda9) Co-authored-by: Jelle Zijlstra <jelle@openai.com> Co-authored-by: Jelle Zijlstra <jelle.zijlstra@gmail.com>
1 parent 2304b68 commit 0262ca1

5 files changed

Lines changed: 48 additions & 11 deletions

File tree

‎Lib/test/test_io/test_memoryio.py‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1004,6 +1004,18 @@ class CStringIOTest(PyStringIOTest):
10041004
ioclass = io.StringIO
10051005
UnsupportedOperation = io.UnsupportedOperation
10061006

1007+
def test_truncate_size_closes_stream(self):
1008+
stream = self.ioclass()
1009+
stream.write("abc")
1010+
1011+
class Size:
1012+
def __index__(self):
1013+
stream.close()
1014+
return 0
1015+
1016+
with self.assertRaises(ValueError):
1017+
stream.truncate(Size())
1018+
10071019
def test_truncate_large_size(self):
10081020
# Sizes above 2**31 - 1 must work where C long is 32-bit.
10091021
if sys.maxsize > 2**31:

‎Lib/test/test_io/test_textio.py‎

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1676,6 +1676,17 @@ def make_text(buffer):
16761676
wrapper.write('x')
16771677
self.assertRaisesRegex(ValueError, "detached", wrapper.read)
16781678

1679+
def test_read_reinitialized_as_write_only(self):
1680+
class Buffer(self.BytesIO):
1681+
def read(self, size=-1):
1682+
stream.__init__(writer, encoding="ascii")
1683+
return b"a"
1684+
1685+
with (self.BufferedWriter(self.BytesIO()) as writer,
1686+
self.TextIOWrapper(Buffer(), encoding="ascii") as stream):
1687+
with self.assertRaises(self.UnsupportedOperation):
1688+
stream.read()
1689+
16791690
def test_reentrant_detach_during_read(self):
16801691
# gh-157363, gh-157364: The buffer must stay alive until its active
16811692
# read operation returns.
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
Fix crashes in :class:`io.TextIOWrapper` and
2+
:class:`io.IncrementalNewlineDecoder` when a callback changes the active
3+
codec or decoded buffer. :class:`io.StringIO` now raises :exc:`ValueError`
4+
if converting the size passed to ``truncate()`` closes the stream.

‎Modules/_io/stringio.c‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -478,6 +478,9 @@ _io_StringIO_truncate_impl(stringio *self, PyObject *pos)
478478
"negative pos value %zd", size);
479479
return NULL;
480480
}
481+
/* __index__ may have closed or reinitialized the stream. */
482+
CHECK_INITIALIZED(self);
483+
CHECK_CLOSED(self);
481484
}
482485

483486
if (size < self->string_size) {

‎Modules/_io/textio.c‎

Lines changed: 18 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -336,8 +336,11 @@ _PyIncrementalNewlineDecoder_decode(PyObject *myself,
336336

337337
/* decode input (with the eventual \r from a previous pass) */
338338
if (self->decoder != Py_None) {
339-
output = PyObject_CallMethodObjArgs(self->decoder,
339+
/* The wrapper can be reinitialized by a decoding callback. */
340+
PyObject *decoder = Py_NewRef(self->decoder);
341+
output = PyObject_CallMethodObjArgs(decoder,
340342
&_Py_ID(decode), input, final ? Py_True : Py_False, NULL);
343+
Py_DECREF(decoder);
341344
}
342345
else {
343346
output = Py_NewRef(input);
@@ -1001,11 +1004,17 @@ _textiowrapper_decode(_PyIO_State *state, PyObject *decoder, PyObject *bytes,
10011004
{
10021005
PyObject *chars;
10031006

1007+
if (decoder == NULL) {
1008+
return _unsupported(state, "not readable");
1009+
}
1010+
/* A codec callback may replace the wrapper's decoder. */
1011+
Py_INCREF(decoder);
10041012
if (Py_IS_TYPE(decoder, state->PyIncrementalNewlineDecoder_Type))
10051013
chars = _PyIncrementalNewlineDecoder_decode(decoder, bytes, eof);
10061014
else
10071015
chars = PyObject_CallMethodObjArgs(decoder, &_Py_ID(decode), bytes,
10081016
eof ? Py_True : Py_False, NULL);
1017+
Py_DECREF(decoder);
10091018

10101019
if (check_decoded(chars) < 0)
10111020
// check_decoded already decreases refcount
@@ -1784,7 +1793,10 @@ _io_TextIOWrapper_write_impl(textio *self, PyObject *text)
17841793
self->encoding_start_of_stream = 0;
17851794
}
17861795
else {
1787-
b = PyObject_CallMethodOneArg(self->encoder, &_Py_ID(encode), text);
1796+
/* A codec callback may reconfigure the stream. */
1797+
PyObject *encoder = Py_NewRef(self->encoder);
1798+
b = PyObject_CallMethodOneArg(encoder, &_Py_ID(encode), text);
1799+
Py_DECREF(encoder);
17881800
}
17891801

17901802
Py_DECREF(text);
@@ -2024,8 +2036,9 @@ textiowrapper_read_chunk(textio *self, Py_ssize_t size_hint)
20242036
if (decoded_chars == NULL)
20252037
goto fail;
20262038

2027-
textiowrapper_set_decoded_chars(self, decoded_chars);
2039+
/* The old chunk's finalizer can clear the new chunk. */
20282040
nchars = PyUnicode_GET_LENGTH(decoded_chars);
2041+
textiowrapper_set_decoded_chars(self, decoded_chars);
20292042
if (nchars > 0)
20302043
self->b2cratio = (double) nbytes / nchars;
20312044
else
@@ -2097,15 +2110,9 @@ _io_TextIOWrapper_read_impl(textio *self, Py_ssize_t n)
20972110
return NULL;
20982111
}
20992112

2100-
_PyIO_State *state = self->state;
2101-
if (Py_IS_TYPE(self->decoder, state->PyIncrementalNewlineDecoder_Type))
2102-
decoded = _PyIncrementalNewlineDecoder_decode(self->decoder,
2103-
bytes, 1);
2104-
else
2105-
decoded = PyObject_CallMethodObjArgs(
2106-
self->decoder, &_Py_ID(decode), bytes, Py_True, NULL);
2113+
decoded = _textiowrapper_decode(self->state, self->decoder, bytes, 1);
21072114
Py_DECREF(bytes);
2108-
if (check_decoded(decoded) < 0)
2115+
if (decoded == NULL)
21092116
goto fail;
21102117

21112118
result = textiowrapper_get_decoded_chars(self, -1);

0 commit comments

Comments
 (0)