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