Skip to content

Commit f88816e

Browse files
committed
pythongh-144563: Fix remote debugging with duplicate libpython mappings from ctypes
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. The validation is only applied when searching for the "PyRuntime" section, not for other sections like "AsyncioDebug" which have different layouts. The fix is applied to all three platform-specific search functions: - macOS: search_map_for_section - Linux: search_linux_map_for_section - Windows: search_windows_map_for_section https://claude.ai/code/session_01PAfGNYfkqzWePF2ejbJHNo
1 parent d736349 commit f88816e

File tree

2 files changed

+78
-5
lines changed

2 files changed

+78
-5
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,

Python/remote_debug.h

Lines changed: 40 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,27 @@ 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+
// Validate that a candidate PyRuntime address points to an initialized runtime
158+
// by checking for the "xdebugpy" cookie. A duplicate mapping (e.g. from ctypes
159+
// dlopen) will have an uninitialized PyRuntime section and fail this check.
160+
static int
161+
_Py_RemoteDebug_ValidatePyRuntimeAddress(proc_handle_t *handle, uintptr_t address)
162+
{
163+
if (address == 0) {
164+
return 0;
165+
}
166+
char cookie[8];
167+
if (_Py_RemoteDebug_ReadRemoteMemory(handle, address, sizeof(cookie), cookie) != 0) {
168+
PyErr_Clear();
169+
return 0;
170+
}
171+
return memcmp(cookie, "xdebugpy", 8) == 0;
172+
}
173+
153174
static void
154175
_Py_RemoteDebug_FreePageCache(proc_handle_t *handle)
155176
{
@@ -562,7 +583,11 @@ search_map_for_section(proc_handle_t *handle, const char* secname, const char* s
562583
uintptr_t result = search_section_in_file(
563584
secname, map_filename, address, size, proc_ref);
564585
if (result != 0) {
565-
return result;
586+
if (strcmp(secname, "PyRuntime") != 0
587+
|| _Py_RemoteDebug_ValidatePyRuntimeAddress(handle, result))
588+
{
589+
return result;
590+
}
566591
}
567592
}
568593

@@ -754,7 +779,12 @@ search_linux_map_for_section(proc_handle_t *handle, const char* secname, const c
754779
if (strstr(filename, substr)) {
755780
retval = search_elf_file_for_section(handle, secname, start, path);
756781
if (retval) {
757-
break;
782+
if (strcmp(secname, "PyRuntime") != 0
783+
|| _Py_RemoteDebug_ValidatePyRuntimeAddress(handle, retval))
784+
{
785+
break;
786+
}
787+
retval = 0;
758788
}
759789
}
760790
}
@@ -882,9 +912,14 @@ search_windows_map_for_section(proc_handle_t* handle, const char* secname, const
882912
for (BOOL hasModule = Module32FirstW(hProcSnap, &moduleEntry); hasModule; hasModule = Module32NextW(hProcSnap, &moduleEntry)) {
883913
// Look for either python executable or DLL
884914
if (wcsstr(moduleEntry.szModule, substr)) {
885-
runtime_addr = analyze_pe(moduleEntry.szExePath, moduleEntry.modBaseAddr, secname);
886-
if (runtime_addr != NULL) {
887-
break;
915+
void *candidate = analyze_pe(moduleEntry.szExePath, moduleEntry.modBaseAddr, secname);
916+
if (candidate != NULL) {
917+
if (strcmp(secname, "PyRuntime") != 0
918+
|| _Py_RemoteDebug_ValidatePyRuntimeAddress(handle, (uintptr_t)candidate))
919+
{
920+
runtime_addr = candidate;
921+
break;
922+
}
888923
}
889924
}
890925
}

0 commit comments

Comments
 (0)