GV

GeezerVison

The World Wide Web, restored to 800 pixels of glory.

Home|About|Gaming cabinet|Archives|How it works|Starter sites

On this site

  • Sign in
  • ActionsCI/CD and automation
  • CodespacesCloud development environments
  • All Features
  • Documentation
  • Training
  • Certifications
  • Build with Copilot
  • Customize Copilot
  • Integrations
  • Trending repos
  • GitHub Sponsors
  • Developer articles
  • Enterprises

This view

  • Original page
  • New address

GeezerVison is showing github.com — gh-157364: Fix use-after-free in TextIOWrapper during reentrant detach by ashm-dev · Pull Request #157370 · python/cpython

Conversation

@ashm-dev

@ashm-dev ashm-dev commented Sep 12, 2026 •
edited by bedevere-app Bot
Loading

Uh oh!

There was an error while loading. Please reload this page.

Copy link
Copy Markdown
Contributor
  • Issue: io.TextIOWrapper UAF triggered by readline() #157364

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

All reactions
@ashm-dev
fix(io): keep TextIOWrapper buffer alive during calls
979a884
@bedevere-app bedevere-app Bot mentioned this pull request Sep 12, 2026
io.TextIOWrapper UAF triggered by readline() #157364
Closed
@bedevere-app bedevere-app Bot added the awaiting review label Sep 12, 2026
@ashm-dev
Merge branch 'main' into pythongh-157364
05510fb
vstinner
vstinner reviewed Sep 12, 2026
View reviewed changes
Comment thread Modules/_io/textio.c Resolved

Uh oh!

There was an error while loading. Please reload this page.

Comment thread Modules/_io/textio.c Resolved

Uh oh!

There was an error while loading. Please reload this page.

@ashm-dev
refactor(io): return strong ref from buffer_access_safe
c3755e4
@ashm-dev
ashm-dev requested a review from vstinner September 13, 2026 08:12
@ashm-dev ashm-dev mentioned this pull request Sep 13, 2026
Heap UAF in io.TextIOWrapper read path via reentrant detach() #157363
Open
vstinner
vstinner approved these changes Sep 13, 2026
View reviewed changes

@vstinner vstinner left a comment

Copy link
Copy Markdown
Member

There was a problem hiding this comment.

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.

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

cmaloney reacted with eyes emoji
All reactions
  • 👀 1 reaction
Comment thread Lib/test/test_io/test_textio.py Resolved

Uh oh!

There was an error while loading. Please reload this page.

@bedevere-app bedevere-app Bot added awaiting merge and removed awaiting review labels Sep 13, 2026
@cmaloney

cmaloney commented Sep 14, 2026

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.

All reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@cmaloney

cmaloney commented Sep 14, 2026 •
edited
Loading

Uh oh!

There was an error while loading. Please reload this page.

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.

    cpython/Modules/_io/bufferedio.c

    Line 1629 in fd0970c

    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".
All reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@ashm-dev

ashm-dev commented Sep 14, 2026

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.

All reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@cmaloney

cmaloney commented Sep 21, 2026 •
edited
Loading

Uh oh!

There was an error while loading. Please reload this page.

Copy link
Copy Markdown
Contributor

For 1: The memoryview can be stored so it needs to keep the BufferedIO whose allocation it is filling alive otherwise use after free is possible. That there is no reference is a bug. I agree the current set of calls don't give a good path there, likely other memoryview / Buffer Protocol methods need to be used. It looks like PyMemoryView_FromObjectAndFlags may be a good fit.

All reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@ashm-dev
fix(io): keep BufferedReader alive via readinto memoryview
9763043
@ashm-dev

ashm-dev commented Sep 22, 2026

Copy link
Copy Markdown
Contributor Author

@cmaloney Done in 9763043.

All reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@ashm-dev

ashm-dev commented Oct 3, 2026

Copy link
Copy Markdown
Contributor Author

@cmaloney Gentle ping — could you please check if the updated `memoryview` approach in 9763043 looks good to you?

@vstinner All CI checks are passing and the review comments have been addressed. Ready for merge whenever you have a moment. Thanks!

All reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@vstinner

vstinner commented Oct 5, 2026

Copy link
Copy Markdown
Member

I don't understand well how _bufferedreader_raw_read_getbuffer() works. How is it different from the current PyBuffer_FillInfo() + PyMemoryView_FromBuffer() code?

All reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@ashm-dev

ashm-dev commented Oct 5, 2026

Copy link
Copy Markdown
Contributor Author

The key difference is reference ownership:

  1. Previous code (PyMemoryView_FromBuffer):
    PyMemoryView_FromBuffer(&buf) explicitly clears mbuf->master.obj = NULL (see Objects/memoryobject.c:787). Because of this, the returned memoryview does not hold a reference to self (b.obj is None). If raw.readinto(b) stores the memoryview and BufferedReader is deallocated, subsequent access to the stored buffer leads to a use-after-free.

  2. New code (_PyMemoryView_FromBufferProc):
    _PyMemoryView_FromBufferProc calls _bufferedreader_raw_read_getbuffer directly on &mbuf->master. There, PyBuffer_FillInfo sets view->obj = Py_NewRef(op), giving the managed buffer a strong reference to self that is not cleared. As a result, the memoryview keeps BufferedReader (and its internal allocation) alive as long as the memoryview is stored. When the memoryview is freed, PyBuffer_Release automatically decrefs self.

We can't use standard PyMemoryView_FromObject((PyObject *)self) here because BufferedReader doesn't implement the buffer protocol on itself (tp_as_buffer is NULL) — it only needs to expose this transient window into its buffer for readinto().

All reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@cmaloney

cmaloney commented Oct 6, 2026 •
edited
Loading

Uh oh!

There was an error while loading. Please reload this page.

Copy link
Copy Markdown
Contributor

I have been triaging some old I/O bugs and came across gh-60198 which is exactly the BufferedReader memoryview bug here. That could be pulled out as a separate smaller PR that is likely quicker to land with its own NEWS.

I think this should have two news entries: one the io.TexTIOWrapper keeping a reference to self->buffer when calling it, which refers to gh-157364 / this issue. A second for the BufferedReader needs to be referenced by the memoryview (gh-60198). Also update the test_bufferedio.py comment for gh-60918. It has a bit more detail about why historically this has been hard to solve.

I don't like adding the two new members to buffered. The start + len we already know and I don't see why storing them as extra members helps solve the particular issue.

In the TextIO the Incref and decref living in very different functions I'm not a big fan of... I would much rather keep buffer_access_safe not modifying the refcnt then have each fo the callsites which does need to keep a non-borrowed reference do a Py_INCREF / Py_NewRef + Py_DECREF pair.

All reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@ashm-dev
fix(io): scope UAF patch to TextIOWrapper
7e1eedd
@ashm-dev

ashm-dev commented Oct 6, 2026

Copy link
Copy Markdown
Contributor Author

@vstinner @cmaloney I narrowed this PR to the TextIOWrapper fix for gh-157364. I removed the BufferedReader change and added the requested check that the wrapper is detached after read() and readline(). Could you both please re-review? @cmaloney, is the remaining reference-handling approach acceptable to you?

All reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

vstinner
vstinner reviewed Oct 6, 2026
View reviewed changes
Comment thread Modules/_io/textio.c Resolved

Uh oh!

There was an error while loading. Please reload this page.

Comment thread Lib/test/test_io/test_textio.py Resolved

Uh oh!

There was an error while loading. Please reload this page.

@vstinner

vstinner commented Oct 6, 2026

Copy link
Copy Markdown
Member

I removed the BufferedReader change and added the requested check that the wrapper is detached after read() and readline().

Just a general remark: you're making many large changes on this PR. It's not easy to follow these changes.

All reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@ashm-dev
refactor(io): preserve detach state update order
aa9a179
@ashm-dev
ashm-dev requested a review from vstinner October 7, 2026 08:22
vstinner
vstinner reviewed Oct 7, 2026
View reviewed changes

@vstinner vstinner left a comment

Copy link
Copy Markdown
Member

There was a problem hiding this comment.

Choose a reason for hiding this comment

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

Good: the test does crash without the fix. So it confirms that the test checks the modified C code.

$ ./python -m test -v test_io.test_textio -m test_reentrant_detach_during_read
(...)Warning -- Unraisable exception
Exception ignored while finalizing file <_io.BufferedReader>:
Traceback (most recent call last):
  File "/home/vstinner/python/main/Lib/test/test_io/test_textio.py", line 1651, in readinto
    wrapper.detach()
    ~~~~~~~~~~~~~~^^
