Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions Include/internal/pycore_debug_offsets.h
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ extern "C" {


#define _Py_Debug_Cookie "xdebugpy"
#define _Py_AsyncioDebug_Cookie "xasyncpy"

#if defined(__APPLE__)
# include <mach-o/loader.h>
Expand Down Expand Up @@ -73,6 +74,7 @@ typedef struct _Py_DebugOffsets {
uint64_t size;
uint64_t finalizing;
uint64_t interpreters_head;
uint64_t asyncio_debug_offsets;
} runtime_state;

// Interpreter state offset;
Expand Down Expand Up @@ -268,6 +270,7 @@ typedef struct _Py_DebugOffsets {
.size = sizeof(_PyRuntimeState), \
.finalizing = offsetof(_PyRuntimeState, _finalizing), \
.interpreters_head = offsetof(_PyRuntimeState, interpreters.head), \
.asyncio_debug_offsets = offsetof(_PyRuntimeState, asyncio_debug_offsets), \
}, \
.interpreter_state = { \
.size = sizeof(PyInterpreterState), \
Expand Down
5 changes: 5 additions & 0 deletions Include/internal/pycore_runtime_structs.h
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,11 @@ struct pyruntimestate {
*/
_Py_DebugOffsets debug_offsets;

/* Address of the _asyncio module's debug offsets table, or NULL until the
module is initialized. Debuggers locate this field through
debug_offsets.runtime_state.asyncio_debug_offsets. */
void *asyncio_debug_offsets;

/* Has been initialized to a safe state.

In order to be effective, this must be set to 0 during or right
Expand Down
39 changes: 38 additions & 1 deletion Lib/test/test_external_inspection.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
requires_remote_subprocess_debugging,
)
from test.support.import_helper import import_module
from test.support.script_helper import make_script
from test.support.script_helper import assert_python_ok, make_script
from test.support.socket_helper import find_unused_port

import subprocess
Expand Down Expand Up @@ -4747,5 +4747,42 @@ async def main():
)


class TestAsyncioDebugOffsets(unittest.TestCase):
def test_asyncio_debug_offsets_reachable_from_runtime(self):
import_module("ctypes")
code = textwrap.dedent("""
import ctypes
import struct
import sys

runtime = ctypes.addressof(
ctypes.c_char.in_dll(ctypes.pythonapi, "_PyRuntime"))
# _Py_DebugOffsets starts with cookie, version and free_threaded,
# followed by runtime_state: size, finalizing, interpreters_head
# and asyncio_debug_offsets.
cookie, version, _ = struct.unpack(
"=8sQQ", ctypes.string_at(runtime, 24))
assert cookie == b"xdebugpy", cookie
assert version == sys.hexversion, version
pointer_offset = struct.unpack(
"=Q", ctypes.string_at(runtime + 48, 8))[0]

def asyncio_table():
return ctypes.c_void_p.from_address(
runtime + pointer_offset).value

assert asyncio_table() is None
import _asyncio
address = asyncio_table()
assert address is not None
cookie, table_version, size = struct.unpack(
"=8sQQ", ctypes.string_at(address, 24))
assert cookie == b"xasyncpy", cookie
assert table_version == version, table_version
assert size == 16 * 8, size
""")
assert_python_ok("-S", "-c", code)


if __name__ == "__main__":
unittest.main()
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
The :mod:`!_asyncio` debug offsets table now starts with a cookie, version
and size, and its address is published in ``_PyRuntime`` so debuggers and
profilers can find it through ``_Py_DebugOffsets`` instead of searching
loaded binaries for the ``AsyncioDebug`` section. :mod:`!_remote_debugging`
now uses this pointer and validates the table header.
12 changes: 11 additions & 1 deletion Modules/_asynciomodule.c
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,9 @@ typedef struct {
|| PyObject_TypeCheck(obj, state->TaskType))

typedef struct _Py_AsyncioModuleDebugOffsets {
char cookie[8] _Py_NONSTRING;
uint64_t version;
uint64_t size;
struct _asyncio_task_object {
uint64_t size;
uint64_t task_name;
Expand All @@ -121,7 +124,10 @@ typedef struct _Py_AsyncioModuleDebugOffsets {
} Py_AsyncioModuleDebugOffsets;

GENERATE_DEBUG_SECTION(AsyncioDebug, Py_AsyncioModuleDebugOffsets _Py_AsyncioDebug)
= {.asyncio_task_object = {
= {.cookie = _Py_AsyncioDebug_Cookie,
.version = PY_VERSION_HEX,
.size = sizeof(Py_AsyncioModuleDebugOffsets),
.asyncio_task_object = {
.size = sizeof(TaskObj),
.task_name = offsetof(TaskObj, task_name),
.task_awaited_by = offsetof(TaskObj, task_awaited_by),
Expand Down Expand Up @@ -4315,6 +4321,10 @@ module_init(asyncio_state *state)
}

state->debug_offsets = &_Py_AsyncioDebug;
// Let debuggers find the table from _PyRuntime. Every interpreter stores
// the same address, and extension modules are never unloaded.
_Py_atomic_store_ptr_release(&_PyRuntime.asyncio_debug_offsets,
&_Py_AsyncioDebug);

Py_DECREF(module);
return 0;
Expand Down
4 changes: 3 additions & 1 deletion Modules/_remote_debugging/_remote_debugging.h
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,9 @@ typedef enum _WIN32_THREADSTATE {
* ============================================================================ */

struct _Py_AsyncioModuleDebugOffsets {
char cookie[8] _Py_NONSTRING;
uint64_t version;
uint64_t size;
struct _asyncio_task_object {
uint64_t size;
uint64_t task_name;
Expand Down Expand Up @@ -712,7 +715,6 @@ iterate_interpreters(
* ASYNCIO FUNCTION DECLARATIONS
* ============================================================================ */

extern uintptr_t _Py_RemoteDebug_GetAsyncioDebugAddress(proc_handle_t* handle);
extern int read_async_debug(RemoteUnwinderObject *unwinder);
extern int ensure_async_debug_offsets(RemoteUnwinderObject *unwinder);

Expand Down
66 changes: 21 additions & 45 deletions Modules/_remote_debugging/asyncio.c
Original file line number Diff line number Diff line change
Expand Up @@ -12,60 +12,33 @@
* ASYNCIO DEBUG ADDRESS FUNCTIONS
* ============================================================================ */

uintptr_t
_Py_RemoteDebug_GetAsyncioDebugAddress(proc_handle_t* handle)
static uintptr_t
get_async_debug_address(RemoteUnwinderObject *unwinder)
{
uintptr_t address;

#ifdef MS_WINDOWS
// On Windows, search for asyncio debug in executable or DLL
address = search_windows_map_for_section(handle, "AsyncioD", L"_asyncio",
NULL);
if (address == 0) {
if (!_Py_RemoteDebug_HasPermissionError()) {
PyObject *exc = PyErr_GetRaisedException();
PyErr_SetString(PyExc_RuntimeError, "Failed to find the AsyncioDebug section in the process.");
_PyErr_ChainExceptions1(exc);
}
}
#elif defined(__linux__) && HAVE_PROCESS_VM_READV
// On Linux, search for asyncio debug in executable or DLL
address = search_linux_map_for_section(handle, "AsyncioDebug", "python",
NULL);
if (address == 0) {
if (!_Py_RemoteDebug_HasPermissionError()) {
PyObject *exc = PyErr_GetRaisedException();
PyErr_SetString(PyExc_RuntimeError, "Failed to find the AsyncioDebug section in the process.");
_PyErr_ChainExceptions1(exc);
}
}
#elif defined(__APPLE__) && TARGET_OS_OSX
// On macOS, try libpython first, then fall back to python
address = search_map_for_section(handle, "AsyncioDebug", "libpython",
NULL);
if (address == 0 && !_Py_RemoteDebug_HasPermissionError()) {
PyErr_Clear();
address = search_map_for_section(handle, "AsyncioDebug", "python",
NULL);
// _asyncio publishes the address of its table in _PyRuntime when it is
// initialized. Read it without the page cache because it changes when the
// target process imports _asyncio.
uintptr_t pointer_addr = unwinder->runtime_start_address
+ (uintptr_t)unwinder->debug_offsets.runtime_state.asyncio_debug_offsets;
uintptr_t address = 0;
if (_Py_RemoteDebug_ReadRemoteMemory(&unwinder->handle, pointer_addr,
sizeof(address), &address) < 0)
{
set_exception_cause(unwinder, PyExc_RuntimeError,
"Failed to read AsyncioDebug address from PyRuntime");
return 0;
}
if (address == 0) {
if (!_Py_RemoteDebug_HasPermissionError()) {
PyObject *exc = PyErr_GetRaisedException();
PyErr_SetString(PyExc_RuntimeError, "Failed to find the AsyncioDebug section in the process.");
_PyErr_ChainExceptions1(exc);
}
PyErr_SetString(PyExc_RuntimeError,
"The _asyncio module is not initialized in the remote process");
}
#else
Py_UNREACHABLE();
#endif

return address;
}

int
read_async_debug(RemoteUnwinderObject *unwinder)
{
uintptr_t async_debug_addr = _Py_RemoteDebug_GetAsyncioDebugAddress(&unwinder->handle);
uintptr_t async_debug_addr = get_async_debug_address(unwinder);
if (!async_debug_addr) {
set_exception_cause(unwinder, PyExc_RuntimeError, "Failed to get AsyncioDebug address");
return -1;
Expand All @@ -77,7 +50,10 @@ read_async_debug(RemoteUnwinderObject *unwinder)
set_exception_cause(unwinder, PyExc_RuntimeError, "Failed to read AsyncioDebug offsets");
return result;
}
if (_PyRemoteDebug_ValidateAsyncDebugOffsets(&unwinder->async_debug_offsets) < 0) {
if (_PyRemoteDebug_ValidateAsyncDebugOffsets(
&unwinder->async_debug_offsets,
unwinder->debug_offsets.version) < 0)
{
set_exception_cause(unwinder, PyExc_RuntimeError, "Invalid AsyncioDebug offsets");
return PY_REMOTE_DEBUG_INVALID_ASYNC_DEBUG_OFFSETS;
}
Expand Down
35 changes: 31 additions & 4 deletions Modules/_remote_debugging/debug_offsets_validation.h
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,8 @@
#define FIELD_SIZE(type, member) sizeof(((type *)0)->member)

enum {
PY_REMOTE_DEBUG_OFFSETS_TOTAL_SIZE = 888,
PY_REMOTE_ASYNC_DEBUG_OFFSETS_TOTAL_SIZE = 104,
PY_REMOTE_DEBUG_OFFSETS_TOTAL_SIZE = 896,
PY_REMOTE_ASYNC_DEBUG_OFFSETS_TOTAL_SIZE = 128,
};

/*
Expand Down Expand Up @@ -256,7 +256,8 @@ validate_fixed_field(
* the unwinder, so no validation is needed for them.
*/
#define PY_REMOTE_DEBUG_RUNTIME_STATE_FIELDS(APPLY, buffer_size) \
APPLY(runtime_state, interpreters_head, sizeof(uintptr_t), _Alignof(uintptr_t), buffer_size)
APPLY(runtime_state, interpreters_head, sizeof(uintptr_t), _Alignof(uintptr_t), buffer_size); \
APPLY(runtime_state, asyncio_debug_offsets, sizeof(uintptr_t), _Alignof(uintptr_t), buffer_size)

/* current_exception also covers the adjacent exc_info pointer. */
#define PY_REMOTE_DEBUG_THREAD_STATE_FIELDS(APPLY, buffer_size) \
Expand Down Expand Up @@ -471,8 +472,34 @@ _PyRemoteDebug_ValidateDebugOffsetsLayout(struct _Py_DebugOffsets *debug_offsets
/* Called once when AsyncioDebug is loaded, before any task inspection uses it. */
static inline int
_PyRemoteDebug_ValidateAsyncDebugOffsets(
struct _Py_AsyncioModuleDebugOffsets *debug_offsets)
struct _Py_AsyncioModuleDebugOffsets *debug_offsets,
uint64_t runtime_version)
{
if (memcmp(debug_offsets->cookie, _Py_AsyncioDebug_Cookie,
sizeof(debug_offsets->cookie)) != 0)
{
PyErr_SetString(PyExc_RuntimeError,
"Invalid AsyncioDebug offsets: bad cookie");
return -1;
}
/* The table must come from the same build as the runtime. */
if (debug_offsets->version != runtime_version) {
PyErr_Format(
PyExc_RuntimeError,
"Invalid AsyncioDebug offsets: version %#llx does not match "
"runtime version %#llx",
(unsigned long long)debug_offsets->version,
(unsigned long long)runtime_version);
return -1;
}
if (debug_offsets->size < sizeof(*debug_offsets)) {
PyErr_Format(
PyExc_RuntimeError,
"Invalid AsyncioDebug offsets: size %llu is smaller than %zu",
(unsigned long long)debug_offsets->size,
sizeof(*debug_offsets));
return -1;
}
PY_REMOTE_DEBUG_VALIDATE_READ_SECTION(asyncio_task_object, SIZEOF_TASK_OBJ);
PY_REMOTE_DEBUG_TASK_OBJECT_FIELDS(
PY_REMOTE_DEBUG_VALIDATE_FIELD,
Expand Down
Loading