Skip to content

Commit 616e611

Browse files
authored
[3.14] gh-144563: Fix remote debugging with duplicate libpython mappings from ctypes (GH-144595) (#144655)
1 parent 8b4210c commit 616e611

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)