RuntimeError: reentrant call inside <_io.BufferedReader>
Fatal Python error: Segmentation fault

<Cannot show all threads while the GIL is disabled>
Stack (most recent call first):
  File "/home/vstinner/python/main/Lib/test/test_io/test_textio.py", line 1661 in test_reentrant_detach_during_read

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

All reactions
vstinner
vstinner reviewed Oct 7, 2026
View reviewed changes
Comment thread Lib/test/test_io/test_textio.py Resolved

Uh oh!

There was an error while loading. Please reload this page.

@vstinner
Apply suggestion from @vstinner
e654017
vstinner
vstinner approved these changes Oct 7, 2026
View reviewed changes

@vstinner vstinner left a comment

Copy link
Copy Markdown
Member

There was a problem hiding this comment.

Choose a reason for hiding this comment

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

LGTM. Returning a strong reference in buffer_access_safe() sounds like a safe move. I checked that buffer_access_safe() callers do Py_DECREF() on the buffer.

./python -m test -R 3:3 test_io doesn't report leaks. But it reports negative reference count which is surprising:

$ ./python -m test -R 3:3 test_io.test_textio
(...)
test_io.test_textio leaked [-1, -1, -1] references, sum=-3 (this is fine)

$ ./python -m test -R 3:3 test_io.test_bufferedio
(...)
test_io.test_bufferedio leaked [-3, -3, -3] references, sum=-9 (this is fine)

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

All reactions
@vstinner

vstinner commented Oct 7, 2026

Copy link
Copy Markdown
Member

$ ./python -m test -R 3:3 test_io.test_textio
test_io.test_textio leaked [-1, -1, -1] references, sum=-3 (this is fine)
$ ./python -m test -R 3:3 test_io.test_bufferedio
test_io.test_bufferedio leaked [-3, -3, -3] references, sum=-9 (this is fine)

Ah, I get the same result on the main branch. So it's not caused by this change.

All reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@vstinner
vstinner enabled auto-merge (squash) October 7, 2026 14:42
@vstinner vstinner added needs backport to 3.14 bugs and security fixes needs backport to 3.15 bugs and security fixes labels Oct 7, 2026
@vstinner

vstinner commented Oct 7, 2026

Copy link
Copy Markdown
Member

Thanks the updates. I enabled auto-merge, please don't touch the PR (your branch) anymore. This change is a bugfix which should be backported to 3.14 and 3.15 branches.

ashm-dev reacted with thumbs up emoji
All reactions
  • 👍 1 reaction

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@cmaloney

cmaloney commented Oct 7, 2026

Copy link
Copy Markdown
Contributor

CI failed on test_profiling which looks independent, re-running.

vstinner and ashm-dev reacted with thumbs up emoji
All reactions
  • 👍 2 reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@vstinner
vstinner merged commit e19dc47 into python:main Oct 7, 2026
104 of 105 checks passed

Uh oh!

There was an error while loading. Please reload this page.

@miss-islington-app

miss-islington-app Bot commented Oct 7, 2026

Copy link
Copy Markdown

Thanks @ashm-dev for the PR, and @vstinner for merging it 🌮🎉.. I'm working now to backport this PR to: 3.14, 3.15.
🐍🍒⛏🤖

All reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@bedevere-app bedevere-app Bot removed the awaiting merge label Oct 7, 2026
@miss-islington-app

miss-islington-app Bot commented Oct 7, 2026

Copy link
Copy Markdown

Sorry, @ashm-dev and @vstinner, I could not cleanly backport this to 3.15 due to a conflict.

Please backport manually with cherry_picker, see the devguide for more information.

cherry_picker e19dc470f4c2afd7ac944896232a3a6ca0204399 3.15
All reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@miss-islington-app miss-islington-app Bot assigned vstinner Oct 7, 2026
@miss-islington-app

miss-islington-app Bot commented Oct 7, 2026

Copy link
Copy Markdown

Sorry, @ashm-dev and @vstinner, I could not cleanly backport this to 3.14 due to a conflict.

Please backport manually with cherry_picker, see the devguide for more information.

cherry_picker e19dc470f4c2afd7ac944896232a3a6ca0204399 3.14
All reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@ashm-dev
ashm-dev deleted the gh-157364 branch October 7, 2026 18:13
@vstinner

vstinner commented Oct 7, 2026

Copy link
Copy Markdown
Member

