Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions Include/internal/pycore_bytesobject.h
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,8 @@ PyAPI_FUNC(PyObject *) _PyBytes_Repeat(PyObject *self, Py_ssize_t n);
*/
#define _PyBytesObject_SIZE (offsetof(PyBytesObject, ob_sval) + 1)

extern int _PyBytes_ResizeKeepOnError(PyObject **pv, Py_ssize_t newsize);

/* --- PyBytesWriter ------------------------------------------------------ */

struct PyBytesWriter {
Expand Down
58 changes: 58 additions & 0 deletions Lib/test/test_bytes.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
"""

import array
import contextlib
import operator
import os
import re
Expand Down Expand Up @@ -48,6 +49,19 @@ def __index__(self):
return self.value


@contextlib.contextmanager
def inject_memory_error(testcase, start):
# Raise SkipTest if _testcapi extension module is missing
_testcapi = import_helper.import_module('_testcapi')

with testcase.assertRaises(MemoryError):
try:
_testcapi.set_nomemory(start)
yield
finally:
_testcapi.remove_mem_hooks()


class BaseBytesTest:

def assertTypedEqual(self, actual, expected):
Expand Down Expand Up @@ -1555,6 +1569,33 @@ def test_resize(self):
self.assertRaises(MemoryError, bytearray().resize, sys.maxsize)
self.assertRaises(MemoryError, bytearray(1000).resize, sys.maxsize)

def test_resize_error(self):
# gh-157242: If bytearray.resize() fails (memory allocation failure),
# the bytearray must be left unchanged.

# Simple bytearray
data = b'some data'
ba = bytearray(data)
with inject_memory_error(self, 0):
ba.resize(1024)
self.assertEqual(ba, bytearray(data))

# growing bytearray with non-zero logical start
ba = bytearray(b'0123456789')
expected = ba[3:]
del ba[:3]
with inject_memory_error(self, 0):
ba.resize(1024)
self.assertEqual(ba, expected)

# shrink bytearray with non-zero logical start
ba = bytearray(b'0123456789')
expected = ba[3:]
del ba[:3]
with inject_memory_error(self, 0):
ba.resize(1)
self.assertEqual(ba, expected)

def test_take_bytes(self):
ba = bytearray(b'ab')
self.assertEqual(ba.take_bytes(), b'ab')
Expand Down Expand Up @@ -1619,6 +1660,23 @@ def test_take_bytes(self):
self.assertEqual(ba, bytearray(b'A'))
self.assertEqual(ord(b'c'), ord('c'))

def test_take_bytes_error(self):
# gh-157242: If bytearray.take_bytes() fails (memory allocation
# failure), the bytearray must be left unchanged.

for to_take, mem_errors in (
(5, (0, 1)),
(None, (0,)),
):
for mem_error in mem_errors:
with self.subTest(mem_error=mem_error, to_take=to_take):
ba = bytearray(b'0123456789')
expected = ba[3:]
del ba[:3]
with inject_memory_error(self, mem_error):
ba.take_bytes(to_take)
self.assertEqual(ba, expected)

@support.cpython_only # tests an implementation detail
def test_take_bytes_optimization(self):
# Validate optimization around taking lots of little chunks out of a
Expand Down
15 changes: 15 additions & 0 deletions Lib/test/test_capi/test_bytes.py
Original file line number Diff line number Diff line change
Expand Up @@ -389,6 +389,21 @@ def test_resize(self):
writer.resize(len(b'number=123456'), b'456')
self.assertEqual(writer.finish(), self.result_type(b'number=123456'))

def test_resize_error(self):
small_buffer = _testcapi.PyBytesWriter_small_buffer
init = b'x' * (small_buffer * 2)
writer = self.create_writer(len(init), init)
size = len(init) + 100
try:
with self.assertRaises(MemoryError):
_testcapi.set_nomemory(0)
writer.resize(size, b'')
finally:
_testcapi.remove_mem_hooks()
suffix = b'still working'
writer.write_bytes(suffix, -1)
self.assertEqual(writer.finish(), self.result_type(init + suffix))

def test_format_i(self):
# Test PyBytesWriter_Format()
writer = self.create_writer()
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
Fix :c:func:`PyBytesWriter_Resize` to handle properly memory allocation
failure. Leave the writer unchanged on error, instead of leaving it in an
inconsistent state. Patch by Victor Stinner.
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
If :meth:`bytearray.resize` or :meth:`bytearray.take_bytes` fails, leave the
:class:`bytearray` unchanged, instead of clearing it. Patch by Victor
Stinner.
143 changes: 139 additions & 4 deletions Modules/_testcapi/mem.c
Original file line number Diff line number Diff line change
Expand Up @@ -177,18 +177,29 @@ fm_remove_hooks(void)
}
}

static void
fm_set_nomemory(int start, int stop)
{
/* Memory allocation fails after 'start' allocation requests, and until
* 'stop' allocation requests except when 'stop' is negative or equal
* to 0 (default) in which case allocation failures never stop. */
FmData.start = start;
FmData.stop = stop;
FmData.count = 0;
fm_setup_hooks();
}

static PyObject *
set_nomemory(PyObject *self, PyObject *args)
{
/* Memory allocation fails after 'start' allocation requests, and until
* 'stop' allocation requests except when 'stop' is negative or equal
* to 0 (default) in which case allocation failures never stop. */
FmData.count = 0;
FmData.stop = 0;
if (!PyArg_ParseTuple(args, "i|i", &FmData.start, &FmData.stop)) {
int start, stop = 0;
if (!PyArg_ParseTuple(args, "i|i", &start, &stop)) {
return NULL;
}
fm_setup_hooks();
fm_set_nomemory(start, stop);
Py_RETURN_NONE;
}

Expand Down Expand Up @@ -821,6 +832,129 @@ get_process_memory_usage(PyObject *self, PyObject *args)
#endif


struct bytes_resize_tracer {
PyObject *create;
PyObject *destroy;
};


static int
bytes_resize_tracer(PyObject *obj, PyRefTracerEvent event, void* data)
{
if (event != PyRefTracer_CREATE && event != PyRefTracer_DESTROY) {
return 0;
}

struct bytes_resize_tracer *tracer = (struct bytes_resize_tracer*)data;
if (!PyBytes_Check(obj)) {
return 0;
}

switch (event) {
case PyRefTracer_CREATE:
tracer->create = obj;
break;
case PyRefTracer_DESTROY:
tracer->destroy = obj;
break;
default:
break;
}
return 0;
}


// When _PyBytes_Resize() resizes a bytes object in-place, check that
// PyRefTracer_DESTROY and PyRefTracer_CREATE events are emitted.
// If no_memory is non-zero, inject MemoryError.
static int
check_bytes_resize_tracer(int no_memory)
{
PyObject *bytes = NULL;
PyRefTracer old_tracer = NULL;
void *old_tracer_data = NULL;
int restore_tracer = 0;

bytes = PyBytes_FromString("hello");
if (bytes == NULL) {
goto error;
}
assert(PyUnstable_Object_IsUniquelyReferenced(bytes));

old_tracer = PyRefTracer_GetTracer(&old_tracer_data);
restore_tracer = 1;

struct bytes_resize_tracer tracer = {0};
if (PyRefTracer_SetTracer(bytes_resize_tracer, &tracer) != 0) {
goto error;
}

PyObject *old_bytes = bytes; // borrowed reference
if (no_memory) {
fm_set_nomemory(0, 0);
int res = _PyBytes_Resize(&bytes, 100);
assert(res < 0);
assert(bytes == NULL);
fm_remove_hooks();

assert(PyErr_ExceptionMatches(PyExc_MemoryError));
PyErr_Clear();
}
else {
if (_PyBytes_Resize(&bytes, 100) < 0) {
assert(bytes == NULL);
goto error;
}
}

if (tracer.destroy != old_bytes) {
PyErr_SetString(PyExc_AssertionError, "PyRefTracer_DESTROY not seen");
goto error;
}

int seen_create;
if (no_memory) {
seen_create = (tracer.create == old_bytes);
}
else {
seen_create = (tracer.create == bytes);
}
if (!seen_create) {
PyErr_SetString(PyExc_AssertionError, "PyRefTracer_CREATE not seen");
goto error;
}

Py_CLEAR(bytes);
if (PyRefTracer_SetTracer(old_tracer, old_tracer_data) != 0) {
restore_tracer = 0;
goto error;
}
return 0;

error:
Py_XDECREF(bytes);
if (restore_tracer) {
if (PyRefTracer_SetTracer(old_tracer, old_tracer_data) != 0) {
return -1;
}
}
return -1;
}


static PyObject*
test_bytes_resize_tracer(PyObject *self, PyObject *Py_UNUSED(ignored))
{
if (check_bytes_resize_tracer(0) < 0) {
return NULL;
}
if (check_bytes_resize_tracer(1) < 0) {
return NULL;
}
Py_RETURN_NONE;
}


static PyMethodDef test_methods[] = {
{"pymem_api_misuse", pymem_api_misuse, METH_NOARGS},
{"pymem_buffer_overflow", pymem_buffer_overflow, METH_NOARGS},
Expand All @@ -838,6 +972,7 @@ static PyMethodDef test_methods[] = {
#if TARGET_OS_OSX || defined(__FreeBSD__)
{"get_process_memory_usage", get_process_memory_usage, METH_VARARGS},
#endif
{"test_bytes_resize_tracer", test_bytes_resize_tracer, METH_NOARGS},

// Tracemalloc tests
{"tracemalloc_track", tracemalloc_track, METH_VARARGS},
Expand Down
Loading
Loading