]> git.ipfire.org Git - thirdparty/Python/cpython.git/commitdiff
[3.14] gh-144563: Fix remote debugging with duplicate libpython mappings from ctypes...
authorBartosz Sławecki <bartosz@ilikepython.com>
Tue, 10 Feb 2026 14:31:49 +0000 (15:31 +0100)
committerGitHub <noreply@github.com>
Tue, 10 Feb 2026 14:31:49 +0000 (14:31 +0000)
Lib/test/test_external_inspection.py
Misc/NEWS.d/next/Core_and_Builtins/2026-02-08-18-13-38.gh-issue-144563.hb3kpp.rst [new file with mode: 0644]
Modules/_remote_debugging_module.c
Python/remote_debug.h

index a709b837161f4878a67657cd4cc58cd50aba3c03..dddb3839af4f073b4fb241a9d072a153161d39b9 100644 (file)
@@ -150,6 +150,44 @@ class TestGetStackTrace(unittest.TestCase):
             else:
                 self.fail("Main thread stack trace not found in result")
 
+    @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_self_trace_after_ctypes_import(self):
+        """Test that RemoteUnwinder works on the same process after _ctypes import.
+
+        When _ctypes is imported, it may call dlopen on the libpython shared
+        library, creating a duplicate mapping in the process address space.
+        The remote debugging code must skip these uninitialized duplicate
+        mappings and find the real PyRuntime. See gh-144563.
+        """
+        # Run the test in a subprocess to avoid side effects
+        script = textwrap.dedent("""\
+            import os
+            import _remote_debugging
+
+            # Should work before _ctypes import
+            unwinder = _remote_debugging.RemoteUnwinder(os.getpid())
+
+            import _ctypes
+
+            # Should still work after _ctypes import (gh-144563)
+            unwinder = _remote_debugging.RemoteUnwinder(os.getpid())
+            """)
+
+        result = subprocess.run(
+            [sys.executable, "-c", script],
+            capture_output=True,
+            text=True,
+            timeout=SHORT_TIMEOUT,
+        )
+        self.assertEqual(
+            result.returncode, 0,
+            f"stdout: {result.stdout}\nstderr: {result.stderr}"
+        )
+
     @skip_if_not_supported
     @unittest.skipIf(
         sys.platform == "linux" and not PROCESS_VM_READV_SUPPORTED,
diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2026-02-08-18-13-38.gh-issue-144563.hb3kpp.rst b/Misc/NEWS.d/next/Core_and_Builtins/2026-02-08-18-13-38.gh-issue-144563.hb3kpp.rst
new file mode 100644 (file)
index 0000000..023f9dc
--- /dev/null
@@ -0,0 +1,4 @@
+Fix interaction of the Tachyon profiler and :mod:`ctypes` and other modules
+that load the Python shared library (if present) in an independent map as
+this was causing the mechanism that loads the binary information to be
+confused. Patch by Pablo Galindo
index b46538b76df16e68a0cc475382f2fb5812d85528..dcf901bf1fec9996c1bc244782ae6e1831bb65c7 100644 (file)
@@ -805,7 +805,7 @@ _Py_RemoteDebug_GetAsyncioDebugAddress(proc_handle_t* handle)
 
 #ifdef MS_WINDOWS
     // On Windows, search for asyncio debug in executable or DLL
-    address = search_windows_map_for_section(handle, "AsyncioD", L"_asyncio");
+    address = search_windows_map_for_section(handle, "AsyncioD", L"_asyncio", NULL);
     if (address == 0) {
         // Error out: 'python' substring covers both executable and DLL
         PyObject *exc = PyErr_GetRaisedException();
@@ -814,7 +814,7 @@ _Py_RemoteDebug_GetAsyncioDebugAddress(proc_handle_t* handle)
     }
 #elif defined(__linux__)
     // On Linux, search for asyncio debug in executable or DLL
-    address = search_linux_map_for_section(handle, "AsyncioDebug", "_asyncio.cpython");
+    address = search_linux_map_for_section(handle, "AsyncioDebug", "_asyncio.cpython", NULL);
     if (address == 0) {
         // Error out: 'python' substring covers both executable and DLL
         PyObject *exc = PyErr_GetRaisedException();
@@ -823,10 +823,10 @@ _Py_RemoteDebug_GetAsyncioDebugAddress(proc_handle_t* handle)
     }
 #elif defined(__APPLE__) && TARGET_OS_OSX
     // On macOS, try libpython first, then fall back to python
-    address = search_map_for_section(handle, "AsyncioDebug", "_asyncio.cpython");
+    address = search_map_for_section(handle, "AsyncioDebug", "_asyncio.cpython", NULL);
     if (address == 0) {
         PyErr_Clear();
-        address = search_map_for_section(handle, "AsyncioDebug", "_asyncio.cpython");
+        address = search_map_for_section(handle, "AsyncioDebug", "_asyncio.cpython", NULL);
     }
     if (address == 0) {
         // Error out: 'python' substring covers both executable and DLL
index ed213859a8afabf66c46ae62231c7311887364b2..21c11789189118d5cea3a77a35400d52618058e2 100644 (file)
@@ -133,6 +133,31 @@ typedef struct {
     Py_ssize_t page_size;
 } proc_handle_t;
 
+// Forward declaration for use in validation function
+static int
+_Py_RemoteDebug_ReadRemoteMemory(proc_handle_t *handle, uintptr_t remote_address, size_t len, void* dst);
+
+// Optional callback to validate a candidate section address found during
+// memory map searches. Returns 1 if the address is valid, 0 to skip it.
+// This allows callers to filter out duplicate/stale mappings (e.g. from
+// ctypes dlopen) whose sections were never initialized.
+typedef int (*section_validator_t)(proc_handle_t *handle, uintptr_t address);
+
+// Validate that a candidate address starts with _Py_Debug_Cookie.
+static int
+_Py_RemoteDebug_ValidatePyRuntimeCookie(proc_handle_t *handle, uintptr_t address)
+{
+    if (address == 0) {
+        return 0;
+    }
+    char buf[sizeof(_Py_Debug_Cookie) - 1];
+    if (_Py_RemoteDebug_ReadRemoteMemory(handle, address, sizeof(buf), buf) != 0) {
+        PyErr_Clear();
+        return 0;
+    }
+    return memcmp(buf, _Py_Debug_Cookie, sizeof(buf)) == 0;
+}
+
 static void
 _Py_RemoteDebug_FreePageCache(proc_handle_t *handle)
 {
@@ -490,7 +515,8 @@ pid_to_task(pid_t pid)
 }
 
 static uintptr_t
-search_map_for_section(proc_handle_t *handle, const char* secname, const char* substr) {
+search_map_for_section(proc_handle_t *handle, const char* secname, const char* substr,
+                       section_validator_t validator) {
     mach_vm_address_t address = 0;
     mach_vm_size_t size = 0;
     mach_msg_type_number_t count = sizeof(vm_region_basic_info_data_64_t);
@@ -542,7 +568,9 @@ search_map_for_section(proc_handle_t *handle, const char* secname, const char* s
         if (strncmp(filename, substr, strlen(substr)) == 0) {
             uintptr_t result = search_section_in_file(
                 secname, map_filename, address, size, proc_ref);
-            if (result != 0) {
+            if (result != 0
+                && (validator == NULL || validator(handle, result)))
+            {
                 return result;
             }
         }
@@ -659,7 +687,8 @@ exit:
 }
 
 static uintptr_t
-search_linux_map_for_section(proc_handle_t *handle, const char* secname, const char* substr)
+search_linux_map_for_section(proc_handle_t *handle, const char* secname, const char* substr,
+                             section_validator_t validator)
 {
     char maps_file_path[64];
     sprintf(maps_file_path, "/proc/%d/maps", handle->pid);
@@ -734,9 +763,12 @@ search_linux_map_for_section(proc_handle_t *handle, const char* secname, const c
 
         if (strstr(filename, substr)) {
             retval = search_elf_file_for_section(handle, secname, start, path);
-            if (retval) {
+            if (retval
+                && (validator == NULL || validator(handle, retval)))
+            {
                 break;
             }
+            retval = 0;
         }
     }
 
@@ -832,7 +864,8 @@ static void* analyze_pe(const wchar_t* mod_path, BYTE* remote_base, const char*
 
 
 static uintptr_t
-search_windows_map_for_section(proc_handle_t* handle, const char* secname, const wchar_t* substr) {
+search_windows_map_for_section(proc_handle_t* handle, const char* secname, const wchar_t* substr,
+                               section_validator_t validator) {
     HANDLE hProcSnap;
     do {
         hProcSnap = CreateToolhelp32Snapshot(TH32CS_SNAPMODULE, handle->pid);
@@ -855,8 +888,11 @@ search_windows_map_for_section(proc_handle_t* handle, const char* secname, const
     for (BOOL hasModule = Module32FirstW(hProcSnap, &moduleEntry); hasModule; hasModule = Module32NextW(hProcSnap, &moduleEntry)) {
         // Look for either python executable or DLL
         if (wcsstr(moduleEntry.szModule, substr)) {
-            runtime_addr = analyze_pe(moduleEntry.szExePath, moduleEntry.modBaseAddr, secname);
-            if (runtime_addr != NULL) {
+            void *candidate = analyze_pe(moduleEntry.szExePath, moduleEntry.modBaseAddr, secname);
+            if (candidate != NULL
+                && (validator == NULL || validator(handle, (uintptr_t)candidate)))
+            {
+                runtime_addr = candidate;
                 break;
             }
         }
@@ -877,7 +913,8 @@ _Py_RemoteDebug_GetPyRuntimeAddress(proc_handle_t* handle)
 
 #ifdef MS_WINDOWS
     // On Windows, search for 'python' in executable or DLL
-    address = search_windows_map_for_section(handle, "PyRuntime", L"python");
+    address = search_windows_map_for_section(handle, "PyRuntime", L"python",
+                                             _Py_RemoteDebug_ValidatePyRuntimeCookie);
     if (address == 0) {
         // Error out: 'python' substring covers both executable and DLL
         PyObject *exc = PyErr_GetRaisedException();
@@ -888,7 +925,8 @@ _Py_RemoteDebug_GetPyRuntimeAddress(proc_handle_t* handle)
     }
 #elif defined(__linux__)
     // On Linux, search for 'python' in executable or DLL
-    address = search_linux_map_for_section(handle, "PyRuntime", "python");
+    address = search_linux_map_for_section(handle, "PyRuntime", "python",
+                                           _Py_RemoteDebug_ValidatePyRuntimeCookie);
     if (address == 0) {
         // Error out: 'python' substring covers both executable and DLL
         PyObject *exc = PyErr_GetRaisedException();
@@ -902,7 +940,8 @@ _Py_RemoteDebug_GetPyRuntimeAddress(proc_handle_t* handle)
     const char* candidates[] = {"libpython", "python", "Python", NULL};
     for (const char** candidate = candidates; *candidate; candidate++) {
         PyErr_Clear();
-        address = search_map_for_section(handle, "PyRuntime", *candidate);
+        address = search_map_for_section(handle, "PyRuntime", *candidate,
+                                         _Py_RemoteDebug_ValidatePyRuntimeCookie);
         if (address != 0) {
             break;
         }