Aha, more conflicts. I will backport the change to 3.14 and 3.15 once the 3.15 branch will be unblocked (next week).

cmaloney reacted with thumbs up emoji
All reactions
  • 👍 1 reaction

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@vstinner

vstinner commented Oct 7, 2026

Copy link
Copy Markdown
Member

PR merged, thanks @ashm-dev for your fix.

ashm-dev reacted with heart emoji
All reactions
  • ❤️ 1 reaction

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@vstinner

vstinner commented Oct 10, 2026

Copy link
Copy Markdown
Member

Backporting this change is non trivial. I have to backport at least these commits

  1. commit db4b1948bc4ad5e7230fe1f2814da409ca1d669b
  2. commit fa0ec86ab682a561e2dc6f50127f2a78f76ce311
  3. commit e19dc470f4c2afd7ac944896232a3a6ca0204399

There are some minor conflicts, and then test_io.textio fails. I will retry next week.

ashm-dev and cmaloney reacted with heart emoji
All reactions
  • ❤️ 2 reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@vstinner

vstinner commented Oct 16, 2026

Copy link
Copy Markdown
Member

Ok, I found the right order and the correct commits to backport: I created #159591 for the 3.15 branch.

cmaloney and ashm-dev reacted with thumbs up emoji
All reactions
  • 👍 2 reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

vstinner added a commit that referenced this pull request Oct 16, 2026
@vstinner @cmaloney
@yihong0618 @brijkapadia @ashm-dev
[3.15] gh-157364: Fix use-after-free in io.TextIOWrapper during reent…
ea1312b
…rant detach (#159591)

* 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 <[email protected]>
Co-authored-by: Victor Stinner <[email protected]>
(cherry picked from commit db4b194)

* gh-154523: Fix data-race in `TextIOWrapper.detach()` (#154565)

(cherry picked from commit 8440a6a)

* gh-157364: Fix use-after-free in io.TextIOWrapper during reentrant detach (#157370)

Co-authored-by: Victor Stinner <[email protected]>
(cherry picked from commit e19dc47)

---------

Co-authored-by: Cody Maloney <[email protected]>
Co-authored-by: yihong0618 <[email protected]>
Co-authored-by: Brij Kapadia <[email protected]>
Co-authored-by: Shamil <[email protected]>
@vstinner vstinner mentioned this pull request Oct 16, 2026
[3.14] gh-157364: Fix use-after-free in io.TextIOWrapper during reentrant detach #159600
Merged
vstinner added a commit that referenced this pull request Oct 16, 2026
@vstinner @cmaloney
@yihong0618 @brijkapadia @ashm-dev
[3.14] gh-157364: Fix use-after-free in io.TextIOWrapper during reent…
13fc16c
…rant detach (#159600)

[3.15] gh-157364: Fix use-after-free in io.TextIOWrapper during reentrant detach (#159591)

* 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.



(cherry picked from commit db4b194)

* gh-154523: Fix data-race in `TextIOWrapper.detach()` (#154565)

(cherry picked from commit 8440a6a)

* gh-157364: Fix use-after-free in io.TextIOWrapper during reentrant detach (#157370)


(cherry picked from commit e19dc47)

---------





(cherry picked from commit ea1312b)

Co-authored-by: Cody Maloney <[email protected]>
Co-authored-by: yihong0618 <[email protected]>
Co-authored-by: Brij Kapadia <[email protected]>
Co-authored-by: Shamil <[email protected]>
@StanFromIreland StanFromIreland removed needs backport to 3.14 bugs and security fixes needs backport to 3.15 bugs and security fixes labels Oct 21, 2026
@scrieve scrieve mentioned this pull request Oct 23, 2026
gh-160427: Return None from TextIOWrapper.buffer once detached #160467
Closed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Reviewers

@vstinner vstinner vstinner approved these changes

Uh oh!

There was an error while loading. Please reload this page.

Assignees

@vstinner vstinner

Labels

None yet

Projects

None yet

Milestone

No milestone

Development

Successfully merging this pull request may close these issues.

Uh oh!

There was an error while loading. Please reload this page.

4 participants

@ashm-dev @cmaloney @vstinner @StanFromIreland
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
You are visitor 01402503 — GeezerVison does not store the pages you look up.
Best viewed with Netscape Navigator 4.7 or Internet Explorer 6 at 800×600. No cookies required.