Skip to content

Commit 35b3947

Browse files
committed
gh-159312: Confine CPU attribution checks to unbound states
1 parent ecac33a commit 35b3947

5 files changed

Lines changed: 88 additions & 208 deletions

File tree

‎Lib/test/test_external_inspection.py‎

Lines changed: 22 additions & 91 deletions
Original file line numberDiff line numberDiff line change
@@ -2596,108 +2596,39 @@ def test_unsupported_platform_error(self):
25962596

25972597
@requires_remote_subprocess_debugging()
25982598
class TestDetectionOfThreadStatus(RemoteInspectionTestBase):
2599-
def _check_cpu_status_with_multiple_thread_states(
2600-
self, *, native=False, subinterpreter=False,
2601-
):
2599+
@skip_if_not_supported
2600+
def test_cpu_status_with_multiple_thread_states(self):
26022601
import_module("_testinternalcapi")
2603-
import_module("_interpreters")
2604-
if native:
2605-
import_module("_hashlib")
2606-
work = "_hashlib.pbkdf2_hmac('sha256', b'password', b'salt', 10_000_000)"
2607-
else:
2608-
work = "pass"
2609-
code = textwrap.dedent(f"""
2610-
import socket
2611-
{"import _hashlib" if native else ""}
2612-
active_sock = socket.socket(fileno={{fd}})
2613-
2602+
import_module("_hashlib")
2603+
code = textwrap.dedent("""
2604+
import _hashlib
26142605
def active():
2615-
active_sock.sendall(b"ready")
26162606
while True:
2617-
{work}
2618-
2607+
_hashlib.pbkdf2_hmac('sha256', b'password', b'salt', 10_000_000)
26192608
active()
26202609
""")
26212610
script = f"""
2622-
import _interpreters, _testinternalcapi
2623-
2611+
import _testinternalcapi
26242612
def suspended():
2625-
if {subinterpreter}:
2626-
interpid = _interpreters.create()
2627-
else:
2628-
interpid = _interpreters.get_current()[0]
2629-
# Create another state on this OS thread.
2630-
code = {code!r}.format(fd=sock.fileno())
2631-
_testinternalcapi.exec_interpreter(interpid, code)
2632-
2613+
sock.sendall(b"ready")
2614+
_testinternalcapi.exec_interpreter(0, {code!r})
26332615
suspended()
26342616
"""
26352617
with self._target_process(script) as (p, sock, _):
26362618
_wait_for_signal(sock, b"ready")
2637-
for mode in (PROFILING_MODE_CPU, PROFILING_MODE_ALL):
2638-
with self.subTest(mode=mode):
2639-
unwinder = RemoteUnwinder(
2640-
p.pid, all_threads=True, mode=mode,
2641-
skip_non_matching_threads=(mode == PROFILING_MODE_CPU),
2642-
)
2643-
for _ in busy_retry(SHORT_TIMEOUT):
2644-
try:
2645-
trace = unwinder.get_stack_trace()
2646-
except TRANSIENT_ERRORS:
2647-
continue
2648-
active = self._find_thread_with_frame(
2649-
trace, lambda f: f.funcname == "active")
2650-
if active is None or not active.status & THREAD_STATUS_ON_CPU:
2651-
continue
2652-
if native and active.status & THREAD_STATUS_HAS_GIL:
2653-
continue
2654-
suspended = self._find_thread_with_frame(
2655-
trace, lambda f: f.funcname == "suspended")
2656-
if mode == PROFILING_MODE_CPU:
2657-
self.assertIsNone(suspended)
2658-
else:
2659-
self.assertIsNotNone(suspended)
2660-
self.assertEqual(suspended.thread_id, active.thread_id)
2661-
self.assertFalse(suspended.status & THREAD_STATUS_ON_CPU)
2662-
break
2663-
2664-
@skip_if_not_supported
2665-
def test_cpu_status_with_multiple_thread_states(self):
2666-
for native in (False, True):
2667-
for subinterpreter in (False, True):
2668-
with self.subTest(native=native, subinterpreter=subinterpreter):
2669-
self._check_cpu_status_with_multiple_thread_states(
2670-
native=native, subinterpreter=subinterpreter)
2671-
2672-
@skip_if_not_supported
2673-
def test_cpu_status_after_tstate_callback(self):
2674-
import_module("_testinternalcapi")
2675-
for retain in (False, True):
2676-
with self.subTest(retain=retain):
2677-
script = f"""
2678-
import _testinternalcapi
2679-
2680-
def ready():
2681-
sock.sendall(b"ready")
2682-
2683-
def native_work():
2684-
_testinternalcapi.native_work_after_tstate_callback(
2685-
ready, {SHORT_TIMEOUT * 3}, {retain})
2686-
2687-
native_work()
2688-
"""
2689-
with self._target_process(script) as (p, sock, _):
2690-
_wait_for_signal(sock, b"ready")
2691-
for mode in (PROFILING_MODE_CPU, PROFILING_MODE_ALL):
2692-
with self.subTest(mode=mode):
2693-
unwinder = RemoteUnwinder(p.pid, all_threads=True, mode=mode)
2694-
for _ in busy_retry(SHORT_TIMEOUT):
2695-
trace = _get_stack_trace_with_retry(unwinder)
2696-
thread = self._find_thread_with_frame(
2697-
trace, lambda f: f.funcname == "native_work")
2698-
if thread and thread.status & THREAD_STATUS_ON_CPU:
2699-
self.assertFalse(thread.status & THREAD_STATUS_HAS_GIL)
2700-
break
2619+
unwinder = RemoteUnwinder(p.pid, all_threads=True, mode=PROFILING_MODE_ALL)
2620+
for _ in busy_retry(SHORT_TIMEOUT):
2621+
trace = _get_stack_trace_with_retry(unwinder)
2622+
active = self._find_thread_with_frame(trace, lambda f: f.funcname == "active")
2623+
if active is None or not active.status & THREAD_STATUS_ON_CPU:
2624+
continue
2625+
if active.status & THREAD_STATUS_HAS_GIL:
2626+
continue
2627+
suspended = self._find_thread_with_frame(trace, lambda f: f.funcname == "suspended")
2628+
self.assertIsNotNone(suspended)
2629+
self.assertEqual(suspended.thread_id, active.thread_id)
2630+
self.assertFalse(suspended.status & THREAD_STATUS_ON_CPU)
2631+
break
27012632

27022633
def _run_thread_status_test(self, mode, check_condition):
27032634
"""

‎Modules/_remote_debugging/_remote_debugging.h‎

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -689,8 +689,7 @@ extern PyObject* unwind_stack_for_thread(
689689
uintptr_t gil_holder_tstate,
690690
uintptr_t gc_frame,
691691
uintptr_t main_thread_tstate,
692-
const RemoteReadPrefetch *prefetch,
693-
int *has_bound_stack
692+
const RemoteReadPrefetch *prefetch
694693
);
695694

696695
/* Thread stopping functions (for blocking mode) */

‎Modules/_remote_debugging/module.c‎

Lines changed: 3 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -650,45 +650,6 @@ read_interp_state_and_maybe_thread_frame(
650650
interp_state_buffer);
651651
}
652652

