Skip to content

[3.14] gh-134876: Add fallback for when process_vm_readv fails with ENOSYS (GH-134878) #135240

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Jun 7, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Misc/ACKS
Original file line number Diff line number Diff line change
Expand Up @@ -658,6 +658,7 @@ Michael Goderbauer
Karan Goel
Jeroen Van Goey
Christoph Gohlke
Daniel Golding
Tim Golden
Yonatan Goldschmidt
Mark Gollahon
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Add support to :pep:`768` remote debugging for Linux kernels which don't
have CONFIG_CROSS_MEMORY_ATTACH configured.
70 changes: 70 additions & 0 deletions Python/remote_debug.h
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,8 @@ typedef struct {
mach_port_t task;
#elif defined(MS_WINDOWS)
HANDLE hProcess;
#elif defined(__linux__)
int memfd;
#endif
page_cache_entry_t pages[MAX_PAGES];
Py_ssize_t page_size;
Expand Down Expand Up @@ -162,6 +164,8 @@ _Py_RemoteDebug_InitProcHandle(proc_handle_t *handle, pid_t pid) {
_set_debug_exception_cause(PyExc_RuntimeError, "Failed to initialize Windows process handle");
return -1;
}
#elif defined(__linux__)
handle->memfd = -1;
#endif
handle->page_size = get_page_size();
for (int i = 0; i < MAX_PAGES; i++) {
Expand All @@ -179,6 +183,11 @@ _Py_RemoteDebug_CleanupProcHandle(proc_handle_t *handle) {
CloseHandle(handle->hProcess);
handle->hProcess = NULL;
}
#elif defined(__linux__)
if (handle->memfd != -1) {
close(handle->memfd);
handle->memfd = -1;
}
#endif
handle->pid = 0;
_Py_RemoteDebug_FreePageCache(handle);
Expand Down Expand Up @@ -907,6 +916,61 @@ _Py_RemoteDebug_GetPyRuntimeAddress(proc_handle_t* handle)
return address;
}

#if defined(__linux__) && HAVE_PROCESS_VM_READV

static int
open_proc_mem_fd(proc_handle_t *handle)
{
char mem_file_path[64];
sprintf(mem_file_path, "/proc/%d/mem", handle->pid);

handle->memfd = open(mem_file_path, O_RDWR);
if (handle->memfd == -1) {
PyErr_SetFromErrno(PyExc_OSError);
_set_debug_exception_cause(PyExc_OSError,
"failed to open file %s: %s", mem_file_path, strerror(errno));
return -1;
}
return 0;
}

// Why is pwritev not guarded? Except on Android API level 23 (no longer
// supported), HAVE_PROCESS_VM_READV is sufficient.
static int
read_remote_memory_fallback(proc_handle_t *handle, uintptr_t remote_address, size_t len, void* dst)
{
if (handle->memfd == -1) {
if (open_proc_mem_fd(handle) < 0) {
return -1;
}
}

struct iovec local[1];
Py_ssize_t result = 0;
Py_ssize_t read_bytes = 0;

do {
local[0].iov_base = (char*)dst + result;
local[0].iov_len = len - result;
off_t offset = remote_address + result;

read_bytes = preadv(handle->memfd, local, 1, offset);
if (read_bytes < 0) {
PyErr_SetFromErrno(PyExc_OSError);
_set_debug_exception_cause(PyExc_OSError,
"preadv failed for PID %d at address 0x%lx "
"(size %zu, partial read %zd bytes): %s",
handle->pid, remote_address + result, len - result, result, strerror(errno));
return -1;
}

result += read_bytes;
} while ((size_t)read_bytes != local[0].iov_len);
return 0;
}

#endif // __linux__

// Platform-independent memory read function
static int
_Py_RemoteDebug_ReadRemoteMemory(proc_handle_t *handle, uintptr_t remote_address, size_t len, void* dst)
Expand All @@ -928,6 +992,9 @@ _Py_RemoteDebug_ReadRemoteMemory(proc_handle_t *handle, uintptr_t remote_address
} while (result < len);
return 0;
#elif defined(__linux__) && HAVE_PROCESS_VM_READV
if (handle->memfd != -1) {
return read_remote_memory_fallback(handle, remote_address, len, dst);
}
struct iovec local[1];
struct iovec remote[1];
Py_ssize_t result = 0;
Expand All @@ -941,6 +1008,9 @@ _Py_RemoteDebug_ReadRemoteMemory(proc_handle_t *handle, uintptr_t remote_address

read_bytes = process_vm_readv(handle->pid, local, 1, remote, 1, 0);
if (read_bytes < 0) {
if (errno == ENOSYS) {
return read_remote_memory_fallback(handle, remote_address, len, dst);
}
PyErr_SetFromErrno(PyExc_OSError);
_set_debug_exception_cause(PyExc_OSError,
"process_vm_readv failed for PID %d at address 0x%lx "
Expand Down
39 changes: 39 additions & 0 deletions Python/remote_debugging.c
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,39 @@ read_memory(proc_handle_t *handle, uint64_t remote_address, size_t len, void* ds
return _Py_RemoteDebug_ReadRemoteMemory(handle, remote_address, len, dst);
}

// Why is pwritev not guarded? Except on Android API level 23 (no longer
// supported), HAVE_PROCESS_VM_READV is sufficient.
#if defined(__linux__) && HAVE_PROCESS_VM_READV
static int
write_memory_fallback(proc_handle_t *handle, uintptr_t remote_address, size_t len, const void* src)
{
if (handle->memfd == -1) {
if (open_proc_mem_fd(handle) < 0) {
return -1;
}
}

struct iovec local[1];
Py_ssize_t result = 0;
Py_ssize_t written = 0;

do {
local[0].iov_base = (char*)src + result;
local[0].iov_len = len - result;
off_t offset = remote_address + result;

written = pwritev(handle->memfd, local, 1, offset);
if (written < 0) {
PyErr_SetFromErrno(PyExc_OSError);
return -1;
}

result += written;
} while ((size_t)written != local[0].iov_len);
return 0;
}
#endif // __linux__

static int
write_memory(proc_handle_t *handle, uintptr_t remote_address, size_t len, const void* src)
{
Expand All @@ -39,6 +72,9 @@ write_memory(proc_handle_t *handle, uintptr_t remote_address, size_t len, const
} while (result < len);
return 0;
#elif defined(__linux__) && HAVE_PROCESS_VM_READV
if (handle->memfd != -1) {
return write_memory_fallback(handle, remote_address, len, src);
}
struct iovec local[1];
struct iovec remote[1];
Py_ssize_t result = 0;
Expand All @@ -52,6 +88,9 @@ write_memory(proc_handle_t *handle, uintptr_t remote_address, size_t len, const

written = process_vm_writev(handle->pid, local, 1, remote, 1, 0);
if (written < 0) {
if (errno == ENOSYS) {
return write_memory_fallback(handle, remote_address, len, src);
}
PyErr_SetFromErrno(PyExc_OSError);
return -1;
}
Expand Down
Loading