diff --git a/Lib/test/test_array.py b/Lib/test/test_array.py index 5c919aea24ed94..7507cbcc170736 100755 --- a/Lib/test/test_array.py +++ b/Lib/test/test_array.py @@ -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() diff --git a/Misc/NEWS.d/next/Library/2026-02-17-10-00-00.gh-issue-142884.328a65.rst b/Misc/NEWS.d/next/Library/2026-02-17-10-00-00.gh-issue-142884.328a65.rst new file mode 100644 index 00000000000000..b157ccc5b7ded5 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-02-17-10-00-00.gh-issue-142884.328a65.rst @@ -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. diff --git a/Modules/arraymodule.c b/Modules/arraymodule.c index ec6a9840131e4d..469298a4793b46 100644 --- a/Modules/arraymodule.c +++ b/Modules/arraymodule.c @@ -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;