653-
static int
654-
filter_cpu_thread_states(PyObject *result, PyObject *bound_threads, int skip)
655-
{
656-
// Prefer a bound stack when states share an OS thread. There may be no
657-
// bound state after a native callback deletes its temporary thread state.
658-
for (Py_ssize_t i = 0; i < PyList_GET_SIZE(result); i++) {
659-
PyObject *threads = PyStructSequence_GET_ITEM(PyList_GET_ITEM(result, i), 1);
660-
for (Py_ssize_t j = PyList_GET_SIZE(threads); j-- > 0;) {
661-
PyObject *thread = PyList_GET_ITEM(threads, j);
662-
PyObject *bound = PyDict_GetItemWithError(
663-
bound_threads, PyStructSequence_GET_ITEM(thread, 0));
664-
if (bound == NULL) {
665-
if (PyErr_Occurred()) {
666-
return -1;
667-
}
668-
continue;
669-
}
670-
long status = PyLong_AsLong(PyStructSequence_GET_ITEM(thread, 1));
671-
if (bound == thread || !(status & THREAD_STATUS_ON_CPU)) {
672-
continue;
673-
}
674-
if (skip) {
675-
if (PySequence_DelItem(threads, j) < 0) {
676-
return -1;
677-
}
678-
}
679-
else {
680-
PyObject *new_status = PyLong_FromLong(status & ~THREAD_STATUS_ON_CPU);
681-
if (new_status == NULL) {
682-
return -1;
683-
}
684-
Py_DECREF(PyStructSequence_GET_ITEM(thread, 1));
685-
PyStructSequence_SET_ITEM(thread, 1, new_status);
686-
}
687-
}
688-
}
689-
return 0;
690-
}
691-
692653
/*[clinic input]
693654
@critical_section
694655
_remote_debugging.RemoteUnwinder.get_stack_trace
@@ -753,15 +714,6 @@ _remote_debugging_RemoteUnwinder_get_stack_trace_impl(RemoteUnwinderObject *self
753714
set_exception_cause(self, PyExc_MemoryError, "Failed to create stack trace result list");
754715
return NULL;
755716
}
756-
PyObject *bound_threads = NULL;
757-
if (self->tstate_addr == 0 &&
758-
(self->mode == PROFILING_MODE_CPU || self->mode == PROFILING_MODE_ALL)) {
759-
bound_threads = PyDict_New();
760-
if (bound_threads == NULL) {
761-
Py_DECREF(result);
762-
return NULL;
763-
}
764-
}
765717

766718
// Iterate over all interpreters
767719
uintptr_t current_interpreter = self->interpreter_addr;
@@ -850,13 +802,11 @@ _remote_debugging_RemoteUnwinder_get_stack_trace_impl(RemoteUnwinderObject *self
850802

851803
while (current_tstate != 0) {
852804
uintptr_t prev_tstate = current_tstate;
853-
int has_bound_stack;
854805
PyObject* frame_info = unwind_stack_for_thread(self, &current_tstate,
855806
gil_holder_tstate,
856807
gc_frame,
857808
main_thread_tstate,
858-
&prefetch,
859-
&has_bound_stack);
809+
&prefetch);
860810
if (!frame_info) {
861811
// Check if this was an intentional skip due to mode-based filtering
862812
if ((self->mode == PROFILING_MODE_CPU || self->mode == PROFILING_MODE_GIL ||
@@ -882,13 +832,10 @@ _remote_debugging_RemoteUnwinder_get_stack_trace_impl(RemoteUnwinderObject *self
882832
goto exit;
883833
}
884834

885-
if (PyList_Append(interpreter_threads, frame_info) == -1 ||
886-
(bound_threads && has_bound_stack &&
887-
PyDict_SetItem(bound_threads,
888-
PyStructSequence_GET_ITEM(frame_info, 0), frame_info) < 0)) {
835+
if (PyList_Append(interpreter_threads, frame_info) == -1) {
889836
Py_DECREF(frame_info);
890837
Py_DECREF(interpreter_threads);
891-
set_exception_cause(self, PyExc_RuntimeError, "Failed to collect thread frame info");
838+
set_exception_cause(self, PyExc_RuntimeError, "Failed to append thread frame info");
892839
Py_CLEAR(result);
893840
goto exit;
894841
}
@@ -943,14 +890,7 @@ _remote_debugging_RemoteUnwinder_get_stack_trace_impl(RemoteUnwinderObject *self
943890
}
944891
}
945892

946-
if (bound_threads != NULL &&
947-
filter_cpu_thread_states(result, bound_threads,
948-
self->mode == PROFILING_MODE_CPU && self->skip_non_matching_threads) < 0) {
949-
Py_CLEAR(result);
950-
}
951-
952893
exit:
953-
Py_XDECREF(bound_threads);
954894
// Invalidate cache entries for threads not seen in this sample.
955895
// Only do this every 1024 iterations to avoid performance overhead.
956896
if (self->cache_frames && result) {

‎Modules/_remote_debugging/threads.c‎

Lines changed: 62 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -293,6 +293,62 @@ typedef struct {
293293
unsigned int :24;
294294
} _thread_status;
295295

296+
static int
297+
has_bound_stack(RemoteUnwinderObject *unwinder, long tid)
298+
{
299+
uintptr_t interp, tstate;
300+
int remaining = 8192;
301+
if (read_ptr(unwinder, unwinder->runtime_start_address +
302+
unwinder->debug_offsets.runtime_state.interpreters_head, &interp) < 0) {
303+
goto unavailable;
304+
}
305+
while (interp && remaining-- > 0) {
306+
if (read_ptr(unwinder, interp +
307+
unwinder->debug_offsets.interpreter_state.threads_head, &tstate) < 0) {
308+
goto unavailable;
309+
}
310+
while (tstate && remaining-- > 0) {
311+
char ts[SIZEOF_THREAD_STATE];
312+
if (_Py_RemoteDebug_ReadRemoteMemory(&unwinder->handle, tstate,
313+
unwinder->debug_offsets.thread_state.size, ts) < 0) {
314+
goto unavailable;
315+
}
316+
if (GET_MEMBER(long, ts, unwinder->debug_offsets.thread_state.native_thread_id) == tid &&
317+
GET_MEMBER(_thread_status, ts, unwinder->debug_offsets.thread_state.status).bound_gilstate) {
318+
uintptr_t frame = GET_MEMBER(uintptr_t, ts, unwinder->debug_offsets.thread_state.current_frame);
319+
uintptr_t base = GET_MEMBER(uintptr_t, ts, unwinder->debug_offsets.thread_state.base_frame);
320+
while (frame && frame != base && remaining-- > 0) {
321+
char data[SIZEOF_INTERP_FRAME];
322+
if (_Py_RemoteDebug_ReadRemoteMemory(&unwinder->handle, frame,
323+
sizeof(data), data) < 0) {
324+
goto unavailable;
325+
}
326+
int valid = is_frame_valid(unwinder, (uintptr_t)data,
327+
GET_MEMBER_NO_TAG(uintptr_t, data, unwinder->debug_offsets.interpreter_frame.executable));
328+
if (valid < 0) {
329+
goto unavailable;
330+
}
331+
if (valid == 1) {
332+
return 1;
333+
}
334+
frame = GET_MEMBER(uintptr_t, data, unwinder->debug_offsets.interpreter_frame.previous);
335+
}
336+
}
337+
tstate = GET_MEMBER(uintptr_t, ts, unwinder->debug_offsets.thread_state.next);
338+
}
339+
if (read_ptr(unwinder, interp +
340+
unwinder->debug_offsets.interpreter_state.next, &interp) < 0) {
341+
goto unavailable;
342+
}
343+
}
344+
return 0;
345+
346+
unavailable:
347+
// Keep the sample when remote state changes during the lookup.
348+
PyErr_Clear();
349+
return 0;
350+
}
351+
296352
static int
297353
read_thread_state_and_maybe_frame(
298354
RemoteUnwinderObject *unwinder,
@@ -335,8 +391,7 @@ unwind_stack_for_thread(
335391
uintptr_t gil_holder_tstate,
336392
uintptr_t gc_frame,
337393
uintptr_t main_thread_tstate,
338-
const RemoteReadPrefetch *prefetch,
339-
int *has_bound_stack
394+
const RemoteReadPrefetch *prefetch
340395
) {
341396
PyObject *frame_info = NULL;
342397
PyObject *thread_id = NULL;
@@ -502,7 +557,11 @@ unwind_stack_for_thread(
502557

503558
if (cpu_status == THREAD_STATE_UNKNOWN) {
504559
status_flags |= THREAD_STATUS_UNKNOWN;
505-
} else if (cpu_status == THREAD_STATE_RUNNING) {
560+
} else if (cpu_status == THREAD_STATE_RUNNING &&
561+
(GET_MEMBER(_thread_status, ts, unwinder->debug_offsets.thread_state.status).bound_gilstate ||
562+
!has_bound_stack(unwinder, tid))) {
563+
// An unbound state may still be doing native work after a callback.
564+
// Exclude it only if another bound stack shares the same OS thread.
506565
status_flags |= THREAD_STATUS_ON_CPU;
507566
}
508567

@@ -619,10 +678,6 @@ unwind_stack_for_thread(
619678
goto error;
620679
}
621680

622-
*has_bound_stack = GET_MEMBER(
623-
_thread_status, ts, unwinder->debug_offsets.thread_state.status).bound_gilstate
624-
&& PyList_GET_SIZE(frame_info) != 0;
625-
626681
// py_status contains status flags (bitfield)
627682
PyStructSequence_SetItem(result, 0, thread_id);
628683
PyStructSequence_SetItem(result, 1, py_status); // Steals reference

0 commit comments

Comments
 (0)