https://github.com/python/cpython/commit/1374fde57420f17d91cdcf87437b2857826c995e
commit: 1374fde57420f17d91cdcf87437b2857826c995e
branch: main
author: Stan Ulbrych <[email protected]>
committer: StanFromIreland <[email protected]>
date: 2026-10-30T10:31:36Z
summary:

gh-159398: Validate strings read from the remote process in `_remote_debugging` 
(#159402)

Co-authored-by: Victor Stinner <[email protected]>
Co-authored-by: Maurycy Pawłowski-Wieroński <[email protected]>

files:
A Misc/NEWS.d/next/Library/2026-10-14-08-50-00.gh-issue-159398.k2Xd7q.rst
M Lib/test/test_external_inspection.py
M Modules/_remote_debugging/object_reading.c
M Modules/_testinternalcapi.c

diff --git a/Lib/test/test_external_inspection.py 
b/Lib/test/test_external_inspection.py
index 48e5a2692c8df3..df0af14d9f7562 100644
--- a/Lib/test/test_external_inspection.py
+++ b/Lib/test/test_external_inspection.py
@@ -901,6 +901,35 @@ def {func_name}():
             )
             self.assertEqual(target.filename, script_name)
 
+    @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",
+    )
+    def test_remote_stack_trace_invalid_unicode_name(self):
+        import_module("_testinternalcapi")
+
+        script = textwrap.dedent("""\
+            import os
+            import _remote_debugging
+            import _testinternalcapi
+
+            def func():
+                return 
_remote_debugging.RemoteUnwinder(os.getpid()).get_stack_trace()
+
+            name = _testinternalcapi.new_invalid_str()
+            func.__code__ = func.__code__.replace(co_qualname=name)
+            print(func())
+            """)
+        result = subprocess.run(
+            [sys.executable, "-c", script],
+            capture_output=True,
+            text=True,
+            timeout=SHORT_TIMEOUT,
+        )
+        self.assertEqual(result.returncode, 0, result)
+        self.assertIn("<unknown function>", result.stdout)
+
     @skip_if_not_supported
     @unittest.skipIf(
         sys.platform == "linux" and not PROCESS_VM_READV_SUPPORTED,
@@ -4575,6 +4604,19 @@ def test_long_qualname_truncated_not_dropped(self):
         self.assertIsNotNone(frame)
         self.assertEqual(frame.funcname, "f" * 1024)
 
+    def test_long_qualname_truncated_to_narrower_kind(self):
+        name = "a" * 1024 + "\U00010300"
+        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("aaa")
+        )
+        self.assertIsNotNone(frame)
+        self.assertEqual(frame.funcname, "a" * 1024)
+
     def test_long_filename_truncated(self):
         """A filename longer than 1024 chars is truncated instead of
         failing the whole sample."""
diff --git 
a/Misc/NEWS.d/next/Library/2026-10-14-08-50-00.gh-issue-159398.k2Xd7q.rst 
b/Misc/NEWS.d/next/Library/2026-10-14-08-50-00.gh-issue-159398.k2Xd7q.rst
new file mode 100644
index 00000000000000..65e10f4ba79284
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2026-10-14-08-50-00.gh-issue-159398.k2Xd7q.rst
@@ -0,0 +1,2 @@
+Fix the sampling profiler crashing when a string read from the target process
+contains code points above ``U+10FFFF``.
diff --git a/Modules/_remote_debugging/object_reading.c 
b/Modules/_remote_debugging/object_reading.c
index 743970db4ddc88..e449b446a1fe54 100644
--- a/Modules/_remote_debugging/object_reading.c
+++ b/Modules/_remote_debugging/object_reading.c
@@ -6,6 +6,7 @@
  
******************************************************************************/
 
 #include "_remote_debugging.h"
