| /* | |
| IMPORTANT: This header file is full of static functions that are not exported. | |
| The reason is that we don't want to export these functions to the Python API | |
| and they can be used both for the interpreter and some shared libraries. The | |
| reason we don't want to export them is to avoid having them participating in | |
| return-oriented programming attacks. | |
| If you need to add a new function ensure that is declared 'static'. | |
| */ | |
| #ifdef __cplusplus | |
| extern "C" { | |
| #endif | |
| #ifdef __clang__ | |
| #define UNUSED __attribute__((unused)) | |
| #elif defined(__GNUC__) | |
| #define UNUSED __attribute__((unused)) | |
| #elif defined(_MSC_VER) | |
| #define UNUSED __pragma(warning(suppress: 4505)) | |
| #else | |
| #define UNUSED | |
| #endif | |
| #if !defined(Py_BUILD_CORE) && !defined(Py_BUILD_CORE_MODULE) | |
| # error "this header requires Py_BUILD_CORE or Py_BUILD_CORE_MODULE define" | |
| #endif | |
| #include "pyconfig.h" | |
| #include "internal/pycore_ceval.h" | |
| #ifdef __linux__ | |
| # include <elf.h> | |
| # include <sys/uio.h> | |
| # include <sys/ptrace.h> | |
| # include <sys/wait.h> | |
| # include <dirent.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> | |
| // PTRACE options - define if not available | |
| # ifndef PTRACE_SEIZE | |
| # define PTRACE_SEIZE 0x4206 | |
| # endif | |
| # ifndef PTRACE_INTERRUPT | |
| # define PTRACE_INTERRUPT 0x4207 | |
| # endif | |
| # ifndef PTRACE_EVENT_STOP | |
| # define PTRACE_EVENT_STOP 128 | |
| # endif | |
| #endif | |
| #if defined(__APPLE__) && defined(TARGET_OS_OSX) && TARGET_OS_OSX | |
| # include <libproc.h> | |
| # include <mach-o/fat.h> | |
| # include <mach-o/loader.h> | |
| # include <mach-o/nlist.h> | |
| # include <mach/error.h> | |
| # include <mach/mach.h> | |
| # include <mach/mach_vm.h> | |
| # include <mach/machine.h> | |
| # include <mach/task_info.h> | |
| # include <mach/thread_act.h> | |
| # include <sys/mman.h> | |
| # include <sys/proc.h> | |
| # include <sys/sysctl.h> | |
| #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 | |
| static inline int | |
| _Py_RemoteDebug_HasPermissionError(void) | |
| { | |
| return PyErr_Occurred() | |
| && PyErr_ExceptionMatches(PyExc_PermissionError); | |
| } | |
| static inline int | |
| _Py_RemoteDebug_IsFatalReadError(void) | |
| { | |
| return _Py_RemoteDebug_HasPermissionError() | |
| || PyErr_ExceptionMatches(PyExc_MemoryError) | |
| || PyErr_ExceptionMatches(PyExc_ProcessLookupError) | |
| || (PyErr_Occurred() && !PyErr_ExceptionMatches(PyExc_Exception)); | |
| } | |
| #define _set_debug_exception_cause(exception, format, ...) \ | |
| do { \ | |
| if (!_Py_RemoteDebug_IsFatalReadError()) { \ | |
| PyThreadState *tstate = _PyThreadState_GET(); \ | |
| if (!_PyErr_Occurred(tstate)) { \ | |
| _PyErr_Format(tstate, exception, format, ##__VA_ARGS__); \ | |
| } else { \ | |
| _PyErr_FormatFromCause(exception, format, ##__VA_ARGS__); \ | |
| } \ | |
| } \ | |
| } while (0) | |
| #define _set_debug_oserror_from_errno(err, format, ...) \ | |
| do { \ | |
| errno = (err); \ | |
| PyErr_SetFromErrno(PyExc_OSError); \ | |
| _set_debug_exception_cause(PyExc_OSError, format, ##__VA_ARGS__); \ | |
| } while (0) | |
| #define _set_debug_oserror_from_errno_with_filename(err, filename, format, ...) \ | |
| do { \ | |
| errno = (err); \ | |
| PyErr_SetFromErrnoWithFilename(PyExc_OSError, filename); \ | |
| _set_debug_exception_cause(PyExc_OSError, format, ##__VA_ARGS__); \ | |
| } while (0) | |
| static inline size_t | |
| get_page_size(void) { | |
| size_t page_size = 0; | |
| if (page_size == 0) { | |
| #ifdef MS_WINDOWS | |
| SYSTEM_INFO si; | |
| GetSystemInfo(&si); | |
| page_size = si.dwPageSize; | |
| #else | |
| long n = sysconf(_SC_PAGESIZE); | |
| page_size = (n > 0) ? (size_t)n : 4096; | |
| #endif | |
| } | |
| return page_size; | |
| } | |
| typedef struct page_cache_entry { | |
| uintptr_t page_addr; // page-aligned base address | |
| char *data; | |
| int valid; | |
| struct page_cache_entry *next; | |
| } page_cache_entry_t; | |
| #define MAX_PAGES 1024 | |
| // Define a platform-independent process handle structure | |
| typedef struct { | |
| pid_t pid; | |
| #if defined(__APPLE__) && defined(TARGET_OS_OSX) && TARGET_OS_OSX | |
| mach_port_t task; | |
| #elif defined(MS_WINDOWS) | |
| HANDLE hProcess; | |
| #elif defined(__linux__) | |
| int memfd; | |
| #endif | |
| page_cache_entry_t pages[MAX_PAGES]; | |
| int page_cache_count; | |
| 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. | |
| UNUSED 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) { | |
| if (!_Py_RemoteDebug_HasPermissionError()) { | |
| PyErr_Clear(); | |
| } | |
| return 0; | |
| } | |
| return memcmp(buf, _Py_Debug_Cookie, sizeof(buf)) == 0; | |
| } | |
| static void | |
| _Py_RemoteDebug_FreePageCache(proc_handle_t *handle) | |
| { | |
| for (int i = 0; i < MAX_PAGES; i++) { | |
| if (handle->pages[i].data) { | |
| PyMem_RawFree(handle->pages[i].data); | |
| } | |
| handle->pages[i].data = NULL; | |
| handle->pages[i].valid = 0; | |
| } | |
| handle->page_cache_count = 0; | |
| } | |
| UNUSED static void | |
| _Py_RemoteDebug_ClearCache(proc_handle_t *handle) | |
| { | |
| for (int i = 0; i < handle->page_cache_count; i++) { | |
| handle->pages[i].valid = 0; | |
| } | |
| handle->page_cache_count = 0; | |
| } | |
| #if defined(__APPLE__) && defined(TARGET_OS_OSX) && TARGET_OS_OSX | |
| static mach_port_t pid_to_task(pid_t pid); | |
| #endif | |
| // Initialize the process handle | |
| UNUSED static int | |
| _Py_RemoteDebug_InitProcHandle(proc_handle_t *handle, pid_t pid) { | |
| handle->pid = 0; | |
| #if defined(__APPLE__) && defined(TARGET_OS_OSX) && TARGET_OS_OSX | |
| handle->task = 0; | |
| #elif defined(MS_WINDOWS) | |
| handle->hProcess = NULL; | |
| #elif defined(__linux__) | |
| handle->memfd = -1; | |
| #endif | |
| handle->page_size = get_page_size(); | |
| handle->page_cache_count = 0; | |
| for (int i = 0; i < MAX_PAGES; i++) { | |
| handle->pages[i].data = NULL; | |
| handle->pages[i].valid = 0; | |
| } | |
| handle->pid = pid; | |
| #if defined(__APPLE__) && defined(TARGET_OS_OSX) && TARGET_OS_OSX | |
| handle->task = pid_to_task(handle->pid); | |
| if (handle->task == 0) { | |
| _set_debug_exception_cause(PyExc_RuntimeError, "Failed to initialize macOS process handle"); | |
| return -1; | |
| } | |
| #elif defined(MS_WINDOWS) | |
| handle->hProcess = OpenProcess( | |
| PROCESS_VM_READ | PROCESS_VM_WRITE | PROCESS_VM_OPERATION | PROCESS_QUERY_INFORMATION | PROCESS_SUSPEND_RESUME, | |
| FALSE, pid); | |
| if (handle->hProcess == NULL) { | |
| DWORD error = GetLastError(); | |
| PyErr_SetFromWindowsErr(error); | |
| _set_debug_exception_cause(PyExc_RuntimeError, "Failed to initialize Windows process handle"); | |
| return -1; | |
| } | |
| #endif | |
| return 0; | |
| } | |
| // Clean up the process handle | |
| UNUSED static void | |
| _Py_RemoteDebug_CleanupProcHandle(proc_handle_t *handle) { | |
| #ifdef MS_WINDOWS | |
| if (handle->hProcess != NULL) { | |
| 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); | |
| } | |
| #if defined(__APPLE__) && defined(TARGET_OS_OSX) && TARGET_OS_OSX | |
| static uintptr_t | |
| return_section_address64( | |
| const char* section, | |
| 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); | |
| 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) { | |
| // The section address only needs the image's ASLR slide. | |
| // VM regions need not match the segment's on-disk size. | |
| 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); | |
| } | |
| return 0; | |
| } | |
| static uintptr_t | |
| return_section_address32( | |
| const char* section, | |
| uintptr_t base, | |
| void* map | |
| ) { | |
| struct mach_header* hdr = (struct mach_header*)map; | |
| int ncmds = hdr->ncmds; | |
| int cmd_cnt = 0; | |
| struct segment_command* cmd = map + sizeof(struct mach_header); | |
| uintptr_t vmaddr = 0; | |
| for (int i = 0; cmd_cnt < 2 && i < ncmds; i++) { | |
| if (cmd->cmd == LC_SEGMENT && strcmp(cmd->segname, "__TEXT") == 0) { | |
| vmaddr = cmd->vmaddr; | |
| } | |
| if (cmd->cmd == LC_SEGMENT && strcmp(cmd->segname, "__DATA") == 0) { | |
| // The section address only needs the image's ASLR slide. | |
| // VM regions need not match the segment's on-disk size. | |
| int nsects = cmd->nsects; | |
| struct section* sec = (struct section*)( | |
| (void*)cmd + sizeof(struct segment_command) | |
| ); | |
| 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*)((void*)cmd + cmd->cmdsize); | |
| } | |
| return 0; | |
| } | |
| static uintptr_t | |
| return_section_address_fat( | |
| const char* section, | |
| uintptr_t base, | |
| void* map | |
| ) { | |
| struct fat_header* fat_hdr = (struct fat_header*)map; | |
| // Determine host CPU type for architecture selection | |
| cpu_type_t cpu; | |
| int is_abi64; | |
| size_t cpu_size = sizeof(cpu), abi64_size = sizeof(is_abi64); | |
| if (sysctlbyname("hw.cputype", &cpu, &cpu_size, NULL, 0) != 0) { | |
| int err = errno; | |
| _set_debug_oserror_from_errno(err, | |
| "Failed to determine CPU type via sysctlbyname " | |
| "for fat binary analysis at 0x%lx: %s", | |
| base, strerror(err)); | |
| return 0; | |
| } | |
| if (sysctlbyname("hw.cpu64bit_capable", &is_abi64, &abi64_size, NULL, 0) != 0) { | |
| int err = errno; | |
| _set_debug_oserror_from_errno(err, | |
| "Failed to determine CPU ABI capability via sysctlbyname " | |
| "for fat binary analysis at 0x%lx: %s", | |
| base, strerror(err)); | |
| return 0; | |
| } | |
| cpu |= is_abi64 * CPU_ARCH_ABI64; | |
| // Check endianness | |
| int swap = fat_hdr->magic == FAT_CIGAM; | |
| struct fat_arch* arch = (struct fat_arch*)(map + sizeof(struct fat_header)); | |
| // Get number of architectures in fat binary | |
| uint32_t nfat_arch = swap ? __builtin_bswap32(fat_hdr->nfat_arch) : fat_hdr->nfat_arch; | |
| // Search for matching architecture | |
| for (uint32_t i = 0; i < nfat_arch; i++) { | |
| cpu_type_t arch_cpu = swap ? __builtin_bswap32(arch[i].cputype) : arch[i].cputype; | |
| if (arch_cpu == cpu) { | |
| // Found matching architecture, now process it | |
| uint32_t offset = swap ? __builtin_bswap32(arch[i].offset) : arch[i].offset; | |
| struct mach_header_64* hdr = (struct mach_header_64*)(map + offset); | |
| // Determine which type of Mach-O it is and process accordingly | |
| switch (hdr->magic) { | |
| case MH_MAGIC: | |
| case MH_CIGAM: | |
| return return_section_address32(section, base, (void*)hdr); | |
| case MH_MAGIC_64: | |
| case MH_CIGAM_64: | |
| return return_section_address64(section, base, (void*)hdr); | |
| default: | |
| PyErr_Format(PyExc_RuntimeError, | |
| "Unknown Mach-O magic number 0x%x in fat binary architecture %u at base 0x%lx", | |
| hdr->magic, i, base); | |
| return 0; | |
| } | |
| } | |
| } | |
| PyErr_Format(PyExc_RuntimeError, | |
| "No matching architecture found for CPU type 0x%x " | |
| "in fat binary at base 0x%lx (%u architectures examined)", | |
| cpu, base, nfat_arch); | |
| return 0; | |
| } | |
| static uintptr_t | |
| search_section_in_file(const char* secname, char* path, uintptr_t base) | |
| { | |
| int fd = open(path, O_RDONLY); | |
| if (fd == -1) { | |
| int err = errno; | |
| _set_debug_oserror_from_errno_with_filename(err, path, | |
| "Cannot open binary file '%s' for section '%s' search: %s", | |
| path, secname, strerror(err)); | |
| return 0; | |
| } | |
| struct stat fs; | |
| if (fstat(fd, &fs) == -1) { | |
| int err = errno; | |
| _set_debug_oserror_from_errno_with_filename(err, path, | |
| "Cannot get file size for binary '%s' during section '%s' search: %s", | |
| path, secname, strerror(err)); | |
| close(fd); | |
| return 0; | |
| } | |
| void* map = mmap(0, fs.st_size, PROT_READ, MAP_SHARED, fd, 0); | |
| if (map == MAP_FAILED) { | |
| int err = errno; | |
| _set_debug_oserror_from_errno_with_filename(err, path, | |
| "Cannot memory map binary file '%s' (size: %lld bytes) for section '%s' search: %s", | |
| path, (long long)fs.st_size, secname, strerror(err)); | |
| close(fd); | |
| return 0; | |
| } | |
| uintptr_t result = 0; | |
| uint32_t magic = *(uint32_t*)map; | |
| switch (magic) { | |
| case MH_MAGIC: | |
| case MH_CIGAM: | |
| result = return_section_address32(secname, base, map); | |
| break; | |
| case MH_MAGIC_64: | |
| case MH_CIGAM_64: | |
| result = return_section_address64(secname, base, map); | |
| break; | |
| case FAT_MAGIC: | |
| case FAT_CIGAM: | |
| result = return_section_address_fat(secname, base, map); | |
| break; | |
| default: | |
| PyErr_Format(PyExc_RuntimeError, | |
| "Unrecognized Mach-O magic number 0x%x in binary file '%s' for section '%s' search", | |
| magic, path, secname); | |
| break; | |
| } | |
| if (munmap(map, fs.st_size) != 0) { | |
| if (!PyErr_Occurred()) { | |
| int err = errno; | |
| _set_debug_oserror_from_errno_with_filename(err, path, | |
| "Failed to unmap binary file '%s' (size: %lld bytes): %s", | |
| path, (long long)fs.st_size, strerror(err)); | |
| } | |
| result = 0; | |
| } | |
| if (close(fd) != 0) { | |
| if (!PyErr_Occurred()) { | |
| int err = errno; | |
| _set_debug_oserror_from_errno_with_filename(err, path, | |
| "Failed to close binary file '%s': %s", | |
| path, strerror(err)); | |
| } | |
| result = 0; | |
| } | |
| 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 port for PID %d (kern_return_t: %d). " | |
| "This typically requires running as root or having the 'com.apple.system-task-ports' entitlement.", | |
| pid, result); | |
| return 0; | |
| } | |
| return task; | |
| } | |
| static uintptr_t | |
| 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); | |
| 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) { | |
| if (!PyErr_Occurred()) { | |
| PyErr_Format(PyExc_PermissionError, | |
| "Cannot get task port for PID %d during section search", | |
| handle->pid); | |
| } | |
| return 0; | |
| } | |
| char map_filename[MAXPATHLEN + 1]; | |
| kern_return_t kr; | |
| while ((kr = 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 (strncmp(filename, substr, strlen(substr)) == 0) { | |
| PyErr_Clear(); | |
| uintptr_t result = search_section_in_file( | |
| secname, map_filename, address); | |
| if (result != 0) { | |
| if (validator == NULL || validator(handle, result)) { | |
| return result; | |
| } | |
| if (_Py_RemoteDebug_HasPermissionError()) { | |
| return 0; | |
| } | |
| } | |
| else if (_Py_RemoteDebug_HasPermissionError()) { | |
| return 0; | |
| } | |
| } | |
| address += size; | |
| } | |
| if (kr != KERN_INVALID_ADDRESS && !PyErr_Occurred()) { | |
| PyErr_Format(PyExc_RuntimeError, | |
| "mach_vm_region failed while searching PID %d for section '%s' " | |
| "(kern_return_t: %d)", | |
| handle->pid, secname, kr); | |
| } | |
| return 0; | |
| } | |
| #endif // (__APPLE__ && defined(TARGET_OS_OSX) && 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) { | |
| int err = errno; | |
| _set_debug_oserror_from_errno_with_filename(err, elf_file, | |
| "Cannot open ELF file '%s' for section '%s' search: %s", | |
| elf_file, secname, strerror(err)); | |
| goto exit; | |
| } | |
| struct stat file_stats; | |
| if (fstat(fd, &file_stats) != 0) { | |
| int err = errno; | |
| _set_debug_oserror_from_errno_with_filename(err, elf_file, | |
| "Cannot get file size for ELF file '%s' during section '%s' search: %s", | |
| elf_file, secname, strerror(err)); | |
| goto exit; | |
| } | |
| file_memory = mmap(NULL, file_stats.st_size, PROT_READ, MAP_PRIVATE, fd, 0); | |
| if (file_memory == MAP_FAILED) { | |
| int err = errno; | |
| _set_debug_oserror_from_errno_with_filename(err, elf_file, | |
| "Cannot memory map ELF file '%s' (size: %lld bytes) for section '%s' search: %s", | |
| elf_file, (long long)file_stats.st_size, secname, strerror(err)); | |
| file_memory = NULL; | |
| goto exit; | |
| } | |
| Elf_Ehdr* elf_header = (Elf_Ehdr*)file_memory; | |
| // Validate ELF header | |
| if (elf_header->e_shstrndx >= elf_header->e_shnum) { | |
| PyErr_Format(PyExc_RuntimeError, | |
| "Invalid ELF file '%s': string table index %u >= section count %u", | |
| elf_file, elf_header->e_shstrndx, elf_header->e_shnum); | |
| goto exit; | |
| } | |
| 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; | |
| } | |
| } | |
| if (section == NULL) { | |
| goto exit; | |
| } | |
| 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 (first_load_segment == NULL) { | |
| PyErr_Format(PyExc_RuntimeError, | |
| "No PT_LOAD segment found in ELF file '%s' (%u program headers examined)", | |
| elf_file, elf_header->e_phnum); | |
| goto exit; | |
| } | |
| 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) { | |
| if (munmap(file_memory, file_stats.st_size) != 0) { | |
| if (!PyErr_Occurred()) { | |
| int err = errno; | |
| _set_debug_oserror_from_errno_with_filename(err, elf_file, | |
| "Failed to unmap ELF file '%s' (size: %lld bytes): %s", | |
| elf_file, (long long)file_stats.st_size, strerror(err)); | |
| } | |
| result = 0; | |
| } | |
| } | |
| if (fd >= 0 && close(fd) != 0) { | |
| if (!PyErr_Occurred()) { | |
| int err = errno; | |
| _set_debug_oserror_from_errno_with_filename(err, elf_file, | |
| "Failed to close ELF file '%s': %s", | |
| elf_file, strerror(err)); | |
| } | |
| result = 0; | |
| } | |
| return result; | |
| } | |
| static const char * | |
| find_debug_cookie(const char *buffer, size_t len) | |
| { | |
| const char *cookie = _Py_Debug_Cookie; | |
| const size_t cookie_len = sizeof(_Py_Debug_Cookie) - 1; | |
| if (len < cookie_len) { | |
| return NULL; | |
| } | |
| size_t pos = 0; | |
| size_t last = len - cookie_len; | |
| while (pos <= last) { | |
| const char *candidate = memchr( | |
| buffer + pos, cookie[0], last - pos + 1); | |
| if (candidate == NULL) { | |
| return NULL; | |
| } | |
| pos = (size_t)(candidate - buffer); | |
| if (memcmp(candidate, cookie, cookie_len) == 0) { | |
| return candidate; | |
| } | |
| pos++; | |
| } | |
| return NULL; | |
| } | |
| static int | |
| linux_map_path_is_deleted(const char *path) | |
| { | |
| static const char deleted_suffix[] = " (deleted)"; | |
| size_t path_len = strlen(path); | |
| size_t suffix_len = sizeof(deleted_suffix) - 1; | |
| return path_len >= suffix_len | |
| && strcmp(path + path_len - suffix_len, deleted_suffix) == 0; | |
| } | |
| static int | |
| linux_map_perms_are_readwrite(const char *perms) | |
| { | |
| return perms[0] == 'r' && perms[1] == 'w'; | |
| } | |
| static uintptr_t | |
| scan_linux_mapping_for_pyruntime_cookie( | |
| proc_handle_t *handle, | |
| uintptr_t start, | |
| uintptr_t end) | |
| { | |
| if (end <= start) { | |
| return 0; | |
| } | |
| const size_t cookie_len = sizeof(_Py_Debug_Cookie) - 1; | |
| const size_t overlap = cookie_len - 1; | |
| const size_t chunk_size = 1024 * 1024; | |
| char *buffer = PyMem_Malloc(chunk_size); | |
| if (buffer == NULL) { | |
| PyErr_NoMemory(); | |
| _set_debug_exception_cause(PyExc_MemoryError, | |
| "Cannot allocate memory while scanning PID %d for PyRuntime cookie", | |
| handle->pid); | |
| return 0; | |
| } | |
| uintptr_t retval = 0; | |
| uintptr_t mapping_size = end - start; | |
| uintptr_t offset = 0; | |
| while (offset < mapping_size) { | |
| uintptr_t remaining = mapping_size - offset; | |
| size_t wanted = remaining > chunk_size | |
| ? chunk_size : (size_t)remaining; | |
| if (_Py_RemoteDebug_ReadRemoteMemory( | |
| handle, start + offset, wanted, buffer) < 0) { | |
| if (_Py_RemoteDebug_HasPermissionError()) { | |
| goto exit; | |
| } | |
| // A candidate mapping can disappear or contain unreadable holes while | |
| // the target process keeps running. Treat those as non-matches and | |
| // keep scanning other candidate mappings. | |
| PyErr_Clear(); | |
| } | |
| else { | |
| const char *hit = find_debug_cookie(buffer, wanted); | |
| if (hit != NULL) { | |
| retval = start + offset + (uintptr_t)(hit - buffer); | |
| goto exit; | |
| } | |
| } | |
| if (wanted <= overlap) { | |
| break; | |
| } | |
| offset += wanted - overlap; | |
| } | |
| exit: | |
| PyMem_Free(buffer); | |
| return retval; | |
| } | |
| static uintptr_t | |
| 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); | |
| FILE* maps_file = fopen(maps_file_path, "r"); | |
| if (maps_file == NULL) { | |
| int err = errno; | |
| _set_debug_oserror_from_errno_with_filename(err, maps_file_path, | |
| "Cannot open process memory map file '%s' for PID %d section search: %s", | |
| maps_file_path, handle->pid, strerror(err)); | |
| return 0; | |
| } | |
| size_t linelen = 0; | |
| size_t linesz = PATH_MAX; | |
| char *line = PyMem_Malloc(linesz); | |
| if (!line) { | |
| fclose(maps_file); | |
| _set_debug_exception_cause(PyExc_MemoryError, | |
| "Cannot allocate memory for reading process map file '%s'", | |
| maps_file_path); | |
| 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); | |
| _set_debug_exception_cause(PyExc_MemoryError, | |
| "Cannot reallocate memory while reading process map file '%s' (attempted size: %zu)", | |
| maps_file_path, linesz); | |
| 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 end = 0; | |
| int path_pos = 0; | |
| char perms[5] = ""; | |
| int fields = sscanf(line, "%lx-%lx %4s %*s %*s %*s %n", | |
| &start, &end, perms, &path_pos); | |
| if (fields < 3 || !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; | |
| if (path[0] == '\0') { | |
| continue; | |
| } | |
| if (path[0] == '[' && path[strlen(path)-1] == ']') { | |
| // Skip [heap], [stack], [anon:cpython:pymalloc], etc. | |
| continue; | |
| } | |
| const char *filename = strrchr(path, '/'); | |
| if (filename) { | |
| filename++; // Move past the '/' | |
| } else { | |
| filename = path; // No directories, or an empty string | |
| } | |
| if (strstr(filename, substr)) { | |
| int deleted_pyruntime_mapping = | |
| strcmp(secname, "PyRuntime") == 0 | |
| && linux_map_path_is_deleted(path); | |
| if (deleted_pyruntime_mapping | |
| && linux_map_perms_are_readwrite(perms)) { | |
| PyErr_Clear(); | |
| retval = scan_linux_mapping_for_pyruntime_cookie( | |
| handle, (uintptr_t)start, (uintptr_t)end); | |
| } | |
| if (!deleted_pyruntime_mapping | |
| && retval == 0 && !PyErr_Occurred()) { | |
| PyErr_Clear(); | |
| retval = search_elf_file_for_section( | |
| handle, secname, start, path); | |
| } | |
| if (retval) { | |
| if (validator == NULL || validator(handle, retval)) { | |
| break; | |
| } | |
| if (_Py_RemoteDebug_HasPermissionError()) { | |
| retval = 0; | |
| break; | |
| } | |
| } | |
| else if (_Py_RemoteDebug_HasPermissionError()) { | |
| break; | |
| } | |
| retval = 0; | |
| } | |
| } | |
| if (retval == 0 && !PyErr_Occurred() && ferror(maps_file)) { | |
| int err = errno; | |
| _set_debug_oserror_from_errno_with_filename(err, maps_file_path, | |
| "Failed to read process map file '%s' for PID %d section search: %s", | |
| maps_file_path, handle->pid, strerror(err)); | |
| } | |
| PyMem_Free(line); | |
| if (fclose(maps_file) != 0) { | |
| if (!PyErr_Occurred()) { | |
| int err = errno; | |
| _set_debug_oserror_from_errno_with_filename(err, maps_file_path, | |
| "Failed to close process map file '%s': %s", | |
| maps_file_path, strerror(err)); | |
| } | |
| retval = 0; | |
| } | |
| return retval; | |
| } | |
| #endif // __linux__ | |
| #ifdef MS_WINDOWS | |
| static int is_process_alive(HANDLE hProcess) { | |
| DWORD exitCode; | |
| if (GetExitCodeProcess(hProcess, &exitCode)) { | |
| return exitCode == STILL_ACTIVE; | |
| } | |
| return 0; | |
| } | |
| 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) { | |
| DWORD error = GetLastError(); | |
| PyErr_SetFromWindowsErr(error); | |
| _set_debug_exception_cause(PyExc_OSError, | |
| "Cannot open PE file for section '%s' analysis (error %lu)", | |
| secname, error); | |
| return NULL; | |
| } | |
| HANDLE hMap = CreateFileMapping(hFile, NULL, PAGE_READONLY, 0, 0, 0); | |
| if (!hMap) { | |
| DWORD error = GetLastError(); | |
| PyErr_SetFromWindowsErr(error); | |
| _set_debug_exception_cause(PyExc_OSError, | |
| "Cannot create file mapping for PE file section '%s' analysis (error %lu)", | |
| secname, error); | |
| CloseHandle(hFile); | |
| return NULL; | |
| } | |
| BYTE* mapView = (BYTE*)MapViewOfFile(hMap, FILE_MAP_READ, 0, 0, 0); | |
| if (!mapView) { | |
| DWORD error = GetLastError(); | |
| PyErr_SetFromWindowsErr(error); | |
| _set_debug_exception_cause(PyExc_OSError, | |
| "Cannot map view of PE file for section '%s' analysis (error %lu)", | |
| secname, error); | |
| CloseHandle(hMap); | |
| CloseHandle(hFile); | |
| return NULL; | |
| } | |
| IMAGE_DOS_HEADER* pDOSHeader = (IMAGE_DOS_HEADER*)mapView; | |
| if (pDOSHeader->e_magic != IMAGE_DOS_SIGNATURE) { | |
| PyErr_Format(PyExc_RuntimeError, | |
| "Invalid DOS signature (0x%x) in PE file for section '%s' analysis (expected 0x%x)", | |
| pDOSHeader->e_magic, secname, IMAGE_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_Format(PyExc_RuntimeError, | |
| "Invalid NT signature (0x%lx) in PE file for section '%s' analysis (expected 0x%lx)", | |
| pNTHeaders->Signature, secname, IMAGE_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, | |
| section_validator_t validator) { | |
| HANDLE hProcSnap; | |
| do { | |
| hProcSnap = CreateToolhelp32Snapshot(TH32CS_SNAPMODULE, handle->pid); | |
| } while (hProcSnap == INVALID_HANDLE_VALUE && GetLastError() == ERROR_BAD_LENGTH); | |
| if (hProcSnap == INVALID_HANDLE_VALUE) { | |
| DWORD error = GetLastError(); | |
| PyErr_SetFromWindowsErr(error); | |
| _set_debug_exception_cause(PyExc_OSError, | |
| "Unable to create module snapshot for PID %d section '%s' " | |
| "search (error %lu). Check permissions or PID validity", | |
| handle->pid, secname, error); | |
| return 0; | |
| } | |
| MODULEENTRY32W moduleEntry; | |
| moduleEntry.dwSize = sizeof(moduleEntry); | |
| void* runtime_addr = NULL; | |
| if (!Module32FirstW(hProcSnap, &moduleEntry)) { | |
| DWORD error = GetLastError(); | |
| PyErr_SetFromWindowsErr(error); | |
| _set_debug_exception_cause(PyExc_OSError, | |
| "Unable to enumerate modules for PID %d section '%s' " | |
| "search (error %lu)", | |
| handle->pid, secname, error); | |
| CloseHandle(hProcSnap); | |
| return 0; | |
| } | |
| do { | |
| // Look for either python executable or DLL | |
| if (wcsstr(moduleEntry.szModule, substr)) { | |
| PyErr_Clear(); | |
| void *candidate = analyze_pe(moduleEntry.szExePath, moduleEntry.modBaseAddr, secname); | |
| if (candidate != NULL) { | |
| if (validator == NULL || validator(handle, (uintptr_t)candidate)) { | |
| runtime_addr = candidate; | |
| break; | |
| } | |
| if (_Py_RemoteDebug_HasPermissionError()) { | |
| break; | |
| } | |
| } | |
| else if (_Py_RemoteDebug_HasPermissionError()) { | |
| break; | |
| } | |
| } | |
| } while (Module32NextW(hProcSnap, &moduleEntry)); | |
| if (runtime_addr == NULL && !PyErr_Occurred()) { | |
| DWORD error = GetLastError(); | |
| if (error != ERROR_NO_MORE_FILES) { | |
| PyErr_SetFromWindowsErr(error); | |
| _set_debug_exception_cause(PyExc_OSError, | |
| "Module enumeration failed for PID %d section '%s' " | |
| "search (error %lu)", | |
| handle->pid, secname, error); | |
| } | |
| } | |
| CloseHandle(hProcSnap); | |
| return (uintptr_t)runtime_addr; | |
| } | |
| #endif // MS_WINDOWS | |
| // Get the PyRuntime section address for any platform | |
| UNUSED static uintptr_t | |
| _Py_RemoteDebug_GetPyRuntimeAddress(proc_handle_t* handle) | |
| { | |
| uintptr_t address; | |
| #ifdef MS_WINDOWS | |
| // On Windows, search for 'python' in executable or DLL | |
| address = search_windows_map_for_section(handle, "PyRuntime", L"python", | |
| _Py_RemoteDebug_ValidatePyRuntimeCookie); | |
| if (address == 0) { | |
| if (!_Py_RemoteDebug_HasPermissionError()) { | |
| // Error out: 'python' substring covers both executable and DLL | |
| PyObject *exc = PyErr_GetRaisedException(); | |
| PyErr_Format(PyExc_RuntimeError, | |
| "Failed to find the PyRuntime section in process %d on Windows platform", | |
| handle->pid); | |
| _PyErr_ChainExceptions1(exc); | |
| } | |
| } | |
| #elif defined(__linux__) && HAVE_PROCESS_VM_READV | |
| // On Linux, search for 'python' in executable or DLL | |
| address = search_linux_map_for_section(handle, "PyRuntime", "python", | |
| _Py_RemoteDebug_ValidatePyRuntimeCookie); | |
| if (address == 0) { | |
| if (!_Py_RemoteDebug_HasPermissionError()) { | |
| // Error out: 'python' substring covers both executable and DLL | |
| PyObject *exc = PyErr_GetRaisedException(); | |
| PyErr_Format(PyExc_RuntimeError, | |
| "Failed to find the PyRuntime section in process %d on Linux platform", | |
| handle->pid); | |
| _PyErr_ChainExceptions1(exc); | |
| } | |
| } | |
| #elif defined(__APPLE__) && defined(TARGET_OS_OSX) && TARGET_OS_OSX | |
| // On macOS, try libpython first, then fall back to python | |
| const char* candidates[] = {"libpython", "python", "Python", NULL}; | |
| for (const char** candidate = candidates; *candidate; candidate++) { | |
| PyErr_Clear(); | |
| address = search_map_for_section(handle, "PyRuntime", *candidate, | |
| _Py_RemoteDebug_ValidatePyRuntimeCookie); | |
| if (address != 0 || _Py_RemoteDebug_HasPermissionError()) { | |
| break; | |
| } | |
| } | |
| if (address == 0) { | |
| if (!_Py_RemoteDebug_HasPermissionError()) { | |
| PyObject *exc = PyErr_GetRaisedException(); | |
| PyErr_Format(PyExc_RuntimeError, | |
| "Failed to find the PyRuntime section in process %d " | |
| "on macOS platform (tried both libpython and python)", | |
| handle->pid); | |
| _PyErr_ChainExceptions1(exc); | |
| } | |
| } | |
| #else | |
| _set_debug_exception_cause(PyExc_RuntimeError, | |
| "Reading the PyRuntime section is not supported on this platform"); | |
| return 0; | |
| #endif | |
| 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) { | |
| int err = errno; | |
| _set_debug_oserror_from_errno_with_filename(err, mem_file_path, | |
| "failed to open file %s: %s", mem_file_path, strerror(err)); | |
| 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 (len == 0) { | |
| return 0; | |
| } | |
| 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) { | |
| int err = errno; | |
| errno = err; | |
| 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(err)); | |
| return -1; | |
| } | |
| if (read_bytes == 0) { | |
| PyErr_Format(PyExc_OSError, | |
| "preadv returned 0 bytes for PID %d at address 0x%lx " | |
| "(size %zu, partial read %zd bytes)", | |
| handle->pid, remote_address + result, len - result, result); | |
| 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) | |
| { | |
| if (len == 0) { | |
| return 0; | |
| } | |
| #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)) { | |
| DWORD error = GetLastError(); | |
| // Check if the process is still alive: we need to be able to tell our caller | |
| // that the process is dead and not just that the read failed. | |
| if (!is_process_alive(handle->hProcess)) { | |
| _set_errno(ESRCH); | |
| PyErr_SetFromErrno(PyExc_OSError); | |
| return -1; | |
| } | |
| PyErr_SetFromWindowsErr(error); | |
| _set_debug_exception_cause(PyExc_OSError, | |
| "ReadProcessMemory failed for PID %d at address 0x%lx " | |
| "(size %zu, partial read %zu bytes): Windows error %lu", | |
| handle->pid, remote_address + result, len - result, result, error); | |
| return -1; | |
| } | |
| if (read_bytes == 0) { | |
| PyErr_Format(PyExc_OSError, | |
| "ReadProcessMemory returned 0 bytes for PID %d at address 0x%lx " | |
| "(size %zu, partial read %zu bytes)", | |
| handle->pid, remote_address + result, len - result, result); | |
| return -1; | |
| } | |
| result += read_bytes; | |
| } 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; | |
| 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) { | |
| int err = errno; | |
| if (err == ENOSYS) { | |
| return read_remote_memory_fallback(handle, remote_address, len, dst); | |
| } | |
| errno = err; | |
| PyErr_SetFromErrno(PyExc_OSError); | |
| if (err == ESRCH) { | |
| return -1; | |
| } | |
| _set_debug_exception_cause(PyExc_OSError, | |
| "process_vm_readv failed for PID %d at address 0x%lx " | |
| "(size %zu, partial read %zd bytes): %s", | |
| handle->pid, remote_address + result, len - result, result, strerror(err)); | |
| return -1; | |
| } | |
| if (read_bytes == 0) { | |
| PyErr_Format(PyExc_OSError, | |
| "process_vm_readv returned 0 bytes for PID %d at address 0x%lx " | |
| "(size %zu, partial read %zd bytes)", | |
| handle->pid, remote_address + result, len - result, result); | |
| return -1; | |
| } | |
| result += read_bytes; | |
| } while ((size_t)read_bytes != local[0].iov_len); | |
| return 0; | |
| #elif defined(__APPLE__) && defined(TARGET_OS_OSX) && TARGET_OS_OSX | |
| mach_vm_size_t bytes_read = 0; | |
| kern_return_t kr = mach_vm_read_overwrite( | |
| handle->task, | |
| (mach_vm_address_t)remote_address, | |
| len, | |
| (mach_vm_address_t)dst, | |
| &bytes_read); | |
| if (kr != KERN_SUCCESS) { | |
| switch (err_get_code(kr)) { | |
| case KERN_PROTECTION_FAILURE: | |
| PyErr_Format(PyExc_PermissionError, | |
| "Memory protection failure reading from PID %d at address " | |
| "0x%lx (size %zu): insufficient permissions", | |
| handle->pid, remote_address, len); | |
| break; | |
| case KERN_INVALID_ARGUMENT: { | |
| // Perform a task_info check to see if the invalid argument is due | |
| // to the process being terminated | |
| task_basic_info_data_t task_basic_info; | |
| mach_msg_type_number_t task_info_count = TASK_BASIC_INFO_COUNT; | |
| kern_return_t task_valid_check = task_info(handle->task, TASK_BASIC_INFO, | |
| (task_info_t)&task_basic_info, | |
| &task_info_count); | |
| if (task_valid_check == KERN_INVALID_ARGUMENT) { | |
| PyErr_Format(PyExc_ProcessLookupError, | |
| "Process %d is no longer accessible (process terminated)", | |
| handle->pid); | |
| } else { | |
| PyErr_Format(PyExc_ValueError, | |
| "Invalid argument to mach_vm_read_overwrite for PID %d at " | |
| "address 0x%lx (size %zu) - check memory permissions", | |
| handle->pid, remote_address, len); | |
| } | |
| break; | |
| } | |
| case KERN_NO_SPACE: | |
| case KERN_MEMORY_ERROR: | |
| PyErr_Format(PyExc_ProcessLookupError, | |
| "Process %d memory space no longer available (process terminated)", | |
| handle->pid); | |
| break; | |
| default: | |
| PyErr_Format(PyExc_RuntimeError, | |
| "mach_vm_read_overwrite failed for PID %d at address 0x%lx " | |
| "(size %zu): kern_return_t %d", | |
| handle->pid, remote_address, len, kr); | |
| } | |
| return -1; | |
| } | |
| if (bytes_read != (mach_vm_size_t)len) { | |
| PyErr_Format(PyExc_OSError, | |
| "mach_vm_read_overwrite read %llu of %zu bytes for PID %d at " | |
| "address 0x%lx", | |
| (unsigned long long)bytes_read, len, handle->pid, remote_address); | |
| return -1; | |
| } | |
| return 0; | |
| #else | |
| Py_UNREACHABLE(); | |
| #endif | |
| } | |
| #if defined(__linux__) && HAVE_PROCESS_VM_READV | |
| // Fallback write using /proc/pid/mem | |
| static int | |
| _Py_RemoteDebug_WriteRemoteMemoryFallback(proc_handle_t *handle, uintptr_t remote_address, size_t len, const void* src) | |
| { | |
| if (len == 0) { | |
| return 0; | |
| } | |
| 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) { | |
| int err = errno; | |
| errno = err; | |
| PyErr_SetFromErrno(PyExc_OSError); | |
| return -1; | |
| } | |
| if (written == 0) { | |
| PyErr_Format(PyExc_OSError, | |
| "pwritev wrote 0 bytes for PID %d at address 0x%lx " | |
| "(size %zu, partial write %zd bytes)", | |
| handle->pid, remote_address + result, len - result, result); | |
| return -1; | |
| } | |
| result += written; | |
| } while ((size_t)written != local[0].iov_len); | |
| return 0; | |
| } | |
| #endif // __linux__ | |
| // Platform-independent memory write function | |
| UNUSED static int | |
| _Py_RemoteDebug_WriteRemoteMemory(proc_handle_t *handle, uintptr_t remote_address, size_t len, const void* src) | |
| { | |
| if (len == 0) { | |
| return 0; | |
| } | |
| #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)) { | |
| DWORD error = GetLastError(); | |
| PyErr_SetFromWindowsErr(error); | |
| _set_debug_exception_cause(PyExc_OSError, | |
| "WriteProcessMemory failed for PID %d at address 0x%lx " | |
| "(size %zu, partial write %zu bytes): Windows error %lu", | |
| handle->pid, remote_address + result, len - result, result, error); | |
| return -1; | |
| } | |
| if (written == 0) { | |
| PyErr_Format(PyExc_OSError, | |
| "WriteProcessMemory wrote 0 bytes for PID %d at address 0x%lx " | |
| "(size %zu, partial write %zu bytes)", | |
| handle->pid, remote_address + result, len - result, result); | |
| return -1; | |
| } | |
| result += written; | |
| } while (result < len); | |
| return 0; | |
| #elif defined(__linux__) && HAVE_PROCESS_VM_READV | |
| if (handle->memfd != -1) { | |
| return _Py_RemoteDebug_WriteRemoteMemoryFallback(handle, remote_address, len, src); | |
| } | |
| 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) { | |
| int err = errno; | |
| if (err == ENOSYS) { | |
| return _Py_RemoteDebug_WriteRemoteMemoryFallback(handle, remote_address, len, src); | |
| } | |
| errno = err; | |
| PyErr_SetFromErrno(PyExc_OSError); | |
| _set_debug_exception_cause(PyExc_OSError, | |
| "process_vm_writev failed for PID %d at address 0x%lx " | |
| "(size %zu, partial write %zd bytes): %s", | |
| handle->pid, remote_address + result, len - result, result, strerror(err)); | |
| return -1; | |
| } | |
| if (written == 0) { | |
| PyErr_Format(PyExc_OSError, | |
| "process_vm_writev wrote 0 bytes for PID %d at address 0x%lx " | |
| "(size %zu, partial write %zd bytes)", | |
| handle->pid, remote_address + result, len - result, result); | |
| return -1; | |
| } | |
| result += written; | |
| } while ((size_t)written != local[0].iov_len); | |
| return 0; | |
| #elif defined(__APPLE__) && defined(TARGET_OS_OSX) && TARGET_OS_OSX | |
| kern_return_t kr = mach_vm_write( | |
| handle->task, | |
| (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 | |
| } | |
| UNUSED static int | |
| _Py_RemoteDebug_PagedReadRemoteMemory(proc_handle_t *handle, | |
| uintptr_t addr, | |
| size_t size, | |
| void *out) | |
| { | |
| size_t page_size = handle->page_size; | |
| uintptr_t page_base = addr & ~(page_size - 1); | |
| size_t offset_in_page = addr - page_base; | |
| if (offset_in_page + size > page_size) { | |
| return _Py_RemoteDebug_ReadRemoteMemory(handle, addr, size, out); | |
| } | |
| // Search only the pages used since the last clear. The cache is cleared | |
| // between profiler samples, so entries are packed at the front. | |
| for (int i = 0; i < handle->page_cache_count; i++) { | |
| page_cache_entry_t *entry = &handle->pages[i]; | |
| if (entry->valid && entry->page_addr == page_base) { | |
| memcpy(out, entry->data + offset_in_page, size); | |
| return 0; | |
| } | |
| } | |
| if (handle->page_cache_count < MAX_PAGES) { | |
| page_cache_entry_t *entry = &handle->pages[handle->page_cache_count]; | |
| if (entry->data == NULL) { | |
| entry->data = PyMem_RawMalloc(page_size); | |
| if (entry->data == NULL) { | |
| PyErr_NoMemory(); | |
| _set_debug_exception_cause(PyExc_MemoryError, | |
| "Cannot allocate %zu bytes for page cache entry " | |
| "during read from PID %d at address 0x%lx", | |
| page_size, handle->pid, addr); | |
| return -1; | |
| } | |
| } | |
| if (_Py_RemoteDebug_ReadRemoteMemory(handle, page_base, page_size, entry->data) < 0) { | |
| // Try to just copy the exact amount as a fallback | |
| PyErr_Clear(); | |
| goto fallback; | |
| } | |
| entry->page_addr = page_base; | |
| entry->valid = 1; | |
| handle->page_cache_count++; | |
| memcpy(out, entry->data + offset_in_page, size); | |
| return 0; | |
| } | |
| fallback: | |
| // Cache full — fallback to uncached read | |
| return _Py_RemoteDebug_ReadRemoteMemory(handle, addr, size, out); | |
| } | |
| typedef struct { | |
| uintptr_t remote_addr; | |
| void *local_buf; | |
| size_t size; | |
| } _Py_RemoteReadSegment; | |
| #define _PY_REMOTE_DEBUG_MAX_BATCHED_SEGMENTS 4 | |
| // Batched read of multiple remote regions in a single syscall when supported. | |
| // Returns total bytes read (>= 0) on success, -1 if batched reads are | |
| // unavailable or the syscall failed. Callers compare the return value against | |
| // cumulative segment sizes to determine which segments were fully populated. | |
| UNUSED static Py_ssize_t | |
| _Py_RemoteDebug_BatchedReadRemoteMemory( | |
| proc_handle_t *handle, | |
| const _Py_RemoteReadSegment *segments, | |
| int nsegs) | |
| { | |
| #if defined(__linux__) && HAVE_PROCESS_VM_READV | |
| if (handle->memfd == -1 | |
| && nsegs > 0 | |
| && nsegs <= _PY_REMOTE_DEBUG_MAX_BATCHED_SEGMENTS) { | |
| struct iovec local[_PY_REMOTE_DEBUG_MAX_BATCHED_SEGMENTS]; | |
| struct iovec remote[_PY_REMOTE_DEBUG_MAX_BATCHED_SEGMENTS]; | |
| for (int i = 0; i < nsegs; i++) { | |
| local[i].iov_base = segments[i].local_buf; | |
| local[i].iov_len = segments[i].size; | |
| remote[i].iov_base = (void *)segments[i].remote_addr; | |
| remote[i].iov_len = segments[i].size; | |
| } | |
| ssize_t nread = process_vm_readv(handle->pid, local, nsegs, remote, nsegs, 0); | |
| if (nread >= 0) { | |
| return (Py_ssize_t)nread; | |
| } | |
| } | |
| #else | |
| (void)handle; | |
| (void)segments; | |
| (void)nsegs; | |
| #endif | |
| return -1; | |
| } | |
| UNUSED static int | |
| _Py_RemoteDebug_ReadDebugOffsets( | |
| proc_handle_t *handle, | |
| uintptr_t *runtime_start_address, | |
| _Py_DebugOffsets* debug_offsets | |
| ) { | |
| *runtime_start_address = _Py_RemoteDebug_GetPyRuntimeAddress(handle); | |
| if (!*runtime_start_address) { | |
| if (!PyErr_Occurred()) { | |
| PyErr_Format(PyExc_RuntimeError, | |
| "Failed to locate PyRuntime address for PID %d", | |
| handle->pid); | |
| } | |
| _set_debug_exception_cause(PyExc_RuntimeError, "PyRuntime address lookup failed during debug offsets initialization"); | |
| return -1; | |
| } | |
| size_t size = sizeof(struct _Py_DebugOffsets); | |
| if (0 != _Py_RemoteDebug_ReadRemoteMemory(handle, *runtime_start_address, size, debug_offsets)) { | |
| _set_debug_exception_cause(PyExc_RuntimeError, "Failed to read debug offsets structure from remote process"); | |
| return -1; | |
| } | |
| return 0; | |
| } | |
| #ifdef __cplusplus | |
| } | |
| #endif |