diff --git a/Lib/test/test_io/test_memoryio.py b/Lib/test/test_io/test_memoryio.py index 624fd78327cae1..423b99779bc6e7 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 00000000000000..54ada2892f76fa --- /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 ab54df8c76758f..b487d6d7beca93 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,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) + if (resize_buffer_lock_held(self, new_size) < 0) { + self->string_size = old_string_size; return NULL; + } } return PyLong_FromSsize_t(new_size);