Skip to content

Commit 73e6301

Browse files
vstinnermaurycy
andauthored
[3.15] gh-157242: Fix PyBytesWriter_Resize() on MemoryError (#157243) (#159260)
gh-157242: Fix PyBytesWriter_Resize() on MemoryError (#157243) If PyBytesWriter_Resize() fails, leave the writer unchanged. Add a new internal _PyBytes_ResizeKeepOnError() function similar to _PyBytes_Resize() but leaves the bytes object unchanged on error. Add BaseWriterTest for BytesWriterTest and ByteArrayWriterTest. Add _testcapi.PyBytesWriter_small_buffer constant. (cherry picked from commit f5dd52d) Co-authored-by: Maurycy Pawłowski-Wieroński <maurycy@maurycy.com>
1 parent c38a347 commit 73e6301

6 files changed

Lines changed: 214 additions & 29 deletions

File tree

‎Include/internal/pycore_bytesobject.h‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,8 @@ _PyBytes_Repeat(char* dest, Py_ssize_t len_dest,
7373
*/
7474
#define _PyBytesObject_SIZE (offsetof(PyBytesObject, ob_sval) + 1)
7575

76+
extern int _PyBytes_ResizeKeepOnError(PyObject **pv, Py_ssize_t newsize);
77+
7678

7779
// Return the cached hash value, or -1 if not cached yet.
7880
static inline Py_hash_t

‎Lib/test/test_capi/test_bytes.py‎

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -465,6 +465,24 @@ def test_example_resize(self):
465465
def test_example_highlevel(self):
466466
self.assertEqual(_testcapi.byteswriter_highlevel(), b'Hello World!')
467467

468+
def test_resize_error(self):
469+
small_buffer = _testcapi.PyBytesWriter_small_buffer
470+
init = b'x' * (small_buffer * 2)
471+
writer = self.create_writer(len(init), init)
472+
size = len(init) + 100
473+
try:
474+
with self.assertRaises(MemoryError):
475+
_testcapi.set_nomemory(0)
476+
writer.resize(size, b'')
477+
finally:
478+
_testcapi.remove_mem_hooks()
479+
suffix = b'still working'
480+
writer.write_bytes(suffix, -1)
481+
self.bytes_equal(writer.finish(), init + suffix)
482+
483+
# Note: PyBytesWriter_Resize() leaves the buffer unchanged (no resize)
484+
# if the new size is smaller than the allocated size
485+
468486

469487
class ByteArrayWriterTest(BaseWriterTest, unittest.TestCase):
470488
RESULT_TYPE = bytearray
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
Fix :c:func:`PyBytesWriter_Resize` to handle properly memory allocation
2+
failure. Leave the writer unchanged on error, instead of leaving it in an
3+
inconsistent state. Patch by Victor Stinner.

‎Modules/_testcapi/bytes.c‎

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -378,6 +378,7 @@ byteswriter_highlevel(PyObject *Py_UNUSED(module), PyObject *Py_UNUSED(args))
378378
static size_t
379379
pybyteswriter_small_buffer_size(void)
380380
{
381+
// PyBytesWriter.obj is the second member, small_buffer is the first member
381382
return offsetof(PyBytesWriter, obj);
382383
}
383384

@@ -503,10 +504,8 @@ _PyTestCapi_Init_Bytes(PyObject *m)
503504
}
504505
Py_DECREF(writer_type);
505506

506-
// PyBytesWriter.obj is the second member, small_buffer is the first member
507507
long size = (long)pybyteswriter_small_buffer_size();
508508
if (PyModule_AddIntConstant(m, "PyBytesWriter_small_buffer", size) < 0) {
509-
Py_DECREF(writer_type);
510509
return -1;
511510
}
512511

‎Modules/_testcapi/mem.c‎

Lines changed: 139 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -155,18 +155,29 @@ fm_remove_hooks(void)
155155
}
156156
}
157157

