Skip to content

Commit e2ab90b

Browse files
pablogsaljohnslavik
authored andcommitted
[3.14] gh-144563: Fix remote debugging with duplicate libpython mappings from ctypes (GH-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. (cherry picked from commit 2c1ca6b) Co-authored-by: Pablo Galindo Salgado <Pablogsal@gmail.com>
1 parent 3fc48c1 commit e2ab90b

File tree

4 files changed

+95
-14
lines changed

4 files changed

+95
-14
lines changed

Lib/test/test_external_inspection.py

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,44 @@ def foo():
150150
else:
151151
self.fail("Main thread stack trace not found in result")
152152

153+
@skip_if_not_supported
154+
@unittest.skipIf(
155+
sys.platform == "linux" and not PROCESS_VM_READV_SUPPORTED,
156+
"Test only runs on Linux with process_vm_readv support",
157+
)
158+
def test_self_trace_after_ctypes_import(self):
159+
"""Test that RemoteUnwinder works on the same process after _ctypes import.
160+
161+
When _ctypes is imported, it may call dlopen on the libpython shared
162+
library, creating a duplicate mapping in the process address space.
163+
The remote debugging code must skip these uninitialized duplicate
164+
mappings and find the real PyRuntime. See gh-144563.
165+
"""
166+
# Run the test in a subprocess to avoid side effects
167+
script = textwrap.dedent("""\
168+
import os
169+
import _remote_debugging
170+
171+
# Should work before _ctypes import
172+
unwinder = _remote_debugging.RemoteUnwinder(os.getpid())
173+
174+
import _ctypes
175+
176+
# Should still work after _ctypes import (gh-144563)
177+
unwinder = _remote_debugging.RemoteUnwinder(os.getpid())
178+
""")
179+
180+
result = subprocess.run(
181+
[sys.executable, "-c", script],
182+
capture_output=True,
183+
text=True,
184+
timeout=SHORT_TIMEOUT,
185+
)
186+
self.assertEqual(
187+
result.returncode, 0,
188+
f"stdout: {result.stdout}\nstderr: {result.stderr}"
189+
)
190+
153191
@skip_if_not_supported
154192
@unittest.skipIf(
155193
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_module.c

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

806806
#ifdef MS_WINDOWS
807807
// On Windows, search for asyncio debug in executable or DLL
808-
address = search_windows_map_for_section(handle, "AsyncioD", L"_asyncio");
808+
address = search_windows_map_for_section(handle, "AsyncioD", L"_asyncio", NULL);
809809
if (address == 0) {
810810
// Error out: 'python' substring covers both executable and DLL
811811
PyObject *exc = PyErr_GetRaisedException();
@@ -814,7 +814,7 @@ _Py_RemoteDebug_GetAsyncioDebugAddress(proc_handle_t* handle)
814814
}
815815
#elif defined(__linux__)
816816
// On Linux, search for asyncio debug in executable or DLL
817-
address = search_linux_map_for_section(handle, "AsyncioDebug", "_asyncio.cpython");
817+
address = search_linux_map_for_section(handle, "AsyncioDebug", "_asyncio.cpython", NULL);
818818
if (address == 0) {
819819
// Error out: 'python' substring covers both executable and DLL
820820
PyObject *exc = PyErr_GetRaisedException();
@@ -823,10 +823,10 @@ _Py_RemoteDebug_GetAsyncioDebugAddress(proc_handle_t* handle)
823823
}
824824
#elif defined(__APPLE__) && TARGET_OS_OSX
825825
// On macOS, try libpython first, then fall back to python
826-
address = search_map_for_section(handle, "AsyncioDebug", "_asyncio.cpython");
826+
address = search_map_for_section(handle, "AsyncioDebug", "_asyncio.cpython", NULL);
827827
if (address == 0) {
828828
PyErr_Clear();
829-
address = search_map_for_section(handle, "AsyncioDebug", "_asyncio.cpython");
829+
address = search_map_for_section(handle, "AsyncioDebug", "_asyncio.cpython", NULL);
830830
}
831831
if (address == 0) {
832832
// 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
@@ -133,6 +133,31 @@ typedef struct {
133133
Py_ssize_t page_size;
134134
} proc_handle_t;
135135

136+
// Forward declaration for use in validation function
137+
static int
138+
_Py_RemoteDebug_ReadRemoteMemory(proc_handle_t *handle, uintptr_t remote_address, size_t len, void* dst);
139+
140+
// Optional callback to validate a candidate section address found during
141+
// memory map searches. Returns 1 if the address is valid, 0 to skip it.
142+
// This allows callers to filter out duplicate/stale mappings (e.g. from
143+
// ctypes dlopen) whose sections were never initialized.
144+
typedef int (*section_validator_t)(proc_handle_t *handle, uintptr_t address);
145+
146+
// Validate that a candidate address starts with _Py_Debug_Cookie.
147+
static int
148+
_Py_RemoteDebug_ValidatePyRuntimeCookie(proc_handle_t *handle, uintptr_t address)
149+
{
150+
if (address == 0) {
151+
return 0;
152+
}
153+
char buf[sizeof(_Py_Debug_Cookie) - 1];
154+
if (_Py_RemoteDebug_ReadRemoteMemory(handle, address, sizeof(buf), buf) != 0) {
155+
PyErr_Clear();
156+
return 0;
157+
}
158+
return memcmp(buf, _Py_Debug_Cookie, sizeof(buf)) == 0;
159+
}
160+
136161
static void
137162
_Py_RemoteDebug_FreePageCache(proc_handle_t *handle)
138163
{
@@ -490,7 +515,8 @@ pid_to_task(pid_t pid)
490515
}
491516

492517
static uintptr_t
493-
search_map_for_section(proc_handle_t *handle, const char* secname, const char* substr) {
518+
search_map_for_section(proc_handle_t *handle, const char* secname, const char* substr,
519+
section_validator_t validator) {
494520
mach_vm_address_t address = 0;
495521
mach_vm_size_t size = 0;
496522
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
542568
if (strncmp(filename, substr, strlen(substr)) == 0) {
543569
uintptr_t result = search_section_in_file(
544570
secname, map_filename, address, size, proc_ref);
545-
if (result != 0) {
571+
if (result != 0
572+
&& (validator == NULL || validator(handle, result)))
573+
{
546574
return result;
547575
}
548576
}
@@ -659,7 +687,8 @@ search_elf_file_for_section(
659687
}
660688

661689
static uintptr_t
662-
search_linux_map_for_section(proc_handle_t *handle, const char* secname, const char* substr)
690+
search_linux_map_for_section(proc_handle_t *handle, const char* secname, const char* substr,
691+
section_validator_t validator)
663692
{
664693
char maps_file_path[64];
665694
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
734763

735764
if (strstr(filename, substr)) {
736765
retval = search_elf_file_for_section(handle, secname, start, path);
737-
if (retval) {
766+
if (retval
767+
&& (validator == NULL || validator(handle, retval)))
768+
{
738769
break;
739770
}
771+
retval = 0;
740772
}
741773
}
742774

@@ -832,7 +864,8 @@ static void* analyze_pe(const wchar_t* mod_path, BYTE* remote_base, const char*
832864

833865

834866
static uintptr_t
835-
search_windows_map_for_section(proc_handle_t* handle, const char* secname, const wchar_t* substr) {
867+
search_windows_map_for_section(proc_handle_t* handle, const char* secname, const wchar_t* substr,
868+
section_validator_t validator) {
836869
HANDLE hProcSnap;
837870
do {
838871
hProcSnap = CreateToolhelp32Snapshot(TH32CS_SNAPMODULE, handle->pid);
@@ -855,8 +888,11 @@ search_windows_map_for_section(proc_handle_t* handle, const char* secname, const
855888
for (BOOL hasModule = Module32FirstW(hProcSnap, &moduleEntry); hasModule; hasModule = Module32NextW(hProcSnap, &moduleEntry)) {
856889
// Look for either python executable or DLL
857890
if (wcsstr(moduleEntry.szModule, substr)) {
858-
runtime_addr = analyze_pe(moduleEntry.szExePath, moduleEntry.modBaseAddr, secname);
859-
if (runtime_addr != NULL) {
891+
void *candidate = analyze_pe(moduleEntry.szExePath, moduleEntry.modBaseAddr, secname);
892+
if (candidate != NULL
893+
&& (validator == NULL || validator(handle, (uintptr_t)candidate)))
894+
{
895+
runtime_addr = candidate;
860896
break;
861897
}
862898
}
@@ -877,7 +913,8 @@ _Py_RemoteDebug_GetPyRuntimeAddress(proc_handle_t* handle)
877913

878914
#ifdef MS_WINDOWS
879915
// On Windows, search for 'python' in executable or DLL
880-
address = search_windows_map_for_section(handle, "PyRuntime", L"python");
916+
address = search_windows_map_for_section(handle, "PyRuntime", L"python",
917+
_Py_RemoteDebug_ValidatePyRuntimeCookie);
881918
if (address == 0) {
882919
// Error out: 'python' substring covers both executable and DLL
883920
PyObject *exc = PyErr_GetRaisedException();
@@ -888,7 +925,8 @@ _Py_RemoteDebug_GetPyRuntimeAddress(proc_handle_t* handle)
888925
}
889926
#elif defined(__linux__)
890927
// On Linux, search for 'python' in executable or DLL
891-
address = search_linux_map_for_section(handle, "PyRuntime", "python");
928+
address = search_linux_map_for_section(handle, "PyRuntime", "python",
929+
_Py_RemoteDebug_ValidatePyRuntimeCookie);
892930
if (address == 0) {
893931
// Error out: 'python' substring covers both executable and DLL
894932
PyObject *exc = PyErr_GetRaisedException();
@@ -902,7 +940,8 @@ _Py_RemoteDebug_GetPyRuntimeAddress(proc_handle_t* handle)
902940
const char* candidates[] = {"libpython", "python", "Python", NULL};
903941
for (const char** candidate = candidates; *candidate; candidate++) {
904942
PyErr_Clear();
905-
address = search_map_for_section(handle, "PyRuntime", *candidate);
943+
address = search_map_for_section(handle, "PyRuntime", *candidate,
944+
_Py_RemoteDebug_ValidatePyRuntimeCookie);
906945
if (address != 0) {
907946
break;
908947
}

0 commit comments

Comments
 (0)