From 7e5244ec7d8c062ef2457125913758a7c1c9110e Mon Sep 17 00:00:00 2001 From: fatelei Date: Thu, 18 Dec 2025 11:43:38 +0800 Subject: [PATCH 1/3] gh-142884: Use-After-Free Vulnerability Fixed in CPython array.array.tofile() --- Lib/test/test_array.py | 15 ++++++++ ...-12-18-11-41-37.gh-issue-142884.kjgukd.rst | 1 + Modules/arraymodule.c | 37 ++++++++++++++----- 3 files changed, 43 insertions(+), 10 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2025-12-18-11-41-37.gh-issue-142884.kjgukd.rst diff --git a/Lib/test/test_array.py b/Lib/test/test_array.py index 83b3c978da3581..e540a509de090e 100755 --- a/Lib/test/test_array.py +++ b/Lib/test/test_array.py @@ -1367,6 +1367,21 @@ def test_frombytearray(self): b = array.array(self.typecode, a) self.assertEqual(a, b) + def test_tofile_concurrent_mutation(self): + BLOCKSIZE = 64 * 1024 + victim = array.array('B', b'\0' * (BLOCKSIZE * 2)) + + class Writer: + cleared = False + def write(self, data): + if not self.cleared: + self.cleared = True + victim.clear() + return 0 + + victim.tofile(Writer()) + + class IntegerNumberTest(NumberTest): def test_type_error(self): a = array.array(self.typecode) diff --git a/Misc/NEWS.d/next/Library/2025-12-18-11-41-37.gh-issue-142884.kjgukd.rst b/Misc/NEWS.d/next/Library/2025-12-18-11-41-37.gh-issue-142884.kjgukd.rst new file mode 100644 index 00000000000000..7dd529c21575fc --- /dev/null +++ b/Misc/NEWS.d/next/Library/2025-12-18-11-41-37.gh-issue-142884.kjgukd.rst @@ -0,0 +1 @@ +Use-After-Free Vulnerability Fixed in CPython array.array.tofile(). diff --git a/Modules/arraymodule.c b/Modules/arraymodule.c index 729e085c19f006..83931ab32056a9 100644 --- a/Modules/arraymodule.c +++ b/Modules/arraymodule.c @@ -1587,35 +1587,52 @@ 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; + const Py_ssize_t BLOCKSIZE = 64*1024; + const Py_ssize_t itemsize = self->ob_descr->itemsize; + + Py_ssize_t nbytes = Py_SIZE(self) * itemsize; 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; - Py_ssize_t size = BLOCKSIZE; + for (Py_ssize_t i = 0; i < nblocks; i++) { + if (self->ob_item == NULL || Py_SIZE(self) == 0) { + break; + } + + if (Py_SIZE(self) > PY_SSIZE_T_MAX / itemsize) { + return PyErr_NoMemory(); + } + + Py_ssize_t current_nbytes = Py_SIZE(self) * itemsize; + const Py_ssize_t offset = i * BLOCKSIZE; + if (offset >= current_nbytes) { + break; + } + + Py_ssize_t size = current_nbytes - offset; + if (size > BLOCKSIZE) { + size = BLOCKSIZE; + } + + char* ptr = self->ob_item + offset; PyObject *bytes, *res; - if (i*BLOCKSIZE + size > nbytes) - size = nbytes - i*BLOCKSIZE; bytes = PyBytes_FromStringAndSize(ptr, size); if (bytes == NULL) return NULL; + res = PyObject_CallMethodOneArg(f, state->str_write, bytes); Py_DECREF(bytes); if (res == NULL) return NULL; - Py_DECREF(res); /* drop write result */ + Py_DECREF(res); } done: From 31f45131e8a8758154b2b8f2c8d76f2d31494a6c Mon Sep 17 00:00:00 2001 From: fatelei Date: Sun, 28 Dec 2025 21:07:13 +0800 Subject: [PATCH 2/3] chore: resolve comment review --- Lib/test/test_array.py | 2 ++ ...-12-18-11-41-37.gh-issue-142884.kjgukd.rst | 2 +- Modules/arraymodule.c | 20 +++++++++---------- 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/Lib/test/test_array.py b/Lib/test/test_array.py index e540a509de090e..81e3ac8f17cefb 100755 --- a/Lib/test/test_array.py +++ b/Lib/test/test_array.py @@ -1368,6 +1368,8 @@ def test_frombytearray(self): self.assertEqual(a, b) def test_tofile_concurrent_mutation(self): + # Keep this test in sync with the implementation in + # Modules/arraymodule.c:array_array_tofile_impl() BLOCKSIZE = 64 * 1024 victim = array.array('B', b'\0' * (BLOCKSIZE * 2)) diff --git a/Misc/NEWS.d/next/Library/2025-12-18-11-41-37.gh-issue-142884.kjgukd.rst b/Misc/NEWS.d/next/Library/2025-12-18-11-41-37.gh-issue-142884.kjgukd.rst index 7dd529c21575fc..dd5b28de2089be 100644 --- a/Misc/NEWS.d/next/Library/2025-12-18-11-41-37.gh-issue-142884.kjgukd.rst +++ b/Misc/NEWS.d/next/Library/2025-12-18-11-41-37.gh-issue-142884.kjgukd.rst @@ -1 +1 @@ -Use-After-Free Vulnerability Fixed in CPython array.array.tofile(). +:mod:`array`: fix a crash in :mod:`array.array.tofile` when the array is concurrently modified by the writer. diff --git a/Modules/arraymodule.c b/Modules/arraymodule.c index 83931ab32056a9..a38c0f53444a12 100644 --- a/Modules/arraymodule.c +++ b/Modules/arraymodule.c @@ -1589,11 +1589,8 @@ array_array_tofile_impl(arrayobject *self, PyTypeObject *cls, PyObject *f) { /* Write 64K blocks at a time */ /* XXX Make the block size settable */ - const Py_ssize_t BLOCKSIZE = 64*1024; - const Py_ssize_t itemsize = self->ob_descr->itemsize; - - Py_ssize_t nbytes = Py_SIZE(self) * itemsize; - Py_ssize_t nblocks = (nbytes + BLOCKSIZE - 1) / BLOCKSIZE; + Py_ssize_t BLOCKSIZE = 64*1024; + Py_ssize_t max_items = PY_SSIZE_T_MAX / self->ob_descr->itemsize; if (Py_SIZE(self) == 0) goto done; @@ -1601,17 +1598,17 @@ array_array_tofile_impl(arrayobject *self, PyTypeObject *cls, PyObject *f) array_state *state = get_array_state_by_class(cls); assert(state != NULL); - for (Py_ssize_t i = 0; i < nblocks; i++) { + Py_ssize_t offset = 0; + while (1) { if (self->ob_item == NULL || Py_SIZE(self) == 0) { break; } - if (Py_SIZE(self) > PY_SSIZE_T_MAX / itemsize) { + if (Py_SIZE(self) > max_items) { return PyErr_NoMemory(); } - Py_ssize_t current_nbytes = Py_SIZE(self) * itemsize; - const Py_ssize_t offset = i * BLOCKSIZE; + Py_ssize_t current_nbytes = Py_SIZE(self) * self->ob_descr->itemsize; if (offset >= current_nbytes) { break; } @@ -1627,12 +1624,13 @@ array_array_tofile_impl(arrayobject *self, PyTypeObject *cls, PyObject *f) bytes = PyBytes_FromStringAndSize(ptr, size); if (bytes == NULL) return NULL; - res = PyObject_CallMethodOneArg(f, state->str_write, bytes); Py_DECREF(bytes); if (res == NULL) return NULL; - Py_DECREF(res); + Py_DECREF(res); /* drop write result */ + + offset += size; } done: From 7712b1dfa1e266bd4c8ec4201e861adf1929b709 Mon Sep 17 00:00:00 2001 From: fatelei Date: Sun, 28 Dec 2025 21:19:00 +0800 Subject: [PATCH 3/3] chore: add total_size --- Modules/arraymodule.c | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/Modules/arraymodule.c b/Modules/arraymodule.c index a38c0f53444a12..1a5cd63300b1cc 100644 --- a/Modules/arraymodule.c +++ b/Modules/arraymodule.c @@ -1600,15 +1600,16 @@ array_array_tofile_impl(arrayobject *self, PyTypeObject *cls, PyObject *f) Py_ssize_t offset = 0; while (1) { - if (self->ob_item == NULL || Py_SIZE(self) == 0) { + Py_ssize_t total_size = Py_SIZE(self); + if (self->ob_item == NULL || total_size == 0) { break; } - if (Py_SIZE(self) > max_items) { + if (total_size > max_items) { return PyErr_NoMemory(); } - Py_ssize_t current_nbytes = Py_SIZE(self) * self->ob_descr->itemsize; + Py_ssize_t current_nbytes = total_size * self->ob_descr->itemsize; if (offset >= current_nbytes) { break; }