Skip to content

Commit db4b194

Browse files
cmaloneyyihong0618vstinner
authored
gh-143008: Fix Null pointer dereferences in TextIOWrapper underlying stream access (#145957)
TextIOWrapper keeps its underlying stream in a member called `self->buffer`. That stream can be detached by user code, such as custom `.flush` implementations resulting in `self->buffer` being set to NULL. The implementation often checked at the start of functions if `self->buffer` is in a good state, but did not always recheck after other Python code was called which could modify `self->buffer`. The cases which need to be re-checked are hard to spot so rather than rely on reviewer effort create better safety by making all self->buffer access go through helper functions. Thank you yihong0618 for the test, NEWS and initial implementation in gh-143041. Co-authored-by: yihong0618 <zouzou0208@gmail.com> Co-authored-by: Victor Stinner <vstinner@python.org>
1 parent 3186547 commit db4b194

5 files changed

Lines changed: 176 additions & 38 deletions

File tree

Lib/test/test_io/test_textio.py

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1560,6 +1560,56 @@ 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 not crash.
1565+
1566+
class DetachOnce(self.BufferedRandom):
1567+
wrapper = None
1568+
1569+
def detach_once(self):
1570+
original = self.wrapper
1571+
self.wrapper = None
1572+
if original is not None:
1573+
original.detach()
1574+
original.flush()
1575+
1576+
class DetachOnFlush(DetachOnce):
1577+
def flush(self):
1578+
self.detach_once()
1579+
1580+
class DetachOnWrite(DetachOnce):
1581+
def write(self, b):
1582+
self.detach_once()
1583+
return len(b)
1584+
1585+
# Separate reference for after detach_once.
1586+
wrapper = None
1587+
1588+
def make_text(buffer):
1589+
nonlocal wrapper
1590+
buffer.wrapper = self.TextIOWrapper(buffer, encoding='utf-8')
1591+
wrapper = buffer.wrapper
1592+
1593+
# Many calls could result in the same null self->buffer crash.
1594+
tests = [
1595+
('truncate', lambda: wrapper.truncate(0)),
1596+
('close', lambda: wrapper.close()),
1597+
('detach', lambda: wrapper.detach()),
1598+
('seek', lambda: wrapper.seek(0)),
1599+
('tell', lambda: wrapper.tell()),
1600+
('reconfigure', lambda: wrapper.reconfigure(line_buffering=True)),
1601+
]
1602+
for name, method in tests:
1603+
with self.subTest(name):
1604+
make_text(DetachOnFlush(self.MockRawIO()))
1605+
self.assertRaisesRegex(ValueError, "detached", method)
1606+
1607+
# Should not crash.
1608+
with self.subTest('read via writeflush'):
1609+
make_text(DetachOnWrite(self.MockRawIO()))
1610+
wrapper.write('x')
1611+
self.assertRaisesRegex(ValueError, "detached", wrapper.read)
1612+
15631613

15641614
class PyTextIOWrapperTest(TextIOWrapperTest, PyTestCase):
15651615
shutdown_error = "LookupError: unknown encoding: ascii"
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Fix crash in :class:`io.TextIOWrapper` when reentrant
2+
:meth:`io.TextIOBase.detach` is called reentrantly from the underlying buffer.
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Fix race conditions when re-initializing a :class:`io.TextIOWrapper` object.

Modules/_io/clinic/textio.c.h

Lines changed: 3 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)