From 00d23bb6355543e7a23bce27e06de5e318c0c024 Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Sat, 12 Sep 2026 02:38:47 +0200 Subject: [PATCH 1/2] gh-157242: Do not close io.BytesIO on MemoryError Replace _PyBytes_Resize() with _PyBytes_ResizeKeepOnError(). Fix truncate(): only set string_size on resize success. --- Lib/test/test_io/test_memoryio.py | 30 +++++++++++++++++++ ...-09-12-03-14-57.gh-issue-157242.ycbtjC.rst | 2 ++ Modules/_io/bytesio.c | 8 +++-- 3 files changed, 37 insertions(+), 3 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2026-09-12-03-14-57.gh-issue-157242.ycbtjC.rst diff --git a/Lib/test/test_io/test_memoryio.py b/Lib/test/test_io/test_memoryio.py index 624fd78327cae10..423b99779bc6e78 100644 --- a/Lib/test/test_io/test_memoryio.py +++ b/Lib/test/test_io/test_memoryio.py @@ -5,6 +5,7 @@ import unittest from test import support +from test.support import import_helper import gc import io @@ -753,6 +754,35 @@ def __buffer__(self, flags): self.assertEqual(memio.getvalue(), b"01AAA56789") self.assertEqual(memio.tell(), 5) + def test_memory_error(self): + # gh-157242: io.BytesIO() must not close the file on MemoryError + _testcapi = import_helper.import_module('_testcapi') + + # write() + stream = self.ioclass() + stream.write(self.buftype('abc')) + with self.assertRaises(MemoryError): + try: + data = self.buftype('def') + _testcapi.set_nomemory(0) + stream.write(data) + finally: + _testcapi.remove_mem_hooks() + stream.write(self.buftype('123')) + self.assertEqual(stream.getvalue(), self.buftype('abc123')) + + # truncate() + data = self.buftype('x' * 100) + stream = self.ioclass() + stream.write(data) + with self.assertRaises(MemoryError): + try: + _testcapi.set_nomemory(0) + stream.truncate(5) + finally: + _testcapi.remove_mem_hooks() + self.assertEqual(stream.getvalue(), data) + class TextIOTestMixin: diff --git a/Misc/NEWS.d/next/Library/2026-09-12-03-14-57.gh-issue-157242.ycbtjC.rst b/Misc/NEWS.d/next/Library/2026-09-12-03-14-57.gh-issue-157242.ycbtjC.rst new file mode 100644 index 000000000000000..54ada2892f76fac --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-09-12-03-14-57.gh-issue-157242.ycbtjC.rst @@ -0,0 +1,2 @@ +:class:`io.BytesIO` is no longer closed on ``write()`` and ``truncate()`` +failure (:exc:`MemoryError`). Patch by Victor Stinner. diff --git a/Modules/_io/bytesio.c b/Modules/_io/bytesio.c index ab54df8c76758f9..9ecb61af40cc23e 100644 --- a/Modules/_io/bytesio.c +++ b/Modules/_io/bytesio.c @@ -1,4 +1,5 @@ #include "Python.h" +#include "pycore_bytesobject.h" // _PyBytes_ResizeKeepOnError() #include "pycore_critical_section.h" // Py_BEGIN_CRITICAL_SECTION() #include "pycore_object.h" #include "pycore_pyatomic_ft_wrappers.h" @@ -108,7 +109,7 @@ resize_unshared_buffer_lock_held(bytesio *self, Py_ssize_t size) Callers must detach first. */ assert(!self->buf_shared); #endif - int ret = _PyBytes_Resize(&self->buf, size); + int ret = _PyBytes_ResizeKeepOnError(&self->buf, size); if (ret == 0) { clear_shared_buf(self); } @@ -758,9 +759,10 @@ _io_BytesIO_truncate_impl(bytesio *self, PyObject *size) } if (new_size < self->string_size) { - self->string_size = new_size; - if (resize_buffer_lock_held(self, new_size) < 0) + if (resize_buffer_lock_held(self, new_size) < 0) { return NULL; + } + self->string_size = new_size; } return PyLong_FromSsize_t(new_size); From 6fda0a7c0f40131113b945465092fcebb9cc5d4f Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Sat, 12 Sep 2026 05:40:17 +0200 Subject: [PATCH 2/2] Fix string_size Revert the string_size change. Instead, restore string_size to its previous value on error. --- Modules/_io/bytesio.c | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Modules/_io/bytesio.c b/Modules/_io/bytesio.c index 9ecb61af40cc23e..b487d6d7beca93f 100644 --- a/Modules/_io/bytesio.c +++ b/Modules/_io/bytesio.c @@ -759,10 +759,12 @@ _io_BytesIO_truncate_impl(bytesio *self, PyObject *size) } if (new_size < self->string_size) { + Py_ssize_t old_string_size = self->string_size; + self->string_size = new_size; if (resize_buffer_lock_held(self, new_size) < 0) { + self->string_size = old_string_size; return NULL; } - self->string_size = new_size; } return PyLong_FromSsize_t(new_size);