gh-131591: Implement PEP 768 (#131937)

Co-authored-by: Ivona Stojanovic <stojanovic.i@hotmail.com>
Co-authored-by: Matt Wozniski <godlygeek@gmail.com>
This commit is contained in:
Pablo Galindo Salgado 2025-04-03 16:20:01 +01:00 committed by GitHub
parent 275056a7fd
commit 943cc1431e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
31 changed files with 1796 additions and 2 deletions

View file

@ -1192,6 +1192,71 @@ _PyEval_DisableGIL(PyThreadState *tstate)
}
#endif
#if defined(Py_REMOTE_DEBUG) && defined(Py_SUPPORTS_REMOTE_DEBUG)
// Note that this function is inline to avoid creating a PLT entry
// that would be an easy target for a ROP gadget.
static inline void run_remote_debugger_script(const char *path)
{
if (0 != PySys_Audit("remote_debugger_script", "s", path)) {
PyErr_FormatUnraisable(
"Audit hook failed for remote debugger script %s", path);
return;
}
// Open the debugger script with the open code hook, and reopen the
// resulting file object to get a C FILE* object.
PyObject* fileobj = PyFile_OpenCode(path);
if (!fileobj) {
PyErr_FormatUnraisable("Can't open debugger script %s", path);
return;
}
int fd = PyObject_AsFileDescriptor(fileobj);
if (fd == -1) {
PyErr_FormatUnraisable("Can't find fd for debugger script %s", path);
}
else {
int dup_fd = -1;
FILE *f = NULL;
#ifdef MS_WINDOWS
dup_fd = _dup(fd);
if (dup_fd != -1) {
f = _fdopen(dup_fd, "r");
}
if (!f) {
_close(dup_fd);
}
#else
dup_fd = dup(fd);
if (dup_fd != -1) {
f = fdopen(dup_fd, "r");
}
if (!f) {
close(dup_fd);
}
#endif
if (!f) {
PyErr_SetFromErrno(PyExc_OSError);
}
else {
PyRun_AnyFileEx(f, path, 1);
}
if (PyErr_Occurred()) {
PyErr_FormatUnraisable("Error executing debugger script %s", path);
}
}
PyObject* res = PyObject_CallMethodNoArgs(fileobj, &_Py_ID(close));
if (!res) {
PyErr_FormatUnraisable("Error closing debugger script %s", path);
} else {
Py_DECREF(res);
}
Py_DECREF(fileobj);
}
#endif
/* Do periodic things, like check for signals and async I/0.
* We need to do reasonably frequently, but not too frequently.
@ -1319,5 +1384,35 @@ _Py_HandlePending(PyThreadState *tstate)
return -1;
}
}
#if defined(Py_REMOTE_DEBUG) && defined(Py_SUPPORTS_REMOTE_DEBUG)
const PyConfig *config = _PyInterpreterState_GetConfig(tstate->interp);
if (config->remote_debug == 1
&& tstate->remote_debugger_support.debugger_pending_call == 1)
{
tstate->remote_debugger_support.debugger_pending_call = 0;
// Immediately make a copy in case of a race with another debugger
// process that's trying to write to the buffer. At least this way
// we'll be internally consistent: what we audit is what we run.
const size_t pathsz
= sizeof(tstate->remote_debugger_support.debugger_script_path);
char *path = PyMem_Malloc(pathsz);
if (path) {
// And don't assume the debugger correctly null terminated it.
memcpy(
path,
tstate->remote_debugger_support.debugger_script_path,
pathsz);
path[pathsz - 1] = '\0';
if (*path) {
run_remote_debugger_script(path);
}
PyMem_Free(path);
}
}
#endif
return 0;
}

View file

@ -1519,6 +1519,104 @@ sys_is_stack_trampoline_active(PyObject *module, PyObject *Py_UNUSED(ignored))
return sys_is_stack_trampoline_active_impl(module);
}
PyDoc_STRVAR(sys_is_remote_debug_enabled__doc__,
"is_remote_debug_enabled($module, /)\n"
"--\n"
"\n"
"Return True if remote debugging is enabled, False otherwise.");
#define SYS_IS_REMOTE_DEBUG_ENABLED_METHODDEF \
{"is_remote_debug_enabled", (PyCFunction)sys_is_remote_debug_enabled, METH_NOARGS, sys_is_remote_debug_enabled__doc__},
static PyObject *
sys_is_remote_debug_enabled_impl(PyObject *module);
static PyObject *
sys_is_remote_debug_enabled(PyObject *module, PyObject *Py_UNUSED(ignored))
{
return sys_is_remote_debug_enabled_impl(module);
}
PyDoc_STRVAR(sys_remote_exec__doc__,
"remote_exec($module, /, pid, script)\n"
"--\n"
"\n"
"Executes a file containing Python code in a given remote Python process.\n"
"\n"
"This function returns immediately, and the code will be executed by the\n"
"target process\'s main thread at the next available opportunity, similarly\n"
"to how signals are handled. There is no interface to determine when the\n"
"code has been executed. The caller is responsible for making sure that\n"
"the file still exists whenever the remote process tries to read it and that\n"
"it hasn\'t been overwritten.\n"
"\n"
"The remote process must be running a CPython interpreter of the same major\n"
"and minor version as the local process. If either the local or remote\n"
"interpreter is pre-release (alpha, beta, or release candidate) then the\n"
"local and remote interpreters must be the same exact version.\n"
"\n"
"Args:\n"
" pid (int): The process ID of the target Python process.\n"
" script (str|bytes): The path to a file containing\n"
" the Python code to be executed.");
#define SYS_REMOTE_EXEC_METHODDEF \
{"remote_exec", _PyCFunction_CAST(sys_remote_exec), METH_FASTCALL|METH_KEYWORDS, sys_remote_exec__doc__},
static PyObject *
sys_remote_exec_impl(PyObject *module, int pid, PyObject *script);
static PyObject *
sys_remote_exec(PyObject *module, PyObject *const *args, Py_ssize_t nargs, PyObject *kwnames)
{
PyObject *return_value = NULL;
#if defined(Py_BUILD_CORE) && !defined(Py_BUILD_CORE_MODULE)
#define NUM_KEYWORDS 2
static struct {
PyGC_Head _this_is_not_used;
PyObject_VAR_HEAD
Py_hash_t ob_hash;
PyObject *ob_item[NUM_KEYWORDS];
} _kwtuple = {
.ob_base = PyVarObject_HEAD_INIT(&PyTuple_Type, NUM_KEYWORDS)
.ob_hash = -1,
.ob_item = { &_Py_ID(pid), &_Py_ID(script), },
};
#undef NUM_KEYWORDS
#define KWTUPLE (&_kwtuple.ob_base.ob_base)
#else // !Py_BUILD_CORE
# define KWTUPLE NULL
#endif // !Py_BUILD_CORE
static const char * const _keywords[] = {"pid", "script", NULL};
static _PyArg_Parser _parser = {
.keywords = _keywords,
.fname = "remote_exec",
.kwtuple = KWTUPLE,
};
#undef KWTUPLE
PyObject *argsbuf[2];
int pid;
PyObject *script;
args = _PyArg_UnpackKeywords(args, nargs, NULL, kwnames, &_parser,
/*minpos*/ 2, /*maxpos*/ 2, /*minkw*/ 0, /*varpos*/ 0, argsbuf);
if (!args) {
goto exit;
}
pid = PyLong_AsInt(args[0]);
if (pid == -1 && PyErr_Occurred()) {
goto exit;
}
script = args[1];
return_value = sys_remote_exec_impl(module, pid, script);
exit:
return return_value;
}
PyDoc_STRVAR(sys__dump_tracelets__doc__,
"_dump_tracelets($module, /, outpath)\n"
"--\n"
@ -1766,4 +1864,4 @@ exit:
#ifndef SYS_GETANDROIDAPILEVEL_METHODDEF
#define SYS_GETANDROIDAPILEVEL_METHODDEF
#endif /* !defined(SYS_GETANDROIDAPILEVEL_METHODDEF) */
/*[clinic end generated code: output=75e202eec4450f50 input=a9049054013a1b77]*/
/*[clinic end generated code: output=1aca52cefbeb800f input=a9049054013a1b77]*/

View file

@ -162,6 +162,7 @@ static const PyConfigSpec PYCONFIG_SPEC[] = {
SPEC(parse_argv, BOOL, READ_ONLY, NO_SYS),
SPEC(pathconfig_warnings, BOOL, READ_ONLY, NO_SYS),
SPEC(perf_profiling, UINT, READ_ONLY, NO_SYS),
SPEC(remote_debug, BOOL, READ_ONLY, NO_SYS),
SPEC(program_name, WSTR, READ_ONLY, NO_SYS),
SPEC(run_command, WSTR_OPT, READ_ONLY, NO_SYS),
SPEC(run_filename, WSTR_OPT, READ_ONLY, NO_SYS),
@ -317,6 +318,7 @@ The following implementation-specific options are available:\n\
-X perf: support the Linux \"perf\" profiler; also PYTHONPERFSUPPORT=1\n\
-X perf_jit: support the Linux \"perf\" profiler with DWARF support;\n\
also PYTHON_PERF_JIT_SUPPORT=1\n\
-X disable-remote-debug: disable remote debugging; also PYTHON_DISABLE_REMOTE_DEBUG\n\
"
#ifdef Py_DEBUG
"-X presite=MOD: import this module before site; also PYTHON_PRESITE\n"
@ -994,6 +996,7 @@ _PyConfig_InitCompatConfig(PyConfig *config)
config->faulthandler = -1;
config->tracemalloc = -1;
config->perf_profiling = -1;
config->remote_debug = -1;
config->module_search_paths_set = 0;
config->parse_argv = 0;
config->site_import = -1;
@ -1986,6 +1989,28 @@ config_init_perf_profiling(PyConfig *config)
}
static PyStatus
config_init_remote_debug(PyConfig *config)
{
#ifndef Py_REMOTE_DEBUG
config->remote_debug = 0;
#else
int active = 1;
const char *env = Py_GETENV("PYTHON_DISABLE_REMOTE_DEBUG");
if (env) {
active = 0;
}
const wchar_t *xoption = config_get_xoption(config, L"disable-remote-debug");
if (xoption) {
active = 0;
}
config->remote_debug = active;
#endif
return _PyStatus_OK();
}
static PyStatus
config_init_tracemalloc(PyConfig *config)
{
@ -2170,6 +2195,13 @@ config_read_complex_options(PyConfig *config)
}
}
if (config->remote_debug < 0) {
status = config_init_remote_debug(config);
if (_PyStatus_EXCEPTION(status)) {
return status;
}
}
if (config->int_max_str_digits < 0) {
status = config_init_int_max_str_digits(config);
if (_PyStatus_EXCEPTION(status)) {
@ -2531,6 +2563,9 @@ config_read(PyConfig *config, int compute_path_config)
if (config->perf_profiling < 0) {
config->perf_profiling = 0;
}
if (config->remote_debug < 0) {
config->remote_debug = -1;
}
if (config->use_hash_seed < 0) {
config->use_hash_seed = 0;
config->hash_seed = 0;

984
Python/remote_debugging.c Normal file
View file

@ -0,0 +1,984 @@
#define _GNU_SOURCE
#include "pyconfig.h"
#include "Python.h"
#include "internal/pycore_runtime.h"
#include "internal/pycore_ceval.h"
#ifdef __linux__
# include <elf.h>
# include <sys/uio.h>
# if INTPTR_MAX == INT64_MAX
# define Elf_Ehdr Elf64_Ehdr
# define Elf_Shdr Elf64_Shdr
# define Elf_Phdr Elf64_Phdr
# else
# define Elf_Ehdr Elf32_Ehdr
# define Elf_Shdr Elf32_Shdr
# define Elf_Phdr Elf32_Phdr
# endif
# include <sys/mman.h>
#endif
#if defined(__APPLE__)
# include <TargetConditionals.h>
// Older macOS SDKs do not define TARGET_OS_OSX
# if !defined(TARGET_OS_OSX)
# define TARGET_OS_OSX 1
# endif
# if TARGET_OS_OSX
# include <libproc.h>
# include <mach-o/fat.h>
# include <mach-o/loader.h>
# include <mach-o/nlist.h>
# include <mach/mach.h>
# include <mach/mach_vm.h>
# include <mach/machine.h>
# include <sys/mman.h>
# include <sys/proc.h>
# include <sys/sysctl.h>
# endif
#endif
#ifdef MS_WINDOWS
// Windows includes and definitions
#include <windows.h>
#include <psapi.h>
#include <tlhelp32.h>
#endif
#include <errno.h>
#include <fcntl.h>
#include <stddef.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#ifndef MS_WINDOWS
#include <sys/param.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>
#endif
#ifndef HAVE_PROCESS_VM_READV
# define HAVE_PROCESS_VM_READV 0
#endif
// Define a platform-independent process handle structure
typedef struct {
pid_t pid;
#ifdef MS_WINDOWS
HANDLE hProcess;
#endif
} proc_handle_t;
// Initialize the process handle
static int
init_proc_handle(proc_handle_t *handle, pid_t pid) {
handle->pid = pid;
#ifdef MS_WINDOWS
handle->hProcess = OpenProcess(
PROCESS_VM_READ | PROCESS_VM_WRITE | PROCESS_VM_OPERATION | PROCESS_QUERY_INFORMATION,
FALSE, pid);
if (handle->hProcess == NULL) {
PyErr_SetFromWindowsErr(0);
return -1;
}
#endif
return 0;
}
// Clean up the process handle
static void
cleanup_proc_handle(proc_handle_t *handle) {
#ifdef MS_WINDOWS
if (handle->hProcess != NULL) {
CloseHandle(handle->hProcess);
handle->hProcess = NULL;
}
#endif
handle->pid = 0;
}
#if defined(Py_REMOTE_DEBUG) && defined(Py_SUPPORTS_REMOTE_DEBUG)
#if defined(__APPLE__) && TARGET_OS_OSX
static uintptr_t
return_section_address(
const char* section,
mach_port_t proc_ref,
uintptr_t base,
void* map
) {
struct mach_header_64* hdr = (struct mach_header_64*)map;
int ncmds = hdr->ncmds;
int cmd_cnt = 0;
struct segment_command_64* cmd = map + sizeof(struct mach_header_64);
mach_vm_size_t size = 0;
mach_msg_type_number_t count = sizeof(vm_region_basic_info_data_64_t);
mach_vm_address_t address = (mach_vm_address_t)base;
vm_region_basic_info_data_64_t r_info;
mach_port_t object_name;
uintptr_t vmaddr = 0;
for (int i = 0; cmd_cnt < 2 && i < ncmds; i++) {
if (cmd->cmd == LC_SEGMENT_64 && strcmp(cmd->segname, "__TEXT") == 0) {
vmaddr = cmd->vmaddr;
}
if (cmd->cmd == LC_SEGMENT_64 && strcmp(cmd->segname, "__DATA") == 0) {
while (cmd->filesize != size) {
address += size;
kern_return_t ret = mach_vm_region(
proc_ref,
&address,
&size,
VM_REGION_BASIC_INFO_64,
(vm_region_info_t)&r_info, // cppcheck-suppress [uninitvar]
&count,
&object_name
);
if (ret != KERN_SUCCESS) {
PyErr_SetString(
PyExc_RuntimeError, "Cannot get any more VM maps.\n");
return 0;
}
}
int nsects = cmd->nsects;
struct section_64* sec = (struct section_64*)(
(void*)cmd + sizeof(struct segment_command_64)
);
for (int j = 0; j < nsects; j++) {
if (strcmp(sec[j].sectname, section) == 0) {
return base + sec[j].addr - vmaddr;
}
}
cmd_cnt++;
}
cmd = (struct segment_command_64*)((void*)cmd + cmd->cmdsize);
}
// We should not be here, but if we are there, we should say about this
PyErr_SetString(
PyExc_RuntimeError, "Cannot find section address.\n");
return 0;
}
static uintptr_t
search_section_in_file(const char* secname, char* path, uintptr_t base, mach_vm_size_t size, mach_port_t proc_ref)
{
int fd = open(path, O_RDONLY);
if (fd == -1) {
PyErr_Format(PyExc_RuntimeError, "Cannot open binary %s\n", path);
return 0;
}
struct stat fs;
if (fstat(fd, &fs) == -1) {
PyErr_Format(PyExc_RuntimeError, "Cannot get size of binary %s\n", path);
close(fd);
return 0;
}
void* map = mmap(0, fs.st_size, PROT_READ, MAP_SHARED, fd, 0);
if (map == MAP_FAILED) {
PyErr_Format(PyExc_RuntimeError, "Cannot map binary %s\n", path);
close(fd);
return 0;
}
uintptr_t result = 0;
struct mach_header_64* hdr = (struct mach_header_64*)map;
switch (hdr->magic) {
case MH_MAGIC:
case MH_CIGAM:
case FAT_MAGIC:
case FAT_CIGAM:
PyErr_SetString(PyExc_RuntimeError, "32-bit Mach-O binaries are not supported");
break;
case MH_MAGIC_64:
case MH_CIGAM_64:
result = return_section_address(secname, proc_ref, base, map);
break;
default:
PyErr_SetString(PyExc_RuntimeError, "Unknown Mach-O magic");
break;
}
munmap(map, fs.st_size);
if (close(fd) != 0) {
PyErr_SetFromErrno(PyExc_OSError);
}
return result;
}
static mach_port_t
pid_to_task(pid_t pid)
{
mach_port_t task;
kern_return_t result;
result = task_for_pid(mach_task_self(), pid, &task);
if (result != KERN_SUCCESS) {
PyErr_Format(PyExc_PermissionError, "Cannot get task for PID %d", pid);
return 0;
}
return task;
}
static uintptr_t
search_map_for_section(proc_handle_t *handle, const char* secname, const char* substr) {
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);
vm_region_basic_info_data_64_t region_info;
mach_port_t object_name;
mach_port_t proc_ref = pid_to_task(handle->pid);
if (proc_ref == 0) {
PyErr_SetString(PyExc_PermissionError, "Cannot get task for PID");
return 0;
}
int match_found = 0;
char map_filename[MAXPATHLEN + 1];
while (mach_vm_region(
proc_ref,
&address,
&size,
VM_REGION_BASIC_INFO_64,
(vm_region_info_t)&region_info,
&count,
&object_name) == KERN_SUCCESS)
{
if ((region_info.protection & VM_PROT_READ) == 0
|| (region_info.protection & VM_PROT_EXECUTE) == 0) {
address += size;
continue;
}
int path_len = proc_regionfilename(
handle->pid, address, map_filename, MAXPATHLEN);
if (path_len == 0) {
address += size;
continue;
}
char* filename = strrchr(map_filename, '/');
if (filename != NULL) {
filename++; // Move past the '/'
} else {
filename = map_filename; // No path, use the whole string
}
if (!match_found && strncmp(filename, substr, strlen(substr)) == 0) {
match_found = 1;
return search_section_in_file(
secname, map_filename, address, size, proc_ref);
}
address += size;
}
PyErr_SetString(PyExc_RuntimeError,
"mach_vm_region failed to find the section");
return 0;
}
#endif // (__APPLE__ && TARGET_OS_OSX)
#if defined(__linux__) && HAVE_PROCESS_VM_READV
static uintptr_t
search_elf_file_for_section(
proc_handle_t *handle,
const char* secname,
uintptr_t start_address,
const char *elf_file)
{
if (start_address == 0) {
return 0;
}
uintptr_t result = 0;
void* file_memory = NULL;
int fd = open(elf_file, O_RDONLY);
if (fd < 0) {
PyErr_SetFromErrno(PyExc_OSError);
goto exit;
}
struct stat file_stats;
if (fstat(fd, &file_stats) != 0) {
PyErr_SetFromErrno(PyExc_OSError);
goto exit;
}
file_memory = mmap(NULL, file_stats.st_size, PROT_READ, MAP_PRIVATE, fd, 0);
if (file_memory == MAP_FAILED) {
PyErr_SetFromErrno(PyExc_OSError);
goto exit;
}
Elf_Ehdr* elf_header = (Elf_Ehdr*)file_memory;
Elf_Shdr* section_header_table = (Elf_Shdr*)(file_memory + elf_header->e_shoff);
Elf_Shdr* shstrtab_section = &section_header_table[elf_header->e_shstrndx];
char* shstrtab = (char*)(file_memory + shstrtab_section->sh_offset);
Elf_Shdr* section = NULL;
for (int i = 0; i < elf_header->e_shnum; i++) {
char* this_sec_name = shstrtab + section_header_table[i].sh_name;
// Move 1 character to account for the leading "."
this_sec_name += 1;
if (strcmp(secname, this_sec_name) == 0) {
section = &section_header_table[i];
break;
}
}
Elf_Phdr* program_header_table = (Elf_Phdr*)(file_memory + elf_header->e_phoff);
// Find the first PT_LOAD segment
Elf_Phdr* first_load_segment = NULL;
for (int i = 0; i < elf_header->e_phnum; i++) {
if (program_header_table[i].p_type == PT_LOAD) {
first_load_segment = &program_header_table[i];
break;
}
}
if (section != NULL && first_load_segment != NULL) {
uintptr_t elf_load_addr = first_load_segment->p_vaddr
- (first_load_segment->p_vaddr % first_load_segment->p_align);
result = start_address + (uintptr_t)section->sh_addr - elf_load_addr;
}
exit:
if (file_memory != NULL) {
munmap(file_memory, file_stats.st_size);
}
if (fd >= 0 && close(fd) != 0) {
PyErr_SetFromErrno(PyExc_OSError);
}
return result;
}
static uintptr_t
search_linux_map_for_section(proc_handle_t *handle, const char* secname, const char* substr)
{
char maps_file_path[64];
sprintf(maps_file_path, "/proc/%d/maps", handle->pid);
FILE* maps_file = fopen(maps_file_path, "r");
if (maps_file == NULL) {
PyErr_SetFromErrno(PyExc_OSError);
return 0;
}
size_t linelen = 0;
size_t linesz = PATH_MAX;
char *line = PyMem_Malloc(linesz);
if (!line) {
fclose(maps_file);
PyErr_NoMemory();
return 0;
}
uintptr_t retval = 0;
while (fgets(line + linelen, linesz - linelen, maps_file) != NULL) {
linelen = strlen(line);
if (line[linelen - 1] != '\n') {
// Read a partial line: realloc and keep reading where we left off.
// Note that even the last line will be terminated by a newline.
linesz *= 2;
char *biggerline = PyMem_Realloc(line, linesz);
if (!biggerline) {
PyMem_Free(line);
fclose(maps_file);
PyErr_NoMemory();
return 0;
}
line = biggerline;
continue;
}
// Read a full line: strip the newline
line[linelen - 1] = '\0';
// and prepare to read the next line into the start of the buffer.
linelen = 0;
unsigned long start = 0;
unsigned long path_pos = 0;
sscanf(line, "%lx-%*x %*s %*s %*s %*s %ln", &start, &path_pos);
if (!path_pos) {
// Line didn't match our format string. This shouldn't be
// possible, but let's be defensive and skip the line.
continue;
}
const char *path = line + path_pos;
const char *filename = strrchr(path, '/');
if (filename) {
filename++; // Move past the '/'
} else {
filename = path; // No directories, or an empty string
}
if (strstr(filename, substr)) {
retval = search_elf_file_for_section(handle, secname, start, path);
if (retval) {
break;
}
}
}
PyMem_Free(line);
fclose(maps_file);
return retval;
}
#endif // __linux__
#ifdef MS_WINDOWS
static void* analyze_pe(const wchar_t* mod_path, BYTE* remote_base, const char* secname) {
HANDLE hFile = CreateFileW(mod_path, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
if (hFile == INVALID_HANDLE_VALUE) {
PyErr_SetFromWindowsErr(0);
return NULL;
}
HANDLE hMap = CreateFileMapping(hFile, NULL, PAGE_READONLY, 0, 0, 0);
if (!hMap) {
PyErr_SetFromWindowsErr(0);
CloseHandle(hFile);
return NULL;
}
BYTE* mapView = (BYTE*)MapViewOfFile(hMap, FILE_MAP_READ, 0, 0, 0);
if (!mapView) {
PyErr_SetFromWindowsErr(0);
CloseHandle(hMap);
CloseHandle(hFile);
return NULL;
}
IMAGE_DOS_HEADER* pDOSHeader = (IMAGE_DOS_HEADER*)mapView;
if (pDOSHeader->e_magic != IMAGE_DOS_SIGNATURE) {
PyErr_SetString(PyExc_RuntimeError, "Invalid DOS signature.");
UnmapViewOfFile(mapView);
CloseHandle(hMap);
CloseHandle(hFile);
return NULL;
}
IMAGE_NT_HEADERS* pNTHeaders = (IMAGE_NT_HEADERS*)(mapView + pDOSHeader->e_lfanew);
if (pNTHeaders->Signature != IMAGE_NT_SIGNATURE) {
PyErr_SetString(PyExc_RuntimeError, "Invalid NT signature.");
UnmapViewOfFile(mapView);
CloseHandle(hMap);
CloseHandle(hFile);
return NULL;
}
IMAGE_SECTION_HEADER* pSection_header = (IMAGE_SECTION_HEADER*)(mapView + pDOSHeader->e_lfanew + sizeof(IMAGE_NT_HEADERS));
void* runtime_addr = NULL;
for (int i = 0; i < pNTHeaders->FileHeader.NumberOfSections; i++) {
const char* name = (const char*)pSection_header[i].Name;
if (strncmp(name, secname, IMAGE_SIZEOF_SHORT_NAME) == 0) {
runtime_addr = remote_base + pSection_header[i].VirtualAddress;
break;
}
}
UnmapViewOfFile(mapView);
CloseHandle(hMap);
CloseHandle(hFile);
return runtime_addr;
}
static uintptr_t
search_windows_map_for_section(proc_handle_t* handle, const char* secname, const wchar_t* substr) {
HANDLE hProcSnap;
do {
hProcSnap = CreateToolhelp32Snapshot(TH32CS_SNAPMODULE, handle->pid);
} while (hProcSnap == INVALID_HANDLE_VALUE && GetLastError() == ERROR_BAD_LENGTH);
if (hProcSnap == INVALID_HANDLE_VALUE) {
PyErr_SetString(PyExc_PermissionError, "Unable to create module snapshot. Check permissions or PID.");
return 0;
}
MODULEENTRY32W moduleEntry;
moduleEntry.dwSize = sizeof(moduleEntry);
void* runtime_addr = NULL;
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) {
break;
}
}
}
CloseHandle(hProcSnap);
return (uintptr_t)runtime_addr;
}
#endif // MS_WINDOWS
// Get the PyRuntime section address for any platform
static uintptr_t
get_py_runtime(proc_handle_t* handle)
{
uintptr_t address = 0;
#ifdef MS_WINDOWS
// On Windows, search for 'python' in executable or DLL
address = search_windows_map_for_section(handle, "PyRuntime", L"python");
if (address == 0) {
// Error out: 'python' substring covers both executable and DLL
PyErr_SetString(PyExc_RuntimeError, "Failed to find the PyRuntime section in the process.");
}
#elif defined(__linux__)
// On Linux, search for 'python' in executable or DLL
address = search_linux_map_for_section(handle, "PyRuntime", "python");
if (address == 0) {
// Error out: 'python' substring covers both executable and DLL
PyErr_SetString(PyExc_RuntimeError, "Failed to find the PyRuntime section in the process.");
}
#else
// On macOS, try libpython first, then fall back to python
address = search_map_for_section(handle, "PyRuntime", "libpython");
if (address == 0) {
// TODO: Differentiate between not found and error
PyErr_Clear();
address = search_map_for_section(handle, "PyRuntime", "python");
}
#endif
return address;
}
// Platform-independent memory read function
static int
read_memory(proc_handle_t *handle, uint64_t remote_address, size_t len, void* dst)
{
#ifdef MS_WINDOWS
SIZE_T read_bytes = 0;
SIZE_T result = 0;
do {
if (!ReadProcessMemory(handle->hProcess, (LPCVOID)(remote_address + result), (char*)dst + result, len - result, &read_bytes)) {
PyErr_SetFromWindowsErr(0);
return -1;
}
result += read_bytes;
} while (result < len);
return 0;
#elif defined(__linux__) && HAVE_PROCESS_VM_READV
struct iovec local[1];
struct iovec remote[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;
remote[0].iov_base = (void*)(remote_address + result);
remote[0].iov_len = len - result;
read_bytes = process_vm_readv(handle->pid, local, 1, remote, 1, 0);
if (read_bytes < 0) {
PyErr_SetFromErrno(PyExc_OSError);
return -1;
}
result += read_bytes;
} while ((size_t)read_bytes != local[0].iov_len);
return 0;
#elif defined(__APPLE__) && TARGET_OS_OSX
Py_ssize_t result = -1;
kern_return_t kr = mach_vm_read_overwrite(
pid_to_task(handle->pid),
(mach_vm_address_t)remote_address,
len,
(mach_vm_address_t)dst,
(mach_vm_size_t*)&result);
if (kr != KERN_SUCCESS) {
switch (kr) {
case KERN_PROTECTION_FAILURE:
PyErr_SetString(PyExc_PermissionError, "Not enough permissions to read memory");
break;
case KERN_INVALID_ARGUMENT:
PyErr_SetString(PyExc_PermissionError, "Invalid argument to mach_vm_read_overwrite");
break;
default:
PyErr_SetString(PyExc_RuntimeError, "Unknown error reading memory");
}
return -1;
}
return 0;
#else
Py_UNREACHABLE();
#endif
}
// Platform-independent memory write function
static int
write_memory(proc_handle_t *handle, uintptr_t remote_address, size_t len, const void* src)
{
#ifdef MS_WINDOWS
SIZE_T written = 0;
SIZE_T result = 0;
do {
if (!WriteProcessMemory(handle->hProcess, (LPVOID)(remote_address + result), (const char*)src + result, len - result, &written)) {
PyErr_SetFromWindowsErr(0);
return -1;
}
result += written;
} while (result < len);
return 0;
#elif defined(__linux__) && HAVE_PROCESS_VM_READV
struct iovec local[1];
struct iovec remote[1];
Py_ssize_t result = 0;
Py_ssize_t written = 0;
do {
local[0].iov_base = (void*)((char*)src + result);
local[0].iov_len = len - result;
remote[0].iov_base = (void*)((char*)remote_address + result);
remote[0].iov_len = len - result;
written = process_vm_writev(handle->pid, local, 1, remote, 1, 0);
if (written < 0) {
PyErr_SetFromErrno(PyExc_OSError);
return -1;
}
result += written;
} while ((size_t)written != local[0].iov_len);
return 0;
#elif defined(__APPLE__) && TARGET_OS_OSX
kern_return_t kr = mach_vm_write(
pid_to_task(handle->pid),
(mach_vm_address_t)remote_address,
(vm_offset_t)src,
(mach_msg_type_number_t)len);
if (kr != KERN_SUCCESS) {
switch (kr) {
case KERN_PROTECTION_FAILURE:
PyErr_SetString(PyExc_PermissionError, "Not enough permissions to write memory");
break;
case KERN_INVALID_ARGUMENT:
PyErr_SetString(PyExc_PermissionError, "Invalid argument to mach_vm_write");
break;
default:
PyErr_Format(PyExc_RuntimeError, "Unknown error writing memory: %d", (int)kr);
}
return -1;
}
return 0;
#else
Py_UNREACHABLE();
#endif
}
static int
is_prerelease_version(uint64_t version)
{
return (version & 0xF0) != 0xF0;
}
static int
ensure_debug_offset_compatibility(const _Py_DebugOffsets* debug_offsets)
{
if (memcmp(debug_offsets->cookie, _Py_Debug_Cookie, sizeof(debug_offsets->cookie)) != 0) {
// The remote is probably running a Python version predating debug offsets.
PyErr_SetString(
PyExc_RuntimeError,
"Can't determine the Python version of the remote process");
return -1;
}
// Assume debug offsets could change from one pre-release version to another,
// or one minor version to another, but are stable across patch versions.
if (is_prerelease_version(Py_Version) && Py_Version != debug_offsets->version) {
PyErr_SetString(
PyExc_RuntimeError,
"Can't send commands from a pre-release Python interpreter"
" to a process running a different Python version");
return -1;
}
if (is_prerelease_version(debug_offsets->version) && Py_Version != debug_offsets->version) {
PyErr_SetString(
PyExc_RuntimeError,
"Can't send commands to a pre-release Python interpreter"
" from a process running a different Python version");
return -1;
}
unsigned int remote_major = (debug_offsets->version >> 24) & 0xFF;
unsigned int remote_minor = (debug_offsets->version >> 16) & 0xFF;
if (PY_MAJOR_VERSION != remote_major || PY_MINOR_VERSION != remote_minor) {
PyErr_Format(
PyExc_RuntimeError,
"Can't send commands from a Python %d.%d process to a Python %d.%d process",
PY_MAJOR_VERSION, PY_MINOR_VERSION, remote_major, remote_minor);
return -1;
}
// The debug offsets differ between free threaded and non-free threaded builds.
if (_Py_Debug_Free_Threaded && !debug_offsets->free_threaded) {
PyErr_SetString(
PyExc_RuntimeError,
"Cannot send commands from a free-threaded Python process"
" to a process running a non-free-threaded version");
return -1;
}
if (!_Py_Debug_Free_Threaded && debug_offsets->free_threaded) {
PyErr_SetString(
PyExc_RuntimeError,
"Cannot send commands to a free-threaded Python process"
" from a process running a non-free-threaded version");
return -1;
}
return 0;
}
static int
read_offsets(
proc_handle_t *handle,
uintptr_t *runtime_start_address,
_Py_DebugOffsets* debug_offsets
) {
*runtime_start_address = get_py_runtime(handle);
if (!*runtime_start_address) {
if (!PyErr_Occurred()) {
PyErr_SetString(
PyExc_RuntimeError, "Failed to get PyRuntime address");
}
return -1;
}
size_t size = sizeof(struct _Py_DebugOffsets);
if (0 != read_memory(handle, *runtime_start_address, size, debug_offsets)) {
return -1;
}
if (ensure_debug_offset_compatibility(debug_offsets)) {
return -1;
}
return 0;
}
static int
send_exec_to_proc_handle(proc_handle_t *handle, int tid, const char *debugger_script_path)
{
uintptr_t runtime_start_address;
struct _Py_DebugOffsets debug_offsets;
if (read_offsets(handle, &runtime_start_address, &debug_offsets)) {
return -1;
}
uintptr_t interpreter_state_list_head = (uintptr_t)debug_offsets.runtime_state.interpreters_head;
uintptr_t interpreter_state_addr;
if (0 != read_memory(
handle,
runtime_start_address + interpreter_state_list_head,
sizeof(void*),
&interpreter_state_addr))
{
return -1;
}
if (interpreter_state_addr == 0) {
PyErr_SetString(PyExc_RuntimeError, "Can't find a running interpreter in the remote process");
return -1;
}
int is_remote_debugging_enabled = 0;
if (0 != read_memory(
handle,
interpreter_state_addr + debug_offsets.debugger_support.remote_debugging_enabled,
sizeof(int),
&is_remote_debugging_enabled))
{
return -1;
}
if (is_remote_debugging_enabled != 1) {
PyErr_SetString(
PyExc_RuntimeError,
"Remote debugging is not enabled in the remote process");
return -1;
}
uintptr_t thread_state_addr;
unsigned long this_tid = 0;
if (tid != 0) {
if (0 != read_memory(
handle,
interpreter_state_addr + debug_offsets.interpreter_state.threads_head,
sizeof(void*),
&thread_state_addr))
{
return -1;
}
while (thread_state_addr != 0) {
if (0 != read_memory(
handle,
thread_state_addr + debug_offsets.thread_state.native_thread_id,
sizeof(this_tid),
&this_tid))
{
return -1;
}
if (this_tid == (unsigned long)tid) {
break;
}
if (0 != read_memory(
handle,
thread_state_addr + debug_offsets.thread_state.next,
sizeof(void*),
&thread_state_addr))
{
return -1;
}
}
if (thread_state_addr == 0) {
PyErr_SetString(
PyExc_RuntimeError,
"Can't find the specified thread in the remote process");
return -1;
}
} else {
if (0 != read_memory(
handle,
interpreter_state_addr + debug_offsets.interpreter_state.threads_main,
sizeof(void*),
&thread_state_addr))
{
return -1;
}
if (thread_state_addr == 0) {
PyErr_SetString(
PyExc_RuntimeError,
"Can't find the main thread in the remote process");
return -1;
}
}
// Ensure our path is not too long
if (debug_offsets.debugger_support.debugger_script_path_size <= strlen(debugger_script_path)) {
PyErr_SetString(PyExc_ValueError, "Debugger script path is too long");
return -1;
}
uintptr_t debugger_script_path_addr = (uintptr_t)(
thread_state_addr +
debug_offsets.debugger_support.remote_debugger_support +
debug_offsets.debugger_support.debugger_script_path);
if (0 != write_memory(
handle,
debugger_script_path_addr,
strlen(debugger_script_path) + 1,
debugger_script_path))
{
return -1;
}
int pending_call = 1;
uintptr_t debugger_pending_call_addr = (uintptr_t)(
thread_state_addr +
debug_offsets.debugger_support.remote_debugger_support +
debug_offsets.debugger_support.debugger_pending_call);
if (0 != write_memory(
handle,
debugger_pending_call_addr,
sizeof(int),
&pending_call))
{
return -1;
}
uintptr_t eval_breaker;
if (0 != read_memory(
handle,
thread_state_addr + debug_offsets.debugger_support.eval_breaker,
sizeof(uintptr_t),
&eval_breaker))
{
return -1;
}
eval_breaker |= _PY_EVAL_PLEASE_STOP_BIT;
if (0 != write_memory(
handle,
thread_state_addr + (uintptr_t)debug_offsets.debugger_support.eval_breaker,
sizeof(uintptr_t),
&eval_breaker))
{
return -1;
}
return 0;
}
#endif // defined(Py_REMOTE_DEBUG) && defined(Py_SUPPORTS_REMOTE_DEBUG)
int
_PySysRemoteDebug_SendExec(int pid, int tid, const char *debugger_script_path)
{
#if !defined(Py_SUPPORTS_REMOTE_DEBUG)
PyErr_SetString(PyExc_RuntimeError, "Remote debugging is not supported on this platform");
return -1;
#elif !defined(Py_REMOTE_DEBUG)
PyErr_SetString(PyExc_RuntimeError, "Remote debugging support has not been compiled in");
return -1;
#else
PyThreadState *tstate = _PyThreadState_GET();
const PyConfig *config = _PyInterpreterState_GetConfig(tstate->interp);
if (config->remote_debug != 1) {
PyErr_SetString(PyExc_RuntimeError, "Remote debugging is not enabled");
return -1;
}
proc_handle_t handle;
if (init_proc_handle(&handle, pid) < 0) {
return -1;
}
int rc = send_exec_to_proc_handle(&handle, tid, debugger_script_path);
cleanup_proc_handle(&handle);
return rc;
#endif
}

