Skip to content

Commit 3375e69

Browse files
gpsheadmiss-islington
authored andcommitted
gh-160324: Keep io.StringIO state after MemoryError (GH-160325)
Allocate before consuming the accumulated writes in realize() and make_intermediate(), and reset the size when the contents are lost, so that later calls do not index outside the internal buffer. 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. ANT-2026-MPHGX2FL (cherry picked from commit 83b047e) Co-authored-by: Gregory P. Smith <68491+gpshead@users.noreply.github.com>
1 parent c67d3db commit 3375e69

3 files changed

Lines changed: 90 additions & 17 deletions

File tree

‎Lib/test/test_memoryio.py‎

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1067,6 +1067,39 @@ def __str__(self):
10671067
memio2.write(MyStr("world"))
10681068
self.assertEqual(memio2.getvalue(), "hello world")
10691069

1070+
@support.nomemtest
1071+
def test_memory_error_consistent_state(self):
1072+
# A MemoryError while copying the accumulated writes into the
1073+
# internal buffer must leave the size of the stream consistent with
1074+
# the buffer, so that using the object again does not read or write
1075+
# outside it.
1076+
piece = "0123456789" * 10
1077+
text = piece * 50
1078+
calls = [("readline",), ("read", 5), ("read",), ("getvalue",),
1079+
("truncate", 3), ("write", "ab"), ("__next__",)]
1080+
for name, *args in calls:
1081+
for n in range(3):
1082+
with self.subTest(method=name, n=n):
1083+
memio = self.ioclass()
1084+
for _ in range(50):
1085+
memio.write(piece)
1086+
memio.seek(0)
1087+
method = getattr(memio, name)
1088+
try:
1089+
with support.inject_memory_error_cm(n, n + 1):
1090+
method(*args)
1091+
except MemoryError:
1092+
pass
1093+
else:
1094+
continue
1095+
# The contents are unchanged.
1096+
self.assertEqual(memio.getvalue(), text)
1097+
self.assertEqual(memio.seek(0, 2), len(text))
1098+
memio.seek(0)
1099+
self.assertEqual(memio.readline(), text)
1100+
memio.write("!")
1101+
self.assertEqual(memio.getvalue(), text + "!")
1102+
10701103

10711104
class CStringIOPickleTest(PyStringIOPickleTest):
10721105
UnsupportedOperation = io.UnsupportedOperation
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
Fix a crash or garbage data when an :class:`io.StringIO` object is used again
2+
after one of its methods failed with :exc:`MemoryError`. The object now keeps
3+
its contents.

‎Modules/_io/stringio.c‎

Lines changed: 54 additions & 17 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

@@ -129,25 +130,52 @@ resize_buffer(stringio *self, size_t size)
129130
return -1;
130131
}
131132

133+
/* Shrink the buffer of the writer to its contents, so that the following
134+
PyUnicodeWriter_Finish() has nothing to allocate and cannot fail.
135+
If this fails, the writer is unchanged. */
136+
static int
137+
shrink_writer(stringio *self)
138+
{
139+
_PyUnicodeWriter *writer = (_PyUnicodeWriter *)self->writer;
140+
if (writer->readonly || writer->buffer == NULL
141+
|| writer->pos == 0 || writer->pos == writer->size)
142+
{
143+
return 0;
144+
}
145+
PyObject *buffer = _PyUnicode_ResizeCompact(writer->buffer, writer->pos);
146+
if (buffer == NULL) {
147+
return -1;
148+
}
149+
writer->buffer = NULL;
150+
_PyUnicodeWriter_SetBuffer(writer, buffer);
151+
return 0;
152+
}
153+
132154
static PyObject *
133155
make_intermediate(stringio *self)
134156
{
135-
PyObject *intermediate = PyUnicodeWriter_Finish(self->writer);
136-
self->writer = NULL;
137-
self->state = STATE_REALIZED;
138-
if (intermediate == NULL)
157+
/* Create the new writer and shrink the old one first: if this fails,
158+
the object keeps its contents. */
159+
PyUnicodeWriter *writer = PyUnicodeWriter_Create(0);
160+
if (writer == NULL)
139161
return NULL;
162+
if (shrink_writer(self) < 0) {
163+
PyUnicodeWriter_Discard(writer);
164+
return NULL;
165+
}
140166

141-
self->writer = PyUnicodeWriter_Create(0);
142-
if (self->writer == NULL) {
143-
Py_DECREF(intermediate);
167+
PyObject *intermediate = PyUnicodeWriter_Finish(self->writer);
168+
self->writer = writer;
169+
if (intermediate == NULL) {
170+
/* The contents are lost, the new writer is empty. */
171+
self->string_size = 0;
144172
return NULL;
145173
}
146174
if (PyUnicodeWriter_WriteStr(self->writer, intermediate)) {
147175
Py_DECREF(intermediate);
176+
self->string_size = 0;
148177
return NULL;
149178
}
150-
self->state = STATE_ACCUMULATING;
151179
return intermediate;
152180
}
153181

@@ -160,23 +188,30 @@ realize(stringio *self)
160188
if (self->state == STATE_REALIZED)
161189
return 0;
162190
assert(self->state == STATE_ACCUMULATING);
191+
192+
/* Allocate the internal buffer and shrink the writer first: if this
193+
fails, the object stays in the accumulating state and keeps its
194+
contents. */
195+
if (resize_buffer(self, self->string_size) < 0)
196+
return -1;
197+
if (shrink_writer(self) < 0)
198+
return -1;
163199
self->state = STATE_REALIZED;
164200

165201
intermediate = PyUnicodeWriter_Finish(self->writer);
166202
self->writer = NULL;
167-
if (intermediate == NULL)
203+
if (intermediate == NULL) {
204+
/* The contents are lost, the internal buffer contains nothing. */
205+
self->string_size = 0;
168206
return -1;
207+
}
169208

170-
/* Append the intermediate string to the internal buffer.
171-
The length should be equal to the current cursor position.
172-
*/
209+
/* Copy the intermediate string to the internal buffer. */
173210
len = PyUnicode_GET_LENGTH(intermediate);
174-
if (resize_buffer(self, len) < 0) {
175-
Py_DECREF(intermediate);
176-
return -1;
177-
}
211+
assert(len == self->string_size);
178212
if (!PyUnicode_AsUCS4(intermediate, self->buf, len, 0)) {
179213
Py_DECREF(intermediate);
214+
self->string_size = 0;
180215
return -1;
181216
}
182217

@@ -344,7 +379,9 @@ _io_StringIO_read_impl(stringio *self, Py_ssize_t size)
344379
/* Optimization for seek(0); read() */
345380
if (self->state == STATE_ACCUMULATING && self->pos == 0 && size == n) {
346381
PyObject *result = make_intermediate(self);
347-
self->pos = self->string_size;
382+
if (result != NULL) {
383+
self->pos = self->string_size;
384+
}
348385
return result;
349386
}
350387

0 commit comments

Comments
 (0)