Skip to content

Commit 1643525

Browse files
authored
gh-158552: Wait for Windows threads to suspend before blocking sampling (#158802)
* gh-158552: Wait for Windows threads to suspend before blocking sampling * Use a named Windows thread enumeration status constant
1 parent 3f02aab commit 1643525

4 files changed

Lines changed: 150 additions & 0 deletions

File tree

‎Lib/test/test_profiling/test_sampling_profiler/test_blocking.py‎

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import subprocess
66
import sys
77
import textwrap
8+
import time
89
import unittest
910
from unittest import mock
1011

@@ -213,3 +214,59 @@ def test_run_blocking_exits_after_target_process_exits(self):
213214
replay.returncode, 0,
214215
f"stdout:\n{replay.stdout}\nstderr:\n{replay.stderr}",
215216
)
217+
218+
219+
@requires_remote_subprocess_debugging()
220+
@unittest.skipUnless(sys.platform == "win32", "Windows only")
221+
class TestBlockingModeSuspension(unittest.TestCase):
222+
def test_all_threads_stop_before_pause_returns(self):
223+
import mmap
224+
225+
tag = f"cpython_blocking_{os.getpid()}_{id(self)}"
226+
script = textwrap.dedent(f'''
227+
import mmap
228+
import struct
229+
import threading
230+
231+
memory = mmap.mmap(-1, 16, tagname={tag!r})
232+
233+
def worker(offset):
234+
counter = 0
235+
while True:
236+
counter += 1
237+
struct.pack_into("q", memory, offset, counter)
238+
239+
for offset in (0, 8):
240+
threading.Thread(target=worker, args=(offset,), daemon=True).start()
241+
_test_sock.sendall(b"working")
242+
_test_sock.recv(1)
243+
''')
244+
with mmap.mmap(-1, 16, tagname=tag) as memory:
245+
with test_subprocess(script, wait_for_working=True) as subproc:
246+
unwinder = _remote_debugging.RemoteUnwinder(
247+
subproc.process.pid, all_threads=True)
248+
deadline = time.monotonic() + SHORT_TIMEOUT
249+
while not all(memory[offset:offset + 8] != bytes(8)
250+
for offset in (0, 8)):
251+
self.assertLess(time.monotonic(), deadline,
252+
"Worker threads did not start")
253+
time.sleep(0.001)
254+
for _ in range(100):
255+
self.assertTrue(unwinder.pause_threads())
256+
try:
257+
before = memory[:]
258+
self.assertFalse(unwinder.pause_threads())
259+
unwinder.get_stack_trace()
260+
time.sleep(0.001)
261+
self.assertEqual(memory[:], before,
262+
"Target memory changed while paused")
263+
finally:
264+
unwinder.resume_threads()
265+
self.assertFalse(unwinder.resume_threads())
266+
before = memory[:]
267+
deadline = time.monotonic() + SHORT_TIMEOUT
268+
while any(memory[offset:offset + 8] == before[offset:offset + 8]
269+
for offset in (0, 8)):
270+
self.assertLess(time.monotonic(), deadline,
271+
"Worker threads did not resume")
272+
time.sleep(0.001)
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
Wait for Windows threads to finish suspending before reading target memory
2+
in the sampling profiler's blocking mode. Resume the target if waiting for
3+
suspension fails.

