From c2d3d6b0dd87aaea2f3a72419e1d1488d3ab9503 Mon Sep 17 00:00:00 2001 From: Taegyun Kim Date: Thu, 26 Feb 2026 22:14:34 +0100 Subject: [PATCH 1/3] gh-144316: Fix missing exception in _remote_debugging with debug=False (#144442) --- .../2026-02-03-19-57-41.gh-issue-144316.wop870.rst | 1 + Modules/_remote_debugging/_remote_debugging.h | 12 ++++++++---- Modules/_remote_debugging/asyncio.c | 1 + Modules/_remote_debugging/code_objects.c | 3 +++ Modules/_remote_debugging/frames.c | 3 +++ Modules/_remote_debugging/module.c | 3 +++ Python/remote_debug.h | 3 ++- 7 files changed, 21 insertions(+), 5 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2026-02-03-19-57-41.gh-issue-144316.wop870.rst diff --git a/Misc/NEWS.d/next/Library/2026-02-03-19-57-41.gh-issue-144316.wop870.rst b/Misc/NEWS.d/next/Library/2026-02-03-19-57-41.gh-issue-144316.wop870.rst new file mode 100644 index 00000000000000..b9d0749f56ba6a --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-02-03-19-57-41.gh-issue-144316.wop870.rst @@ -0,0 +1 @@ +Fix crash in ``_remote_debugging`` that caused ``test_external_inspection`` to intermittently fail. Patch by Taegyun Kim. diff --git a/Modules/_remote_debugging/_remote_debugging.h b/Modules/_remote_debugging/_remote_debugging.h index 78add74423b608..7bcb2f483234ec 100644 --- a/Modules/_remote_debugging/_remote_debugging.h +++ b/Modules/_remote_debugging/_remote_debugging.h @@ -29,6 +29,7 @@ extern "C" { #include "internal/pycore_interpframe.h" // FRAME_OWNED_BY_INTERPRETER #include "internal/pycore_llist.h" // struct llist_node #include "internal/pycore_long.h" // _PyLong_GetZero +#include "internal/pycore_pyerrors.h" // _PyErr_FormatFromCause #include "internal/pycore_stackref.h" // Py_TAG_BITS #include "../../Python/remote_debug.h" @@ -173,10 +174,13 @@ typedef enum _WIN32_THREADSTATE { #define THREAD_STATUS_HAS_EXCEPTION (1 << 4) /* Exception cause macro */ -#define set_exception_cause(unwinder, exc_type, message) \ - if (unwinder->debug) { \ - _set_debug_exception_cause(exc_type, message); \ - } +#define set_exception_cause(unwinder, exc_type, message) \ + do { \ + assert(PyErr_Occurred() && "function returned -1 without setting exception"); \ + if (unwinder->debug) { \ + _set_debug_exception_cause(exc_type, message); \ + } \ + } while (0) /* ============================================================================ * TYPE DEFINITIONS diff --git a/Modules/_remote_debugging/asyncio.c b/Modules/_remote_debugging/asyncio.c index fc059659511fd8..69478634de6926 100644 --- a/Modules/_remote_debugging/asyncio.c +++ b/Modules/_remote_debugging/asyncio.c @@ -121,6 +121,7 @@ iterate_set_entries( // Validate mask and num_els to prevent huge loop iterations from garbage data if (mask < 0 || mask >= MAX_SET_TABLE_SIZE || num_els < 0 || num_els > mask + 1) { + PyErr_SetString(PyExc_RuntimeError, "Invalid set object (corrupted remote memory)"); set_exception_cause(unwinder, PyExc_RuntimeError, "Invalid set object (corrupted remote memory)"); return -1; diff --git a/Modules/_remote_debugging/code_objects.c b/Modules/_remote_debugging/code_objects.c index 9b7b4dc22b873b..91f7a02005391a 100644 --- a/Modules/_remote_debugging/code_objects.c +++ b/Modules/_remote_debugging/code_objects.c @@ -446,6 +446,9 @@ parse_code_object(RemoteUnwinderObject *unwinder, if (tlbc_entry) { // Validate index bounds (also catches negative values since tlbc_index is signed) if (ctx->tlbc_index < 0 || ctx->tlbc_index >= tlbc_entry->tlbc_array_size) { + PyErr_Format(PyExc_RuntimeError, + "Invalid tlbc_index %d (array size %zd, corrupted remote memory)", + ctx->tlbc_index, tlbc_entry->tlbc_array_size); set_exception_cause(unwinder, PyExc_RuntimeError, "Invalid tlbc_index (corrupted remote memory)"); goto error; diff --git a/Modules/_remote_debugging/frames.c b/Modules/_remote_debugging/frames.c index 02c48205b85a37..2ace0c0f7676ae 100644 --- a/Modules/_remote_debugging/frames.c +++ b/Modules/_remote_debugging/frames.c @@ -49,6 +49,8 @@ process_single_stack_chunk( // Size must be at least enough for the header and reasonably bounded if (actual_size <= offsetof(_PyStackChunk, data) || actual_size > MAX_STACK_CHUNK_SIZE) { PyMem_RawFree(this_chunk); + PyErr_Format(PyExc_RuntimeError, + "Invalid stack chunk size %zu (corrupted remote memory)", actual_size); set_exception_cause(unwinder, PyExc_RuntimeError, "Invalid stack chunk size (corrupted remote memory)"); return -1; @@ -244,6 +246,7 @@ parse_frame_from_chunks( ) { void *frame_ptr = find_frame_in_chunks(chunks, address); if (!frame_ptr) { + PyErr_Format(PyExc_RuntimeError, "Frame at address 0x%lx not found in stack chunks", address); set_exception_cause(unwinder, PyExc_RuntimeError, "Frame not found in stack chunks"); return -1; } diff --git a/Modules/_remote_debugging/module.c b/Modules/_remote_debugging/module.c index 26ebed13098f0e..040bd3db377315 100644 --- a/Modules/_remote_debugging/module.c +++ b/Modules/_remote_debugging/module.c @@ -595,6 +595,9 @@ _remote_debugging_RemoteUnwinder_get_stack_trace_impl(RemoteUnwinderObject *self // Detect cycle: if current_tstate didn't advance, we have corrupted data if (current_tstate == prev_tstate) { Py_DECREF(interpreter_threads); + PyErr_Format(PyExc_RuntimeError, + "Thread list cycle detected at address 0x%lx (corrupted remote memory)", + current_tstate); set_exception_cause(self, PyExc_RuntimeError, "Thread list cycle detected (corrupted remote memory)"); Py_CLEAR(result); diff --git a/Python/remote_debug.h b/Python/remote_debug.h index 4ae1166e885485..7628fb04ba5bae 100644 --- a/Python/remote_debug.h +++ b/Python/remote_debug.h @@ -1302,6 +1302,7 @@ _Py_RemoteDebug_PagedReadRemoteMemory(proc_handle_t *handle, if (entry->data == NULL) { entry->data = PyMem_RawMalloc(page_size); if (entry->data == NULL) { + PyErr_NoMemory(); _set_debug_exception_cause(PyExc_MemoryError, "Cannot allocate %zu bytes for page cache entry " "during read from PID %d at address 0x%lx", @@ -1311,7 +1312,7 @@ _Py_RemoteDebug_PagedReadRemoteMemory(proc_handle_t *handle, } if (_Py_RemoteDebug_ReadRemoteMemory(handle, page_base, page_size, entry->data) < 0) { - // Try to just copy the exact ammount as a fallback + // Try to just copy the exact amount as a fallback PyErr_Clear(); goto fallback; } From f3a381e54fcabb2d8649cbfc2dff9933ee7c4d0b Mon Sep 17 00:00:00 2001 From: Sergey B Kirpichev Date: Fri, 27 Feb 2026 01:02:39 +0300 Subject: [PATCH 2/3] gh-141510: support frozendict's in the C decimal module (gh-145165) --- Lib/test/test_decimal.py | 6 ++++++ Modules/_decimal/_decimal.c | 4 ++-- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/Lib/test/test_decimal.py b/Lib/test/test_decimal.py index b520b062ebc685..fe8c8ce12da0bf 100644 --- a/Lib/test/test_decimal.py +++ b/Lib/test/test_decimal.py @@ -3963,15 +3963,21 @@ def test_flag_comparisons(self): d.update(c.flags) self.assertEqual(d, c.flags) self.assertEqual(c.flags, d) + self.assertEqual(frozendict(d), c.flags) + self.assertEqual(c.flags, frozendict(d)) d[Inexact] = True self.assertNotEqual(d, c.flags) self.assertNotEqual(c.flags, d) + self.assertNotEqual(frozendict(d), c.flags) + self.assertNotEqual(c.flags, frozendict(d)) # Invalid SignalDict d = {Inexact:False} self.assertNotEqual(d, c.flags) self.assertNotEqual(c.flags, d) + self.assertNotEqual(frozendict(d), c.flags) + self.assertNotEqual(c.flags, frozendict(d)) d = ["xyz"] self.assertNotEqual(d, c.flags) diff --git a/Modules/_decimal/_decimal.c b/Modules/_decimal/_decimal.c index dcea4da8f24268..c42757e042e7ef 100644 --- a/Modules/_decimal/_decimal.c +++ b/Modules/_decimal/_decimal.c @@ -552,7 +552,7 @@ dict_as_flags(decimal_state *state, PyObject *val) uint32_t flags = 0; int x; - if (!PyDict_Check(val)) { + if (!PyAnyDict_Check(val)) { PyErr_SetString(PyExc_TypeError, "argument must be a signal dict"); return DEC_INVALID_SIGNALS; @@ -802,7 +802,7 @@ signaldict_richcompare(PyObject *v, PyObject *w, int op) if (PyDecSignalDict_Check(state, w)) { res = (SdFlags(v)==SdFlags(w)) ^ (op==Py_NE) ? Py_True : Py_False; } - else if (PyDict_Check(w)) { + else if (PyAnyDict_Check(w)) { uint32_t flags = dict_as_flags(state, w); if (flags & DEC_ERRORS) { if (flags & DEC_INVALID_SIGNALS) { From 3fc945df22a169e039c3f21b44c0d08390a00c0c Mon Sep 17 00:00:00 2001 From: AdamKorcz <44787359+AdamKorcz@users.noreply.github.com> Date: Thu, 26 Feb 2026 22:35:08 +0000 Subject: [PATCH 3/3] gh-144872: fix heap buffer overflow `_PyTokenizer_ensure_utf8` (#144807) --- Lib/test/test_source_encoding.py | 17 +++++++++++++++++ ...26-02-16-12-28-43.gh-issue-144872.k9_Q30.rst | 1 + Parser/tokenizer/helpers.c | 6 ++++-- 3 files changed, 22 insertions(+), 2 deletions(-) create mode 100644 Misc/NEWS.d/next/Core_and_Builtins/2026-02-16-12-28-43.gh-issue-144872.k9_Q30.rst diff --git a/Lib/test/test_source_encoding.py b/Lib/test/test_source_encoding.py index 46b291192df429..8ac64b3105708f 100644 --- a/Lib/test/test_source_encoding.py +++ b/Lib/test/test_source_encoding.py @@ -65,6 +65,23 @@ def test_issue7820(self): # two bytes in common with the UTF-8 BOM self.assertRaises(SyntaxError, eval, b'\xef\xbb\x20') + def test_truncated_utf8_at_eof(self): + # Regression test for https://issues.oss-fuzz.com/issues/451112368 + # Truncated multi-byte UTF-8 sequences at end of input caused an + # out-of-bounds read in Parser/tokenizer/helpers.c:valid_utf8(). + truncated = [ + b'\xc2', # 2-byte lead, missing 1 continuation + b'\xdf', # 2-byte lead, missing 1 continuation + b'\xe0', # 3-byte lead, missing 2 continuations + b'\xe0\xa0', # 3-byte lead, missing 1 continuation + b'\xf0\x90', # 4-byte lead, missing 2 continuations + b'\xf0\x90\x80', # 4-byte lead, missing 1 continuation + b'\xf3', # 4-byte lead, missing 3 (the oss-fuzz reproducer) + ] + for seq in truncated: + with self.subTest(seq=seq): + self.assertRaises(SyntaxError, compile, seq, '', 'exec') + @support.requires_subprocess() def test_20731(self): sub = subprocess.Popen([sys.executable, diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2026-02-16-12-28-43.gh-issue-144872.k9_Q30.rst b/Misc/NEWS.d/next/Core_and_Builtins/2026-02-16-12-28-43.gh-issue-144872.k9_Q30.rst new file mode 100644 index 00000000000000..c06bf01baee6fd --- /dev/null +++ b/Misc/NEWS.d/next/Core_and_Builtins/2026-02-16-12-28-43.gh-issue-144872.k9_Q30.rst @@ -0,0 +1 @@ +Fix heap buffer overflow in the parser found by OSS-Fuzz. diff --git a/Parser/tokenizer/helpers.c b/Parser/tokenizer/helpers.c index fda8216a3005b9..9542969ad3127b 100644 --- a/Parser/tokenizer/helpers.c +++ b/Parser/tokenizer/helpers.c @@ -494,9 +494,11 @@ valid_utf8(const unsigned char* s) return 0; } length = expected + 1; - for (; expected; expected--) - if (s[expected] < 0x80 || s[expected] >= 0xC0) + for (int i = 1; i <= expected; i++) { + if (s[i] < 0x80 || s[i] >= 0xC0) { return 0; + } + } return length; }