From 0f2d244671e1e1dbf88cf17284e9ba76d6babf77 Mon Sep 17 00:00:00 2001 From: Taegyun Kim Date: Tue, 3 Feb 2026 14:33:20 -0500 Subject: [PATCH 1/9] gh-144316: Fix missing exception in _remote_debugging with debug=False --- Modules/_remote_debugging/_remote_debugging.h | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/Modules/_remote_debugging/_remote_debugging.h b/Modules/_remote_debugging/_remote_debugging.h index 78add74423b608..bfbd371ac1f1a7 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" @@ -174,9 +175,15 @@ typedef enum _WIN32_THREADSTATE { /* Exception cause macro */ #define set_exception_cause(unwinder, exc_type, message) \ - if (unwinder->debug) { \ - _set_debug_exception_cause(exc_type, message); \ - } + do { \ + if (!PyErr_ExceptionMatches(PyExc_PermissionError)) { \ + if (!PyErr_Occurred()) { \ + PyErr_SetString(exc_type, message); \ + } else if (unwinder->debug) { \ + _PyErr_FormatFromCause(exc_type, "%s", message); \ + } \ + } \ + } while (0) /* ============================================================================ * TYPE DEFINITIONS From 66b060d9629e00e3a4bbe4dd22c961678b4ce64e Mon Sep 17 00:00:00 2001 From: "blurb-it[bot]" <43283697+blurb-it[bot]@users.noreply.github.com> Date: Tue, 3 Feb 2026 19:57:43 +0000 Subject: [PATCH 2/9] =?UTF-8?q?=F0=9F=93=9C=F0=9F=A4=96=20Added=20by=20blu?= =?UTF-8?q?rb=5Fit.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../next/Library/2026-02-03-19-57-41.gh-issue-144316.wop870.rst | 1 + 1 file changed, 1 insertion(+) 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..0c3049243a19d7 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-02-03-19-57-41.gh-issue-144316.wop870.rst @@ -0,0 +1 @@ +Fix missing exception handling in :mod:`_remote_debugging` when ``debug=False``. The ``set_exception_cause`` macro now always sets an exception to satisfy the C API contract, fixing intermittent assertion failures. Exception chaining with ``debug=True`` is preserved. Patch by Taegyun Kim. From 3c72749f413c989e36cc2285bb93be006e604b12 Mon Sep 17 00:00:00 2001 From: Taegyun Kim Date: Tue, 3 Feb 2026 15:11:24 -0500 Subject: [PATCH 3/9] Vertically algin continuation character (\) per PEP-7 --- Modules/_remote_debugging/_remote_debugging.h | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/Modules/_remote_debugging/_remote_debugging.h b/Modules/_remote_debugging/_remote_debugging.h index bfbd371ac1f1a7..897cb11185bc95 100644 --- a/Modules/_remote_debugging/_remote_debugging.h +++ b/Modules/_remote_debugging/_remote_debugging.h @@ -174,15 +174,15 @@ typedef enum _WIN32_THREADSTATE { #define THREAD_STATUS_HAS_EXCEPTION (1 << 4) /* Exception cause macro */ -#define set_exception_cause(unwinder, exc_type, message) \ - do { \ - if (!PyErr_ExceptionMatches(PyExc_PermissionError)) { \ - if (!PyErr_Occurred()) { \ - PyErr_SetString(exc_type, message); \ - } else if (unwinder->debug) { \ - _PyErr_FormatFromCause(exc_type, "%s", message); \ - } \ - } \ +#define set_exception_cause(unwinder, exc_type, message) \ + do { \ + if (!PyErr_ExceptionMatches(PyExc_PermissionError)) { \ + if (!PyErr_Occurred()) { \ + PyErr_SetString(exc_type, message); \ + } else if (unwinder->debug) { \ + _PyErr_FormatFromCause(exc_type, "%s", message); \ + } \ + } \ } while (0) /* ============================================================================ From c42b64c5cb21a2842d925c76abdabfbdf124099a Mon Sep 17 00:00:00 2001 From: Taegyun Kim Date: Tue, 3 Feb 2026 15:12:49 -0500 Subject: [PATCH 4/9] Shorten news entry as suggested --- .../next/Library/2026-02-03-19-57-41.gh-issue-144316.wop870.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 index 0c3049243a19d7..b9d0749f56ba6a 100644 --- 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 @@ -1 +1 @@ -Fix missing exception handling in :mod:`_remote_debugging` when ``debug=False``. The ``set_exception_cause`` macro now always sets an exception to satisfy the C API contract, fixing intermittent assertion failures. Exception chaining with ``debug=True`` is preserved. Patch by Taegyun Kim. +Fix crash in ``_remote_debugging`` that caused ``test_external_inspection`` to intermittently fail. Patch by Taegyun Kim. From f2ad0ef113c0ef722bb1d93c98308493ebfb3f16 Mon Sep 17 00:00:00 2001 From: Taegyun Kim Date: Wed, 4 Feb 2026 12:26:06 -0500 Subject: [PATCH 5/9] Call PyErr_NoMemory() on when PyMem_RawMalloc() returns NULL; typo ammount->amount --- Python/remote_debug.h | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Python/remote_debug.h b/Python/remote_debug.h index dba6da3bad4197..6c45a7e455d5bb 100644 --- a/Python/remote_debug.h +++ b/Python/remote_debug.h @@ -1263,6 +1263,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", @@ -1272,7 +1273,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 c0a803728179c776bb85b37adb70446014458e67 Mon Sep 17 00:00:00 2001 From: Taegyun Kim Date: Wed, 4 Feb 2026 12:32:48 -0500 Subject: [PATCH 6/9] Update macro to crash in debug mode when error is not set --- Modules/_remote_debugging/_remote_debugging.h | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/Modules/_remote_debugging/_remote_debugging.h b/Modules/_remote_debugging/_remote_debugging.h index 897cb11185bc95..8d16aa5431c8c2 100644 --- a/Modules/_remote_debugging/_remote_debugging.h +++ b/Modules/_remote_debugging/_remote_debugging.h @@ -173,14 +173,22 @@ typedef enum _WIN32_THREADSTATE { #define THREAD_STATUS_GIL_REQUESTED (1 << 3) #define THREAD_STATUS_HAS_EXCEPTION (1 << 4) -/* Exception cause macro */ +/* Exception cause macro - chains context to existing exceptions in debug mode. + * This macro assumes an exception has already been set by the failing function. + * If no exception exists, this indicates a bug that should be fixed. */ #define set_exception_cause(unwinder, exc_type, message) \ do { \ if (!PyErr_ExceptionMatches(PyExc_PermissionError)) { \ - if (!PyErr_Occurred()) { \ + if (PyErr_Occurred()) { \ + if (unwinder->debug) { \ + /* Chain exception with context */ \ + _PyErr_FormatFromCause(exc_type, "%s", message); \ + } \ + } else { \ + /* BUG: Exception should have been set by caller */ \ + /* Fallback prevents crash; assert catches bug in debug builds */ \ PyErr_SetString(exc_type, message); \ - } else if (unwinder->debug) { \ - _PyErr_FormatFromCause(exc_type, "%s", message); \ + assert(PyErr_Occurred() && "function returned -1 without setting exception"); \ } \ } \ } while (0) From 173cde81fdf78f3bfe57d5596b2983b29202ee72 Mon Sep 17 00:00:00 2001 From: Taegyun Kim Date: Wed, 4 Feb 2026 12:35:18 -0500 Subject: [PATCH 7/9] Adjust vertical bars per PEP-7 --- Modules/_remote_debugging/_remote_debugging.h | 28 +++++++++---------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/Modules/_remote_debugging/_remote_debugging.h b/Modules/_remote_debugging/_remote_debugging.h index 8d16aa5431c8c2..73b04b28a19ffc 100644 --- a/Modules/_remote_debugging/_remote_debugging.h +++ b/Modules/_remote_debugging/_remote_debugging.h @@ -176,21 +176,21 @@ typedef enum _WIN32_THREADSTATE { /* Exception cause macro - chains context to existing exceptions in debug mode. * This macro assumes an exception has already been set by the failing function. * If no exception exists, this indicates a bug that should be fixed. */ -#define set_exception_cause(unwinder, exc_type, message) \ - do { \ - if (!PyErr_ExceptionMatches(PyExc_PermissionError)) { \ - if (PyErr_Occurred()) { \ - if (unwinder->debug) { \ - /* Chain exception with context */ \ - _PyErr_FormatFromCause(exc_type, "%s", message); \ - } \ - } else { \ - /* BUG: Exception should have been set by caller */ \ - /* Fallback prevents crash; assert catches bug in debug builds */ \ - PyErr_SetString(exc_type, message); \ +#define set_exception_cause(unwinder, exc_type, message) \ + do { \ + if (!PyErr_ExceptionMatches(PyExc_PermissionError)) { \ + if (PyErr_Occurred()) { \ + if (unwinder->debug) { \ + /* Chain exception with context */ \ + _PyErr_FormatFromCause(exc_type, "%s", message); \ + } \ + } else { \ + /* BUG: Exception should have been set by caller */ \ + /* Fallback prevents crash; assert catches bug in debug builds */ \ + PyErr_SetString(exc_type, message); \ assert(PyErr_Occurred() && "function returned -1 without setting exception"); \ - } \ - } \ + } \ + } \ } while (0) /* ============================================================================ From 29774d71dc576eb9c3661e132d5482e3e5643d55 Mon Sep 17 00:00:00 2001 From: Taegyun Kim Date: Wed, 4 Feb 2026 15:18:35 -0500 Subject: [PATCH 8/9] Assert before calling _set_debug_exception_cause --- Modules/_remote_debugging/_remote_debugging.h | 25 ++++++------------- 1 file changed, 7 insertions(+), 18 deletions(-) diff --git a/Modules/_remote_debugging/_remote_debugging.h b/Modules/_remote_debugging/_remote_debugging.h index 73b04b28a19ffc..7bcb2f483234ec 100644 --- a/Modules/_remote_debugging/_remote_debugging.h +++ b/Modules/_remote_debugging/_remote_debugging.h @@ -173,24 +173,13 @@ typedef enum _WIN32_THREADSTATE { #define THREAD_STATUS_GIL_REQUESTED (1 << 3) #define THREAD_STATUS_HAS_EXCEPTION (1 << 4) -/* Exception cause macro - chains context to existing exceptions in debug mode. - * This macro assumes an exception has already been set by the failing function. - * If no exception exists, this indicates a bug that should be fixed. */ -#define set_exception_cause(unwinder, exc_type, message) \ - do { \ - if (!PyErr_ExceptionMatches(PyExc_PermissionError)) { \ - if (PyErr_Occurred()) { \ - if (unwinder->debug) { \ - /* Chain exception with context */ \ - _PyErr_FormatFromCause(exc_type, "%s", message); \ - } \ - } else { \ - /* BUG: Exception should have been set by caller */ \ - /* Fallback prevents crash; assert catches bug in debug builds */ \ - PyErr_SetString(exc_type, message); \ - assert(PyErr_Occurred() && "function returned -1 without setting exception"); \ - } \ - } \ +/* Exception cause macro */ +#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) /* ============================================================================ From 39b6abe0e9880acde3ef2f3f8aaa82dbf03fbadc Mon Sep 17 00:00:00 2001 From: Taegyun Kim Date: Wed, 4 Feb 2026 15:57:20 -0500 Subject: [PATCH 9/9] Make sure to call PyErr before calling set_exception_cause --- 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 +++ 4 files changed, 10 insertions(+) diff --git a/Modules/_remote_debugging/asyncio.c b/Modules/_remote_debugging/asyncio.c index 3fcc939fd0e876..5385b42f1713ec 100644 --- a/Modules/_remote_debugging/asyncio.c +++ b/Modules/_remote_debugging/asyncio.c @@ -117,6 +117,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);