From 05176596d992283e50e4304d584258209179b8d7 Mon Sep 17 00:00:00 2001 From: Stan Ulbrych Date: Wed, 14 Oct 2026 09:08:38 +0100 Subject: [PATCH 1/2] Validate strings read from remote process in `_remote_debugging` --- Lib/test/test_external_inspection.py | 29 +++++++++++ ...-10-14-08-50-00.gh-issue-159398.k2Xd7q.rst | 2 + Modules/_remote_debugging/object_reading.c | 50 +++++++++---------- Modules/_testinternalcapi.c | 13 +++++ 4 files changed, 68 insertions(+), 26 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2026-10-14-08-50-00.gh-issue-159398.k2Xd7q.rst diff --git a/Lib/test/test_external_inspection.py b/Lib/test/test_external_inspection.py index 9fff8e8bff91f7a..16dcf4eff2bdc64 100644 --- a/Lib/test/test_external_inspection.py +++ b/Lib/test/test_external_inspection.py @@ -746,6 +746,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.stderr) + self.assertIn("", result.stdout) + @skip_if_not_supported @unittest.skipIf( sys.platform == "linux" and not PROCESS_VM_READV_SUPPORTED, 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 000000000000000..65e10f4ba792840 --- /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 743970db4ddc88a..a4e8758780e92eb 100644 --- a/Modules/_remote_debugging/object_reading.c +++ b/Modules/_remote_debugging/object_reading.c @@ -91,23 +91,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 +119,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 +145,20 @@ 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; } } + 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 f629b8e528faca7..10dd15bb3c45827 100644 --- a/Modules/_testinternalcapi.c +++ b/Modules/_testinternalcapi.c @@ -2056,6 +2056,18 @@ unicode_transformdecimalandspacetoascii(PyObject *self, PyObject *arg) return _PyUnicode_TransformDecimalAndSpaceToASCII(arg); } +/* Return a one-character str holding a code point above U+10FFFF. */ +static PyObject * +new_invalid_str(PyObject *self, PyObject *Py_UNUSED(ignored)) +{ + PyObject *str = PyUnicode_New(1, 0x10FFFF); + if (str == NULL) { + return NULL; + } + PyUnicode_4BYTE_DATA(str)[0] = 0x110000; + return str; +} + static PyObject * test_pyobject_is_freed(const char *test_name, PyObject *op) { @@ -3395,6 +3407,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}, From c9cd04758acf7c7525d76badbbee915e8a27dc5d Mon Sep 17 00:00:00 2001 From: Stan Ulbrych Date: Wed, 14 Oct 2026 14:40:28 +0100 Subject: [PATCH 2/2] Apply Victor's suggestions, and actually fix the bug Co-authored-by: vstinner@python.org --- Modules/_remote_debugging/object_reading.c | 17 +++++++++++++++++ Modules/_testinternalcapi.c | 5 +++-- 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/Modules/_remote_debugging/object_reading.c b/Modules/_remote_debugging/object_reading.c index a4e8758780e92eb..794eb035236fdc7 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 /* ============================================================================ @@ -153,6 +154,22 @@ read_py_str( } } + 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_ValueError, + "character U+%x is not in range [U+0000; U+%x]", + ch, _Py_MAX_UNICODE); + set_exception_cause(unwinder, PyExc_ValueError, + "Unicode string contains invalid character"); + return NULL; + } + } + } + PyObject *result = PyUnicode_FromKindAndData(kind, data, len); PyMem_Free(data); if (result == NULL) { diff --git a/Modules/_testinternalcapi.c b/Modules/_testinternalcapi.c index 10dd15bb3c45827..f20303ea026afd9 100644 --- a/Modules/_testinternalcapi.c +++ b/Modules/_testinternalcapi.c @@ -2056,15 +2056,16 @@ unicode_transformdecimalandspacetoascii(PyObject *self, PyObject *arg) return _PyUnicode_TransformDecimalAndSpaceToASCII(arg); } -/* Return a one-character str holding a code point above U+10FFFF. */ +/* Return a str holding code points above U+10FFFF. */ static PyObject * new_invalid_str(PyObject *self, PyObject *Py_UNUSED(ignored)) { - PyObject *str = PyUnicode_New(1, 0x10FFFF); + PyObject *str = PyUnicode_New(2, 0x10FFFF); if (str == NULL) { return NULL; } PyUnicode_4BYTE_DATA(str)[0] = 0x110000; + PyUnicode_4BYTE_DATA(str)[1] = 0x110000; return str; }