From addc87f58c13d53290064d71638492f669580815 Mon Sep 17 00:00:00 2001 From: Matthias Goergens Date: Thu, 30 Jul 2026 13:51:59 +0800 Subject: [PATCH] gh-148286: Fix undefined behaviour in io.StringIO.read() after 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. --- .../2026-07-30-12-05-00.gh-issue-148286.Bv3Nq8.rst | 7 +++++++ Modules/_io/stringio.c | 11 +++++++++++ Tools/ubsan/suppressions.txt | 3 --- 3 files changed, 18 insertions(+), 3 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2026-07-30-12-05-00.gh-issue-148286.Bv3Nq8.rst diff --git a/Misc/NEWS.d/next/Library/2026-07-30-12-05-00.gh-issue-148286.Bv3Nq8.rst b/Misc/NEWS.d/next/Library/2026-07-30-12-05-00.gh-issue-148286.Bv3Nq8.rst new file mode 100644 index 000000000000000..749c3de18f5ae9e --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-07-30-12-05-00.gh-issue-148286.Bv3Nq8.rst @@ -0,0 +1,7 @@ +Fix undefined behaviour in :meth:`!io.StringIO.read` after an overseek. +Reading from a position past the end of the buffer formed the pointer +``self->buf + self->pos`` before returning the empty string; for a large +enough position the arithmetic wrapped and produced a pointer below the +buffer. :class:`io.StringIO` now returns the empty string early, as +``_stringio_readline()`` already did, which removes the +``Modules/_io/stringio.c`` entry from ``Tools/ubsan/suppressions.txt``. diff --git a/Modules/_io/stringio.c b/Modules/_io/stringio.c index 27605b4c44239e9..ff74acee0bbc8f9 100644 --- a/Modules/_io/stringio.c +++ b/Modules/_io/stringio.c @@ -349,6 +349,17 @@ _io_StringIO_read_impl(stringio *self, Py_ssize_t size) } ENSURE_REALIZED(self); + + /* In case of overseek, return the empty string, as _stringio_readline() + does. `size` is already clamped to 0 here, so nothing would be read + through the pointer -- but forming `self->buf + self->pos` when + `self->pos` is past the end of the buffer is undefined behaviour on + its own, and for a large enough `self->pos` the multiplication by + sizeof(Py_UCS4) wraps and yields a pointer below `self->buf`. */ + if (self->pos >= self->string_size) { + return Py_GetConstant(Py_CONSTANT_EMPTY_STR); + } + output = self->buf + self->pos; self->pos += size; return PyUnicode_FromKindAndData(PyUnicode_4BYTE_KIND, output, size); diff --git a/Tools/ubsan/suppressions.txt b/Tools/ubsan/suppressions.txt index 2b9ad49ebf1eefb..b22663e47759e9b 100644 --- a/Tools/ubsan/suppressions.txt +++ b/Tools/ubsan/suppressions.txt @@ -14,6 +14,3 @@ shift-base:Modules/_ctypes/cfield.c # Modules/_ctypes/cfield.c:640:1: runtime error: signed integer overflow: -2147483648 - 1 cannot be represented in type 'int' signed-integer-overflow:Modules/_ctypes/cfield.c - -# Modules/_io/stringio.c:350:24: runtime error: addition of unsigned offset to 0x7fd01ec25850 overflowed to 0x7fd01ec2584c -pointer-overflow:Modules/_io/stringio.c