gh-148286: Fix undefined behaviour in io.StringIO.read() after an overseek - #154913
Open
matthiasgoergens wants to merge 1 commit into
Open
gh-148286: Fix undefined behaviour in io.StringIO.read() after an overseek#154913matthiasgoergens wants to merge 1 commit into
matthiasgoergens wants to merge 1 commit into
Conversation
…an overseek
_io_StringIO_read_impl() clamps the read size to zero when the position is
past the end of the buffer, but still formed the pointer
output = self->buf + self->pos;
before returning the empty string. Nothing is read through that pointer,
but forming it is undefined behaviour on its own, and because self->buf is
a Py_UCS4 *, the byte offset is self->pos * 4: for self->pos == 2**62 - 1
the multiplication wraps and yields a pointer four bytes BELOW self->buf.
That is what UBSan reported:
io.StringIO("abc").seek(2**62 - 1) followed by .read()
stringio.c:352: addition of unsigned offset to 0x... overflowed to 0x...
Return the empty string early, exactly as _stringio_readline() already
does for the same overseek condition. self->pos += size is a no-op here
since size is zero, and ENSURE_REALIZED() is kept ahead of the guard so
the state transition and its error path are unchanged.
The existing test_io suite already covers this path, which is how UBSan
found it. The Modules/_io/stringio.c entry in Tools/ubsan/suppressions.txt
is no longer needed; with it removed, test_io passes under
UBSAN_OPTIONS=halt_on_error=1, where before it aborts at stringio.c:352.
matthiasgoergens
force-pushed
the
stringio-overseek-ub
branch
from
July 30, 2026 09:39
987fb6a to
addc87f
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
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.
_io_StringIO_read_impl()clamps the read size to zero when the position is past the end of the buffer, but still forms the pointerself->buf + self->posbefore returning. Nothing is read through it, but forming it is undefined behaviour on its own, and sinceself->bufis aPy_UCS4 *the byte offset isself->pos * 4— so forself->pos == 2**62 - 1the multiplication wraps and yields a pointer four bytes belowself->buf._stringio_readline()already guards exactly this condition a few lines below, with the comment "In case of overseek, return the empty string", so this applies the same guard on the read path.self->pos += sizeis a no-op here becausesizeis zero, andENSURE_REALIZED()is deliberately left ahead of the guard so the state transition and its error path are unchanged.The existing
test_iosuite already covers this path — that is how UBSan found it — so no new test is needed. With theModules/_io/stringio.centry removed fromTools/ubsan/suppressions.txt,test_iopasses underUBSAN_OPTIONS=halt_on_error=1, where before it aborts atstringio.c:352.