diff --git a/Lib/test/test_external_inspection.py b/Lib/test/test_external_inspection.py index a709b837161f48..dddb3839af4f07 100644 --- a/Lib/test/test_external_inspection.py +++ b/Lib/test/test_external_inspection.py @@ -150,6 +150,44 @@ def foo(): 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 index 00000000000000..023f9dce20124f --- /dev/null +++ b/Misc/NEWS.d/next/Core_and_Builtins/2026-02-08-18-13-38.gh-issue-144563.hb3kpp.rst @@ -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 diff --git a/Modules/_remote_debugging_module.c b/Modules/_remote_debugging_module.c index b46538b76df16e..dcf901bf1fec99 100644 --- a/Modules/_remote_debugging_module.c +++ b/Modules/_remote_debugging_module.c @@ -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 diff --git a/Python/remote_debug.h b/Python/remote_debug.h index ed213859a8afab..21c11789189118 100644 --- a/Python/remote_debug.h +++ b/Python/remote_debug.h @@ -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 @@ search_elf_file_for_section( } 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; }