/*
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);
}

#define _set_debug_exception_cause(exception, format, ...) \
    do { \
        if (!_Py_RemoteDebug_HasPermissionError()) { \
            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
        page_size = (size_t)getpagesize();
#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,
    mach_port_t proc_ref,
    uintptr_t base,
    void* map
) {
    struct mach_header_64* hdr = (struct mach_header_64*)map;
    int ncmds = hdr->ncmds;

    int cmd_cnt = 0;
    struct segment_command_64* cmd = map + sizeof(struct mach_header_64);

    mach_vm_size_t size = 0;
    mach_msg_type_number_t count = sizeof(vm_region_basic_info_data_64_t);
    mach_vm_address_t address = (mach_vm_address_t)base;
    vm_region_basic_info_data_64_t r_info;
    mach_port_t object_name;
    uintptr_t vmaddr = 0;

    for (int i = 0; cmd_cnt < 2 && i < ncmds; i++) {
        if (cmd->cmd == LC_SEGMENT_64 && strcmp(cmd->segname, "__TEXT") == 0) {
            vmaddr = cmd->vmaddr;
        }
        if (cmd->cmd == LC_SEGMENT_64 && strcmp(cmd->segname, "__DATA") == 0) {
            while (cmd->filesize != size) {
                address += size;
                kern_return_t ret = mach_vm_region(
                    proc_ref,
                    &address,
                    &size,
                    VM_REGION_BASIC_INFO_64,
                    (vm_region_info_t)&r_info,  // cppcheck-suppress [uninitvar]
                    &count,
                    &object_name
                );
                if (ret != KERN_SUCCESS) {
                    PyErr_Format(PyExc_RuntimeError,
                        "mach_vm_region failed while parsing 64-bit Mach-O binary "
                        "at base address 0x%lx (kern_return_t: %d)",
                        base, ret);
                    return 0;
                }
            }

            int nsects = cmd->nsects;
            struct section_64* sec = (struct section_64*)(
                (void*)cmd + sizeof(struct segment_command_64)
                );
            for (int j = 0; j < nsects; j++) {
                if (strcmp(sec[j].sectname, section) == 0) {
                    return base + sec[j].addr - vmaddr;
                }
            }
            cmd_cnt++;
        }

        cmd = (struct segment_command_64*)((void*)cmd + cmd->cmdsize);
    }

    return 0;
}

static uintptr_t
return_section_address32(
    const char* section,
    mach_port_t proc_ref,
    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);

    mach_vm_size_t size = 0;
    mach_msg_type_number_t count = sizeof(vm_region_basic_info_data_t);
    mach_vm_address_t address = (mach_vm_address_t)base;
    vm_region_basic_info_data_t r_info;
    mach_port_t object_name;
    uintptr_t vmaddr = 0;

    for (int i = 0; cmd_cnt < 2 && i < ncmds; i++) {
        if (cmd->cmd == LC_SEGMENT && strcmp(cmd->segname, "__TEXT") == 0) {
            vmaddr = cmd->vmaddr;
        }
        if (cmd->cmd == LC_SEGMENT && strcmp(cmd->segname, "__DATA") == 0) {
            while (cmd->filesize != size) {
                address += size;
                kern_return_t ret = mach_vm_region(
                    proc_ref,
                    &address,
                    &size,
                    VM_REGION_BASIC_INFO,
                    (vm_region_info_t)&r_info,  // cppcheck-suppress [uninitvar]
                    &count,
                    &object_name
                );
                if (ret != KERN_SUCCESS) {
                    PyErr_Format(PyExc_RuntimeError,
                        "mach_vm_region failed while parsing 32-bit Mach-O binary "
                        "at base address 0x%lx (kern_return_t: %d)",
                        base, ret);
                    return 0;
                }
            }

            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,
    mach_port_t proc_ref,
    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, proc_ref, base, (void*)hdr);

                case MH_MAGIC_64:
                case MH_CIGAM_64:
                    return return_section_address64(section, proc_ref, 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, mach_vm_size_t size, mach_port_t proc_ref)
{
    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, proc_ref, base, map);
        break;
    case MH_MAGIC_64:
    case MH_CIGAM_64:
        result = return_section_address64(secname, proc_ref, base, map);
        break;
    case FAT_MAGIC:
    case FAT_CIGAM:
        result = return_section_address_fat(secname, proc_ref, 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, size, proc_ref);
            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
