mirror of
https://github.com/python/cpython.git
synced 2026-06-04 16:50:51 +00:00
Use exact remote reads for interpreter state, thread state, and interpreter frame structs instead of pulling full remote pages into the profiler page cache. This matches the core change from python/cpython#149585. The profiler clears the page cache between samples, so live entries are always packed at the front. Track the live count and only clear/search that prefix instead of scanning all 1024 slots on the hot path. Use the frame cache to predict the next thread state and top frame address, then batch interpreter/thread/frame reads with process_vm_readv when profiling a Linux target. Reuse prefetched frame buffers in the frame walker when the prediction is valid. Cache the last FrameInfo tuple per code object/instruction offset, reuse cached thread id objects, and append cached parent frames directly on full frame-cache hits. This cuts Python allocation churn in the steady-state profiler path.
887 lines
28 KiB
C
887 lines
28 KiB
C
/******************************************************************************
|
|
* Remote Debugging Module - Thread Functions
|
|
*
|
|
* This file contains functions for iterating threads and determining
|
|
* thread status in remote process memory.
|
|
******************************************************************************/
|
|
|
|
#include "_remote_debugging.h"
|
|
|
|
#ifndef MS_WINDOWS
|
|
#include <unistd.h>
|
|
#endif
|
|
|
|
#ifdef __linux__
|
|
#include <dirent.h>
|
|
#include <sys/ptrace.h>
|
|
#include <sys/wait.h>
|
|
#endif
|
|
|
|
/* ============================================================================
|
|
* 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;
|
|
}
|
|
|
|
// Check exc_state.exc_value (exception being handled in except block)
|
|
// exc_state is embedded in PyThreadState, so we read it directly from
|
|
// the thread state buffer. This catches most cases; nested exception
|
|
// handlers where exc_info points elsewhere are rare.
|
|
if (!has_exception) {
|
|
uintptr_t exc_value = GET_MEMBER(uintptr_t, ts,
|
|
unwinder->debug_offsets.thread_state.exc_state +
|
|
unwinder->debug_offsets.err_stackitem.exc_value);
|
|
if (exc_value != 0) {
|
|
has_exception = 1;
|
|
}
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
// In cache mode, copying stack chunks is more expensive than direct memory reads
|
|
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);
|
|
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) {
|
|
uintptr_t lpf_addr =
|
|
*current_tstate + (uintptr_t)unwinder->debug_offsets.thread_state.last_profiled_frame;
|
|
if (_Py_RemoteDebug_WriteRemoteMemory(&unwinder->handle, lpf_addr,
|
|
sizeof(uintptr_t), &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) {
|
|
st->tids = NULL;
|
|
st->count = 0;
|
|
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;
|
|
|
|
struct dirent *entry;
|
|
while ((entry = readdir(dir)) != NULL) {
|
|
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);
|
|
st->tids = NULL;
|
|
st->count = 0;
|
|
PyErr_NoMemory();
|
|
return -1;
|
|
}
|
|
unwinder->thread_tids = new_tids;
|
|
unwinder->thread_tids_capacity = new_cap;
|
|
}
|
|
unwinder->thread_tids[st->count++] = (pid_t)tid;
|
|
}
|
|
|
|
st->tids = unwinder->thread_tids;
|
|
closedir(dir);
|
|
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)
|
|
{
|
|
if (ptrace(PTRACE_SEIZE, tid, NULL, 0) == 0) {
|
|
return 0;
|
|
}
|
|
if (errno == ESRCH) {
|
|
return 1; // Thread gone, skip
|
|
}
|
|
if (errno == 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 (errno == EINVAL || errno == EIO) {
|
|
// Fallback for older kernels
|
|
if (ptrace(PTRACE_ATTACH, tid, NULL, NULL) == 0) {
|
|
int status;
|
|
waitpid(tid, &status, __WALL);
|
|
return 0;
|
|
}
|
|
if (errno == ESRCH || errno == 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;
|
|
}
|
|
|
|
for (size_t i = 0; i < st->count; i++) {
|
|
pid_t tid = st->tids[i];
|
|
|
|
int ret = seize_thread(tid);
|
|
if (ret == 1) {
|
|
continue; // Thread gone, skip
|
|
}
|
|
if (ret < 0) {
|
|
detach_threads(st, i);
|
|
PyErr_Format(PyExc_RuntimeError, "Failed to seize thread %d: %s", tid, strerror(errno));
|
|
st->tids = NULL;
|
|
st->count = 0;
|
|
return -1;
|
|
}
|
|
|
|
if (ptrace(PTRACE_INTERRUPT, tid, NULL, NULL) == -1 && errno != ESRCH) {
|
|
detach_threads(st, i + 1);
|
|
PyErr_Format(PyExc_RuntimeError, "Failed to interrupt thread %d: %s", tid, strerror(errno));
|
|
st->tids = NULL;
|
|
st->count = 0;
|
|
return -1;
|
|
}
|
|
|
|
int status;
|
|
if (waitpid(tid, &status, __WALL) == -1 && errno != ECHILD && errno != ESRCH) {
|
|
detach_threads(st, i + 1);
|
|
PyErr_Format(PyExc_RuntimeError, "waitpid failed for thread %d: %s", tid, strerror(errno));
|
|
st->tids = NULL;
|
|
st->count = 0;
|
|
return -1;
|
|
}
|
|
}
|
|
|
|
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)
|
|
|
|
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;
|
|
_Py_RemoteDebug_ClearCache(&unwinder->handle);
|
|
return 0;
|
|
}
|
|
|
|
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
|