diff options
author | Pablo Galindo Salgado <Pablogsal@gmail.com> | 2025-04-03 16:20:01 +0100 |
---|---|---|
committer | GitHub <noreply@github.com> | 2025-04-03 16:20:01 +0100 |
commit | 943cc1431ebf4a265b79f69fa286787e77410fe9 (patch) | |
tree | 00f0c5bd86dc48bfd105c8a9aad56bee022b89c4 /Python | |
parent | 275056a7fdcbe36aaac494b4183ae59943a338eb (diff) | |
download | cpython-943cc1431ebf4a265b79f69fa286787e77410fe9.tar.gz cpython-943cc1431ebf4a265b79f69fa286787e77410fe9.zip |
gh-131591: Implement PEP 768 (#131937)
Co-authored-by: Ivona Stojanovic <stojanovic.i@hotmail.com>
Co-authored-by: Matt Wozniski <godlygeek@gmail.com>
Diffstat (limited to 'Python')
-rw-r--r-- | Python/ceval_gil.c | 95 | ||||
-rw-r--r-- | Python/clinic/sysmodule.c.h | 100 | ||||
-rw-r--r-- | Python/initconfig.c | 35 | ||||
-rw-r--r-- | Python/remote_debugging.c | 984 | ||||
-rw-r--r-- | Python/sysmodule.c | 116 |
5 files changed, 1329 insertions, 1 deletions
diff --git a/Python/ceval_gil.c b/Python/ceval_gil.c index 77632b283b2..a455779a659 100644 --- a/Python/ceval_gil.c +++ b/Python/ceval_gil.c @@ -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; } diff --git a/Python/clinic/sysmodule.c.h b/Python/clinic/sysmodule.c.h index d274b0c924c..8b73ccefc30 100644 --- a/Python/clinic/sysmodule.c.h +++ b/Python/clinic/sysmodule.c.h @@ -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]*/ diff --git a/Python/initconfig.c b/Python/initconfig.c index bc48ebd61f2..e398b1cbeaa 100644 --- a/Python/initconfig.c +++ b/Python/initconfig.c @@ -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; @@ -1987,6 +1990,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) { int nframe; @@ -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; diff --git a/Python/remote_debugging.c b/Python/remote_debugging.c new file mode 100644 index 00000000000..9b2297b5627 --- /dev/null +++ b/Python/remote_debugging.c @@ -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)®ion_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 = §ion_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 = §ion_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 +} diff --git a/Python/sysmodule.c b/Python/sysmodule.c index 1b2019a9f74..2a28fab2f51 100644 --- a/Python/sysmodule.c +++ b/Python/sysmodule.c @@ -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 |