Coverage Report

Created: 2026-05-16 06:46

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/cpython/Python/remote_debug.h
Line
Count
Source
1
/*
2
IMPORTANT: This header file is full of static functions that are not exported.
3
4
The reason is that we don't want to export these functions to the Python API
5
and they can be used both for the interpreter and some shared libraries. The
6
reason we don't want to export them is to avoid having them participating in
7
return-oriented programming attacks.
8
9
If you need to add a new function ensure that is declared 'static'.
10
*/
11
12
#ifdef __cplusplus
13
extern "C" {
14
#endif
15
16
#ifdef __clang__
17
    #define UNUSED __attribute__((unused))
18
#elif defined(__GNUC__)
19
    #define UNUSED __attribute__((unused))
20
#elif defined(_MSC_VER)
21
    #define UNUSED __pragma(warning(suppress: 4505))
22
#else
23
    #define UNUSED
24
#endif
25
26
#if !defined(Py_BUILD_CORE) && !defined(Py_BUILD_CORE_MODULE)
27
#  error "this header requires Py_BUILD_CORE or Py_BUILD_CORE_MODULE define"
28
#endif
29
30
#include "pyconfig.h"
31
#include "internal/pycore_ceval.h"
32
33
#ifdef __linux__
34
#    include <elf.h>
35
#    include <sys/uio.h>
36
#    include <sys/ptrace.h>
37
#    include <sys/wait.h>
38
#    include <dirent.h>
39
#    if INTPTR_MAX == INT64_MAX
40
0
#        define Elf_Ehdr Elf64_Ehdr
41
0
#        define Elf_Shdr Elf64_Shdr
42
0
#        define Elf_Phdr Elf64_Phdr
43
#    else
44
#        define Elf_Ehdr Elf32_Ehdr
45
#        define Elf_Shdr Elf32_Shdr
46
#        define Elf_Phdr Elf32_Phdr
47
#    endif
48
#    include <sys/mman.h>
49
50
// PTRACE options - define if not available
51
#    ifndef PTRACE_SEIZE
52
#        define PTRACE_SEIZE 0x4206
53
#    endif
54
#    ifndef PTRACE_INTERRUPT
55
#        define PTRACE_INTERRUPT 0x4207
56
#    endif
57
#    ifndef PTRACE_EVENT_STOP
58
#        define PTRACE_EVENT_STOP 128
59
#    endif
60
#endif
61
62
#if defined(__APPLE__) && defined(TARGET_OS_OSX) && TARGET_OS_OSX
63
#  include <libproc.h>
64
#  include <mach-o/fat.h>
65
#  include <mach-o/loader.h>
66
#  include <mach-o/nlist.h>
67
#  include <mach/error.h>
68
#  include <mach/mach.h>
69
#  include <mach/mach_vm.h>
70
#  include <mach/machine.h>
71
#  include <mach/task_info.h>
72
#  include <mach/thread_act.h>
73
#  include <sys/mman.h>
74
#  include <sys/proc.h>
75
#  include <sys/sysctl.h>
76
#endif
77
78
#ifdef MS_WINDOWS
79
    // Windows includes and definitions
80
#include <windows.h>
81
#include <psapi.h>
82
#include <tlhelp32.h>
83
#endif
84
85
#include <errno.h>
86
#include <fcntl.h>
87
#include <stddef.h>
88
#include <stdint.h>
89
#include <stdio.h>
90
#include <stdlib.h>
91
#include <string.h>
92
#ifndef MS_WINDOWS
93
#include <sys/param.h>
94
#include <sys/stat.h>
95
#include <sys/types.h>
96
#include <unistd.h>
97
#endif
98
99
#ifndef HAVE_PROCESS_VM_READV
100
#    define HAVE_PROCESS_VM_READV 0
101
#endif
102
103
#define _set_debug_exception_cause(exception, format, ...) \
104
0
    do { \
105
0
        if (!PyErr_ExceptionMatches(PyExc_PermissionError)) { \
106
0
            PyThreadState *tstate = _PyThreadState_GET(); \
107
0
            if (!_PyErr_Occurred(tstate)) { \
108
0
                _PyErr_Format(tstate, exception, format, ##__VA_ARGS__); \
109
0
            } else { \
110
0
                _PyErr_FormatFromCause(exception, format, ##__VA_ARGS__); \
111
0
            } \
112
0
        } \
113
0
    } while (0)
114
115
static inline size_t
116
0
get_page_size(void) {
117
0
    size_t page_size = 0;
118
0
    if (page_size == 0) {
119
#ifdef MS_WINDOWS
120
        SYSTEM_INFO si;
121
        GetSystemInfo(&si);
122
        page_size = si.dwPageSize;
123
#else
124
0
        page_size = (size_t)getpagesize();
125
0
#endif
126
0
    }
127
0
    return page_size;
128
0
}
129
130
typedef struct page_cache_entry {
131
    uintptr_t page_addr; // page-aligned base address
132
    char *data;
133
    int valid;
134
    struct page_cache_entry *next;
135
} page_cache_entry_t;
136
137
0
#define MAX_PAGES 1024
138
139
// Define a platform-independent process handle structure
140
typedef struct {
141
    pid_t pid;
142
#if defined(__APPLE__) && defined(TARGET_OS_OSX) && TARGET_OS_OSX
143
    mach_port_t task;
144
#elif defined(MS_WINDOWS)
145
    HANDLE hProcess;
146
#elif defined(__linux__)
147
    int memfd;
148
#endif
149
    page_cache_entry_t pages[MAX_PAGES];
150
    Py_ssize_t page_size;
151
} proc_handle_t;
152
153
// Forward declaration for use in validation function
154
static int
155
_Py_RemoteDebug_ReadRemoteMemory(proc_handle_t *handle, uintptr_t remote_address, size_t len, void* dst);
156
157
// Optional callback to validate a candidate section address found during
158
// memory map searches. Returns 1 if the address is valid, 0 to skip it.
159
// This allows callers to filter out duplicate/stale mappings (e.g. from
160
// ctypes dlopen) whose sections were never initialized.
161
typedef int (*section_validator_t)(proc_handle_t *handle, uintptr_t address);
162
163
// Validate that a candidate address starts with _Py_Debug_Cookie.
164
static int
165
_Py_RemoteDebug_ValidatePyRuntimeCookie(proc_handle_t *handle, uintptr_t address)
166
0
{
167
0
    if (address == 0) {
168
0
        return 0;
169
0
    }
170
0
    char buf[sizeof(_Py_Debug_Cookie) - 1];
171
0
    if (_Py_RemoteDebug_ReadRemoteMemory(handle, address, sizeof(buf), buf) != 0) {
172
0
        PyErr_Clear();
173
0
        return 0;
174
0
    }
175
0
    return memcmp(buf, _Py_Debug_Cookie, sizeof(buf)) == 0;
176
0
}
177
178
static void
179
_Py_RemoteDebug_FreePageCache(proc_handle_t *handle)
180
0
{
181
0
    for (int i = 0; i < MAX_PAGES; i++) {
182
0
        if (handle->pages[i].data) {
183
0
            PyMem_RawFree(handle->pages[i].data);
184
0
        }
185
0
        handle->pages[i].data = NULL;
186
0
        handle->pages[i].valid = 0;
187
0
    }
188
0
}
189
190
UNUSED static void
191
_Py_RemoteDebug_ClearCache(proc_handle_t *handle)
192
0
{
193
0
    for (int i = 0; i < MAX_PAGES; i++) {
194
0
        handle->pages[i].valid = 0;
195
0
    }
196
0
}
197
198
#if defined(__APPLE__) && defined(TARGET_OS_OSX) && TARGET_OS_OSX
199
static mach_port_t pid_to_task(pid_t pid);
200
#endif
201
202
// Initialize the process handle
203
UNUSED static int
204
0
_Py_RemoteDebug_InitProcHandle(proc_handle_t *handle, pid_t pid) {
205
0
    handle->pid = pid;
206
#if defined(__APPLE__) && defined(TARGET_OS_OSX) && TARGET_OS_OSX
207
    handle->task = pid_to_task(handle->pid);
208
    if (handle->task == 0) {
209
        _set_debug_exception_cause(PyExc_RuntimeError, "Failed to initialize macOS process handle");
210
        return -1;
211
    }
212
#elif defined(MS_WINDOWS)
213
    handle->hProcess = OpenProcess(
214
        PROCESS_VM_READ | PROCESS_VM_WRITE | PROCESS_VM_OPERATION | PROCESS_QUERY_INFORMATION | PROCESS_SUSPEND_RESUME,
215
        FALSE, pid);
216
    if (handle->hProcess == NULL) {
217
        PyErr_SetFromWindowsErr(0);
218
        _set_debug_exception_cause(PyExc_RuntimeError, "Failed to initialize Windows process handle");
219
        return -1;
220
    }
221
#elif defined(__linux__)
222
    handle->memfd = -1;
223
0
#endif
224
0
    handle->page_size = get_page_size();
225
0
    for (int i = 0; i < MAX_PAGES; i++) {
226
0
        handle->pages[i].data = NULL;
227
0
        handle->pages[i].valid = 0;
228
0
    }
229
0
    return 0;
230
0
}
231
232
// Clean up the process handle
233
UNUSED static void
234
0
_Py_RemoteDebug_CleanupProcHandle(proc_handle_t *handle) {
235
#ifdef MS_WINDOWS
236
    if (handle->hProcess != NULL) {
237
        CloseHandle(handle->hProcess);
238
        handle->hProcess = NULL;
239
    }
240
#elif defined(__linux__)
241
0
    if (handle->memfd != -1) {
242
0
        close(handle->memfd);
243
0
        handle->memfd = -1;
244
0
    }
245
0
#endif
246
0
    handle->pid = 0;
247
0
    _Py_RemoteDebug_FreePageCache(handle);
248
0
}
249
250
#if defined(__APPLE__) && defined(TARGET_OS_OSX) && TARGET_OS_OSX
251
252
static uintptr_t
253
return_section_address64(
254
    const char* section,
255
    mach_port_t proc_ref,
256
    uintptr_t base,
257
    void* map
258
) {
259
    struct mach_header_64* hdr = (struct mach_header_64*)map;
260
    int ncmds = hdr->ncmds;
261
262
    int cmd_cnt = 0;
263
    struct segment_command_64* cmd = map + sizeof(struct mach_header_64);
264
265
    mach_vm_size_t size = 0;
266
    mach_msg_type_number_t count = sizeof(vm_region_basic_info_data_64_t);
267
    mach_vm_address_t address = (mach_vm_address_t)base;
268
    vm_region_basic_info_data_64_t r_info;
269
    mach_port_t object_name;
270
    uintptr_t vmaddr = 0;
271
272
    for (int i = 0; cmd_cnt < 2 && i < ncmds; i++) {
273
        if (cmd->cmd == LC_SEGMENT_64 && strcmp(cmd->segname, "__TEXT") == 0) {
274
            vmaddr = cmd->vmaddr;
275
        }
276
        if (cmd->cmd == LC_SEGMENT_64 && strcmp(cmd->segname, "__DATA") == 0) {
277
            while (cmd->filesize != size) {
278
                address += size;
279
                kern_return_t ret = mach_vm_region(
280
                    proc_ref,
281
                    &address,
282
                    &size,
283
                    VM_REGION_BASIC_INFO_64,
284
                    (vm_region_info_t)&r_info,  // cppcheck-suppress [uninitvar]
285
                    &count,
286
                    &object_name
287
                );
288
                if (ret != KERN_SUCCESS) {
289
                    PyErr_Format(PyExc_RuntimeError,
290
                        "mach_vm_region failed while parsing 64-bit Mach-O binary "
291
                        "at base address 0x%lx (kern_return_t: %d)",
292
                        base, ret);
293
                    return 0;
294
                }
295
            }
296
297
            int nsects = cmd->nsects;
298
            struct section_64* sec = (struct section_64*)(
299
                (void*)cmd + sizeof(struct segment_command_64)
300
                );
301
            for (int j = 0; j < nsects; j++) {
302
                if (strcmp(sec[j].sectname, section) == 0) {
303
                    return base + sec[j].addr - vmaddr;
304
                }
305
            }
306
            cmd_cnt++;
307
        }
308
309
        cmd = (struct segment_command_64*)((void*)cmd + cmd->cmdsize);
310
    }
311
312
    return 0;
313
}
314
315
static uintptr_t
316
return_section_address32(
317
    const char* section,
318
    mach_port_t proc_ref,
319
    uintptr_t base,
320
    void* map
321
) {
322
    struct mach_header* hdr = (struct mach_header*)map;
323
    int ncmds = hdr->ncmds;
324
325
    int cmd_cnt = 0;
326
    struct segment_command* cmd = map + sizeof(struct mach_header);
327
328
    mach_vm_size_t size = 0;
329
    mach_msg_type_number_t count = sizeof(vm_region_basic_info_data_t);
330
    mach_vm_address_t address = (mach_vm_address_t)base;
331
    vm_region_basic_info_data_t r_info;
332
    mach_port_t object_name;
333
    uintptr_t vmaddr = 0;
334
335
    for (int i = 0; cmd_cnt < 2 && i < ncmds; i++) {
336
        if (cmd->cmd == LC_SEGMENT && strcmp(cmd->segname, "__TEXT") == 0) {
337
            vmaddr = cmd->vmaddr;
338
        }
339
        if (cmd->cmd == LC_SEGMENT && strcmp(cmd->segname, "__DATA") == 0) {
340
            while (cmd->filesize != size) {
341
                address += size;
342
                kern_return_t ret = mach_vm_region(
343
                    proc_ref,
344
                    &address,
345
                    &size,
346
                    VM_REGION_BASIC_INFO,
347
                    (vm_region_info_t)&r_info,  // cppcheck-suppress [uninitvar]
348
                    &count,
349
                    &object_name
350
                );
351
                if (ret != KERN_SUCCESS) {
352
                    PyErr_Format(PyExc_RuntimeError,
353
                        "mach_vm_region failed while parsing 32-bit Mach-O binary "
354
                        "at base address 0x%lx (kern_return_t: %d)",
355
                        base, ret);
356
                    return 0;
357
                }
358
            }
359
360
            int nsects = cmd->nsects;
361
            struct section* sec = (struct section*)(
362
                (void*)cmd + sizeof(struct segment_command)
363
                );
364
            for (int j = 0; j < nsects; j++) {
365
                if (strcmp(sec[j].sectname, section) == 0) {
366
                    return base + sec[j].addr - vmaddr;
367
                }
368
            }
369
            cmd_cnt++;
370
        }
371
372
        cmd = (struct segment_command*)((void*)cmd + cmd->cmdsize);
373
    }
374
375
    return 0;
376
}
377
378
static uintptr_t
379
return_section_address_fat(
380
    const char* section,
381
    mach_port_t proc_ref,
382
    uintptr_t base,
383
    void* map
384
) {
385
    struct fat_header* fat_hdr = (struct fat_header*)map;
386
387
    // Determine host CPU type for architecture selection
388
    cpu_type_t cpu;
389
    int is_abi64;
390
    size_t cpu_size = sizeof(cpu), abi64_size = sizeof(is_abi64);
391
392
    if (sysctlbyname("hw.cputype", &cpu, &cpu_size, NULL, 0) != 0) {
393
        PyErr_Format(PyExc_OSError,
394
            "Failed to determine CPU type via sysctlbyname "
395
            "for fat binary analysis at 0x%lx: %s",
396
            base, strerror(errno));
397
        return 0;
398
    }
399
    if (sysctlbyname("hw.cpu64bit_capable", &is_abi64, &abi64_size, NULL, 0) != 0) {
400
        PyErr_Format(PyExc_OSError,
401
            "Failed to determine CPU ABI capability via sysctlbyname "
402
            "for fat binary analysis at 0x%lx: %s",
403
            base, strerror(errno));
404
        return 0;
405
    }
406
407
    cpu |= is_abi64 * CPU_ARCH_ABI64;
408
409
    // Check endianness
410
    int swap = fat_hdr->magic == FAT_CIGAM;
411
    struct fat_arch* arch = (struct fat_arch*)(map + sizeof(struct fat_header));
412
413
    // Get number of architectures in fat binary
414
    uint32_t nfat_arch = swap ? __builtin_bswap32(fat_hdr->nfat_arch) : fat_hdr->nfat_arch;
415
416
    // Search for matching architecture
417
    for (uint32_t i = 0; i < nfat_arch; i++) {
418
        cpu_type_t arch_cpu = swap ? __builtin_bswap32(arch[i].cputype) : arch[i].cputype;
419
420
        if (arch_cpu == cpu) {
421
            // Found matching architecture, now process it
422
            uint32_t offset = swap ? __builtin_bswap32(arch[i].offset) : arch[i].offset;
423
            struct mach_header_64* hdr = (struct mach_header_64*)(map + offset);
424
425
            // Determine which type of Mach-O it is and process accordingly
426
            switch (hdr->magic) {
427
                case MH_MAGIC:
428
                case MH_CIGAM:
429
                    return return_section_address32(section, proc_ref, base, (void*)hdr);
430
431
                case MH_MAGIC_64:
432
                case MH_CIGAM_64:
433
                    return return_section_address64(section, proc_ref, base, (void*)hdr);
434
435
                default:
436
                    PyErr_Format(PyExc_RuntimeError,
437
                        "Unknown Mach-O magic number 0x%x in fat binary architecture %u at base 0x%lx",
438
                        hdr->magic, i, base);
439
                    return 0;
440
            }
441
        }
442
    }
443
444
    PyErr_Format(PyExc_RuntimeError,
445
        "No matching architecture found for CPU type 0x%x "
446
        "in fat binary at base 0x%lx (%u architectures examined)",
447
        cpu, base, nfat_arch);
448
    return 0;
449
}
450
451
static uintptr_t
452
search_section_in_file(const char* secname, char* path, uintptr_t base, mach_vm_size_t size, mach_port_t proc_ref)
453
{
454
    int fd = open(path, O_RDONLY);
455
    if (fd == -1) {
456
        PyErr_Format(PyExc_OSError,
457
            "Cannot open binary file '%s' for section '%s' search: %s",
458
            path, secname, strerror(errno));
459
        return 0;
460
    }
461
462
    struct stat fs;
463
    if (fstat(fd, &fs) == -1) {
464
        PyErr_Format(PyExc_OSError,
465
            "Cannot get file size for binary '%s' during section '%s' search: %s",
466
            path, secname, strerror(errno));
467
        close(fd);
468
        return 0;
469
    }
470
471
    void* map = mmap(0, fs.st_size, PROT_READ, MAP_SHARED, fd, 0);
472
    if (map == MAP_FAILED) {
473
        PyErr_Format(PyExc_OSError,
474
            "Cannot memory map binary file '%s' (size: %lld bytes) for section '%s' search: %s",
475
            path, (long long)fs.st_size, secname, strerror(errno));
476
        close(fd);
477
        return 0;
478
    }
479
480
    uintptr_t result = 0;
481
    uint32_t magic = *(uint32_t*)map;
482
483
    switch (magic) {
484
    case MH_MAGIC:
485
    case MH_CIGAM:
486
        result = return_section_address32(secname, proc_ref, base, map);
487
        break;
488
    case MH_MAGIC_64:
489
    case MH_CIGAM_64:
490
        result = return_section_address64(secname, proc_ref, base, map);
491
        break;
492
    case FAT_MAGIC:
493
    case FAT_CIGAM:
494
        result = return_section_address_fat(secname, proc_ref, base, map);
495
        break;
496
    default:
497
        PyErr_Format(PyExc_RuntimeError,
498
            "Unrecognized Mach-O magic number 0x%x in binary file '%s' for section '%s' search",
499
            magic, path, secname);
500
        break;
501
    }
502
503
    if (munmap(map, fs.st_size) != 0) {
504
        PyErr_Format(PyExc_OSError,
505
            "Failed to unmap binary file '%s' (size: %lld bytes): %s",
506
            path, (long long)fs.st_size, strerror(errno));
507
        result = 0;
508
    }
509
    if (close(fd) != 0) {
510
        PyErr_Format(PyExc_OSError,
511
            "Failed to close binary file '%s': %s",
512
            path, strerror(errno));
513
        result = 0;
514
    }
515
    return result;
516
}
517
518
519
static mach_port_t
520
pid_to_task(pid_t pid)
521
{
522
    mach_port_t task;
523
    kern_return_t result;
524
525
    result = task_for_pid(mach_task_self(), pid, &task);
526
    if (result != KERN_SUCCESS) {
527
        PyErr_Format(PyExc_PermissionError,
528
            "Cannot get task port for PID %d (kern_return_t: %d). "
529
            "This typically requires running as root or having the 'com.apple.system-task-ports' entitlement.",
530
            pid, result);
531
        return 0;
532
    }
533
    return task;
534
}
535
536
static uintptr_t
537
search_map_for_section(proc_handle_t *handle, const char* secname, const char* substr,
538
                       section_validator_t validator) {
539
    mach_vm_address_t address = 0;
540
    mach_vm_size_t size = 0;
541
    mach_msg_type_number_t count = sizeof(vm_region_basic_info_data_64_t);
542
    vm_region_basic_info_data_64_t region_info;
543
    mach_port_t object_name;
544
545
    mach_port_t proc_ref = pid_to_task(handle->pid);
546
    if (proc_ref == 0) {
547
        if (!PyErr_Occurred()) {
548
            PyErr_Format(PyExc_PermissionError,
549
                "Cannot get task port for PID %d during section search",
550
                handle->pid);
551
        }
552
        return 0;
553
    }
554
555
    char map_filename[MAXPATHLEN + 1];
556
557
    while (mach_vm_region(
558
        proc_ref,
559
        &address,
560
        &size,
561
        VM_REGION_BASIC_INFO_64,
562
        (vm_region_info_t)&region_info,
563
        &count,
564
        &object_name) == KERN_SUCCESS)
565
    {
566
567
        if ((region_info.protection & VM_PROT_READ) == 0
568
            || (region_info.protection & VM_PROT_EXECUTE) == 0) {
569
            address += size;
570
            continue;
571
        }
572
573
        int path_len = proc_regionfilename(
574
            handle->pid, address, map_filename, MAXPATHLEN);
575
        if (path_len == 0) {
576
            address += size;
577
            continue;
578
        }
579
580
        char* filename = strrchr(map_filename, '/');
581
        if (filename != NULL) {
582
            filename++;  // Move past the '/'
583
        } else {
584
            filename = map_filename;  // No path, use the whole string
585
        }
586
587
        if (strncmp(filename, substr, strlen(substr)) == 0) {
588
            uintptr_t result = search_section_in_file(
589
                secname, map_filename, address, size, proc_ref);
590
            if (result != 0
591
                && (validator == NULL || validator(handle, result)))
592
            {
593
                return result;
594
            }
595
        }
596
597
        address += size;
598
    }
599
600
    return 0;
601
}
602
603
#endif // (__APPLE__ && defined(TARGET_OS_OSX) && TARGET_OS_OSX)
604
605
#if defined(__linux__) && HAVE_PROCESS_VM_READV
606
static uintptr_t
607
search_elf_file_for_section(
608
        proc_handle_t *handle,
609
        const char* secname,
610
        uintptr_t start_address,
611
        const char *elf_file)
612
0
{
613
0
    if (start_address == 0) {
614
0
        return 0;
615
0
    }
616
617
0
    uintptr_t result = 0;
618
0
    void* file_memory = NULL;
619
620
0
    int fd = open(elf_file, O_RDONLY);
621
0
    if (fd < 0) {
622
0
        PyErr_Format(PyExc_OSError,
623
0
            "Cannot open ELF file '%s' for section '%s' search: %s",
624
0
            elf_file, secname, strerror(errno));
625
0
        goto exit;
626
0
    }
627
628
0
    struct stat file_stats;
629
0
    if (fstat(fd, &file_stats) != 0) {
630
0
        PyErr_Format(PyExc_OSError,
631
0
            "Cannot get file size for ELF file '%s' during section '%s' search: %s",
632
0
            elf_file, secname, strerror(errno));
633
0
        goto exit;
634
0
    }
635
636
0
    file_memory = mmap(NULL, file_stats.st_size, PROT_READ, MAP_PRIVATE, fd, 0);
637
0
    if (file_memory == MAP_FAILED) {
638
0
        PyErr_Format(PyExc_OSError,
639
0
            "Cannot memory map ELF file '%s' (size: %lld bytes) for section '%s' search: %s",
640
0
            elf_file, (long long)file_stats.st_size, secname, strerror(errno));
641
0
        goto exit;
642
0
    }
643
644
0
    Elf_Ehdr* elf_header = (Elf_Ehdr*)file_memory;
645
646
    // Validate ELF header
647
0
    if (elf_header->e_shstrndx >= elf_header->e_shnum) {
648
0
        PyErr_Format(PyExc_RuntimeError,
649
0
            "Invalid ELF file '%s': string table index %u >= section count %u",
650
0
            elf_file, elf_header->e_shstrndx, elf_header->e_shnum);
651
0
        goto exit;
652
0
    }
653
654
0
    Elf_Shdr* section_header_table = (Elf_Shdr*)(file_memory + elf_header->e_shoff);
655
656
0
    Elf_Shdr* shstrtab_section = &section_header_table[elf_header->e_shstrndx];
657
0
    char* shstrtab = (char*)(file_memory + shstrtab_section->sh_offset);
658
659
0
    Elf_Shdr* section = NULL;
660
0
    for (int i = 0; i < elf_header->e_shnum; i++) {
661
0
        char* this_sec_name = shstrtab + section_header_table[i].sh_name;
662
        // Move 1 character to account for the leading "."
663
0
        this_sec_name += 1;
664
0
        if (strcmp(secname, this_sec_name) == 0) {
665
0
            section = &section_header_table[i];
666
0
            break;
667
0
        }
668
0
    }
669
670
0
    if (section == NULL) {
671
0
        goto exit;
672
0
    }
673
674
0
    Elf_Phdr* program_header_table = (Elf_Phdr*)(file_memory + elf_header->e_phoff);
675
    // Find the first PT_LOAD segment
676
0
    Elf_Phdr* first_load_segment = NULL;
677
0
    for (int i = 0; i < elf_header->e_phnum; i++) {
678
0
        if (program_header_table[i].p_type == PT_LOAD) {
679
0
            first_load_segment = &program_header_table[i];
680
0
            break;
681
0
        }
682
0
    }
683
684
0
    if (first_load_segment == NULL) {
685
0
        PyErr_Format(PyExc_RuntimeError,
686
0
            "No PT_LOAD segment found in ELF file '%s' (%u program headers examined)",
687
0
            elf_file, elf_header->e_phnum);
688
0
        goto exit;
689
0
    }
690
691
0
    uintptr_t elf_load_addr = first_load_segment->p_vaddr
692
0
        - (first_load_segment->p_vaddr % first_load_segment->p_align);
693
0
    result = start_address + (uintptr_t)section->sh_addr - elf_load_addr;
694
695
0
exit:
696
0
    if (file_memory != NULL) {
697
0
        munmap(file_memory, file_stats.st_size);
698
0
    }
699
0
    if (fd >= 0 && close(fd) != 0) {
700
0
        PyErr_Format(PyExc_OSError,
701
0
            "Failed to close ELF file '%s': %s",
702
0
            elf_file, strerror(errno));
703
0
        result = 0;
704
0
    }
705
0
    return result;
706
0
}
707
708
static uintptr_t
709
search_linux_map_for_section(proc_handle_t *handle, const char* secname, const char* substr,
710
                             section_validator_t validator)
711
0
{
712
0
    char maps_file_path[64];
713
0
    sprintf(maps_file_path, "/proc/%d/maps", handle->pid);
714
715
0
    FILE* maps_file = fopen(maps_file_path, "r");
716
0
    if (maps_file == NULL) {
717
0
        PyErr_Format(PyExc_OSError,
718
0
            "Cannot open process memory map file '%s' for PID %d section search: %s",
719
0
            maps_file_path, handle->pid, strerror(errno));
720
0
        return 0;
721
0
    }
722
723
0
    size_t linelen = 0;
724
0
    size_t linesz = PATH_MAX;
725
0
    char *line = PyMem_Malloc(linesz);
726
0
    if (!line) {
727
0
        fclose(maps_file);
728
0
        _set_debug_exception_cause(PyExc_MemoryError,
729
0
            "Cannot allocate memory for reading process map file '%s'",
730
0
            maps_file_path);
731
0
        return 0;
732
0
    }
733
734
0
    uintptr_t retval = 0;
735
736
0
    while (fgets(line + linelen, linesz - linelen, maps_file) != NULL) {
737
0
        linelen = strlen(line);
738
0
        if (line[linelen - 1] != '\n') {
739
            // Read a partial line: realloc and keep reading where we left off.
740
            // Note that even the last line will be terminated by a newline.
741
0
            linesz *= 2;
742
0
            char *biggerline = PyMem_Realloc(line, linesz);
743
0
            if (!biggerline) {
744
0
                PyMem_Free(line);
745
0
                fclose(maps_file);
746
0
                _set_debug_exception_cause(PyExc_MemoryError,
747
0
                    "Cannot reallocate memory while reading process map file '%s' (attempted size: %zu)",
748
0
                    maps_file_path, linesz);
749
0
                return 0;
750
0
            }
751
0
            line = biggerline;
752
0
            continue;
753
0
        }
754
755
        // Read a full line: strip the newline
756
0
        line[linelen - 1] = '\0';
757
        // and prepare to read the next line into the start of the buffer.
758
0
        linelen = 0;
759
760
0
        unsigned long start = 0;
761
0
        unsigned long path_pos = 0;
762
0
        sscanf(line, "%lx-%*x %*s %*s %*s %*s %ln", &start, &path_pos);
763
764
0
        if (!path_pos) {
765
            // Line didn't match our format string.  This shouldn't be
766
            // possible, but let's be defensive and skip the line.
767
0
            continue;
768
0
        }
769
770
0
        const char *path = line + path_pos;
771
0
        if (path[0] == '[' && path[strlen(path)-1] == ']') {
772
            // Skip [heap], [stack], [anon:cpython:pymalloc], etc.
773
0
            continue;
774
0
        }
775
776
0
        const char *filename = strrchr(path, '/');
777
0
        if (filename) {
778
0
            filename++;  // Move past the '/'
779
0
        } else {
780
0
            filename = path;  // No directories, or an empty string
781
0
        }
782
783
0
        if (strstr(filename, substr)) {
784
0
            PyErr_Clear();
785
0
            retval = search_elf_file_for_section(handle, secname, start, path);
786
0
            if (retval
787
0
                && (validator == NULL || validator(handle, retval)))
788
0
            {
789
0
                break;
790
0
            }
791
0
            retval = 0;
792
0
        }
793
0
    }
794
795
0
    PyMem_Free(line);
796
0
    if (fclose(maps_file) != 0) {
797
0
        PyErr_Format(PyExc_OSError,
798
0
            "Failed to close process map file '%s': %s",
799
0
            maps_file_path, strerror(errno));
800
0
        retval = 0;
801
0
    }
802
803
0
    return retval;
804
0
}
805
806
807
#endif // __linux__
808
809
#ifdef MS_WINDOWS
810
811
static int is_process_alive(HANDLE hProcess) {
812
    DWORD exitCode;
813
    if (GetExitCodeProcess(hProcess, &exitCode)) {
814
        return exitCode == STILL_ACTIVE;
815
    }
816
    return 0;
817
}
818
819
static void* analyze_pe(const wchar_t* mod_path, BYTE* remote_base, const char* secname) {
820
    HANDLE hFile = CreateFileW(mod_path, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
821
    if (hFile == INVALID_HANDLE_VALUE) {
822
        PyErr_SetFromWindowsErr(0);
823
        DWORD error = GetLastError();
824
        PyErr_Format(PyExc_OSError,
825
            "Cannot open PE file for section '%s' analysis (error %lu)",
826
            secname, error);
827
        return NULL;
828
    }
829
830
    HANDLE hMap = CreateFileMapping(hFile, NULL, PAGE_READONLY, 0, 0, 0);
831
    if (!hMap) {
832
        PyErr_SetFromWindowsErr(0);
833
        DWORD error = GetLastError();
834
        PyErr_Format(PyExc_OSError,
835
            "Cannot create file mapping for PE file section '%s' analysis (error %lu)",
836
            secname, error);
837
        CloseHandle(hFile);
838
        return NULL;
839
    }
840
841
    BYTE* mapView = (BYTE*)MapViewOfFile(hMap, FILE_MAP_READ, 0, 0, 0);
842
    if (!mapView) {
843
        PyErr_SetFromWindowsErr(0);
844
        DWORD error = GetLastError();
845
        PyErr_Format(PyExc_OSError,
846
            "Cannot map view of PE file for section '%s' analysis (error %lu)",
847
            secname, error);
848
        CloseHandle(hMap);
849
        CloseHandle(hFile);
850
        return NULL;
851
    }
852
853
    IMAGE_DOS_HEADER* pDOSHeader = (IMAGE_DOS_HEADER*)mapView;
854
    if (pDOSHeader->e_magic != IMAGE_DOS_SIGNATURE) {
855
        PyErr_Format(PyExc_RuntimeError,
856
            "Invalid DOS signature (0x%x) in PE file for section '%s' analysis (expected 0x%x)",
857
            pDOSHeader->e_magic, secname, IMAGE_DOS_SIGNATURE);
858
        UnmapViewOfFile(mapView);
859
        CloseHandle(hMap);
860
        CloseHandle(hFile);
861
        return NULL;
862
    }
863
864
    IMAGE_NT_HEADERS* pNTHeaders = (IMAGE_NT_HEADERS*)(mapView + pDOSHeader->e_lfanew);
865
    if (pNTHeaders->Signature != IMAGE_NT_SIGNATURE) {
866
        PyErr_Format(PyExc_RuntimeError,
867
            "Invalid NT signature (0x%lx) in PE file for section '%s' analysis (expected 0x%lx)",
868
            pNTHeaders->Signature, secname, IMAGE_NT_SIGNATURE);
869
        UnmapViewOfFile(mapView);
870
        CloseHandle(hMap);
871
        CloseHandle(hFile);
872
        return NULL;
873
    }
874
875
    IMAGE_SECTION_HEADER* pSection_header = (IMAGE_SECTION_HEADER*)(mapView + pDOSHeader->e_lfanew + sizeof(IMAGE_NT_HEADERS));
876
    void* runtime_addr = NULL;
877
878
    for (int i = 0; i < pNTHeaders->FileHeader.NumberOfSections; i++) {
879
        const char* name = (const char*)pSection_header[i].Name;
880
        if (strncmp(name, secname, IMAGE_SIZEOF_SHORT_NAME) == 0) {
881
            runtime_addr = remote_base + pSection_header[i].VirtualAddress;
882
            break;
883
        }
884
    }
885
886
    UnmapViewOfFile(mapView);
887
    CloseHandle(hMap);
888
    CloseHandle(hFile);
889
890
    return runtime_addr;
891
}
892
893
894
static uintptr_t
895
search_windows_map_for_section(proc_handle_t* handle, const char* secname, const wchar_t* substr,
896
                               section_validator_t validator) {
897
    HANDLE hProcSnap;
898
    do {
899
        hProcSnap = CreateToolhelp32Snapshot(TH32CS_SNAPMODULE, handle->pid);
900
    } while (hProcSnap == INVALID_HANDLE_VALUE && GetLastError() == ERROR_BAD_LENGTH);
901
902
    if (hProcSnap == INVALID_HANDLE_VALUE) {
903
        PyErr_SetFromWindowsErr(0);
904
        DWORD error = GetLastError();
905
        PyErr_Format(PyExc_PermissionError,
906
            "Unable to create module snapshot for PID %d section '%s' "
907
            "search (error %lu). Check permissions or PID validity",
908
            handle->pid, secname, error);
909
        return 0;
910
    }
911
912
    MODULEENTRY32W moduleEntry;
913
    moduleEntry.dwSize = sizeof(moduleEntry);
914
    void* runtime_addr = NULL;
915
916
    for (BOOL hasModule = Module32FirstW(hProcSnap, &moduleEntry); hasModule; hasModule = Module32NextW(hProcSnap, &moduleEntry)) {
917
        // Look for either python executable or DLL
918
        if (wcsstr(moduleEntry.szModule, substr)) {
919
            void *candidate = analyze_pe(moduleEntry.szExePath, moduleEntry.modBaseAddr, secname);
920
            if (candidate != NULL
921
                && (validator == NULL || validator(handle, (uintptr_t)candidate)))
922
            {
923
                runtime_addr = candidate;
924
                break;
925
            }
926
        }
927
    }
928
929
    CloseHandle(hProcSnap);
930
931
    return (uintptr_t)runtime_addr;
932
}
933
934
#endif // MS_WINDOWS
935
936
// Get the PyRuntime section address for any platform
937
UNUSED static uintptr_t
938
_Py_RemoteDebug_GetPyRuntimeAddress(proc_handle_t* handle)
939
0
{
940
0
    uintptr_t address;
941
942
#ifdef MS_WINDOWS
943
    // On Windows, search for 'python' in executable or DLL
944
    address = search_windows_map_for_section(handle, "PyRuntime", L"python",
945
                                             _Py_RemoteDebug_ValidatePyRuntimeCookie);
946
    if (address == 0) {
947
        // Error out: 'python' substring covers both executable and DLL
948
        PyObject *exc = PyErr_GetRaisedException();
949
        PyErr_Format(PyExc_RuntimeError,
950
            "Failed to find the PyRuntime section in process %d on Windows platform",
951
            handle->pid);
952
        _PyErr_ChainExceptions1(exc);
953
    }
954
#elif defined(__linux__) && HAVE_PROCESS_VM_READV
955
    // On Linux, search for 'python' in executable or DLL
956
0
    address = search_linux_map_for_section(handle, "PyRuntime", "python",
957
0
                                           _Py_RemoteDebug_ValidatePyRuntimeCookie);
958
0
    if (address == 0) {
959
        // Error out: 'python' substring covers both executable and DLL
960
0
        PyObject *exc = PyErr_GetRaisedException();
961
0
        PyErr_Format(PyExc_RuntimeError,
962
0
            "Failed to find the PyRuntime section in process %d on Linux platform",
963
0
            handle->pid);
964
0
        _PyErr_ChainExceptions1(exc);
965
0
    }
966
#elif defined(__APPLE__) && defined(TARGET_OS_OSX) && TARGET_OS_OSX
967
    // On macOS, try libpython first, then fall back to python
968
    const char* candidates[] = {"libpython", "python", "Python", NULL};
969
    for (const char** candidate = candidates; *candidate; candidate++) {
970
        PyErr_Clear();
971
        address = search_map_for_section(handle, "PyRuntime", *candidate,
972
                                         _Py_RemoteDebug_ValidatePyRuntimeCookie);
973
        if (address != 0) {
974
            break;
975
        }
976
    }
977
    if (address == 0) {
978
        PyObject *exc = PyErr_GetRaisedException();
979
        PyErr_Format(PyExc_RuntimeError,
980
            "Failed to find the PyRuntime section in process %d "
981
            "on macOS platform (tried both libpython and python)",
982
            handle->pid);
983
        _PyErr_ChainExceptions1(exc);
984
    }
985
#else
986
    _set_debug_exception_cause(PyExc_RuntimeError,
987
        "Reading the PyRuntime section is not supported on this platform");
988
    return 0;
989
#endif
990
991
0
    return address;
992
0
}
993
994
#if defined(__linux__) && HAVE_PROCESS_VM_READV
995
996
static int
997
open_proc_mem_fd(proc_handle_t *handle)
998
0
{
999
0
    char mem_file_path[64];
1000
0
    sprintf(mem_file_path, "/proc/%d/mem", handle->pid);
1001
1002
0
    handle->memfd = open(mem_file_path, O_RDWR);
1003
0
    if (handle->memfd == -1) {
1004
0
        PyErr_SetFromErrno(PyExc_OSError);
1005
0
        _set_debug_exception_cause(PyExc_OSError,
1006
0
            "failed to open file %s: %s", mem_file_path, strerror(errno));
1007
0
        return -1;
1008
0
    }
1009
0
    return 0;
1010
0
}
1011
1012
// Why is pwritev not guarded? Except on Android API level 23 (no longer
1013
// supported), HAVE_PROCESS_VM_READV is sufficient.
1014
static int
1015
read_remote_memory_fallback(proc_handle_t *handle, uintptr_t remote_address, size_t len, void* dst)
1016
0
{
1017
0
    if (handle->memfd == -1) {
1018
0
        if (open_proc_mem_fd(handle) < 0) {
1019
0
            return -1;
1020
0
        }
1021
0
    }
1022
1023
0
    struct iovec local[1];
1024
0
    Py_ssize_t result = 0;
1025
0
    Py_ssize_t read_bytes = 0;
1026
1027
0
    do {
1028
0
        local[0].iov_base = (char*)dst + result;
1029
0
        local[0].iov_len = len - result;
1030
0
        off_t offset = remote_address + result;
1031
1032
0
        read_bytes = preadv(handle->memfd, local, 1, offset);
1033
0
        if (read_bytes < 0) {
1034
0
            PyErr_SetFromErrno(PyExc_OSError);
1035
0
            _set_debug_exception_cause(PyExc_OSError,
1036
0
                "preadv failed for PID %d at address 0x%lx "
1037
0
                "(size %zu, partial read %zd bytes): %s",
1038
0
                handle->pid, remote_address + result, len - result, result, strerror(errno));
1039
0
            return -1;
1040
0
        }
1041
1042
0
        result += read_bytes;
1043
0
    } while ((size_t)read_bytes != local[0].iov_len);
1044
0
    return 0;
1045
0
}
1046
1047
#endif // __linux__
1048
1049
// Platform-independent memory read function
1050
static int
1051
_Py_RemoteDebug_ReadRemoteMemory(proc_handle_t *handle, uintptr_t remote_address, size_t len, void* dst)
1052
0
{
1053
#ifdef MS_WINDOWS
1054
    SIZE_T read_bytes = 0;
1055
    SIZE_T result = 0;
1056
    do {
1057
        if (!ReadProcessMemory(handle->hProcess, (LPCVOID)(remote_address + result), (char*)dst + result, len - result, &read_bytes)) {
1058
            // Check if the process is still alive: we need to be able to tell our caller
1059
            // that the process is dead and not just that the read failed.
1060
            if (!is_process_alive(handle->hProcess)) {
1061
                _set_errno(ESRCH);
1062
                PyErr_SetFromErrno(PyExc_OSError);
1063
                return -1;
1064
            }
1065
            PyErr_SetFromWindowsErr(0);
1066
            DWORD error = GetLastError();
1067
            _set_debug_exception_cause(PyExc_OSError,
1068
                "ReadProcessMemory failed for PID %d at address 0x%lx "
1069
                "(size %zu, partial read %zu bytes): Windows error %lu",
1070
                handle->pid, remote_address + result, len - result, result, error);
1071
            return -1;
1072
        }
1073
        result += read_bytes;
1074
    } while (result < len);
1075
    return 0;
1076
#elif defined(__linux__) && HAVE_PROCESS_VM_READV
1077
0
    if (handle->memfd != -1) {
1078
0
        return read_remote_memory_fallback(handle, remote_address, len, dst);
1079
0
    }
1080
0
    struct iovec local[1];
1081
0
    struct iovec remote[1];
1082
0
    Py_ssize_t result = 0;
1083
0
    Py_ssize_t read_bytes = 0;
1084
1085
0
    do {
1086
0
        local[0].iov_base = (char*)dst + result;
1087
0
        local[0].iov_len = len - result;
1088
0
        remote[0].iov_base = (void*)(remote_address + result);
1089
0
        remote[0].iov_len = len - result;
1090
1091
0
        read_bytes = process_vm_readv(handle->pid, local, 1, remote, 1, 0);
1092
0
        if (read_bytes < 0) {
1093
0
            if (errno == ENOSYS) {
1094
0
                return read_remote_memory_fallback(handle, remote_address, len, dst);
1095
0
            }
1096
0
            PyErr_SetFromErrno(PyExc_OSError);
1097
0
            if (errno == ESRCH) {
1098
0
                return -1;
1099
0
            }
1100
0
            _set_debug_exception_cause(PyExc_OSError,
1101
0
                "process_vm_readv failed for PID %d at address 0x%lx "
1102
0
                "(size %zu, partial read %zd bytes): %s",
1103
0
                handle->pid, remote_address + result, len - result, result, strerror(errno));
1104
0
            return -1;
1105
0
        }
1106
1107
0
        result += read_bytes;
1108
0
    } while ((size_t)read_bytes != local[0].iov_len);
1109
0
    return 0;
1110
#elif defined(__APPLE__) && defined(TARGET_OS_OSX) && TARGET_OS_OSX
1111
    Py_ssize_t result = -1;
1112
    kern_return_t kr = mach_vm_read_overwrite(
1113
        handle->task,
1114
        (mach_vm_address_t)remote_address,
1115
        len,
1116
        (mach_vm_address_t)dst,
1117
        (mach_vm_size_t*)&result);
1118
1119
    if (kr != KERN_SUCCESS) {
1120
        switch (err_get_code(kr)) {
1121
        case KERN_PROTECTION_FAILURE:
1122
            PyErr_Format(PyExc_PermissionError,
1123
                "Memory protection failure reading from PID %d at address "
1124
                "0x%lx (size %zu): insufficient permissions",
1125
                handle->pid, remote_address, len);
1126
            break;
1127
        case KERN_INVALID_ARGUMENT: {
1128
            // Perform a task_info check to see if the invalid argument is due
1129
            // to the process being terminated
1130
            task_basic_info_data_t task_basic_info;
1131
            mach_msg_type_number_t task_info_count = TASK_BASIC_INFO_COUNT;
1132
            kern_return_t task_valid_check = task_info(handle->task, TASK_BASIC_INFO,
1133
                                                        (task_info_t)&task_basic_info,
1134
                                                        &task_info_count);
1135
            if (task_valid_check == KERN_INVALID_ARGUMENT) {
1136
                PyErr_Format(PyExc_ProcessLookupError,
1137
                    "Process %d is no longer accessible (process terminated)",
1138
                    handle->pid);
1139
            } else {
1140
                PyErr_Format(PyExc_ValueError,
1141
                    "Invalid argument to mach_vm_read_overwrite for PID %d at "
1142
                    "address 0x%lx (size %zu) - check memory permissions",
1143
                    handle->pid, remote_address, len);
1144
            }
1145
            break;
1146
        }
1147
        case KERN_NO_SPACE:
1148
        case KERN_MEMORY_ERROR:
1149
            PyErr_Format(PyExc_ProcessLookupError,
1150
                "Process %d memory space no longer available (process terminated)",
1151
                handle->pid);
1152
            break;
1153
        default:
1154
            PyErr_Format(PyExc_RuntimeError,
1155
                "mach_vm_read_overwrite failed for PID %d at address 0x%lx "
1156
                "(size %zu): kern_return_t %d",
1157
                handle->pid, remote_address, len, kr);
1158
        }
1159
        return -1;
1160
    }
1161
    return 0;
1162
#else
1163
    Py_UNREACHABLE();
1164
#endif
1165
0
}
1166
1167
#if defined(__linux__) && HAVE_PROCESS_VM_READV
1168
// Fallback write using /proc/pid/mem
1169
static int
1170
_Py_RemoteDebug_WriteRemoteMemoryFallback(proc_handle_t *handle, uintptr_t remote_address, size_t len, const void* src)
1171
0
{
1172
0
    if (handle->memfd == -1) {
1173
0
        if (open_proc_mem_fd(handle) < 0) {
1174
0
            return -1;
1175
0
        }
1176
0
    }
1177
1178
0
    struct iovec local[1];
1179
0
    Py_ssize_t result = 0;
1180
0
    Py_ssize_t written = 0;
1181
1182
0
    do {
1183
0
        local[0].iov_base = (char*)src + result;
1184
0
        local[0].iov_len = len - result;
1185
0
        off_t offset = remote_address + result;
1186
1187
0
        written = pwritev(handle->memfd, local, 1, offset);
1188
0
        if (written < 0) {
1189
0
            PyErr_SetFromErrno(PyExc_OSError);
1190
0
            return -1;
1191
0
        }
1192
1193
0
        result += written;
1194
0
    } while ((size_t)written != local[0].iov_len);
1195
0
    return 0;
1196
0
}
1197
#endif // __linux__
1198
1199
// Platform-independent memory write function
1200
UNUSED static int
1201
_Py_RemoteDebug_WriteRemoteMemory(proc_handle_t *handle, uintptr_t remote_address, size_t len, const void* src)
1202
0
{
1203
#ifdef MS_WINDOWS
1204
    SIZE_T written = 0;
1205
    SIZE_T result = 0;
1206
    do {
1207
        if (!WriteProcessMemory(handle->hProcess, (LPVOID)(remote_address + result), (const char*)src + result, len - result, &written)) {
1208
            PyErr_SetFromWindowsErr(0);
1209
            DWORD error = GetLastError();
1210
            _set_debug_exception_cause(PyExc_OSError,
1211
                "WriteProcessMemory failed for PID %d at address 0x%lx "
1212
                "(size %zu, partial write %zu bytes): Windows error %lu",
1213
                handle->pid, remote_address + result, len - result, result, error);
1214
            return -1;
1215
        }
1216
        result += written;
1217
    } while (result < len);
1218
    return 0;
1219
#elif defined(__linux__) && HAVE_PROCESS_VM_READV
1220
0
    if (handle->memfd != -1) {
1221
0
        return _Py_RemoteDebug_WriteRemoteMemoryFallback(handle, remote_address, len, src);
1222
0
    }
1223
0
    struct iovec local[1];
1224
0
    struct iovec remote[1];
1225
0
    Py_ssize_t result = 0;
1226
0
    Py_ssize_t written = 0;
1227
1228
0
    do {
1229
0
        local[0].iov_base = (void*)((char*)src + result);
1230
0
        local[0].iov_len = len - result;
1231
0
        remote[0].iov_base = (void*)((char*)remote_address + result);
1232
0
        remote[0].iov_len = len - result;
1233
1234
0
        written = process_vm_writev(handle->pid, local, 1, remote, 1, 0);
1235
0
        if (written < 0) {
1236
0
            if (errno == ENOSYS) {
1237
0
                return _Py_RemoteDebug_WriteRemoteMemoryFallback(handle, remote_address, len, src);
1238
0
            }
1239
0
            PyErr_SetFromErrno(PyExc_OSError);
1240
0
            _set_debug_exception_cause(PyExc_OSError,
1241
0
                "process_vm_writev failed for PID %d at address 0x%lx "
1242
0
                "(size %zu, partial write %zd bytes): %s",
1243
0
                handle->pid, remote_address + result, len - result, result, strerror(errno));
1244
0
            return -1;
1245
0
        }
1246
1247
0
        result += written;
1248
0
    } while ((size_t)written != local[0].iov_len);
1249
0
    return 0;
1250
#elif defined(__APPLE__) && defined(TARGET_OS_OSX) && TARGET_OS_OSX
1251
    kern_return_t kr = mach_vm_write(
1252
        handle->task,
1253
        (mach_vm_address_t)remote_address,
1254
        (vm_offset_t)src,
1255
        (mach_msg_type_number_t)len);
1256
1257
    if (kr != KERN_SUCCESS) {
1258
        switch (kr) {
1259
        case KERN_PROTECTION_FAILURE:
1260
            PyErr_SetString(PyExc_PermissionError, "Not enough permissions to write memory");
1261
            break;
1262
        case KERN_INVALID_ARGUMENT:
1263
            PyErr_SetString(PyExc_PermissionError, "Invalid argument to mach_vm_write");
1264
            break;
1265
        default:
1266
            PyErr_Format(PyExc_RuntimeError, "Unknown error writing memory: %d", (int)kr);
1267
        }
1268
        return -1;
1269
    }
1270
    return 0;
1271
#else
1272
    Py_UNREACHABLE();
1273
#endif
1274
0
}
1275
1276
UNUSED static int
1277
_Py_RemoteDebug_PagedReadRemoteMemory(proc_handle_t *handle,
1278
                                      uintptr_t addr,
1279
                                      size_t size,
1280
                                      void *out)
1281
0
{
1282
0
    size_t page_size = handle->page_size;
1283
0
    uintptr_t page_base = addr & ~(page_size - 1);
1284
0
    size_t offset_in_page = addr - page_base;
1285
0
1286
0
    if (offset_in_page + size > page_size) {
1287
0
        return _Py_RemoteDebug_ReadRemoteMemory(handle, addr, size, out);
1288
0
    }
1289
0
1290
0
    // Search for valid cached page
1291
0
    for (int i = 0; i < MAX_PAGES; i++) {
1292
0
        page_cache_entry_t *entry = &handle->pages[i];
1293
0
        if (entry->valid && entry->page_addr == page_base) {
1294
0
            memcpy(out, entry->data + offset_in_page, size);
1295
0
            return 0;
1296
0
        }
1297
0
    }
1298
0
1299
0
    // Find reusable slot
1300
0
    for (int i = 0; i < MAX_PAGES; i++) {
1301
0
        page_cache_entry_t *entry = &handle->pages[i];
1302
0
        if (!entry->valid) {
1303
0
            if (entry->data == NULL) {
1304
0
                entry->data = PyMem_RawMalloc(page_size);
1305
0
                if (entry->data == NULL) {
1306
0
                    PyErr_NoMemory();
1307
0
                    _set_debug_exception_cause(PyExc_MemoryError,
1308
0
                        "Cannot allocate %zu bytes for page cache entry "
1309
0
                        "during read from PID %d at address 0x%lx",
1310
0
                        page_size, handle->pid, addr);
1311
0
                    return -1;
1312
0
                }
1313
0
            }
1314
0
1315
0
            if (_Py_RemoteDebug_ReadRemoteMemory(handle, page_base, page_size, entry->data) < 0) {
1316
0
                // Try to just copy the exact amount as a fallback
1317
0
                PyErr_Clear();
1318
0
                goto fallback;
1319
0
            }
1320
0
1321
0
            entry->page_addr = page_base;
1322
0
            entry->valid = 1;
1323
0
            memcpy(out, entry->data + offset_in_page, size);
1324
0
            return 0;
1325
0
        }
1326
0
    }
1327
0
1328
0
fallback:
1329
0
    // Cache full — fallback to uncached read
1330
0
    return _Py_RemoteDebug_ReadRemoteMemory(handle, addr, size, out);
1331
0
}
1332
1333
UNUSED static int
1334
_Py_RemoteDebug_ReadDebugOffsets(
1335
    proc_handle_t *handle,
1336
    uintptr_t *runtime_start_address,
1337
    _Py_DebugOffsets* debug_offsets
1338
0
) {
1339
0
    *runtime_start_address = _Py_RemoteDebug_GetPyRuntimeAddress(handle);
1340
0
    if (!*runtime_start_address) {
1341
0
        if (!PyErr_Occurred()) {
1342
0
            PyErr_Format(PyExc_RuntimeError,
1343
0
                "Failed to locate PyRuntime address for PID %d",
1344
0
                handle->pid);
1345
0
        }
1346
0
        _set_debug_exception_cause(PyExc_RuntimeError, "PyRuntime address lookup failed during debug offsets initialization");
1347
0
        return -1;
1348
0
    }
1349
0
    size_t size = sizeof(struct _Py_DebugOffsets);
1350
0
    if (0 != _Py_RemoteDebug_ReadRemoteMemory(handle, *runtime_start_address, size, debug_offsets)) {
1351
0
        _set_debug_exception_cause(PyExc_RuntimeError, "Failed to read debug offsets structure from remote process");
1352
0
        return -1;
1353
0
    }
1354
0
    return 0;
1355
0
}
1356
1357
#ifdef __cplusplus
1358
}
1359
#endif