Skip to content

Commit f7eca0d

Browse files
committed
fix: TextIOWrapper.truncate via re-entrant flush
Signed-off-by: yihong0618 <zouzou0208@gmail.com>
1 parent 8b64dd8 commit f7eca0d

File tree

3 files changed

+75
-7
lines changed

3 files changed

+75
-7
lines changed

Lib/test/test_io/test_textio.py

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1560,6 +1560,54 @@ def closed(self):
15601560
wrapper = self.TextIOWrapper(raw)
15611561
wrapper.close() # should not crash
15621562

1563+
def test_reentrant_detach_during_flush(self):
1564+
# gh-143008: Reentrant detach() during flush should raise RuntimeError
1565+
# instead of crashing.
1566+
wrapper = None
1567+
1568+
class BadRaw(self.RawIOBase):
1569+
def write(self, b): return len(b)
1570+
def read(self, n=-1): return b''
1571+
def readable(self): return True
1572+
def writable(self): return True
1573+
def seekable(self): return True
1574+
def seek(self, pos, whence=0): return 0
1575+
def tell(self): return 0
1576+
1577+
class EvilBuffer(self.BufferedRandom):
1578+
detach_on_write = False
1579+
1580+
def flush(self):
1581+
if wrapper is not None and not self.detach_on_write:
1582+
wrapper.detach()
1583+
return super().flush()
1584+
1585+
def write(self, b):
1586+
if wrapper is not None and self.detach_on_write:
1587+
wrapper.detach()
1588+
return len(b)
1589+
1590+
tests = [
1591+
('truncate', lambda: wrapper.truncate(0)),
1592+
('close', lambda: wrapper.close()),
1593+
('detach', lambda: wrapper.detach()),
1594+
('seek', lambda: wrapper.seek(0)),
1595+
('tell', lambda: wrapper.tell()),
1596+
('reconfigure', lambda: wrapper.reconfigure(line_buffering=True)),
1597+
]
1598+
for name, method in tests:
1599+
with self.subTest(name):
1600+
wrapper = self.TextIOWrapper(EvilBuffer(BadRaw()), encoding='utf-8')
1601+
self.assertRaisesRegex(RuntimeError, "reentrant", method)
1602+
wrapper = None
1603+
1604+
with self.subTest('read via writeflush'):
1605+
EvilBuffer.detach_on_write = True
1606+
wrapper = self.TextIOWrapper(EvilBuffer(BadRaw()), encoding='utf-8')
1607+
wrapper.write('x')
1608+
self.assertRaisesRegex(RuntimeError, "reentrant", wrapper.read)
1609+
wrapper = None
1610+
15631611

15641612
class PyTextIOWrapperTest(TextIOWrapperTest, PyTestCase):
15651613
shutdown_error = "LookupError: unknown encoding: ascii"
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Fix crash in :class:`io.TextIOWrapper` when reentrant ``detach()`` called.

Modules/_io/textio.c

Lines changed: 26 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -667,6 +667,7 @@ struct textio
667667
PyObject_HEAD
668668
int ok; /* initialized? */
669669
int detached;
670+
int flushing; /* prevent reentrant detach during flush */
670671
Py_ssize_t chunk_size;
671672
PyObject *buffer;
672673
PyObject *encoding;
@@ -725,6 +726,16 @@ struct textio
725726

726727
#define textio_CAST(op) ((textio *)(op))
727728

729+
/* gh-143007 need to check for reentrant flush */
730+
static inline int
731+
_textiowrapper_flush(textio *self)
732+
{
733+
self->flushing = 1;
734+
int result = _PyFile_Flush((PyObject *)self);
735+
self->flushing = 0;
736+
return result;
737+
}
738+
728739
static void
729740
textiowrapper_set_decoded_chars(textio *self, PyObject *chars);
730741

