From 1502a524c76c7e43d386f8accab9d16cadcda6df Mon Sep 17 00:00:00 2001 From: Cody Maloney Date: Tue, 27 Oct 2026 15:44:33 -0700 Subject: [PATCH 1/3] gh-160781: Fix BytesIO overflow handling for large sizes BytesIO.write() at a position near sys.maxsize raised SystemError ("Negative size passed to PyBytes_FromStringAndSize") because the overallocated buffer size could exceed PY_SSIZE_T_MAX. Check the computed allocation, not only the requested size. BytesIO.truncate() converted its argument with PyLong_AsLong(), which rejects sizes above 2**31 - 1 on Windows where C long is 32-bit. Use PyNumber_AsSsize_t() like the Py_ssize_t Argument Clinic converter it used before. Co-Authored-By: Claude Opus 5.5 --- Lib/test/test_io/test_memoryio.py | 9 +++++++++ .../2026-10-27-15-26-53.gh-issue-160781.4VSeQK.rst | 3 +++ .../2026-10-27-15-42-16.gh-issue-160781.VGLB9S.rst | 2 ++ Modules/_io/bytesio.c | 5 ++++- 4 files changed, 18 insertions(+), 1 deletion(-) create mode 100644 Misc/NEWS.d/next/Library/2026-10-27-15-26-53.gh-issue-160781.4VSeQK.rst create mode 100644 Misc/NEWS.d/next/Library/2026-10-27-15-42-16.gh-issue-160781.VGLB9S.rst diff --git a/Lib/test/test_io/test_memoryio.py b/Lib/test/test_io/test_memoryio.py index 6a286e5a62ef44c..b4e97fa21f222dd 100644 --- a/Lib/test/test_io/test_memoryio.py +++ b/Lib/test/test_io/test_memoryio.py @@ -591,6 +591,10 @@ def test_issue141311(self): self.assertEqual(sys.maxsize, memio.seek(sys.maxsize)) buf = bytearray(2) self.assertEqual(0, memio.readinto(buf)) + # Writing there must not overflow the buffer size. + memio.seek(sys.maxsize - 1) + self.assertRaises((MemoryError, OverflowError), memio.write, b"x") + self.assertEqual(memio.getvalue(), b"") def test_peek(self): buf = self.buftype("1234567890") @@ -984,6 +988,11 @@ def __index__(self): finally: size.result.release() + # Sizes above 2**31 - 1 must work where C long is 32-bit. + stream = self.ioclass(b'abc') + self.assertEqual(stream.truncate(2**31), 2**31) + self.assertEqual(stream.getvalue(), b'abc') + def test_readinto_overlapping_buffer(self): with self.ioclass(b'abcd') as stream: with stream.getbuffer()[1:] as target: diff --git a/Misc/NEWS.d/next/Library/2026-10-27-15-26-53.gh-issue-160781.4VSeQK.rst b/Misc/NEWS.d/next/Library/2026-10-27-15-26-53.gh-issue-160781.4VSeQK.rst new file mode 100644 index 000000000000000..f6463a5d5313690 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-10-27-15-26-53.gh-issue-160781.4VSeQK.rst @@ -0,0 +1,3 @@ +Fix :class:`io.BytesIO` raising :exc:`SystemError` instead of +:exc:`OverflowError` when writing at a position close to +:data:`sys.maxsize`. diff --git a/Misc/NEWS.d/next/Library/2026-10-27-15-42-16.gh-issue-160781.VGLB9S.rst b/Misc/NEWS.d/next/Library/2026-10-27-15-42-16.gh-issue-160781.VGLB9S.rst new file mode 100644 index 000000000000000..1ad3a9c6d36e8de --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-10-27-15-42-16.gh-issue-160781.VGLB9S.rst @@ -0,0 +1,2 @@ +Fix :meth:`io.BytesIO.truncate` rejecting sizes larger than ``2**31 - 1`` on +Windows. diff --git a/Modules/_io/bytesio.c b/Modules/_io/bytesio.c index 01f63f8a677d371..2571da4538363f1 100644 --- a/Modules/_io/bytesio.c +++ b/Modules/_io/bytesio.c @@ -221,6 +221,9 @@ resize_buffer_lock_held(bytesio *self, size_t size) /* Major upsize; resize up to exact size */ alloc = size + 1; } + if (alloc > PY_SSIZE_T_MAX) { + goto overflow; + } if (SHARED_BUF(self)) { if (unshare_buffer_lock_held(self, alloc) < 0) @@ -755,7 +758,7 @@ _io_BytesIO_truncate_impl(bytesio *self, PyObject *size) new_size = self->pos; } else { - new_size = PyLong_AsLong(size); + new_size = PyNumber_AsSsize_t(size, PyExc_OverflowError); if (new_size == -1 && PyErr_Occurred()) { return NULL; } From c5608f39b4b9be0bb9bd2d6ae0e278ce0c501df7 Mon Sep 17 00:00:00 2001 From: Cody Maloney Date: Tue, 27 Oct 2026 16:00:13 -0700 Subject: [PATCH 2/3] Fix docs reference --- .../Library/2026-10-27-15-42-16.gh-issue-160781.VGLB9S.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Misc/NEWS.d/next/Library/2026-10-27-15-42-16.gh-issue-160781.VGLB9S.rst b/Misc/NEWS.d/next/Library/2026-10-27-15-42-16.gh-issue-160781.VGLB9S.rst index 1ad3a9c6d36e8de..73c8c177fc88282 100644 --- a/Misc/NEWS.d/next/Library/2026-10-27-15-42-16.gh-issue-160781.VGLB9S.rst +++ b/Misc/NEWS.d/next/Library/2026-10-27-15-42-16.gh-issue-160781.VGLB9S.rst @@ -1,2 +1,2 @@ -Fix :meth:`io.BytesIO.truncate` rejecting sizes larger than ``2**31 - 1`` on -Windows. +Fix :class:`io.BytesIO` implementation of :meth:`io.IOBase.truncate` rejecting +sizes larger than ``2**31 - 1`` on Windows. From 0a0f6fd04940b856d4a167cac2af1a0adcc8ddad Mon Sep 17 00:00:00 2001 From: Cody Maloney Date: Tue, 27 Oct 2026 16:05:14 -0700 Subject: [PATCH 3/3] Skip the 2**31 truncate when Py_ssize_t is 32-bit --- Lib/test/test_io/test_memoryio.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/Lib/test/test_io/test_memoryio.py b/Lib/test/test_io/test_memoryio.py index b4e97fa21f222dd..dd6dc057d8b8ae8 100644 --- a/Lib/test/test_io/test_memoryio.py +++ b/Lib/test/test_io/test_memoryio.py @@ -989,9 +989,10 @@ def __index__(self): size.result.release() # Sizes above 2**31 - 1 must work where C long is 32-bit. - stream = self.ioclass(b'abc') - self.assertEqual(stream.truncate(2**31), 2**31) - self.assertEqual(stream.getvalue(), b'abc') + if sys.maxsize > 2**31: + stream = self.ioclass(b'abc') + self.assertEqual(stream.truncate(2**31), 2**31) + self.assertEqual(stream.getvalue(), b'abc') def test_readinto_overlapping_buffer(self): with self.ioclass(b'abcd') as stream: