Skip to content
Closed
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
52 changes: 52 additions & 0 deletions Lib/test/test_array.py
Original file line number Diff line number Diff line change
Expand Up @@ -1737,6 +1737,58 @@ def __float__(self):
self.assertRaises(IndexError, victim.__setitem__, 1, Float())
self.assertEqual(len(victim), 0)

# Tests for use-after-free in array.tofile() when the writer
# callback mutates the array.
# See: https://github.com/python/cpython/issues/142884.

def test_tofile_reentrant_write_clear(self):
# tofile() must not crash when f.write() clears the array.
# Needs >64 KB so tofile() uses multiple blocks.
BLOCKSIZE = 64 * 1024
victim = array.array('B', b'\0' * (BLOCKSIZE * 2))

class Writer:
armed = True
def write(self, data):
if Writer.armed:
Writer.armed = False
victim.clear()
return len(data)

victim.tofile(Writer()) # must not crash

def test_tofile_reentrant_write_shrink(self):
# tofile() must not crash when f.write() shrinks the array.
BLOCKSIZE = 64 * 1024
victim = array.array('B', b'\0' * (BLOCKSIZE * 2))

class Writer:
armed = True
def write(self, data):
if Writer.armed:
Writer.armed = False
victim[:] = array.array('B', b'\0')
return len(data)

victim.tofile(Writer()) # must not crash

def test_tofile_reentrant_write_reallocate(self):
# tofile() must not crash when f.write() clears and
# reallocates the array to a smaller buffer.
BLOCKSIZE = 64 * 1024
victim = array.array('B', b'\0' * (BLOCKSIZE * 2))

class Writer:
armed = True
def write(self, data):
if Writer.armed:
Writer.armed = False
victim.clear()
victim.append(0)
return len(data)

victim.tofile(Writer()) # must not crash


if __name__ == "__main__":
unittest.main()
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
Fix crash in :meth:`array.array.tofile` when a reentrant ``write()``
callback mutates the array. The function now re-checks the array size on
every iteration instead of caching it once at the start.
23 changes: 13 additions & 10 deletions Modules/arraymodule.c
Original file line number Diff line number Diff line change
Expand Up @@ -1659,31 +1659,34 @@ static PyObject *
array_array_tofile_impl(arrayobject *self, PyTypeObject *cls, PyObject *f)
/*[clinic end generated code: output=4560c628d9c18bc2 input=5a24da7a7b407b52]*/
{
Py_ssize_t nbytes = Py_SIZE(self) * self->ob_descr->itemsize;
/* Write 64K blocks at a time */
/* XXX Make the block size settable */
int BLOCKSIZE = 64*1024;
Py_ssize_t nblocks = (nbytes + BLOCKSIZE - 1) / BLOCKSIZE;
Py_ssize_t i;

if (Py_SIZE(self) == 0)
goto done;


array_state *state = get_array_state_by_class(cls);
assert(state != NULL);

for (i = 0; i < nblocks; i++) {
char* ptr = self->ob_item + i*BLOCKSIZE;
/* Re-check Py_SIZE() on every iteration because f.write() could
execute arbitrary Python code that modifies or clears the array. */
for (i = 0; ; i++) {
Py_ssize_t nbytes = Py_SIZE(self) * self->ob_descr->itemsize;
Py_ssize_t offset = (Py_ssize_t)i * BLOCKSIZE;
if (offset >= nbytes)
break;

Py_ssize_t size = BLOCKSIZE;
PyObject *bytes, *res;
if (offset + size > nbytes)
size = nbytes - offset;

if (i*BLOCKSIZE + size > nbytes)
size = nbytes - i*BLOCKSIZE;
bytes = PyBytes_FromStringAndSize(ptr, size);
char *ptr = self->ob_item + offset;
PyObject *bytes = PyBytes_FromStringAndSize(ptr, size);
if (bytes == NULL)
return NULL;
res = PyObject_CallMethodOneArg(f, state->str_write, bytes);
PyObject *res = PyObject_CallMethodOneArg(f, state->str_write, bytes);
Py_DECREF(bytes);
if (res == NULL)
return NULL;
Expand Down
Loading