| /****************************************************************************** |
| * Python Remote Debugging Module |
| * |
| * This module provides functionality to debug Python processes remotely by |
| * reading their memory and reconstructing stack traces and asyncio task states. |
| ******************************************************************************/ |
| |
| #define _GNU_SOURCE |
| |
| /* ============================================================================ |
| * HEADERS AND INCLUDES |
| * ============================================================================ */ |
| |
| #include <errno.h> |
| #include <fcntl.h> |
| #include <stddef.h> |
| #include <stdint.h> |
| #include <stdio.h> |
| #include <stdlib.h> |
| #include <string.h> |
| |
| #ifndef Py_BUILD_CORE_BUILTIN |
| # define Py_BUILD_CORE_MODULE 1 |
| #endif |
| #include "Python.h" |
| #include <internal/pycore_debug_offsets.h> // _Py_DebugOffsets |
| #include <internal/pycore_frame.h> // FRAME_SUSPENDED_YIELD_FROM |
| #include <internal/pycore_interpframe.h> // FRAME_OWNED_BY_CSTACK |
| #include <internal/pycore_llist.h> // struct llist_node |
| #include <internal/pycore_stackref.h> // Py_TAG_BITS |
| #include "../Python/remote_debug.h" |
| |
| #ifndef HAVE_PROCESS_VM_READV |
| # define HAVE_PROCESS_VM_READV 0 |
| #endif |
| |
| /* ============================================================================ |
| * TYPE DEFINITIONS AND STRUCTURES |
| * ============================================================================ */ |
| |
| #define GET_MEMBER(type, obj, offset) (*(type*)((char*)(obj) + (offset))) |
| #define CLEAR_PTR_TAG(ptr) (((uintptr_t)(ptr) & ~Py_TAG_BITS)) |
| #define GET_MEMBER_NO_TAG(type, obj, offset) (type)(CLEAR_PTR_TAG(*(type*)((char*)(obj) + (offset)))) |
| |
| /* Size macros for opaque buffers */ |
| #define SIZEOF_BYTES_OBJ sizeof(PyBytesObject) |
| #define SIZEOF_CODE_OBJ sizeof(PyCodeObject) |
| #define SIZEOF_GEN_OBJ sizeof(PyGenObject) |
| #define SIZEOF_INTERP_FRAME sizeof(_PyInterpreterFrame) |
| #define SIZEOF_LLIST_NODE sizeof(struct llist_node) |
| #define SIZEOF_PAGE_CACHE_ENTRY sizeof(page_cache_entry_t) |
| #define SIZEOF_PYOBJECT sizeof(PyObject) |
| #define SIZEOF_SET_OBJ sizeof(PySetObject) |
| #define SIZEOF_TASK_OBJ 4096 |
| #define SIZEOF_THREAD_STATE sizeof(PyThreadState) |
| #define SIZEOF_TYPE_OBJ sizeof(PyTypeObject) |
| #define SIZEOF_UNICODE_OBJ sizeof(PyUnicodeObject) |
| #define SIZEOF_LONG_OBJ sizeof(PyLongObject) |
| |
| // Calculate the minimum buffer size needed to read interpreter state fields |
| // We need to read code_object_generation and potentially tlbc_generation |
| #ifndef MAX |
| #define MAX(a, b) ((a) > (b) ? (a) : (b)) |
| #endif |
| |
| #ifdef Py_GIL_DISABLED |
| #define INTERP_STATE_MIN_SIZE MAX(MAX(MAX(offsetof(PyInterpreterState, _code_object_generation) + sizeof(uint64_t), \ |
| offsetof(PyInterpreterState, tlbc_indices.tlbc_generation) + sizeof(uint32_t)), \ |
| offsetof(PyInterpreterState, threads.head) + sizeof(void*)), \ |
| offsetof(PyInterpreterState, _gil.last_holder) + sizeof(PyThreadState*)) |
| #else |
| #define INTERP_STATE_MIN_SIZE MAX(MAX(offsetof(PyInterpreterState, _code_object_generation) + sizeof(uint64_t), \ |
| offsetof(PyInterpreterState, threads.head) + sizeof(void*)), \ |
| offsetof(PyInterpreterState, _gil.last_holder) + sizeof(PyThreadState*)) |
| #endif |
| #define INTERP_STATE_BUFFER_SIZE MAX(INTERP_STATE_MIN_SIZE, 256) |
| |
| #define MAX_TLBC_SIZE 2048 |
| |
| // Copied from Modules/_asynciomodule.c because it's not exported |
| |
| struct _Py_AsyncioModuleDebugOffsets { |
| struct _asyncio_task_object { |
| uint64_t size; |
| uint64_t task_name; |
| uint64_t task_awaited_by; |
| uint64_t task_is_task; |
| uint64_t task_awaited_by_is_set; |
| uint64_t task_coro; |
| uint64_t task_node; |
| } asyncio_task_object; |
| struct _asyncio_interpreter_state { |
| uint64_t size; |
| uint64_t asyncio_tasks_head; |
| } asyncio_interpreter_state; |
| struct _asyncio_thread_state { |
| uint64_t size; |
| uint64_t asyncio_running_loop; |
| uint64_t asyncio_running_task; |
| uint64_t asyncio_tasks_head; |
| } asyncio_thread_state; |
| }; |
| |
| /* ============================================================================ |
| * STRUCTSEQ TYPE DEFINITIONS |
| * ============================================================================ */ |
| |
| // TaskInfo structseq type - replaces 4-tuple (task_id, task_name, coroutine_stack, awaited_by) |
| static PyStructSequence_Field TaskInfo_fields[] = { |
| {"task_id", "Task ID (memory address)"}, |
| {"task_name", "Task name"}, |
| {"coroutine_stack", "Coroutine call stack"}, |
| {"awaited_by", "Tasks awaiting this task"}, |
| {NULL} |
| }; |
| |
| static PyStructSequence_Desc TaskInfo_desc = { |
| "_remote_debugging.TaskInfo", |
| "Information about an asyncio task", |
| TaskInfo_fields, |
| 4 |
| }; |
| |
| // FrameInfo structseq type - replaces 3-tuple (filename, lineno, funcname) |
| static PyStructSequence_Field FrameInfo_fields[] = { |
| {"filename", "Source code filename"}, |
| {"lineno", "Line number"}, |
| {"funcname", "Function name"}, |
| {NULL} |
| }; |
| |
| static PyStructSequence_Desc FrameInfo_desc = { |
| "_remote_debugging.FrameInfo", |
| "Information about a frame", |
| FrameInfo_fields, |
| 3 |
| }; |
| |
| // CoroInfo structseq type - replaces 2-tuple (call_stack, task_name) |
| static PyStructSequence_Field CoroInfo_fields[] = { |
| {"call_stack", "Coroutine call stack"}, |
| {"task_name", "Task name"}, |
| {NULL} |
| }; |
| |
| static PyStructSequence_Desc CoroInfo_desc = { |
| "_remote_debugging.CoroInfo", |
| "Information about a coroutine", |
| CoroInfo_fields, |
| 2 |
| }; |
| |
| // ThreadInfo structseq type - replaces 2-tuple (thread_id, frame_info) |
| static PyStructSequence_Field ThreadInfo_fields[] = { |
| {"thread_id", "Thread ID"}, |
| {"frame_info", "Frame information"}, |
| {NULL} |
| }; |
| |
| static PyStructSequence_Desc ThreadInfo_desc = { |
| "_remote_debugging.ThreadInfo", |
| "Information about a thread", |
| ThreadInfo_fields, |
| 2 |
| }; |
| |
| // InterpreterInfo structseq type - replaces 2-tuple (interpreter_id, thread_list) |
| static PyStructSequence_Field InterpreterInfo_fields[] = { |
| {"interpreter_id", "Interpreter ID"}, |
| {"threads", "List of threads in this interpreter"}, |
| {NULL} |
| }; |
| |
| static PyStructSequence_Desc InterpreterInfo_desc = { |
| "_remote_debugging.InterpreterInfo", |
| "Information about an interpreter", |
| InterpreterInfo_fields, |
| 2 |
| }; |
| |
| // AwaitedInfo structseq type - replaces 2-tuple (tid, awaited_by_list) |
| static PyStructSequence_Field AwaitedInfo_fields[] = { |
| {"thread_id", "Thread ID"}, |
| {"awaited_by", "List of tasks awaited by this thread"}, |
| {NULL} |
| }; |
| |
| static PyStructSequence_Desc AwaitedInfo_desc = { |
| "_remote_debugging.AwaitedInfo", |
| "Information about what a thread is awaiting", |
| AwaitedInfo_fields, |
| 2 |
| }; |
| |
| typedef struct { |
| PyObject *func_name; |
| PyObject *file_name; |
| int first_lineno; |
| PyObject *linetable; // bytes |
| uintptr_t addr_code_adaptive; |
| } CachedCodeMetadata; |
| |
| typedef struct { |
| /* Types */ |
| PyTypeObject *RemoteDebugging_Type; |
| PyTypeObject *TaskInfo_Type; |
| PyTypeObject *FrameInfo_Type; |
| PyTypeObject *CoroInfo_Type; |
| PyTypeObject *ThreadInfo_Type; |
| PyTypeObject *InterpreterInfo_Type; |
| PyTypeObject *AwaitedInfo_Type; |
| } RemoteDebuggingState; |
| |
| typedef struct { |
| PyObject_HEAD |
| proc_handle_t handle; |
| uintptr_t runtime_start_address; |
| struct _Py_DebugOffsets debug_offsets; |
| int async_debug_offsets_available; |
| struct _Py_AsyncioModuleDebugOffsets async_debug_offsets; |
| uintptr_t interpreter_addr; |
| uintptr_t tstate_addr; |
| uint64_t code_object_generation; |
| _Py_hashtable_t *code_object_cache; |
| int debug; |
| int only_active_thread; |
| RemoteDebuggingState *cached_state; // Cached module state |
| #ifdef Py_GIL_DISABLED |
| // TLBC cache invalidation tracking |
| uint32_t tlbc_generation; // Track TLBC index pool changes |
| _Py_hashtable_t *tlbc_cache; // Cache of TLBC arrays by code object address |
| #endif |
| } RemoteUnwinderObject; |
| |
| #define RemoteUnwinder_CAST(op) ((RemoteUnwinderObject *)(op)) |
| |
| typedef struct |
| { |
| int lineno; |
| int end_lineno; |
| int column; |
| int end_column; |
| } LocationInfo; |
| |
| typedef struct { |
| uintptr_t remote_addr; |
| size_t size; |
| void *local_copy; |
| } StackChunkInfo; |
| |
| typedef struct { |
| StackChunkInfo *chunks; |
| size_t count; |
| } StackChunkList; |
| |
| #include "clinic/_remote_debugging_module.c.h" |
| |
| /*[clinic input] |
| module _remote_debugging |
| [clinic start generated code]*/ |
| /*[clinic end generated code: output=da39a3ee5e6b4b0d input=5f507d5b2e76a7f7]*/ |
| |
| |
| /* ============================================================================ |
| * FORWARD DECLARATIONS |
| * ============================================================================ */ |
| |
| static inline int |
| is_frame_valid( |
| RemoteUnwinderObject *unwinder, |
| uintptr_t frame_addr, |
| uintptr_t code_object_addr |
| ); |
| |
| typedef int (*thread_processor_func)( |
| RemoteUnwinderObject *unwinder, |
| uintptr_t thread_state_addr, |
| unsigned long tid, |
| void *context |
| ); |
| |
| typedef int (*set_entry_processor_func)( |
| RemoteUnwinderObject *unwinder, |
| uintptr_t key_addr, |
| void *context |
| ); |
| |
| |
| static int |
| parse_task( |
| RemoteUnwinderObject *unwinder, |
| uintptr_t task_address, |
| PyObject *render_to |
| ); |
| |
| static int |
| parse_coro_chain( |
| RemoteUnwinderObject *unwinder, |
| uintptr_t coro_address, |
| PyObject *render_to |
| ); |
| |
| /* Forward declarations for task parsing functions */ |
| static int parse_frame_object( |
| RemoteUnwinderObject *unwinder, |
| PyObject** result, |
| uintptr_t address, |
| uintptr_t* address_of_code_object, |
| uintptr_t* previous_frame |
| ); |
| |
| static int |
| parse_async_frame_chain( |
| RemoteUnwinderObject *unwinder, |
| PyObject *calls, |
| uintptr_t address_of_thread, |
| uintptr_t running_task_code_obj |
| ); |
| |
| static int read_py_ptr(RemoteUnwinderObject *unwinder, uintptr_t address, uintptr_t *ptr_addr); |
| static int read_Py_ssize_t(RemoteUnwinderObject *unwinder, uintptr_t address, Py_ssize_t *size); |
| |
| static int process_task_and_waiters(RemoteUnwinderObject *unwinder, uintptr_t task_addr, PyObject *result); |
| static int process_task_awaited_by(RemoteUnwinderObject *unwinder, uintptr_t task_address, set_entry_processor_func processor, void *context); |
| static int find_running_task_in_thread(RemoteUnwinderObject *unwinder, uintptr_t thread_state_addr, uintptr_t *running_task_addr); |
| static int get_task_code_object(RemoteUnwinderObject *unwinder, uintptr_t task_addr, uintptr_t *code_obj_addr); |
| static int append_awaited_by(RemoteUnwinderObject *unwinder, unsigned long tid, uintptr_t head_addr, PyObject *result); |
| |
| /* ============================================================================ |
| * UTILITY FUNCTIONS AND HELPERS |
| * ============================================================================ */ |
| |
| #define set_exception_cause(unwinder, exc_type, message) \ |
| if (unwinder->debug) { \ |
| _set_debug_exception_cause(exc_type, message); \ |
| } |
| |
| static void |
| cached_code_metadata_destroy(void *ptr) |
| { |
| CachedCodeMetadata *meta = (CachedCodeMetadata *)ptr; |
| Py_DECREF(meta->func_name); |
| Py_DECREF(meta->file_name); |
| Py_DECREF(meta->linetable); |
| PyMem_RawFree(meta); |
| } |
| |
| static inline RemoteDebuggingState * |
| RemoteDebugging_GetState(PyObject *module) |
| { |
| void *state = _PyModule_GetState(module); |
| assert(state != NULL); |
| return (RemoteDebuggingState *)state; |
| } |
| |
| static inline RemoteDebuggingState * |
| RemoteDebugging_GetStateFromType(PyTypeObject *type) |
| { |
| PyObject *module = PyType_GetModule(type); |
| assert(module != NULL); |
| return RemoteDebugging_GetState(module); |
| } |
| |
| static inline RemoteDebuggingState * |
| RemoteDebugging_GetStateFromObject(PyObject *obj) |
| { |
| RemoteUnwinderObject *unwinder = (RemoteUnwinderObject *)obj; |
| if (unwinder->cached_state == NULL) { |
| unwinder->cached_state = RemoteDebugging_GetStateFromType(Py_TYPE(obj)); |
| } |
| return unwinder->cached_state; |
| } |
| |
| static inline int |
| RemoteDebugging_InitState(RemoteDebuggingState *st) |
| { |
| return 0; |
| } |
| |
| static int |
| is_prerelease_version(uint64_t version) |
| { |
| return (version & 0xF0) != 0xF0; |
| } |
| |
| static inline int |
| validate_debug_offsets(struct _Py_DebugOffsets *debug_offsets) |
| { |
| if (memcmp(debug_offsets->cookie, _Py_Debug_Cookie, sizeof(debug_offsets->cookie)) != 0) { |
| // The remote is probably running a Python version predating debug offsets. |
| PyErr_SetString( |
| PyExc_RuntimeError, |
| "Can't determine the Python version of the remote process"); |
| return -1; |
| } |
| |
| // Assume debug offsets could change from one pre-release version to another, |
| // or one minor version to another, but are stable across patch versions. |
| if (is_prerelease_version(Py_Version) && Py_Version != debug_offsets->version) { |
| PyErr_SetString( |
| PyExc_RuntimeError, |
| "Can't attach from a pre-release Python interpreter" |
| " to a process running a different Python version"); |
| return -1; |
| } |
| |
| if (is_prerelease_version(debug_offsets->version) && Py_Version != debug_offsets->version) { |
| PyErr_SetString( |
| PyExc_RuntimeError, |
| "Can't attach to a pre-release Python interpreter" |
| " from a process running a different Python version"); |
| return -1; |
| } |
| |
| unsigned int remote_major = (debug_offsets->version >> 24) & 0xFF; |
| unsigned int remote_minor = (debug_offsets->version >> 16) & 0xFF; |
| |
| if (PY_MAJOR_VERSION != remote_major || PY_MINOR_VERSION != remote_minor) { |
| PyErr_Format( |
| PyExc_RuntimeError, |
| "Can't attach from a Python %d.%d process to a Python %d.%d process", |
| PY_MAJOR_VERSION, PY_MINOR_VERSION, remote_major, remote_minor); |
| return -1; |
| } |
| |
| // The debug offsets differ between free threaded and non-free threaded builds. |
| if (_Py_Debug_Free_Threaded && !debug_offsets->free_threaded) { |
| PyErr_SetString( |
| PyExc_RuntimeError, |
| "Cannot attach from a free-threaded Python process" |
| " to a process running a non-free-threaded version"); |
| return -1; |
| } |
| |
| if (!_Py_Debug_Free_Threaded && debug_offsets->free_threaded) { |
| PyErr_SetString( |
| PyExc_RuntimeError, |
| "Cannot attach to a free-threaded Python process" |
| " from a process running a non-free-threaded version"); |
| return -1; |
| } |
| |
| return 0; |
| } |
| |
| // Generic function to iterate through all threads |
| static int |
| iterate_threads( |
| RemoteUnwinderObject *unwinder, |
| thread_processor_func processor, |
| void *context |
| ) { |
| uintptr_t thread_state_addr; |
| unsigned long tid = 0; |
| |
| if (0 > _Py_RemoteDebug_PagedReadRemoteMemory( |
| &unwinder->handle, |
| unwinder->interpreter_addr + unwinder->debug_offsets.interpreter_state.threads_main, |
| sizeof(void*), |
| &thread_state_addr)) |
| { |
| set_exception_cause(unwinder, PyExc_RuntimeError, "Failed to read main thread state"); |
| return -1; |
| } |
| |
| while (thread_state_addr != 0) { |
| if (0 > _Py_RemoteDebug_PagedReadRemoteMemory( |
| &unwinder->handle, |
| thread_state_addr + unwinder->debug_offsets.thread_state.native_thread_id, |
| sizeof(tid), |
| &tid)) |
| { |
| set_exception_cause(unwinder, PyExc_RuntimeError, "Failed to read thread ID"); |
| return -1; |
| } |
| |
| // Call the processor function for this thread |
| if (processor(unwinder, thread_state_addr, tid, context) < 0) { |
| return -1; |
| } |
| |
| // Move to next thread |
| if (0 > _Py_RemoteDebug_PagedReadRemoteMemory( |
| &unwinder->handle, |
| thread_state_addr + unwinder->debug_offsets.thread_state.next, |
| sizeof(void*), |
| &thread_state_addr)) |
| { |
| set_exception_cause(unwinder, PyExc_RuntimeError, "Failed to read next thread state"); |
| return -1; |
| } |
| } |
| |
| return 0; |
| } |
| |
| // Generic function to iterate through set entries |
| static int |
| iterate_set_entries( |
| RemoteUnwinderObject *unwinder, |
| uintptr_t set_addr, |
| set_entry_processor_func processor, |
| void *context |
| ) { |
| char set_object[SIZEOF_SET_OBJ]; |
| if (_Py_RemoteDebug_PagedReadRemoteMemory(&unwinder->handle, set_addr, |
| SIZEOF_SET_OBJ, set_object) < 0) { |
| set_exception_cause(unwinder, PyExc_RuntimeError, "Failed to read set object"); |
| return -1; |
| } |
| |
| Py_ssize_t num_els = GET_MEMBER(Py_ssize_t, set_object, unwinder->debug_offsets.set_object.used); |
| Py_ssize_t set_len = GET_MEMBER(Py_ssize_t, set_object, unwinder->debug_offsets.set_object.mask) + 1; |
| uintptr_t table_ptr = GET_MEMBER(uintptr_t, set_object, unwinder->debug_offsets.set_object.table); |
| |
| Py_ssize_t i = 0; |
| Py_ssize_t els = 0; |
| while (i < set_len && els < num_els) { |
| uintptr_t key_addr; |
| if (read_py_ptr(unwinder, table_ptr, &key_addr) < 0) { |
| set_exception_cause(unwinder, PyExc_RuntimeError, "Failed to read set entry key"); |
| return -1; |
| } |
| |
| if ((void*)key_addr != NULL) { |
| Py_ssize_t ref_cnt; |
| if (read_Py_ssize_t(unwinder, table_ptr, &ref_cnt) < 0) { |
| set_exception_cause(unwinder, PyExc_RuntimeError, "Failed to read set entry ref count"); |
| return -1; |
| } |
| |
| if (ref_cnt) { |
| // Process this valid set entry |
| if (processor(unwinder, key_addr, context) < 0) { |
| return -1; |
| } |
| els++; |
| } |
| } |
| table_ptr += sizeof(void*) * 2; |
| i++; |
| } |
| |
| return 0; |
| } |
| |
| // Processor function for task waiters |
| static int |
| process_waiter_task( |
| RemoteUnwinderObject *unwinder, |
| uintptr_t key_addr, |
| void *context |
| ) { |
| PyObject *result = (PyObject *)context; |
| return process_task_and_waiters(unwinder, key_addr, result); |
| } |
| |
| // Processor function for parsing tasks in sets |
| static int |
| process_task_parser( |
| RemoteUnwinderObject *unwinder, |
| uintptr_t key_addr, |
| void *context |
| ) { |
| PyObject *awaited_by = (PyObject *)context; |
| return parse_task(unwinder, key_addr, awaited_by); |
| } |
| |
| /* ============================================================================ |
| * MEMORY READING FUNCTIONS |
| * ============================================================================ */ |
| |
| #define DEFINE_MEMORY_READER(type_name, c_type, error_msg) \ |
| static inline int \ |
| read_##type_name(RemoteUnwinderObject *unwinder, uintptr_t address, c_type *result) \ |
| { \ |
| int res = _Py_RemoteDebug_PagedReadRemoteMemory(&unwinder->handle, address, sizeof(c_type), result); \ |
| if (res < 0) { \ |
| set_exception_cause(unwinder, PyExc_RuntimeError, error_msg); \ |
| return -1; \ |
| } \ |
| return 0; \ |
| } |
| |
| DEFINE_MEMORY_READER(ptr, uintptr_t, "Failed to read pointer from remote memory") |
| DEFINE_MEMORY_READER(Py_ssize_t, Py_ssize_t, "Failed to read Py_ssize_t from remote memory") |
| DEFINE_MEMORY_READER(char, char, "Failed to read char from remote memory") |
| |
| static int |
| read_py_ptr(RemoteUnwinderObject *unwinder, uintptr_t address, uintptr_t *ptr_addr) |
| { |
| if (read_ptr(unwinder, address, ptr_addr)) { |
| set_exception_cause(unwinder, PyExc_RuntimeError, "Failed to read Python pointer"); |
| return -1; |
| } |
| *ptr_addr &= ~Py_TAG_BITS; |
| return 0; |
| } |
| |
| /* ============================================================================ |
| * PYTHON OBJECT READING FUNCTIONS |
| * ============================================================================ */ |
| |
| static PyObject * |
| read_py_str( |
| RemoteUnwinderObject *unwinder, |
| uintptr_t address, |
| Py_ssize_t max_len |
| ) { |
| PyObject *result = NULL; |
| char *buf = NULL; |
| |
| // Read the entire PyUnicodeObject at once |
| char unicode_obj[SIZEOF_UNICODE_OBJ]; |
| int res = _Py_RemoteDebug_PagedReadRemoteMemory( |
| &unwinder->handle, |
| address, |
| SIZEOF_UNICODE_OBJ, |
| unicode_obj |
| ); |
| if (res < 0) { |
| set_exception_cause(unwinder, PyExc_RuntimeError, "Failed to read PyUnicodeObject"); |
| goto err; |
| } |
| |
| Py_ssize_t len = GET_MEMBER(Py_ssize_t, unicode_obj, unwinder->debug_offsets.unicode_object.length); |
| if (len < 0 || len > max_len) { |
| PyErr_Format(PyExc_RuntimeError, |
| "Invalid string length (%zd) at 0x%lx", len, address); |
| set_exception_cause(unwinder, PyExc_RuntimeError, "Invalid string length in remote Unicode object"); |
| return NULL; |
| } |
| |
| buf = (char *)PyMem_RawMalloc(len+1); |
| if (buf == NULL) { |
| PyErr_NoMemory(); |
| set_exception_cause(unwinder, PyExc_MemoryError, "Failed to allocate buffer for string reading"); |
| return NULL; |
| } |
| |
| size_t offset = unwinder->debug_offsets.unicode_object.asciiobject_size; |
| res = _Py_RemoteDebug_PagedReadRemoteMemory(&unwinder->handle, address + offset, len, buf); |
| if (res < 0) { |
| set_exception_cause(unwinder, PyExc_RuntimeError, "Failed to read string data from remote memory"); |
| goto err; |
| } |
| buf[len] = '\0'; |
| |
| result = PyUnicode_FromStringAndSize(buf, len); |
| if (result == NULL) { |
| set_exception_cause(unwinder, PyExc_RuntimeError, "Failed to create PyUnicode from remote string data"); |
| goto err; |
| } |
| |
| PyMem_RawFree(buf); |
| assert(result != NULL); |
| return result; |
| |
| err: |
| if (buf != NULL) { |
| PyMem_RawFree(buf); |
| } |
| return NULL; |
| } |
| |
| static PyObject * |
| read_py_bytes( |
| RemoteUnwinderObject *unwinder, |
| uintptr_t address, |
| Py_ssize_t max_len |
| ) { |
| PyObject *result = NULL; |
| char *buf = NULL; |
| |
| // Read the entire PyBytesObject at once |
| char bytes_obj[SIZEOF_BYTES_OBJ]; |
| int res = _Py_RemoteDebug_PagedReadRemoteMemory( |
| &unwinder->handle, |
| address, |
| SIZEOF_BYTES_OBJ, |
| bytes_obj |
| ); |
| if (res < 0) { |
| set_exception_cause(unwinder, PyExc_RuntimeError, "Failed to read PyBytesObject"); |
| goto err; |
| } |
| |
| Py_ssize_t len = GET_MEMBER(Py_ssize_t, bytes_obj, unwinder->debug_offsets.bytes_object.ob_size); |
| if (len < 0 || len > max_len) { |
| PyErr_Format(PyExc_RuntimeError, |
| "Invalid bytes length (%zd) at 0x%lx", len, address); |
| set_exception_cause(unwinder, PyExc_RuntimeError, "Invalid bytes length in remote bytes object"); |
| return NULL; |
| } |
| |
| buf = (char *)PyMem_RawMalloc(len+1); |
| if (buf == NULL) { |
| PyErr_NoMemory(); |
| set_exception_cause(unwinder, PyExc_MemoryError, "Failed to allocate buffer for bytes reading"); |
| return NULL; |
| } |
| |
| size_t offset = unwinder->debug_offsets.bytes_object.ob_sval; |
| res = _Py_RemoteDebug_PagedReadRemoteMemory(&unwinder->handle, address + offset, len, buf); |
| if (res < 0) { |
| set_exception_cause(unwinder, PyExc_RuntimeError, "Failed to read bytes data from remote memory"); |
| goto err; |
| } |
| buf[len] = '\0'; |
| |
| result = PyBytes_FromStringAndSize(buf, len); |
| if (result == NULL) { |
| set_exception_cause(unwinder, PyExc_RuntimeError, "Failed to create PyBytes from remote bytes data"); |
| goto err; |
| } |
| |
| PyMem_RawFree(buf); |
| assert(result != NULL); |
| return result; |
| |
| err: |
| if (buf != NULL) { |
| PyMem_RawFree(buf); |
| } |
| return NULL; |
| } |
| |
| static long |
| read_py_long( |
| RemoteUnwinderObject *unwinder, |
| uintptr_t address |
| ) |
| { |
| unsigned int shift = PYLONG_BITS_IN_DIGIT; |
| |
| // Read the entire PyLongObject at once |
| char long_obj[SIZEOF_LONG_OBJ]; |
| int bytes_read = _Py_RemoteDebug_PagedReadRemoteMemory( |
| &unwinder->handle, |
| address, |
| unwinder->debug_offsets.long_object.size, |
| long_obj); |
| if (bytes_read < 0) { |
| set_exception_cause(unwinder, PyExc_RuntimeError, "Failed to read PyLongObject"); |
| return -1; |
| } |
| |
| uintptr_t lv_tag = GET_MEMBER(uintptr_t, long_obj, unwinder->debug_offsets.long_object.lv_tag); |
| int negative = (lv_tag & 3) == 2; |
| Py_ssize_t size = lv_tag >> 3; |
| |
| if (size == 0) { |
| return 0; |
| } |
| |
| // If the long object has inline digits, use them directly |
| digit *digits; |
| if (size <= _PY_NSMALLNEGINTS + _PY_NSMALLPOSINTS) { |
| // For small integers, digits are inline in the long_value.ob_digit array |
| digits = (digit *)PyMem_RawMalloc(size * sizeof(digit)); |
| if (!digits) { |
| PyErr_NoMemory(); |
| set_exception_cause(unwinder, PyExc_MemoryError, "Failed to allocate digits for small PyLong"); |
| return -1; |
| } |
| memcpy(digits, long_obj + unwinder->debug_offsets.long_object.ob_digit, size * sizeof(digit)); |
| } else { |
| // For larger integers, we need to read the digits separately |
| digits = (digit *)PyMem_RawMalloc(size * sizeof(digit)); |
| if (!digits) { |
| PyErr_NoMemory(); |
| set_exception_cause(unwinder, PyExc_MemoryError, "Failed to allocate digits for large PyLong"); |
| return -1; |
| } |
| |
| bytes_read = _Py_RemoteDebug_PagedReadRemoteMemory( |
| &unwinder->handle, |
| address + unwinder->debug_offsets.long_object.ob_digit, |
| sizeof(digit) * size, |
| digits |
| ); |
| if (bytes_read < 0) { |
| set_exception_cause(unwinder, PyExc_RuntimeError, "Failed to read PyLong digits from remote memory"); |
| goto error; |
| } |
| } |
| |
| long long value = 0; |
| |
| // In theory this can overflow, but because of llvm/llvm-project#16778 |
| // we can't use __builtin_mul_overflow because it fails to link with |
| // __muloti4 on aarch64. In practice this is fine because all we're |
| // testing here are task numbers that would fit in a single byte. |
| for (Py_ssize_t i = 0; i < size; ++i) { |
| long long factor = digits[i] * (1UL << (Py_ssize_t)(shift * i)); |
| value += factor; |
| } |
| PyMem_RawFree(digits); |
| if (negative) { |
| value *= -1; |
| } |
| return (long)value; |
| error: |
| PyMem_RawFree(digits); |
| return -1; |
| } |
| |
| /* ============================================================================ |
| * ASYNCIO DEBUG FUNCTIONS |
| * ============================================================================ */ |
| |
| // Get the PyAsyncioDebug section address for any platform |
| static uintptr_t |
| _Py_RemoteDebug_GetAsyncioDebugAddress(proc_handle_t* handle) |
| { |
| uintptr_t address; |
| |
| #ifdef MS_WINDOWS |
| // On Windows, search for asyncio debug in executable or DLL |
| address = search_windows_map_for_section(handle, "AsyncioD", L"_asyncio"); |
| if (address == 0) { |
| // Error out: 'python' substring covers both executable and DLL |
| PyObject *exc = PyErr_GetRaisedException(); |
| PyErr_SetString(PyExc_RuntimeError, "Failed to find the AsyncioDebug section in the process."); |
| _PyErr_ChainExceptions1(exc); |
| } |
| #elif defined(__linux__) |
| // On Linux, search for asyncio debug in executable or DLL |
| address = search_linux_map_for_section(handle, "AsyncioDebug", "python"); |
| if (address == 0) { |
| // Error out: 'python' substring covers both executable and DLL |
| PyObject *exc = PyErr_GetRaisedException(); |
| PyErr_SetString(PyExc_RuntimeError, "Failed to find the AsyncioDebug section in the process."); |
| _PyErr_ChainExceptions1(exc); |
| } |
| #elif defined(__APPLE__) && TARGET_OS_OSX |
| // On macOS, try libpython first, then fall back to python |
| address = search_map_for_section(handle, "AsyncioDebug", "libpython"); |
| if (address == 0) { |
| PyErr_Clear(); |
| address = search_map_for_section(handle, "AsyncioDebug", "python"); |
| } |
| if (address == 0) { |
| // Error out: 'python' substring covers both executable and DLL |
| PyObject *exc = PyErr_GetRaisedException(); |
| PyErr_SetString(PyExc_RuntimeError, "Failed to find the AsyncioDebug section in the process."); |
| _PyErr_ChainExceptions1(exc); |
| } |
| #else |
| Py_UNREACHABLE(); |
| #endif |
| |
| return address; |
| } |
| |
| static int |
| read_async_debug( |
| RemoteUnwinderObject *unwinder |
| ) { |
| uintptr_t async_debug_addr = _Py_RemoteDebug_GetAsyncioDebugAddress(&unwinder->handle); |
| if (!async_debug_addr) { |
| set_exception_cause(unwinder, PyExc_RuntimeError, "Failed to get AsyncioDebug address"); |
| return -1; |
| } |
| |
| size_t size = sizeof(struct _Py_AsyncioModuleDebugOffsets); |
| int result = _Py_RemoteDebug_PagedReadRemoteMemory(&unwinder->handle, async_debug_addr, size, &unwinder->async_debug_offsets); |
| if (result < 0) { |
| set_exception_cause(unwinder, PyExc_RuntimeError, "Failed to read AsyncioDebug offsets"); |
| } |
| return result; |
| } |
| |
| /* ============================================================================ |
| * ASYNCIO TASK PARSING FUNCTIONS |
| * ============================================================================ */ |
| |
| static PyObject * |
| parse_task_name( |
| RemoteUnwinderObject *unwinder, |
| uintptr_t task_address |
| ) { |
| // Read the entire TaskObj at once |
| char task_obj[SIZEOF_TASK_OBJ]; |
| int err = _Py_RemoteDebug_PagedReadRemoteMemory( |
| &unwinder->handle, |
| task_address, |
| unwinder->async_debug_offsets.asyncio_task_object.size, |
| task_obj); |
| if (err < 0) { |
| set_exception_cause(unwinder, PyExc_RuntimeError, "Failed to read task object"); |
| return NULL; |
| } |
| |
| uintptr_t task_name_addr = GET_MEMBER_NO_TAG(uintptr_t, task_obj, unwinder->async_debug_offsets.asyncio_task_object.task_name); |
| |
| // The task name can be a long or a string so we need to check the type |
| char task_name_obj[SIZEOF_PYOBJECT]; |
| err = _Py_RemoteDebug_PagedReadRemoteMemory( |
| &unwinder->handle, |
| task_name_addr, |
| SIZEOF_PYOBJECT, |
| task_name_obj); |
| if (err < 0) { |
| set_exception_cause(unwinder, PyExc_RuntimeError, "Failed to read task name object"); |
| return NULL; |
| } |
| |
| // Now read the type object to get the flags |
| char type_obj[SIZEOF_TYPE_OBJ]; |
| err = _Py_RemoteDebug_PagedReadRemoteMemory( |
| &unwinder->handle, |
| GET_MEMBER(uintptr_t, task_name_obj, unwinder->debug_offsets.pyobject.ob_type), |
| SIZEOF_TYPE_OBJ, |
| type_obj); |
| if (err < 0) { |
| set_exception_cause(unwinder, PyExc_RuntimeError, "Failed to read task name type object"); |
| return NULL; |
| } |
| |
| if ((GET_MEMBER(unsigned long, type_obj, unwinder->debug_offsets.type_object.tp_flags) & Py_TPFLAGS_LONG_SUBCLASS)) { |
| long res = read_py_long(unwinder, task_name_addr); |
| if (res == -1) { |
| set_exception_cause(unwinder, PyExc_RuntimeError, "Task name PyLong parsing failed"); |
| return NULL; |
| } |
| return PyUnicode_FromFormat("Task-%d", res); |
| } |
| |
| if(!(GET_MEMBER(unsigned long, type_obj, unwinder->debug_offsets.type_object.tp_flags) & Py_TPFLAGS_UNICODE_SUBCLASS)) { |
| PyErr_SetString(PyExc_RuntimeError, "Invalid task name object"); |
| set_exception_cause(unwinder, PyExc_RuntimeError, "Task name object is neither long nor unicode"); |
| return NULL; |
| } |
| |
| return read_py_str( |
| unwinder, |
| task_name_addr, |
| 255 |
| ); |
| } |
| |
| static int parse_task_awaited_by( |
| RemoteUnwinderObject *unwinder, |
| uintptr_t task_address, |
| PyObject *awaited_by |
| ) { |
| return process_task_awaited_by(unwinder, task_address, process_task_parser, awaited_by); |
| } |
| |
| static int |
| handle_yield_from_frame( |
| RemoteUnwinderObject *unwinder, |
| uintptr_t gi_iframe_addr, |
| uintptr_t gen_type_addr, |
| PyObject *render_to |
| ) { |
| // Read the entire interpreter frame at once |
| char iframe[SIZEOF_INTERP_FRAME]; |
| int err = _Py_RemoteDebug_PagedReadRemoteMemory( |
| &unwinder->handle, |
| gi_iframe_addr, |
| SIZEOF_INTERP_FRAME, |
| iframe); |
| if (err < 0) { |
| set_exception_cause(unwinder, PyExc_RuntimeError, "Failed to read interpreter frame in yield_from handler"); |
| return -1; |
| } |
| |
| if (GET_MEMBER(char, iframe, unwinder->debug_offsets.interpreter_frame.owner) != FRAME_OWNED_BY_GENERATOR) { |
| PyErr_SetString( |
| PyExc_RuntimeError, |
| "generator doesn't own its frame \\_o_/"); |
| set_exception_cause(unwinder, PyExc_RuntimeError, "Frame ownership mismatch in yield_from"); |
| return -1; |
| } |
| |
| uintptr_t stackpointer_addr = GET_MEMBER_NO_TAG(uintptr_t, iframe, unwinder->debug_offsets.interpreter_frame.stackpointer); |
| |
| if ((void*)stackpointer_addr != NULL) { |
| uintptr_t gi_await_addr; |
| err = read_py_ptr( |
| unwinder, |
| stackpointer_addr - sizeof(void*), |
| &gi_await_addr); |
| if (err) { |
| set_exception_cause(unwinder, PyExc_RuntimeError, "Failed to read gi_await address"); |
| return -1; |
| } |
| |
| if ((void*)gi_await_addr != NULL) { |
| uintptr_t gi_await_addr_type_addr; |
| err = read_ptr( |
| unwinder, |
| gi_await_addr + unwinder->debug_offsets.pyobject.ob_type, |
| &gi_await_addr_type_addr); |
| if (err) { |
| set_exception_cause(unwinder, PyExc_RuntimeError, "Failed to read gi_await type address"); |
| return -1; |
| } |
| |
| if (gen_type_addr == gi_await_addr_type_addr) { |
| /* This needs an explanation. We always start with parsing |
| native coroutine / generator frames. Ultimately they |
| are awaiting on something. That something can be |
| a native coroutine frame or... an iterator. |
| If it's the latter -- we can't continue building |
| our chain. So the condition to bail out of this is |
| to do that when the type of the current coroutine |
| doesn't match the type of whatever it points to |
| in its cr_await. |
| */ |
| err = parse_coro_chain(unwinder, gi_await_addr, render_to); |
| if (err) { |
| set_exception_cause(unwinder, PyExc_RuntimeError, "Failed to parse coroutine chain in yield_from"); |
| return -1; |
| } |
| } |
| } |
| } |
| |
| return 0; |
| } |
| |
| static int |
| parse_coro_chain( |
| RemoteUnwinderObject *unwinder, |
| uintptr_t coro_address, |
| PyObject *render_to |
| ) { |
| assert((void*)coro_address != NULL); |
| |
| // Read the entire generator object at once |
| char gen_object[SIZEOF_GEN_OBJ]; |
| int err = _Py_RemoteDebug_PagedReadRemoteMemory( |
| &unwinder->handle, |
| coro_address, |
| SIZEOF_GEN_OBJ, |
| gen_object); |
| if (err < 0) { |
| set_exception_cause(unwinder, PyExc_RuntimeError, "Failed to read generator object in coro chain"); |
| return -1; |
| } |
| |
| int8_t frame_state = GET_MEMBER(int8_t, gen_object, unwinder->debug_offsets.gen_object.gi_frame_state); |
| if (frame_state == FRAME_CLEARED) { |
| return 0; |
| } |
| |
| uintptr_t gen_type_addr = GET_MEMBER(uintptr_t, gen_object, unwinder->debug_offsets.pyobject.ob_type); |
| |
| PyObject* name = NULL; |
| |
| // Parse the previous frame using the gi_iframe from local copy |
| uintptr_t prev_frame; |
| uintptr_t gi_iframe_addr = coro_address + unwinder->debug_offsets.gen_object.gi_iframe; |
| uintptr_t address_of_code_object = 0; |
| if (parse_frame_object(unwinder, &name, gi_iframe_addr, &address_of_code_object, &prev_frame) < 0) { |
| set_exception_cause(unwinder, PyExc_RuntimeError, "Failed to parse frame object in coro chain"); |
| return -1; |
| } |
| |
| if (!name) { |
| return 0; |
| } |
| |
| if (PyList_Append(render_to, name)) { |
| Py_DECREF(name); |
| set_exception_cause(unwinder, PyExc_RuntimeError, "Failed to append frame to coro chain"); |
| return -1; |
| } |
| Py_DECREF(name); |
| |
| if (frame_state == FRAME_SUSPENDED_YIELD_FROM) { |
| return handle_yield_from_frame(unwinder, gi_iframe_addr, gen_type_addr, render_to); |
| } |
| |
| return 0; |
| } |
| |
| static PyObject* |
| create_task_result( |
| RemoteUnwinderObject *unwinder, |
| uintptr_t task_address |
| ) { |
| PyObject* result = NULL; |
| PyObject *call_stack = NULL; |
| PyObject *tn = NULL; |
| char task_obj[SIZEOF_TASK_OBJ]; |
| uintptr_t coro_addr; |
| |
| // Create call_stack first since it's the first tuple element |
| call_stack = PyList_New(0); |
| if (call_stack == NULL) { |
| set_exception_cause(unwinder, PyExc_MemoryError, "Failed to create call stack list"); |
| goto error; |
| } |
| |
| // Create task name/address for second tuple element |
| tn = PyLong_FromUnsignedLongLong(task_address); |
| if (tn == NULL) { |
| set_exception_cause(unwinder, PyExc_RuntimeError, "Failed to create task name/address"); |
| goto error; |
| } |
| |
| // Parse coroutine chain |
| if (_Py_RemoteDebug_PagedReadRemoteMemory(&unwinder->handle, task_address, |
| unwinder->async_debug_offsets.asyncio_task_object.size, |
| task_obj) < 0) { |
| set_exception_cause(unwinder, PyExc_RuntimeError, "Failed to read task object for coro chain"); |
| goto error; |
| } |
| |
| coro_addr = GET_MEMBER_NO_TAG(uintptr_t, task_obj, unwinder->async_debug_offsets.asyncio_task_object.task_coro); |
| |
| if ((void*)coro_addr != NULL) { |
| if (parse_coro_chain(unwinder, coro_addr, call_stack) < 0) { |
| set_exception_cause(unwinder, PyExc_RuntimeError, "Failed to parse coroutine chain"); |
| goto error; |
| } |
| |
| if (PyList_Reverse(call_stack)) { |
| set_exception_cause(unwinder, PyExc_RuntimeError, "Failed to reverse call stack"); |
| goto error; |
| } |
| } |
| |
| // Create final CoroInfo result |
| RemoteDebuggingState *state = RemoteDebugging_GetStateFromObject((PyObject*)unwinder); |
| result = PyStructSequence_New(state->CoroInfo_Type); |
| if (result == NULL) { |
| set_exception_cause(unwinder, PyExc_MemoryError, "Failed to create CoroInfo"); |
| goto error; |
| } |
| |
| // PyStructSequence_SetItem steals references, so we don't need to DECREF on success |
| PyStructSequence_SetItem(result, 0, call_stack); // This steals the reference |
| PyStructSequence_SetItem(result, 1, tn); // This steals the reference |
| |
| return result; |
| |
| error: |
| Py_XDECREF(result); |
| Py_XDECREF(call_stack); |
| Py_XDECREF(tn); |
| return NULL; |
| } |
| |
| static int |
| parse_task( |
| RemoteUnwinderObject *unwinder, |
| uintptr_t task_address, |
| PyObject *render_to |
| ) { |
| char is_task; |
| PyObject* result = NULL; |
| int err; |
| |
| err = read_char( |
| unwinder, |
| task_address + unwinder->async_debug_offsets.asyncio_task_object.task_is_task, |
| &is_task); |
| if (err) { |
| set_exception_cause(unwinder, PyExc_RuntimeError, "Failed to read is_task flag"); |
| goto error; |
| } |
| |
| if (is_task) { |
| result = create_task_result(unwinder, task_address); |
| if (!result) { |
| set_exception_cause(unwinder, PyExc_RuntimeError, "Failed to create task result"); |
| goto error; |
| } |
| } else { |
| // Create an empty CoroInfo for non-task objects |
| RemoteDebuggingState *state = RemoteDebugging_GetStateFromObject((PyObject*)unwinder); |
| result = PyStructSequence_New(state->CoroInfo_Type); |
| if (result == NULL) { |
| set_exception_cause(unwinder, PyExc_MemoryError, "Failed to create empty CoroInfo"); |
| goto error; |
| } |
| PyObject *empty_list = PyList_New(0); |
| if (empty_list == NULL) { |
| set_exception_cause(unwinder, PyExc_MemoryError, "Failed to create empty list"); |
| goto error; |
| } |
| PyObject *task_name = PyLong_FromUnsignedLongLong(task_address); |
| if (task_name == NULL) { |
| Py_DECREF(empty_list); |
| set_exception_cause(unwinder, PyExc_RuntimeError, "Failed to create task name"); |
| goto error; |
| } |
| PyStructSequence_SetItem(result, 0, empty_list); // This steals the reference |
| PyStructSequence_SetItem(result, 1, task_name); // This steals the reference |
| } |
| if (PyList_Append(render_to, result)) { |
| set_exception_cause(unwinder, PyExc_RuntimeError, "Failed to append task result to render list"); |
| goto error; |
| } |
| |
| Py_DECREF(result); |
| return 0; |
| |
| error: |
| Py_XDECREF(result); |
| return -1; |
| } |
| |
| static int |
| process_single_task_node( |
| RemoteUnwinderObject *unwinder, |
| uintptr_t task_addr, |
| PyObject **task_info, |
| PyObject *result |
| ) { |
| PyObject *tn = NULL; |
| PyObject *current_awaited_by = NULL; |
| PyObject *task_id = NULL; |
| PyObject *result_item = NULL; |
| PyObject *coroutine_stack = NULL; |
| |
| tn = parse_task_name(unwinder, task_addr); |
| if (tn == NULL) { |
| set_exception_cause(unwinder, PyExc_RuntimeError, "Failed to parse task name in single task node"); |
| goto error; |
| } |
| |
| current_awaited_by = PyList_New(0); |
| if (current_awaited_by == NULL) { |
| set_exception_cause(unwinder, PyExc_MemoryError, "Failed to create awaited_by list in single task node"); |
| goto error; |
| } |
| |
| // Extract the coroutine stack for this task |
| coroutine_stack = PyList_New(0); |
| if (coroutine_stack == NULL) { |
| set_exception_cause(unwinder, PyExc_MemoryError, "Failed to create coroutine stack list in single task node"); |
| goto error; |
| } |
| |
| if (parse_task(unwinder, task_addr, coroutine_stack) < 0) { |
| set_exception_cause(unwinder, PyExc_RuntimeError, "Failed to parse task coroutine stack in single task node"); |
| goto error; |
| } |
| |
| task_id = PyLong_FromUnsignedLongLong(task_addr); |
| if (task_id == NULL) { |
| set_exception_cause(unwinder, PyExc_RuntimeError, "Failed to create task ID in single task node"); |
| goto error; |
| } |
| |
| RemoteDebuggingState *state = RemoteDebugging_GetStateFromObject((PyObject*)unwinder); |
| result_item = PyStructSequence_New(state->TaskInfo_Type); |
| if (result_item == NULL) { |
| set_exception_cause(unwinder, PyExc_MemoryError, "Failed to create TaskInfo in single task node"); |
| goto error; |
| } |
| |
| PyStructSequence_SetItem(result_item, 0, task_id); // steals ref |
| PyStructSequence_SetItem(result_item, 1, tn); // steals ref |
| PyStructSequence_SetItem(result_item, 2, coroutine_stack); // steals ref |
| PyStructSequence_SetItem(result_item, 3, current_awaited_by); // steals ref |
| |
| // References transferred to tuple |
| task_id = NULL; |
| tn = NULL; |
| coroutine_stack = NULL; |
| current_awaited_by = NULL; |
| |
| if (PyList_Append(result, result_item)) { |
| Py_DECREF(result_item); |
| set_exception_cause(unwinder, PyExc_RuntimeError, "Failed to append result item in single task node"); |
| return -1; |
| } |
| if (task_info != NULL) { |
| *task_info = result_item; |
| } |
| Py_DECREF(result_item); |
| |
| // Get back current_awaited_by reference for parse_task_awaited_by |
| current_awaited_by = PyStructSequence_GetItem(result_item, 3); |
| if (parse_task_awaited_by(unwinder, task_addr, current_awaited_by) < 0) { |
| set_exception_cause(unwinder, PyExc_RuntimeError, "Failed to parse awaited_by in single task node"); |
| // No cleanup needed here since all references were transferred to result_item |
| // and result_item was already added to result list and decreffed |
| return -1; |
| } |
| |
| return 0; |
| |
| error: |
| Py_XDECREF(tn); |
| Py_XDECREF(current_awaited_by); |
| Py_XDECREF(task_id); |
| Py_XDECREF(result_item); |
| Py_XDECREF(coroutine_stack); |
| return -1; |
| } |
| |
| // Thread processor for get_all_awaited_by |
| static int |
| process_thread_for_awaited_by( |
| RemoteUnwinderObject *unwinder, |
| uintptr_t thread_state_addr, |
| unsigned long tid, |
| void *context |
| ) { |
| PyObject *result = (PyObject *)context; |
| uintptr_t head_addr = thread_state_addr + unwinder->async_debug_offsets.asyncio_thread_state.asyncio_tasks_head; |
| return append_awaited_by(unwinder, tid, head_addr, result); |
| } |
| |
| // Generic function to process task awaited_by |
| static int |
| process_task_awaited_by( |
| RemoteUnwinderObject *unwinder, |
| uintptr_t task_address, |
| set_entry_processor_func processor, |
| void *context |
| ) { |
| // Read the entire TaskObj at once |
| char task_obj[SIZEOF_TASK_OBJ]; |
| if (_Py_RemoteDebug_PagedReadRemoteMemory(&unwinder->handle, task_address, |
| unwinder->async_debug_offsets.asyncio_task_object.size, |
| task_obj) < 0) { |
| set_exception_cause(unwinder, PyExc_RuntimeError, "Failed to read task object"); |
| return -1; |
| } |
| |
| uintptr_t task_ab_addr = GET_MEMBER_NO_TAG(uintptr_t, task_obj, unwinder->async_debug_offsets.asyncio_task_object.task_awaited_by); |
| if ((void*)task_ab_addr == NULL) { |
| return 0; // No tasks waiting for this one |
| } |
| |
| char awaited_by_is_a_set = GET_MEMBER(char, task_obj, unwinder->async_debug_offsets.asyncio_task_object.task_awaited_by_is_set); |
| |
| if (awaited_by_is_a_set) { |
| return iterate_set_entries(unwinder, task_ab_addr, processor, context); |
| } else { |
| // Single task waiting |
| return processor(unwinder, task_ab_addr, context); |
| } |
| } |
| |
| static int |
| process_running_task_chain( |
| RemoteUnwinderObject *unwinder, |
| uintptr_t running_task_addr, |
| uintptr_t thread_state_addr, |
| PyObject *result |
| ) { |
| uintptr_t running_task_code_obj = 0; |
| if(get_task_code_object(unwinder, running_task_addr, &running_task_code_obj) < 0) { |
| return -1; |
| } |
| |
| // First, add this task to the result |
| PyObject *task_info = NULL; |
| if (process_single_task_node(unwinder, running_task_addr, &task_info, result) < 0) { |
| return -1; |
| } |
| |
| // Get the chain from the current frame to this task |
| PyObject *coro_chain = PyStructSequence_GET_ITEM(task_info, 2); |
| assert(coro_chain != NULL); |
| if (PyList_GET_SIZE(coro_chain) != 1) { |
| set_exception_cause(unwinder, PyExc_RuntimeError, "Coro chain is not a single item"); |
| return -1; |
| } |
| PyObject *coro_info = PyList_GET_ITEM(coro_chain, 0); |
| assert(coro_info != NULL); |
| PyObject *frame_chain = PyStructSequence_GET_ITEM(coro_info, 0); |
| assert(frame_chain != NULL); |
| |
| // Clear the coro_chain |
| if (PyList_Clear(frame_chain) < 0) { |
| set_exception_cause(unwinder, PyExc_RuntimeError, "Failed to clear coroutine chain"); |
| return -1; |
| } |
| |
| // Add the chain from the current frame to this task |
| if (parse_async_frame_chain(unwinder, frame_chain, thread_state_addr, running_task_code_obj) < 0) { |
| return -1; |
| } |
| |
| // Now find all tasks that are waiting for this task and process them |
| if (process_task_awaited_by(unwinder, running_task_addr, process_waiter_task, result) < 0) { |
| return -1; |
| } |
| |
| return 0; |
| } |
| |
| // Thread processor for get_async_stack_trace |
| static int |
| process_thread_for_async_stack_trace( |
| RemoteUnwinderObject *unwinder, |
| uintptr_t thread_state_addr, |
| unsigned long tid, |
| void *context |
| ) { |
| PyObject *result = (PyObject *)context; |
| |
| // Find running task in this thread |
| uintptr_t running_task_addr; |
| if (find_running_task_in_thread(unwinder, thread_state_addr, &running_task_addr) < 0) { |
| return 0; |
| } |
| |
| // If we found a running task, process it and its waiters |
| if ((void*)running_task_addr != NULL) { |
| PyObject *task_list = PyList_New(0); |
| if (task_list == NULL) { |
| set_exception_cause(unwinder, PyExc_MemoryError, "Failed to create task list for thread"); |
| return -1; |
| } |
| |
| if (process_running_task_chain(unwinder, running_task_addr, thread_state_addr, task_list) < 0) { |
| Py_DECREF(task_list); |
| set_exception_cause(unwinder, PyExc_RuntimeError, "Failed to process running task chain"); |
| return -1; |
| } |
| |
| // Create AwaitedInfo structure for this thread |
| PyObject *tid_py = PyLong_FromUnsignedLong(tid); |
| if (tid_py == NULL) { |
| Py_DECREF(task_list); |
| set_exception_cause(unwinder, PyExc_RuntimeError, "Failed to create thread ID"); |
| return -1; |
| } |
| |
| RemoteDebuggingState *state = RemoteDebugging_GetStateFromObject((PyObject*)unwinder); |
| PyObject *awaited_info = PyStructSequence_New(state->AwaitedInfo_Type); |
| if (awaited_info == NULL) { |
| Py_DECREF(tid_py); |
| Py_DECREF(task_list); |
| set_exception_cause(unwinder, PyExc_MemoryError, "Failed to create AwaitedInfo"); |
| return -1; |
| } |
| |
| PyStructSequence_SetItem(awaited_info, 0, tid_py); // steals ref |
| PyStructSequence_SetItem(awaited_info, 1, task_list); // steals ref |
| |
| if (PyList_Append(result, awaited_info)) { |
| Py_DECREF(awaited_info); |
| set_exception_cause(unwinder, PyExc_RuntimeError, "Failed to append AwaitedInfo to result"); |
| return -1; |
| } |
| Py_DECREF(awaited_info); |
| } |
| |
| return 0; |
| } |
| |
| static int |
| process_task_and_waiters( |
| RemoteUnwinderObject *unwinder, |
| uintptr_t task_addr, |
| PyObject *result |
| ) { |
| // First, add this task to the result |
| if (process_single_task_node(unwinder, task_addr, NULL, result) < 0) { |
| return -1; |
| } |
| |
| // Now find all tasks that are waiting for this task and process them |
| return process_task_awaited_by(unwinder, task_addr, process_waiter_task, result); |
| } |
| |
| static int |
| find_running_task_in_thread( |
| RemoteUnwinderObject *unwinder, |
| uintptr_t thread_state_addr, |
| uintptr_t *running_task_addr |
| ) { |
| *running_task_addr = (uintptr_t)NULL; |
| |
| uintptr_t address_of_running_loop; |
| int bytes_read = read_py_ptr( |
| unwinder, |
| thread_state_addr + unwinder->async_debug_offsets.asyncio_thread_state.asyncio_running_loop, |
| &address_of_running_loop); |
| if (bytes_read == -1) { |
| set_exception_cause(unwinder, PyExc_RuntimeError, "Failed to read running loop address"); |
| return -1; |
| } |
| |
| // no asyncio loop is now running |
| if ((void*)address_of_running_loop == NULL) { |
| return 0; |
| } |
| |
| int err = read_ptr( |
| unwinder, |
| thread_state_addr + unwinder->async_debug_offsets.asyncio_thread_state.asyncio_running_task, |
| running_task_addr); |
| if (err) { |
| set_exception_cause(unwinder, PyExc_RuntimeError, "Failed to read running task address"); |
| return -1; |
| } |
| |
| return 0; |
| } |
| |
| static int |
| get_task_code_object(RemoteUnwinderObject *unwinder, uintptr_t task_addr, uintptr_t *code_obj_addr) { |
| uintptr_t running_coro_addr = 0; |
| |
| if(read_py_ptr( |
| unwinder, |
| task_addr + unwinder->async_debug_offsets.asyncio_task_object.task_coro, |
| &running_coro_addr) < 0) { |
| set_exception_cause(unwinder, PyExc_RuntimeError, "Running task coro read failed"); |
| return -1; |
| } |
| |
| if (running_coro_addr == 0) { |
| PyErr_SetString(PyExc_RuntimeError, "Running task coro is NULL"); |
| set_exception_cause(unwinder, PyExc_RuntimeError, "Running task coro address is NULL"); |
| return -1; |
| } |
| |
| // note: genobject's gi_iframe is an embedded struct so the address to |
| // the offset leads directly to its first field: f_executable |
| if (read_py_ptr( |
| unwinder, |
| running_coro_addr + unwinder->debug_offsets.gen_object.gi_iframe, code_obj_addr) < 0) { |
| set_exception_cause(unwinder, PyExc_RuntimeError, "Failed to read running task code object"); |
| return -1; |
| } |
| |
| if (*code_obj_addr == 0) { |
| PyErr_SetString(PyExc_RuntimeError, "Running task code object is NULL"); |
| set_exception_cause(unwinder, PyExc_RuntimeError, "Running task code object address is NULL"); |
| return -1; |
| } |
| |
| return 0; |
| } |
| |
| /* ============================================================================ |
| * TLBC CACHING FUNCTIONS |
| * ============================================================================ */ |
| |
| #ifdef Py_GIL_DISABLED |
| |
| typedef struct { |
| void *tlbc_array; // Local copy of the TLBC array |
| Py_ssize_t tlbc_array_size; // Size of the TLBC array |
| uint32_t generation; // Generation when this was cached |
| } TLBCCacheEntry; |
| |
| static void |
| tlbc_cache_entry_destroy(void *ptr) |
| { |
| TLBCCacheEntry *entry = (TLBCCacheEntry *)ptr; |
| if (entry->tlbc_array) { |
| PyMem_RawFree(entry->tlbc_array); |
| } |
| PyMem_RawFree(entry); |
| } |
| |
| static TLBCCacheEntry * |
| get_tlbc_cache_entry(RemoteUnwinderObject *self, uintptr_t code_addr, uint32_t current_generation) |
| { |
| void *key = (void *)code_addr; |
| TLBCCacheEntry *entry = _Py_hashtable_get(self->tlbc_cache, key); |
| |
| if (entry && entry->generation != current_generation) { |
| // Entry is stale, remove it by setting to NULL |
| _Py_hashtable_set(self->tlbc_cache, key, NULL); |
| entry = NULL; |
| } |
| |
| return entry; |
| } |
| |
| static int |
| cache_tlbc_array(RemoteUnwinderObject *unwinder, uintptr_t code_addr, uintptr_t tlbc_array_addr, uint32_t generation) |
| { |
| uintptr_t tlbc_array_ptr; |
| void *tlbc_array = NULL; |
| TLBCCacheEntry *entry = NULL; |
| |
| // Read the TLBC array pointer |
| if (read_ptr(unwinder, tlbc_array_addr, &tlbc_array_ptr) != 0) { |
| PyErr_SetString(PyExc_RuntimeError, "Failed to read TLBC array pointer"); |
| set_exception_cause(unwinder, PyExc_RuntimeError, "Failed to read TLBC array pointer"); |
| return 0; // Read error |
| } |
| |
| // Validate TLBC array pointer |
| if (tlbc_array_ptr == 0) { |
| PyErr_SetString(PyExc_RuntimeError, "TLBC array pointer is NULL"); |
| return 0; // No TLBC array |
| } |
| |
| // Read the TLBC array size |
| Py_ssize_t tlbc_size; |
| if (_Py_RemoteDebug_PagedReadRemoteMemory(&unwinder->handle, tlbc_array_ptr, sizeof(tlbc_size), &tlbc_size) != 0) { |
| PyErr_SetString(PyExc_RuntimeError, "Failed to read TLBC array size"); |
| set_exception_cause(unwinder, PyExc_RuntimeError, "Failed to read TLBC array size"); |
| return 0; // Read error |
| } |
| |
| // Validate TLBC array size |
| if (tlbc_size <= 0) { |
| PyErr_SetString(PyExc_RuntimeError, "Invalid TLBC array size"); |
| return 0; // Invalid size |
| } |
| |
| if (tlbc_size > MAX_TLBC_SIZE) { |
| PyErr_SetString(PyExc_RuntimeError, "TLBC array size exceeds maximum limit"); |
| return 0; // Invalid size |
| } |
| |
| // Allocate and read the entire TLBC array |
| size_t array_data_size = tlbc_size * sizeof(void*); |
| tlbc_array = PyMem_RawMalloc(sizeof(Py_ssize_t) + array_data_size); |
| if (!tlbc_array) { |
| PyErr_NoMemory(); |
| set_exception_cause(unwinder, PyExc_MemoryError, "Failed to allocate TLBC array"); |
| return 0; // Memory error |
| } |
| |
| if (_Py_RemoteDebug_PagedReadRemoteMemory(&unwinder->handle, tlbc_array_ptr, sizeof(Py_ssize_t) + array_data_size, tlbc_array) != 0) { |
| PyMem_RawFree(tlbc_array); |
| set_exception_cause(unwinder, PyExc_RuntimeError, "Failed to read TLBC array data"); |
| return 0; // Read error |
| } |
| |
| // Create cache entry |
| entry = PyMem_RawMalloc(sizeof(TLBCCacheEntry)); |
| if (!entry) { |
| PyErr_NoMemory(); |
| PyMem_RawFree(tlbc_array); |
| set_exception_cause(unwinder, PyExc_MemoryError, "Failed to allocate TLBC cache entry"); |
| return 0; // Memory error |
| } |
| |
| entry->tlbc_array = tlbc_array; |
| entry->tlbc_array_size = tlbc_size; |
| entry->generation = generation; |
| |
| // Store in cache |
| void *key = (void *)code_addr; |
| if (_Py_hashtable_set(unwinder->tlbc_cache, key, entry) < 0) { |
| tlbc_cache_entry_destroy(entry); |
| set_exception_cause(unwinder, PyExc_RuntimeError, "Failed to store TLBC entry in cache"); |
| return 0; // Cache error |
| } |
| |
| return 1; // Success |
| } |
| |
| |
| |
| #endif |
| |
| /* ============================================================================ |
| * LINE TABLE PARSING FUNCTIONS |
| * ============================================================================ */ |
| |
| static int |
| scan_varint(const uint8_t **ptr) |
| { |
| unsigned int read = **ptr; |
| *ptr = *ptr + 1; |
| unsigned int val = read & 63; |
| unsigned int shift = 0; |
| while (read & 64) { |
| read = **ptr; |
| *ptr = *ptr + 1; |
| shift += 6; |
| val |= (read & 63) << shift; |
| } |
| return val; |
| } |
| |
| static int |
| scan_signed_varint(const uint8_t **ptr) |
| { |
| unsigned int uval = scan_varint(ptr); |
| if (uval & 1) { |
| return -(int)(uval >> 1); |
| } |
| else { |
| return uval >> 1; |
| } |
| } |
| |
| static bool |
| parse_linetable(const uintptr_t addrq, const char* linetable, int firstlineno, LocationInfo* info) |
| { |
| const uint8_t* ptr = (const uint8_t*)(linetable); |
| uint64_t addr = 0; |
| info->lineno = firstlineno; |
| |
| while (*ptr != '\0') { |
| // See InternalDocs/code_objects.md for where these magic numbers are from |
| // and for the decoding algorithm. |
| uint8_t first_byte = *(ptr++); |
| uint8_t code = (first_byte >> 3) & 15; |
| size_t length = (first_byte & 7) + 1; |
| uintptr_t end_addr = addr + length; |
| switch (code) { |
| case PY_CODE_LOCATION_INFO_NONE: { |
| break; |
| } |
| case PY_CODE_LOCATION_INFO_LONG: { |
| int line_delta = scan_signed_varint(&ptr); |
| info->lineno += line_delta; |
| info->end_lineno = info->lineno + scan_varint(&ptr); |
| info->column = scan_varint(&ptr) - 1; |
| info->end_column = scan_varint(&ptr) - 1; |
| break; |
| } |
| case PY_CODE_LOCATION_INFO_NO_COLUMNS: { |
| int line_delta = scan_signed_varint(&ptr); |
| info->lineno += line_delta; |
| info->column = info->end_column = -1; |
| break; |
| } |
| case PY_CODE_LOCATION_INFO_ONE_LINE0: |
| case PY_CODE_LOCATION_INFO_ONE_LINE1: |
| case PY_CODE_LOCATION_INFO_ONE_LINE2: { |
| int line_delta = code - 10; |
| info->lineno += line_delta; |
| info->end_lineno = info->lineno; |
| info->column = *(ptr++); |
| info->end_column = *(ptr++); |
| break; |
| } |
| default: { |
| uint8_t second_byte = *(ptr++); |
| if ((second_byte & 128) != 0) { |
| return false; |
| } |
| info->column = code << 3 | (second_byte >> 4); |
| info->end_column = info->column + (second_byte & 15); |
| break; |
| } |
| } |
| if (addr <= addrq && end_addr > addrq) { |
| return true; |
| } |
| addr = end_addr; |
| } |
| return false; |
| } |
| |
| /* ============================================================================ |
| * CODE OBJECT AND FRAME PARSING FUNCTIONS |
| * ============================================================================ */ |
| |
| static int |
| parse_code_object(RemoteUnwinderObject *unwinder, |
| PyObject **result, |
| uintptr_t address, |
| uintptr_t instruction_pointer, |
| uintptr_t *previous_frame, |
| int32_t tlbc_index) |
| { |
| void *key = (void *)address; |
| CachedCodeMetadata *meta = NULL; |
| PyObject *func = NULL; |
| PyObject *file = NULL; |
| PyObject *linetable = NULL; |
| PyObject *lineno = NULL; |
| PyObject *tuple = NULL; |
| |
| #ifdef Py_GIL_DISABLED |
| // In free threading builds, code object addresses might have the low bit set |
| // as a flag, so we need to mask it off to get the real address |
| uintptr_t real_address = address & (~1); |
| #else |
| uintptr_t real_address = address; |
| #endif |
| |
| if (unwinder && unwinder->code_object_cache != NULL) { |
| meta = _Py_hashtable_get(unwinder->code_object_cache, key); |
| } |
| |
| if (meta == NULL) { |
| char code_object[SIZEOF_CODE_OBJ]; |
| if (_Py_RemoteDebug_PagedReadRemoteMemory( |
| &unwinder->handle, real_address, SIZEOF_CODE_OBJ, code_object) < 0) |
| { |
| set_exception_cause(unwinder, PyExc_RuntimeError, "Failed to read code object"); |
| goto error; |
| } |
| |
| func = read_py_str(unwinder, |
| GET_MEMBER(uintptr_t, code_object, unwinder->debug_offsets.code_object.qualname), 1024); |
| if (!func) { |
| set_exception_cause(unwinder, PyExc_RuntimeError, "Failed to read function name from code object"); |
| goto error; |
| } |
| |
| file = read_py_str(unwinder, |
| GET_MEMBER(uintptr_t, code_object, unwinder->debug_offsets.code_object.filename), 1024); |
| if (!file) { |
| set_exception_cause(unwinder, PyExc_RuntimeError, "Failed to read filename from code object"); |
| goto error; |
| } |
| |
| linetable = read_py_bytes(unwinder, |
| GET_MEMBER(uintptr_t, code_object, unwinder->debug_offsets.code_object.linetable), 4096); |
| if (!linetable) { |
| set_exception_cause(unwinder, PyExc_RuntimeError, "Failed to read linetable from code object"); |
| goto error; |
| } |
| |
| meta = PyMem_RawMalloc(sizeof(CachedCodeMetadata)); |
| if (!meta) { |
| PyErr_NoMemory(); |
| set_exception_cause(unwinder, PyExc_MemoryError, "Failed to allocate cached code metadata"); |
| goto error; |
| } |
| |
| meta->func_name = func; |
| meta->file_name = file; |
| meta->linetable = linetable; |
| meta->first_lineno = GET_MEMBER(int, code_object, unwinder->debug_offsets.code_object.firstlineno); |
| meta->addr_code_adaptive = real_address + unwinder->debug_offsets.code_object.co_code_adaptive; |
| |
| if (unwinder && unwinder->code_object_cache && _Py_hashtable_set(unwinder->code_object_cache, key, meta) < 0) { |
| cached_code_metadata_destroy(meta); |
| set_exception_cause(unwinder, PyExc_RuntimeError, "Failed to cache code metadata"); |
| goto error; |
| } |
| |
| // Ownership transferred to meta |
| func = NULL; |
| file = NULL; |
| linetable = NULL; |
| } |
| |
| uintptr_t ip = instruction_pointer; |
| ptrdiff_t addrq; |
| |
| #ifdef Py_GIL_DISABLED |
| // Handle thread-local bytecode (TLBC) in free threading builds |
| if (tlbc_index == 0 || unwinder->debug_offsets.code_object.co_tlbc == 0 || unwinder == NULL) { |
| // No TLBC or no unwinder - use main bytecode directly |
| addrq = (uint16_t *)ip - (uint16_t *)meta->addr_code_adaptive; |
| goto done_tlbc; |
| } |
| |
| // Try to get TLBC data from cache (we'll get generation from the caller) |
| TLBCCacheEntry *tlbc_entry = get_tlbc_cache_entry(unwinder, real_address, unwinder->tlbc_generation); |
| |
| if (!tlbc_entry) { |
| // Cache miss - try to read and cache TLBC array |
| if (!cache_tlbc_array(unwinder, real_address, real_address + unwinder->debug_offsets.code_object.co_tlbc, unwinder->tlbc_generation)) { |
| set_exception_cause(unwinder, PyExc_RuntimeError, "Failed to cache TLBC array"); |
| goto error; |
| } |
| tlbc_entry = get_tlbc_cache_entry(unwinder, real_address, unwinder->tlbc_generation); |
| } |
| |
| if (tlbc_entry && tlbc_index < tlbc_entry->tlbc_array_size) { |
| // Use cached TLBC data |
| uintptr_t *entries = (uintptr_t *)((char *)tlbc_entry->tlbc_array + sizeof(Py_ssize_t)); |
| uintptr_t tlbc_bytecode_addr = entries[tlbc_index]; |
| |
| if (tlbc_bytecode_addr != 0) { |
| // Calculate offset from TLBC bytecode |
| addrq = (uint16_t *)ip - (uint16_t *)tlbc_bytecode_addr; |
| goto done_tlbc; |
| } |
| } |
| |
| // Fall back to main bytecode |
| addrq = (uint16_t *)ip - (uint16_t *)meta->addr_code_adaptive; |
| |
| done_tlbc: |
| #else |
| // Non-free-threaded build, always use the main bytecode |
| (void)tlbc_index; // Suppress unused parameter warning |
| (void)unwinder; // Suppress unused parameter warning |
| addrq = (uint16_t *)ip - (uint16_t *)meta->addr_code_adaptive; |
| #endif |
| ; // Empty statement to avoid C23 extension warning |
| LocationInfo info = {0}; |
| bool ok = parse_linetable(addrq, PyBytes_AS_STRING(meta->linetable), |
| meta->first_lineno, &info); |
| if (!ok) { |
| info.lineno = -1; |
| } |
| |
| lineno = PyLong_FromLong(info.lineno); |
| if (!lineno) { |
| set_exception_cause(unwinder, PyExc_RuntimeError, "Failed to create line number object"); |
| goto error; |
| } |
| |
| RemoteDebuggingState *state = RemoteDebugging_GetStateFromObject((PyObject*)unwinder); |
| tuple = PyStructSequence_New(state->FrameInfo_Type); |
| if (!tuple) { |
| set_exception_cause(unwinder, PyExc_MemoryError, "Failed to create FrameInfo for code object"); |
| goto error; |
| } |
| |
| Py_INCREF(meta->func_name); |
| Py_INCREF(meta->file_name); |
| PyStructSequence_SetItem(tuple, 0, meta->file_name); |
| PyStructSequence_SetItem(tuple, 1, lineno); |
| PyStructSequence_SetItem(tuple, 2, meta->func_name); |
| |
| *result = tuple; |
| return 0; |
| |
| error: |
| Py_XDECREF(func); |
| Py_XDECREF(file); |
| Py_XDECREF(linetable); |
| Py_XDECREF(lineno); |
| Py_XDECREF(tuple); |
| return -1; |
| } |
| |
| /* ============================================================================ |
| * STACK CHUNK MANAGEMENT FUNCTIONS |
| * ============================================================================ */ |
| |
| static void |
| cleanup_stack_chunks(StackChunkList *chunks) |
| { |
| for (size_t i = 0; i < chunks->count; ++i) { |
| PyMem_RawFree(chunks->chunks[i].local_copy); |
| } |
| PyMem_RawFree(chunks->chunks); |
| } |
| |
| static int |
| process_single_stack_chunk( |
| RemoteUnwinderObject *unwinder, |
| uintptr_t chunk_addr, |
| StackChunkInfo *chunk_info |
| ) { |
| // Start with default size assumption |
| size_t current_size = _PY_DATA_STACK_CHUNK_SIZE; |
| |
| char *this_chunk = PyMem_RawMalloc(current_size); |
| if (!this_chunk) { |
| PyErr_NoMemory(); |
| set_exception_cause(unwinder, PyExc_MemoryError, "Failed to allocate stack chunk buffer"); |
| return -1; |
| } |
| |
| if (_Py_RemoteDebug_PagedReadRemoteMemory(&unwinder->handle, chunk_addr, current_size, this_chunk) < 0) { |
| PyMem_RawFree(this_chunk); |
| set_exception_cause(unwinder, PyExc_RuntimeError, "Failed to read stack chunk"); |
| return -1; |
| } |
| |
| // Check actual size and reread if necessary |
| size_t actual_size = GET_MEMBER(size_t, this_chunk, offsetof(_PyStackChunk, size)); |
| if (actual_size != current_size) { |
| this_chunk = PyMem_RawRealloc(this_chunk, actual_size); |
| if (!this_chunk) { |
| PyErr_NoMemory(); |
| set_exception_cause(unwinder, PyExc_MemoryError, "Failed to reallocate stack chunk buffer"); |
| return -1; |
| } |
| |
| if (_Py_RemoteDebug_PagedReadRemoteMemory(&unwinder->handle, chunk_addr, actual_size, this_chunk) < 0) { |
| PyMem_RawFree(this_chunk); |
| set_exception_cause(unwinder, PyExc_RuntimeError, "Failed to reread stack chunk with correct size"); |
| return -1; |
| } |
| current_size = actual_size; |
| } |
| |
| chunk_info->remote_addr = chunk_addr; |
| chunk_info->size = current_size; |
| chunk_info->local_copy = this_chunk; |
| return 0; |
| } |
| |
| static int |
| copy_stack_chunks(RemoteUnwinderObject *unwinder, |
| uintptr_t tstate_addr, |
| StackChunkList *out_chunks) |
| { |
| uintptr_t chunk_addr; |
| StackChunkInfo *chunks = NULL; |
| size_t count = 0; |
| size_t max_chunks = 16; |
| |
| if (read_ptr(unwinder, tstate_addr + unwinder->debug_offsets.thread_state.datastack_chunk, &chunk_addr)) { |
| set_exception_cause(unwinder, PyExc_RuntimeError, "Failed to read initial stack chunk address"); |
| return -1; |
| } |
| |
| chunks = PyMem_RawMalloc(max_chunks * sizeof(StackChunkInfo)); |
| if (!chunks) { |
| PyErr_NoMemory(); |
| set_exception_cause(unwinder, PyExc_MemoryError, "Failed to allocate stack chunks array"); |
| return -1; |
| } |
| |
| while (chunk_addr != 0) { |
| // Grow array if needed |
| if (count >= max_chunks) { |
| max_chunks *= 2; |
| StackChunkInfo *new_chunks = PyMem_RawRealloc(chunks, max_chunks * sizeof(StackChunkInfo)); |
| if (!new_chunks) { |
| PyErr_NoMemory(); |
| set_exception_cause(unwinder, PyExc_MemoryError, "Failed to grow stack chunks array"); |
| goto error; |
| } |
| chunks = new_chunks; |
| } |
| |
| // Process this chunk |
| if (process_single_stack_chunk(unwinder, chunk_addr, &chunks[count]) < 0) { |
| set_exception_cause(unwinder, PyExc_RuntimeError, "Failed to process stack chunk"); |
| goto error; |
| } |
| |
| // Get next chunk address and increment count |
| chunk_addr = GET_MEMBER(uintptr_t, chunks[count].local_copy, offsetof(_PyStackChunk, previous)); |
| count++; |
| } |
| |
| out_chunks->chunks = chunks; |
| out_chunks->count = count; |
| return 0; |
| |
| error: |
| for (size_t i = 0; i < count; ++i) { |
| PyMem_RawFree(chunks[i].local_copy); |
| } |
| PyMem_RawFree(chunks); |
| return -1; |
| } |
| |
| static void * |
| find_frame_in_chunks(StackChunkList *chunks, uintptr_t remote_ptr) |
| { |
| for (size_t i = 0; i < chunks->count; ++i) { |
| uintptr_t base = chunks->chunks[i].remote_addr + offsetof(_PyStackChunk, data); |
| size_t payload = chunks->chunks[i].size - offsetof(_PyStackChunk, data); |
| |
| if (remote_ptr >= base && remote_ptr < base + payload) { |
| return (char *)chunks->chunks[i].local_copy + (remote_ptr - chunks->chunks[i].remote_addr); |
| } |
| } |
| return NULL; |
| } |
| |
| static int |
| parse_frame_from_chunks( |
| RemoteUnwinderObject *unwinder, |
| PyObject **result, |
| uintptr_t address, |
| uintptr_t *previous_frame, |
| StackChunkList *chunks |
| ) { |
| void *frame_ptr = find_frame_in_chunks(chunks, address); |
| if (!frame_ptr) { |
| set_exception_cause(unwinder, PyExc_RuntimeError, "Frame not found in stack chunks"); |
| return -1; |
| } |
| |
| char *frame = (char *)frame_ptr; |
| *previous_frame = GET_MEMBER(uintptr_t, frame, unwinder->debug_offsets.interpreter_frame.previous); |
| uintptr_t code_object = GET_MEMBER_NO_TAG(uintptr_t, frame_ptr, unwinder->debug_offsets.interpreter_frame.executable); |
| int frame_valid = is_frame_valid(unwinder, (uintptr_t)frame, code_object); |
| if (frame_valid != 1) { |
| return frame_valid; |
| } |
| |
| uintptr_t instruction_pointer = GET_MEMBER(uintptr_t, frame, unwinder->debug_offsets.interpreter_frame.instr_ptr); |
| |
| // Get tlbc_index for free threading builds |
| int32_t tlbc_index = 0; |
| #ifdef Py_GIL_DISABLED |
| if (unwinder->debug_offsets.interpreter_frame.tlbc_index != 0) { |
| tlbc_index = GET_MEMBER(int32_t, frame, unwinder->debug_offsets.interpreter_frame.tlbc_index); |
| } |
| #endif |
| |
| return parse_code_object(unwinder, result, code_object, instruction_pointer, previous_frame, tlbc_index); |
| } |
| |
| /* ============================================================================ |
| * INTERPRETER STATE AND THREAD DISCOVERY FUNCTIONS |
| * ============================================================================ */ |
| |
| static int |
| populate_initial_state_data( |
| int all_threads, |
| RemoteUnwinderObject *unwinder, |
| uintptr_t runtime_start_address, |
| uintptr_t *interpreter_state, |
| uintptr_t *tstate |
| ) { |
| uint64_t interpreter_state_list_head = |
| unwinder->debug_offsets.runtime_state.interpreters_head; |
| |
| uintptr_t address_of_interpreter_state; |
| int bytes_read = _Py_RemoteDebug_PagedReadRemoteMemory( |
| &unwinder->handle, |
| runtime_start_address + interpreter_state_list_head, |
| sizeof(void*), |
| &address_of_interpreter_state); |
| if (bytes_read < 0) { |
| set_exception_cause(unwinder, PyExc_RuntimeError, "Failed to read interpreter state address"); |
| return -1; |
| } |
| |
| if (address_of_interpreter_state == 0) { |
| PyErr_SetString(PyExc_RuntimeError, "No interpreter state found"); |
| set_exception_cause(unwinder, PyExc_RuntimeError, "Interpreter state is NULL"); |
| return -1; |
| } |
| |
| *interpreter_state = address_of_interpreter_state; |
| |
| if (all_threads) { |
| *tstate = 0; |
| return 0; |
| } |
| |
| uintptr_t address_of_thread = address_of_interpreter_state + |
| unwinder->debug_offsets.interpreter_state.threads_main; |
| |
| if (_Py_RemoteDebug_PagedReadRemoteMemory( |
| &unwinder->handle, |
| address_of_thread, |
| sizeof(void*), |
| tstate) < 0) { |
| set_exception_cause(unwinder, PyExc_RuntimeError, "Failed to read main thread state address"); |
| return -1; |
| } |
| |
| return 0; |
| } |
| |
| |
| static int |
| find_running_frame( |
| RemoteUnwinderObject *unwinder, |
| uintptr_t address_of_thread, |
| uintptr_t *frame |
| ) { |
| if ((void*)address_of_thread != NULL) { |
| int err = read_ptr( |
| unwinder, |
| address_of_thread + unwinder->debug_offsets.thread_state.current_frame, |
| frame); |
| if (err) { |
| set_exception_cause(unwinder, PyExc_RuntimeError, "Failed to read current frame pointer"); |
| return -1; |
| } |
| return 0; |
| } |
| |
| *frame = (uintptr_t)NULL; |
| return 0; |
| } |
| |
| /* ============================================================================ |
| * FRAME PARSING FUNCTIONS |
| * ============================================================================ */ |
| |
| static inline int |
| is_frame_valid( |
| RemoteUnwinderObject *unwinder, |
| uintptr_t frame_addr, |
| uintptr_t code_object_addr |
| ) { |
| if ((void*)code_object_addr == NULL) { |
| return 0; |
| } |
| |
| void* frame = (void*)frame_addr; |
| |
| if (GET_MEMBER(char, frame, unwinder->debug_offsets.interpreter_frame.owner) == FRAME_OWNED_BY_CSTACK || |
| GET_MEMBER(char, frame, unwinder->debug_offsets.interpreter_frame.owner) == FRAME_OWNED_BY_INTERPRETER) { |
| return 0; // C frame |
| } |
| |
| if (GET_MEMBER(char, frame, unwinder->debug_offsets.interpreter_frame.owner) != FRAME_OWNED_BY_GENERATOR |
| && GET_MEMBER(char, frame, unwinder->debug_offsets.interpreter_frame.owner) != FRAME_OWNED_BY_THREAD) { |
| PyErr_Format(PyExc_RuntimeError, "Unhandled frame owner %d.\n", |
| GET_MEMBER(char, frame, unwinder->debug_offsets.interpreter_frame.owner)); |
| set_exception_cause(unwinder, PyExc_RuntimeError, "Unhandled frame owner type in async frame"); |
| return -1; |
| } |
| return 1; |
| } |
| |
| static int |
| parse_frame_object( |
| RemoteUnwinderObject *unwinder, |
| PyObject** result, |
| uintptr_t address, |
| uintptr_t* address_of_code_object, |
| uintptr_t* previous_frame |
| ) { |
| char frame[SIZEOF_INTERP_FRAME]; |
| *address_of_code_object = 0; |
| |
| Py_ssize_t bytes_read = _Py_RemoteDebug_PagedReadRemoteMemory( |
| &unwinder->handle, |
| address, |
| SIZEOF_INTERP_FRAME, |
| frame |
| ); |
| if (bytes_read < 0) { |
| set_exception_cause(unwinder, PyExc_RuntimeError, "Failed to read interpreter frame"); |
| return -1; |
| } |
| |
| *previous_frame = GET_MEMBER(uintptr_t, frame, unwinder->debug_offsets.interpreter_frame.previous); |
| uintptr_t code_object = GET_MEMBER_NO_TAG(uintptr_t, frame, unwinder->debug_offsets.interpreter_frame.executable); |
| int frame_valid = is_frame_valid(unwinder, (uintptr_t)frame, code_object); |
| if (frame_valid != 1) { |
| return frame_valid; |
| } |
| |
| uintptr_t instruction_pointer = GET_MEMBER(uintptr_t, frame, unwinder->debug_offsets.interpreter_frame.instr_ptr); |
| |
| // Get tlbc_index for free threading builds |
| int32_t tlbc_index = 0; |
| #ifdef Py_GIL_DISABLED |
| if (unwinder->debug_offsets.interpreter_frame.tlbc_index != 0) { |
| tlbc_index = GET_MEMBER(int32_t, frame, unwinder->debug_offsets.interpreter_frame.tlbc_index); |
| } |
| #endif |
| |
| *address_of_code_object = code_object; |
| return parse_code_object(unwinder, result, code_object, instruction_pointer, previous_frame, tlbc_index); |
| } |
| |
| static int |
| parse_async_frame_chain( |
| RemoteUnwinderObject *unwinder, |
| PyObject *calls, |
| uintptr_t address_of_thread, |
| uintptr_t running_task_code_obj |
| ) { |
| uintptr_t address_of_current_frame; |
| if (find_running_frame(unwinder, address_of_thread, &address_of_current_frame) < 0) { |
| set_exception_cause(unwinder, PyExc_RuntimeError, "Running frame search failed in async chain"); |
| return -1; |
| } |
| |
| while ((void*)address_of_current_frame != NULL) { |
| PyObject* frame_info = NULL; |
| uintptr_t address_of_code_object; |
| int res = parse_frame_object( |
| unwinder, |
| &frame_info, |
| address_of_current_frame, |
| &address_of_code_object, |
| &address_of_current_frame |
| ); |
| |
| if (res < 0) { |
| set_exception_cause(unwinder, PyExc_RuntimeError, "Async frame object parsing failed in chain"); |
| return -1; |
| } |
| |
| if (!frame_info) { |
| continue; |
| } |
| |
| if (PyList_Append(calls, frame_info) == -1) { |
| Py_DECREF(frame_info); |
| set_exception_cause(unwinder, PyExc_RuntimeError, "Failed to append frame info to async chain"); |
| return -1; |
| } |
| |
| Py_DECREF(frame_info); |
| |
| if (address_of_code_object == running_task_code_obj) { |
| break; |
| } |
| } |
| |
| return 0; |
| } |
| |
| /* ============================================================================ |
| * AWAITED BY PARSING FUNCTIONS |
| * ============================================================================ */ |
| |
| static int |
| append_awaited_by_for_thread( |
| RemoteUnwinderObject *unwinder, |
| uintptr_t head_addr, |
| PyObject *result |
| ) { |
| char task_node[SIZEOF_LLIST_NODE]; |
| |
| if (_Py_RemoteDebug_PagedReadRemoteMemory(&unwinder->handle, head_addr, |
| sizeof(task_node), task_node) < 0) { |
| set_exception_cause(unwinder, PyExc_RuntimeError, "Failed to read task node head"); |
| return -1; |
| } |
| |
| size_t iteration_count = 0; |
| const size_t MAX_ITERATIONS = 2 << 15; // A reasonable upper bound |
| |
| while (GET_MEMBER(uintptr_t, task_node, unwinder->debug_offsets.llist_node.next) != head_addr) { |
| if (++iteration_count > MAX_ITERATIONS) { |
| PyErr_SetString(PyExc_RuntimeError, "Task list appears corrupted"); |
| set_exception_cause(unwinder, PyExc_RuntimeError, "Task list iteration limit exceeded"); |
| return -1; |
| } |
| |
| if (GET_MEMBER(uintptr_t, task_node, unwinder->debug_offsets.llist_node.next) == 0) { |
| PyErr_SetString(PyExc_RuntimeError, |
| "Invalid linked list structure reading remote memory"); |
| set_exception_cause(unwinder, PyExc_RuntimeError, "NULL pointer in task linked list"); |
| return -1; |
| } |
| |
| uintptr_t task_addr = (uintptr_t)GET_MEMBER(uintptr_t, task_node, unwinder->debug_offsets.llist_node.next) |
| - unwinder->async_debug_offsets.asyncio_task_object.task_node; |
| |
| if (process_single_task_node(unwinder, task_addr, NULL, result) < 0) { |
| set_exception_cause(unwinder, PyExc_RuntimeError, "Failed to process task node in awaited_by"); |
| return -1; |
| } |
| |
| // Read next node |
| if (_Py_RemoteDebug_PagedReadRemoteMemory( |
| &unwinder->handle, |
| (uintptr_t)GET_MEMBER(uintptr_t, task_node, unwinder->debug_offsets.llist_node.next), |
| sizeof(task_node), |
| task_node) < 0) { |
| set_exception_cause(unwinder, PyExc_RuntimeError, "Failed to read next task node in awaited_by"); |
| return -1; |
| } |
| } |
| |
| return 0; |
| } |
| |
| static int |
| append_awaited_by( |
| RemoteUnwinderObject *unwinder, |
| unsigned long tid, |
| uintptr_t head_addr, |
| PyObject *result) |
| { |
| PyObject *tid_py = PyLong_FromUnsignedLong(tid); |
| if (tid_py == NULL) { |
| set_exception_cause(unwinder, PyExc_RuntimeError, "Failed to create thread ID object"); |
| return -1; |
| } |
| |
| PyObject* awaited_by_for_thread = PyList_New(0); |
| if (awaited_by_for_thread == NULL) { |
| Py_DECREF(tid_py); |
| set_exception_cause(unwinder, PyExc_MemoryError, "Failed to create awaited_by thread list"); |
| return -1; |
| } |
| |
| RemoteDebuggingState *state = RemoteDebugging_GetStateFromObject((PyObject*)unwinder); |
| PyObject *result_item = PyStructSequence_New(state->AwaitedInfo_Type); |
| if (result_item == NULL) { |
| Py_DECREF(tid_py); |
| Py_DECREF(awaited_by_for_thread); |
| set_exception_cause(unwinder, PyExc_MemoryError, "Failed to create AwaitedInfo"); |
| return -1; |
| } |
| |
| PyStructSequence_SetItem(result_item, 0, tid_py); // steals ref |
| PyStructSequence_SetItem(result_item, 1, awaited_by_for_thread); // steals ref |
| if (PyList_Append(result, result_item)) { |
| Py_DECREF(result_item); |
| set_exception_cause(unwinder, PyExc_RuntimeError, "Failed to append awaited_by result item"); |
| return -1; |
| } |
| Py_DECREF(result_item); |
| |
| if (append_awaited_by_for_thread(unwinder, head_addr, awaited_by_for_thread)) |
| { |
| set_exception_cause(unwinder, PyExc_RuntimeError, "Failed to append awaited_by for thread"); |
| return -1; |
| } |
| |
| return 0; |
| } |
| |
| /* ============================================================================ |
| * STACK UNWINDING FUNCTIONS |
| * ============================================================================ */ |
| |
| static int |
| process_frame_chain( |
| RemoteUnwinderObject *unwinder, |
| uintptr_t initial_frame_addr, |
| StackChunkList *chunks, |
| PyObject *frame_info |
| ) { |
| uintptr_t frame_addr = initial_frame_addr; |
| uintptr_t prev_frame_addr = 0; |
| const size_t MAX_FRAMES = 1024; |
| size_t frame_count = 0; |
| |
| while ((void*)frame_addr != NULL) { |
| PyObject *frame = NULL; |
| uintptr_t next_frame_addr = 0; |
| |
| if (++frame_count > MAX_FRAMES) { |
| PyErr_SetString(PyExc_RuntimeError, "Too many stack frames (possible infinite loop)"); |
| set_exception_cause(unwinder, PyExc_RuntimeError, "Frame chain iteration limit exceeded"); |
| return -1; |
| } |
| |
| // Try chunks first, fallback to direct memory read |
| if (parse_frame_from_chunks(unwinder, &frame, frame_addr, &next_frame_addr, chunks) < 0) { |
| PyErr_Clear(); |
| uintptr_t address_of_code_object = 0; |
| if (parse_frame_object(unwinder, &frame, frame_addr, &address_of_code_object ,&next_frame_addr) < 0) { |
| set_exception_cause(unwinder, PyExc_RuntimeError, "Failed to parse frame object in chain"); |
| return -1; |
| } |
| } |
| |
| if (!frame) { |
| break; |
| } |
| |
| if (prev_frame_addr && frame_addr != prev_frame_addr) { |
| PyErr_Format(PyExc_RuntimeError, |
| "Broken frame chain: expected frame at 0x%lx, got 0x%lx", |
| prev_frame_addr, frame_addr); |
| Py_DECREF(frame); |
| set_exception_cause(unwinder, PyExc_RuntimeError, "Frame chain consistency check failed"); |
| return -1; |
| } |
| |
| if (PyList_Append(frame_info, frame) == -1) { |
| Py_DECREF(frame); |
| set_exception_cause(unwinder, PyExc_RuntimeError, "Failed to append frame to frame info list"); |
| return -1; |
| } |
| Py_DECREF(frame); |
| |
| prev_frame_addr = next_frame_addr; |
| frame_addr = next_frame_addr; |
| } |
| |
| return 0; |
| } |
| |
| static PyObject* |
| unwind_stack_for_thread( |
| RemoteUnwinderObject *unwinder, |
| uintptr_t *current_tstate |
| ) { |
| PyObject *frame_info = NULL; |
| PyObject *thread_id = NULL; |
| PyObject *result = NULL; |
| StackChunkList chunks = {0}; |
| |
| char ts[SIZEOF_THREAD_STATE]; |
| int bytes_read = _Py_RemoteDebug_PagedReadRemoteMemory( |
| &unwinder->handle, *current_tstate, unwinder->debug_offsets.thread_state.size, ts); |
| if (bytes_read < 0) { |
| set_exception_cause(unwinder, PyExc_RuntimeError, "Failed to read thread state"); |
| goto error; |
| } |
| |
| uintptr_t frame_addr = GET_MEMBER(uintptr_t, ts, unwinder->debug_offsets.thread_state.current_frame); |
| |
| frame_info = PyList_New(0); |
| if (!frame_info) { |
| set_exception_cause(unwinder, PyExc_MemoryError, "Failed to create frame info list"); |
| goto error; |
| } |
| |
| if (copy_stack_chunks(unwinder, *current_tstate, &chunks) < 0) { |
| set_exception_cause(unwinder, PyExc_RuntimeError, "Failed to copy stack chunks"); |
| goto error; |
| } |
| |
| if (process_frame_chain(unwinder, frame_addr, &chunks, frame_info) < 0) { |
| set_exception_cause(unwinder, PyExc_RuntimeError, "Failed to process frame chain"); |
| goto error; |
| } |
| |
| *current_tstate = GET_MEMBER(uintptr_t, ts, unwinder->debug_offsets.thread_state.next); |
| |
| thread_id = PyLong_FromLongLong( |
| GET_MEMBER(long, ts, unwinder->debug_offsets.thread_state.native_thread_id)); |
| if (thread_id == NULL) { |
| set_exception_cause(unwinder, PyExc_RuntimeError, "Failed to create thread ID"); |
| goto error; |
| } |
| |
| RemoteDebuggingState *state = RemoteDebugging_GetStateFromObject((PyObject*)unwinder); |
| result = PyStructSequence_New(state->ThreadInfo_Type); |
| if (result == NULL) { |
| set_exception_cause(unwinder, PyExc_MemoryError, "Failed to create ThreadInfo"); |
| goto error; |
| } |
| |
| PyStructSequence_SetItem(result, 0, thread_id); // Steals reference |
| PyStructSequence_SetItem(result, 1, frame_info); // Steals reference |
| |
| cleanup_stack_chunks(&chunks); |
| return result; |
| |
| error: |
| Py_XDECREF(frame_info); |
| Py_XDECREF(thread_id); |
| Py_XDECREF(result); |
| cleanup_stack_chunks(&chunks); |
| return NULL; |
| } |
| |
| |
| /* ============================================================================ |
| * REMOTEUNWINDER CLASS IMPLEMENTATION |
| * ============================================================================ */ |
| |
| /*[clinic input] |
| class _remote_debugging.RemoteUnwinder "RemoteUnwinderObject *" "&RemoteUnwinder_Type" |
| [clinic start generated code]*/ |
| /*[clinic end generated code: output=da39a3ee5e6b4b0d input=55f164d8803318be]*/ |
| |
| /*[clinic input] |
| @permit_long_summary |
| @permit_long_docstring_body |
| _remote_debugging.RemoteUnwinder.__init__ |
| pid: int |
| * |
| all_threads: bool = False |
| only_active_thread: bool = False |
| debug: bool = False |
| |
| Initialize a new RemoteUnwinder object for debugging a remote Python process. |
| |
| Args: |
| pid: Process ID of the target Python process to debug |
| all_threads: If True, initialize state for all threads in the process. |
| If False, only initialize for the main thread. |
| only_active_thread: If True, only sample the thread holding the GIL. |
| Cannot be used together with all_threads=True. |
| debug: If True, chain exceptions to explain the sequence of events that |
| lead to the exception. |
| |
| The RemoteUnwinder provides functionality to inspect and debug a running Python |
| process, including examining thread states, stack frames and other runtime data. |
| |
| Raises: |
| PermissionError: If access to the target process is denied |
| OSError: If unable to attach to the target process or access its memory |
| RuntimeError: If unable to read debug information from the target process |
| ValueError: If both all_threads and only_active_thread are True |
| [clinic start generated code]*/ |
| |
| static int |
| _remote_debugging_RemoteUnwinder___init___impl(RemoteUnwinderObject *self, |
| int pid, int all_threads, |
| int only_active_thread, |
| int debug) |
| /*[clinic end generated code: output=13ba77598ecdcbe1 input=cfc21663fbe263c4]*/ |
| { |
| // Validate that all_threads and only_active_thread are not both True |
| if (all_threads && only_active_thread) { |
| PyErr_SetString(PyExc_ValueError, |
| "all_threads and only_active_thread cannot both be True"); |
| return -1; |
| } |
| |
| #ifdef Py_GIL_DISABLED |
| if (only_active_thread) { |
| PyErr_SetString(PyExc_ValueError, |
| "only_active_thread is not supported when Py_GIL_DISABLED is not defined"); |
| return -1; |
| } |
| #endif |
| |
| self->debug = debug; |
| self->only_active_thread = only_active_thread; |
| self->cached_state = NULL; |
| if (_Py_RemoteDebug_InitProcHandle(&self->handle, pid) < 0) { |
| set_exception_cause(self, PyExc_RuntimeError, "Failed to initialize process handle"); |
| return -1; |
| } |
| |
| self->runtime_start_address = _Py_RemoteDebug_GetPyRuntimeAddress(&self->handle); |
| if (self->runtime_start_address == 0) { |
| set_exception_cause(self, PyExc_RuntimeError, "Failed to get Python runtime address"); |
| return -1; |
| } |
| |
| if (_Py_RemoteDebug_ReadDebugOffsets(&self->handle, |
| &self->runtime_start_address, |
| &self->debug_offsets) < 0) |
| { |
| set_exception_cause(self, PyExc_RuntimeError, "Failed to read debug offsets"); |
| return -1; |
| } |
| |
| // Validate that the debug offsets are valid |
| if(validate_debug_offsets(&self->debug_offsets) == -1) { |
| set_exception_cause(self, PyExc_RuntimeError, "Invalid debug offsets found"); |
| return -1; |
| } |
| |
| // Try to read async debug offsets, but don't fail if they're not available |
| self->async_debug_offsets_available = 1; |
| if (read_async_debug(self) < 0) { |
| PyErr_Clear(); |
| memset(&self->async_debug_offsets, 0, sizeof(self->async_debug_offsets)); |
| self->async_debug_offsets_available = 0; |
| } |
| |
| if (populate_initial_state_data(all_threads, self, self->runtime_start_address, |
| &self->interpreter_addr ,&self->tstate_addr) < 0) |
| { |
| set_exception_cause(self, PyExc_RuntimeError, "Failed to populate initial state data"); |
| return -1; |
| } |
| |
| self->code_object_cache = _Py_hashtable_new_full( |
| _Py_hashtable_hash_ptr, |
| _Py_hashtable_compare_direct, |
| NULL, // keys are stable pointers, don't destroy |
| cached_code_metadata_destroy, |
| NULL |
| ); |
| if (self->code_object_cache == NULL) { |
| PyErr_NoMemory(); |
| set_exception_cause(self, PyExc_MemoryError, "Failed to create code object cache"); |
| return -1; |
| } |
| |
| #ifdef Py_GIL_DISABLED |
| // Initialize TLBC cache |
| self->tlbc_generation = 0; |
| self->tlbc_cache = _Py_hashtable_new_full( |
| _Py_hashtable_hash_ptr, |
| _Py_hashtable_compare_direct, |
| NULL, // keys are stable pointers, don't destroy |
| tlbc_cache_entry_destroy, |
| NULL |
| ); |
| if (self->tlbc_cache == NULL) { |
| _Py_hashtable_destroy(self->code_object_cache); |
| PyErr_NoMemory(); |
| set_exception_cause(self, PyExc_MemoryError, "Failed to create TLBC cache"); |
| return -1; |
| } |
| #endif |
| |
| return 0; |
| } |
| |
| /*[clinic input] |
| @permit_long_docstring_body |
| @critical_section |
| _remote_debugging.RemoteUnwinder.get_stack_trace |
| |
| Returns stack traces for all interpreters and threads in process. |
| |
| Each element in the returned list is a tuple of (interpreter_id, thread_list), where: |
| - interpreter_id is the interpreter identifier |
| - thread_list is a list of tuples (thread_id, frame_list) for threads in that interpreter |
| - thread_id is the OS thread identifier |
| - frame_list is a list of tuples (function_name, filename, line_number) representing |
| the Python stack frames for that thread, ordered from most recent to oldest |
| |
| The threads returned depend on the initialization parameters: |
| - If only_active_thread was True: returns only the thread holding the GIL across all interpreters |
| - If all_threads was True: returns all threads across all interpreters |
| - Otherwise: returns only the main thread of each interpreter |
| |
| Example: |
| [ |
| (0, [ # Main interpreter |
| (1234, [ |
| ('process_data', 'worker.py', 127), |
| ('run_worker', 'worker.py', 45), |
| ('main', 'app.py', 23) |
| ]), |
| (1235, [ |
| ('handle_request', 'server.py', 89), |
| ('serve_forever', 'server.py', 52) |
| ]) |
| ]), |
| (1, [ # Sub-interpreter |
| (1236, [ |
| ('sub_worker', 'sub.py', 15) |
| ]) |
| ]) |
| ] |
| |
| Raises: |
| RuntimeError: If there is an error copying memory from the target process |
| OSError: If there is an error accessing the target process |
| PermissionError: If access to the target process is denied |
| UnicodeDecodeError: If there is an error decoding strings from the target process |
| |
| [clinic start generated code]*/ |
| |
| static PyObject * |
| _remote_debugging_RemoteUnwinder_get_stack_trace_impl(RemoteUnwinderObject *self) |
| /*[clinic end generated code: output=666192b90c69d567 input=bcff01c73cccc1c0]*/ |
| { |
| PyObject* result = PyList_New(0); |
| if (!result) { |
| set_exception_cause(self, PyExc_MemoryError, "Failed to create stack trace result list"); |
| return NULL; |
| } |
| |
| // Iterate over all interpreters |
| uintptr_t current_interpreter = self->interpreter_addr; |
| while (current_interpreter != 0) { |
| // Read interpreter state to get the interpreter ID |
| char interp_state_buffer[INTERP_STATE_BUFFER_SIZE]; |
| if (_Py_RemoteDebug_PagedReadRemoteMemory( |
| &self->handle, |
| current_interpreter, |
| INTERP_STATE_BUFFER_SIZE, |
| interp_state_buffer) < 0) { |
| set_exception_cause(self, PyExc_RuntimeError, "Failed to read interpreter state buffer"); |
| Py_CLEAR(result); |
| goto exit; |
| } |
| |
| int64_t interpreter_id = GET_MEMBER(int64_t, interp_state_buffer, |
| self->debug_offsets.interpreter_state.id); |
| |
| // Get code object generation from buffer |
| uint64_t code_object_generation = GET_MEMBER(uint64_t, interp_state_buffer, |
| self->debug_offsets.interpreter_state.code_object_generation); |
| |
| if (code_object_generation != self->code_object_generation) { |
| self->code_object_generation = code_object_generation; |
| _Py_hashtable_clear(self->code_object_cache); |
| } |
| |
| #ifdef Py_GIL_DISABLED |
| // Check TLBC generation and invalidate cache if needed |
| uint32_t current_tlbc_generation = GET_MEMBER(uint32_t, interp_state_buffer, |
| self->debug_offsets.interpreter_state.tlbc_generation); |
| if (current_tlbc_generation != self->tlbc_generation) { |
| self->tlbc_generation = current_tlbc_generation; |
| _Py_hashtable_clear(self->tlbc_cache); |
| } |
| #endif |
| |
| // Create a list to hold threads for this interpreter |
| PyObject *interpreter_threads = PyList_New(0); |
| if (!interpreter_threads) { |
| set_exception_cause(self, PyExc_MemoryError, "Failed to create interpreter threads list"); |
| Py_CLEAR(result); |
| goto exit; |
| } |
| |
| uintptr_t current_tstate; |
| if (self->only_active_thread) { |
| // Find the GIL holder for THIS interpreter |
| int gil_locked = GET_MEMBER(int, interp_state_buffer, |
| self->debug_offsets.interpreter_state.gil_runtime_state_locked); |
| |
| if (!gil_locked) { |
| // This interpreter's GIL is not locked, skip it |
| Py_DECREF(interpreter_threads); |
| goto next_interpreter; |
| } |
| |
| // Get the GIL holder for this interpreter |
| current_tstate = (uintptr_t)GET_MEMBER(PyThreadState*, interp_state_buffer, |
| self->debug_offsets.interpreter_state.gil_runtime_state_holder); |
| } else if (self->tstate_addr == 0) { |
| // Get all threads for this interpreter |
| current_tstate = GET_MEMBER(uintptr_t, interp_state_buffer, |
| self->debug_offsets.interpreter_state.threads_head); |
| } else { |
| // Target specific thread (only process first interpreter) |
| current_tstate = self->tstate_addr; |
| } |
| |
| while (current_tstate != 0) { |
| PyObject* frame_info = unwind_stack_for_thread(self, ¤t_tstate); |
| if (!frame_info) { |
| Py_DECREF(interpreter_threads); |
| set_exception_cause(self, PyExc_RuntimeError, "Failed to unwind stack for thread"); |
| Py_CLEAR(result); |
| goto exit; |
| } |
| |
| if (PyList_Append(interpreter_threads, frame_info) == -1) { |
| Py_DECREF(frame_info); |
| Py_DECREF(interpreter_threads); |
| set_exception_cause(self, PyExc_RuntimeError, "Failed to append thread frame info"); |
| Py_CLEAR(result); |
| goto exit; |
| } |
| Py_DECREF(frame_info); |
| |
| // If targeting specific thread or only active thread, process just one |
| if (self->tstate_addr || self->only_active_thread) { |
| break; |
| } |
| } |
| |
| // Create the InterpreterInfo StructSequence |
| RemoteDebuggingState *state = RemoteDebugging_GetStateFromObject((PyObject*)self); |
| PyObject *interpreter_info = PyStructSequence_New(state->InterpreterInfo_Type); |
| if (!interpreter_info) { |
| Py_DECREF(interpreter_threads); |
| set_exception_cause(self, PyExc_MemoryError, "Failed to create InterpreterInfo"); |
| Py_CLEAR(result); |
| goto exit; |
| } |
| |
| PyObject *interp_id = PyLong_FromLongLong(interpreter_id); |
| if (!interp_id) { |
| Py_DECREF(interpreter_threads); |
| Py_DECREF(interpreter_info); |
| set_exception_cause(self, PyExc_MemoryError, "Failed to create interpreter ID"); |
| Py_CLEAR(result); |
| goto exit; |
| } |
| |
| PyStructSequence_SetItem(interpreter_info, 0, interp_id); // steals reference |
| PyStructSequence_SetItem(interpreter_info, 1, interpreter_threads); // steals reference |
| |
| // Add this interpreter to the result list |
| if (PyList_Append(result, interpreter_info) == -1) { |
| Py_DECREF(interpreter_info); |
| set_exception_cause(self, PyExc_RuntimeError, "Failed to append interpreter info"); |
| Py_CLEAR(result); |
| goto exit; |
| } |
| Py_DECREF(interpreter_info); |
| |
| next_interpreter: |
| |
| // Get the next interpreter address |
| current_interpreter = GET_MEMBER(uintptr_t, interp_state_buffer, |
| self->debug_offsets.interpreter_state.next); |
| |
| // If we're targeting a specific thread, stop after first interpreter |
| if (self->tstate_addr != 0) { |
| break; |
| } |
| } |
| |
| exit: |
| _Py_RemoteDebug_ClearCache(&self->handle); |
| return result; |
| } |
| |
| /*[clinic input] |
| @permit_long_summary |
| @permit_long_docstring_body |
| @critical_section |
| _remote_debugging.RemoteUnwinder.get_all_awaited_by |
| |
| Get all tasks and their awaited_by relationships from the remote process. |
| |
| This provides a tree structure showing which tasks are waiting for other tasks. |
| |
| For each task, returns: |
| 1. The call stack frames leading to where the task is currently executing |
| 2. The name of the task |
| 3. A list of tasks that this task is waiting for, with their own frames/names/etc |
| |
| Returns a list of [frames, task_name, subtasks] where: |
| - frames: List of (func_name, filename, lineno) showing the call stack |
| - task_name: String identifier for the task |
| - subtasks: List of tasks being awaited by this task, in same format |
| |
| Raises: |
| RuntimeError: If AsyncioDebug section is not available in the remote process |
| MemoryError: If memory allocation fails |
| OSError: If reading from the remote process fails |
| |
| Example output: |
| [ |
| # Task c2_root waiting for two subtasks |
| [ |
| # Call stack of c2_root |
| [("c5", "script.py", 10), ("c4", "script.py", 14)], |
| "c2_root", |
| [ |
| # First subtask (sub_main_2) and what it's waiting for |
| [ |
| [("c1", "script.py", 23)], |
| "sub_main_2", |
| [...] |
| ], |
| # Second subtask and its waiters |
| [...] |
| ] |
| ] |
| ] |
| [clinic start generated code]*/ |
| |
| static PyObject * |
| _remote_debugging_RemoteUnwinder_get_all_awaited_by_impl(RemoteUnwinderObject *self) |
| /*[clinic end generated code: output=6a49cd345e8aec53 input=307f754cbe38250c]*/ |
| { |
| if (!self->async_debug_offsets_available) { |
| PyErr_SetString(PyExc_RuntimeError, "AsyncioDebug section not available"); |
| set_exception_cause(self, PyExc_RuntimeError, "AsyncioDebug section unavailable in get_all_awaited_by"); |
| return NULL; |
| } |
| |
| PyObject *result = PyList_New(0); |
| if (result == NULL) { |
| set_exception_cause(self, PyExc_MemoryError, "Failed to create awaited_by result list"); |
| goto result_err; |
| } |
| |
| // Process all threads |
| if (iterate_threads(self, process_thread_for_awaited_by, result) < 0) { |
| goto result_err; |
| } |
| |
| uintptr_t head_addr = self->interpreter_addr |
| + self->async_debug_offsets.asyncio_interpreter_state.asyncio_tasks_head; |
| |
| // On top of a per-thread task lists used by default by asyncio to avoid |
| // contention, there is also a fallback per-interpreter list of tasks; |
| // any tasks still pending when a thread is destroyed will be moved to the |
| // per-interpreter task list. It's unlikely we'll find anything here, but |
| // interesting for debugging. |
| if (append_awaited_by(self, 0, head_addr, result)) |
| { |
| set_exception_cause(self, PyExc_RuntimeError, "Failed to append interpreter awaited_by in get_all_awaited_by"); |
| goto result_err; |
| } |
| |
| _Py_RemoteDebug_ClearCache(&self->handle); |
| return result; |
| |
| result_err: |
| _Py_RemoteDebug_ClearCache(&self->handle); |
| Py_XDECREF(result); |
| return NULL; |
| } |
| |
| /*[clinic input] |
| @permit_long_summary |
| @permit_long_docstring_body |
| @critical_section |
| _remote_debugging.RemoteUnwinder.get_async_stack_trace |
| |
| Get the currently running async tasks and their dependency graphs from the remote process. |
| |
| This returns information about running tasks and all tasks that are waiting for them, |
| forming a complete dependency graph for each thread's active task. |
| |
| For each thread with a running task, returns the running task plus all tasks that |
| transitively depend on it (tasks waiting for the running task, tasks waiting for |
| those tasks, etc.). |
| |
| Returns a list of per-thread results, where each thread result contains: |
| - Thread ID |
| - List of task information for the running task and all its waiters |
| |
| Each task info contains: |
| - Task ID (memory address) |
| - Task name |
| - Call stack frames: List of (func_name, filename, lineno) |
| - List of tasks waiting for this task (recursive structure) |
| |
| Raises: |
| RuntimeError: If AsyncioDebug section is not available in the target process |
| MemoryError: If memory allocation fails |
| OSError: If reading from the remote process fails |
| |
| Example output (similar structure to get_all_awaited_by but only for running tasks): |
| [ |
| # Thread 140234 results |
| (140234, [ |
| # Running task and its complete waiter dependency graph |
| (4345585712, 'main_task', |
| [("run_server", "server.py", 127), ("main", "app.py", 23)], |
| [ |
| # Tasks waiting for main_task |
| (4345585800, 'worker_1', [...], [...]), |
| (4345585900, 'worker_2', [...], [...]) |
| ]) |
| ]) |
| ] |
| |
| [clinic start generated code]*/ |
| |
| static PyObject * |
| _remote_debugging_RemoteUnwinder_get_async_stack_trace_impl(RemoteUnwinderObject *self) |
| /*[clinic end generated code: output=6433d52b55e87bbe input=6129b7d509a887c9]*/ |
| { |
| if (!self->async_debug_offsets_available) { |
| PyErr_SetString(PyExc_RuntimeError, "AsyncioDebug section not available"); |
| set_exception_cause(self, PyExc_RuntimeError, "AsyncioDebug section unavailable in get_async_stack_trace"); |
| return NULL; |
| } |
| |
| PyObject *result = PyList_New(0); |
| if (result == NULL) { |
| set_exception_cause(self, PyExc_MemoryError, "Failed to create result list in get_async_stack_trace"); |
| return NULL; |
| } |
| |
| // Process all threads |
| if (iterate_threads(self, process_thread_for_async_stack_trace, result) < 0) { |
| goto result_err; |
| } |
| |
| _Py_RemoteDebug_ClearCache(&self->handle); |
| return result; |
| result_err: |
| _Py_RemoteDebug_ClearCache(&self->handle); |
| Py_XDECREF(result); |
| return NULL; |
| } |
| |
| static PyMethodDef RemoteUnwinder_methods[] = { |
| _REMOTE_DEBUGGING_REMOTEUNWINDER_GET_STACK_TRACE_METHODDEF |
| _REMOTE_DEBUGGING_REMOTEUNWINDER_GET_ALL_AWAITED_BY_METHODDEF |
| _REMOTE_DEBUGGING_REMOTEUNWINDER_GET_ASYNC_STACK_TRACE_METHODDEF |
| {NULL, NULL} |
| }; |
| |
| static void |
| RemoteUnwinder_dealloc(PyObject *op) |
| { |
| RemoteUnwinderObject *self = RemoteUnwinder_CAST(op); |
| PyTypeObject *tp = Py_TYPE(self); |
| if (self->code_object_cache) { |
| _Py_hashtable_destroy(self->code_object_cache); |
| } |
| #ifdef Py_GIL_DISABLED |
| if (self->tlbc_cache) { |
| _Py_hashtable_destroy(self->tlbc_cache); |
| } |
| #endif |
| if (self->handle.pid != 0) { |
| _Py_RemoteDebug_ClearCache(&self->handle); |
| _Py_RemoteDebug_CleanupProcHandle(&self->handle); |
| } |
| PyObject_Del(self); |
| Py_DECREF(tp); |
| } |
| |
| static PyType_Slot RemoteUnwinder_slots[] = { |
| {Py_tp_doc, (void *)"RemoteUnwinder(pid): Inspect stack of a remote Python process."}, |
| {Py_tp_methods, RemoteUnwinder_methods}, |
| {Py_tp_init, _remote_debugging_RemoteUnwinder___init__}, |
| {Py_tp_dealloc, RemoteUnwinder_dealloc}, |
| {0, NULL} |
| }; |
| |
| static PyType_Spec RemoteUnwinder_spec = { |
| .name = "_remote_debugging.RemoteUnwinder", |
| .basicsize = sizeof(RemoteUnwinderObject), |
| .flags = ( |
| Py_TPFLAGS_DEFAULT |
| | Py_TPFLAGS_IMMUTABLETYPE |
| ), |
| .slots = RemoteUnwinder_slots, |
| }; |
| |
| /* ============================================================================ |
| * MODULE INITIALIZATION |
| * ============================================================================ */ |
| |
| static int |
| _remote_debugging_exec(PyObject *m) |
| { |
| RemoteDebuggingState *st = RemoteDebugging_GetState(m); |
| #define CREATE_TYPE(mod, type, spec) \ |
| do { \ |
| type = (PyTypeObject *)PyType_FromMetaclass(NULL, mod, spec, NULL); \ |
| if (type == NULL) { \ |
| return -1; \ |
| } \ |
| } while (0) |
| |
| CREATE_TYPE(m, st->RemoteDebugging_Type, &RemoteUnwinder_spec); |
| |
| if (PyModule_AddType(m, st->RemoteDebugging_Type) < 0) { |
| return -1; |
| } |
| |
| // Initialize structseq types |
| st->TaskInfo_Type = PyStructSequence_NewType(&TaskInfo_desc); |
| if (st->TaskInfo_Type == NULL) { |
| return -1; |
| } |
| if (PyModule_AddType(m, st->TaskInfo_Type) < 0) { |
| return -1; |
| } |
| |
| st->FrameInfo_Type = PyStructSequence_NewType(&FrameInfo_desc); |
| if (st->FrameInfo_Type == NULL) { |
| return -1; |
| } |
| if (PyModule_AddType(m, st->FrameInfo_Type) < 0) { |
| return -1; |
| } |
| |
| st->CoroInfo_Type = PyStructSequence_NewType(&CoroInfo_desc); |
| if (st->CoroInfo_Type == NULL) { |
| return -1; |
| } |
| if (PyModule_AddType(m, st->CoroInfo_Type) < 0) { |
| return -1; |
| } |
| |
| st->ThreadInfo_Type = PyStructSequence_NewType(&ThreadInfo_desc); |
| if (st->ThreadInfo_Type == NULL) { |
| return -1; |
| } |
| if (PyModule_AddType(m, st->ThreadInfo_Type) < 0) { |
| return -1; |
| } |
| |
| st->InterpreterInfo_Type = PyStructSequence_NewType(&InterpreterInfo_desc); |
| if (st->InterpreterInfo_Type == NULL) { |
| return -1; |
| } |
| if (PyModule_AddType(m, st->InterpreterInfo_Type) < 0) { |
| return -1; |
| } |
| |
| st->AwaitedInfo_Type = PyStructSequence_NewType(&AwaitedInfo_desc); |
| if (st->AwaitedInfo_Type == NULL) { |
| return -1; |
| } |
| if (PyModule_AddType(m, st->AwaitedInfo_Type) < 0) { |
| return -1; |
| } |
| #ifdef Py_GIL_DISABLED |
| PyUnstable_Module_SetGIL(m, Py_MOD_GIL_NOT_USED); |
| #endif |
| int rc = PyModule_AddIntConstant(m, "PROCESS_VM_READV_SUPPORTED", HAVE_PROCESS_VM_READV); |
| if (rc < 0) { |
| return -1; |
| } |
| if (RemoteDebugging_InitState(st) < 0) { |
| return -1; |
| } |
| return 0; |
| } |
| |
| static int |
| remote_debugging_traverse(PyObject *mod, visitproc visit, void *arg) |
| { |
| RemoteDebuggingState *state = RemoteDebugging_GetState(mod); |
| Py_VISIT(state->RemoteDebugging_Type); |
| Py_VISIT(state->TaskInfo_Type); |
| Py_VISIT(state->FrameInfo_Type); |
| Py_VISIT(state->CoroInfo_Type); |
| Py_VISIT(state->ThreadInfo_Type); |
| Py_VISIT(state->InterpreterInfo_Type); |
| Py_VISIT(state->AwaitedInfo_Type); |
| return 0; |
| } |
| |
| static int |
| remote_debugging_clear(PyObject *mod) |
| { |
| RemoteDebuggingState *state = RemoteDebugging_GetState(mod); |
| Py_CLEAR(state->RemoteDebugging_Type); |
| Py_CLEAR(state->TaskInfo_Type); |
| Py_CLEAR(state->FrameInfo_Type); |
| Py_CLEAR(state->CoroInfo_Type); |
| Py_CLEAR(state->ThreadInfo_Type); |
| Py_CLEAR(state->InterpreterInfo_Type); |
| Py_CLEAR(state->AwaitedInfo_Type); |
| return 0; |
| } |
| |
| static void |
| remote_debugging_free(void *mod) |
| { |
| (void)remote_debugging_clear((PyObject *)mod); |
| } |
| |
| static PyModuleDef_Slot remote_debugging_slots[] = { |
| {Py_mod_exec, _remote_debugging_exec}, |
| {Py_mod_multiple_interpreters, Py_MOD_PER_INTERPRETER_GIL_SUPPORTED}, |
| {Py_mod_gil, Py_MOD_GIL_NOT_USED}, |
| {0, NULL}, |
| }; |
| |
| static PyMethodDef remote_debugging_methods[] = { |
| {NULL, NULL, 0, NULL}, |
| }; |
| |
| static struct PyModuleDef remote_debugging_module = { |
| PyModuleDef_HEAD_INIT, |
| .m_name = "_remote_debugging", |
| .m_size = sizeof(RemoteDebuggingState), |
| .m_methods = remote_debugging_methods, |
| .m_slots = remote_debugging_slots, |
| .m_traverse = remote_debugging_traverse, |
| .m_clear = remote_debugging_clear, |
| .m_free = remote_debugging_free, |
| }; |
| |
| PyMODINIT_FUNC |
| PyInit__remote_debugging(void) |
| { |
| return PyModuleDef_Init(&remote_debugging_module); |
| } |