@@ -1108,6 +1119,7 @@ _io_TextIOWrapper___init___impl(textio *self, PyObject *buffer,
11081119

11091120
self->ok = 0;
11101121
self->detached = 0;
1122+
self->flushing = 0;
11111123

11121124
if (encoding == NULL) {
11131125
PyInterpreterState *interp = _PyInterpreterState_GET();
@@ -1422,7 +1434,7 @@ _io_TextIOWrapper_reconfigure_impl(textio *self, PyObject *encoding,
14221434
return NULL;
14231435
}
14241436

1425-
if (_PyFile_Flush((PyObject *)self) < 0) {
1437+
if (_textiowrapper_flush(self) < 0) {
14261438
return NULL;
14271439
}
14281440
self->b2cratio = 0;
@@ -1565,7 +1577,12 @@ _io_TextIOWrapper_detach_impl(textio *self)
15651577
{
15661578
PyObject *buffer;
15671579
CHECK_ATTACHED(self);
1568-
if (_PyFile_Flush((PyObject *)self) < 0) {
1580+
if (self->flushing) {
1581+
PyErr_SetString(PyExc_RuntimeError,
1582+
"reentrant call to detach() is not allowed");
1583+
return NULL;
1584+
}
1585+
if (_textiowrapper_flush(self) < 0) {
15691586
return NULL;
15701587
}
15711588
buffer = self->buffer;
@@ -1636,9 +1653,11 @@ _textiowrapper_writeflush(textio *self)
16361653
Py_DECREF(pending);
16371654

16381655
PyObject *ret;
1656+
self->flushing = 1;
16391657
do {
16401658
ret = PyObject_CallMethodOneArg(self->buffer, &_Py_ID(write), b);
16411659
} while (ret == NULL && _PyIO_trap_eintr());
1660+
self->flushing = 0;
16421661
Py_DECREF(b);
16431662
// NOTE: We cleared buffer but we don't know how many bytes are actually written
16441663
// when an error occurred.
@@ -2583,7 +2602,7 @@ _io_TextIOWrapper_seek_impl(textio *self, PyObject *cookieObj, int whence)
25832602
goto fail;
25842603
}
25852604

2586-
if (_PyFile_Flush((PyObject *)self) < 0) {
2605+
if (_textiowrapper_flush(self) < 0) {
25872606
goto fail;
25882607
}
25892608

@@ -2630,7 +2649,7 @@ _io_TextIOWrapper_seek_impl(textio *self, PyObject *cookieObj, int whence)
26302649
goto fail;
26312650
}
26322651

2633-
if (_PyFile_Flush((PyObject *)self) < 0) {
2652+
if (_textiowrapper_flush(self) < 0) {
26342653
goto fail;
26352654
}
26362655

@@ -2757,7 +2776,7 @@ _io_TextIOWrapper_tell_impl(textio *self)
27572776

27582777
if (_textiowrapper_writeflush(self) < 0)
27592778
return NULL;
2760-
if (_PyFile_Flush((PyObject *)self) < 0) {
2779+
if (_textiowrapper_flush(self) < 0) {
27612780
goto fail;
27622781
}
27632782

@@ -2967,7 +2986,7 @@ _io_TextIOWrapper_truncate_impl(textio *self, PyObject *pos)
29672986
{
29682987
CHECK_ATTACHED(self)
29692988

2970-
if (_PyFile_Flush((PyObject *)self) < 0) {
2989+
if (_textiowrapper_flush(self) < 0) {
29712990
return NULL;
29722991
}
29732992

@@ -3165,7 +3184,7 @@ _io_TextIOWrapper_close_impl(textio *self)
31653184
PyErr_Clear();
31663185
}
31673186
}
3168-
if (_PyFile_Flush((PyObject *)self) < 0) {
3187+
if (_textiowrapper_flush(self) < 0) {
31693188
exc = PyErr_GetRaisedException();
31703189
}
31713190

0 commit comments

Comments
 (0)