158+
static void
159+
fm_set_nomemory(int start, int stop)
160+
{
161+
/* Memory allocation fails after 'start' allocation requests, and until
162+
* 'stop' allocation requests except when 'stop' is negative or equal
163+
* to 0 (default) in which case allocation failures never stop. */
164+
FmData.start = start;
165+
FmData.stop = stop;
166+
FmData.count = 0;
167+
fm_setup_hooks();
168+
}
169+
158170
static PyObject *
159171
set_nomemory(PyObject *self, PyObject *args)
160172
{
161173
/* Memory allocation fails after 'start' allocation requests, and until
162174
* 'stop' allocation requests except when 'stop' is negative or equal
163175
* to 0 (default) in which case allocation failures never stop. */
164-
FmData.count = 0;
165-
FmData.stop = 0;
166-
if (!PyArg_ParseTuple(args, "i|i", &FmData.start, &FmData.stop)) {
176+
int start, stop = 0;
177+
if (!PyArg_ParseTuple(args, "i|i", &start, &stop)) {
167178
return NULL;
168179
}
169-
fm_setup_hooks();
180+
fm_set_nomemory(start, stop);
170181
Py_RETURN_NONE;
171182
}
172183

@@ -731,6 +742,129 @@ tracemalloc_track_race(PyObject *self, PyObject *args)
731742
}
732743

733744