+#include "internal/pycore_unicodeobject.h"  // _Py_MAX_UNICODE
 #include <limits.h>
 
 /* ============================================================================
@@ -91,23 +92,15 @@ read_py_str(
     }
 
     int kind = (int)state.kind;
-    Py_UCS4 max_char;
-    switch (kind) {
-        case PyUnicode_1BYTE_KIND:
-            max_char = state.ascii ? 0x7F : 0xFF;
-            break;
-        case PyUnicode_2BYTE_KIND:
-            max_char = 0xFFFF;
-            break;
-        case PyUnicode_4BYTE_KIND:
-            max_char = 0x10FFFF;
-            break;
-        default:
-            PyErr_Format(PyExc_RuntimeError,
-                         "Invalid Unicode kind %d at 0x%lx", kind, address);
-            set_exception_cause(unwinder, PyExc_RuntimeError,
-                                "Invalid kind in remote Unicode object");
-            return NULL;
+    if (kind != PyUnicode_1BYTE_KIND
+        && kind != PyUnicode_2BYTE_KIND
+        && kind != PyUnicode_4BYTE_KIND)
+    {
+        PyErr_Format(PyExc_RuntimeError,
+                     "Invalid Unicode kind %d at 0x%lx", kind, address);
+        set_exception_cause(unwinder, PyExc_RuntimeError,
+                            "Invalid kind in remote Unicode object");
+        return NULL;
     }
 
     size_t header_size = state.ascii
@@ -127,16 +120,16 @@ read_py_str(
         return NULL;
     }
 
-    PyObject *result = PyUnicode_New(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;
+        return Py_GetConstant(Py_CONSTANT_EMPTY_STR);
     }
 
-    void *data = PyUnicode_DATA(result);
+    char *data = PyMem_Malloc(nbytes);
+    if (data == NULL) {
+        PyErr_NoMemory();
+        set_exception_cause(unwinder, PyExc_MemoryError, "Failed to allocate 
buffer for remote string");
+        return NULL;
+    }
 
     // Reuse data already present in the header read; only round-trip for
     // whatever spills past it.
@@ -153,14 +146,36 @@ read_py_str(
             &unwinder->handle,
             address + header_size + inline_bytes,
             nbytes - inline_bytes,
-            (char *)data + inline_bytes);
+            data + inline_bytes);
         if (res < 0) {
-            Py_DECREF(result);
+            PyMem_Free(data);
             set_exception_cause(unwinder, PyExc_RuntimeError, "Failed to read 
string data from remote memory");
             return NULL;
         }
     }
 
+    if (kind == PyUnicode_4BYTE_KIND) {
+        Py_UCS4 *ucs4 = (Py_UCS4 *)data;
+        for (Py_ssize_t i = 0; i < len; i++) {
+            Py_UCS4 ch = ucs4[i];
+            if (ch > _Py_MAX_UNICODE) {
+                PyMem_Free(data);
+                PyErr_Format(PyExc_RuntimeError,
+                             "character U+%x is not in range [U+0000; U+%x]",
+                             ch, _Py_MAX_UNICODE);
+                set_exception_cause(unwinder, PyExc_RuntimeError,
+                                    "Unicode string contains invalid 
character");
+                return NULL;
+            }
+        }
+    }
+
+    PyObject *result = PyUnicode_FromKindAndData(kind, data, len);
+    PyMem_Free(data);
+    if (result == NULL) {
+        set_exception_cause(unwinder, PyExc_RuntimeError, "Invalid character 
data in remote Unicode object");
+        return NULL;
+    }
     return result;
 }
 
diff --git a/Modules/_testinternalcapi.c b/Modules/_testinternalcapi.c
index 5b5516046fff40..b3945b1a7634dc 100644
--- a/Modules/_testinternalcapi.c
+++ b/Modules/_testinternalcapi.c
@@ -2060,6 +2060,19 @@ unicode_transformdecimalandspacetoascii(PyObject *self, 
PyObject *arg)
     return _PyUnicode_TransformDecimalAndSpaceToASCII(arg);
 }
 
+/* Return a str holding code points above U+10FFFF. */
+static PyObject *
+new_invalid_str(PyObject *self, PyObject *Py_UNUSED(ignored))
+{
+    PyObject *str = PyUnicode_New(2, _Py_MAX_UNICODE);
+    if (str == NULL) {
+        return NULL;
+    }
+    PyUnicode_4BYTE_DATA(str)[0] = _Py_MAX_UNICODE + 1;
+    PyUnicode_4BYTE_DATA(str)[1] = _Py_MAX_UNICODE + 2;
+    return str;
+}
+
 static PyObject *
 test_pyobject_is_freed(const char *test_name, PyObject *op)
 {
@@ -3402,6 +3415,7 @@ static PyMethodDef module_functions[] = {
     {"_PyTraceMalloc_GetTraceback", tracemalloc_get_traceback, METH_VARARGS},
     {"test_tstate_capi", test_tstate_capi, METH_NOARGS, NULL},
     {"_PyUnicode_TransformDecimalAndSpaceToASCII", 
unicode_transformdecimalandspacetoascii, METH_O},
+    {"new_invalid_str", new_invalid_str, METH_NOARGS},
     {"check_pyobject_forbidden_bytes_is_freed",
                             check_pyobject_forbidden_bytes_is_freed, 
METH_NOARGS},
     {"check_pyobject_freed_is_freed", check_pyobject_freed_is_freed, 
METH_NOARGS},

_______________________________________________
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