Skip to content

gh-157364: Fix use-after-free in TextIOWrapper during reentrant detach - #157370

Open
ashm-dev wants to merge 3 commits into
python:mainfrom
ashm-dev:gh-157364
Open

gh-157364: Fix use-after-free in TextIOWrapper during reentrant detach#157370
ashm-dev wants to merge 3 commits into
python:mainfrom
ashm-dev:gh-157364

Conversation

@ashm-dev

@ashm-dev ashm-dev commented Sep 12, 2026

Copy link
Copy Markdown
Contributor

Comment thread Modules/_io/textio.c Outdated
Comment thread Modules/_io/textio.c Outdated

@vstinner vstinner left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM. I just have a last request on the test.

@cmaloney: Would you mind to double check this change? You wrote the first iteration if I recall correctly.

wrapper = self.TextIOWrapper(
self.BufferedReader(raw), encoding="utf-8")
method = getattr(wrapper, method_name)
self.assertEqual(method(), "ab\n")

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you check that wrapper is actually detached? Maybe get wrapper.buffer and expect PyExc_ValueError("underlying buffer has been detached")?

@cmaloney

Copy link
Copy Markdown
Contributor

I will need a couple more days to look at this, it seems like this is more invasive than necessary to me and makes a number of not needed for the core UAF bug report.

@cmaloney

cmaloney commented Sep 14, 2026

Copy link
Copy Markdown
Contributor

For gh-157364 I think the bug, and solution, is actually in Buffered I/O. This PR changes Text I/O to keep one more reference, which may also be needed. The issue though is that Buffered I/O has an internal allocation which it passes as an argument to the Raw I/O .readinto method. That Raw I/O call may store and use that reference for an arbitrary amount of time. As long as it is stored the Buffered I/O should not get deallocated.

That makes two pieces to fix here:

  1. The Buffer Protocol object currently doesn't reference the Buffered I/O that allocates and deallocates the buffer. That means if the buffer is stored anywhere than used later you can get a use after free. No Text I/O needed.
    if (PyBuffer_FillInfo(&buf, NULL, start, len, 0, PyBUF_CONTIG) == -1)
  2. The Buffered I/O while buffered.read() is executing gets de-allocated. Keeping an additional reference in the Text I/O for that case will prevent that but it also feels like "When in a method on an object the interpreter should keep that object alive".

@ashm-dev

Copy link
Copy Markdown
Contributor Author

Thanks for looking into this, @cmaloney!

Regarding point 1: passing (PyObject *)self to PyBuffer_FillInfo at line 1629 unfortunately doesn't work. Immediately after, PyMemoryView_FromBuffer(&buf) explicitly clears master.obj (Objects/memoryobject.c:787):

mbuf->master = *info;
mbuf->master.obj = NULL;

Because of this, the resulting memoryview still does not hold a reference to self (b.obj remains None). Additionally, PyBuffer_FillInfo acquires a new reference via Py_XNewRef(obj), which PyMemoryView_FromBuffer drops without Py_DECREF, causing self to leak a reference on every single read. Also, for read1(), the underlying buffer isn't even self->buffer—it is transient memory from PyBytesWriter.

Regarding point 2: CPython method calls via PyObject_CallMethod* do not automatically keep self alive if the caller holds only a borrowed reference. Since calling read1() triggers arbitrary Python code (via the raw stream's readinto), reentrantly detaching or clearing the buffer from Python code drops the last reference. Keeping a strong reference across the call in TextIOWrapper is the standard pattern across CPython for guarding against reentrant deallocation here.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants