https://github.com/python/cpython/commit/616e6118442832544fab0b93e9dd7d15411a2a58
commit: 616e6118442832544fab0b93e9dd7d15411a2a58
branch: 3.14
author: Bartosz Sławecki <[email protected]>
committer: pablogsal <[email protected]>
date: 2026-02-10T14:31:49Z
summary:
[3.14] gh-144563: Fix remote debugging with duplicate libpython mappings from
ctypes (GH-144595) (#144655)
files:
A
Misc/NEWS.d/next/Core_and_Builtins/2026-02-08-18-13-38.gh-issue-144563.hb3kpp.rst
M Lib/test/test_external_inspection.py
M Modules/_remote_debugging_module.c
M Python/remote_debug.h
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;
}
_______________________________________________
Python-checkins mailing list -- [email protected]
To unsubscribe send an email to [email protected]
https://mail.python.org/mailman3//lists/python-checkins.python.org
Member address: [email protected]