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-148286: Fix undefined behaviour in io.StringIO.read() after an overseek by matthiasgoergens · Pull Request #154913 · python/cpython

Conversation

@matthiasgoergens

@matthiasgoergens matthiasgoergens commented Jul 30, 2026 •
edited by bedevere-app Bot
Loading

Uh oh!

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

Copy link
Copy Markdown
Contributor

_io_StringIO_read_impl() clamps the read size to zero when the position is past the end of the buffer, but still forms the pointer self->buf + self->pos before returning. Nothing is read through it, but forming it is undefined behaviour on its own, and since self->buf is a Py_UCS4 * the byte offset is self->pos * 4 — so for self->pos == 2**62 - 1 the multiplication wraps and yields a pointer four bytes below self->buf.

io.StringIO("abc").seek(2**62 - 1)   # then .read()
stringio.c:352: addition of unsigned offset to 0x... overflowed to 0x...

_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 += size is a no-op here because size is zero, and ENSURE_REALIZED() is deliberately left ahead of the guard so the state transition and its error path are unchanged.

The existing test_io suite already covers this path — that is how UBSan found it — so no new test is needed. With the Modules/_io/stringio.c entry removed from Tools/ubsan/suppressions.txt, test_io passes under UBSAN_OPTIONS=halt_on_error=1, where before it aborts at stringio.c:352.

  • Issue: Various UB warnings found by UBSan #148286

Sorry, something went wrong.

Uh oh!

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

All reactions
@bedevere-app bedevere-app Bot mentioned this pull request Jul 30, 2026
Various UB warnings found by UBSan #148286
Open
@bedevere-app bedevere-app Bot added the awaiting review label Jul 30, 2026
@matthiasgoergens
pythongh-148286: Fix undefined behaviour in io.StringIO.read() after …
addc87f
…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
matthiasgoergens force-pushed the stringio-overseek-ub branch from 987fb6a to addc87f Compare July 30, 2026 09:39
StanFromIreland
StanFromIreland reviewed Jul 30, 2026
View reviewed changes
Comment thread Misc/NEWS.d/next/Library/2026-07-30-12-05-00.gh-issue-148286.Bv3Nq8.rst Outdated
@@ -0,0 +1,7 @@
Fix undefined behaviour in :meth:`!io.StringIO.read` after an overseek.

@StanFromIreland StanFromIreland Jul 30, 2026

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.

Same idea as with the ctypes PR, avoid implementation/infra details.

Sorry, something went wrong.

Uh oh!

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

All reactions
picnixz
picnixz requested changes Jul 31, 2026
View reviewed changes

@picnixz picnixz 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.

Please add a regression test.

Sorry, something went wrong.

Uh oh!

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

All reactions
@bedevere-app bedevere-app Bot added awaiting changes and removed awaiting review labels Jul 31, 2026
@bedevere-app

bedevere-app Bot commented Jul 31, 2026

Copy link
Copy Markdown

A Python core developer has requested some changes be made to your pull request before we can consider merging it. If you could please address their requests along with any other requests in other reviews from core developers that would be appreciated.

Once you have made the requested changes, please leave a comment on this pull request containing the phrase I have made the requested changes; please review again. I will then notify any core developers who have left a review that you're ready for them to take another look at this pull request.

All reactions

Sorry, something went wrong.

Uh oh!

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

@matthiasgoergens
Add a regression test for large overseeks; trim the NEWS entry
0d659fa
Review feedback: picnixz asked for a regression test, StanFromIreland
for the NEWS entry to drop implementation and infrastructure detail.

The existing test_overseek only seeks one past the end, which is why it
never exercised this: the pointer arithmetic only wraps for positions
above PY_SSIZE_T_MAX // 4. The new test covers 2**31, 2**62 and
sys.maxsize, and sits in MemoryTestMixin so it runs for BytesIO as well
as StringIO, in both the C and pure-Python implementations. Verified
that all four already satisfy it, which is the point -- it pins the
documented behaviour rather than detecting the UB.

NEWS entry cut to the user-visible statement.
@matthiasgoergens

matthiasgoergens commented Aug 1, 2026

Copy link
Copy Markdown
Contributor Author

Thanks both.

Regression test — added in 0d659fa, as test_overseek_far_past_end.

One thing worth being explicit about, because it affects how much the
test is worth: it passes on unfixed builds too. I checked before
writing it — io.StringIO and _pyio.StringIO, and BytesIO for both,
already return '' with the correct tell() and getvalue() at every
offset it tries. The bug is undefined behaviour, and UB is not generally
observable; a normal build gives no signal here.

