Skip to content

Commit be520c8

Browse files
committed
Add macOS support
1 parent 8b7f010 commit be520c8

File tree

5 files changed

+232
-32
lines changed

5 files changed

+232
-32
lines changed

Lib/test/test_external_inspection.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,7 @@ def _make_test_script(script_dir, script_basename, source):
2323

2424
class TestGetStackTrace(unittest.TestCase):
2525

26-
@unittest.skipIf(sys.platform != "linux", "Test only runs on Linux")
27-
@unittest.skipIf(not PROCESS_VM_READV_SUPPORTED, "Test only runs on Linux with process_vm_readv support")
26+
@unittest.skipIf(sys.platform == "linux" and not PROCESS_VM_READV_SUPPORTED, "Test only runs on Linux with process_vm_readv support")
2827
def test_stack_trace(self):
2928
# Spawn a process with some realistic Python code
3029
script = textwrap.dedent("""\

Modules/_testexternalinspection.c

Lines changed: 228 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22

33
#ifdef __linux__
44
# include <elf.h>
5-
# include <sys/mman.h>
65
# include <sys/uio.h>
76
#if INTPTR_MAX == INT64_MAX
87
# define Elf_Ehdr Elf64_Ehdr
@@ -13,27 +12,229 @@
1312
#endif
1413
#endif
1514

15+
#ifdef __APPLE__
16+
#include <mach-o/fat.h>
17+
#include <mach-o/loader.h>
18+
#include <mach-o/nlist.h>
19+
#include <mach/mach.h>
20+
#include <mach/mach_vm.h>
21+
#include <mach/machine.h>
22+
#include <libproc.h>
23+
#endif
24+
1625
#include <errno.h>
1726
#include <fcntl.h>
27+
#include <stddef.h>
28+
#include <stdint.h>
1829
#include <stdio.h>
1930
#include <stdlib.h>
2031
#include <string.h>
32+
#include <sys/mman.h>
33+
#include <sys/param.h>
34+
#include <sys/proc.h>
2135
#include <sys/stat.h>
36+
#include <sys/sysctl.h>
2237
#include <sys/types.h>
2338
#include <unistd.h>
24-
#include <stdint.h>
2539

2640
#ifndef Py_BUILD_CORE_BUILTIN
2741
# define Py_BUILD_CORE_MODULE 1
2842
#endif
2943
#include "Python.h"
3044
#include <internal/pycore_runtime.h>
3145

46+
#ifndef HAVE_PROCESS_VM_READV
47+
#define HAVE_PROCESS_VM_READV 0
48+
#endif
49+
50+
#ifdef __APPLE__
51+
52+
static void* analyze_macho64(mach_port_t proc_ref, void *base, void *map) {
53+
struct mach_header_64 *hdr = (struct mach_header_64 *)map;
54+
int ncmds = hdr->ncmds;
55+
56+
int cmd_cnt = 0;
57+
struct segment_command_64 *cmd = map + sizeof(struct mach_header_64);
58+
59+
mach_vm_size_t size = 0;
60+
mach_msg_type_number_t count = sizeof(vm_region_basic_info_data_64_t);
61+
mach_vm_address_t address = (mach_vm_address_t)base;
62+
vm_region_basic_info_data_64_t region_info;
63+
mach_port_t object_name;
64+
65+
for (register int i = 0; cmd_cnt < 2 && i < ncmds; i++) {
66+
if (cmd->cmd == LC_SEGMENT_64 && strcmp(cmd->segname, "__DATA") == 0) {
67+
while (cmd->filesize != size) {
68+
address += size;
69+
if (mach_vm_region(
70+
proc_ref, &address, &size, VM_REGION_BASIC_INFO_64,
71+
(vm_region_info_t)&region_info, // cppcheck-suppress [uninitvar]
72+
&count, &object_name) != KERN_SUCCESS) {
73+
printf("Cannot get any more VM maps.\n");
74+
return 0;
75+
}
76+
}
77+
base = (void *)address - cmd->vmaddr;
78+
79+
int nsects = cmd->nsects;
80+
struct section_64 *sec =
81+
(struct section_64 *)((void *)cmd +
82+
sizeof(struct segment_command_64));
83+
for (register int j = 0; j < nsects; j++) {
84+
if (strcmp(sec[j].sectname, "PyRuntime") == 0) {
85+
return base + sec[j].addr;
86+
}
87+
}
88+
cmd_cnt++;
89+
}
90+
91+
cmd = (struct segment_command_64 *)((void *)cmd + cmd->cmdsize);
92+
}
93+
return 0;
94+
}
95+
96+
typedef struct {
97+
void *addr;
98+
size_t size;
99+
} map_t;
100+
101+
static inline map_t *map_new(int fd, size_t size, int flags) {
102+
void *addr = mmap(0, size, PROT_READ, flags, fd, 0);
103+
if (!(addr))
104+
return NULL;
105+
106+
map_t *map = malloc(sizeof(map_t));
107+
if (map == MAP_FAILED) {
108+
munmap(map, size);
109+
return NULL;
110+
}
111+
112+
map->size = size;
113+
map->addr = addr;
114+
115+
return map;
116+
}
117+
118+
static void* analyze_macho(char *path, void *base, mach_vm_size_t size,
119+
mach_port_t proc_ref) {
120+
int fd = open(path, O_RDONLY);
121+
if (fd == -1) {
122+
printf("Cannot open binary %s\n", path);
123+
return 0;
124+
}
125+
126+
// This would cause problem if allocated in the stack frame
127+
void *fs_buffer = malloc(sizeof(struct stat));
128+
struct stat *fs = (struct stat *)fs_buffer;
129+
map_t *map = NULL;
130+
if (fstat(fd, fs) == -1) {
131+
printf("Cannot get size of binary %s\n", path);
132+
return 0;
133+
}
134+
135+
map = map_new(fd, fs->st_size, MAP_SHARED);
136+
if (!map) {
137+
printf("Cannot map binary %s\n", path);
138+
return 0;
139+
}
140+
141+
void *map_addr = map->addr;
142+
143+
struct mach_header_64 *hdr = (struct mach_header_64 *)map_addr;
144+
switch (hdr->magic) {
145+
case MH_MAGIC:
146+
case MH_CIGAM:
147+
case FAT_MAGIC:
148+
case FAT_CIGAM:
149+
printf("Mach-O 32 or FAT binaries are not supported\n");
150+
return 0;
151+
case MH_MAGIC_64:
152+
case MH_CIGAM_64:
153+
return analyze_macho64(proc_ref, base, map_addr);
154+
default:
155+
printf("Unknown Mach-O magic\n");
156+
return 0;
157+
}
158+
159+
return 0;
160+
}
161+
162+
static mach_port_t pid_to_task(pid_t pid) {
163+
mach_port_t task;
164+
kern_return_t result;
165+
166+
result = task_for_pid(mach_task_self(), pid, &task);
167+
if (result != KERN_SUCCESS) {
168+
printf("Call to task_for_pid failed on PID %d: %s", pid, mach_error_string(result));
169+
return 0;
170+
}
171+
return task;
172+
}
173+
174+
static void* get_py_runtime(pid_t pid) {
175+
mach_vm_address_t address = 0;
176+
mach_vm_size_t size = 0;
177+
mach_msg_type_number_t count = sizeof(vm_region_basic_info_data_64_t);
178+
vm_region_basic_info_data_64_t region_info;
179+
mach_port_t object_name;
180+
181+
mach_port_t proc_ref = pid_to_task(pid);
182+
if (proc_ref == 0) {
183+
printf("Cannot get task for PID\n");
184+
return NULL;
185+
}
186+
187+
int match_found = 0;
188+
char map_filename[MAXPATHLEN + 1];
189+
void* result_address = NULL;
190+
while (mach_vm_region(
191+
proc_ref, &address, &size, VM_REGION_BASIC_INFO_64,
192+
(vm_region_info_t)&region_info, // cppcheck-suppress [uninitvar]
193+
&count, &object_name) == KERN_SUCCESS) {
194+
195+
int path_len = proc_regionfilename(pid, address, map_filename, MAXPATHLEN);
196+
if (path_len == 0) {
197+
address += size;
198+
continue;
199+
}
200+
201+
char *filename = strrchr(map_filename, '/');
202+
if (filename != NULL) {
203+
filename++; // Move past the '/'
204+
} else {
205+
filename = map_filename; // No path, use the whole string
206+
}
207+
208+
// Check if the filename starts with "python" or "libpython"
209+
if (!match_found && strncmp(filename, "python", 6) == 0) {
210+
match_found = 1;
211+
result_address = analyze_macho(map_filename, (void *)address, size, proc_ref);
212+
}
213+
if (strncmp(filename, "libpython", 9) == 0) {
214+
match_found = 1;
215+
result_address = analyze_macho(map_filename, (void *)address, size, proc_ref);
216+
break;
217+
}
218+
219+
address += size;
220+
}
221+
return result_address;
222+
}
223+
#endif
32224

33225

34-
unsigned long
35-
get_py_runtime(char* elf_file) {
36226
#ifdef __linux__
227+
void*
228+
get_py_runtime(pid_t pid) {
229+
230+
char elf_file[256];
231+
unsigned long start_address = find_python_map_start_address(pid, elf_file);
232+
233+
if (start_address == 0) {
234+
PyErr_SetString(PyExc_RuntimeError, "No memory map associated with python or libpython found");
235+
return NULL;
236+
}
237+
37238
unsigned long result = 0;
38239

39240
int fd = open(elf_file, O_RDONLY);
@@ -70,15 +271,12 @@ get_py_runtime(char* elf_file) {
70271
}
71272

72273
if (py_runtime_section != NULL) {
73-
result = (unsigned long)py_runtime_section->sh_addr;
274+
result = start_address + (unsigned long)py_runtime_section->sh_addr;
74275
}
75276

76277
close(fd);
77278
munmap(file_memory, file_stats.st_size);
78279
return result;
79-
#else
80-
return 0;
81-
#endif
82280
}
83281

84282
unsigned long
@@ -129,15 +327,13 @@ find_python_map_start_address(pid_t pid, char* result_filename) {
129327

130328
return result_address;
131329
}
330+
#endif
132331

133332
ssize_t
134333
read_memory(pid_t pid, void* remote_address, ssize_t size, void* local_address) {
135-
#ifndef HAVE_PROCESS_VM_READV
136-
return -1
137-
#else
138334
ssize_t total_bytes_read = 0;
335+
#ifdef __linux__
139336
ssize_t bytes_read;
140-
141337
while (total_bytes_read < size) {
142338
struct iovec local_iov = {(char*)local_address + total_bytes_read, size - total_bytes_read};
143339
struct iovec remote_iov = {(char*)remote_address + total_bytes_read, size - total_bytes_read};
@@ -154,9 +350,23 @@ read_memory(pid_t pid, void* remote_address, ssize_t size, void* local_address)
154350
break;
155351
}
156352
}
157-
158-
return total_bytes_read;
353+
#elif defined(__APPLE__)
354+
ssize_t result = -1;
355+
kern_return_t kr = mach_vm_read_overwrite(
356+
pid_to_task(pid),
357+
(mach_vm_address_t)remote_address,
358+
size,
359+
(mach_vm_address_t)local_address,
360+
(mach_vm_size_t *)&result);
361+
362+
if (kr != KERN_SUCCESS) {
363+
return -1;
364+
}
365+
total_bytes_read = size;
366+
#else
367+
return -1;
159368
#endif
369+
return total_bytes_read;
160370
}
161371

162372
int
@@ -191,26 +401,16 @@ get_stack_trace(PyObject* self, PyObject* args) {
191401
return NULL;
192402
}
193403

194-
char map_filename[256];
195-
unsigned long start_address = find_python_map_start_address(pid, map_filename);
196-
197-
if (start_address == 0) {
198-
PyErr_SetString(PyExc_RuntimeError, "No memory map associated with python or libpython found");
199-
return NULL;
200-
}
201-
202-
unsigned long py_runtime_address = get_py_runtime(map_filename);
203-
if (py_runtime_address == 0) {
404+
void* runtime_start_address = get_py_runtime(pid);
405+
if (runtime_start_address == NULL) {
204406
PyErr_SetString(PyExc_RuntimeError, "Failed to get .PyRuntime address");
205407
return NULL;
206408
}
207409

208-
void* runtime_state_address = (void*)start_address + py_runtime_address;
209-
210410
size_t size = sizeof(struct _Py_DebugOffsets);
211411
struct _Py_DebugOffsets local_debug_offsets;
212412

213-
ssize_t bytes_read = read_memory(pid, runtime_state_address, size, &local_debug_offsets);
413+
ssize_t bytes_read = read_memory(pid, runtime_start_address, size, &local_debug_offsets);
214414
if (bytes_read == -1) {
215415
return NULL;
216416
}
@@ -220,7 +420,7 @@ get_stack_trace(PyObject* self, PyObject* args) {
220420
void* address_of_interpreter_state;
221421
bytes_read = read_memory(
222422
pid,
223-
(void*)(runtime_state_address + thread_state_list_head),
423+
(void*)(runtime_start_address + thread_state_list_head),
224424
sizeof(void*),
225425
&address_of_interpreter_state);
226426
if (bytes_read == -1) {

Tools/build/generate_stdlib_module_names.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
'_testinternalcapi',
3535
'_testmultiphase',
3636
'_testsinglephase',
37+
'_testexternalinspection',
3738
'_xxsubinterpreters',
3839
'_xxinterpchannels',
3940
'_xxinterpqueues',

configure

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

configure.ac

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7457,7 +7457,7 @@ PY_STDLIB_MOD([_testinternalcapi], [test "$TEST_MODULES" = yes])
74577457
PY_STDLIB_MOD([_testbuffer], [test "$TEST_MODULES" = yes])
74587458
PY_STDLIB_MOD([_testimportmultiple], [test "$TEST_MODULES" = yes], [test "$ac_cv_func_dlopen" = yes])
74597459
PY_STDLIB_MOD([_testmultiphase], [test "$TEST_MODULES" = yes], [test "$ac_cv_func_dlopen" = yes])
7460-
PY_STDLIB_MOD([_testexternalinspection], [test "$TEST_MODULES" = yes], [test "$ac_cv_func_process_vm_readv" = yes])
7460+
PY_STDLIB_MOD([_testexternalinspection], [test "$TEST_MODULES" = yes])
74617461
PY_STDLIB_MOD([xxsubtype], [test "$TEST_MODULES" = yes])
74627462
PY_STDLIB_MOD([_xxtestfuzz], [test "$TEST_MODULES" = yes])
74637463
PY_STDLIB_MOD([_ctypes_test],

0 commit comments

Comments
 (0)