Skip to content

Commit addc87f

Browse files
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.
1 parent f4b1d3e commit addc87f

3 files changed

Lines changed: 18 additions & 3 deletions

File tree

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
Fix undefined behaviour in :meth:`!io.StringIO.read` after an overseek.
2+
Reading from a position past the end of the buffer formed the pointer
3+
``self->buf + self->pos`` before returning the empty string; for a large
4+
enough position the arithmetic wrapped and produced a pointer below the
5+
buffer. :class:`io.StringIO` now returns the empty string early, as
6+
``_stringio_readline()`` already did, which removes the
7+
``Modules/_io/stringio.c`` entry from ``Tools/ubsan/suppressions.txt``.

Modules/_io/stringio.c

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -349,6 +349,17 @@ _io_StringIO_read_impl(stringio *self, Py_ssize_t size)
349349
}
350350

351351
ENSURE_REALIZED(self);
352+
353+
/* In case of overseek, return the empty string, as _stringio_readline()
354+
does. `size` is already clamped to 0 here, so nothing would be read
355+
through the pointer -- but forming `self->buf + self->pos` when
356+
`self->pos` is past the end of the buffer is undefined behaviour on
357+
its own, and for a large enough `self->pos` the multiplication by
358+
sizeof(Py_UCS4) wraps and yields a pointer below `self->buf`. */
359+
if (self->pos >= self->string_size) {
360+
return Py_GetConstant(Py_CONSTANT_EMPTY_STR);
361+
}
362+
352363
output = self->buf + self->pos;
353364
self->pos += size;
354365
return PyUnicode_FromKindAndData(PyUnicode_4BYTE_KIND, output, size);

Tools/ubsan/suppressions.txt

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,3 @@ shift-base:Modules/_ctypes/cfield.c
1414

1515
# Modules/_ctypes/cfield.c:640:1: runtime error: signed integer overflow: -2147483648 - 1 cannot be represented in type 'int'
1616
signed-integer-overflow:Modules/_ctypes/cfield.c
17-
18-
# Modules/_io/stringio.c:350:24: runtime error: addition of unsigned offset to 0x7fd01ec25850 overflowed to 0x7fd01ec2584c
19-
pointer-overflow:Modules/_io/stringio.c

0 commit comments

Comments
 (0)