https://github.com/python/cpython/commit/ded533b1fa8d038ee013caa657ca76b12d50a9a6
commit: ded533b1fa8d038ee013caa657ca76b12d50a9a6
branch: 3.14
author: Pablo Galindo Salgado <[email protected]>
committer: pablogsal <[email protected]>
date: 2026-02-26T22:39:48Z
summary:

[3.14] gh-144316: Fix missing exception in _remote_debugging with debug=False 
(GH-144442) (#145280)

files:
A Misc/NEWS.d/next/Library/2026-02-03-19-57-41.gh-issue-144316.wop870.rst
M Modules/_remote_debugging_module.c
M Python/remote_debug.h

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_module.c 
b/Modules/_remote_debugging_module.c
index dcf901bf1fec99..a26e6820f558f6 100644
--- a/Modules/_remote_debugging_module.c
+++ b/Modules/_remote_debugging_module.c
@@ -77,6 +77,8 @@
                                   offsetof(PyInterpreterState, 
_gil.last_holder) + sizeof(PyThreadState*))
 #endif
 #define INTERP_STATE_BUFFER_SIZE MAX(INTERP_STATE_MIN_SIZE, 256)
+#define MAX_STACK_CHUNK_SIZE (16 * 1024 * 1024)  /* 16 MB max for stack chunks 
*/
+#define MAX_SET_TABLE_SIZE (1 << 20)  /* 1 million entries max for set 
iteration */
 
 
 
@@ -318,10 +320,13 @@ static int append_awaited_by(RemoteUnwinderObject 
*unwinder, unsigned long tid,
  * UTILITY FUNCTIONS AND HELPERS
  * 
============================================================================ */
 
-#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)
 
 static void
 cached_code_metadata_destroy(void *ptr)
@@ -498,8 +503,15 @@ iterate_set_entries(
     }
 
     Py_ssize_t num_els = GET_MEMBER(Py_ssize_t, set_object, 
unwinder->debug_offsets.set_object.used);
-    Py_ssize_t set_len = GET_MEMBER(Py_ssize_t, set_object, 
unwinder->debug_offsets.set_object.mask) + 1;
+    Py_ssize_t mask = GET_MEMBER(Py_ssize_t, set_object, 
unwinder->debug_offsets.set_object.mask);
     uintptr_t table_ptr = GET_MEMBER(uintptr_t, set_object, 
unwinder->debug_offsets.set_object.table);
+    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;
+    }
+    Py_ssize_t set_len = mask + 1;
 
     Py_ssize_t i = 0;
     Py_ssize_t els = 0;
@@ -1825,7 +1837,15 @@ parse_code_object(RemoteUnwinderObject *unwinder,
         tlbc_entry = get_tlbc_cache_entry(unwinder, real_address, 
unwinder->tlbc_generation);
     }
 
-    if (tlbc_entry && tlbc_index < tlbc_entry->tlbc_array_size) {
+    if (tlbc_entry) {
+        if (tlbc_index < 0 || tlbc_index >= tlbc_entry->tlbc_array_size) {
+            PyErr_Format(PyExc_RuntimeError,
+                "Invalid tlbc_index %d (array size %zd, corrupted remote 
memory)",
+                tlbc_index, tlbc_entry->tlbc_array_size);
+            set_exception_cause(unwinder, PyExc_RuntimeError,
+                "Invalid tlbc_index (corrupted remote memory)");
+            goto error;
+        }
         // Use cached TLBC data
         uintptr_t *entries = (uintptr_t *)((char *)tlbc_entry->tlbc_array + 
sizeof(Py_ssize_t));
         uintptr_t tlbc_bytecode_addr = entries[tlbc_index];
@@ -1924,6 +1944,15 @@ process_single_stack_chunk(
     // Check actual size and reread if necessary
     size_t actual_size = GET_MEMBER(size_t, this_chunk, 
offsetof(_PyStackChunk, size));
     if (actual_size != current_size) {
+        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;
+        }
+
         this_chunk = PyMem_RawRealloc(this_chunk, actual_size);
         if (!this_chunk) {
             PyErr_NoMemory();
@@ -2027,6 +2056,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;
     }
@@ -2736,6 +2766,7 @@ 
_remote_debugging_RemoteUnwinder_get_stack_trace_impl(RemoteUnwinderObject *self
     }
 
     while (current_tstate != 0) {
+        uintptr_t prev_tstate = current_tstate;
         PyObject* frame_info = unwind_stack_for_thread(self, &current_tstate);
         if (!frame_info) {
             Py_CLEAR(result);
@@ -2751,6 +2782,16 @@ 
_remote_debugging_RemoteUnwinder_get_stack_trace_impl(RemoteUnwinderObject *self
         }
         Py_DECREF(frame_info);
 
+        if (current_tstate == prev_tstate) {
+            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);
+            goto exit;
+        }
+
         // We are targeting a single tstate, break here
         if (self->tstate_addr) {
             break;
diff --git a/Python/remote_debug.h b/Python/remote_debug.h
index 21c11789189118..df1225f2eda101 100644
--- a/Python/remote_debug.h
+++ b/Python/remote_debug.h
@@ -1134,6 +1134,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",
@@ -1143,7 +1144,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;
             }

_______________________________________________
Python-checkins mailing list -- [email protected]
To unsubscribe send an email to [email protected]
https://mail.python.org/mailman3//lists/python-checkins.python.org
Member address: [email protected]

Reply via email to