So the test pins the documented behaviour rather than detecting the
defect. It is still worth having: the existing test_overseek only
seeks one past the end, so nothing covered the large-offset case at all,
and that is precisely where the arithmetic wraps — the pointer overflow
only occurs above PY_SSIZE_T_MAX // 4. It is in MemoryTestMixin, so
it runs for BytesIO as well as StringIO, C and pure-Python.

What actually guards the fix is the other half of the diff: removing

pointer-overflow:Modules/_io/stringio.c

from Tools/ubsan/suppressions.txt, which .github/workflows/reusable-san.yml
consumes. With the suppression gone, the UBSan job fails if the UB comes
back. That is the regression guard; the unit test is the behavioural
one. Happy to add more of the latter if you would like, but I did not
want to imply it catches what it does not.

NEWS entry — trimmed to the user-visible statement, no macro names,
no description of the fix, no suppressions note.

I have made the requested changes; please review again

All reactions

Sorry, something went wrong.

Uh oh!

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

@bedevere-app bedevere-app Bot added awaiting change review and removed awaiting changes labels Aug 1, 2026
@bedevere-app

bedevere-app Bot commented Aug 1, 2026

Copy link
Copy Markdown

Thanks for making the requested changes!

@picnixz: please review the changes made to this pull request.

All reactions

Sorry, something went wrong.

Uh oh!

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

@bedevere-app
bedevere-app Bot requested a review from picnixz August 1, 2026 09:10
@matthiasgoergens
Drop the added test; the existing testSeek already covers this
9d3ba83
Verified under UBSan with CI's own configuration
(--with-undefined-behavior-sanitizer --with-strict-overflow, clang):
CStringIOTest.testSeek already triggers the report, because it does
seek(sys.maxsize) followed by read(4), which is exactly the case where
the pointer arithmetic wraps. That is what produced the diagnostic
recorded in the suppressions file.

My added test was therefore redundant for detection: small overseeks
(11, 100, 2**31) produce no report at all, and the large offsets it used
were already covered. Removed rather than kept as noise.

Left a comment on testSeek instead, since its name gives no hint that
those two lines guard a UB fix, and trimming them would silently remove
the coverage.
@matthiasgoergens

matthiasgoergens commented Aug 1, 2026

Copy link
Copy Markdown
Contributor Author

Correction to my last comment, and a retraction: the test I added was
redundant and I have removed it
(9d3ba83).

I said the suppression removal was the regression guard and the unit
test the behavioural one. The first half is right; the second oversold
it. CStringIOTest.testSeek already covers this — it has all along:

self.assertEqual(sys.maxsize, bytesIo.seek(sys.maxsize))
self.assertEqual(self.EOF, bytesIo.read(4))

That is exactly the case where the arithmetic wraps, and it is what
produced the diagnostic recorded in Tools/ubsan/suppressions.txt.

Verified rather than argued, building with CI's own configuration
(--with-undefined-behavior-sanitizer --with-strict-overflow, clang 22):

UBSan report
unfixed, testSeek fires — addition of unsigned offset to 0x…a60 overflowed to 0x…a5c
unfixed, overseek by 11 / 100 / 2³¹ none
unfixed, offsets ≥ 2⁶²−1 fires
fixed, full test_io none; all 9 tests OK

So small overseeks produce no report at all, which is why the existing
test_overseek never surfaced this, and why my large-offset test added
no coverage that testSeek did not already provide. I have left a
comment on those two lines instead — the test's name gives no hint that
they guard a UB fix, and trimming them would silently remove the
coverage.

@picnixz — if you would still prefer an explicitly-named regression
test even though it duplicates existing coverage, say so and I will add
one back; I did not want to leave a test in that could not fail.


Unrelated but worth flagging, since it cost me a while and may cost
others the same.
-fno-strict-overflow silently disables clang's
pointer-overflow sanitizer, and re-adding -fsanitize=pointer-overflow
afterwards does not bring it back:

-fsanitize=undefined                                          -> reports
-fsanitize=undefined -fno-strict-overflow                     -> silent
-fsanitize=undefined -fno-strict-overflow
                     -fsanitize=pointer-overflow              -> silent

--with-strict-overflow defaults to no, so an ordinary
./configure --with-undefined-behavior-sanitizer build cannot see any
pointer-overflow: finding. The CI job is fine — reusable-san.yml
passes --with-strict-overflow explicitly — but anyone reproducing a
suppressions-file entry locally with the obvious configure line will get
silence and reasonably conclude the entry is stale. I did, for several
builds, before spotting it.

@StanFromIreland this may be worth a line in Tools/ubsan/README or
next to the pointer-overflow entries, given you own that file. Happy
to send that as a separate PR if useful.

All reactions

Sorry, something went wrong.

Uh oh!

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

