gh-91048: Add better error messages for remote debugging for CI builds (#134682)

This commit is contained in:
Pablo Galindo Salgado 2025-05-26 15:31:47 +01:00 committed by GitHub
parent 965662ee4a
commit 0909d6d8e8
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 540 additions and 129 deletions

View file

@ -892,6 +892,7 @@ _PyStaticObjects_CheckRefcnt(PyInterpreterState *interp) {
_PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(data));
_PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(database));
_PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(day));
_PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(debug));
_PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(decode));
_PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(decoder));
_PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(default));

View file

@ -383,6 +383,7 @@ struct _Py_global_strings {
STRUCT_FOR_ID(data)
STRUCT_FOR_ID(database)
STRUCT_FOR_ID(day)
STRUCT_FOR_ID(debug)
STRUCT_FOR_ID(decode)
STRUCT_FOR_ID(decoder)
STRUCT_FOR_ID(default)

View file

@ -890,6 +890,7 @@ extern "C" {
INIT_ID(data), \
INIT_ID(database), \
INIT_ID(day), \
INIT_ID(debug), \
INIT_ID(decode), \
INIT_ID(decoder), \
INIT_ID(default), \

View file

@ -1320,6 +1320,10 @@ _PyUnicode_InitStaticStrings(PyInterpreterState *interp) {
_PyUnicode_InternStatic(interp, &string);
assert(_PyUnicode_CheckConsistency(string, 1));
assert(PyUnicode_GET_LENGTH(string) != 1);
string = &_Py_ID(debug);
_PyUnicode_InternStatic(interp, &string);
assert(_PyUnicode_CheckConsistency(string, 1));
assert(PyUnicode_GET_LENGTH(string) != 1);
string = &_Py_ID(decode);
_PyUnicode_InternStatic(interp, &string);
assert(_PyUnicode_CheckConsistency(string, 1));

View file

@ -34,17 +34,17 @@ skip_if_not_supported = unittest.skipIf(
def get_stack_trace(pid):
unwinder = RemoteUnwinder(pid, all_threads=True)
unwinder = RemoteUnwinder(pid, all_threads=True, debug=True)
return unwinder.get_stack_trace()
def get_async_stack_trace(pid):
unwinder = RemoteUnwinder(pid)
unwinder = RemoteUnwinder(pid, debug=True)
return unwinder.get_async_stack_trace()
def get_all_awaited_by(pid):
unwinder = RemoteUnwinder(pid)
unwinder = RemoteUnwinder(pid, debug=True)
return unwinder.get_all_awaited_by()

File diff suppressed because it is too large Load diff

View file

@ -10,7 +10,7 @@ preserve
#include "pycore_modsupport.h" // _PyArg_UnpackKeywords()
PyDoc_STRVAR(_remote_debugging_RemoteUnwinder___init____doc__,
"RemoteUnwinder(pid, *, all_threads=False)\n"
"RemoteUnwinder(pid, *, all_threads=False, debug=False)\n"
"--\n"
"\n"
"Initialize a new RemoteUnwinder object for debugging a remote Python process.\n"
@ -19,6 +19,8 @@ PyDoc_STRVAR(_remote_debugging_RemoteUnwinder___init____doc__,
" pid: Process ID of the target Python process to debug\n"
" all_threads: If True, initialize state for all threads in the process.\n"
" If False, only initialize for the main thread.\n"
" debug: If True, chain exceptions to explain the sequence of events that\n"
" lead to the exception.\n"
"\n"
"The RemoteUnwinder provides functionality to inspect and debug a running Python\n"
"process, including examining thread states, stack frames and other runtime data.\n"
@ -30,7 +32,8 @@ PyDoc_STRVAR(_remote_debugging_RemoteUnwinder___init____doc__,
static int
_remote_debugging_RemoteUnwinder___init___impl(RemoteUnwinderObject *self,
int pid, int all_threads);
int pid, int all_threads,
int debug);
static int
_remote_debugging_RemoteUnwinder___init__(PyObject *self, PyObject *args, PyObject *kwargs)
@ -38,7 +41,7 @@ _remote_debugging_RemoteUnwinder___init__(PyObject *self, PyObject *args, PyObje
int return_value = -1;
#if defined(Py_BUILD_CORE) && !defined(Py_BUILD_CORE_MODULE)
#define NUM_KEYWORDS 2
#define NUM_KEYWORDS 3
static struct {
PyGC_Head _this_is_not_used;
PyObject_VAR_HEAD
@ -47,7 +50,7 @@ _remote_debugging_RemoteUnwinder___init__(PyObject *self, PyObject *args, PyObje
} _kwtuple = {
.ob_base = PyVarObject_HEAD_INIT(&PyTuple_Type, NUM_KEYWORDS)
.ob_hash = -1,
.ob_item = { &_Py_ID(pid), &_Py_ID(all_threads), },
.ob_item = { &_Py_ID(pid), &_Py_ID(all_threads), &_Py_ID(debug), },
};
#undef NUM_KEYWORDS
#define KWTUPLE (&_kwtuple.ob_base.ob_base)
@ -56,19 +59,20 @@ _remote_debugging_RemoteUnwinder___init__(PyObject *self, PyObject *args, PyObje
# define KWTUPLE NULL
#endif // !Py_BUILD_CORE
static const char * const _keywords[] = {"pid", "all_threads", NULL};
static const char * const _keywords[] = {"pid", "all_threads", "debug", NULL};
static _PyArg_Parser _parser = {
.keywords = _keywords,
.fname = "RemoteUnwinder",
.kwtuple = KWTUPLE,
};
#undef KWTUPLE
PyObject *argsbuf[2];
PyObject *argsbuf[3];
PyObject * const *fastargs;
Py_ssize_t nargs = PyTuple_GET_SIZE(args);
Py_ssize_t noptargs = nargs + (kwargs ? PyDict_GET_SIZE(kwargs) : 0) - 1;
int pid;
int all_threads = 0;
int debug = 0;
fastargs = _PyArg_UnpackKeywords(_PyTuple_CAST(args)->ob_item, nargs, kwargs, NULL, &_parser,
/*minpos*/ 1, /*maxpos*/ 1, /*minkw*/ 0, /*varpos*/ 0, argsbuf);
@ -82,12 +86,21 @@ _remote_debugging_RemoteUnwinder___init__(PyObject *self, PyObject *args, PyObje
if (!noptargs) {
goto skip_optional_kwonly;
}
all_threads = PyObject_IsTrue(fastargs[1]);
if (all_threads < 0) {
if (fastargs[1]) {
all_threads = PyObject_IsTrue(fastargs[1]);
if (all_threads < 0) {
goto exit;
}
if (!--noptargs) {
goto skip_optional_kwonly;
}
}
debug = PyObject_IsTrue(fastargs[2]);
if (debug < 0) {
goto exit;
}
skip_optional_kwonly:
return_value = _remote_debugging_RemoteUnwinder___init___impl((RemoteUnwinderObject *)self, pid, all_threads);
return_value = _remote_debugging_RemoteUnwinder___init___impl((RemoteUnwinderObject *)self, pid, all_threads, debug);
exit:
return return_value;
@ -240,4 +253,4 @@ _remote_debugging_RemoteUnwinder_get_async_stack_trace(PyObject *self, PyObject
return return_value;
}
/*[clinic end generated code: output=654772085f1f4bf6 input=a9049054013a1b77]*/
/*[clinic end generated code: output=774ec34aa653402d input=a9049054013a1b77]*/

View file

@ -73,6 +73,18 @@ extern "C" {
# define HAVE_PROCESS_VM_READV 0
#endif
#define _set_debug_exception_cause(exception, format, ...) \
do { \
if (!PyErr_ExceptionMatches(PyExc_PermissionError)) { \
PyThreadState *tstate = _PyThreadState_GET(); \
if (!_PyErr_Occurred(tstate)) { \
_PyErr_Format(tstate, exception, format, ##__VA_ARGS__); \
} else { \
_PyErr_FormatFromCause(exception, format, ##__VA_ARGS__); \
} \
} \
} while (0)
static inline size_t
get_page_size(void) {
size_t page_size = 0;
@ -137,12 +149,17 @@ _Py_RemoteDebug_InitProcHandle(proc_handle_t *handle, pid_t pid) {
handle->pid = pid;
#if defined(__APPLE__) && defined(TARGET_OS_OSX) && TARGET_OS_OSX
handle->task = pid_to_task(handle->pid);
if (handle->task == 0) {
_set_debug_exception_cause(PyExc_RuntimeError, "Failed to initialize macOS process handle");
return -1;
}
#elif defined(MS_WINDOWS)
handle->hProcess = OpenProcess(
PROCESS_VM_READ | PROCESS_VM_WRITE | PROCESS_VM_OPERATION | PROCESS_QUERY_INFORMATION,
FALSE, pid);
if (handle->hProcess == NULL) {
PyErr_SetFromWindowsErr(0);
_set_debug_exception_cause(PyExc_RuntimeError, "Failed to initialize Windows process handle");
return -1;
}
#endif
@ -206,8 +223,10 @@ return_section_address64(
&object_name
);
if (ret != KERN_SUCCESS) {
PyErr_SetString(
PyExc_RuntimeError, "Cannot get any more VM maps.\n");
PyErr_Format(PyExc_RuntimeError,
"mach_vm_region failed while parsing 64-bit Mach-O binary "
"at base address 0x%lx (kern_return_t: %d)",
base, ret);
return 0;
}
}
@ -227,9 +246,6 @@ return_section_address64(
cmd = (struct segment_command_64*)((void*)cmd + cmd->cmdsize);
}
// We should not be here, but if we are there, we should say about this
PyErr_SetString(
PyExc_RuntimeError, "Cannot find section address.\n");
return 0;
}
@ -270,8 +286,10 @@ return_section_address32(
&object_name
);
if (ret != KERN_SUCCESS) {
PyErr_SetString(
PyExc_RuntimeError, "Cannot get any more VM maps.\n");
PyErr_Format(PyExc_RuntimeError,
"mach_vm_region failed while parsing 32-bit Mach-O binary "
"at base address 0x%lx (kern_return_t: %d)",
base, ret);
return 0;
}
}
@ -291,9 +309,6 @@ return_section_address32(
cmd = (struct segment_command*)((void*)cmd + cmd->cmdsize);
}
// We should not be here, but if we are there, we should say about this
PyErr_SetString(
PyExc_RuntimeError, "Cannot find section address.\n");
return 0;
}
@ -311,8 +326,20 @@ return_section_address_fat(
int is_abi64;
size_t cpu_size = sizeof(cpu), abi64_size = sizeof(is_abi64);
sysctlbyname("hw.cputype", &cpu, &cpu_size, NULL, 0);
sysctlbyname("hw.cpu64bit_capable", &is_abi64, &abi64_size, NULL, 0);
if (sysctlbyname("hw.cputype", &cpu, &cpu_size, NULL, 0) != 0) {
PyErr_Format(PyExc_OSError,
"Failed to determine CPU type via sysctlbyname "
"for fat binary analysis at 0x%lx: %s",
base, strerror(errno));
return 0;
}
if (sysctlbyname("hw.cpu64bit_capable", &is_abi64, &abi64_size, NULL, 0) != 0) {
PyErr_Format(PyExc_OSError,
"Failed to determine CPU ABI capability via sysctlbyname "
"for fat binary analysis at 0x%lx: %s",
base, strerror(errno));
return 0;
}
cpu |= is_abi64 * CPU_ARCH_ABI64;
@ -343,13 +370,18 @@ return_section_address_fat(
return return_section_address64(section, proc_ref, base, (void*)hdr);
default:
PyErr_SetString(PyExc_RuntimeError, "Unknown Mach-O magic in fat binary.\n");
PyErr_Format(PyExc_RuntimeError,
"Unknown Mach-O magic number 0x%x in fat binary architecture %u at base 0x%lx",
hdr->magic, i, base);
return 0;
}
}
}
PyErr_SetString(PyExc_RuntimeError, "No matching architecture found in fat binary.\n");
PyErr_Format(PyExc_RuntimeError,
"No matching architecture found for CPU type 0x%x "
"in fat binary at base 0x%lx (%u architectures examined)",
cpu, base, nfat_arch);
return 0;
}
@ -358,20 +390,26 @@ search_section_in_file(const char* secname, char* path, uintptr_t base, mach_vm_
{
int fd = open(path, O_RDONLY);
if (fd == -1) {
PyErr_Format(PyExc_RuntimeError, "Cannot open binary %s\n", path);
PyErr_Format(PyExc_OSError,
"Cannot open binary file '%s' for section '%s' search: %s",
path, secname, strerror(errno));
return 0;
}
struct stat fs;
if (fstat(fd, &fs) == -1) {
PyErr_Format(PyExc_RuntimeError, "Cannot get size of binary %s\n", path);
PyErr_Format(PyExc_OSError,
"Cannot get file size for binary '%s' during section '%s' search: %s",
path, secname, strerror(errno));
close(fd);
return 0;
}
void* map = mmap(0, fs.st_size, PROT_READ, MAP_SHARED, fd, 0);
if (map == MAP_FAILED) {
PyErr_Format(PyExc_RuntimeError, "Cannot map binary %s\n", path);
PyErr_Format(PyExc_OSError,
"Cannot memory map binary file '%s' (size: %lld bytes) for section '%s' search: %s",
path, (long long)fs.st_size, secname, strerror(errno));
close(fd);
return 0;
}
@ -393,13 +431,22 @@ search_section_in_file(const char* secname, char* path, uintptr_t base, mach_vm_
result = return_section_address_fat(secname, proc_ref, base, map);
break;
default:
PyErr_SetString(PyExc_RuntimeError, "Unknown Mach-O magic");
PyErr_Format(PyExc_RuntimeError,
"Unrecognized Mach-O magic number 0x%x in binary file '%s' for section '%s' search",
magic, path, secname);
break;
}
munmap(map, fs.st_size);
if (munmap(map, fs.st_size) != 0) {
PyErr_Format(PyExc_OSError,
"Failed to unmap binary file '%s' (size: %lld bytes): %s",
path, (long long)fs.st_size, strerror(errno));
result = 0;
}
if (close(fd) != 0) {
PyErr_SetFromErrno(PyExc_OSError);
PyErr_Format(PyExc_OSError,
"Failed to close binary file '%s': %s",
path, strerror(errno));
result = 0;
}
return result;
@ -414,7 +461,10 @@ pid_to_task(pid_t pid)
result = task_for_pid(mach_task_self(), pid, &task);
if (result != KERN_SUCCESS) {
PyErr_Format(PyExc_PermissionError, "Cannot get task for PID %d", pid);
PyErr_Format(PyExc_PermissionError,
"Cannot get task port for PID %d (kern_return_t: %d). "
"This typically requires running as root or having the 'com.apple.system-task-ports' entitlement.",
pid, result);
return 0;
}
return task;
@ -431,13 +481,15 @@ search_map_for_section(proc_handle_t *handle, const char* secname, const char* s
mach_port_t proc_ref = pid_to_task(handle->pid);
if (proc_ref == 0) {
if (!PyErr_Occurred()) {
PyErr_SetString(PyExc_PermissionError, "Cannot get task for PID");
PyErr_Format(PyExc_PermissionError,
"Cannot get task port for PID %d during section search",
handle->pid);
}
return 0;
}
int match_found = 0;
char map_filename[MAXPATHLEN + 1];
while (mach_vm_region(
proc_ref,
&address,
@ -447,6 +499,7 @@ search_map_for_section(proc_handle_t *handle, const char* secname, const char* s
&count,
&object_name) == KERN_SUCCESS)
{
if ((region_info.protection & VM_PROT_READ) == 0
|| (region_info.protection & VM_PROT_EXECUTE) == 0) {
address += size;
@ -467,17 +520,17 @@ search_map_for_section(proc_handle_t *handle, const char* secname, const char* s
filename = map_filename; // No path, use the whole string
}
if (!match_found && strncmp(filename, substr, strlen(substr)) == 0) {
match_found = 1;
return search_section_in_file(
if (strncmp(filename, substr, strlen(substr)) == 0) {
uintptr_t result = search_section_in_file(
secname, map_filename, address, size, proc_ref);
if (result != 0) {
return result;
}
}
address += size;
}
PyErr_SetString(PyExc_RuntimeError,
"mach_vm_region failed to find the section");
return 0;
}
@ -500,24 +553,38 @@ search_elf_file_for_section(
int fd = open(elf_file, O_RDONLY);
if (fd < 0) {
PyErr_SetFromErrno(PyExc_OSError);
PyErr_Format(PyExc_OSError,
"Cannot open ELF file '%s' for section '%s' search: %s",
elf_file, secname, strerror(errno));
goto exit;
}
struct stat file_stats;
if (fstat(fd, &file_stats) != 0) {
PyErr_SetFromErrno(PyExc_OSError);
PyErr_Format(PyExc_OSError,
"Cannot get file size for ELF file '%s' during section '%s' search: %s",
elf_file, secname, strerror(errno));
goto exit;
}
file_memory = mmap(NULL, file_stats.st_size, PROT_READ, MAP_PRIVATE, fd, 0);
if (file_memory == MAP_FAILED) {
PyErr_SetFromErrno(PyExc_OSError);
PyErr_Format(PyExc_OSError,
"Cannot memory map ELF file '%s' (size: %lld bytes) for section '%s' search: %s",
elf_file, (long long)file_stats.st_size, secname, strerror(errno));
goto exit;
}
Elf_Ehdr* elf_header = (Elf_Ehdr*)file_memory;
// Validate ELF header
if (elf_header->e_shstrndx >= elf_header->e_shnum) {
PyErr_Format(PyExc_RuntimeError,
"Invalid ELF file '%s': string table index %u >= section count %u",
elf_file, elf_header->e_shstrndx, elf_header->e_shnum);
goto exit;
}
Elf_Shdr* section_header_table = (Elf_Shdr*)(file_memory + elf_header->e_shoff);
Elf_Shdr* shstrtab_section = &section_header_table[elf_header->e_shstrndx];
@ -534,6 +601,10 @@ search_elf_file_for_section(
}
}
if (section == NULL) {
goto exit;
}
Elf_Phdr* program_header_table = (Elf_Phdr*)(file_memory + elf_header->e_phoff);
// Find the first PT_LOAD segment
Elf_Phdr* first_load_segment = NULL;
@ -544,18 +615,25 @@ search_elf_file_for_section(
}
}
if (section != NULL && first_load_segment != NULL) {
uintptr_t elf_load_addr = first_load_segment->p_vaddr
- (first_load_segment->p_vaddr % first_load_segment->p_align);
result = start_address + (uintptr_t)section->sh_addr - elf_load_addr;
if (first_load_segment == NULL) {
PyErr_Format(PyExc_RuntimeError,
"No PT_LOAD segment found in ELF file '%s' (%u program headers examined)",
elf_file, elf_header->e_phnum);
goto exit;
}
uintptr_t elf_load_addr = first_load_segment->p_vaddr
- (first_load_segment->p_vaddr % first_load_segment->p_align);
result = start_address + (uintptr_t)section->sh_addr - elf_load_addr;
exit:
if (file_memory != NULL) {
munmap(file_memory, file_stats.st_size);
}
if (fd >= 0 && close(fd) != 0) {
PyErr_SetFromErrno(PyExc_OSError);
PyErr_Format(PyExc_OSError,
"Failed to close ELF file '%s': %s",
elf_file, strerror(errno));
result = 0;
}
return result;
@ -569,7 +647,9 @@ search_linux_map_for_section(proc_handle_t *handle, const char* secname, const c
FILE* maps_file = fopen(maps_file_path, "r");
if (maps_file == NULL) {
PyErr_SetFromErrno(PyExc_OSError);
PyErr_Format(PyExc_OSError,
"Cannot open process memory map file '%s' for PID %d section search: %s",
maps_file_path, handle->pid, strerror(errno));
return 0;
}
@ -578,11 +658,16 @@ search_linux_map_for_section(proc_handle_t *handle, const char* secname, const c
char *line = PyMem_Malloc(linesz);
if (!line) {
fclose(maps_file);
PyErr_NoMemory();
_set_debug_exception_cause(PyExc_MemoryError,
"Cannot allocate memory for reading process map file '%s'",
maps_file_path);
return 0;
}
uintptr_t retval = 0;
int lines_processed = 0;
int matches_found = 0;
while (fgets(line + linelen, linesz - linelen, maps_file) != NULL) {
linelen = strlen(line);
if (line[linelen - 1] != '\n') {
@ -593,7 +678,9 @@ search_linux_map_for_section(proc_handle_t *handle, const char* secname, const c
if (!biggerline) {
PyMem_Free(line);
fclose(maps_file);
PyErr_NoMemory();
_set_debug_exception_cause(PyExc_MemoryError,
"Cannot reallocate memory while reading process map file '%s' (attempted size: %zu)",
maps_file_path, linesz);
return 0;
}
line = biggerline;
@ -604,6 +691,7 @@ search_linux_map_for_section(proc_handle_t *handle, const char* secname, const c
line[linelen - 1] = '\0';
// and prepare to read the next line into the start of the buffer.
linelen = 0;
lines_processed++;
unsigned long start = 0;
unsigned long path_pos = 0;
@ -624,6 +712,7 @@ search_linux_map_for_section(proc_handle_t *handle, const char* secname, const c
}
if (strstr(filename, substr)) {
matches_found++;
retval = search_elf_file_for_section(handle, secname, start, path);
if (retval) {
break;
@ -633,7 +722,9 @@ search_linux_map_for_section(proc_handle_t *handle, const char* secname, const c
PyMem_Free(line);
if (fclose(maps_file) != 0) {
PyErr_SetFromErrno(PyExc_OSError);
PyErr_Format(PyExc_OSError,
"Failed to close process map file '%s': %s",
maps_file_path, strerror(errno));
retval = 0;
}
@ -649,11 +740,20 @@ static void* analyze_pe(const wchar_t* mod_path, BYTE* remote_base, const char*
HANDLE hFile = CreateFileW(mod_path, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
if (hFile == INVALID_HANDLE_VALUE) {
PyErr_SetFromWindowsErr(0);
DWORD error = GetLastError();
PyErr_Format(PyExc_OSError,
"Cannot open PE file for section '%s' analysis (error %lu)",
secname, error);
return NULL;
}
HANDLE hMap = CreateFileMapping(hFile, NULL, PAGE_READONLY, 0, 0, 0);
if (!hMap) {
PyErr_SetFromWindowsErr(0);
DWORD error = GetLastError();
PyErr_Format(PyExc_OSError,
"Cannot create file mapping for PE file section '%s' analysis (error %lu)",
secname, error);
CloseHandle(hFile);
return NULL;
}
@ -661,6 +761,10 @@ static void* analyze_pe(const wchar_t* mod_path, BYTE* remote_base, const char*
BYTE* mapView = (BYTE*)MapViewOfFile(hMap, FILE_MAP_READ, 0, 0, 0);
if (!mapView) {
PyErr_SetFromWindowsErr(0);
DWORD error = GetLastError();
PyErr_Format(PyExc_OSError,
"Cannot map view of PE file for section '%s' analysis (error %lu)",
secname, error);
CloseHandle(hMap);
CloseHandle(hFile);
return NULL;
@ -668,7 +772,9 @@ static void* analyze_pe(const wchar_t* mod_path, BYTE* remote_base, const char*
IMAGE_DOS_HEADER* pDOSHeader = (IMAGE_DOS_HEADER*)mapView;
if (pDOSHeader->e_magic != IMAGE_DOS_SIGNATURE) {
PyErr_SetString(PyExc_RuntimeError, "Invalid DOS signature.");
PyErr_Format(PyExc_RuntimeError,
"Invalid DOS signature (0x%x) in PE file for section '%s' analysis (expected 0x%x)",
pDOSHeader->e_magic, secname, IMAGE_DOS_SIGNATURE);
UnmapViewOfFile(mapView);
CloseHandle(hMap);
CloseHandle(hFile);
@ -677,7 +783,9 @@ static void* analyze_pe(const wchar_t* mod_path, BYTE* remote_base, const char*
IMAGE_NT_HEADERS* pNTHeaders = (IMAGE_NT_HEADERS*)(mapView + pDOSHeader->e_lfanew);
if (pNTHeaders->Signature != IMAGE_NT_SIGNATURE) {
PyErr_SetString(PyExc_RuntimeError, "Invalid NT signature.");
PyErr_Format(PyExc_RuntimeError,
"Invalid NT signature (0x%lx) in PE file for section '%s' analysis (expected 0x%lx)",
pNTHeaders->Signature, secname, IMAGE_NT_SIGNATURE);
UnmapViewOfFile(mapView);
CloseHandle(hMap);
CloseHandle(hFile);
@ -711,17 +819,27 @@ search_windows_map_for_section(proc_handle_t* handle, const char* secname, const
} while (hProcSnap == INVALID_HANDLE_VALUE && GetLastError() == ERROR_BAD_LENGTH);
if (hProcSnap == INVALID_HANDLE_VALUE) {
PyErr_SetString(PyExc_PermissionError, "Unable to create module snapshot. Check permissions or PID.");
PyErr_SetFromWindowsErr(0);
DWORD error = GetLastError();
PyErr_Format(PyExc_PermissionError,
"Unable to create module snapshot for PID %d section '%s' "
"search (error %lu). Check permissions or PID validity",
handle->pid, secname, error);
return 0;
}
MODULEENTRY32W moduleEntry;
moduleEntry.dwSize = sizeof(moduleEntry);
void* runtime_addr = NULL;
int modules_examined = 0;
int matches_found = 0;
for (BOOL hasModule = Module32FirstW(hProcSnap, &moduleEntry); hasModule; hasModule = Module32NextW(hProcSnap, &moduleEntry)) {
modules_examined++;
// Look for either python executable or DLL
if (wcsstr(moduleEntry.szModule, substr)) {
matches_found++;
runtime_addr = analyze_pe(moduleEntry.szExePath, moduleEntry.modBaseAddr, secname);
if (runtime_addr != NULL) {
break;
@ -730,6 +848,7 @@ search_windows_map_for_section(proc_handle_t* handle, const char* secname, const
}
CloseHandle(hProcSnap);
return (uintptr_t)runtime_addr;
}
@ -747,7 +866,9 @@ _Py_RemoteDebug_GetPyRuntimeAddress(proc_handle_t* handle)
if (address == 0) {
// Error out: 'python' substring covers both executable and DLL
PyObject *exc = PyErr_GetRaisedException();
PyErr_SetString(PyExc_RuntimeError, "Failed to find the PyRuntime section in the process.");
PyErr_Format(PyExc_RuntimeError,
"Failed to find the PyRuntime section in process %d on Windows platform",
handle->pid);
_PyErr_ChainExceptions1(exc);
}
#elif defined(__linux__)
@ -756,16 +877,28 @@ _Py_RemoteDebug_GetPyRuntimeAddress(proc_handle_t* handle)
if (address == 0) {
// Error out: 'python' substring covers both executable and DLL
PyObject *exc = PyErr_GetRaisedException();
PyErr_SetString(PyExc_RuntimeError, "Failed to find the PyRuntime section in the process.");
PyErr_Format(PyExc_RuntimeError,
"Failed to find the PyRuntime section in process %d on Linux platform",
handle->pid);
_PyErr_ChainExceptions1(exc);
}
#elif defined(__APPLE__) && defined(TARGET_OS_OSX) && TARGET_OS_OSX
// On macOS, try libpython first, then fall back to python
address = search_map_for_section(handle, "PyRuntime", "libpython");
if (address == 0) {
// TODO: Differentiate between not found and error
const char* candidates[] = {"libpython", "python", "Python", NULL};
for (const char** candidate = candidates; *candidate; candidate++) {
PyErr_Clear();
address = search_map_for_section(handle, "PyRuntime", "python");
address = search_map_for_section(handle, "PyRuntime", *candidate);
if (address != 0) {
break;
}
}
if (address == 0) {
PyObject *exc = PyErr_GetRaisedException();
PyErr_Format(PyExc_RuntimeError,
"Failed to find the PyRuntime section in process %d "
"on macOS platform (tried both libpython and python)",
handle->pid);
_PyErr_ChainExceptions1(exc);
}
#else
Py_UNREACHABLE();
@ -784,6 +917,11 @@ _Py_RemoteDebug_ReadRemoteMemory(proc_handle_t *handle, uintptr_t remote_address
do {
if (!ReadProcessMemory(handle->hProcess, (LPCVOID)(remote_address + result), (char*)dst + result, len - result, &read_bytes)) {
PyErr_SetFromWindowsErr(0);
DWORD error = GetLastError();
_set_debug_exception_cause(PyExc_OSError,
"ReadProcessMemory failed for PID %d at address 0x%lx "
"(size %zu, partial read %zu bytes): Windows error %lu",
handle->pid, remote_address + result, len - result, result, error);
return -1;
}
result += read_bytes;
@ -804,6 +942,10 @@ _Py_RemoteDebug_ReadRemoteMemory(proc_handle_t *handle, uintptr_t remote_address
read_bytes = process_vm_readv(handle->pid, local, 1, remote, 1, 0);
if (read_bytes < 0) {
PyErr_SetFromErrno(PyExc_OSError);
_set_debug_exception_cause(PyExc_OSError,
"process_vm_readv failed for PID %d at address 0x%lx "
"(size %zu, partial read %zd bytes): %s",
handle->pid, remote_address + result, len - result, result, strerror(errno));
return -1;
}
@ -822,13 +964,22 @@ _Py_RemoteDebug_ReadRemoteMemory(proc_handle_t *handle, uintptr_t remote_address
if (kr != KERN_SUCCESS) {
switch (kr) {
case KERN_PROTECTION_FAILURE:
PyErr_SetString(PyExc_PermissionError, "Not enough permissions to read memory");
PyErr_Format(PyExc_PermissionError,
"Memory protection failure reading from PID %d at address "
"0x%lx (size %zu): insufficient permissions",
handle->pid, remote_address, len);
break;
case KERN_INVALID_ARGUMENT:
PyErr_SetString(PyExc_PermissionError, "Invalid argument to mach_vm_read_overwrite");
PyErr_Format(PyExc_ValueError,
"Invalid argument to mach_vm_read_overwrite for PID %d at "
"address 0x%lx (size %zu)",
handle->pid, remote_address, len);
break;
default:
PyErr_SetString(PyExc_RuntimeError, "Unknown error reading memory");
PyErr_Format(PyExc_RuntimeError,
"mach_vm_read_overwrite failed for PID %d at address 0x%lx "
"(size %zu): kern_return_t %d",
handle->pid, remote_address, len, kr);
}
return -1;
}
@ -868,7 +1019,10 @@ _Py_RemoteDebug_PagedReadRemoteMemory(proc_handle_t *handle,
if (entry->data == NULL) {
entry->data = PyMem_RawMalloc(page_size);
if (entry->data == NULL) {
PyErr_NoMemory();
_set_debug_exception_cause(PyExc_MemoryError,
"Cannot allocate %zu bytes for page cache entry "
"during read from PID %d at address 0x%lx",
page_size, handle->pid, addr);
return -1;
}
}
@ -900,13 +1054,16 @@ _Py_RemoteDebug_ReadDebugOffsets(
*runtime_start_address = _Py_RemoteDebug_GetPyRuntimeAddress(handle);
if (!*runtime_start_address) {
if (!PyErr_Occurred()) {
PyErr_SetString(
PyExc_RuntimeError, "Failed to get PyRuntime address");
PyErr_Format(PyExc_RuntimeError,
"Failed to locate PyRuntime address for PID %d",
handle->pid);
}
_set_debug_exception_cause(PyExc_RuntimeError, "PyRuntime address lookup failed during debug offsets initialization");
return -1;
}
size_t size = sizeof(struct _Py_DebugOffsets);
if (0 != _Py_RemoteDebug_ReadRemoteMemory(handle, *runtime_start_address, size, debug_offsets)) {
_set_debug_exception_cause(PyExc_RuntimeError, "Failed to read debug offsets structure from remote process");
return -1;
}
return 0;