Coverage Report

Created: 2025-12-14 07:06

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