Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 43 additions & 0 deletions Lib/test/test_io/test_textio.py
Original file line number Diff line number Diff line change
Expand Up @@ -1610,6 +1610,49 @@ def make_text(buffer):
wrapper.write('x')
self.assertRaisesRegex(ValueError, "detached", wrapper.read)

def test_reentrant_seek_during_tell(self):
# gh-153539: reading short of _CHUNK_SIZE leaves residual bytes in the
# snapshot, so tell() re-decodes and calls the decoder's getstate(); a
# reentrant seek() there must not free the snapshot tell() still uses.
# C-only: _pyio binds next_input as a strong local and cannot crash.
wrapper = None
armed = False

class ReentrantDecoder(codecs.IncrementalDecoder):
def decode(self, input, final=False):
return bytes(input).decode("latin-1")
def getstate(self):
nonlocal armed
if wrapper is not None and armed:
armed = False
wrapper.seek(0)
return (b"", 0)
def setstate(self, state):
pass

def search(name):
if name != "reentrant_tell_test":
return None
return codecs.CodecInfo(
name=name,
encode=lambda s, e='strict': (s.encode("latin-1"), len(s)),
decode=lambda b, e='strict': (bytes(b).decode("latin-1"), len(b)),
incrementaldecoder=ReentrantDecoder)

codecs.register(search)
self.addCleanup(codecs.unregister, search)
raw = self.BytesIO(b"abcdefghijklmnop" * 8)
wrapper = self.TextIOWrapper(self.BufferedReader(raw),
encoding="reentrant_tell_test", newline="")
wrapper._CHUNK_SIZE = 8
wrapper.read(5)
armed = True
self.assertIsInstance(wrapper.tell(), int)
# tell() at the snapshot boundary takes the early return that owns and
# must release next_input; exercise it too (leak-checked under -R).
wrapper.seek(0)
self.assertIsInstance(wrapper.tell(), int)


class PyTextIOWrapperTest(TextIOWrapperTest, PyTestCase):
shutdown_error = "LookupError: unknown encoding: ascii"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Fix a crash in the C implementation of :meth:`io.TextIOWrapper.tell` when the
decoder's ``getstate`` method triggers a reentrant seek. Patch by tonghuaroot.
8 changes: 7 additions & 1 deletion Modules/_io/textio.c
Original file line number Diff line number Diff line change
Expand Up @@ -2823,7 +2823,7 @@ _io_TextIOWrapper_tell_impl(textio *self)
PyObject *res;
PyObject *posobj = NULL;
cookie_type cookie = {0,0,0,0,0};
PyObject *next_input;
PyObject *next_input = NULL;
Py_ssize_t chars_to_skip, chars_decoded;
Py_ssize_t skip_bytes, skip_back;
PyObject *saved_state = NULL;
Expand Down Expand Up @@ -2875,11 +2875,15 @@ _io_TextIOWrapper_tell_impl(textio *self)

assert (PyBytes_Check(next_input));

/* Own next_input: a reentrant seek in the decoder can drop self->snapshot. */
Py_INCREF(next_input);

cookie.start_pos -= PyBytes_GET_SIZE(next_input);

/* How many decoded characters have been used up since the snapshot? */
if (self->decoded_chars_used == 0) {
/* We haven't moved from the snapshot point. */
Py_DECREF(next_input);
return textiowrapper_build_cookie(&cookie);
}

Expand Down Expand Up @@ -3020,6 +3024,7 @@ _io_TextIOWrapper_tell_impl(textio *self)
}

finally:
Py_XDECREF(next_input);
res = PyObject_CallMethodOneArg(
self->decoder, &_Py_ID(setstate), saved_state);
Py_DECREF(saved_state);
Expand All @@ -3032,6 +3037,7 @@ _io_TextIOWrapper_tell_impl(textio *self)
return textiowrapper_build_cookie(&cookie);

fail:
Py_XDECREF(next_input);
if (saved_state) {
PyObject *exc = PyErr_GetRaisedException();
res = PyObject_CallMethodOneArg(
Expand Down
Loading