Skip to content

Commit 2c1ca6b

Browse files
authored
gh-144563: Fix remote debugging with duplicate libpython mappings from ctypes (#144595)
When _ctypes is imported, it may call dlopen on the libpython shared library, causing the dynamic linker to load a second mapping of the library into the process address space. The remote debugging code iterates memory regions from low addresses upward and returns the first mapping whose filename matches libpython. After _ctypes is imported, it finds the dlopen'd copy first, but that copy's PyRuntime section was never initialized, so reading debug offsets from it fails. Fix this by validating each candidate PyRuntime address before accepting it. The validation reads the first 8 bytes and checks for the "xdebugpy" cookie that is only present in an initialized PyRuntime. Uninitialized duplicate mappings will fail this check and be skipped, allowing the search to continue to the real, initialized PyRuntime.
1 parent d2d2459 commit 2c1ca6b

File tree

4 files changed

+99
-14
lines changed

4 files changed

+99
-14
lines changed

Lib/test/test_external_inspection.py

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -516,6 +516,44 @@ def foo():
516516
finally:
517517
_cleanup_sockets(client_socket, server_socket)
518518

519+
@skip_if_not_supported
520+
@unittest.skipIf(
521+
sys.platform == "linux" and not PROCESS_VM_READV_SUPPORTED,
522+
"Test only runs on Linux with process_vm_readv support",
523+
)
524+
def test_self_trace_after_ctypes_import(self):
525+
"""Test that RemoteUnwinder works on the same process after _ctypes import.
526+
527+
When _ctypes is imported, it may call dlopen on the libpython shared
528+
library, creating a duplicate mapping in the process address space.
529+
The remote debugging code must skip these uninitialized duplicate
530+
mappings and find the real PyRuntime. See gh-144563.
531+
"""
532+
# Run the test in a subprocess to avoid side effects
533+
script = textwrap.dedent("""\
534+
import os
535+
import _remote_debugging
536+
537+
# Should work before _ctypes import
538+
unwinder = _remote_debugging.RemoteUnwinder(os.getpid())
539+
540+
import _ctypes
541+
542+
# Should still work after _ctypes import (gh-144563)
543+
unwinder = _remote_debugging.RemoteUnwinder(os.getpid())
544+
""")
545+
546+
result = subprocess.run(
547+
[sys.executable, "-c", script],
548+
capture_output=True,
549+
text=True,
550+
timeout=SHORT_TIMEOUT,
551+
)
552+
self.assertEqual(
553+
result.returncode, 0,
554+
f"stdout: {result.stdout}\nstderr: {result.stderr}"
555+
)
556+
519557
@skip_if_not_supported
520558
@unittest.skipIf(
521559
sys.platform == "linux" and not PROCESS_VM_READV_SUPPORTED,
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
Fix interaction of the Tachyon profiler and :mod:`ctypes` and other modules
2+
that load the Python shared library (if present) in an independent map as
3+
this was causing the mechanism that loads the binary information to be
4+
confused. Patch by Pablo Galindo

Modules/_remote_debugging/asyncio.c

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,8 @@ _Py_RemoteDebug_GetAsyncioDebugAddress(proc_handle_t* handle)
1818

1919
#ifdef MS_WINDOWS
2020
// On Windows, search for asyncio debug in executable or DLL
21-
address = search_windows_map_for_section(handle, "AsyncioD", L"_asyncio");
21+
address = search_windows_map_for_section(handle, "AsyncioD", L"_asyncio",
22+
NULL);
2223
if (address == 0) {
2324
// Error out: 'python' substring covers both executable and DLL
2425
PyObject *exc = PyErr_GetRaisedException();
@@ -27,7 +28,8 @@ _Py_RemoteDebug_GetAsyncioDebugAddress(proc_handle_t* handle)
2728
}
2829
#elif defined(__linux__) && HAVE_PROCESS_VM_READV
2930
// On Linux, search for asyncio debug in executable or DLL
30-
address = search_linux_map_for_section(handle, "AsyncioDebug", "python");
31+
address = search_linux_map_for_section(handle, "AsyncioDebug", "python",
32+
NULL);
3133
if (address == 0) {
3234
// Error out: 'python' substring covers both executable and DLL
3335
PyObject *exc = PyErr_GetRaisedException();
@@ -36,10 +38,12 @@ _Py_RemoteDebug_GetAsyncioDebugAddress(proc_handle_t* handle)
3638
}
3739
#elif defined(__APPLE__) && TARGET_OS_OSX
3840
// On macOS, try libpython first, then fall back to python
39-
address = search_map_for_section(handle, "AsyncioDebug", "libpython");
41+
address = search_map_for_section(handle, "AsyncioDebug", "libpython",
42+
NULL);
4043
if (address == 0) {
4144
PyErr_Clear();
42-
address = search_map_for_section(handle, "AsyncioDebug", "python");
45+
address = search_map_for_section(handle, "AsyncioDebug", "python",
46+
NULL);
4347
}
4448
if (address == 0) {
4549
// Error out: 'python' substring covers both executable and DLL

Python/remote_debug.h

Lines changed: 49 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,31 @@ typedef struct {
150150
Py_ssize_t page_size;
151151
} proc_handle_t;
152152

153+
// Forward declaration for use in validation function
154+
static int
155+
_Py_RemoteDebug_ReadRemoteMemory(proc_handle_t *handle, uintptr_t remote_address, size_t len, void* dst);
156+
157+
// Optional callback to validate a candidate section address found during
158+
// memory map searches. Returns 1 if the address is valid, 0 to skip it.
159+
// This allows callers to filter out duplicate/stale mappings (e.g. from
160+
// ctypes dlopen) whose sections were never initialized.
161+
typedef int (*section_validator_t)(proc_handle_t *handle, uintptr_t address);
162+
163+
// Validate that a candidate address starts with _Py_Debug_Cookie.
164+
static int
165+
_Py_RemoteDebug_ValidatePyRuntimeCookie(proc_handle_t *handle, uintptr_t address)
166+
{
167+
if (address == 0) {
168+
return 0;
169+
}
170+
char buf[sizeof(_Py_Debug_Cookie) - 1];
171+
if (_Py_RemoteDebug_ReadRemoteMemory(handle, address, sizeof(buf), buf) != 0) {
172+
PyErr_Clear();
173+
return 0;
174+
}
175+
return memcmp(buf, _Py_Debug_Cookie, sizeof(buf)) == 0;
176+
}
177+
153178
static void
154179
_Py_RemoteDebug_FreePageCache(proc_handle_t *handle)
155180
{
@@ -509,7 +534,8 @@ pid_to_task(pid_t pid)
509534
}
510535

511536
static uintptr_t
512-
search_map_for_section(proc_handle_t *handle, const char* secname, const char* substr) {
537+
search_map_for_section(proc_handle_t *handle, const char* secname, const char* substr,
538+
section_validator_t validator) {
513539
mach_vm_address_t address = 0;
514540
mach_vm_size_t size = 0;
515541
mach_msg_type_number_t count = sizeof(vm_region_basic_info_data_64_t);
@@ -561,7 +587,9 @@ search_map_for_section(proc_handle_t *handle, const char* secname, const char* s
561587
if (strncmp(filename, substr, strlen(substr)) == 0) {
562588
uintptr_t result = search_section_in_file(
563589
secname, map_filename, address, size, proc_ref);
564-
if (result != 0) {
590+
if (result != 0
591+
&& (validator == NULL || validator(handle, result)))
592+
{
565593
return result;
566594
}
567595
}
@@ -678,7 +706,8 @@ search_elf_file_for_section(
678706
}
679707

680708
static uintptr_t
681-
search_linux_map_for_section(proc_handle_t *handle, const char* secname, const char* substr)
709+
search_linux_map_for_section(proc_handle_t *handle, const char* secname, const char* substr,
710+
section_validator_t validator)
682711
{
683712
char maps_file_path[64];
684713
sprintf(maps_file_path, "/proc/%d/maps", handle->pid);
@@ -753,9 +782,12 @@ search_linux_map_for_section(proc_handle_t *handle, const char* secname, const c
753782

754783
if (strstr(filename, substr)) {
755784
retval = search_elf_file_for_section(handle, secname, start, path);
756-
if (retval) {
785+
if (retval
786+
&& (validator == NULL || validator(handle, retval)))
787+
{
757788
break;
758789
}
790+
retval = 0;
759791
}
760792
}
761793

@@ -859,7 +891,8 @@ static void* analyze_pe(const wchar_t* mod_path, BYTE* remote_base, const char*
859891

860892

861893
static uintptr_t
862-
search_windows_map_for_section(proc_handle_t* handle, const char* secname, const wchar_t* substr) {
894+
search_windows_map_for_section(proc_handle_t* handle, const char* secname, const wchar_t* substr,
895+
section_validator_t validator) {
863896
HANDLE hProcSnap;
864897
do {
865898
hProcSnap = CreateToolhelp32Snapshot(TH32CS_SNAPMODULE, handle->pid);
@@ -882,8 +915,11 @@ search_windows_map_for_section(proc_handle_t* handle, const char* secname, const
882915
for (BOOL hasModule = Module32FirstW(hProcSnap, &moduleEntry); hasModule; hasModule = Module32NextW(hProcSnap, &moduleEntry)) {
883916
// Look for either python executable or DLL
884917
if (wcsstr(moduleEntry.szModule, substr)) {
885-
runtime_addr = analyze_pe(moduleEntry.szExePath, moduleEntry.modBaseAddr, secname);
886-
if (runtime_addr != NULL) {
918+
void *candidate = analyze_pe(moduleEntry.szExePath, moduleEntry.modBaseAddr, secname);
919+
if (candidate != NULL
920+
&& (validator == NULL || validator(handle, (uintptr_t)candidate)))
921+
{
922+
runtime_addr = candidate;
887923
break;
888924
}
889925
}
@@ -904,7 +940,8 @@ _Py_RemoteDebug_GetPyRuntimeAddress(proc_handle_t* handle)
904940

905941
#ifdef MS_WINDOWS
906942
// On Windows, search for 'python' in executable or DLL
907-
address = search_windows_map_for_section(handle, "PyRuntime", L"python");
943+
address = search_windows_map_for_section(handle, "PyRuntime", L"python",
944+
_Py_RemoteDebug_ValidatePyRuntimeCookie);
908945
if (address == 0) {
909946
// Error out: 'python' substring covers both executable and DLL
910947
PyObject *exc = PyErr_GetRaisedException();
@@ -915,7 +952,8 @@ _Py_RemoteDebug_GetPyRuntimeAddress(proc_handle_t* handle)
915952
}
916953
#elif defined(__linux__) && HAVE_PROCESS_VM_READV
917954
// On Linux, search for 'python' in executable or DLL
918-
address = search_linux_map_for_section(handle, "PyRuntime", "python");
955+
address = search_linux_map_for_section(handle, "PyRuntime", "python",
956+
_Py_RemoteDebug_ValidatePyRuntimeCookie);
919957
if (address == 0) {
920958
// Error out: 'python' substring covers both executable and DLL
921959
PyObject *exc = PyErr_GetRaisedException();
@@ -929,7 +967,8 @@ _Py_RemoteDebug_GetPyRuntimeAddress(proc_handle_t* handle)
929967
const char* candidates[] = {"libpython", "python", "Python", NULL};
930968
for (const char** candidate = candidates; *candidate; candidate++) {
931969
PyErr_Clear();
932-
address = search_map_for_section(handle, "PyRuntime", *candidate);
970+
address = search_map_for_section(handle, "PyRuntime", *candidate,
971+
_Py_RemoteDebug_ValidatePyRuntimeCookie);
933972
if (address != 0) {
934973
break;
935974
}

0 commit comments

Comments
 (0)