Coverage Report

Created: 2025-08-26 06:26

/src/cpython/Python/remote_debug.h
Line
Count
Source (jump to first uncovered line)
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
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
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
        const char *filename = strrchr(path, '/');
726
0
        if (filename) {
727
0
            filename++;  // Move past the '/'
728
0
        } else {
729
0
            filename = path;  // No directories, or an empty string
730
0
        }
731
732
0
        if (strstr(filename, substr)) {
733
0
            retval = search_elf_file_for_section(handle, secname, start, path);
734
0
            if (retval) {
735
0
                break;
736
0
            }
737
0
        }
738
0
    }
739
740
0
    PyMem_Free(line);
741
0
    if (fclose(maps_file) != 0) {
742
0
        PyErr_Format(PyExc_OSError,
743
0
            "Failed to close process map file '%s': %s",
744
0
            maps_file_path, strerror(errno));
745
0
        retval = 0;
746
0
    }
747
748
0
    return retval;
749
0
}
750
751
752
#endif // __linux__
753
754
#ifdef MS_WINDOWS
755
756
static int is_process_alive(HANDLE hProcess) {
757
    DWORD exitCode;
758
    if (GetExitCodeProcess(hProcess, &exitCode)) {
759
        return exitCode == STILL_ACTIVE;
760
    }
761
    return 0;
762
}
763
764
static void* analyze_pe(const wchar_t* mod_path, BYTE* remote_base, const char* secname) {
765
    HANDLE hFile = CreateFileW(mod_path, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
766
    if (hFile == INVALID_HANDLE_VALUE) {
767
        PyErr_SetFromWindowsErr(0);
768
        DWORD error = GetLastError();
769
        PyErr_Format(PyExc_OSError,
770
            "Cannot open PE file for section '%s' analysis (error %lu)",
771
            secname, error);
772
        return NULL;
773
    }
774
775
    HANDLE hMap = CreateFileMapping(hFile, NULL, PAGE_READONLY, 0, 0, 0);
776
    if (!hMap) {
777
        PyErr_SetFromWindowsErr(0);
778
        DWORD error = GetLastError();
779
        PyErr_Format(PyExc_OSError,
780
            "Cannot create file mapping for PE file section '%s' analysis (error %lu)",
781
            secname, error);
782
        CloseHandle(hFile);
783
        return NULL;
784
    }
785
786
    BYTE* mapView = (BYTE*)MapViewOfFile(hMap, FILE_MAP_READ, 0, 0, 0);
787
    if (!mapView) {
788
        PyErr_SetFromWindowsErr(0);
789
        DWORD error = GetLastError();
790
        PyErr_Format(PyExc_OSError,
791
            "Cannot map view of PE file for section '%s' analysis (error %lu)",
792
            secname, error);
793
        CloseHandle(hMap);
794
        CloseHandle(hFile);
795
        return NULL;
796
    }
797
798
    IMAGE_DOS_HEADER* pDOSHeader = (IMAGE_DOS_HEADER*)mapView;
799
    if (pDOSHeader->e_magic != IMAGE_DOS_SIGNATURE) {
800
        PyErr_Format(PyExc_RuntimeError,
801
            "Invalid DOS signature (0x%x) in PE file for section '%s' analysis (expected 0x%x)",
802
            pDOSHeader->e_magic, secname, IMAGE_DOS_SIGNATURE);
803
        UnmapViewOfFile(mapView);
804
        CloseHandle(hMap);
805
        CloseHandle(hFile);
806
        return NULL;
807
    }
808
809
    IMAGE_NT_HEADERS* pNTHeaders = (IMAGE_NT_HEADERS*)(mapView + pDOSHeader->e_lfanew);
810
    if (pNTHeaders->Signature != IMAGE_NT_SIGNATURE) {
811
        PyErr_Format(PyExc_RuntimeError,
812
            "Invalid NT signature (0x%lx) in PE file for section '%s' analysis (expected 0x%lx)",
813
            pNTHeaders->Signature, secname, IMAGE_NT_SIGNATURE);
814
        UnmapViewOfFile(mapView);
815
        CloseHandle(hMap);
816
        CloseHandle(hFile);
817
        return NULL;
818
    }
819
820
    IMAGE_SECTION_HEADER* pSection_header = (IMAGE_SECTION_HEADER*)(mapView + pDOSHeader->e_lfanew + sizeof(IMAGE_NT_HEADERS));
821
    void* runtime_addr = NULL;
822
823
    for (int i = 0; i < pNTHeaders->FileHeader.NumberOfSections; i++) {
824
        const char* name = (const char*)pSection_header[i].Name;
825
        if (strncmp(name, secname, IMAGE_SIZEOF_SHORT_NAME) == 0) {
826
            runtime_addr = remote_base + pSection_header[i].VirtualAddress;
827
            break;
828
        }
829
    }
830
831
    UnmapViewOfFile(mapView);
832
    CloseHandle(hMap);
833
    CloseHandle(hFile);
834
835
    return runtime_addr;
836
}
837
838
839
static uintptr_t
840
search_windows_map_for_section(proc_handle_t* handle, const char* secname, const wchar_t* substr) {
841
    HANDLE hProcSnap;
842
    do {
843
        hProcSnap = CreateToolhelp32Snapshot(TH32CS_SNAPMODULE, handle->pid);
844
    } while (hProcSnap == INVALID_HANDLE_VALUE && GetLastError() == ERROR_BAD_LENGTH);
845
846
    if (hProcSnap == INVALID_HANDLE_VALUE) {
847
        PyErr_SetFromWindowsErr(0);
848
        DWORD error = GetLastError();
849
        PyErr_Format(PyExc_PermissionError,
850
            "Unable to create module snapshot for PID %d section '%s' "
851
            "search (error %lu). Check permissions or PID validity",
852
            handle->pid, secname, error);
853
        return 0;
854
    }
855
856
    MODULEENTRY32W moduleEntry;
857
    moduleEntry.dwSize = sizeof(moduleEntry);
858
    void* runtime_addr = NULL;
859
860
    for (BOOL hasModule = Module32FirstW(hProcSnap, &moduleEntry); hasModule; hasModule = Module32NextW(hProcSnap, &moduleEntry)) {
861
        // Look for either python executable or DLL
862
        if (wcsstr(moduleEntry.szModule, substr)) {
863
            runtime_addr = analyze_pe(moduleEntry.szExePath, moduleEntry.modBaseAddr, secname);
864
            if (runtime_addr != NULL) {
865
                break;
866
            }
867
        }
868
    }
869
870
    CloseHandle(hProcSnap);
871
872
    return (uintptr_t)runtime_addr;
873
}
874
875
#endif // MS_WINDOWS
876
877
// Get the PyRuntime section address for any platform
878
static uintptr_t
879
_Py_RemoteDebug_GetPyRuntimeAddress(proc_handle_t* handle)
880
0
{
881
0
    uintptr_t address;
882
883
#ifdef MS_WINDOWS
884
    // On Windows, search for 'python' in executable or DLL
885
    address = search_windows_map_for_section(handle, "PyRuntime", L"python");
886
    if (address == 0) {
887
        // Error out: 'python' substring covers both executable and DLL
888
        PyObject *exc = PyErr_GetRaisedException();
889
        PyErr_Format(PyExc_RuntimeError,
890
            "Failed to find the PyRuntime section in process %d on Windows platform",
891
            handle->pid);
892
        _PyErr_ChainExceptions1(exc);
893
    }
894
#elif defined(__linux__)
895
    // On Linux, search for 'python' in executable or DLL
896
0
    address = search_linux_map_for_section(handle, "PyRuntime", "python");
897
0
    if (address == 0) {
898
        // Error out: 'python' substring covers both executable and DLL
899
0
        PyObject *exc = PyErr_GetRaisedException();
900
0
        PyErr_Format(PyExc_RuntimeError,
901
0
            "Failed to find the PyRuntime section in process %d on Linux platform",
902
0
            handle->pid);
903
0
        _PyErr_ChainExceptions1(exc);
904
0
    }
905
#elif defined(__APPLE__) && defined(TARGET_OS_OSX) && TARGET_OS_OSX
906
    // On macOS, try libpython first, then fall back to python
907
    const char* candidates[] = {"libpython", "python", "Python", NULL};
908
    for (const char** candidate = candidates; *candidate; candidate++) {
909
        PyErr_Clear();
910
        address = search_map_for_section(handle, "PyRuntime", *candidate);
911
        if (address != 0) {
912
            break;
913
        }
914
    }
915
    if (address == 0) {
916
        PyObject *exc = PyErr_GetRaisedException();
917
        PyErr_Format(PyExc_RuntimeError,
918
            "Failed to find the PyRuntime section in process %d "
919
            "on macOS platform (tried both libpython and python)",
920
            handle->pid);
921
        _PyErr_ChainExceptions1(exc);
922
    }
923
#else
924
    _set_debug_exception_cause(PyExc_RuntimeError,
925
        "Reading the PyRuntime section is not supported on this platform");
926
    return 0;
927
#endif
928
929
0
    return address;
930
0
}
931
932
#if defined(__linux__) && HAVE_PROCESS_VM_READV
933
934
static int
935
open_proc_mem_fd(proc_handle_t *handle)
936
0
{
937
0
    char mem_file_path[64];
938
0
    sprintf(mem_file_path, "/proc/%d/mem", handle->pid);
939
940
0
    handle->memfd = open(mem_file_path, O_RDWR);
941
0
    if (handle->memfd == -1) {
942
0
        PyErr_SetFromErrno(PyExc_OSError);
943
0
        _set_debug_exception_cause(PyExc_OSError,
944
0
            "failed to open file %s: %s", mem_file_path, strerror(errno));
945
0
        return -1;
946
0
    }
947
0
    return 0;
948
0
}
949
950
// Why is pwritev not guarded? Except on Android API level 23 (no longer
951
// supported), HAVE_PROCESS_VM_READV is sufficient.
952
static int
953
read_remote_memory_fallback(proc_handle_t *handle, uintptr_t remote_address, size_t len, void* dst)
954
0
{
955
0
    if (handle->memfd == -1) {
956
0
        if (open_proc_mem_fd(handle) < 0) {
957
0
            return -1;
958
0
        }
959
0
    }
960
961
0
    struct iovec local[1];
962
0
    Py_ssize_t result = 0;
963
0
    Py_ssize_t read_bytes = 0;
964
965
0
    do {
966
0
        local[0].iov_base = (char*)dst + result;
967
0
        local[0].iov_len = len - result;
968
0
        off_t offset = remote_address + result;
969
970
0
        read_bytes = preadv(handle->memfd, local, 1, offset);
971
0
        if (read_bytes < 0) {
972
0
            PyErr_SetFromErrno(PyExc_OSError);
973
0
            _set_debug_exception_cause(PyExc_OSError,
974
0
                "preadv failed for PID %d at address 0x%lx "
975
0
                "(size %zu, partial read %zd bytes): %s",
976
0
                handle->pid, remote_address + result, len - result, result, strerror(errno));
977
0
            return -1;
978
0
        }
979
980
0
        result += read_bytes;
981
0
    } while ((size_t)read_bytes != local[0].iov_len);
982
0
    return 0;
983
0
}
984
985
#endif // __linux__
986
987
// Platform-independent memory read function
988
static int
989
_Py_RemoteDebug_ReadRemoteMemory(proc_handle_t *handle, uintptr_t remote_address, size_t len, void* dst)
990
0
{
991
#ifdef MS_WINDOWS
992
    SIZE_T read_bytes = 0;
993
    SIZE_T result = 0;
994
    do {
995
        if (!ReadProcessMemory(handle->hProcess, (LPCVOID)(remote_address + result), (char*)dst + result, len - result, &read_bytes)) {
996
            // Check if the process is still alive: we need to be able to tell our caller
997
            // that the process is dead and not just that the read failed.
998
            if (!is_process_alive(handle->hProcess)) {
999
                _set_errno(ESRCH);
1000
                PyErr_SetFromErrno(PyExc_OSError);
1001
                return -1;
1002
            }
1003
            PyErr_SetFromWindowsErr(0);
1004
            DWORD error = GetLastError();
1005
            _set_debug_exception_cause(PyExc_OSError,
1006
                "ReadProcessMemory failed for PID %d at address 0x%lx "
1007
                "(size %zu, partial read %zu bytes): Windows error %lu",
1008
                handle->pid, remote_address + result, len - result, result, error);
1009
            return -1;
1010
        }
1011
        result += read_bytes;
1012
    } while (result < len);
1013
    return 0;
1014
#elif defined(__linux__) && HAVE_PROCESS_VM_READV
1015
0
    if (handle->memfd != -1) {
1016
0
        return read_remote_memory_fallback(handle, remote_address, len, dst);
1017
0
    }
1018
0
    struct iovec local[1];
1019
0
    struct iovec remote[1];
1020
0
    Py_ssize_t result = 0;
1021
0
    Py_ssize_t read_bytes = 0;
1022
1023
0
    do {
1024
0
        local[0].iov_base = (char*)dst + result;
1025
0
        local[0].iov_len = len - result;
1026
0
        remote[0].iov_base = (void*)(remote_address + result);
1027
0
        remote[0].iov_len = len - result;
1028
1029
0
        read_bytes = process_vm_readv(handle->pid, local, 1, remote, 1, 0);
1030
0
        if (read_bytes < 0) {
1031
0
            if (errno == ENOSYS) {
1032
0
                return read_remote_memory_fallback(handle, remote_address, len, dst);
1033
0
            }
1034
0
            PyErr_SetFromErrno(PyExc_OSError);
1035
0
            if (errno == ESRCH) {
1036
0
                return -1;
1037
0
            }
1038
0
            _set_debug_exception_cause(PyExc_OSError,
1039
0
                "process_vm_readv failed for PID %d at address 0x%lx "
1040
0
                "(size %zu, partial read %zd bytes): %s",
1041
0
                handle->pid, remote_address + result, len - result, result, strerror(errno));
1042
0
            return -1;
1043
0
        }
1044
1045
0
        result += read_bytes;
1046
0
    } while ((size_t)read_bytes != local[0].iov_len);
1047
0
    return 0;
1048
#elif defined(__APPLE__) && defined(TARGET_OS_OSX) && TARGET_OS_OSX
1049
    Py_ssize_t result = -1;
1050
    kern_return_t kr = mach_vm_read_overwrite(
1051
        handle->task,
1052
        (mach_vm_address_t)remote_address,
1053
        len,
1054
        (mach_vm_address_t)dst,
1055
        (mach_vm_size_t*)&result);
1056
1057
    if (kr != KERN_SUCCESS) {
1058
        switch (err_get_code(kr)) {
1059
        case KERN_PROTECTION_FAILURE:
1060
            PyErr_Format(PyExc_PermissionError,
1061
                "Memory protection failure reading from PID %d at address "
1062
                "0x%lx (size %zu): insufficient permissions",
1063
                handle->pid, remote_address, len);
1064
            break;
1065
        case KERN_INVALID_ARGUMENT: {
1066
            // Perform a task_info check to see if the invalid argument is due
1067
            // to the process being terminated
1068
            task_basic_info_data_t task_basic_info;
1069
            mach_msg_type_number_t task_info_count = TASK_BASIC_INFO_COUNT;
1070
            kern_return_t task_valid_check = task_info(handle->task, TASK_BASIC_INFO,
1071
                                                        (task_info_t)&task_basic_info,
1072
                                                        &task_info_count);
1073
            if (task_valid_check == KERN_INVALID_ARGUMENT) {
1074
                PyErr_Format(PyExc_ProcessLookupError,
1075
                    "Process %d is no longer accessible (process terminated)",
1076
                    handle->pid);
1077
            } else {
1078
                PyErr_Format(PyExc_ValueError,
1079
                    "Invalid argument to mach_vm_read_overwrite for PID %d at "
1080
                    "address 0x%lx (size %zu) - check memory permissions",
1081
                    handle->pid, remote_address, len);
1082
            }
1083
            break;
1084
        }
1085
        case KERN_NO_SPACE:
1086
        case KERN_MEMORY_ERROR:
1087
            PyErr_Format(PyExc_ProcessLookupError,
1088
                "Process %d memory space no longer available (process terminated)",
1089
                handle->pid);
1090
            break;
1091
        default:
1092
            PyErr_Format(PyExc_RuntimeError,
1093
                "mach_vm_read_overwrite failed for PID %d at address 0x%lx "
1094
                "(size %zu): kern_return_t %d",
1095
                handle->pid, remote_address, len, kr);
1096
        }
1097
        return -1;
1098
    }
1099
    return 0;
1100
#else
1101
    Py_UNREACHABLE();
1102
#endif
1103
0
}
1104
1105
UNUSED static int
1106
_Py_RemoteDebug_PagedReadRemoteMemory(proc_handle_t *handle,
1107
                                      uintptr_t addr,
1108
                                      size_t size,
1109
                                      void *out)
1110
0
{
1111
0
    size_t page_size = handle->page_size;
1112
0
    uintptr_t page_base = addr & ~(page_size - 1);
1113
0
    size_t offset_in_page = addr - page_base;
1114
0
1115
0
    if (offset_in_page + size > page_size) {
1116
0
        return _Py_RemoteDebug_ReadRemoteMemory(handle, addr, size, out);
1117
0
    }
1118
0
1119
0
    // Search for valid cached page
1120
0
    for (int i = 0; i < MAX_PAGES; i++) {
1121
0
        page_cache_entry_t *entry = &handle->pages[i];
1122
0
        if (entry->valid && entry->page_addr == page_base) {
1123
0
            memcpy(out, entry->data + offset_in_page, size);
1124
0
            return 0;
1125
0
        }
1126
0
    }
1127
0
1128
0
    // Find reusable slot
1129
0
    for (int i = 0; i < MAX_PAGES; i++) {
1130
0
        page_cache_entry_t *entry = &handle->pages[i];
1131
0
        if (!entry->valid) {
1132
0
            if (entry->data == NULL) {
1133
0
                entry->data = PyMem_RawMalloc(page_size);
1134
0
                if (entry->data == NULL) {
1135
0
                    _set_debug_exception_cause(PyExc_MemoryError,
1136
0
                        "Cannot allocate %zu bytes for page cache entry "
1137
0
                        "during read from PID %d at address 0x%lx",
1138
0
                        page_size, handle->pid, addr);
1139
0
                    return -1;
1140
0
                }
1141
0
            }
1142
0
1143
0
            if (_Py_RemoteDebug_ReadRemoteMemory(handle, page_base, page_size, entry->data) < 0) {
1144
0
                // Try to just copy the exact ammount as a fallback
1145
0
                PyErr_Clear();
1146
0
                goto fallback;
1147
0
            }
1148
0
1149
0
            entry->page_addr = page_base;
1150
0
            entry->valid = 1;
1151
0
            memcpy(out, entry->data + offset_in_page, size);
1152
0
            return 0;
1153
0
        }
1154
0
    }
1155
0
1156
0
fallback:
1157
0
    // Cache full — fallback to uncached read
1158
0
    return _Py_RemoteDebug_ReadRemoteMemory(handle, addr, size, out);
1159
0
}
1160
1161
static int
1162
_Py_RemoteDebug_ReadDebugOffsets(
1163
    proc_handle_t *handle,
1164
    uintptr_t *runtime_start_address,
1165
    _Py_DebugOffsets* debug_offsets
1166
0
) {
1167
0
    *runtime_start_address = _Py_RemoteDebug_GetPyRuntimeAddress(handle);
1168
0
    if (!*runtime_start_address) {
1169
0
        if (!PyErr_Occurred()) {
1170
0
            PyErr_Format(PyExc_RuntimeError,
1171
0
                "Failed to locate PyRuntime address for PID %d",
1172
0
                handle->pid);
1173
0
        }
1174
0
        _set_debug_exception_cause(PyExc_RuntimeError, "PyRuntime address lookup failed during debug offsets initialization");
1175
0
        return -1;
1176
0
    }
1177
0
    size_t size = sizeof(struct _Py_DebugOffsets);
1178
0
    if (0 != _Py_RemoteDebug_ReadRemoteMemory(handle, *runtime_start_address, size, debug_offsets)) {
1179
0
        _set_debug_exception_cause(PyExc_RuntimeError, "Failed to read debug offsets structure from remote process");
1180
0
        return -1;
1181
0
    }
1182
0
    return 0;
1183
0
}
1184
1185
#ifdef __cplusplus
1186
}
1187
#endif