Skip to content

Commit fa0ec86

Browse files
authored
gh-153539: Fix use-after-free in TextIOWrapper.tell() with a reentrant decoder (GH-153540)
TextIOWrapper.tell() used a borrowed next_input from the snapshot across the decoder's getstate/decode/setstate calls, so a decoder that reenters seek() from getstate could free it and leave tell() reading freed memory. Own the reference across those calls, matching the sibling textiowrapper_read_chunk.
1 parent f40043e commit fa0ec86

3 files changed

Lines changed: 53 additions & 1 deletion

File tree

Lib/test/test_io/test_textio.py

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1610,6 +1610,49 @@ def make_text(buffer):
16101610
wrapper.write('x')
16111611
self.assertRaisesRegex(ValueError, "detached", wrapper.read)
16121612

1613+
def test_reentrant_seek_during_tell(self):
1614+
# gh-153539: reading short of _CHUNK_SIZE leaves residual bytes in the
1615+
# snapshot, so tell() re-decodes and calls the decoder's getstate(); a
1616+
# reentrant seek() there must not free the snapshot tell() still uses.
1617+
# C-only: _pyio binds next_input as a strong local and cannot crash.
1618+
wrapper = None
1619+
armed = False
1620+
1621+
class ReentrantDecoder(codecs.IncrementalDecoder):
1622+
def decode(self, input, final=False):
1623+
return bytes(input).decode("latin-1")
1624+
def getstate(self):
1625+
nonlocal armed
1626+
if wrapper is not None and armed:
1627+
armed = False
1628+
wrapper.seek(0)
1629+
return (b"", 0)
1630+
def setstate(self, state):
1631+
pass
1632+
1633+
def search(name):
1634+
if name != "reentrant_tell_test":
1635+
return None
1636+
return codecs.CodecInfo(
1637+
name=name,
1638+
encode=lambda s, e='strict': (s.encode("latin-1"), len(s)),
1639+
decode=lambda b, e='strict': (bytes(b).decode("latin-1"), len(b)),
1640+
incrementaldecoder=ReentrantDecoder)
1641+
1642+
codecs.register(search)
1643+
self.addCleanup(codecs.unregister, search)
1644+
raw = self.BytesIO(b"abcdefghijklmnop" * 8)
1645+
wrapper = self.TextIOWrapper(self.BufferedReader(raw),
1646+
encoding="reentrant_tell_test", newline="")
1647+
wrapper._CHUNK_SIZE = 8
1648+
wrapper.read(5)
1649+
armed = True
1650+
self.assertIsInstance(wrapper.tell(), int)
1651+
# tell() at the snapshot boundary takes the early return that owns and
1652+
# must release next_input; exercise it too (leak-checked under -R).
1653+
wrapper.seek(0)
1654+
self.assertIsInstance(wrapper.tell(), int)
1655+
16131656

16141657
class PyTextIOWrapperTest(TextIOWrapperTest, PyTestCase):
16151658
shutdown_error = "LookupError: unknown encoding: ascii"
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
Fix a crash in the C implementation of :meth:`io.TextIOWrapper.tell` when the
2+
decoder's ``getstate`` method triggers a reentrant seek, or when another thread
3+
seeks the same stream concurrently. Patch by tonghuaroot.

Modules/_io/textio.c

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2823,7 +2823,7 @@ _io_TextIOWrapper_tell_impl(textio *self)
28232823
PyObject *res;
28242824
PyObject *posobj = NULL;
28252825
cookie_type cookie = {0,0,0,0,0};
2826-
PyObject *next_input;
2826+
PyObject *next_input = NULL;
28272827
Py_ssize_t chars_to_skip, chars_decoded;
28282828
Py_ssize_t skip_bytes, skip_back;
28292829
PyObject *saved_state = NULL;
@@ -2875,11 +2875,15 @@ _io_TextIOWrapper_tell_impl(textio *self)
28752875

28762876
assert (PyBytes_Check(next_input));
28772877

2878+
/* Own next_input: a reentrant or concurrent seek can drop the snapshot. */
2879+
Py_INCREF(next_input);
2880+
28782881
cookie.start_pos -= PyBytes_GET_SIZE(next_input);
28792882

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

@@ -3020,6 +3024,7 @@ _io_TextIOWrapper_tell_impl(textio *self)
30203024
}
30213025

30223026
finally:
3027+
Py_XDECREF(next_input);
30233028
res = PyObject_CallMethodOneArg(
30243029
self->decoder, &_Py_ID(setstate), saved_state);
30253030
Py_DECREF(saved_state);
@@ -3032,6 +3037,7 @@ _io_TextIOWrapper_tell_impl(textio *self)
30323037
return textiowrapper_build_cookie(&cookie);
30333038

30343039
fail:
3040+
Py_XDECREF(next_input);
30353041
if (saved_state) {
30363042
PyObject *exc = PyErr_GetRaisedException();
30373043
res = PyObject_CallMethodOneArg(

0 commit comments

Comments
 (0)