diff --git a/Include/internal/pycore_global_objects_fini_generated.h b/Include/internal/pycore_global_objects_fini_generated.h index 6df1c01f151f68e..82d65090d0610d0 100644 --- a/Include/internal/pycore_global_objects_fini_generated.h +++ b/Include/internal/pycore_global_objects_fini_generated.h @@ -1334,6 +1334,9 @@ _PyStaticObjects_CheckRefcnt(PyInterpreterState *interp) { _PyStaticObject_CheckRefcnt((PyObject *)&_Py_STR(native)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_STR(str_replace_inf)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_STR(type_params)); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_STR(unknown_file)); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_STR(unknown_function)); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_STR(unreadable_frame)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_STR(utf_8)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(AGEN_CLOSED)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(AGEN_CREATED)); diff --git a/Include/internal/pycore_global_strings.h b/Include/internal/pycore_global_strings.h index 873344fbdcb67b0..98bb25eecc83eb2 100644 --- a/Include/internal/pycore_global_strings.h +++ b/Include/internal/pycore_global_strings.h @@ -54,6 +54,9 @@ struct _Py_global_strings { STRUCT_FOR_STR(native, "") STRUCT_FOR_STR(str_replace_inf, "1e309") STRUCT_FOR_STR(type_params, ".type_params") + STRUCT_FOR_STR(unknown_file, "") + STRUCT_FOR_STR(unknown_function, "") + STRUCT_FOR_STR(unreadable_frame, "") STRUCT_FOR_STR(utf_8, "utf-8") } literals; diff --git a/Include/internal/pycore_runtime_init_generated.h b/Include/internal/pycore_runtime_init_generated.h index 378f27ca17b5079..8e96a19f592680e 100644 --- a/Include/internal/pycore_runtime_init_generated.h +++ b/Include/internal/pycore_runtime_init_generated.h @@ -1329,6 +1329,9 @@ extern "C" { INIT_STR(native, ""), \ INIT_STR(str_replace_inf, "1e309"), \ INIT_STR(type_params, ".type_params"), \ + INIT_STR(unknown_file, ""), \ + INIT_STR(unknown_function, ""), \ + INIT_STR(unreadable_frame, ""), \ INIT_STR(utf_8, "utf-8"), \ } diff --git a/Include/internal/pycore_unicodeobject_generated.h b/Include/internal/pycore_unicodeobject_generated.h index daf6840aa47f328..a74de6de97e572f 100644 --- a/Include/internal/pycore_unicodeobject_generated.h +++ b/Include/internal/pycore_unicodeobject_generated.h @@ -3460,10 +3460,22 @@ _PyUnicode_InitStaticStrings(PyInterpreterState *interp) { _PyUnicode_InternStatic(interp, &string); assert(_PyUnicode_CheckConsistency(string, 1)); assert(PyUnicode_GET_LENGTH(string) != 1); + string = &_Py_STR(unknown_file); + _PyUnicode_InternStatic(interp, &string); + assert(_PyUnicode_CheckConsistency(string, 1)); + assert(PyUnicode_GET_LENGTH(string) != 1); + string = &_Py_STR(unknown_function); + _PyUnicode_InternStatic(interp, &string); + assert(_PyUnicode_CheckConsistency(string, 1)); + assert(PyUnicode_GET_LENGTH(string) != 1); string = &_Py_STR(anon_unknown); _PyUnicode_InternStatic(interp, &string); assert(_PyUnicode_CheckConsistency(string, 1)); assert(PyUnicode_GET_LENGTH(string) != 1); + string = &_Py_STR(unreadable_frame); + _PyUnicode_InternStatic(interp, &string); + assert(_PyUnicode_CheckConsistency(string, 1)); + assert(PyUnicode_GET_LENGTH(string) != 1); string = &_Py_STR(json_decoder); _PyUnicode_InternStatic(interp, &string); assert(_PyUnicode_CheckConsistency(string, 1)); diff --git a/Lib/asyncio/tools.py b/Lib/asyncio/tools.py index 2ac1738d15c6c72..fd494912549bf8c 100644 --- a/Lib/asyncio/tools.py +++ b/Lib/asyncio/tools.py @@ -27,6 +27,10 @@ def __init__( # ─── indexing helpers ─────────────────────────────────────────── def _format_stack_entry(elem: str|FrameInfo) -> str: if not isinstance(elem, str): + if elem.location is None: + if elem.filename in ("", "~"): + return f"{elem.funcname}" + return f"{elem.funcname} {elem.filename}" if elem.location.lineno == 0 and elem.filename == "": return f"{elem.funcname}" else: @@ -190,8 +194,7 @@ def build_task_table(result): # Build coroutine stack string frames = [frame for coro in task_info.coroutine_stack for frame in coro.call_stack] - coro_stack = " -> ".join(_format_stack_entry(x).split(" ")[0] - for x in frames) + coro_stack = " -> ".join(x.funcname for x in frames) # Handle tasks with no awaiters if not task_info.awaited_by: @@ -202,8 +205,7 @@ def build_task_table(result): # Handle tasks with awaiters for coro_info in task_info.awaited_by: parent_id = coro_info.task_name - awaiter_frames = [_format_stack_entry(x).split(" ")[0] - for x in coro_info.call_stack] + awaiter_frames = [x.funcname for x in coro_info.call_stack] awaiter_chain = " -> ".join(awaiter_frames) awaiter_name = id2name.get(parent_id, "Unknown") parent_id_str = (hex(parent_id) if isinstance(parent_id, int) diff --git a/Lib/test/test_asyncio/test_tools.py b/Lib/test/test_asyncio/test_tools.py index df934164eb9fd60..2b8e4940333196b 100644 --- a/Lib/test/test_asyncio/test_tools.py +++ b/Lib/test/test_asyncio/test_tools.py @@ -1558,6 +1558,82 @@ def test_table_output_format(self): class TestAsyncioToolsEdgeCases(unittest.TestCase): + def test_frames_without_location_tree(self): + """Frames the unwinder could not fully read - should not crash.""" + input_ = [ + AwaitedInfo( + thread_id=1, + awaited_by=[ + TaskInfo( + task_id=1, + task_name="Task-A", + coroutine_stack=[ + CoroInfo( + call_stack=[ + FrameInfo("", "~", None), + FrameInfo("", "app.py", None), + FrameInfo("big", "big.py", None), + ], + task_name=1 + ) + ], + awaited_by=[] + ) + ] + ) + ] + self.assertEqual( + tools.build_async_tree(input_), + [[ + "└── (T) Task-A", + " └── big big.py", + " └── app.py", + " └── ", + ]], + ) + + def test_frames_without_location_table(self): + """Frame names are not truncated at the first space.""" + input_ = [ + AwaitedInfo( + thread_id=1, + awaited_by=[ + TaskInfo( + task_id=1, + task_name="Task-A", + coroutine_stack=[ + CoroInfo( + call_stack=[ + FrameInfo("", "~", None) + ], + task_name=1 + ) + ], + awaited_by=[ + CoroInfo( + call_stack=[ + FrameInfo("", "app.py", None) + ], + task_name=2 + ) + ] + ) + ] + ) + ] + self.assertEqual( + tools.build_task_table(input_), + [[ + 1, + "0x1", + "Task-A", + "", + "", + "Unknown", + "0x2", + ]], + ) + def test_task_awaits_self(self): """A task directly awaits itself - should raise a cycle.""" input_ = [ diff --git a/Lib/test/test_external_inspection.py b/Lib/test/test_external_inspection.py index 910fe96d5e7d81e..d81f3cd7babb021 100644 --- a/Lib/test/test_external_inspection.py +++ b/Lib/test/test_external_inspection.py @@ -3854,5 +3854,178 @@ def test_get_stats_disabled_raises(self): client_socket.sendall(b"done") +@requires_remote_subprocess_debugging() +@skip_if_not_supported +@unittest.skipIf( + sys.platform == "linux" and not PROCESS_VM_READV_SUPPORTED, + "Test only runs on Linux with process_vm_readv support", +) +class TestMetadataDegradation(RemoteInspectionTestBase): + """Tests for graceful degradation of oversized code-object metadata.""" + + @contextmanager + def _running_target(self, script_body): + """Run a target script (socket handshake prepended), yield (process, socket).""" + port = find_unused_port() + script = ( + textwrap.dedent( + f"""\ + import time, socket + sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + sock.connect(('localhost', {port})) + """ + ) + + textwrap.dedent(script_body) + ) + + with os_helper.temp_dir() as work_dir: + script_dir = os.path.join(work_dir, "script_pkg") + os.mkdir(script_dir) + + server_socket = _create_server_socket(port) + script_name = _make_test_script(script_dir, "script", script) + client_socket = None + + try: + with _managed_subprocess([sys.executable, script_name]) as p: + client_socket, _ = server_socket.accept() + server_socket.close() + server_socket = None + yield p, client_socket + finally: + _cleanup_sockets(client_socket, server_socket) + + def _sample_until_frame(self, pid, predicate): + """Sample until a frame matching predicate appears; return that frame.""" + unwinder = RemoteUnwinder(pid, all_threads=True) + traces = _get_stack_trace_with_retry( + unwinder, + condition=lambda t: self._find_frame_in_trace(t, predicate) + is not None, + ) + return self._find_frame_in_trace(traces, predicate) + + def test_long_qualname_truncated_not_dropped(self): + """A qualname longer than 1024 chars is truncated with a marker + instead of failing the whole sample.""" + script_body = """\ + src = ("def " + "f" * 1100 + "():\\n" + " sock.sendall(b'ready')\\n" + " time.sleep(10_000)\\n") + ns = {"sock": sock, "time": time} + exec(src, ns) + ns["f" * 1100]() + """ + with self._running_target(script_body) as (p, client_socket): + _wait_for_signal(client_socket, b"ready") + frame = self._sample_until_frame( + p.pid, lambda f: f.funcname.startswith("fff") + ) + self.assertEqual( + frame.funcname, "f" * 1024 + "(len=1100)" + ) + + def test_long_filename_truncated(self): + """A filename longer than 1024 chars is truncated with a marker + instead of failing the whole sample.""" + script_body = """\ + src = ("def g():\\n" + " sock.sendall(b'ready')\\n" + " time.sleep(10_000)\\n") + ns = {"sock": sock, "time": time} + exec(compile(src, "x" * 1500 + ".py", "exec"), ns) + ns["g"]() + """ + with self._running_target(script_body) as (p, client_socket): + _wait_for_signal(client_socket, b"ready") + frame = self._sample_until_frame( + p.pid, lambda f: f.funcname == "g" + ) + self.assertEqual( + frame.filename, "x" * 1024 + "(len=1503)" + ) + + def test_long_task_name_truncated(self): + """An asyncio task name longer than 255 chars is truncated with a + marker instead of failing the whole sample.""" + script_body = """\ + import asyncio + + async def worker(): + sock.sendall(b"ready") + await asyncio.sleep(10_000) + + async def main(): + await asyncio.create_task(worker(), name="T" * 300) + + asyncio.run(main()) + """ + with self._running_target(script_body) as (p, client_socket): + _wait_for_signal(client_socket, b"ready") + names = [ + task.task_name + for awaited_info in get_all_awaited_by(p.pid) + for task in awaited_info.awaited_by + ] + self.assertIn("T" * 255 + "(len=300)", names) + + def test_oversized_linetable_degrades_to_no_location(self): + """A linetable over MAX_LINETABLE_SIZE degrades to a frame without + location instead of failing the whole sample.""" + script_body = """\ + body = " x = 1\\n" * 16_000 + src = ("def big():\\n" + body + "\\n" + " sock.sendall(b'ready')\\n" + " time.sleep(10_000)\\n") + ns = {"sock": sock, "time": time} + exec(compile(src, "big_linetable.py", "exec"), ns) + sock.sendall(b"lt:%d\\n" % len(ns["big"].__code__.co_linetable)) + ns["big"]() + """ + with self._running_target(script_body) as (p, client_socket): + buffer = _wait_for_signal(client_socket, [b"lt:", b"ready"]) + linetable_size = int( + buffer.partition(b"lt:")[2].partition(b"\n")[0] + ) + self.assertGreater(linetable_size, 64 * 1024) + + frame = self._sample_until_frame( + p.pid, lambda f: f.funcname == "big" + ) + self.assertIsNone(frame.location) + self.assertEqual(frame.filename, "big_linetable.py") + + @unittest.skipIf( + sys.platform == "win32", + "Process death maps to ProcessLookupError only on POSIX platforms", + ) + def test_dead_process_raises_not_degrades(self): + """Death of the target raises ProcessLookupError instead of + degrading to synthetic frames.""" + script_body = """\ + sock.sendall(b"ready") + time.sleep(10_000) + """ + with self._running_target(script_body) as (p, client_socket): + _wait_for_signal(client_socket, b"ready") + unwinder = RemoteUnwinder(p.pid, all_threads=True) + _get_stack_trace_with_retry(unwinder) + + p.kill() + p.wait() + + for _ in busy_retry(SHORT_TIMEOUT, error=False): + try: + unwinder.get_stack_trace() + except ProcessLookupError: + break + except RuntimeError: + continue + else: + self.fail( + "ProcessLookupError never raised for dead process" + ) + + if __name__ == "__main__": unittest.main() diff --git a/Lib/test/test_profiling/test_sampling_profiler/test_binary_format.py b/Lib/test/test_profiling/test_sampling_profiler/test_binary_format.py index e4963dca9c96636..52509759ed9fe54 100644 --- a/Lib/test/test_profiling/test_sampling_profiler/test_binary_format.py +++ b/Lib/test/test_profiling/test_sampling_profiler/test_binary_format.py @@ -628,6 +628,26 @@ def test_same_line_different_columns(self): collector, count = self.roundtrip(samples) self.assertEqual(count, 3) + def test_synthetic_frames_roundtrip(self): + """Degraded/sentinel frames (location=None) survive the binary format.""" + frames = [ + FrameInfo(("~", None, name, None)) + for name in ( + "", + "", + "", + "", + "", + ) + ] + frames.append(FrameInfo(("app.py", None, "", None))) + frames.append(FrameInfo(("", None, "real_func", None))) + samples = [[make_interpreter(0, [make_thread(1, frames)])]] + + collector, count = self.roundtrip(samples) + self.assertEqual(count, 1) + self.assert_samples_equal(samples, collector) + class TestBinaryEdgeCases(BinaryFormatTestBase): """Tests for edge cases in binary format.""" diff --git a/Misc/NEWS.d/next/Library/2026-07-19-22-08-05.gh-issue-154194.1bqRdx.rst b/Misc/NEWS.d/next/Library/2026-07-19-22-08-05.gh-issue-154194.1bqRdx.rst new file mode 100644 index 000000000000000..0919dbbc9eb53a4 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-07-19-22-08-05.gh-issue-154194.1bqRdx.rst @@ -0,0 +1,3 @@ +Fix the sampling profiler dropping entire samples when a non-fatal read fails; +frames now keep any readable metadata, and long funcnames, filenames and +asyncio task names are truncated instead. Patch by Maurycy Pawłowski-Wieroński. diff --git a/Modules/_remote_debugging/_remote_debugging.h b/Modules/_remote_debugging/_remote_debugging.h index fa37fb7b2167ecf..1448495906c5450 100644 --- a/Modules/_remote_debugging/_remote_debugging.h +++ b/Modules/_remote_debugging/_remote_debugging.h @@ -180,7 +180,7 @@ typedef enum _WIN32_THREADSTATE { #define set_exception_cause(unwinder, exc_type, message) \ do { \ assert(PyErr_Occurred() && "function returned -1 without setting exception"); \ - if (unwinder->debug && !_Py_RemoteDebug_HasPermissionError()) { \ + if (unwinder->debug && !_Py_RemoteDebug_IsFatalReadError()) { \ _set_debug_exception_cause(exc_type, message); \ } \ } while (0) diff --git a/Modules/_remote_debugging/code_objects.c b/Modules/_remote_debugging/code_objects.c index f83252524b96ff4..851a15f71f72221 100644 --- a/Modules/_remote_debugging/code_objects.c +++ b/Modules/_remote_debugging/code_objects.c @@ -338,11 +338,16 @@ parse_code_object(RemoteUnwinderObject *unwinder, PyObject **result, const CodeObjectContext *ctx) { + _Py_DECLARE_STR(unknown_function, ""); + _Py_DECLARE_STR(unknown_file, ""); + _Py_DECLARE_STR(unreadable_frame, ""); + void *key = (void *)ctx->code_addr; CachedCodeMetadata *meta = NULL; PyObject *func = NULL; PyObject *file = NULL; PyObject *linetable = NULL; + int code_metadata_incomplete = 0; #ifdef Py_GIL_DISABLED // In free threading builds, code object addresses might have the low bit set @@ -366,30 +371,59 @@ parse_code_object(RemoteUnwinderObject *unwinder, if (_Py_RemoteDebug_PagedReadRemoteMemory( &unwinder->handle, real_address, SIZEOF_CODE_OBJ, code_object) < 0) { - set_exception_cause(unwinder, PyExc_RuntimeError, "Failed to read code object"); - goto error; + if (_Py_RemoteDebug_IsFatalReadError()) { + set_exception_cause(unwinder, PyExc_RuntimeError, "Failed to read code object"); + goto error; + } + PyErr_Clear(); + PyObject *tuple = make_frame_info( + unwinder, _Py_LATIN1_CHR('~'), Py_None, + &_Py_STR(unreadable_frame), Py_None); + if (tuple == NULL) { + goto error; + } + *result = tuple; + return 0; } func = read_py_str(unwinder, GET_MEMBER(uintptr_t, code_object, unwinder->debug_offsets.code_object.qualname), 1024); if (!func) { - set_exception_cause(unwinder, PyExc_RuntimeError, "Failed to read function name from code object"); - goto error; + if (_Py_RemoteDebug_IsFatalReadError()) { + set_exception_cause(unwinder, PyExc_RuntimeError, "Failed to read function name from code object"); + goto error; + } + PyErr_Clear(); + func = Py_NewRef(&_Py_STR(unknown_function)); + code_metadata_incomplete = 1; } file = read_py_str(unwinder, GET_MEMBER(uintptr_t, code_object, unwinder->debug_offsets.code_object.filename), 1024); if (!file) { - set_exception_cause(unwinder, PyExc_RuntimeError, "Failed to read filename from code object"); - goto error; + if (_Py_RemoteDebug_IsFatalReadError()) { + set_exception_cause(unwinder, PyExc_RuntimeError, "Failed to read filename from code object"); + goto error; + } + PyErr_Clear(); + file = Py_NewRef(&_Py_STR(unknown_file)); + code_metadata_incomplete = 1; + } + + if (code_metadata_incomplete) { + goto degraded; } linetable = read_py_bytes(unwinder, GET_MEMBER(uintptr_t, code_object, unwinder->debug_offsets.code_object.linetable), MAX_LINETABLE_SIZE); if (!linetable) { - set_exception_cause(unwinder, PyExc_RuntimeError, "Failed to read linetable from code object"); - goto error; + if (_Py_RemoteDebug_IsFatalReadError()) { + set_exception_cause(unwinder, PyExc_RuntimeError, "Failed to read linetable from code object"); + goto error; + } + PyErr_Clear(); + goto degraded; } meta = PyMem_RawMalloc(sizeof(CachedCodeMetadata)); @@ -542,6 +576,18 @@ parse_code_object(RemoteUnwinderObject *unwinder, *result = tuple; return 0; +degraded: { + PyObject *degraded_tuple = make_frame_info(unwinder, file, Py_None, + func, Py_None); + Py_CLEAR(func); + Py_CLEAR(file); + if (!degraded_tuple) { + return -1; + } + *result = degraded_tuple; + return 0; +} + error: Py_XDECREF(func); Py_XDECREF(file); diff --git a/Modules/_remote_debugging/object_reading.c b/Modules/_remote_debugging/object_reading.c index 1cea96a2151fcc6..e0b1ee029786659 100644 --- a/Modules/_remote_debugging/object_reading.c +++ b/Modules/_remote_debugging/object_reading.c @@ -64,12 +64,14 @@ read_py_str( } Py_ssize_t len = GET_MEMBER(Py_ssize_t, unicode_obj, unwinder->debug_offsets.unicode_object.length); - if (len < 0 || len > max_len) { + if (len < 0) { PyErr_Format(PyExc_RuntimeError, "Invalid string length (%zd) at 0x%lx", len, address); set_exception_cause(unwinder, PyExc_RuntimeError, "Invalid string length in remote Unicode object"); return NULL; } + Py_ssize_t read_len = Py_MIN(len, max_len); + int truncated = read_len != len; // Inspect state to pick the right data offset and character width. // We rely on the remote process sharing this Python version's @@ -111,51 +113,54 @@ read_py_str( ? (size_t)unwinder->debug_offsets.unicode_object.asciiobject_size : (size_t)unwinder->debug_offsets.unicode_object.compactunicodeobject_size; - // len * kind is bounded by max_len * 4 (kind <= 4, len <= max_len), so + // read_len * kind is bounded by max_len * 4 (kind <= 4, len <= max_len), so // the multiplication can't overflow for any caller-sane max_len, but the // explicit cap here keeps a corrupted remote `length` from later turning // into a giant allocation. - size_t nbytes = (size_t)len * (size_t)kind; - if ((size_t)len > (SIZE_MAX / 4) || nbytes > (size_t)max_len * 4) { - PyErr_Format(PyExc_RuntimeError, - "Implausible Unicode byte size %zu at 0x%lx", nbytes, address); - set_exception_cause(unwinder, PyExc_RuntimeError, - "Garbage byte size in remote Unicode object"); - return NULL; - } + size_t nbytes = (size_t)read_len * (size_t)kind; - PyObject *result = PyUnicode_New(len, max_char); + PyObject *result = PyUnicode_New(read_len, max_char); if (result == NULL) { set_exception_cause(unwinder, PyExc_RuntimeError, "Failed to allocate PyUnicode for remote string"); return NULL; } - if (nbytes == 0) { - return result; - } - void *data = PyUnicode_DATA(result); + if (nbytes > 0) { + void *data = PyUnicode_DATA(result); + + // Reuse data already present in the header read; only round-trip for + // whatever spills past it. + size_t inline_avail = (header_size < SIZEOF_UNICODE_OBJ) + ? SIZEOF_UNICODE_OBJ - header_size + : 0; + size_t inline_bytes = nbytes < inline_avail ? nbytes : inline_avail; + if (inline_bytes > 0) { + memcpy(data, unicode_obj + header_size, inline_bytes); + } - // Reuse data already present in the header read; only round-trip for - // whatever spills past it. - size_t inline_avail = (header_size < SIZEOF_UNICODE_OBJ) - ? SIZEOF_UNICODE_OBJ - header_size - : 0; - size_t inline_bytes = nbytes < inline_avail ? nbytes : inline_avail; - if (inline_bytes > 0) { - memcpy(data, unicode_obj + header_size, inline_bytes); + if (nbytes > inline_bytes) { + res = _Py_RemoteDebug_PagedReadRemoteMemory( + &unwinder->handle, + address + header_size + inline_bytes, + nbytes - inline_bytes, + (char *)data + inline_bytes); + if (res < 0) { + Py_DECREF(result); + set_exception_cause(unwinder, PyExc_RuntimeError, "Failed to read string data from remote memory"); + return NULL; + } + } } - if (nbytes > inline_bytes) { - res = _Py_RemoteDebug_PagedReadRemoteMemory( - &unwinder->handle, - address + header_size + inline_bytes, - nbytes - inline_bytes, - (char *)data + inline_bytes); - if (res < 0) { - Py_DECREF(result); - set_exception_cause(unwinder, PyExc_RuntimeError, "Failed to read string data from remote memory"); + if (truncated) { + PyObject *truncated_result = PyUnicode_FromFormat( + "%U(len=%zd)", result, len); + Py_DECREF(result); + if (truncated_result == NULL) { + set_exception_cause(unwinder, PyExc_RuntimeError, "Failed to append truncation marker to remote string"); return NULL; } + return truncated_result; } return result; diff --git a/Python/remote_debug.h b/Python/remote_debug.h index 7b7380d25bf496f..c3c467e87edbf61 100644 --- a/Python/remote_debug.h +++ b/Python/remote_debug.h @@ -107,9 +107,18 @@ _Py_RemoteDebug_HasPermissionError(void) && PyErr_ExceptionMatches(PyExc_PermissionError); } +static inline int +_Py_RemoteDebug_IsFatalReadError(void) +{ + return _Py_RemoteDebug_HasPermissionError() + || PyErr_ExceptionMatches(PyExc_MemoryError) + || PyErr_ExceptionMatches(PyExc_ProcessLookupError) + || (PyErr_Occurred() && !PyErr_ExceptionMatches(PyExc_Exception)); +} + #define _set_debug_exception_cause(exception, format, ...) \ do { \ - if (!_Py_RemoteDebug_HasPermissionError()) { \ + if (!_Py_RemoteDebug_IsFatalReadError()) { \ PyThreadState *tstate = _PyThreadState_GET(); \ if (!_PyErr_Occurred(tstate)) { \ _PyErr_Format(tstate, exception, format, ##__VA_ARGS__); \