https://github.com/python/cpython/commit/7d25916b41de4fc28e04b4944864ad1094a9e8e0
commit: 7d25916b41de4fc28e04b4944864ad1094a9e8e0
branch: main
author: Maurycy Pawłowski-Wieroński <[email protected]>
committer: pablogsal <[email protected]>
date: 2026-10-05T00:37:21Z
summary:

gh-154194: Degrade frames in Tachyon instead of failing the sample (#154195)

* degrade gracefully

* news

* better NEWS wording

* do not raise on MAX_REMOTE_STR_READ

* bye MAX_REMOTE_STR_READ

* fix -m asyncio ps|pstree

* test truncation and linetable sentinel

* simpler

* simpler

* redundant now

* respect #157790 in the news

---------

Co-authored-by: Pablo Galindo Salgado <[email protected]>

files:
A Misc/NEWS.d/next/Library/2026-07-19-22-08-05.gh-issue-154194.1bqRdx.rst
M Include/internal/pycore_global_objects_fini_generated.h
M Include/internal/pycore_global_strings.h
M Include/internal/pycore_runtime_init_generated.h
M Include/internal/pycore_unicodeobject_generated.h
M Lib/asyncio/tools.py
M Lib/test/test_asyncio/test_tools.py
M Lib/test/test_external_inspection.py
M Lib/test/test_profiling/test_sampling_profiler/test_binary_format.py
M Modules/_remote_debugging/_remote_debugging.h
M Modules/_remote_debugging/code_objects.c
M Python/remote_debug.h

diff --git a/Include/internal/pycore_global_objects_fini_generated.h 
b/Include/internal/pycore_global_objects_fini_generated.h
index bcbfcdd9eaaec5e..0e2fab84aec0ae6 100644
--- a/Include/internal/pycore_global_objects_fini_generated.h
+++ b/Include/internal/pycore_global_objects_fini_generated.h
@@ -45,6 +45,9 @@ _PyStaticObjects_CheckAll(PyInterpreterState *interp) {
     _PyStaticObject_CheckUnicodeSingleton((PyObject *)&_Py_STR(native), 
"<native>", 8);
     _PyStaticObject_CheckUnicodeSingleton((PyObject 
*)&_Py_STR(str_replace_inf), "1e309", 5);
     _PyStaticObject_CheckUnicodeSingleton((PyObject *)&_Py_STR(type_params), 
".type_params", 12);
+    _PyStaticObject_CheckUnicodeSingleton((PyObject *)&_Py_STR(unknown_file), 
"<unknown file>", 14);
+    _PyStaticObject_CheckUnicodeSingleton((PyObject 
*)&_Py_STR(unknown_function), "<unknown function>", 18);
+    _PyStaticObject_CheckUnicodeSingleton((PyObject 
*)&_Py_STR(unreadable_frame), "<unreadable frame>", 18);
     _PyStaticObject_CheckUnicodeSingleton((PyObject *)&_Py_STR(utf_8), 
"utf-8", 5);
     _PyStaticObject_CheckUnicodeSingleton((PyObject *)&_Py_ID(AGEN_CLOSED), 
"AGEN_CLOSED", 11);
     _PyStaticObject_CheckUnicodeSingleton((PyObject *)&_Py_ID(AGEN_CREATED), 
"AGEN_CREATED", 12);
diff --git a/Include/internal/pycore_global_strings.h 
b/Include/internal/pycore_global_strings.h
index 2e5020578ad69b8..cbfa80f76b64cd1 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, "<native>")
         STRUCT_FOR_STR(str_replace_inf, "1e309")
         STRUCT_FOR_STR(type_params, ".type_params")
+        STRUCT_FOR_STR(unknown_file, "<unknown file>")
+        STRUCT_FOR_STR(unknown_function, "<unknown function>")
+        STRUCT_FOR_STR(unreadable_frame, "<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 c95b2d4f95a50da..34dc6228a0bcb08 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, "<native>"), \
     INIT_STR(str_replace_inf, "1e309"), \
     INIT_STR(type_params, ".type_params"), \
+    INIT_STR(unknown_file, "<unknown file>"), \
+    INIT_STR(unknown_function, "<unknown function>"), \
+    INIT_STR(unreadable_frame, "<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 795515260daecb7..f2f3b9de4a0d87d 100644
--- a/Include/internal/pycore_unicodeobject_generated.h
+++ b/Include/internal/pycore_unicodeobject_generated.h
@@ -892,7 +892,10 @@ _PyUnicode_InitStaticStrings(PyInterpreterState *interp) {
         offsetof(struct _Py_global_strings, 
literals._py_native._ascii.ob_base),
         offsetof(struct _Py_global_strings, 
literals._py_anon_setcomp._ascii.ob_base),
         offsetof(struct _Py_global_strings, 
literals._py_anon_string._ascii.ob_base),
+        offsetof(struct _Py_global_strings, 
literals._py_unknown_file._ascii.ob_base),
+        offsetof(struct _Py_global_strings, 
literals._py_unknown_function._ascii.ob_base),
         offsetof(struct _Py_global_strings, 
literals._py_anon_unknown._ascii.ob_base),
+        offsetof(struct _Py_global_strings, 
literals._py_unreadable_frame._ascii.ob_base),
         offsetof(struct _Py_global_strings, 
literals._py_json_decoder._ascii.ob_base),
         offsetof(struct _Py_global_strings, 
literals._py_list_err._ascii.ob_base),
         offsetof(struct _Py_global_strings, literals._py_utf_8._ascii.ob_base),
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("<unreadable frame>", "~", None),
+                                    FrameInfo("<unknown function>", "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",
+                "        └──  <unknown function> app.py",
+                "            └──  <unreadable frame>",
+            ]],
+        )
+
+    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("<unreadable frame>", "~", None)
+                                ],
+                                task_name=1
+                            )
+                        ],
+                        awaited_by=[
+                            CoroInfo(
+                                call_stack=[
+                                    FrameInfo("<unknown function>", "app.py", 
None)
+                                ],
+                                task_name=2
+                            )
+                        ]
+                    )
+                ]
+            )
+        ]
+        self.assertEqual(
+            tools.build_task_table(input_),
+            [[
+                1,
+                "0x1",
+                "Task-A",
+                "<unreadable frame>",
+                "<unknown function>",
+                "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 f487c5943352b2e..6b055ec6b842686 100644
--- a/Lib/test/test_external_inspection.py
+++ b/Lib/test/test_external_inspection.py
@@ -4132,6 +4132,93 @@ def test_get_stats_disabled_raises(self):
             client_socket.sendall(b"done")
 
 
+@requires_remote_subprocess_debugging()
+@skip_if_not_supported
[email protected](
+    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."""
+
+    def test_long_qualname_truncated_not_dropped(self):
+        """A qualname longer than 1024 chars is truncated instead of
+        failing the whole sample."""
+        name = "f" * 1100
+        src = f"def {name}(sample):\n    return sample()\n"
+        ns = {}
+        exec(src, ns)
+
+        trace = ns[name](RemoteUnwinder(os.getpid()).get_stack_trace)
+        frame = self._find_frame_in_trace(
+            trace, lambda f: f.funcname.startswith("fff")
+        )
+        self.assertIsNotNone(frame)
+        self.assertEqual(frame.funcname, "f" * 1024)
+
+    def test_long_filename_truncated(self):
+        """A filename longer than 1024 chars is truncated instead of
+        failing the whole sample."""
+        src = "def g(sample):\n    return sample()\n"
+        ns = {}
+        exec(compile(src, "x" * 1500 + ".py", "exec"), ns)
+
+        trace = ns["g"](RemoteUnwinder(os.getpid()).get_stack_trace)
+        frame = self._find_frame_in_trace(trace, lambda f: f.funcname == "g")
+        self.assertIsNotNone(frame)
+        self.assertEqual(frame.filename, "x" * 1024)
+
+    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."""
+        src = (
+            "def big(sample):\n"
+            + "    x = 1\n" * 20_000
+            + "    return sample()\n"
+        )
+        ns = {}
+        exec(compile(src, "big_linetable.py", "exec"), ns)
+        big = ns["big"]
+        self.assertGreater(len(big.__code__.co_linetable), 64 * 1024)
+
+        trace = big(RemoteUnwinder(os.getpid()).get_stack_trace)
+        frame = self._find_frame_in_trace(
+            trace, 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 = """\
+            import time
+            sock.sendall(b"ready")
+            time.sleep(10_000)
+            """
+        with self._target_process(script_body) as (p, client_socket, 
make_unwinder):
+            _wait_for_signal(client_socket, b"ready")
+            unwinder = make_unwinder()
+            _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")
+
+
 @requires_remote_subprocess_debugging()
 class TestFrameChainLimits(RemoteInspectionTestBase):
     """Frame chain walks abort instead of looping/overflowing on deep 
chains."""
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 ff944b3163ec3e8..d33cc8b619adb1c 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
@@ -665,6 +665,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 (
+                "<GC>",
+                "<native>",
+                "<unknown function>",
+                "<unknown file>",
+                "<unreadable frame>",
+            )
+        ]
+        frames.append(FrameInfo(("app.py", None, "<unknown function>", None)))
+        frames.append(FrameInfo(("<unknown file>", 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..9971cd3f8dc25bd
--- /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 and filenames 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 3aecf3982b95d75..cf923014445f558 100644
--- a/Modules/_remote_debugging/_remote_debugging.h
+++ b/Modules/_remote_debugging/_remote_debugging.h
@@ -182,7 +182,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 6ad058850196269..081aaa49fb49ffb 100644
--- a/Modules/_remote_debugging/code_objects.c
+++ b/Modules/_remote_debugging/code_objects.c
@@ -341,11 +341,16 @@ parse_code_object(RemoteUnwinderObject *unwinder,
                   PyObject **result,
                   const CodeObjectContext *ctx)
 {
+    _Py_DECLARE_STR(unknown_function, "<unknown function>");
+    _Py_DECLARE_STR(unknown_file, "<unknown file>");
+    _Py_DECLARE_STR(unreadable_frame, "<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
@@ -369,30 +374,50 @@ 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()) {
+                goto error;
+            }
+            PyErr_Clear();
+            func = Py_NewRef(&_Py_STR(unreadable_frame));
+            file = Py_NewRef(_Py_LATIN1_CHR('~'));
+            goto degraded;
         }
 
         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()) {
+                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()) {
+                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()) {
+                goto error;
+            }
+            PyErr_Clear();
+            goto degraded;
         }
 
         meta = PyMem_RawMalloc(sizeof(CachedCodeMetadata));
@@ -561,6 +586,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/Python/remote_debug.h b/Python/remote_debug.h
index caea3d5c062e5f6..a279cd9dfd03052 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__); \

_______________________________________________
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