Skip to content

Commit 3ca92d9

Browse files
[3.15] gh-157242: Do not close io.BytesIO on MemoryError (GH-157344) (#159275)
gh-157242: Do not close io.BytesIO on MemoryError (GH-157344) Replace _PyBytes_Resize() with _PyBytes_ResizeKeepOnError(). Fix truncate(): on error, restore string_size to its previous value. (cherry picked from commit 23eda00) Co-authored-by: Victor Stinner <vstinner@python.org>
1 parent 55000c6 commit 3ca92d9

3 files changed

Lines changed: 38 additions & 2 deletions

File tree

‎Lib/test/test_io/test_memoryio.py‎

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
import unittest
77
from test import support
8+
from test.support import import_helper
89

910
import gc
1011
import io
@@ -651,6 +652,35 @@ def __buffer__(self, flags):
651652
self.assertEqual(memio.getvalue(), b"01AAA56789")
652653
self.assertEqual(memio.tell(), 5)
653654

655+
def test_memory_error(self):
656+
# gh-157242: io.BytesIO() must not close the file on MemoryError
657+
_testcapi = import_helper.import_module('_testcapi')
658+
659+
# write()
660+
stream = self.ioclass()
661+
stream.write(self.buftype('abc'))
662+
with self.assertRaises(MemoryError):
663+
try:
664+
data = self.buftype('def')
665+
_testcapi.set_nomemory(0)
666+
stream.write(data)
667+
finally:
668+
_testcapi.remove_mem_hooks()
669+
stream.write(self.buftype('123'))
670+
self.assertEqual(stream.getvalue(), self.buftype('abc123'))
671+
672+
# truncate()
673+
data = self.buftype('x' * 100)
674+
stream = self.ioclass()
675+
stream.write(data)
676+
with self.assertRaises(MemoryError):
677+
try:
678+
_testcapi.set_nomemory(0)
679+
stream.truncate(5)
680+
finally:
681+
_testcapi.remove_mem_hooks()
682+
self.assertEqual(stream.getvalue(), data)
683+
654684

655685
class TextIOTestMixin:
656686

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
:class:`io.BytesIO` is no longer closed on ``write()`` and ``truncate()``
2+
failure (:exc:`MemoryError`). Patch by Victor Stinner.

‎Modules/_io/bytesio.c‎

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
#include "Python.h"
2+
#include "pycore_bytesobject.h" // _PyBytes_ResizeKeepOnError()
23
#include "pycore_critical_section.h" // Py_BEGIN_CRITICAL_SECTION()
34
#include "pycore_object.h"
45
#include "pycore_pyatomic_ft_wrappers.h"
@@ -107,7 +108,7 @@ resize_unshared_buffer_lock_held(bytesio *self, Py_ssize_t size)
107108
Callers must detach first. */
108109
assert(!self->buf_shared);
109110
#endif
110-
int ret = _PyBytes_Resize(&self->buf, size);
111+
int ret = _PyBytes_ResizeKeepOnError(&self->buf, size);
111112
if (ret == 0) {
112113
clear_shared_buf(self);
113114
}
@@ -712,9 +713,12 @@ _io_BytesIO_truncate_impl(bytesio *self, PyObject *size)
712713
}
713714

714715
if (new_size < self->string_size) {
716+
Py_ssize_t old_string_size = self->string_size;
715717
self->string_size = new_size;
716-
if (resize_buffer_lock_held(self, new_size) < 0)
718+
if (resize_buffer_lock_held(self, new_size) < 0) {
719+
self->string_size = old_string_size;
717720
return NULL;
721+
}
718722
}
719723

720724
return PyLong_FromSsize_t(new_size);

0 commit comments

Comments
 (0)