View file

@ -2421,6 +2421,120 @@ sys_is_stack_trampoline_active_impl(PyObject *module)
Py_RETURN_FALSE;
}
/*[clinic input]
sys.is_remote_debug_enabled
Return True if remote debugging is enabled, False otherwise.
[clinic start generated code]*/
static PyObject *
sys_is_remote_debug_enabled_impl(PyObject *module)
/*[clinic end generated code: output=7ca3d38bdd5935eb input=7335c4a2fe8cf4f3]*/
{
#ifndef Py_REMOTE_DEBUG
Py_RETURN_FALSE;
#else
const PyConfig *config = _Py_GetConfig();
return PyBool_FromLong(config->remote_debug);
#endif
}
static PyObject *
sys_remote_exec_unicode_path(PyObject *module, int pid, PyObject *script)
{
const char *debugger_script_path = PyUnicode_AsUTF8(script);
if (debugger_script_path == NULL) {
return NULL;
}
#ifdef MS_WINDOWS
// Use UTF-16 (wide char) version of the path for permission checks
wchar_t *debugger_script_path_w = PyUnicode_AsWideCharString(script, NULL);
if (debugger_script_path_w == NULL) {
return NULL;
}
// Check file attributes using wide character version (W) instead of ANSI (A)
DWORD attr = GetFileAttributesW(debugger_script_path_w);
PyMem_Free(debugger_script_path_w);
if (attr == INVALID_FILE_ATTRIBUTES) {
DWORD err = GetLastError();
if (err == ERROR_FILE_NOT_FOUND || err == ERROR_PATH_NOT_FOUND) {
PyErr_SetString(PyExc_FileNotFoundError, "Script file does not exist");
}
else if (err == ERROR_ACCESS_DENIED) {
PyErr_SetString(PyExc_PermissionError, "Script file cannot be read");
}
else {
PyErr_SetFromWindowsErr(0);
}
return NULL;
}
#else
if (access(debugger_script_path, F_OK | R_OK) != 0) {
switch (errno) {
case ENOENT:
PyErr_SetString(PyExc_FileNotFoundError, "Script file does not exist");
break;
case EACCES:
PyErr_SetString(PyExc_PermissionError, "Script file cannot be read");
break;
default:
PyErr_SetFromErrno(PyExc_OSError);
}
return NULL;
}
#endif
if (_PySysRemoteDebug_SendExec(pid, 0, debugger_script_path) < 0) {
return NULL;
}
Py_RETURN_NONE;
}
/*[clinic input]
sys.remote_exec
pid: int
script: object
Executes a file containing Python code in a given remote Python process.
This function returns immediately, and the code will be executed by the
target process's main thread at the next available opportunity, similarly
to how signals are handled. There is no interface to determine when the
code has been executed. The caller is responsible for making sure that
the file still exists whenever the remote process tries to read it and that
it hasn't been overwritten.
The remote process must be running a CPython interpreter of the same major
and minor version as the local process. If either the local or remote
interpreter is pre-release (alpha, beta, or release candidate) then the
local and remote interpreters must be the same exact version.
Args:
pid (int): The process ID of the target Python process.
script (str|bytes): The path to a file containing
the Python code to be executed.
[clinic start generated code]*/
static PyObject *
sys_remote_exec_impl(PyObject *module, int pid, PyObject *script)
/*[clinic end generated code: output=7d94c56afe4a52c0 input=39908ca2c5fe1eb0]*/
{
PyObject *ret = NULL;
PyObject *path;
if (PyUnicode_FSDecoder(script, &path)) {
ret = sys_remote_exec_unicode_path(module, pid, path);
Py_DECREF(path);
}
return ret;
}
/*[clinic input]
sys._dump_tracelets
@ -2695,6 +2809,8 @@ static PyMethodDef sys_methods[] = {
SYS_ACTIVATE_STACK_TRAMPOLINE_METHODDEF
SYS_DEACTIVATE_STACK_TRAMPOLINE_METHODDEF
SYS_IS_STACK_TRAMPOLINE_ACTIVE_METHODDEF
SYS_IS_REMOTE_DEBUG_ENABLED_METHODDEF
SYS_REMOTE_EXEC_METHODDEF
SYS_UNRAISABLEHOOK_METHODDEF
SYS_GET_INT_MAX_STR_DIGITS_METHODDEF
SYS_SET_INT_MAX_STR_DIGITS_METHODDEF