745+
struct bytes_resize_tracer {
746+
PyObject *create;
747+
PyObject *destroy;
748+
};
749+
750+
751+
static int
752+
bytes_resize_tracer(PyObject *obj, PyRefTracerEvent event, void* data)
753+
{
754+
if (event != PyRefTracer_CREATE && event != PyRefTracer_DESTROY) {
755+
return 0;
756+
}
757+
758+
struct bytes_resize_tracer *tracer = (struct bytes_resize_tracer*)data;
759+
if (!PyBytes_Check(obj)) {
760+
return 0;
761+
}
762+
763+
switch (event) {
764+
case PyRefTracer_CREATE:
765+
tracer->create = obj;
766+
break;
767+
case PyRefTracer_DESTROY:
768+
tracer->destroy = obj;
769+
break;
770+
default:
771+
break;
772+
}
773+
return 0;
774+
}
775+
776+
777+
// When _PyBytes_Resize() resizes a bytes object in-place, check that
778+
// PyRefTracer_DESTROY and PyRefTracer_CREATE events are emitted.
779+
// If no_memory is non-zero, inject MemoryError.
780+
static int
781+
check_bytes_resize_tracer(int no_memory)
782+
{
783+
PyObject *bytes = NULL;
784+
PyRefTracer old_tracer = NULL;
785+
void *old_tracer_data = NULL;
786+
int restore_tracer = 0;
787+
788+
bytes = PyBytes_FromString("hello");
789+
if (bytes == NULL) {
790+
goto error;
791+
}
792+
assert(PyUnstable_Object_IsUniquelyReferenced(bytes));
793+
794+
old_tracer = PyRefTracer_GetTracer(&old_tracer_data);
795+
restore_tracer = 1;
796+
797+
struct bytes_resize_tracer tracer = {0};
798+
if (PyRefTracer_SetTracer(bytes_resize_tracer, &tracer) != 0) {
799+
goto error;
800+
}
801+
802+
PyObject *old_bytes = bytes; // borrowed reference
803+
if (no_memory) {
804+
fm_set_nomemory(0, 0);
805+
int res = _PyBytes_Resize(&bytes, 100);
806+
assert(res < 0);
807+
assert(bytes == NULL);
808+
fm_remove_hooks();
809+
810+
assert(PyErr_ExceptionMatches(PyExc_MemoryError));
811+
PyErr_Clear();
812+
}
813+
else {
814+
if (_PyBytes_Resize(&bytes, 100) < 0) {
815+
assert(bytes == NULL);
816+
goto error;
817+
}
818+
}
819+
820+
if (tracer.destroy != old_bytes) {
821+
PyErr_SetString(PyExc_AssertionError, "PyRefTracer_DESTROY not seen");
822+
goto error;
823+
}
824+
825+
int seen_create;
826+
if (no_memory) {
827+
seen_create = (tracer.create == old_bytes);
828+
}
829+
else {
830+
seen_create = (tracer.create == bytes);
831+
}
832+
if (!seen_create) {
833+
PyErr_SetString(PyExc_AssertionError, "PyRefTracer_CREATE not seen");
834+
goto error;
835+
}
836+
837+
Py_CLEAR(bytes);
838+
if (PyRefTracer_SetTracer(old_tracer, old_tracer_data) != 0) {
839+
restore_tracer = 0;
840+
goto error;
841+
}
842+
return 0;
843+
844+
error:
845+
Py_XDECREF(bytes);
846+
if (restore_tracer) {
847+
if (PyRefTracer_SetTracer(old_tracer, old_tracer_data) != 0) {
848+
return -1;
849+
}
850+
}
851+
return -1;
852+
}
853+
854+
855+
static PyObject*
856+
test_bytes_resize_tracer(PyObject *self, PyObject *Py_UNUSED(ignored))
857+
{
858+
if (check_bytes_resize_tracer(0) < 0) {
859+
return NULL;
860+
}
861+
if (check_bytes_resize_tracer(1) < 0) {
862+
return NULL;
863+
}
864+
Py_RETURN_NONE;
865+
}
866+
867+
734868
static PyMethodDef test_methods[] = {
735869
{"pymem_api_misuse", pymem_api_misuse, METH_NOARGS},
736870
{"pymem_buffer_overflow", pymem_buffer_overflow, METH_NOARGS},
@@ -745,6 +879,7 @@ static PyMethodDef test_methods[] = {
745879
{"test_pymem_setrawallocators", test_pymem_setrawallocators, METH_NOARGS},
746880
{"test_pyobject_new", test_pyobject_new, METH_NOARGS},
747881
{"test_pyobject_setallocators", test_pyobject_setallocators, METH_NOARGS},
882+
{"test_bytes_resize_tracer", test_bytes_resize_tracer, METH_NOARGS},
748883

749884
// Tracemalloc tests
750885
{"tracemalloc_track", tracemalloc_track, METH_VARARGS},

‎Objects/bytesobject.c‎

Lines changed: 51 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -3333,69 +3333,96 @@ PyBytes_ConcatAndDel(PyObject **pv, PyObject *w)
33333333
does *not* include that), and a trailing \0 byte is stored.
33343334
*/
33353335

3336+
// Similar to _PyBytes_Resize(), but leaves the object unchanged on error.
33363337
int
3337-
_PyBytes_Resize(PyObject **pv, Py_ssize_t newsize)
3338+
_PyBytes_ResizeKeepOnError(PyObject **pv, Py_ssize_t newsize)
33383339
{
3339-
PyObject *v;
3340-
PyBytesObject *sv;
3341-
v = *pv;
3340+
PyObject *v = *pv;
3341+
PyObject *result;
3342+
33423343
if (!PyBytes_Check(v) || newsize < 0) {
3343-
*pv = 0;
3344-
Py_DECREF(v);
33453344
PyErr_BadInternalCall();
33463345
return -1;
33473346
}
3347+
33483348
Py_ssize_t oldsize = PyBytes_GET_SIZE(v);
33493349
if (oldsize == newsize) {
33503350
/* return early if newsize equals to v->ob_size */
33513351
return 0;
33523352
}
3353+
33533354
if (oldsize == 0) {
3354-
*pv = _PyBytes_FromSize(newsize, 0);
3355+
result = _PyBytes_FromSize(newsize, 0);
3356+
if (result == NULL) {
3357+
return -1;
3358+
}
3359+
*pv = result;
33553360
Py_DECREF(v);
3356-
return (*pv == NULL) ? -1 : 0;
3361+
return 0;
33573362
}
3363+
33583364
if (newsize == 0) {
3359-
*pv = bytes_get_empty();
3365+
*pv = bytes_get_empty(); // cannot fail
33603366
Py_DECREF(v);
33613367
return 0;
33623368
}
3369+
33633370
if (!_PyObject_IsUniquelyReferenced(v)) {
33643371
// Allocate and then copy so we don't get a shared immortal
33653372
// one-character singleton!
3366-
*pv = _PyBytes_FromSize(newsize, 0);
3367-
if (*pv) {
3368-
memcpy(PyBytes_AS_STRING(*pv), PyBytes_AS_STRING(v),
3369-
Py_MIN(oldsize, newsize));
3373+
result = _PyBytes_FromSize(newsize, 0);
3374+
if (!result) {
3375+
return -1;
33703376
}
3377+
3378+
memcpy(PyBytes_AS_STRING(result), PyBytes_AS_STRING(v),
3379+
Py_MIN(oldsize, newsize));
3380+
*pv = result;
33713381
Py_DECREF(v);
3372-
return (*pv == NULL) ? -1 : 0;
3382+
return 0;
33733383
}
33743384
assert(v != bytes_get_empty());
33753385

33763386
#ifdef Py_TRACE_REFS
33773387
_Py_ForgetReference(v);
33783388
#endif
33793389
_PyReftracerTrack(v, PyRefTracer_DESTROY);
3380-
*pv = (PyObject *)
3381-
PyObject_Realloc(v, PyBytesObject_SIZE + newsize);
3382-
if (*pv == NULL) {
3383-
#ifdef Py_REF_DEBUG
3384-
_Py_DecRefTotal(_PyThreadState_GET());
3390+
3391+
result = (PyObject *)PyObject_Realloc(v, PyBytesObject_SIZE + newsize);
3392+
if (result == NULL) {
3393+
#ifdef Py_TRACE_REFS
3394+
_Py_AddToAllObjects(v);
33853395
#endif
3386-
PyObject_Free(v);
3396+
_PyReftracerTrack(v, PyRefTracer_CREATE);
3397+
33873398
PyErr_NoMemory();
33883399
return -1;
33893400
}
3390-
_Py_NewReferenceNoTotal(*pv);
3391-
sv = (PyBytesObject *) *pv;
3401+
3402+
*pv = result;
3403+
v = result;
3404+
_Py_NewReferenceNoTotal(v);
3405+
PyBytesObject *sv = (PyBytesObject *)v;
33923406
Py_SET_SIZE(sv, newsize);
33933407
sv->ob_sval[newsize] = '\0';
33943408
set_ob_shash(sv, -1); /* invalidate cached hash value */
33953409
return 0;
33963410
}
33973411

33983412

3413+
int
3414+
_PyBytes_Resize(PyObject **pv, Py_ssize_t newsize)
3415+
{
3416+
int res = _PyBytes_ResizeKeepOnError(pv, newsize);
3417+
if (res < 0) {
3418+
PyObject *v = *pv;
3419+
*pv = NULL;
3420+
Py_DECREF(v);
3421+
}
3422+
return res;
3423+
}
3424+
3425+
33993426
/*********************** Bytes Iterator ****************************/
34003427

34013428
typedef struct {
@@ -3633,7 +3660,8 @@ byteswriter_resize(PyBytesWriter *writer, Py_ssize_t size, int resize)
36333660
}
36343661
}
36353662
else {
3636-
if (_PyBytes_Resize(&writer->obj, size)) {
3663+
if (_PyBytes_ResizeKeepOnError(&writer->obj, size)) {
3664+
assert(writer->obj != NULL);
36373665
return -1;
36383666
}
36393667
}

0 commit comments

Comments
 (0)