Files
cpython/Python/remote_debug.h
T
+2 4ceb3faf2c [3.15] Restore deferred backports for 3.15.1, including GH-158874 (#158882)
* [3.15] gh-153364: Make frame, coroutine, and task-waiter chain walks iterative and bounded (GH-153365) (#158813)

gh-153364: Make frame, coroutine, and task-waiter chain walks iterative and bounded (GH-153365)

* let me declare single limit

* use our new limit in process_frame_chain()

* add it in parse_async_frame_chain()

* parse_coro_chain()

* NEWS

* async in the message?

* test

* no race

* process_task_awaited_by

* process_task_awaited_by limit test

* NEWS

* MAX_TASK_WAITER_CHAIN_DEPTH

* TASK_WAITER_CHAIN_DEPTH in test

* TASK_WAITER_CHAIN_DEPTH 256

* prevent the drift with the comment

* better naming, better style

* MAX_TASK_WAITER_CHAIN_DEPTH comment

* task-waiter iterative bfs walk

* iterative coro-walk

* nicer news

* 1 << 14

* comment

* unused read_Py_ssize_t

* fix tombstones

* simplify

* correct msg

* better test

* news for tombstones

* left-over from when testing buggy version

* redundant new line
(cherry picked from commit e0861c6ae7)

Co-authored-by: Maurycy Pawłowski-Wieroński <maurycy@maurycy.com>
(cherry picked from commit 9e401cf7f5)

* [3.15] gh-155811: Add a seqcount to `gc_stats` to prevent torn reads (GH-155828) (#158829)

* update_seq

* no need for XCHGL, MOVL is enough?

* gh-155811: Retry an inconsistent GC snapshot once

---------
(cherry picked from commit 5fecd448bb)

Co-authored-by: Pablo Galindo Salgado <Pablogsal@gmail.com>
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
(cherry picked from commit 028124039c)

* [3.15] gh-151292: `_remote_debugging`: Do not corrupt the binary file when hitting `OverflowError` (GH-152892) (#158830)

gh-151292: `_remote_debugging`: Do not corrupt the binary file when hitting `OverflowError` (GH-152892)

* the kolektor

* test

* better test

* news

* =Q, move const to the base, not self.running

* gh-151292: Track binary writer finalization state

---------
(cherry picked from commit f839c061d3)

Co-authored-by: Maurycy Pawłowski-Wieroński <maurycy@maurycy.com>
Co-authored-by: Pablo Galindo Salgado <Pablogsal@gmail.com>
(cherry picked from commit 8a7c23f316)

* [3.15] gh-158583: Fix uninitialized memory read in bytes.fromhex() (GH-158584) (#158691)

gh-158583: Fix uninitialized memory read in bytes.fromhex() (GH-158584)
(cherry picked from commit 9d22a5334b)

Co-authored-by: Victor Stinner <vstinner@python.org>
(cherry picked from commit 4f7af46e25)

* [3.15] gh-154194: Degrade frames in Tachyon instead of failing the sample (GH-154195) (#158831)

* gh-154194: Degrade frames in Tachyon instead of failing the sample (#154195)

* degrade gracefully

* news

* better NEWS wording

* do not raise on MAX_REMOTE_STR_READ

* bye MAX_REMOTE_STR_READ

* fix -m asyncio ps|pstree

* test truncation and linetable sentinel

* simpler

* simpler

* redundant now

* respect #157790 in the news

---------

Co-authored-by: Pablo Galindo Salgado <Pablogsal@gmail.com>
(cherry picked from commit 7d25916b41)

* Preserve the stable ABI when creating fallback frame names

---------

Co-authored-by: Maurycy Pawłowski-Wieroński <maurycy@maurycy.com>
(cherry picked from commit c27f494061)

* [3.15] Add MSan to CI (GH-158625) (#158832)

Run a job that the test suite with MSan to the CI (#158625)

* Run the test suite with MSan in CI

* Additional fixes

* Add `_Py_MSAN_UNPOISON_STRING`

* Apply Victor's suggestions

* Apply Victor's suggestions

---------

(cherry picked from commit b93fb19a6e)

Co-authored-by: Stan Ulbrych <stan@python.org>
Co-authored-by: Victor Stinner <victor.stinner@gmail.com>
(cherry picked from commit 1bc78dea18)

* [3.15] gh-156810: Write the profiler's collapsed-stack export as UTF-8 (GH-156811) (#156814)

gh-156810: Write the profiler's collapsed-stack export as UTF-8 (GH-156811)
(cherry picked from commit c3706f4c68)

Co-authored-by: tonghuaroot (童话) <tonghuaroot@gmail.com>
(cherry picked from commit fce28da9b7)

* [3.15] gh-158552: Wait for Windows threads to suspend before blocking sampling (GH-158802) (#158845)

gh-158552: Wait for Windows threads to suspend before blocking sampling (GH-158802)

* gh-158552: Wait for Windows threads to suspend before blocking sampling

* Use a named Windows thread enumeration status constant
(cherry picked from commit 1643525f0f)

Co-authored-by: Pablo Galindo Salgado <Pablogsal@gmail.com>
(cherry picked from commit 31288da21d)

* [3.15] gh-152721: Fix quadratic RLE replay time in the profiling binary reader (GH-152722) (#158850)

Backport of GH-152722.

Co-authored-by: tonghuaroot <tonghuaroot@gmail.com>
(cherry picked from commit ebaca2ac2d)

* [3.15] gh-156545: Fix flamegraph export RecursionError on deeply recursive programs (GH-156546) (#158851)

Backport of GH-156546.

Co-authored-by: tonghuaroot (童话) <tonghuaroot@gmail.com>
(cherry picked from commit 0ac7217040)

* [3.15] gh-158540: Add the profiled script's directory to sys.path (GH-158548) (#158844)

gh-158540: Add the profiled script's directory to sys.path (GH-158548)

* gh-158540: Add the profiled script's directory to sys.path

When a script is profiled with ``python -m profiling.sampling run
script.py`` from another directory, the script cannot import modules
placed next to it, because ``_sync_coordinator._execute_script()``
executes it with the working directory (added by
``_setup_environment()`` for the module case) as ``sys.path[0]``
instead of the script's own directory.

Make the script's directory importable in ``_execute_script()``,
matching the behavior of ``python script.py``.

Add a regression test that runs the coordinator on a script importing
a sibling module.

* Update _sync_coordinator.py Comment simplified

* gh-158540: Resolve symlinks when adding the script directory to sys.path

``python script.py`` resolves symlinks when computing ``sys.path[0]``, so
a script reached through a symlink (``link.py -> sub/where.py``) imports
modules from the real script's directory.  Apply ``os.path.realpath()``
before taking the directory name, and make sure the result is placed at
the front of ``sys.path`` even if it was already listed.

Add a regression test for a symlinked script.

---------
(cherry picked from commit 3f02aab9fa)

Co-authored-by: he_tao <53343436+hetaozdh@users.noreply.github.com>
Co-authored-by: Eduardo Villalpando Mello <eduardo.villalpando.mello@gmail.com>
(cherry picked from commit f52d831397)

* [3.15] gh-153838: Skip non-regular source files in the heatmap exporter (GH-153839) (#158853)

Backport of GH-153839.

Co-authored-by: tonghuaroot <tonghuaroot@gmail.com>
(cherry picked from commit 48998df251)

* [3.15] gh-158539: Fix exception mode missing handlers in generators/coroutines (GH-158581) (#158852)

* [3.15] gh-158539: Fix exception mode missing handlers in generators/coroutines (GH-158581)

Backport of GH-158581.

Co-authored-by: LucasZhou <donghao.zhou@outlook.com>

* [3.15] gh-158539: Use portable static assertion messages

* [3.15] gh-158539: Keep layout assertions with debug-offset validation

* [3.15] gh-158539: Use the platform guard for in-process inspection tests

---------

Co-authored-by: LucasZhou <donghao.zhou@outlook.com>
(cherry picked from commit d625ecbb8d)

* [3.15] gh-158522: Fix truncated stack for a task whose coroutine recurses (GH-158526) (#158870)

Co-authored-by: Timofei Ivankov <128279579+deadlovelll@users.noreply.github.com>
(cherry picked from commit a904b397ed)

* gh-156545, gh-158539: Fix deep flamegraph export on small C stacks and macOS runtime lookup (#158874)

(cherry picked from commit 114de198c0)

Include the C-stack test helper from main, introduced by
ce5ae29ef9, which the regression test
requires but 3.15 does not yet provide.

---------

Co-authored-by: Miss Islington (bot) <31488909+miss-islington@users.noreply.github.com>
Co-authored-by: Maurycy Pawłowski-Wieroński <maurycy@maurycy.com>
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
Co-authored-by: Victor Stinner <vstinner@python.org>
Co-authored-by: Stan Ulbrych <stan@python.org>
Co-authored-by: Victor Stinner <victor.stinner@gmail.com>
Co-authored-by: tonghuaroot (童话) <tonghuaroot@gmail.com>
Co-authored-by: he_tao <53343436+hetaozdh@users.noreply.github.com>
Co-authored-by: Eduardo Villalpando Mello <eduardo.villalpando.mello@gmail.com>
Co-authored-by: LucasZhou <donghao.zhou@outlook.com>
Co-authored-by: Timofei Ivankov <128279579+deadlovelll@users.noreply.github.com>
2026-10-11 15:42:59 +01:00

1682 lines
54 KiB
C

/*
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.
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)&region_info,
&count,
&object_name)) == KERN_SUCCESS)
{
if ((region_info.protection & VM_PROT_READ) == 0
|| (region_info.protection & VM_PROT_EXECUTE) == 0) {
address += size;
continue;
}
int path_len = proc_regionfilename(
handle->pid, address, map_filename, MAXPATHLEN);
if (path_len == 0) {
address += size;
continue;
}
char* filename = strrchr(map_filename, '/');
if (filename != NULL) {
filename++; // Move past the '/'
} else {
filename = map_filename; // No path, use the whole string
}
if (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 = &section_header_table[elf_header->e_shstrndx];
char* shstrtab = (char*)(file_memory + shstrtab_section->sh_offset);
Elf_Shdr* section = NULL;
for (int i = 0; i < elf_header->e_shnum; i++) {
char* this_sec_name = shstrtab + section_header_table[i].sh_name;
// Move 1 character to account for the leading "."
this_sec_name += 1;
if (strcmp(secname, this_sec_name) == 0) {
section = &section_header_table[i];
break;
}
}
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