Skip to content
38 changes: 38 additions & 0 deletions Lib/test/test_bytes.py
Original file line number Diff line number Diff line change
Expand Up @@ -1889,6 +1889,44 @@ def __index__(self):
with self.assertRaises(IndexError):
self._testlimitedcapi.sequence_setitem(b, 0, Boom())

def test_mutating_index_inbounds(self):
class MutatesOnIndex:
new_ba: bytearray

def __init__(self):
self.ba = bytearray(0x180)

def __index__(self):
# Clear the original bytearray, mutating it during index assignment.
# If the internal buffers are held over this operation, they become dangling
# However, this will fail a bounds check as above (as the clear sets bounds to zero)
self.ba.clear()
# At this moment, the bytearray potentially has a dangling pointer
# Create a new bytearray to catch any writes
self.new_ba = bytearray(0x180)
# Ensure bounds check passes
self.ba.extend([0] * 0x180)
return 0

with self.subTest("skip_bounds_safety"):
instance = MutatesOnIndex()
instance.ba[instance] = ord("?")
self.assertEqual(instance.ba[0], ord("?"), "Assigned bytearray not altered")
self.assertEqual(instance.new_ba, bytearray(0x180), "Wrong object altered")

with self.subTest("skip_bounds_safety_capi"):
instance = MutatesOnIndex()
instance.ba[instance] = ord("?")
self._testlimitedcapi.sequence_setitem(instance.ba, instance, ord("?"))
self.assertEqual(instance.ba[0], ord("?"), "Assigned bytearray not altered")
self.assertEqual(instance.new_ba, bytearray(0x180), "Wrong object altered")

with self.subTest("skip_bounds_safety_slice"):
instance = MutatesOnIndex()
instance.ba[instance:1] = [ord("?")]
self.assertEqual(instance.ba[0], ord("?"), "Assigned bytearray not altered")
self.assertEqual(instance.new_ba, bytearray(0x180), "Wrong object altered")


class AssortedBytesTest(unittest.TestCase):
#
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
Fix an issue where a :class:`bytearray` item assignment could crash or write
to the wrong bytearray when resized by the new value's :meth:`__index__`
method.
7 changes: 5 additions & 2 deletions Objects/bytearrayobject.c
Original file line number Diff line number Diff line change
Expand Up @@ -709,7 +709,8 @@ bytearray_ass_subscript_lock_held(PyObject *op, PyObject *index, PyObject *value
_Py_CRITICAL_SECTION_ASSERT_OBJECT_LOCKED(op);
PyByteArrayObject *self = _PyByteArray_CAST(op);
Py_ssize_t start, stop, step, slicelen;
char *buf = PyByteArray_AS_STRING(self);
// GH-91153: we cannot store a reference to the internal buffer here, as _getbytevalue might call into python code
// that could then invalidate it.

if (_PyIndex_Check(index)) {
Py_ssize_t i = PyNumber_AsSsize_t(index, PyExc_IndexError);
Expand Down Expand Up @@ -744,7 +745,7 @@ bytearray_ass_subscript_lock_held(PyObject *op, PyObject *index, PyObject *value
}
else {
assert(0 <= ival && ival < 256);
buf[i] = (char)ival;
PyByteArray_AS_STRING(self)[i] = (char)ival;
return 0;
}
}
Expand Down Expand Up @@ -805,6 +806,7 @@ bytearray_ass_subscript_lock_held(PyObject *op, PyObject *index, PyObject *value
/* Delete slice */
size_t cur;
Py_ssize_t i;
char* buf = PyByteArray_AS_STRING(self);

if (!_canresize(self))
return -1;
Expand Down Expand Up @@ -845,6 +847,7 @@ bytearray_ass_subscript_lock_held(PyObject *op, PyObject *index, PyObject *value
/* Assign slice */
Py_ssize_t i;
size_t cur;
char* buf = PyByteArray_AS_STRING(self);

if (needed != slicelen) {
PyErr_Format(PyExc_ValueError,
Expand Down
Loading