From 9976c2b6349a079ae39931d960b8c147e21c6c3f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C3=A9n=C3=A9dikt=20Tran?= <10796600+picnixz@users.noreply.github.com> Date: Sat, 27 Dec 2025 12:32:52 +0000 Subject: [PATCH 1/6] gh-143195: fix UAF in `{bytearray,memoryview}.hex(sep)` via re-entrant `sep.__len__` (#143209) --- Lib/test/test_bytes.py | 13 +++++++++++++ Lib/test/test_memoryview.py | 14 ++++++++++++++ .../2025-12-27-10-14-26.gh-issue-143195.MNldfr.rst | 3 +++ Objects/bytearrayobject.c | 8 +++++++- Objects/memoryobject.c | 8 +++++++- 5 files changed, 44 insertions(+), 2 deletions(-) create mode 100644 Misc/NEWS.d/next/Core_and_Builtins/2025-12-27-10-14-26.gh-issue-143195.MNldfr.rst diff --git a/Lib/test/test_bytes.py b/Lib/test/test_bytes.py index 21be61e4fec720..44b16c7d91e996 100644 --- a/Lib/test/test_bytes.py +++ b/Lib/test/test_bytes.py @@ -2092,6 +2092,19 @@ def make_case(): with self.assertRaises(BufferError): ba.rsplit(evil) + def test_hex_use_after_free(self): + # Prevent UAF in bytearray.hex(sep) with re-entrant sep.__len__. + # Regression test for https://github.com/python/cpython/issues/143195. + ba = bytearray(b'\xAA') + + class S(bytes): + def __len__(self): + ba.clear() + return 1 + + self.assertRaises(BufferError, ba.hex, S(b':')) + + class AssortedBytesTest(unittest.TestCase): # # Test various combinations of bytes and bytearray diff --git a/Lib/test/test_memoryview.py b/Lib/test/test_memoryview.py index 1bd58eb6408833..51b107103f6836 100644 --- a/Lib/test/test_memoryview.py +++ b/Lib/test/test_memoryview.py @@ -442,6 +442,20 @@ def test_issue22668(self): self.assertEqual(c.format, "H") self.assertEqual(d.format, "H") + def test_hex_use_after_free(self): + # Prevent UAF in memoryview.hex(sep) with re-entrant sep.__len__. + # Regression test for https://github.com/python/cpython/issues/143195. + ba = bytearray(b'A' * 1024) + mv = memoryview(ba) + + class S(bytes): + def __len__(self): + mv.release() + ba.clear() + return 1 + + self.assertRaises(BufferError, mv.hex, S(b':')) + # Variations on source objects for the buffer: bytes-like objects, then arrays # with itemsize > 1. diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2025-12-27-10-14-26.gh-issue-143195.MNldfr.rst b/Misc/NEWS.d/next/Core_and_Builtins/2025-12-27-10-14-26.gh-issue-143195.MNldfr.rst new file mode 100644 index 00000000000000..66dc5e22f0ab23 --- /dev/null +++ b/Misc/NEWS.d/next/Core_and_Builtins/2025-12-27-10-14-26.gh-issue-143195.MNldfr.rst @@ -0,0 +1,3 @@ +Fix use-after-free crashes in :meth:`bytearray.hex` and :meth:`memoryview.hex` +when the separator's :meth:`~object.__len__` mutates the original object. +Patch by Bénédikt Tran. diff --git a/Objects/bytearrayobject.c b/Objects/bytearrayobject.c index 338c71ad38f7aa..8a454aa48a0930 100644 --- a/Objects/bytearrayobject.c +++ b/Objects/bytearrayobject.c @@ -2664,7 +2664,13 @@ bytearray_hex_impl(PyByteArrayObject *self, PyObject *sep, int bytes_per_sep) { char* argbuf = PyByteArray_AS_STRING(self); Py_ssize_t arglen = PyByteArray_GET_SIZE(self); - return _Py_strhex_with_sep(argbuf, arglen, sep, bytes_per_sep); + // Prevent 'self' from being freed if computing len(sep) mutates 'self' + // in _Py_strhex_with_sep(). + // See: https://github.com/python/cpython/issues/143195. + self->ob_exports++; + PyObject *res = _Py_strhex_with_sep(argbuf, arglen, sep, bytes_per_sep); + self->ob_exports--; + return res; } static PyObject * diff --git a/Objects/memoryobject.c b/Objects/memoryobject.c index f1232f389210ea..2fd1d784b92ec8 100644 --- a/Objects/memoryobject.c +++ b/Objects/memoryobject.c @@ -2349,7 +2349,13 @@ memoryview_hex_impl(PyMemoryViewObject *self, PyObject *sep, CHECK_RELEASED(self); if (MV_C_CONTIGUOUS(self->flags)) { - return _Py_strhex_with_sep(src->buf, src->len, sep, bytes_per_sep); + // Prevent 'self' from being freed if computing len(sep) mutates 'self' + // in _Py_strhex_with_sep(). + // See: https://github.com/python/cpython/issues/143195. + self->exports++; + PyObject *ret = _Py_strhex_with_sep(src->buf, src->len, sep, bytes_per_sep); + self->exports--; + return ret; } PyBytesWriter *writer = PyBytesWriter_Create(src->len); From 7726119651342bba224e5cc5869859ba7c0416e9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C3=A9n=C3=A9dikt=20Tran?= <10796600+picnixz@users.noreply.github.com> Date: Sat, 27 Dec 2025 12:57:03 +0000 Subject: [PATCH 2/6] gh-138122: fix AC warnings in `Modules/_remote_debugging/module.c` (#143218) --- Modules/_remote_debugging/module.c | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/Modules/_remote_debugging/module.c b/Modules/_remote_debugging/module.c index 737787a331f948..26ebed13098f0e 100644 --- a/Modules/_remote_debugging/module.c +++ b/Modules/_remote_debugging/module.c @@ -947,6 +947,7 @@ _remote_debugging_RemoteUnwinder_get_stats_impl(RemoteUnwinderObject *self) } /*[clinic input] +@permit_long_docstring_body @critical_section _remote_debugging.RemoteUnwinder.pause_threads @@ -963,7 +964,7 @@ Returns True if threads were successfully paused, False if they were already pau static PyObject * _remote_debugging_RemoteUnwinder_pause_threads_impl(RemoteUnwinderObject *self) -/*[clinic end generated code: output=aaf2bdc0a725750c input=78601c60dbc245fe]*/ +/*[clinic end generated code: output=aaf2bdc0a725750c input=d8a266f19a81c67e]*/ { #ifdef Py_REMOTE_DEBUG_SUPPORTS_BLOCKING if (self->threads_stopped) { @@ -985,6 +986,7 @@ _remote_debugging_RemoteUnwinder_pause_threads_impl(RemoteUnwinderObject *self) } /*[clinic input] +@permit_long_docstring_body @critical_section _remote_debugging.RemoteUnwinder.resume_threads @@ -997,7 +999,7 @@ Returns True if threads were successfully resumed, False if they were not paused static PyObject * _remote_debugging_RemoteUnwinder_resume_threads_impl(RemoteUnwinderObject *self) -/*[clinic end generated code: output=8d6781ea37095536 input=67ca813bd804289e]*/ +/*[clinic end generated code: output=8d6781ea37095536 input=16baaaab007f4259]*/ { #ifdef Py_REMOTE_DEBUG_SUPPORTS_BLOCKING if (!self->threads_stopped) { @@ -1261,6 +1263,7 @@ class _remote_debugging.BinaryWriter "BinaryWriterObject *" "&PyBinaryWriter_Typ /*[clinic end generated code: output=da39a3ee5e6b4b0d input=e948838b90a2003c]*/ /*[clinic input] +@permit_long_docstring_body _remote_debugging.BinaryWriter.__init__ filename: str sample_interval_us: unsigned_long_long @@ -1285,7 +1288,7 @@ _remote_debugging_BinaryWriter___init___impl(BinaryWriterObject *self, unsigned long long sample_interval_us, unsigned long long start_time_us, int compression) -/*[clinic end generated code: output=014c0306f1bacf4b input=57497fe3cb9214a6]*/ +/*[clinic end generated code: output=014c0306f1bacf4b input=3bdf01c1cc2f5a1d]*/ { if (self->writer) { binary_writer_destroy(self->writer); @@ -1300,6 +1303,7 @@ _remote_debugging_BinaryWriter___init___impl(BinaryWriterObject *self, } /*[clinic input] +@permit_long_docstring_body _remote_debugging.BinaryWriter.write_sample stack_frames: object timestamp_us: unsigned_long_long @@ -1315,7 +1319,7 @@ static PyObject * _remote_debugging_BinaryWriter_write_sample_impl(BinaryWriterObject *self, PyObject *stack_frames, unsigned long long timestamp_us) -/*[clinic end generated code: output=24d5b86679b4128f input=dce3148417482624]*/ +/*[clinic end generated code: output=24d5b86679b4128f input=4e6d832d360bea46]*/ { if (!self->writer) { PyErr_SetString(PyExc_ValueError, "Writer is closed"); @@ -1422,6 +1426,7 @@ _remote_debugging_BinaryWriter___exit___impl(BinaryWriterObject *self, } /*[clinic input] +@permit_long_docstring_body _remote_debugging.BinaryWriter.get_stats Get encoding statistics for the writer. @@ -1432,7 +1437,7 @@ record counts, frames written/saved, and compression ratio. static PyObject * _remote_debugging_BinaryWriter_get_stats_impl(BinaryWriterObject *self) -/*[clinic end generated code: output=06522cd52544df89 input=82968491b53ad277]*/ +/*[clinic end generated code: output=06522cd52544df89 input=837c874ffdebd24c]*/ { if (!self->writer) { PyErr_SetString(PyExc_ValueError, "Writer is closed"); @@ -1754,6 +1759,7 @@ _remote_debugging_zstd_available_impl(PyObject *module) * ============================================================================ */ /*[clinic input] +@permit_long_docstring_body _remote_debugging.get_child_pids pid: int @@ -1779,7 +1785,7 @@ Child processes may exit or new ones may be created after the list is returned. static PyObject * _remote_debugging_get_child_pids_impl(PyObject *module, int pid, int recursive) -/*[clinic end generated code: output=1ae2289c6b953e4b input=3395cbe7f17066c9]*/ +/*[clinic end generated code: output=1ae2289c6b953e4b input=19d8d5d6e2b59e6e]*/ { return enumerate_child_pids((pid_t)pid, recursive); } From 00e24b80e092e7d36dc189fd260b2a4e730a6e7f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C3=A9n=C3=A9dikt=20Tran?= <10796600+picnixz@users.noreply.github.com> Date: Sat, 27 Dec 2025 13:12:03 +0000 Subject: [PATCH 3/6] gh-142664: fix UAF in `memoryview.__hash__` via re-entrant data's `__hash__` (#143217) --- Lib/test/test_memoryview.py | 14 ++++++++++++++ .../2025-12-27-13-18-12.gh-issue-142664.peeEDV.rst | 3 +++ Objects/memoryobject.c | 13 ++++++++++--- 3 files changed, 27 insertions(+), 3 deletions(-) create mode 100644 Misc/NEWS.d/next/Core_and_Builtins/2025-12-27-13-18-12.gh-issue-142664.peeEDV.rst diff --git a/Lib/test/test_memoryview.py b/Lib/test/test_memoryview.py index 51b107103f6836..656318668e6d6e 100644 --- a/Lib/test/test_memoryview.py +++ b/Lib/test/test_memoryview.py @@ -387,6 +387,20 @@ def test_hash_writable(self): m = self._view(b) self.assertRaises(ValueError, hash, m) + def test_hash_use_after_free(self): + # Prevent crash in memoryview(v).__hash__ with re-entrant v.__hash__. + # Regression test for https://github.com/python/cpython/issues/142664. + class E(array.array): + def __hash__(self): + mv.release() + self.clear() + return 123 + + v = E('B', b'A' * 4096) + mv = memoryview(v).toreadonly() # must be read-only for hash() + self.assertRaises(BufferError, hash, mv) + self.assertRaises(BufferError, mv.__hash__) + def test_weakref(self): # Check memoryviews are weakrefable for tp in self._types: diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2025-12-27-13-18-12.gh-issue-142664.peeEDV.rst b/Misc/NEWS.d/next/Core_and_Builtins/2025-12-27-13-18-12.gh-issue-142664.peeEDV.rst new file mode 100644 index 00000000000000..39c218395cc4d3 --- /dev/null +++ b/Misc/NEWS.d/next/Core_and_Builtins/2025-12-27-13-18-12.gh-issue-142664.peeEDV.rst @@ -0,0 +1,3 @@ +Fix a use-after-free crash in :meth:`memoryview.__hash__ ` +when the ``__hash__`` method of the referenced object mutates that object or +the view. Patch by Bénédikt Tran. diff --git a/Objects/memoryobject.c b/Objects/memoryobject.c index 2fd1d784b92ec8..f50de3e4c64071 100644 --- a/Objects/memoryobject.c +++ b/Objects/memoryobject.c @@ -3231,9 +3231,16 @@ memory_hash(PyObject *_self) "memoryview: hashing is restricted to formats 'B', 'b' or 'c'"); return -1; } - if (view->obj != NULL && PyObject_Hash(view->obj) == -1) { - /* Keep the original error message */ - return -1; + if (view->obj != NULL) { + // Prevent 'self' from being freed when computing the item's hash. + // See https://github.com/python/cpython/issues/142664. + self->exports++; + int rc = PyObject_Hash(view->obj); + self->exports--; + if (rc == -1) { + /* Keep the original error message */ + return -1; + } } if (!MV_C_CONTIGUOUS(self->flags)) { From 3a728e5f93c1e4c125406eeeb76d5df1c1726409 Mon Sep 17 00:00:00 2001 From: Pablo Galindo Salgado Date: Sat, 27 Dec 2025 13:38:11 +0000 Subject: [PATCH 4/6] gh-131591: Do not free page caches that weren't allocated (#143205) --- Python/remote_debug.h | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Python/remote_debug.h b/Python/remote_debug.h index d9c5c480fe9a86..dba6da3bad4197 100644 --- a/Python/remote_debug.h +++ b/Python/remote_debug.h @@ -154,7 +154,9 @@ static void _Py_RemoteDebug_FreePageCache(proc_handle_t *handle) { for (int i = 0; i < MAX_PAGES; i++) { - PyMem_RawFree(handle->pages[i].data); + if (handle->pages[i].data) { + PyMem_RawFree(handle->pages[i].data); + } handle->pages[i].data = NULL; handle->pages[i].valid = 0; } From 84fcdbd86ecd81f7cc793e22268a029ac6cf29c2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C3=A9n=C3=A9dikt=20Tran?= <10796600+picnixz@users.noreply.github.com> Date: Sat, 27 Dec 2025 14:30:09 +0000 Subject: [PATCH 5/6] gh-142664: fix `PyObject_Hash` invokation post GH-143217 (#143223) --- Objects/memoryobject.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Objects/memoryobject.c b/Objects/memoryobject.c index f50de3e4c64071..f3b7e4a396b4a1 100644 --- a/Objects/memoryobject.c +++ b/Objects/memoryobject.c @@ -3235,9 +3235,9 @@ memory_hash(PyObject *_self) // Prevent 'self' from being freed when computing the item's hash. // See https://github.com/python/cpython/issues/142664. self->exports++; - int rc = PyObject_Hash(view->obj); + Py_hash_t h = PyObject_Hash(view->obj); self->exports--; - if (rc == -1) { + if (h == -1) { /* Keep the original error message */ return -1; } From 61ee04834b096be00678c6819b4957f3f4413a5e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C3=A9n=C3=A9dikt=20Tran?= <10796600+picnixz@users.noreply.github.com> Date: Sat, 27 Dec 2025 14:57:13 +0000 Subject: [PATCH 6/6] gh-142557: fix UAF in `bytearray.__mod__` when object is mutated while formatting `%`-style arguments (#143213) --- Lib/test/test_bytes.py | 12 ++++++++++++ .../2025-12-27-12-25-06.gh-issue-142557.KWOc8b.rst | 3 +++ Objects/bytearrayobject.c | 10 +++++++++- 3 files changed, 24 insertions(+), 1 deletion(-) create mode 100644 Misc/NEWS.d/next/Core_and_Builtins/2025-12-27-12-25-06.gh-issue-142557.KWOc8b.rst diff --git a/Lib/test/test_bytes.py b/Lib/test/test_bytes.py index 44b16c7d91e996..e0baeece34c7b3 100644 --- a/Lib/test/test_bytes.py +++ b/Lib/test/test_bytes.py @@ -1382,6 +1382,18 @@ def test_bytearray_api(self): except OSError: pass + def test_mod_concurrent_mutation(self): + # Prevent crash in __mod__ when formatting mutates the bytearray. + # Regression test for https://github.com/python/cpython/issues/142557. + fmt = bytearray(b"%a end") + + class S: + def __repr__(self): + fmt.clear() + return "E" + + self.assertRaises(BufferError, fmt.__mod__, S()) + def test_reverse(self): b = bytearray(b'hello') self.assertEqual(b.reverse(), None) diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2025-12-27-12-25-06.gh-issue-142557.KWOc8b.rst b/Misc/NEWS.d/next/Core_and_Builtins/2025-12-27-12-25-06.gh-issue-142557.KWOc8b.rst new file mode 100644 index 00000000000000..b7f7a585906c34 --- /dev/null +++ b/Misc/NEWS.d/next/Core_and_Builtins/2025-12-27-12-25-06.gh-issue-142557.KWOc8b.rst @@ -0,0 +1,3 @@ +Fix a use-after-free crash in :ref:`bytearray.__mod__ ` when +the :class:`!bytearray` is mutated while formatting the ``%``-style arguments. +Patch by Bénédikt Tran. diff --git a/Objects/bytearrayobject.c b/Objects/bytearrayobject.c index 8a454aa48a0930..5262ac20c07300 100644 --- a/Objects/bytearrayobject.c +++ b/Objects/bytearrayobject.c @@ -2843,7 +2843,15 @@ bytearray_mod_lock_held(PyObject *v, PyObject *w) _Py_CRITICAL_SECTION_ASSERT_OBJECT_LOCKED(v); if (!PyByteArray_Check(v)) Py_RETURN_NOTIMPLEMENTED; - return _PyBytes_FormatEx(PyByteArray_AS_STRING(v), PyByteArray_GET_SIZE(v), w, 1); + + PyByteArrayObject *self = _PyByteArray_CAST(v); + /* Increase exports to prevent bytearray storage from changing during op. */ + self->ob_exports++; + PyObject *res = _PyBytes_FormatEx( + PyByteArray_AS_STRING(v), PyByteArray_GET_SIZE(v), w, 1 + ); + self->ob_exports--; + return res; } static PyObject *