Skip to content

Commit 8cfce4e

Browse files
committed
gh-160324: Keep io.StringIO contents after MemoryError
Shrink the buffer of the accumulating writer before finishing it, so that getvalue(), read() and the other methods leave the contents unchanged when they fail with MemoryError instead of dropping them.
1 parent 3a6fae4 commit 8cfce4e

3 files changed

Lines changed: 42 additions & 12 deletions

File tree

‎Lib/test/test_io/test_memoryio.py‎

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1161,14 +1161,13 @@ def test_memory_error_consistent_state(self):
11611161
pass
11621162
else:
11631163
continue
1164-
# The contents are either intact or lost, never garbage.
1165-
value = memio.getvalue()
1166-
self.assertIn(value, (text, ""))
1167-
self.assertEqual(memio.seek(0, 2), len(value))
1164+
# The contents are unchanged.
1165+
self.assertEqual(memio.getvalue(), text)
1166+
self.assertEqual(memio.seek(0, 2), len(text))
11681167
memio.seek(0)
1169-
self.assertEqual(memio.readline(), value)
1168+
self.assertEqual(memio.readline(), text)
11701169
memio.write("!")
1171-
self.assertEqual(memio.getvalue(), value + "!")
1170+
self.assertEqual(memio.getvalue(), text + "!")
11721171

11731172

11741173
class CStringIOPickleTest(PyStringIOPickleTest):
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
Fix a crash or garbage data when an :class:`io.StringIO` object is used again
22
after one of its methods failed with :exc:`MemoryError`. The object now keeps
3-
its contents or becomes empty.
3+
its contents.

‎Modules/_io/stringio.c‎

Lines changed: 36 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
#include "Python.h"
22
#include <stddef.h> // offsetof()
33
#include "pycore_object.h"
4+
#include "pycore_unicodeobject.h" // _PyUnicode_ResizeCompact()
45
#include "pycore_weakref.h" // FT_CLEAR_WEAKREFS()
56
#include "_iomodule.h"
67

@@ -143,14 +144,39 @@ resize_buffer(stringio *self, size_t size)
143144
return -1;
144145
}
145146

147+
/* Shrink the buffer of the writer to its contents, so that the following
148+
PyUnicodeWriter_Finish() has nothing to allocate and cannot fail.
149+
If this fails, the writer is unchanged. */
150+
static int
151+
shrink_writer(stringio *self)
152+
{
153+
_PyUnicodeWriter *writer = (_PyUnicodeWriter *)self->writer;
154+
if (writer->readonly || writer->buffer == NULL
155+
|| writer->pos == 0 || writer->pos == writer->size)
156+
{
157+
return 0;
158+
}
159+
PyObject *buffer = _PyUnicode_ResizeCompact(writer->buffer, writer->pos);
160+
if (buffer == NULL) {
161+
return -1;
162+
}
163+
writer->buffer = NULL;
164+
_PyUnicodeWriter_SetBuffer(writer, buffer);
165+
return 0;
166+
}
167+
146168
static PyObject *
147169
make_intermediate(stringio *self)
148170
{
149-
/* Create the new writer first: if this fails, the object keeps its
150-
contents. */
171+
/* Create the new writer and shrink the old one first: if this fails,
172+
the object keeps its contents. */
151173
PyUnicodeWriter *writer = PyUnicodeWriter_Create(0);
152174
if (writer == NULL)
153175
return NULL;
176+
if (shrink_writer(self) < 0) {
177+
PyUnicodeWriter_Discard(writer);
178+
return NULL;
179+
}
154180

155181
PyObject *intermediate = PyUnicodeWriter_Finish(self->writer);
156182
self->writer = writer;
@@ -177,10 +203,13 @@ realize(stringio *self)
177203
return 0;
178204
assert(self->state == STATE_ACCUMULATING);
179205

180-
/* Allocate the internal buffer first: if this fails, the object stays
181-
in the accumulating state and keeps its contents. */
206+
/* Allocate the internal buffer and shrink the writer first: if this
207+
fails, the object stays in the accumulating state and keeps its
208+
contents. */
182209
if (resize_buffer(self, self->string_size) < 0)
183210
return -1;
211+
if (shrink_writer(self) < 0)
212+
return -1;
184213
self->state = STATE_REALIZED;
185214

186215
intermediate = PyUnicodeWriter_Finish(self->writer);
@@ -364,7 +393,9 @@ _io_StringIO_read_impl(stringio *self, Py_ssize_t size)
364393
/* Optimization for seek(0); read() */
365394
if (self->state == STATE_ACCUMULATING && self->pos == 0 && size == n) {
366395
PyObject *result = make_intermediate(self);
367-
self->pos = self->string_size;
396+
if (result != NULL) {
397+
self->pos = self->string_size;
398+
}
368399
return result;
369400
}
370401

0 commit comments

Comments
 (0)