mirror of
https://github.com/python/cpython.git
synced 2026-06-28 03:41:13 +00:00
gh-151613: Fix remote debugging frame cache ABA (#151614)
The remote debugging frame cache previously used only the last_profiled_frame address as its cache anchor. If a frame returned and a later frame reused the same _PyInterpreterFrame address, the profiler could accept a stale cache entry and splice parent frames from a different call chain into the current stack.
This adds a last_profiled_frame_seq counter next to last_profiled_frame, increments it when the anchor advances, stores it in frame cache entries, and validates cache hits against both the frame address and the sequence. Cache miss walks now copy stack chunks before storing new cache entries so stored continuations come from a stable snapshot. The new regression test exercises alternating call chains and checks that cached stacks never contain frames from both branches.
(cherry picked from commit 8cda6ae2f1)
918 lines
29 KiB
C
918 lines
29 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;
|
|
}
|
|
|
|
// 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);
|
|
_set_debug_oserror_from_errno_with_filename(err, task_path,
|
|
"Failed to read process task directory '%s': %s",
|
|
task_path, strerror(err));
|
|
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);
|
|
_set_debug_oserror_from_errno_with_filename(err, task_path,
|
|
"Failed to close process task directory '%s': %s",
|
|
task_path, strerror(err));
|
|
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);
|
|
_set_debug_oserror_from_errno(err,
|
|
"Failed to seize thread %d: %s", tid, strerror(err));
|
|
_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);
|
|
_set_debug_oserror_from_errno(err,
|
|
"Failed to interrupt thread %d: %s", tid, strerror(err));
|
|
_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);
|
|
_set_debug_oserror_from_errno(err,
|
|
"waitpid failed for thread %d: %s", tid, strerror(err));
|
|
_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)
|
|
|
|
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
|