| /****************************************************************************** | |
| * Remote Debugging Module - Thread Functions | |
| * | |
| * This file contains functions for iterating threads and determining | |
| * thread status in remote process memory. | |
| ******************************************************************************/ | |
| #include "_remote_debugging.h" | |
| #include "pycore_fileutils.h" // _Py_strerror() | |
| #ifndef MS_WINDOWS | |
| #include <unistd.h> | |
| #endif | |
| #ifdef __linux__ | |
| #include <dirent.h> | |
| #include <sys/ptrace.h> | |
| #include <sys/wait.h> | |
| #endif | |
| /* Bound traversal of corrupted remote exception chains. */ | |
| #define MAX_EXCEPTION_CHAIN_DEPTH (2 << 15) | |
| /* ============================================================================ | |
| * THREAD ITERATION FUNCTIONS | |
| * ============================================================================ */ | |
| int | |
| iterate_threads( | |
| RemoteUnwinderObject *unwinder, | |
| thread_processor_func processor, | |
| void *context | |
| ) { | |
| uintptr_t thread_state_addr; | |
| unsigned long tid = 0; | |
| const size_t MAX_THREADS = 8192; | |
| size_t thread_count = 0; | |
| if (0 > _Py_RemoteDebug_PagedReadRemoteMemory( | |
| &unwinder->handle, | |
| unwinder->interpreter_addr + (uintptr_t)unwinder->debug_offsets.interpreter_state.threads_head, | |
| sizeof(void*), | |
| &thread_state_addr)) | |
| { | |
| set_exception_cause(unwinder, PyExc_RuntimeError, "Failed to read threads head"); | |
| return -1; | |
| } | |
| while (thread_state_addr != 0 && thread_count < MAX_THREADS) { | |
| thread_count++; | |
| if (0 > _Py_RemoteDebug_PagedReadRemoteMemory( | |
| &unwinder->handle, | |
| thread_state_addr + (uintptr_t)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 + (uintptr_t)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; | |
| } | |
| /* ============================================================================ | |
| * INTERPRETER STATE AND THREAD DISCOVERY FUNCTIONS | |
| * ============================================================================ */ | |
| int | |
| populate_initial_state_data( | |
| int all_threads, | |
| RemoteUnwinderObject *unwinder, | |
| uintptr_t runtime_start_address, | |
| uintptr_t *interpreter_state, | |
| uintptr_t *tstate | |
| ) { | |
| uintptr_t interpreter_state_list_head = | |
| (uintptr_t)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 + | |
| (uintptr_t)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; | |
| } | |
| 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 + (uintptr_t)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; | |
| } | |
| /* ============================================================================ | |
| * THREAD STATUS FUNCTIONS | |
| * ============================================================================ */ | |
| int | |
| get_thread_status(RemoteUnwinderObject *unwinder, uint64_t tid, uint64_t pthread_id) { | |
| #if defined(__APPLE__) && TARGET_OS_OSX | |
| if (!unwinder->thread_id_offset_initialized) { | |
| uint64_t *tids = (uint64_t *)PyMem_Malloc(MAX_NATIVE_THREADS * sizeof(uint64_t)); | |
| if (!tids) { | |
| // Non-fatal: thread status is best-effort | |
| return THREAD_STATE_UNKNOWN; | |
| } | |
| int n = proc_pidinfo(unwinder->handle.pid, PROC_PIDLISTTHREADS, 0, tids, MAX_NATIVE_THREADS * sizeof(uint64_t)) / sizeof(uint64_t); | |
| if (n <= 0) { | |
| PyMem_Free(tids); | |
| return THREAD_STATE_UNKNOWN; | |
| } | |
| uint64_t min_offset = UINT64_MAX; | |
| for (int i = 0; i < n; i++) { | |
| uint64_t offset = tids[i] - pthread_id; | |
| if (offset < min_offset) { | |
| min_offset = offset; | |
| } | |
| } | |
| unwinder->thread_id_offset = min_offset; | |
| unwinder->thread_id_offset_initialized = 1; | |
| PyMem_Free(tids); | |
| } | |
| struct proc_threadinfo ti; | |
| uint64_t tid_with_offset = pthread_id + unwinder->thread_id_offset; | |
| if (proc_pidinfo(unwinder->handle.pid, PROC_PIDTHREADINFO, tid_with_offset, &ti, sizeof(ti)) != sizeof(ti)) { | |
| return THREAD_STATE_UNKNOWN; | |
| } | |
| if (ti.pth_run_state == TH_STATE_RUNNING) { | |
| return THREAD_STATE_RUNNING; | |
| } | |
| return THREAD_STATE_IDLE; | |
| #elif defined(__linux__) | |
| char stat_path[256]; | |
| char buffer[2048] = ""; | |
| snprintf(stat_path, sizeof(stat_path), "/proc/%d/task/%" PRIu64 "/stat", unwinder->handle.pid, tid); | |
| int fd = open(stat_path, O_RDONLY); | |
| if (fd == -1) { | |
| return THREAD_STATE_UNKNOWN; | |
| } | |
| if (read(fd, buffer, 2047) == 0) { | |
| close(fd); | |
| return THREAD_STATE_UNKNOWN; | |
| } | |
| close(fd); | |
| char *p = strchr(buffer, ')'); | |
| if (!p) { | |
| return THREAD_STATE_UNKNOWN; | |
| } | |
| p += 2; // Skip ") " | |
| if (*p == ' ') { | |
| p++; | |
| } | |
| switch (*p) { | |
| case 'R': // Running | |
| return THREAD_STATE_RUNNING; | |
| case 'S': // Interruptible sleep | |
| case 'D': // Uninterruptible sleep | |
| case 'T': // Stopped | |
| case 'Z': // Zombie | |
| case 'I': // Idle kernel thread | |
| return THREAD_STATE_IDLE; | |
| default: | |
| return THREAD_STATE_UNKNOWN; | |
| } | |
| #elif defined(MS_WINDOWS) | |
| ULONG n; | |
| NTSTATUS status = NtQuerySystemInformation( | |
| SystemProcessInformation, | |
| unwinder->win_process_buffer, | |
| unwinder->win_process_buffer_size, | |
| &n | |
| ); | |
| if (status == STATUS_INFO_LENGTH_MISMATCH) { | |
| // Buffer was too small so we reallocate a larger one and try again. | |
| unwinder->win_process_buffer_size = n; | |
| PVOID new_buffer = PyMem_Realloc(unwinder->win_process_buffer, n); | |
| if (!new_buffer) { | |
| // Match Linux/macOS: degrade gracefully on alloc failure | |
| return THREAD_STATE_UNKNOWN; | |
| } | |
| unwinder->win_process_buffer = new_buffer; | |
| return get_thread_status(unwinder, tid, pthread_id); | |
| } | |
| if (status != STATUS_SUCCESS) { | |
| return THREAD_STATE_UNKNOWN; | |
| } | |
| SYSTEM_PROCESS_INFORMATION *pi = (SYSTEM_PROCESS_INFORMATION *)unwinder->win_process_buffer; | |
| while ((ULONG)(ULONG_PTR)pi->UniqueProcessId != unwinder->handle.pid) { | |
| if (pi->NextEntryOffset == 0) { | |
| // Process not found (may have exited) | |
| return THREAD_STATE_UNKNOWN; | |
| } | |
| pi = (SYSTEM_PROCESS_INFORMATION *)(((BYTE *)pi) + pi->NextEntryOffset); | |
| } | |
| SYSTEM_THREAD_INFORMATION *ti = (SYSTEM_THREAD_INFORMATION *)((char *)pi + sizeof(SYSTEM_PROCESS_INFORMATION)); | |
| for (size_t i = 0; i < pi->NumberOfThreads; i++, ti++) { | |
| if (ti->ClientId.UniqueThread == (HANDLE)tid) { | |
| return ti->ThreadState != WIN32_THREADSTATE_RUNNING ? THREAD_STATE_IDLE : THREAD_STATE_RUNNING; | |
| } | |
| } | |
| // Thread not found (may have exited) | |
| return THREAD_STATE_UNKNOWN; | |
| #else | |
| return THREAD_STATE_UNKNOWN; | |
| #endif | |
| } | |
| /* ============================================================================ | |
| * STACK UNWINDING FUNCTIONS | |
| * ============================================================================ */ | |
| typedef struct { | |
| unsigned int initialized:1; | |
| unsigned int bound:1; | |
| unsigned int unbound:1; | |
| unsigned int bound_gilstate:1; | |
| unsigned int active:1; | |
| unsigned int finalizing:1; | |
| unsigned int cleared:1; | |
| unsigned int finalized:1; | |
| unsigned int :24; | |
| } _thread_status; | |
| static int | |
| read_thread_state_and_maybe_frame( | |
| RemoteUnwinderObject *unwinder, | |
| uintptr_t tstate_addr, | |
| size_t tstate_size, | |
| char *tstate_buffer, | |
| uintptr_t predicted_frame_addr, | |
| char *frame_buffer, | |
| int *frame_read) | |
| { | |
| *frame_read = 0; | |
| if (predicted_frame_addr != 0) { | |
| _Py_RemoteReadSegment segments[2] = { | |
| {tstate_addr, tstate_buffer, tstate_size}, | |
| {predicted_frame_addr, frame_buffer, SIZEOF_INTERP_FRAME}, | |
| }; | |
| Py_ssize_t nread = _Py_RemoteDebug_BatchedReadRemoteMemory( | |
| &unwinder->handle, segments, 2); | |
| int completed = 0; | |
| if (nread >= (Py_ssize_t)tstate_size) { | |
| completed = 1; | |
| if (nread == (Py_ssize_t)(tstate_size + SIZEOF_INTERP_FRAME)) { | |
| completed = 2; | |
| } | |
| } | |
| STATS_BATCHED_READ(unwinder, 2, completed); | |
| if (completed >= 1) { | |
| *frame_read = completed == 2; | |
| return 0; | |
| } | |
| } | |
| return _Py_RemoteDebug_ReadRemoteMemory( | |
| &unwinder->handle, tstate_addr, tstate_size, tstate_buffer); | |
| } | |
| PyObject* | |
| unwind_stack_for_thread( | |
| RemoteUnwinderObject *unwinder, | |
| uintptr_t *current_tstate, | |
| uintptr_t gil_holder_tstate, | |
| uintptr_t gc_frame, | |
| uintptr_t main_thread_tstate, | |
| const RemoteReadPrefetch *prefetch | |
| ) { | |
| PyObject *frame_info = NULL; | |
| PyObject *thread_id = NULL; | |
| PyObject *result = NULL; | |
| StackChunkList chunks = {0}; | |
| char local_ts[SIZEOF_THREAD_STATE]; | |
| char local_prefetched_frame[SIZEOF_INTERP_FRAME]; | |
| const char *ts; | |
| RemoteReadPrefetch ctx_prefetch = {0}; | |
| if (prefetch->tstate && prefetch->tstate_addr == *current_tstate) { | |
| ts = prefetch->tstate; | |
| if (prefetch->frame) { | |
| ctx_prefetch.frame = prefetch->frame; | |
| ctx_prefetch.frame_addr = prefetch->frame_addr; | |
| } | |
| } | |
| else if (unwinder->cache_frames) { | |
| uintptr_t predicted_frame_addr = 0; | |
| int have_prefetched_frame = 0; | |
| FrameCacheEntry *entry = frame_cache_find_by_tstate(unwinder, *current_tstate); | |
| if (entry && entry->num_addrs > 0) { | |
| predicted_frame_addr = entry->addrs[0]; | |
| } | |
| int rc = read_thread_state_and_maybe_frame( | |
| unwinder, | |
| *current_tstate, | |
| (size_t)unwinder->debug_offsets.thread_state.size, | |
| local_ts, | |
| predicted_frame_addr, | |
| local_prefetched_frame, | |
| &have_prefetched_frame); | |
| if (rc < 0) { | |
| set_exception_cause(unwinder, PyExc_RuntimeError, "Failed to read thread state"); | |
| goto error; | |
| } | |
| ts = local_ts; | |
| if (have_prefetched_frame) { | |
| ctx_prefetch.frame = local_prefetched_frame; | |
| ctx_prefetch.frame_addr = predicted_frame_addr; | |
| } | |
| } | |
| else { | |
| int rc = _Py_RemoteDebug_ReadRemoteMemory( | |
| &unwinder->handle, | |
| *current_tstate, | |
| (size_t)unwinder->debug_offsets.thread_state.size, | |
| local_ts); | |
| if (rc < 0) { | |
| set_exception_cause(unwinder, PyExc_RuntimeError, "Failed to read thread state"); | |
| goto error; | |
| } | |
| ts = local_ts; | |
| } | |
| STATS_INC(unwinder, memory_reads); | |
| STATS_ADD(unwinder, memory_bytes_read, unwinder->debug_offsets.thread_state.size); | |
| if (ctx_prefetch.frame) { | |
| STATS_INC(unwinder, memory_reads); | |
| STATS_ADD(unwinder, memory_bytes_read, SIZEOF_INTERP_FRAME); | |
| } | |
| long tid = GET_MEMBER(long, ts, unwinder->debug_offsets.thread_state.native_thread_id); | |
| // Calculate thread status using flags (always) | |
| int status_flags = 0; | |
| // Check GIL status | |
| int has_gil = 0; | |
| int gil_requested = 0; | |
| #ifdef Py_GIL_DISABLED | |
| int active = GET_MEMBER(_thread_status, ts, unwinder->debug_offsets.thread_state.status).active; | |
| has_gil = active; | |
| (void)gil_requested; // unused | |
| #else | |
| // Read holds_gil directly from thread state | |
| has_gil = GET_MEMBER(int, ts, unwinder->debug_offsets.thread_state.holds_gil); | |
| // Check if thread is actively requesting the GIL | |
| if (unwinder->debug_offsets.thread_state.gil_requested != 0) { | |
| gil_requested = GET_MEMBER(int, ts, unwinder->debug_offsets.thread_state.gil_requested); | |
| } | |
| // Set GIL_REQUESTED flag if thread is waiting | |
| if (!has_gil && gil_requested) { | |
| status_flags |= THREAD_STATUS_GIL_REQUESTED; | |
| } | |
| #endif | |
| if (has_gil) { | |
| status_flags |= THREAD_STATUS_HAS_GIL; | |
| // gh-142207 for remote debugging. | |
| gil_requested = 0; | |
| } | |
| // Check exception state (both raised and handled exceptions) | |
| int has_exception = 0; | |
| // Check current_exception (exception being raised/propagated) | |
| uintptr_t current_exception = GET_MEMBER(uintptr_t, ts, | |
| unwinder->debug_offsets.thread_state.current_exception); | |
| if (current_exception != 0) { | |
| has_exception = 1; | |
| } | |
| // Generators and coroutines use their own exception stack items. | |
| // Follow exc_info to find the innermost handler, as sys.exception() does. | |
| if (!has_exception) { | |
| uintptr_t exc_info = GET_MEMBER(uintptr_t, ts, | |
| unwinder->debug_offsets.thread_state.current_exception + | |
| sizeof(uintptr_t)); | |
| uintptr_t exc_state_addr = | |
| *current_tstate + unwinder->debug_offsets.thread_state.exc_state; | |
| uintptr_t exc_value_offset = | |
| unwinder->debug_offsets.err_stackitem.exc_value; | |
| uintptr_t previous_item_offset = | |
| exc_value_offset + sizeof(uintptr_t); | |
| for (int depth = 0; exc_info != 0 && depth < MAX_EXCEPTION_CHAIN_DEPTH; | |
| depth++) | |
| { | |
| if (exc_info == exc_state_addr) { | |
| // Bottom of the chain: the stack item embedded in the thread | |
| // state, which is already in the local thread state buffer. | |
| uintptr_t exc_value = GET_MEMBER(uintptr_t, ts, | |
| unwinder->debug_offsets.thread_state.exc_state + | |
| exc_value_offset); | |
| if (exc_value != 0) { | |
| has_exception = 1; | |
| } | |
| break; | |
| } | |
| uintptr_t exc_value = 0; | |
| if (read_ptr(unwinder, exc_info + exc_value_offset, &exc_value) < 0) { | |
| PyErr_Clear(); // Best effort: treat as no active exception | |
| break; | |
| } | |
| if (exc_value != 0) { | |
| has_exception = 1; | |
| break; | |
| } | |
| uintptr_t previous_item = 0; | |
| if (read_ptr(unwinder, exc_info + previous_item_offset, | |
| &previous_item) < 0) { | |
| PyErr_Clear(); | |
| break; | |
| } | |
| exc_info = previous_item; | |
| } | |
| } | |
| if (has_exception) { | |
| status_flags |= THREAD_STATUS_HAS_EXCEPTION; | |
| } | |
| // Check CPU status | |
| long pthread_id = GET_MEMBER(long, ts, unwinder->debug_offsets.thread_state.thread_id); | |
| // Optimization: only check CPU status if needed by mode because it's expensive | |
| int cpu_status = THREAD_STATE_UNKNOWN; | |
| if (unwinder->mode == PROFILING_MODE_CPU || unwinder->mode == PROFILING_MODE_ALL) { | |
| cpu_status = get_thread_status(unwinder, tid, pthread_id); | |
| } | |
| if (cpu_status == THREAD_STATE_UNKNOWN) { | |
| status_flags |= THREAD_STATUS_UNKNOWN; | |
| } else if (cpu_status == THREAD_STATE_RUNNING) { | |
| status_flags |= THREAD_STATUS_ON_CPU; | |
| } | |
| if (*current_tstate == main_thread_tstate) { | |
| status_flags |= THREAD_STATUS_MAIN_THREAD; | |
| } | |
| // Check if we should skip this thread based on mode | |
| int should_skip = 0; | |
| if (unwinder->skip_non_matching_threads) { | |
| if (unwinder->mode == PROFILING_MODE_CPU) { | |
| // Skip if not on CPU | |
| should_skip = !(status_flags & THREAD_STATUS_ON_CPU); | |
| } else if (unwinder->mode == PROFILING_MODE_GIL) { | |
| // Skip if doesn't have GIL | |
| should_skip = !(status_flags & THREAD_STATUS_HAS_GIL); | |
| } else if (unwinder->mode == PROFILING_MODE_EXCEPTION) { | |
| // Skip if thread doesn't have an exception active | |
| should_skip = !(status_flags & THREAD_STATUS_HAS_EXCEPTION); | |
| } | |
| // PROFILING_MODE_WALL and PROFILING_MODE_ALL never skip | |
| } | |
| if (should_skip) { | |
| // Advance to next thread and return NULL to skip processing | |
| *current_tstate = GET_MEMBER(uintptr_t, ts, unwinder->debug_offsets.thread_state.next); | |
| return NULL; | |
| } | |
| uintptr_t frame_addr = GET_MEMBER(uintptr_t, ts, unwinder->debug_offsets.thread_state.current_frame); | |
| uintptr_t base_frame_addr = GET_MEMBER(uintptr_t, ts, unwinder->debug_offsets.thread_state.base_frame); | |
| frame_info = PyList_New(0); | |
| if (!frame_info) { | |
| set_exception_cause(unwinder, PyExc_MemoryError, "Failed to create frame info list"); | |
| goto error; | |
| } | |
| // Cache mode skips this for full hits, but cache misses copy chunks before | |
| // walking so newly stored cache entries come from a stable stack snapshot. | |
| if (!unwinder->cache_frames) { | |
| if (copy_stack_chunks(unwinder, *current_tstate, &chunks) < 0) { | |
| set_exception_cause(unwinder, PyExc_RuntimeError, "Failed to copy stack chunks"); | |
| goto error; | |
| } | |
| } | |
| uintptr_t addrs[FRAME_CACHE_MAX_FRAMES]; | |
| FrameWalkContext ctx = { | |
| .frame_addr = frame_addr, | |
| .thread_state_addr = *current_tstate, | |
| .base_frame_addr = base_frame_addr, | |
| .gc_frame = gc_frame, | |
| .chunks = &chunks, | |
| .prefetch = ctx_prefetch, | |
| .frame_info = frame_info, | |
| .frame_addrs = addrs, | |
| .num_addrs = 0, | |
| .max_addrs = FRAME_CACHE_MAX_FRAMES, | |
| }; | |
| assert(ctx.max_addrs == FRAME_CACHE_MAX_FRAMES); | |
| if (unwinder->cache_frames) { | |
| // Use cache to avoid re-reading unchanged parent frames | |
| ctx.last_profiled.frame = GET_MEMBER(uintptr_t, ts, | |
| unwinder->debug_offsets.thread_state.last_profiled_frame); | |
| ctx.last_profiled.seq = GET_MEMBER(uintptr_t, ts, | |
| unwinder->debug_offsets.thread_state.last_profiled_frame_seq); | |
| if (collect_frames_with_cache(unwinder, &ctx, tid) < 0) { | |
| set_exception_cause(unwinder, PyExc_RuntimeError, "Failed to collect frames"); | |
| goto error; | |
| } | |
| // Update last_profiled_frame for next sample if it changed | |
| if (frame_addr != ctx.last_profiled.frame) { | |
| if (set_last_profiled_frame(unwinder, *current_tstate, frame_addr) < 0) { | |
| PyErr_Clear(); // Non-fatal | |
| } | |
| } | |
| } else { | |
| // No caching - process entire frame chain with base_frame validation | |
| if (process_frame_chain(unwinder, &ctx) < 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); | |
| if (unwinder->cache_frames) { | |
| FrameCacheEntry *entry = frame_cache_find(unwinder, (uint64_t)tid); | |
| if (entry && entry->thread_id_obj) { | |
| thread_id = Py_NewRef(entry->thread_id_obj); | |
| } | |
| } | |
| if (thread_id == NULL) { | |
| thread_id = PyLong_FromLongLong(tid); | |
| 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; | |
| } | |
| // Always use status_flags | |
| PyObject *py_status = PyLong_FromLong(status_flags); | |
| if (py_status == NULL) { | |
| set_exception_cause(unwinder, PyExc_RuntimeError, "Failed to create thread status"); | |
| goto error; | |
| } | |
| // py_status contains status flags (bitfield) | |
| PyStructSequence_SetItem(result, 0, thread_id); | |
| PyStructSequence_SetItem(result, 1, py_status); // Steals reference | |
| PyStructSequence_SetItem(result, 2, 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; | |
| } | |
| /* ============================================================================ | |
| * PROCESS STOP FUNCTIONS | |
| * ============================================================================ */ | |
| #if defined(__APPLE__) && TARGET_OS_OSX | |
| void | |
| _Py_RemoteDebug_InitThreadsState(RemoteUnwinderObject *unwinder, _Py_RemoteDebug_ThreadsState *st) | |
| { | |
| st->task = MACH_PORT_NULL; | |
| st->suspended = 0; | |
| } | |
| int | |
| _Py_RemoteDebug_StopAllThreads(RemoteUnwinderObject *unwinder, _Py_RemoteDebug_ThreadsState *st) | |
| { | |
| kern_return_t kr = task_suspend(unwinder->handle.task); | |
| if (kr != KERN_SUCCESS) { | |
| if (kr == MACH_SEND_INVALID_DEST) { | |
| PyErr_Format(PyExc_ProcessLookupError, | |
| "Process %d has terminated", unwinder->handle.pid); | |
| } else { | |
| PyErr_Format(PyExc_RuntimeError, | |
| "task_suspend failed for PID %d: kern_return_t %d", | |
| unwinder->handle.pid, kr); | |
| } | |
| return -1; | |
| } | |
| st->task = unwinder->handle.task; | |
| st->suspended = 1; | |
| _Py_RemoteDebug_ClearCache(&unwinder->handle); | |
| return 0; | |
| } | |
| void | |
| _Py_RemoteDebug_ResumeAllThreads(RemoteUnwinderObject *unwinder, _Py_RemoteDebug_ThreadsState *st) | |
| { | |
| if (!st->suspended || st->task == MACH_PORT_NULL) { | |
| return; | |
| } | |
| task_resume(st->task); | |
| st->task = MACH_PORT_NULL; | |
| st->suspended = 0; | |
| } | |
| #elif defined(__linux__) | |
| void | |
| _Py_RemoteDebug_InitThreadsState(RemoteUnwinderObject *unwinder, _Py_RemoteDebug_ThreadsState *st) | |
| { | |
| st->tids = NULL; | |
| st->count = 0; | |
| } | |
| static int | |
| read_thread_ids(RemoteUnwinderObject *unwinder, _Py_RemoteDebug_ThreadsState *st) | |
| { | |
| char task_path[64]; | |
| snprintf(task_path, sizeof(task_path), "/proc/%d/task", unwinder->handle.pid); | |
| DIR *dir = opendir(task_path); | |
| if (dir == NULL) { | |
| _Py_RemoteDebug_InitThreadsState(unwinder, st); | |
| if (errno == ENOENT || errno == ESRCH) { | |
| PyErr_Format(PyExc_ProcessLookupError, | |
| "Process %d has terminated", unwinder->handle.pid); | |
| } else { | |
| PyErr_SetFromErrnoWithFilename(PyExc_OSError, task_path); | |
| } | |
| return -1; | |
| } | |
| st->count = 0; | |
| for (;;) { | |
| errno = 0; | |
| struct dirent *entry = readdir(dir); | |
| if (entry == NULL) { | |
| if (errno != 0) { | |
| int err = errno; | |
| closedir(dir); | |
| _Py_RemoteDebug_InitThreadsState(unwinder, st); | |
| PyObject *message = _Py_strerror(err); | |
| if (message != NULL) { | |
| _set_debug_oserror_from_errno_with_filename(err, task_path, | |
| "Failed to read process task directory '%s': %S", | |
| task_path, message); | |
| Py_DECREF(message); | |
| } | |
| return -1; | |
| } | |
| break; | |
| } | |
| if (entry->d_name[0] < '1' || entry->d_name[0] > '9') { | |
| continue; | |
| } | |
| char *endptr; | |
| long tid = strtol(entry->d_name, &endptr, 10); | |
| if (*endptr != '\0' || tid <= 0) { | |
| continue; | |
| } | |
| if (st->count >= unwinder->thread_tids_capacity) { | |
| size_t new_cap = unwinder->thread_tids_capacity == 0 ? 64 : unwinder->thread_tids_capacity * 2; | |
| pid_t *new_tids = PyMem_RawRealloc(unwinder->thread_tids, new_cap * sizeof(pid_t)); | |
| if (new_tids == NULL) { | |
| closedir(dir); | |
| _Py_RemoteDebug_InitThreadsState(unwinder, st); | |
| PyErr_NoMemory(); | |
| return -1; | |
| } | |
| unwinder->thread_tids = new_tids; | |
| unwinder->thread_tids_capacity = new_cap; | |
| } | |
| unwinder->thread_tids[st->count++] = (pid_t)tid; | |
| } | |
| if (closedir(dir) != 0) { | |
| int err = errno; | |
| _Py_RemoteDebug_InitThreadsState(unwinder, st); | |
| PyObject *message = _Py_strerror(err); | |
| if (message != NULL) { | |
| _set_debug_oserror_from_errno_with_filename(err, task_path, | |
| "Failed to close process task directory '%s': %S", | |
| task_path, message); | |
| Py_DECREF(message); | |
| } | |
| return -1; | |
| } | |
| st->tids = unwinder->thread_tids; | |
| return 0; | |
| } | |
| static inline void | |
| detach_threads(_Py_RemoteDebug_ThreadsState *st, size_t up_to) | |
| { | |
| for (size_t j = 0; j < up_to; j++) { | |
| ptrace(PTRACE_DETACH, st->tids[j], NULL, NULL); | |
| } | |
| } | |
| static int | |
| seize_thread(pid_t tid, int *err) | |
| { | |
| if (ptrace(PTRACE_SEIZE, tid, NULL, 0) == 0) { | |
| return 0; | |
| } | |
| *err = errno; | |
| if (*err == ESRCH) { | |
| return 1; // Thread gone, skip | |
| } | |
| if (*err == EPERM) { | |
| // Thread may have exited, be in a special state, or already be traced. | |
| // Skip rather than fail - this avoids endless retry loops when | |
| // threads transiently become inaccessible. | |
| return 1; | |
| } | |
| if (*err == EINVAL || *err == EIO) { | |
| // Fallback for older kernels | |
| if (ptrace(PTRACE_ATTACH, tid, NULL, NULL) == 0) { | |
| int status; | |
| waitpid(tid, &status, __WALL); | |
| return 0; | |
| } | |
| *err = errno; | |
| if (*err == ESRCH || *err == EPERM) { | |
| return 1; // Thread gone or inaccessible | |
| } | |
| } | |
| return -1; // Real error | |
| } | |
| int | |
| _Py_RemoteDebug_StopAllThreads(RemoteUnwinderObject *unwinder, _Py_RemoteDebug_ThreadsState *st) | |
| { | |
| if (read_thread_ids(unwinder, st) < 0) { | |
| return -1; | |
| } | |
| size_t n_tids = st->count; | |
| size_t seized = 0; | |
| for (size_t i = 0; i < n_tids; i++) { | |
| pid_t tid = st->tids[i]; | |
| int err = 0; | |
| int ret = seize_thread(tid, &err); | |
| if (ret == 1) { | |
| continue; // Thread gone, skip | |
| } | |
| if (ret < 0) { | |
| detach_threads(st, seized); | |
| PyObject *message = _Py_strerror(err); | |
| if (message != NULL) { | |
| _set_debug_oserror_from_errno(err, | |
| "Failed to seize thread %d: %S", tid, message); | |
| Py_DECREF(message); | |
| } | |
| _Py_RemoteDebug_InitThreadsState(unwinder, st); | |
| return -1; | |
| } | |
| st->tids[seized++] = tid; | |
| if (ptrace(PTRACE_INTERRUPT, tid, NULL, NULL) == -1) { | |
| err = errno; | |
| if (err != ESRCH) { | |
| detach_threads(st, seized); | |
| PyObject *message = _Py_strerror(err); | |
| if (message != NULL) { | |
| _set_debug_oserror_from_errno(err, | |
| "Failed to interrupt thread %d: %S", tid, message); | |
| Py_DECREF(message); | |
| } | |
| _Py_RemoteDebug_InitThreadsState(unwinder, st); | |
| return -1; | |
| } | |
| } | |
| int status; | |
| if (waitpid(tid, &status, __WALL) == -1) { | |
| err = errno; | |
| if (err != ECHILD && err != ESRCH) { | |
| detach_threads(st, seized); | |
| PyObject *message = _Py_strerror(err); | |
| if (message != NULL) { | |
| _set_debug_oserror_from_errno(err, | |
| "waitpid failed for thread %d: %S", tid, message); | |
| Py_DECREF(message); | |
| } | |
| _Py_RemoteDebug_InitThreadsState(unwinder, st); | |
| return -1; | |
| } | |
| } | |
| } | |
| st->count = seized; | |
| return 0; | |
| } | |
| void | |
| _Py_RemoteDebug_ResumeAllThreads(RemoteUnwinderObject *unwinder, _Py_RemoteDebug_ThreadsState *st) | |
| { | |
| if (st->tids == NULL || st->count == 0) { | |
| return; | |
| } | |
| detach_threads(st, st->count); | |
| st->tids = NULL; | |
| st->count = 0; | |
| } | |
| #elif defined(MS_WINDOWS) | |
| static int | |
| wait_for_threads_to_stop(RemoteUnwinderObject *unwinder) | |
| { | |
| typedef NTSTATUS (NTAPI *NtGetNextThreadFunc)( | |
| HANDLE, HANDLE, ACCESS_MASK, ULONG, ULONG, PHANDLE); | |
| static NtGetNextThreadFunc pNtGetNextThread = NULL; | |
| static int tried_load = 0; | |
| if (!tried_load) { | |
| HMODULE hNtdll = GetModuleHandleW(L"ntdll.dll"); | |
| if (hNtdll) { | |
| pNtGetNextThread = (NtGetNextThreadFunc)GetProcAddress( | |
| hNtdll, "NtGetNextThread"); | |
| } | |
| tried_load = 1; | |
| } | |
| if (pNtGetNextThread == NULL) { | |
| PyErr_SetString(PyExc_RuntimeError, "NtGetNextThread not available"); | |
| return -1; | |
| } | |
| HANDLE previous = NULL; | |
| int result = -1; | |
| for (;;) { | |
| HANDLE next = NULL; | |
| // Enumerate with the available access, then obtain context access | |
| // separately so threads that deny it are not silently skipped. | |
| NTSTATUS status = pNtGetNextThread(unwinder->handle.hProcess, | |
| previous, MAXIMUM_ALLOWED, 0, 0, &next); | |
| if (previous != NULL) { | |
| CloseHandle(previous); | |
| } | |
| previous = next; | |
| if (status == STATUS_NO_MORE_ENTRIES) { | |
| break; | |
| } | |
| if (status < 0) { | |
| if (!is_process_alive(unwinder->handle.hProcess)) { | |
| PyErr_Format(PyExc_ProcessLookupError, | |
| "Process %d has terminated", unwinder->handle.pid); | |
| } | |
| else { | |
| PyErr_Format(PyExc_RuntimeError, | |
| "NtGetNextThread failed: 0x%lx", status); | |
| } | |
| goto done; | |
| } | |
| HANDLE thread; | |
| if (!DuplicateHandle(GetCurrentProcess(), next, GetCurrentProcess(), | |
| &thread, THREAD_GET_CONTEXT | SYNCHRONIZE, | |
| FALSE, 0)) { | |
| PyErr_SetFromWindowsErr(GetLastError()); | |
| goto done; | |
| } | |
| // Suspension is asynchronous. Reading the context waits for the | |
| // thread to stop before we start reading the target's memory. | |
| CONTEXT context = {.ContextFlags = CONTEXT_CONTROL}; | |
| if (!GetThreadContext(thread, &context)) { | |
| DWORD error = GetLastError(); | |
| int exited = WaitForSingleObject(thread, 0) == WAIT_OBJECT_0; | |
| CloseHandle(thread); | |
| if (exited) { | |
| continue; | |
| } | |
| PyErr_SetFromWindowsErr(error); | |
| goto done; | |
| } | |
| CloseHandle(thread); | |
| } | |
| if (!is_process_alive(unwinder->handle.hProcess)) { | |
| PyErr_Format(PyExc_ProcessLookupError, | |
| "Process %d has terminated", unwinder->handle.pid); | |
| goto done; | |
| } | |
| result = 0; | |
| done: | |
| if (previous != NULL) { | |
| CloseHandle(previous); | |
| } | |
| return result; | |
| } | |
| void | |
| _Py_RemoteDebug_InitThreadsState(RemoteUnwinderObject *unwinder, _Py_RemoteDebug_ThreadsState *st) | |
| { | |
| st->hProcess = NULL; | |
| st->suspended = 0; | |
| } | |
| int | |
| _Py_RemoteDebug_StopAllThreads(RemoteUnwinderObject *unwinder, _Py_RemoteDebug_ThreadsState *st) | |
| { | |
| static NtSuspendProcessFunc pNtSuspendProcess = NULL; | |
| static int tried_load = 0; | |
| if (!tried_load) { | |
| HMODULE hNtdll = GetModuleHandleW(L"ntdll.dll"); | |
| if (hNtdll) { | |
| pNtSuspendProcess = (NtSuspendProcessFunc)GetProcAddress(hNtdll, "NtSuspendProcess"); | |
| } | |
| tried_load = 1; | |
| } | |
| if (pNtSuspendProcess == NULL) { | |
| PyErr_SetString(PyExc_RuntimeError, "NtSuspendProcess not available"); | |
| return -1; | |
| } | |
| NTSTATUS status = pNtSuspendProcess(unwinder->handle.hProcess); | |
| if (status >= 0) { | |
| st->hProcess = unwinder->handle.hProcess; | |
| st->suspended = 1; | |
| if (wait_for_threads_to_stop(unwinder) < 0) { | |
| // pause_threads() has not yet set threads_stopped, so its caller | |
| // will not resume the process when we return an error. | |
| _Py_RemoteDebug_ResumeAllThreads(unwinder, st); | |
| return -1; | |
| } | |
| _Py_RemoteDebug_ClearCache(&unwinder->handle); | |
| return 0; | |
| } | |
| if (!is_process_alive(unwinder->handle.hProcess)) { | |
| PyErr_Format(PyExc_ProcessLookupError, | |
| "Process %d has terminated", unwinder->handle.pid); | |
| return -1; | |
| } | |
| PyErr_Format(PyExc_RuntimeError, "NtSuspendProcess failed: 0x%lx", status); | |
| return -1; | |
| } | |
| void | |
| _Py_RemoteDebug_ResumeAllThreads(RemoteUnwinderObject *unwinder, _Py_RemoteDebug_ThreadsState *st) | |
| { | |
| if (!st->suspended || st->hProcess == NULL) { | |
| return; | |
| } | |
| static NtResumeProcessFunc pNtResumeProcess = NULL; | |
| static int tried_load = 0; | |
| if (!tried_load) { | |
| HMODULE hNtdll = GetModuleHandleW(L"ntdll.dll"); | |
| if (hNtdll) { | |
| pNtResumeProcess = (NtResumeProcessFunc)GetProcAddress(hNtdll, "NtResumeProcess"); | |
| } | |
| tried_load = 1; | |
| } | |
| if (pNtResumeProcess != NULL) { | |
| pNtResumeProcess(st->hProcess); | |
| } | |
| st->hProcess = NULL; | |
| st->suspended = 0; | |
| } | |
| #else | |
| void | |
| _Py_RemoteDebug_InitThreadsState(RemoteUnwinderObject *unwinder, _Py_RemoteDebug_ThreadsState *st) | |
| { | |
| (void)unwinder; | |
| (void)st; | |
| } | |
| int | |
| _Py_RemoteDebug_StopAllThreads(RemoteUnwinderObject *unwinder, _Py_RemoteDebug_ThreadsState *st) | |
| { | |
| (void)unwinder; | |
| (void)st; | |
| return 0; | |
| } | |
| void | |
| _Py_RemoteDebug_ResumeAllThreads(RemoteUnwinderObject *unwinder, _Py_RemoteDebug_ThreadsState *st) | |
| { | |
| (void)unwinder; | |
| (void)st; | |
| } | |
| #endif |