‎Modules/_remote_debugging/_remote_debugging.h‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,7 @@ typedef struct {
105105
#ifdef MS_WINDOWS
106106
#define STATUS_SUCCESS ((NTSTATUS)0x00000000L)
107107
#define STATUS_INFO_LENGTH_MISMATCH ((NTSTATUS)0xC0000004L)
108+
#define STATUS_NO_MORE_ENTRIES ((NTSTATUS)0x8000001AL)
108109
typedef enum _WIN32_THREADSTATE {
109110
WIN32_THREADSTATE_INITIALIZED = 0,
110111
WIN32_THREADSTATE_READY = 1,

‎Modules/_remote_debugging/threads.c‎

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -828,6 +828,89 @@ _Py_RemoteDebug_ResumeAllThreads(RemoteUnwinderObject *unwinder, _Py_RemoteDebug
828828

829829
#elif defined(MS_WINDOWS)
830830

831+
static int
832+
wait_for_threads_to_stop(RemoteUnwinderObject *unwinder)
833+
{
834+
typedef NTSTATUS (NTAPI *NtGetNextThreadFunc)(
835+
HANDLE, HANDLE, ACCESS_MASK, ULONG, ULONG, PHANDLE);
836+
static NtGetNextThreadFunc pNtGetNextThread = NULL;
837+
static int tried_load = 0;
838+
839+
if (!tried_load) {
840+
HMODULE hNtdll = GetModuleHandleW(L"ntdll.dll");
841+
if (hNtdll) {
842+
pNtGetNextThread = (NtGetNextThreadFunc)GetProcAddress(
843+
hNtdll, "NtGetNextThread");
844+
}
845+
tried_load = 1;
846+
}
847+
if (pNtGetNextThread == NULL) {
848+
PyErr_SetString(PyExc_RuntimeError, "NtGetNextThread not available");
849+
return -1;
850+
}
851+
852+
HANDLE previous = NULL;
853+
int result = -1;
854+
for (;;) {
855+
HANDLE next = NULL;
856+
// Enumerate with the available access, then obtain context access
857+
// separately so threads that deny it are not silently skipped.
858+
NTSTATUS status = pNtGetNextThread(unwinder->handle.hProcess,
859+
previous, MAXIMUM_ALLOWED, 0, 0, &next);
860+
if (previous != NULL) {
861+
CloseHandle(previous);
862+
}
863+
previous = next;
864+
if (status == STATUS_NO_MORE_ENTRIES) {
865+
break;
866+
}
867+
if (status < 0) {
868+
if (!is_process_alive(unwinder->handle.hProcess)) {
869+
PyErr_Format(PyExc_ProcessLookupError,
870+
"Process %d has terminated", unwinder->handle.pid);
871+
}
872+
else {
873+
PyErr_Format(PyExc_RuntimeError,
874+
"NtGetNextThread failed: 0x%lx", status);
875+
}
876+
goto done;
877+
}
878+
HANDLE thread;
879+
if (!DuplicateHandle(GetCurrentProcess(), next, GetCurrentProcess(),
880+
&thread, THREAD_GET_CONTEXT | SYNCHRONIZE,
881+
FALSE, 0)) {
882+
PyErr_SetFromWindowsErr(GetLastError());
883+
goto done;
884+
}
885+
// Suspension is asynchronous. Reading the context waits for the
886+
// thread to stop before we start reading the target's memory.
887+
CONTEXT context = {.ContextFlags = CONTEXT_CONTROL};
888+
if (!GetThreadContext(thread, &context)) {
889+
DWORD error = GetLastError();
890+
int exited = WaitForSingleObject(thread, 0) == WAIT_OBJECT_0;
891+
CloseHandle(thread);
892+
if (exited) {
893+
continue;
894+
}
895+
PyErr_SetFromWindowsErr(error);
896+
goto done;
897+
}
898+
CloseHandle(thread);
899+
}
900+
if (!is_process_alive(unwinder->handle.hProcess)) {
901+
PyErr_Format(PyExc_ProcessLookupError,
902+
"Process %d has terminated", unwinder->handle.pid);
903+
goto done;
904+
}
905+
result = 0;
906+
907+
done:
908+
if (previous != NULL) {
909+
CloseHandle(previous);
910+
}
911+
return result;
912+
}
913+
831914
void
832915
_Py_RemoteDebug_InitThreadsState(RemoteUnwinderObject *unwinder, _Py_RemoteDebug_ThreadsState *st)
833916
{
@@ -858,6 +941,12 @@ _Py_RemoteDebug_StopAllThreads(RemoteUnwinderObject *unwinder, _Py_RemoteDebug_T
858941
if (status >= 0) {
859942
st->hProcess = unwinder->handle.hProcess;
860943
st->suspended = 1;
944+
if (wait_for_threads_to_stop(unwinder) < 0) {
945+
// pause_threads() has not yet set threads_stopped, so its caller
946+
// will not resume the process when we return an error.
947+
_Py_RemoteDebug_ResumeAllThreads(unwinder, st);
948+
return -1;
949+
}
861950
_Py_RemoteDebug_ClearCache(&unwinder->handle);
862951
return 0;
863952
}

0 commit comments

Comments
 (0)