@matthiasgoergens
Restore the suppressed reference in the NEWS entry
047e967
Dropping the '!' from :meth:`!io.StringIO.read` while trimming the entry
broke the docs build: there is no doc target for io.StringIO.read, so
nitpicky mode reports 'py:meth reference target not found'. The original
had the bang for exactly that reason.

Every build check passed; Docs was the only real failure of the three
(the other two are the unresolved-review gate and the aggregate that
depends on it).
@matthiasgoergens

matthiasgoergens commented Aug 5, 2026

Copy link
Copy Markdown
Contributor Author

@picnixz Please see the new version whenever you have time.

All reactions

Sorry, something went wrong.

Uh oh!

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

@matthiasgoergens

matthiasgoergens commented Aug 5, 2026

Copy link
Copy Markdown
Contributor Author

Thanks, and sorry for the back-and-forth — let me make the case properly,
because there is a close precedent that cuts both ways and I'd rather
address it head-on.

The identical bug in BytesIO was fixed as gh-141311 (#141333): forming
buf + pos with pos past the end of the buffer. That PR added a named
regression test (test_issue141311), and I expect that's the shape you
have in mind here. The difference is what a test can observe: gh-141311
was an assertion failure, so a plain Python test goes red on an unfixed
build. This bug is only observable to UBSan — I added exactly that test
(0d659fa, test_overseek_far_past_end) and it passes on unfixed
builds
, because the post-fix behaviour is byte-for-byte the pre-fix
behaviour: both paths return the empty-string singleton
(PyUnicode_FromKindAndData(kind, …, 0) also funnels into
_Py_RETURN_UNICODE_EMPTY()), with the same tell() afterwards. A test
that cannot fail seemed worse than none, so I removed it (9d3ba83)
rather than leave something that looks like a guard but isn't.

The actual regression guard is the Tools/ubsan/suppressions.txt entry
removal: since #148168 UBSan hard-fails CI on new reports, and the
existing CStringIOTest.testSeek —

self.assertEqual(sys.maxsize, bytesIo.seek(sys.maxsize))
self.assertEqual(self.EOF, bytesIo.read(4))

— is precisely the case where pos * sizeof(Py_UCS4) wraps; I verified
on an unfixed build (--with-undefined-behavior-sanitizer --with-strict-overflow, CI's own configuration) that those two lines are
what produced the diagnostic recorded in the suppressions file. So if
anyone reintroduces the unguarded arithmetic, the existing suite fails
under the UBSan job with no new test needed. I added a comment on those
lines saying so, since the test's name gives no hint.

If you'd still rather have an explicitly named
test_overseek_far_past_end even knowing it can't fail pre-fix, I'm
happy to add it back — it does document intent. Just say the word.

All reactions

Sorry, something went wrong.

Uh oh!

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

@matthiasgoergens

matthiasgoergens commented Aug 11, 2026

Copy link
Copy Markdown
Contributor Author

I have made the requested changes; please review again.

Added test_read_far_past_end (C and Python StringIO). I want to be straight about what it does and does not do, since I retracted an earlier attempt in this thread: on a normal build it cannot fail on the unfixed code, because the bug is pointer UB with no observable Python-level effect (I checked: unfixed, read() already returns '' with tell() unchanged at every offset it tries). It pins the documented behaviour at the large offsets nothing else covered, and under a UBSan build — now that this PR removes the Modules/_io/stringio.c entry from Tools/ubsan/suppressions.txt — it is the reproducer that reports at stringio.c. This mirrors gh-141311, where the BytesIO overseek fix landed with test_issue141311, a behavioural EOF test of the same non-observable-on-release kind.

Happy to drop it and rely on the existing testSeek (which already seeks to sys.maxsize and reads) plus the suppression removal if you'd prefer the suite not carry a near-duplicate.

All reactions

Sorry, something went wrong.

Uh oh!

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

@bedevere-app

bedevere-app Bot commented Aug 11, 2026

Copy link
Copy Markdown

Thanks for making the requested changes!

@picnixz: please review the changes made to this pull request.

All reactions

Sorry, something went wrong.

Uh oh!

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

@github-actions

github-actions Bot commented Nov 11, 2026

Copy link
Copy Markdown

This PR is stale because it has been open for 90 days with no activity.

All reactions

Sorry, something went wrong.

Uh oh!

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

@github-actions github-actions Bot added the stale Stale PR or inactive for long period of time. label Nov 11, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Reviewers

@StanFromIreland StanFromIreland StanFromIreland left review comments
@picnixz picnixz Awaiting requested review from picnixz

Uh oh!

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

Assignees

No one assigned

Labels

awaiting change review stale Stale PR or inactive for long period of time.

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.

3 participants

@matthiasgoergens @picnixz @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.