| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
Sorry, something went wrong.
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 pythongh-143041. Co-authored-by: yihong0618 <zouzou0208@gmail.com>
|
wow that is cool thanks |
Sorry, something went wrong.
|
@colesbury could you take a look as you've done some recent work on TextIO safety around threading. These cases are generally single threaded but generally the pattern "Implementation usually checked state; then did an operation which could modify it and didn't recheck". |
Sorry, something went wrong.
|
I'm a bit confused about the relationship here between _textiowrapper_buffer_safe and CHECK_ATTACHED. When can buffer be NULL and CHECK_ATTACHED not trigger? |
Sorry, something went wrong.
|
Less important, but please match the CPython style for C code, e.g.:
|
Sorry, something went wrong.
CHECK_ATTACHED, by way of CHECK_INITIALIZED, checks self->ok first. The self->ok flag is set to 0 at start of __init__, 1 at the end, and cleared/reset to 0 during .clear() as well as at the start of the Py_tp_dealloc. During __init__ there are calls to check the file is in a good state and get some attributes (remember: self->buf is the underlying file-like object not an internal buffer). The more complicated case is during deletion where there is an interaction with the base class. TextIOWrapper implements Py_tp_dealloc and its base class (TextIOBase which has a base class IOBase) implements a Py_tp_finalize with the function iobase_finalize. That function tries to ensure all data is written in the full I/O "stack" before any part of it is closed out of order. It tries to have Text I/O flush and close, then Buffered I/O (self->buf), then the Raw I/O. If there is an underlying file (self->buf != NULL) during those operations need to try and write data in the TextIOWrapper even though self->ok is 0. Adding to the complexity here all the objects involved and the multiple operations (flush(), close(), .closed) can be overridden and could call TextIOWrapper.clear() or TextIOWrapper.detach() causing self->buf to become NULL.
Sorry for missing those bits, doing a review of that and will update for it shortly. |
Sorry, something went wrong.
|
There are a couple cases I'm not sure how to PEP 7, in particular: /* hits 81 cols */
PyObject *bytes = _textiowrapper_buffer_callmethod_noargs(self,
&_Py_ID(read));
/* hits 81 cols */
res = _textiowrapper_buffer_callmethod_onearg(self,
&_Py_ID(_dealloc_warn),
(PyObject *)self);
PyObject *input_chunk = _textiowrapper_buffer_callmethod_onearg(self,
&_Py_ID(read), bytes_to_feed);A couple directions I looked at but don't know how to decide between:
|
Sorry, something went wrong.
|
I think it's fine to make the function names shorter, i.e., buffer_callmethod_onearg instead of _textiowrapper_buffer_callmethod_onearg. File-scope static functions don't need a prefix or underscore. In general the preference is for wrapping function calls like the following (from PEP 7 example): PyErr_Format(PyExc_TypeError,
"cannot create '%.100s' instances",
type->tp_name);But if that's not practical due to overly long lines, than just try to fit the style of surround code. For example, from elsewhere in textio.c: PyObject *incrementalDecoder = PyObject_CallFunctionObjArgs(
(PyObject *)state->PyIncrementalNewlineDecoder_Type,
self->decoder, self->readtranslate ? Py_True : Py_False, NULL); |
Sorry, something went wrong.
|
This should also resolve gh-143007 |
Sorry, something went wrong.
|
I think thish is ready for re-review @colesbury |
Sorry, something went wrong.
| leading to NULL pointer dereferences (see gh-143008, gh-142594). Protect against | ||
| that by using helpers to check self->buffer validity at callsites. */ | ||
| static PyObject * | ||
| buffer_access_safe(textio *self) |
There was a problem hiding this comment.
I suggest to rename buffer_xxx() functions to textiowrapper_buffer_xxx(), and rename this function to textiowrapper_buffer_get().
Sorry, something went wrong.
Sorry, something went wrong.
| /* During destruction self->ok is 0 but self->buffer is non-NULL and this | ||
| needs to error in that case which the safe buffer wrapper does not. | ||
|
|
||
| Match original behavior by calling CHECK_ATTACHED explicitly. */ |
There was a problem hiding this comment.
textiowrapper_dealloc() sets ok to 0 and almost immediately sets buffer to NULL. So I don't understand well when it's possible that ok is 0 and buffer is non-NULL.
This comment is unclear to me. I suggest rephrasing it to something like:
"Call CHECK_ATTACHED() to raise an exception if ..., when ... happens."
The "Match original behavior" part is unclear to me.
Sorry, something went wrong.
There was a problem hiding this comment.
Moved to checking self->ok directly and just returning true in that case which differs more in this bug fix but I think is a lot more understandable. Described a couple cases this is hit by the test suite.
Sorry, something went wrong.
There was a problem hiding this comment.
LGTM.
I don't think that it's worth it to backport the "check buffer" fixes. #143008 issue was found by fuzzing. Regular Python code should never meet this bug in practice. Triggering the code requires to design a very precise class which misbehaves.
But IMO the critical section on TextIOWrapper.__init__ should be backported. Once this PR is merged, you can create a fix for 3.15 (and then we can backport it).
Sorry, something went wrong.
| ('reconfigure', lambda: wrapper.reconfigure(line_buffering=True)), | ||
| ] | ||
| for name, method in tests: | ||
| with self.subTest(name), set_recursion_limit(100): |
There was a problem hiding this comment.
Why do you reduce the recursion limit?
Sorry, something went wrong.
There was a problem hiding this comment.
Why use unlimited recursion at all? Call the method, detach the buffer, set the flag, call the method yet one time if the flag was not set.
Sorry, something went wrong.
There was a problem hiding this comment.
Removed the recursion
Sorry, something went wrong.
| wrapper_ref = lambda: None | ||
| del wrapper |
There was a problem hiding this comment.
Would it work to just assign wrapper to None (and leave wrapper_ref unchanged)?
Sorry, something went wrong.
There was a problem hiding this comment.
Removed wrapper_ref entirely and reworked how the classes work to simplify
Sorry, something went wrong.
There was a problem hiding this comment.
I wonder, would not it be simpler if set buffer to None instead of NULL? How does it work in the Python implementation?
Sorry, something went wrong.
| from collections import UserList | ||
| from test import support | ||
| from test.support import os_helper, threading_helper | ||
| from test.support import ( |
There was a problem hiding this comment.
It is better to not mix import of module attributes and submodules in one statement.
Complex multiline import is less readable than separate oneline imports.
Sorry, something went wrong.
There was a problem hiding this comment.
Removed set_recursion_limit entirely
Sorry, something went wrong.
| ('reconfigure', lambda: wrapper.reconfigure(line_buffering=True)), | ||
| ] | ||
| for name, method in tests: | ||
| with self.subTest(name), set_recursion_limit(100): |
There was a problem hiding this comment.
Why use unlimited recursion at all? Call the method, detach the buffer, set the flag, call the method yet one time if the flag was not set.
Sorry, something went wrong.
Co-authored-by: Victor Stinner <vstinner@python.org>
|
_pyio uses None to indicate no buffer. Can definitely migrate _io.TextIOWrapper to do that but that's a substantial code delta for an edge case and doesn't simplify fixing the time of check time of use issue here. Moving to None would make it calling methods on None rather than a null pointer de-reference. Would still need most this change of "always check just before using". re: Recursion most the tests don't rely on it. The one that does is when flush() calls detach itself which results in another call to flush(). gh-143041 resolved that by making a new "state" effectively of "are detaching, don't detach again". Can do a similar pattern here if want to eliminate the recursion potentially: In .detach() change self->detached and/or self->ok before calling _PyFile_Flush(self) and making sure .flush() handles that case appropriately. I'm hesitant to do that as it changes the set of states a TextIOWrapper can be in a bit and I don't have those well mapped out mentally. Limiting recursion means the test exits after some number of recursion instances which validates it no longer segfaults / NULL pointer dereference is resolved. |
Sorry, something went wrong.
There was a problem hiding this comment.
Found a refactoring that removed the recursion while not making too much complex
Sorry, something went wrong.
| from collections import UserList | ||
| from test import support | ||
| from test.support import os_helper, threading_helper | ||
| from test.support import ( |
There was a problem hiding this comment.
Removed set_recursion_limit entirely
Sorry, something went wrong.
| ('reconfigure', lambda: wrapper.reconfigure(line_buffering=True)), | ||
| ] | ||
| for name, method in tests: | ||
| with self.subTest(name), set_recursion_limit(100): |
There was a problem hiding this comment.
Removed the recursion
Sorry, something went wrong.
|
Merged, thanks @cmaloney for the fix. No one should meet this bug in practice, since it requires a custom buffer object which detach() when the stream is not supposed to be detached. But well, it's good to fix the code to support this corner case. @cmaloney: You can create a change for the 3.15 branch to backport the critical section if you want. |
Sorry, something went wrong.
|
Critical section only backport: gh-151203 |
Sorry, something went wrong.
…lying stream access (python#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 pythongh-143041. Co-authored-by: yihong0618 <zouzou0208@gmail.com> Co-authored-by: Victor Stinner <vstinner@python.org>
…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 <zouzou0208@gmail.com> Co-authored-by: Victor Stinner <vstinner@python.org> (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 <vstinner@python.org> (cherry picked from commit e19dc47) --------- Co-authored-by: Cody Maloney <cmaloney@users.noreply.github.com> Co-authored-by: yihong0618 <zouzou0208@gmail.com> Co-authored-by: Brij Kapadia <97006829+brijkapadia@users.noreply.github.com> Co-authored-by: Shamil <ashm.tech@proton.me>
…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 <cmaloney@users.noreply.github.com> Co-authored-by: yihong0618 <zouzou0208@gmail.com> Co-authored-by: Brij Kapadia <97006829+brijkapadia@users.noreply.github.com> Co-authored-by: Shamil <ashm.tech@proton.me>
| Back | FazBrowse Home | New Git URL |
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 which could modify self->buffer.
The cases which need to be re-checked are hard to spot so rather than rely on reviewer effort make a better safety net by changing all self->buffer access to go through helper functions.
Thank you @yihong0618 for the test, NEWS and an initial implementation in gh-143041.