From 58756efc47ca7a57a07c6ec0ad284da01012c6b3 Mon Sep 17 00:00:00 2001 From: "Gregory P. Smith" Date: Thu, 2 Apr 2026 23:36:24 +0000 Subject: [PATCH 1/2] Validate async debug offsets read from remote process The _remote_debugging module reads async_debug_offsets from the target process's memory but did not validate them, unlike debug_offsets which go through validate_debug_offsets(). The asyncio_task_object.size field is used as the read length into fixed-size 4096-byte stack buffers (SIZEOF_TASK_OBJ); a malicious or compromised target process could supply a larger size and overflow the debugger's stack. Add validate_async_debug_offsets() and call it from read_async_debug() (the single chokepoint for loading these offsets) to bound the task object size and the member offsets that index into the local buffer. --- Modules/_remote_debugging/asyncio.c | 48 ++++++++++++++++++++++++++++- 1 file changed, 47 insertions(+), 1 deletion(-) diff --git a/Modules/_remote_debugging/asyncio.c b/Modules/_remote_debugging/asyncio.c index 263c502a857004..8abb63bdd2df9d 100644 --- a/Modules/_remote_debugging/asyncio.c +++ b/Modules/_remote_debugging/asyncio.c @@ -58,6 +58,47 @@ _Py_RemoteDebug_GetAsyncioDebugAddress(proc_handle_t* handle) return address; } +// The async debug offsets are read from the memory of the remote process and +// must be treated as untrusted: a compromised or malicious target could +// supply out-of-range values to overflow the fixed-size task_obj stack +// buffers used by the parsing helpers below. +static int +validate_async_debug_offsets(struct _Py_AsyncioModuleDebugOffsets *offsets) +{ + uint64_t task_size = offsets->asyncio_task_object.size; + if (task_size == 0 || task_size > SIZEOF_TASK_OBJ) { + PyErr_Format( + PyExc_RuntimeError, + "Invalid AsyncioDebug offsets: task object size %" PRIu64 + " is outside the supported range (1..%d)", + task_size, SIZEOF_TASK_OBJ); + return -1; + } + +#define CHECK_MEMBER(member, type) \ + do { \ + uint64_t off = offsets->asyncio_task_object.member; \ + if (off > task_size || task_size - off < sizeof(type)) { \ + PyErr_Format( \ + PyExc_RuntimeError, \ + "Invalid AsyncioDebug offsets: task object member '" \ + #member "' offset %" PRIu64 " is out of bounds for" \ + " task object size %" PRIu64, \ + off, task_size); \ + return -1; \ + } \ + } while (0) + + CHECK_MEMBER(task_name, uintptr_t); + CHECK_MEMBER(task_awaited_by, uintptr_t); + CHECK_MEMBER(task_awaited_by_is_set, uint8_t); + CHECK_MEMBER(task_coro, uintptr_t); + +#undef CHECK_MEMBER + + return 0; +} + int read_async_debug(RemoteUnwinderObject *unwinder) { @@ -71,8 +112,13 @@ read_async_debug(RemoteUnwinderObject *unwinder) int result = _Py_RemoteDebug_PagedReadRemoteMemory(&unwinder->handle, async_debug_addr, size, &unwinder->async_debug_offsets); if (result < 0) { set_exception_cause(unwinder, PyExc_RuntimeError, "Failed to read AsyncioDebug offsets"); + return -1; } - return result; + if (validate_async_debug_offsets(&unwinder->async_debug_offsets) < 0) { + set_exception_cause(unwinder, PyExc_RuntimeError, "Invalid AsyncioDebug offsets"); + return -1; + } + return 0; } int From 83bda8a9b1b05448aff78b1773e77b7876473582 Mon Sep 17 00:00:00 2001 From: "Gregory P. Smith" Date: Mon, 6 Apr 2026 17:03:21 +0000 Subject: [PATCH 2/2] Add NEWS entry for gh-148178 --- .../Library/2026-04-06-00-00-00.gh-issue-148178.aSdBg0.rst | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 Misc/NEWS.d/next/Library/2026-04-06-00-00-00.gh-issue-148178.aSdBg0.rst diff --git a/Misc/NEWS.d/next/Library/2026-04-06-00-00-00.gh-issue-148178.aSdBg0.rst b/Misc/NEWS.d/next/Library/2026-04-06-00-00-00.gh-issue-148178.aSdBg0.rst new file mode 100644 index 00000000000000..7e404a4bb65295 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-04-06-00-00-00.gh-issue-148178.aSdBg0.rst @@ -0,0 +1,5 @@ +Harden :mod:`!_remote_debugging` against malformed asyncio debug offset +tables in the target process. The size and member offsets read from the +remote ``AsyncioDebug`` section are now validated before use so that +out-of-range values raise :exc:`RuntimeError` instead of being consumed +by the unwinder.