From bd7278381959e2e522935d5637f8919ab42ba624 Mon Sep 17 00:00:00 2001 From: raminfp Date: Tue, 17 Feb 2026 21:04:40 +0330 Subject: [PATCH] gh-142884: Fix use-after-free in array.array.tofile() with reentrant writer array_array_tofile_impl() pre-computed nbytes and nblocks once at the start of the function. If the file-like object's write() callback mutated the array (e.g. by clearing it or replacing its contents), the cached values became stale and subsequent iterations read from freed or invalid memory. Fix by re-checking Py_SIZE(self) on every loop iteration so the loop terminates safely when the array is modified during the write callback. --- Lib/test/test_array.py | 52 +++++++++++++++++++ ...-02-17-10-00-00.gh-issue-142884.328a65.rst | 3 ++ Modules/arraymodule.c | 23 ++++---- 3 files changed, 68 insertions(+), 10 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2026-02-17-10-00-00.gh-issue-142884.328a65.rst 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;