Coverage Report

Created: 2025-07-11 06:24

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