From f6f2faf578dd7ab28c2e7ff621103518a9544035 Mon Sep 17 00:00:00 2001 From: Pablo Galindo Salgado Date: Mon, 6 Apr 2026 21:57:13 +0100 Subject: [PATCH 1/8] gh-148178: Validate remote debug offset tables on load Treat the debug offset tables read from a target process as untrusted input and validate them before the unwinder uses any reported sizes or offsets. Add a shared validator in debug_offsets_validation.h and run it once when _Py_DebugOffsets is loaded and once when AsyncioDebug is loaded. The checks cover section sizes used for fixed local buffers and every offset that is later dereferenced against a local buffer or local object view. This keeps the bounds checks out of the sampling hot path while rejecting malformed tables up front. --- ...-04-06-13-55-00.gh-issue-148178.Rs7kLm.rst | 2 + Modules/_remote_debugging/_remote_debugging.h | 1 + Modules/_remote_debugging/asyncio.c | 14 +- .../debug_offsets_validation.h | 430 ++++++++++++++++++ Modules/_remote_debugging/frames.c | 4 +- Modules/_remote_debugging/module.c | 9 +- 6 files changed, 455 insertions(+), 5 deletions(-) create mode 100644 Misc/NEWS.d/next/Security/2026-04-06-13-55-00.gh-issue-148178.Rs7kLm.rst create mode 100644 Modules/_remote_debugging/debug_offsets_validation.h diff --git a/Misc/NEWS.d/next/Security/2026-04-06-13-55-00.gh-issue-148178.Rs7kLm.rst b/Misc/NEWS.d/next/Security/2026-04-06-13-55-00.gh-issue-148178.Rs7kLm.rst new file mode 100644 index 00000000000000..ed138a54a859de --- /dev/null +++ b/Misc/NEWS.d/next/Security/2026-04-06-13-55-00.gh-issue-148178.Rs7kLm.rst @@ -0,0 +1,2 @@ +Hardened :mod:`!_remote_debugging` by validating remote debug offset tables +before using them to size memory reads or interpret remote layouts. diff --git a/Modules/_remote_debugging/_remote_debugging.h b/Modules/_remote_debugging/_remote_debugging.h index 3722273dfd2998..aaa9d515a3b909 100644 --- a/Modules/_remote_debugging/_remote_debugging.h +++ b/Modules/_remote_debugging/_remote_debugging.h @@ -417,6 +417,7 @@ extern void cached_code_metadata_destroy(void *ptr); /* Validation */ extern int is_prerelease_version(uint64_t version); extern int validate_debug_offsets(struct _Py_DebugOffsets *debug_offsets); +#define PY_REMOTE_DEBUG_INVALID_ASYNC_DEBUG_OFFSETS (-2) /* ============================================================================ * MEMORY READING FUNCTION DECLARATIONS diff --git a/Modules/_remote_debugging/asyncio.c b/Modules/_remote_debugging/asyncio.c index 263c502a857004..fc7487d4044bfb 100644 --- a/Modules/_remote_debugging/asyncio.c +++ b/Modules/_remote_debugging/asyncio.c @@ -6,6 +6,7 @@ ******************************************************************************/ #include "_remote_debugging.h" +#include "debug_offsets_validation.h" /* ============================================================================ * ASYNCIO DEBUG ADDRESS FUNCTIONS @@ -71,8 +72,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 result; } - return result; + if (_PyRemoteDebug_ValidateAsyncDebugOffsets(&unwinder->async_debug_offsets) < 0) { + set_exception_cause(unwinder, PyExc_RuntimeError, "Invalid AsyncioDebug offsets"); + return PY_REMOTE_DEBUG_INVALID_ASYNC_DEBUG_OFFSETS; + } + return 0; } int @@ -85,7 +91,11 @@ ensure_async_debug_offsets(RemoteUnwinderObject *unwinder) // Try to load async debug offsets (the target process may have // loaded asyncio since we last checked) - if (read_async_debug(unwinder) < 0) { + int result = read_async_debug(unwinder); + if (result == PY_REMOTE_DEBUG_INVALID_ASYNC_DEBUG_OFFSETS) { + return -1; + } + if (result < 0) { PyErr_Clear(); PyErr_SetString(PyExc_RuntimeError, "AsyncioDebug section not available"); set_exception_cause(unwinder, PyExc_RuntimeError, diff --git a/Modules/_remote_debugging/debug_offsets_validation.h b/Modules/_remote_debugging/debug_offsets_validation.h new file mode 100644 index 00000000000000..bd264caa5ad643 --- /dev/null +++ b/Modules/_remote_debugging/debug_offsets_validation.h @@ -0,0 +1,430 @@ +#ifndef Py_REMOTE_DEBUG_OFFSETS_VALIDATION_H +#define Py_REMOTE_DEBUG_OFFSETS_VALIDATION_H + +/* + * The remote debugging tables are read from the target process and must be + * treated as untrusted input. This header centralizes the one-time validation + * that runs immediately after those tables are read, before the unwinder uses + * any reported sizes or offsets to copy remote structs into fixed local + * buffers or to interpret those local copies. + * + * The key rule is simple: every offset that is later dereferenced against a + * local buffer or local object view must appear in one of the field lists + * below. Validation then checks two bounds for each field: + * + * 1. The field must fit within the section size reported by the target. + * 2. The same field must also fit within the local buffer or local layout the + * debugger will actually use. + * + * Sections that are copied into fixed local buffers also have their reported + * size checked against the corresponding local buffer size up front. + * + * This is intentionally front-loaded. Once validation succeeds, the hot path + * can keep using the raw offsets without adding per-sample bounds checks. + * + * Maintenance rule: if either exported table grows, the static_asserts below + * should yell at you. When that happens, update the matching field lists in + * this file in the same change. And if you add a new field that the unwinder + * is going to poke at later, put it in the right list here too, so nobody has + * to rediscover this the annoying way. + */ +#define FIELD_SIZE(type, member) sizeof(((type *)0)->member) + +enum { + PY_REMOTE_DEBUG_OFFSETS_TOTAL_SIZE = 840, + PY_REMOTE_ASYNC_DEBUG_OFFSETS_TOTAL_SIZE = 104, +}; + +/* + * These asserts are the coordination tripwire for table growth. If either + * exported table changes size, update the validation lists below in the same + * change. + */ +static_assert( + sizeof(_Py_DebugOffsets) == PY_REMOTE_DEBUG_OFFSETS_TOTAL_SIZE, + "Update _remote_debugging validation for _Py_DebugOffsets"); +static_assert( + sizeof(struct _Py_AsyncioModuleDebugOffsets) == + PY_REMOTE_ASYNC_DEBUG_OFFSETS_TOTAL_SIZE, + "Update _remote_debugging validation for _Py_AsyncioModuleDebugOffsets"); + +/* + * This logic lives in a private header because it is shared by module.c and + * asyncio.c. Keep the helpers static inline so they stay local to those users + * without adding another compilation unit or exported symbols. + */ +static inline int +validate_section_size(const char *section_name, uint64_t size) +{ + if (size == 0) { + PyErr_Format( + PyExc_RuntimeError, + "Invalid debug offsets: %s.size must be greater than zero", + section_name); + return -1; + } + return 0; +} + +static inline int +validate_read_size(const char *section_name, uint64_t size, size_t buffer_size) +{ + if (validate_section_size(section_name, size) < 0) { + return -1; + } + if (size > buffer_size) { + PyErr_Format( + PyExc_RuntimeError, + "Invalid debug offsets: %s.size=%llu exceeds local buffer size %zu", + section_name, + (unsigned long long)size, + buffer_size); + return -1; + } + return 0; +} + +static inline int +validate_span( + const char *field_name, + uint64_t offset, + size_t width, + uint64_t limit, + const char *limit_name) +{ + uint64_t span = (uint64_t)width; + if (span > limit || offset > limit - span) { + PyErr_Format( + PyExc_RuntimeError, + "Invalid debug offsets: %s=%llu with width %zu exceeds %s %llu", + field_name, + (unsigned long long)offset, + width, + limit_name, + (unsigned long long)limit); + return -1; + } + return 0; +} + +static inline int +validate_field( + const char *field_name, + uint64_t reported_size, + uint64_t offset, + size_t width, + size_t buffer_size) +{ + if (validate_span(field_name, offset, width, reported_size, "reported size") < 0) { + return -1; + } + return validate_span(field_name, offset, width, buffer_size, "local buffer size"); +} + +static inline int +validate_nested_field( + const char *field_name, + uint64_t reported_size, + uint64_t base_offset, + uint64_t nested_offset, + size_t width, + size_t buffer_size) +{ + if (base_offset > UINT64_MAX - nested_offset) { + PyErr_Format( + PyExc_RuntimeError, + "Invalid debug offsets: %s overflows the offset calculation", + field_name); + return -1; + } + return validate_field( + field_name, + reported_size, + base_offset + nested_offset, + width, + buffer_size); +} + +static inline int +validate_fixed_field( + const char *field_name, + uint64_t offset, + size_t width, + size_t buffer_size) +{ + return validate_span(field_name, offset, width, buffer_size, "local buffer size"); +} + +#define PY_REMOTE_DEBUG_VALIDATE_SECTION(section) \ + do { \ + if (validate_section_size(#section, debug_offsets->section.size) < 0) { \ + return -1; \ + } \ + } while (0) + +#define PY_REMOTE_DEBUG_VALIDATE_READ_SECTION(section, buffer_size) \ + do { \ + if (validate_read_size(#section, debug_offsets->section.size, buffer_size) < 0) { \ + return -1; \ + } \ + } while (0) + +#define PY_REMOTE_DEBUG_VALIDATE_FIELD(section, field, field_size, buffer_size) \ + do { \ + if (validate_field( \ + #section "." #field, \ + debug_offsets->section.size, \ + debug_offsets->section.field, \ + field_size, \ + buffer_size) < 0) { \ + return -1; \ + } \ + } while (0) + +#define PY_REMOTE_DEBUG_VALIDATE_NESTED_FIELD(section, base, nested_section, field, field_size, buffer_size) \ + do { \ + if (validate_nested_field( \ + #section "." #base "." #field, \ + debug_offsets->section.size, \ + debug_offsets->section.base, \ + debug_offsets->nested_section.field, \ + field_size, \ + buffer_size) < 0) { \ + return -1; \ + } \ + } while (0) + +#define PY_REMOTE_DEBUG_VALIDATE_FIXED_FIELD(section, field, field_size, buffer_size) \ + do { \ + if (validate_fixed_field( \ + #section "." #field, \ + debug_offsets->section.field, \ + field_size, \ + buffer_size) < 0) { \ + return -1; \ + } \ + } while (0) + +/* + * Each list below must include every offset that is later dereferenced against + * a local buffer or local object view. The validator checks that each field + * stays within both the remote table's reported section size and the local + * buffer size we use when reading that section. If a new dereferenced field is + * added to the offset tables, add it to the matching list here. + */ +#define PY_REMOTE_DEBUG_RUNTIME_STATE_FIELDS(APPLY, buffer_size) \ + APPLY(runtime_state, interpreters_head, sizeof(uintptr_t), buffer_size) + +#define PY_REMOTE_DEBUG_THREAD_STATE_FIELDS(APPLY, buffer_size) \ + APPLY(thread_state, native_thread_id, sizeof(long), buffer_size); \ + APPLY(thread_state, interp, sizeof(uintptr_t), buffer_size); \ + APPLY(thread_state, datastack_chunk, sizeof(uintptr_t), buffer_size); \ + APPLY(thread_state, status, FIELD_SIZE(PyThreadState, _status), buffer_size); \ + APPLY(thread_state, holds_gil, sizeof(int), buffer_size); \ + APPLY(thread_state, gil_requested, sizeof(int), buffer_size); \ + APPLY(thread_state, current_exception, sizeof(uintptr_t), buffer_size); \ + APPLY(thread_state, thread_id, sizeof(long), buffer_size); \ + APPLY(thread_state, next, sizeof(uintptr_t), buffer_size); \ + APPLY(thread_state, current_frame, sizeof(uintptr_t), buffer_size); \ + APPLY(thread_state, base_frame, sizeof(uintptr_t), buffer_size); \ + APPLY(thread_state, last_profiled_frame, sizeof(uintptr_t), buffer_size) + +#define PY_REMOTE_DEBUG_INTERPRETER_STATE_FIELDS(APPLY, buffer_size) \ + APPLY(interpreter_state, id, sizeof(int64_t), buffer_size); \ + APPLY(interpreter_state, next, sizeof(uintptr_t), buffer_size); \ + APPLY(interpreter_state, threads_head, sizeof(uintptr_t), buffer_size); \ + APPLY(interpreter_state, threads_main, sizeof(uintptr_t), buffer_size); \ + APPLY(interpreter_state, gil_runtime_state_locked, sizeof(int), buffer_size); \ + APPLY(interpreter_state, gil_runtime_state_holder, sizeof(PyThreadState *), buffer_size); \ + APPLY(interpreter_state, code_object_generation, sizeof(uint64_t), buffer_size); \ + APPLY(interpreter_state, tlbc_generation, sizeof(uint32_t), buffer_size) + +#define PY_REMOTE_DEBUG_INTERPRETER_FRAME_FIELDS(APPLY, buffer_size) \ + APPLY(interpreter_frame, previous, sizeof(uintptr_t), buffer_size); \ + APPLY(interpreter_frame, executable, sizeof(uintptr_t), buffer_size); \ + APPLY(interpreter_frame, instr_ptr, sizeof(uintptr_t), buffer_size); \ + APPLY(interpreter_frame, owner, sizeof(char), buffer_size); \ + APPLY(interpreter_frame, stackpointer, sizeof(uintptr_t), buffer_size); \ + APPLY(interpreter_frame, tlbc_index, sizeof(int32_t), buffer_size) + +#define PY_REMOTE_DEBUG_CODE_OBJECT_FIELDS(APPLY, buffer_size) \ + APPLY(code_object, qualname, sizeof(uintptr_t), buffer_size); \ + APPLY(code_object, filename, sizeof(uintptr_t), buffer_size); \ + APPLY(code_object, linetable, sizeof(uintptr_t), buffer_size); \ + APPLY(code_object, firstlineno, sizeof(int), buffer_size); \ + APPLY(code_object, co_code_adaptive, sizeof(char), buffer_size); \ + APPLY(code_object, co_tlbc, sizeof(uintptr_t), buffer_size) + +#define PY_REMOTE_DEBUG_SET_OBJECT_FIELDS(APPLY, buffer_size) \ + APPLY(set_object, used, sizeof(Py_ssize_t), buffer_size); \ + APPLY(set_object, mask, sizeof(Py_ssize_t), buffer_size); \ + APPLY(set_object, table, sizeof(uintptr_t), buffer_size) + +#define PY_REMOTE_DEBUG_LONG_OBJECT_FIELDS(APPLY, buffer_size) \ + APPLY(long_object, lv_tag, sizeof(uintptr_t), buffer_size); \ + APPLY(long_object, ob_digit, sizeof(digit), buffer_size) + +#define PY_REMOTE_DEBUG_BYTES_OBJECT_FIELDS(APPLY, buffer_size) \ + APPLY(bytes_object, ob_size, sizeof(Py_ssize_t), buffer_size); \ + APPLY(bytes_object, ob_sval, sizeof(char), buffer_size) + +#define PY_REMOTE_DEBUG_UNICODE_OBJECT_FIELDS(APPLY, buffer_size) \ + APPLY(unicode_object, length, sizeof(Py_ssize_t), buffer_size); \ + APPLY(unicode_object, asciiobject_size, sizeof(char), buffer_size) + +#define PY_REMOTE_DEBUG_GEN_OBJECT_FIELDS(APPLY, buffer_size) \ + APPLY(gen_object, gi_frame_state, sizeof(int8_t), buffer_size); \ + APPLY(gen_object, gi_iframe, FIELD_SIZE(PyGenObject, gi_iframe), buffer_size) + +#define PY_REMOTE_DEBUG_TASK_OBJECT_FIELDS(APPLY, buffer_size) \ + APPLY(asyncio_task_object, task_name, sizeof(uintptr_t), buffer_size); \ + APPLY(asyncio_task_object, task_awaited_by, sizeof(uintptr_t), buffer_size); \ + APPLY(asyncio_task_object, task_is_task, sizeof(char), buffer_size); \ + APPLY(asyncio_task_object, task_awaited_by_is_set, sizeof(char), buffer_size); \ + APPLY(asyncio_task_object, task_coro, sizeof(uintptr_t), buffer_size); \ + APPLY(asyncio_task_object, task_node, SIZEOF_LLIST_NODE, buffer_size) + +#define PY_REMOTE_DEBUG_ASYNC_INTERPRETER_STATE_FIELDS(APPLY, buffer_size) \ + APPLY(asyncio_interpreter_state, asyncio_tasks_head, SIZEOF_LLIST_NODE, buffer_size) + +#define PY_REMOTE_DEBUG_ASYNC_THREAD_STATE_FIELDS(APPLY, buffer_size) \ + APPLY(asyncio_thread_state, asyncio_running_loop, sizeof(uintptr_t), buffer_size); \ + APPLY(asyncio_thread_state, asyncio_running_task, sizeof(uintptr_t), buffer_size); \ + APPLY(asyncio_thread_state, asyncio_tasks_head, SIZEOF_LLIST_NODE, buffer_size) + +/* Called once after reading _Py_DebugOffsets, before any hot-path use. */ +static inline int +_PyRemoteDebug_ValidateDebugOffsetsLayout(struct _Py_DebugOffsets *debug_offsets) +{ + PY_REMOTE_DEBUG_VALIDATE_SECTION(runtime_state); + PY_REMOTE_DEBUG_RUNTIME_STATE_FIELDS( + PY_REMOTE_DEBUG_VALIDATE_FIELD, + sizeof(_PyRuntimeState)); + + PY_REMOTE_DEBUG_VALIDATE_SECTION(interpreter_state); + PY_REMOTE_DEBUG_INTERPRETER_STATE_FIELDS( + PY_REMOTE_DEBUG_VALIDATE_FIELD, + INTERP_STATE_BUFFER_SIZE); + + PY_REMOTE_DEBUG_VALIDATE_READ_SECTION(thread_state, SIZEOF_THREAD_STATE); + PY_REMOTE_DEBUG_THREAD_STATE_FIELDS( + PY_REMOTE_DEBUG_VALIDATE_FIELD, + SIZEOF_THREAD_STATE); + PY_REMOTE_DEBUG_VALIDATE_FIXED_FIELD( + err_stackitem, + exc_value, + sizeof(uintptr_t), + sizeof(_PyErr_StackItem)); + PY_REMOTE_DEBUG_VALIDATE_NESTED_FIELD( + thread_state, + exc_state, + err_stackitem, + exc_value, + sizeof(uintptr_t), + SIZEOF_THREAD_STATE); + + PY_REMOTE_DEBUG_VALIDATE_READ_SECTION(gc, SIZEOF_GC_RUNTIME_STATE); + PY_REMOTE_DEBUG_VALIDATE_FIELD(gc, frame, sizeof(uintptr_t), SIZEOF_GC_RUNTIME_STATE); + PY_REMOTE_DEBUG_VALIDATE_NESTED_FIELD( + interpreter_state, + gc, + gc, + frame, + sizeof(uintptr_t), + INTERP_STATE_BUFFER_SIZE); + + PY_REMOTE_DEBUG_VALIDATE_SECTION(interpreter_frame); + PY_REMOTE_DEBUG_INTERPRETER_FRAME_FIELDS( + PY_REMOTE_DEBUG_VALIDATE_FIELD, + SIZEOF_INTERP_FRAME); + + PY_REMOTE_DEBUG_VALIDATE_SECTION(code_object); + PY_REMOTE_DEBUG_CODE_OBJECT_FIELDS( + PY_REMOTE_DEBUG_VALIDATE_FIELD, + SIZEOF_CODE_OBJ); + + PY_REMOTE_DEBUG_VALIDATE_SECTION(pyobject); + PY_REMOTE_DEBUG_VALIDATE_FIELD(pyobject, ob_type, sizeof(uintptr_t), SIZEOF_PYOBJECT); + + PY_REMOTE_DEBUG_VALIDATE_SECTION(type_object); + PY_REMOTE_DEBUG_VALIDATE_FIELD( + type_object, + tp_flags, + sizeof(unsigned long), + SIZEOF_TYPE_OBJ); + + PY_REMOTE_DEBUG_VALIDATE_SECTION(set_object); + PY_REMOTE_DEBUG_SET_OBJECT_FIELDS( + PY_REMOTE_DEBUG_VALIDATE_FIELD, + SIZEOF_SET_OBJ); + + PY_REMOTE_DEBUG_VALIDATE_READ_SECTION(long_object, SIZEOF_LONG_OBJ); + PY_REMOTE_DEBUG_LONG_OBJECT_FIELDS( + PY_REMOTE_DEBUG_VALIDATE_FIELD, + SIZEOF_LONG_OBJ); + + PY_REMOTE_DEBUG_VALIDATE_SECTION(bytes_object); + PY_REMOTE_DEBUG_BYTES_OBJECT_FIELDS( + PY_REMOTE_DEBUG_VALIDATE_FIELD, + SIZEOF_BYTES_OBJ); + + PY_REMOTE_DEBUG_VALIDATE_SECTION(unicode_object); + PY_REMOTE_DEBUG_UNICODE_OBJECT_FIELDS( + PY_REMOTE_DEBUG_VALIDATE_FIELD, + SIZEOF_UNICODE_OBJ); + + PY_REMOTE_DEBUG_VALIDATE_SECTION(gen_object); + PY_REMOTE_DEBUG_GEN_OBJECT_FIELDS( + PY_REMOTE_DEBUG_VALIDATE_FIELD, + SIZEOF_GEN_OBJ); + + PY_REMOTE_DEBUG_VALIDATE_FIXED_FIELD( + llist_node, + next, + sizeof(uintptr_t), + SIZEOF_LLIST_NODE); + + return 0; +} + +/* Called once when AsyncioDebug is loaded, before any task inspection uses it. */ +static inline int +_PyRemoteDebug_ValidateAsyncDebugOffsets( + struct _Py_AsyncioModuleDebugOffsets *debug_offsets) +{ + PY_REMOTE_DEBUG_VALIDATE_READ_SECTION(asyncio_task_object, SIZEOF_TASK_OBJ); + PY_REMOTE_DEBUG_TASK_OBJECT_FIELDS( + PY_REMOTE_DEBUG_VALIDATE_FIELD, + SIZEOF_TASK_OBJ); + PY_REMOTE_DEBUG_VALIDATE_SECTION(asyncio_interpreter_state); + PY_REMOTE_DEBUG_ASYNC_INTERPRETER_STATE_FIELDS( + PY_REMOTE_DEBUG_VALIDATE_FIELD, + sizeof(PyInterpreterState)); + PY_REMOTE_DEBUG_VALIDATE_SECTION(asyncio_thread_state); + PY_REMOTE_DEBUG_ASYNC_THREAD_STATE_FIELDS( + PY_REMOTE_DEBUG_VALIDATE_FIELD, + sizeof(_PyThreadStateImpl)); + return 0; +} + +#undef PY_REMOTE_DEBUG_VALIDATE_SECTION +#undef PY_REMOTE_DEBUG_VALIDATE_READ_SECTION +#undef PY_REMOTE_DEBUG_VALIDATE_FIELD +#undef PY_REMOTE_DEBUG_VALIDATE_NESTED_FIELD +#undef PY_REMOTE_DEBUG_VALIDATE_FIXED_FIELD +#undef PY_REMOTE_DEBUG_RUNTIME_STATE_FIELDS +#undef PY_REMOTE_DEBUG_THREAD_STATE_FIELDS +#undef PY_REMOTE_DEBUG_INTERPRETER_STATE_FIELDS +#undef PY_REMOTE_DEBUG_INTERPRETER_FRAME_FIELDS +#undef PY_REMOTE_DEBUG_CODE_OBJECT_FIELDS +#undef PY_REMOTE_DEBUG_SET_OBJECT_FIELDS +#undef PY_REMOTE_DEBUG_LONG_OBJECT_FIELDS +#undef PY_REMOTE_DEBUG_BYTES_OBJECT_FIELDS +#undef PY_REMOTE_DEBUG_UNICODE_OBJECT_FIELDS +#undef PY_REMOTE_DEBUG_GEN_OBJECT_FIELDS +#undef PY_REMOTE_DEBUG_TASK_OBJECT_FIELDS +#undef PY_REMOTE_DEBUG_ASYNC_INTERPRETER_STATE_FIELDS +#undef PY_REMOTE_DEBUG_ASYNC_THREAD_STATE_FIELDS +#undef FIELD_SIZE + +#endif diff --git a/Modules/_remote_debugging/frames.c b/Modules/_remote_debugging/frames.c index a0b4a1e8a1e542..bbdfce3f7201d9 100644 --- a/Modules/_remote_debugging/frames.c +++ b/Modules/_remote_debugging/frames.c @@ -148,7 +148,9 @@ find_frame_in_chunks(StackChunkList *chunks, uintptr_t remote_ptr) uintptr_t base = chunks->chunks[i].remote_addr + offsetof(_PyStackChunk, data); size_t payload = chunks->chunks[i].size - offsetof(_PyStackChunk, data); - if (remote_ptr >= base && remote_ptr < base + payload) { + if (payload >= SIZEOF_INTERP_FRAME && + remote_ptr >= base && + remote_ptr <= base + payload - SIZEOF_INTERP_FRAME) { return (char *)chunks->chunks[i].local_copy + (remote_ptr - chunks->chunks[i].remote_addr); } } diff --git a/Modules/_remote_debugging/module.c b/Modules/_remote_debugging/module.c index f86bbf8ce5526e..32f2cbacf2143b 100644 --- a/Modules/_remote_debugging/module.c +++ b/Modules/_remote_debugging/module.c @@ -7,6 +7,7 @@ #include "_remote_debugging.h" #include "binary_io.h" +#include "debug_offsets_validation.h" /* Forward declarations for clinic-generated code */ typedef struct { @@ -240,7 +241,7 @@ validate_debug_offsets(struct _Py_DebugOffsets *debug_offsets) return -1; } - return 0; + return _PyRemoteDebug_ValidateDebugOffsetsLayout(debug_offsets); } /* ============================================================================ @@ -374,7 +375,11 @@ _remote_debugging_RemoteUnwinder___init___impl(RemoteUnwinderObject *self, // Try to read async debug offsets, but don't fail if they're not available self->async_debug_offsets_available = 1; - if (read_async_debug(self) < 0) { + int async_debug_result = read_async_debug(self); + if (async_debug_result == PY_REMOTE_DEBUG_INVALID_ASYNC_DEBUG_OFFSETS) { + return -1; + } + if (async_debug_result < 0) { PyErr_Clear(); memset(&self->async_debug_offsets, 0, sizeof(self->async_debug_offsets)); self->async_debug_offsets_available = 0; From 215390a0efeffad298a09dc40b171d139b601e21 Mon Sep 17 00:00:00 2001 From: Pablo Galindo Salgado Date: Sun, 12 Apr 2026 02:38:12 +0100 Subject: [PATCH 2/8] Apply suggestions from code review Co-authored-by: Gregory P. Smith <68491+gpshead@users.noreply.github.com> --- .../debug_offsets_validation.h | 35 +++++++++++-------- 1 file changed, 21 insertions(+), 14 deletions(-) diff --git a/Modules/_remote_debugging/debug_offsets_validation.h b/Modules/_remote_debugging/debug_offsets_validation.h index bd264caa5ad643..806754df528fdb 100644 --- a/Modules/_remote_debugging/debug_offsets_validation.h +++ b/Modules/_remote_debugging/debug_offsets_validation.h @@ -206,24 +206,27 @@ validate_fixed_field( } while (0) /* - * Each list below must include every offset that is later dereferenced against - * a local buffer or local object view. The validator checks that each field - * stays within both the remote table's reported section size and the local - * buffer size we use when reading that section. If a new dereferenced field is - * added to the offset tables, add it to the matching list here. + * Each list below must include every offset that is later dereferenced against + * a local buffer or local object view. The validator checks that each field + * stays within both the remote table's reported section size and the local + * buffer size we use when reading that section. If a new dereferenced field is + * added to the offset tables, add it to the matching list here. + * + * Sections not listed here are present in the offset tables but not used by + * 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), buffer_size) #define PY_REMOTE_DEBUG_THREAD_STATE_FIELDS(APPLY, buffer_size) \ - APPLY(thread_state, native_thread_id, sizeof(long), buffer_size); \ - APPLY(thread_state, interp, sizeof(uintptr_t), buffer_size); \ - APPLY(thread_state, datastack_chunk, sizeof(uintptr_t), buffer_size); \ - APPLY(thread_state, status, FIELD_SIZE(PyThreadState, _status), buffer_size); \ - APPLY(thread_state, holds_gil, sizeof(int), buffer_size); \ - APPLY(thread_state, gil_requested, sizeof(int), buffer_size); \ - APPLY(thread_state, current_exception, sizeof(uintptr_t), buffer_size); \ - APPLY(thread_state, thread_id, sizeof(long), buffer_size); \ + APPLY(thread_state, native_thread_id, sizeof(unsigned long), buffer_size); \ + APPLY(thread_state, interp, sizeof(uintptr_t), buffer_size); \ + APPLY(thread_state, datastack_chunk, sizeof(uintptr_t), buffer_size); \ + APPLY(thread_state, status, FIELD_SIZE(PyThreadState, _status), buffer_size); \ + APPLY(thread_state, holds_gil, sizeof(int), buffer_size); \ + APPLY(thread_state, gil_requested, sizeof(int), buffer_size); \ + APPLY(thread_state, current_exception, sizeof(uintptr_t), buffer_size); \ + APPLY(thread_state, thread_id, sizeof(unsigned long), buffer_size); \ APPLY(thread_state, next, sizeof(uintptr_t), buffer_size); \ APPLY(thread_state, current_frame, sizeof(uintptr_t), buffer_size); \ APPLY(thread_state, base_frame, sizeof(uintptr_t), buffer_size); \ @@ -296,7 +299,11 @@ validate_fixed_field( static inline int _PyRemoteDebug_ValidateDebugOffsetsLayout(struct _Py_DebugOffsets *debug_offsets) { - PY_REMOTE_DEBUG_VALIDATE_SECTION(runtime_state); + /* Validate every field the unwinder dereferences against a local buffer + * or local object view. Fields used only for remote address arithmetic + * (e.g. runtime_state.interpreters_head) are also checked as a sanity + * bound on the offset value. */ + PY_REMOTE_DEBUG_VALIDATE_SECTION(runtime_state); PY_REMOTE_DEBUG_RUNTIME_STATE_FIELDS( PY_REMOTE_DEBUG_VALIDATE_FIELD, sizeof(_PyRuntimeState)); From d3b0fb0abd799f061eccad063f99c78d8d731392 Mon Sep 17 00:00:00 2001 From: maurycy <5383+maurycy@users.noreply.github.com> Date: Sun, 12 Apr 2026 20:57:39 +0200 Subject: [PATCH 3/8] bring back https://github.com/python/cpython/compare/f12f6c4f0467837e25a445339f2e5a24388a317d..b8dcd8fa2b3be846b1dad6d2afb1dccd968789b3 --- Lib/test/test_external_inspection.py | 86 ++++++++++++++++++++++++++++ 1 file changed, 86 insertions(+) diff --git a/Lib/test/test_external_inspection.py b/Lib/test/test_external_inspection.py index ec7192b1b89184..3e07b5996521b7 100644 --- a/Lib/test/test_external_inspection.py +++ b/Lib/test/test_external_inspection.py @@ -1,5 +1,6 @@ import unittest import os +import json import textwrap import contextlib import importlib @@ -418,6 +419,91 @@ def _frame_to_lineno_tuple(frame): filename, location, funcname, opcode = frame return (filename, location.lineno, funcname, opcode) + @contextmanager + def _poisoned_debug_offsets_process(self, poison_code): + script = ( + textwrap.dedent( + """\ + import json + import os + import struct + import sys + import threading + import time + + cookie = b"xdebugpy" + pid = os.getpid() + + def find_debug_offsets(): + with open(f"/proc/{pid}/maps") as f: + for line in f: + parts = line.split() + if len(parts) < 2 or "rw" not in parts[1]: + continue + start, end = (int(x, 16) for x in parts[0].split("-")) + if end - start > 10_000_000: + continue + try: + fd = os.open(f"/proc/{pid}/mem", os.O_RDONLY) + os.lseek(fd, start, 0) + data = os.read(fd, end - start) + os.close(fd) + except OSError: + continue + off = data.find(cookie) + if off == -1: + continue + version = struct.unpack_from("> 24) & 0xFF) != sys.version_info.major: + continue + return start + off + raise RuntimeError("debug offsets not found") + + addr = find_debug_offsets() + """ + ) + + textwrap.dedent(poison_code) + + "\n" + + textwrap.dedent( + """\ + print( + json.dumps({"pid": pid, "native_tid": threading.get_native_id()}), + flush=True, + ) + time.sleep(60) + """ + ) + ) + + proc = subprocess.Popen( + [sys.executable, "-c", script], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True, + ) + try: + line = proc.stdout.readline() + if not line: + stderr = proc.stderr.read() + self.fail( + "poisoned child failed to initialize: " + f"{stderr.strip() or 'no stderr output'}" + ) + yield proc, json.loads(line) + finally: + try: + proc.terminate() + proc.wait(timeout=SHORT_TIMEOUT) + except subprocess.TimeoutExpired: + proc.kill() + proc.wait(timeout=SHORT_TIMEOUT) + except OSError: + pass + if proc.stdout is not None: + proc.stdout.close() + if proc.stderr is not None: + proc.stderr.close() + def _extract_coroutine_stacks_lineno_only(self, stack_trace): """Extract coroutine stacks with line numbers only (no column offsets). From dac7c756758b0ca7c414bf4225fbbfac54e96004 Mon Sep 17 00:00:00 2001 From: maurycy <5383+maurycy@users.noreply.github.com> Date: Sun, 12 Apr 2026 21:06:16 +0200 Subject: [PATCH 4/8] demonstrate the bug? --- Lib/test/test_external_inspection.py | 31 ++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/Lib/test/test_external_inspection.py b/Lib/test/test_external_inspection.py index 3e07b5996521b7..2bfcf1202b99dc 100644 --- a/Lib/test/test_external_inspection.py +++ b/Lib/test/test_external_inspection.py @@ -3660,5 +3660,36 @@ def test_get_stats_disabled_raises(self): client_socket.sendall(b"done") +@requires_remote_subprocess_debugging() +@unittest.skipUnless(sys.platform == "linux", "requires /proc/self/mem") +@unittest.skipIf( + not PROCESS_VM_READV_SUPPORTED, + "Run on Linux with process_vm_readv support", +) +class TestInvalidDebugOffsets(RemoteInspectionTestBase): + @skip_if_not_supported + @unittest.skipUnless( + os.environ.get("PYTHON_REMOTE_DEBUG_UBSAN_PROBE") == "1", + "with UBSan probe", + ) + def test_ubsan_probe_misaligned_interpreter_state_id_offset(self): + poison_code = textwrap.dedent( + """\ + interpreter_state_offset = 8 + 8 + 8 + 3 * 8 + interpreter_id_offset = interpreter_state_offset + 8 + fd = os.open(f"/proc/{pid}/mem", os.O_WRONLY) + os.lseek(fd, addr + interpreter_id_offset, 0) + os.write(fd, struct.pack(" Date: Sun, 12 Apr 2026 21:13:54 +0200 Subject: [PATCH 5/8] now? --- Modules/_remote_debugging/_remote_debugging.h | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Modules/_remote_debugging/_remote_debugging.h b/Modules/_remote_debugging/_remote_debugging.h index aaa9d515a3b909..07738d45e42d24 100644 --- a/Modules/_remote_debugging/_remote_debugging.h +++ b/Modules/_remote_debugging/_remote_debugging.h @@ -120,9 +120,10 @@ typedef enum _WIN32_THREADSTATE { * MACROS AND CONSTANTS * ============================================================================ */ -#define GET_MEMBER(type, obj, offset) (*(type*)((char*)(obj) + (offset))) +#define GET_MEMBER(type, obj, offset) \ + (*(const type *)memcpy(&(type){0}, (const char *)(obj) + (offset), sizeof(type))) #define CLEAR_PTR_TAG(ptr) (((uintptr_t)(ptr) & ~Py_TAG_BITS)) -#define GET_MEMBER_NO_TAG(type, obj, offset) (type)(CLEAR_PTR_TAG(*(type*)((char*)(obj) + (offset)))) +#define GET_MEMBER_NO_TAG(type, obj, offset) (type)(CLEAR_PTR_TAG(GET_MEMBER(type, obj, offset))) /* Size macros for opaque buffers */ #define SIZEOF_BYTES_OBJ sizeof(PyBytesObject) From 0a372d2a5a842e3d20bbe55ae2f13afaaa664648 Mon Sep 17 00:00:00 2001 From: maurycy <5383+maurycy@users.noreply.github.com> Date: Sun, 12 Apr 2026 21:20:33 +0200 Subject: [PATCH 6/8] fail fast w/ RuntimeError --- .../debug_offsets_validation.h | 164 +++++++++++------- 1 file changed, 105 insertions(+), 59 deletions(-) diff --git a/Modules/_remote_debugging/debug_offsets_validation.h b/Modules/_remote_debugging/debug_offsets_validation.h index 806754df528fdb..7fee092f9279f5 100644 --- a/Modules/_remote_debugging/debug_offsets_validation.h +++ b/Modules/_remote_debugging/debug_offsets_validation.h @@ -107,14 +107,36 @@ validate_span( return 0; } +static inline int +validate_alignment( + const char *field_name, + uint64_t offset, + size_t alignment) +{ + if (alignment > 1 && offset % alignment != 0) { + PyErr_Format( + PyExc_RuntimeError, + "Invalid debug offsets: %s=%llu is not aligned to %zu bytes", + field_name, + (unsigned long long)offset, + alignment); + return -1; + } + return 0; +} + static inline int validate_field( const char *field_name, uint64_t reported_size, uint64_t offset, size_t width, + size_t alignment, size_t buffer_size) { + if (validate_alignment(field_name, offset, alignment) < 0) { + return -1; + } if (validate_span(field_name, offset, width, reported_size, "reported size") < 0) { return -1; } @@ -128,6 +150,7 @@ validate_nested_field( uint64_t base_offset, uint64_t nested_offset, size_t width, + size_t alignment, size_t buffer_size) { if (base_offset > UINT64_MAX - nested_offset) { @@ -142,6 +165,7 @@ validate_nested_field( reported_size, base_offset + nested_offset, width, + alignment, buffer_size); } @@ -150,8 +174,12 @@ validate_fixed_field( const char *field_name, uint64_t offset, size_t width, + size_t alignment, size_t buffer_size) { + if (validate_alignment(field_name, offset, alignment) < 0) { + return -1; + } return validate_span(field_name, offset, width, buffer_size, "local buffer size"); } @@ -169,19 +197,20 @@ validate_fixed_field( } \ } while (0) -#define PY_REMOTE_DEBUG_VALIDATE_FIELD(section, field, field_size, buffer_size) \ +#define PY_REMOTE_DEBUG_VALIDATE_FIELD(section, field, field_size, field_alignment, buffer_size) \ do { \ if (validate_field( \ #section "." #field, \ debug_offsets->section.size, \ debug_offsets->section.field, \ field_size, \ + field_alignment, \ buffer_size) < 0) { \ return -1; \ } \ } while (0) -#define PY_REMOTE_DEBUG_VALIDATE_NESTED_FIELD(section, base, nested_section, field, field_size, buffer_size) \ +#define PY_REMOTE_DEBUG_VALIDATE_NESTED_FIELD(section, base, nested_section, field, field_size, field_alignment, buffer_size) \ do { \ if (validate_nested_field( \ #section "." #base "." #field, \ @@ -189,17 +218,19 @@ validate_fixed_field( debug_offsets->section.base, \ debug_offsets->nested_section.field, \ field_size, \ + field_alignment, \ buffer_size) < 0) { \ return -1; \ } \ } while (0) -#define PY_REMOTE_DEBUG_VALIDATE_FIXED_FIELD(section, field, field_size, buffer_size) \ +#define PY_REMOTE_DEBUG_VALIDATE_FIXED_FIELD(section, field, field_size, field_alignment, buffer_size) \ do { \ if (validate_fixed_field( \ #section "." #field, \ debug_offsets->section.field, \ field_size, \ + field_alignment, \ buffer_size) < 0) { \ return -1; \ } \ @@ -216,84 +247,84 @@ 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), buffer_size) + APPLY(runtime_state, interpreters_head, sizeof(uintptr_t), _Alignof(uintptr_t), buffer_size) #define PY_REMOTE_DEBUG_THREAD_STATE_FIELDS(APPLY, buffer_size) \ - APPLY(thread_state, native_thread_id, sizeof(unsigned long), buffer_size); \ - APPLY(thread_state, interp, sizeof(uintptr_t), buffer_size); \ - APPLY(thread_state, datastack_chunk, sizeof(uintptr_t), buffer_size); \ - APPLY(thread_state, status, FIELD_SIZE(PyThreadState, _status), buffer_size); \ - APPLY(thread_state, holds_gil, sizeof(int), buffer_size); \ - APPLY(thread_state, gil_requested, sizeof(int), buffer_size); \ - APPLY(thread_state, current_exception, sizeof(uintptr_t), buffer_size); \ - APPLY(thread_state, thread_id, sizeof(unsigned long), buffer_size); \ - APPLY(thread_state, next, sizeof(uintptr_t), buffer_size); \ - APPLY(thread_state, current_frame, sizeof(uintptr_t), buffer_size); \ - APPLY(thread_state, base_frame, sizeof(uintptr_t), buffer_size); \ - APPLY(thread_state, last_profiled_frame, sizeof(uintptr_t), buffer_size) + APPLY(thread_state, native_thread_id, sizeof(unsigned long), _Alignof(long), buffer_size); \ + APPLY(thread_state, interp, sizeof(uintptr_t), _Alignof(uintptr_t), buffer_size); \ + APPLY(thread_state, datastack_chunk, sizeof(uintptr_t), _Alignof(uintptr_t), buffer_size); \ + APPLY(thread_state, status, FIELD_SIZE(PyThreadState, _status), _Alignof(unsigned int), buffer_size); \ + APPLY(thread_state, holds_gil, sizeof(int), _Alignof(int), buffer_size); \ + APPLY(thread_state, gil_requested, sizeof(int), _Alignof(int), buffer_size); \ + APPLY(thread_state, current_exception, sizeof(uintptr_t), _Alignof(uintptr_t), buffer_size); \ + APPLY(thread_state, thread_id, sizeof(unsigned long), _Alignof(long), buffer_size); \ + APPLY(thread_state, next, sizeof(uintptr_t), _Alignof(uintptr_t), buffer_size); \ + APPLY(thread_state, current_frame, sizeof(uintptr_t), _Alignof(uintptr_t), buffer_size); \ + APPLY(thread_state, base_frame, sizeof(uintptr_t), _Alignof(uintptr_t), buffer_size); \ + APPLY(thread_state, last_profiled_frame, sizeof(uintptr_t), _Alignof(uintptr_t), buffer_size) #define PY_REMOTE_DEBUG_INTERPRETER_STATE_FIELDS(APPLY, buffer_size) \ - APPLY(interpreter_state, id, sizeof(int64_t), buffer_size); \ - APPLY(interpreter_state, next, sizeof(uintptr_t), buffer_size); \ - APPLY(interpreter_state, threads_head, sizeof(uintptr_t), buffer_size); \ - APPLY(interpreter_state, threads_main, sizeof(uintptr_t), buffer_size); \ - APPLY(interpreter_state, gil_runtime_state_locked, sizeof(int), buffer_size); \ - APPLY(interpreter_state, gil_runtime_state_holder, sizeof(PyThreadState *), buffer_size); \ - APPLY(interpreter_state, code_object_generation, sizeof(uint64_t), buffer_size); \ - APPLY(interpreter_state, tlbc_generation, sizeof(uint32_t), buffer_size) + APPLY(interpreter_state, id, sizeof(int64_t), _Alignof(int64_t), buffer_size); \ + APPLY(interpreter_state, next, sizeof(uintptr_t), _Alignof(uintptr_t), buffer_size); \ + APPLY(interpreter_state, threads_head, sizeof(uintptr_t), _Alignof(uintptr_t), buffer_size); \ + APPLY(interpreter_state, threads_main, sizeof(uintptr_t), _Alignof(uintptr_t), buffer_size); \ + APPLY(interpreter_state, gil_runtime_state_locked, sizeof(int), _Alignof(int), buffer_size); \ + APPLY(interpreter_state, gil_runtime_state_holder, sizeof(PyThreadState *), _Alignof(PyThreadState *), buffer_size); \ + APPLY(interpreter_state, code_object_generation, sizeof(uint64_t), _Alignof(uint64_t), buffer_size); \ + APPLY(interpreter_state, tlbc_generation, sizeof(uint32_t), _Alignof(uint32_t), buffer_size) #define PY_REMOTE_DEBUG_INTERPRETER_FRAME_FIELDS(APPLY, buffer_size) \ - APPLY(interpreter_frame, previous, sizeof(uintptr_t), buffer_size); \ - APPLY(interpreter_frame, executable, sizeof(uintptr_t), buffer_size); \ - APPLY(interpreter_frame, instr_ptr, sizeof(uintptr_t), buffer_size); \ - APPLY(interpreter_frame, owner, sizeof(char), buffer_size); \ - APPLY(interpreter_frame, stackpointer, sizeof(uintptr_t), buffer_size); \ - APPLY(interpreter_frame, tlbc_index, sizeof(int32_t), buffer_size) + APPLY(interpreter_frame, previous, sizeof(uintptr_t), _Alignof(uintptr_t), buffer_size); \ + APPLY(interpreter_frame, executable, sizeof(uintptr_t), _Alignof(uintptr_t), buffer_size); \ + APPLY(interpreter_frame, instr_ptr, sizeof(uintptr_t), _Alignof(uintptr_t), buffer_size); \ + APPLY(interpreter_frame, owner, sizeof(char), _Alignof(char), buffer_size); \ + APPLY(interpreter_frame, stackpointer, sizeof(uintptr_t), _Alignof(uintptr_t), buffer_size); \ + APPLY(interpreter_frame, tlbc_index, sizeof(int32_t), _Alignof(int32_t), buffer_size) #define PY_REMOTE_DEBUG_CODE_OBJECT_FIELDS(APPLY, buffer_size) \ - APPLY(code_object, qualname, sizeof(uintptr_t), buffer_size); \ - APPLY(code_object, filename, sizeof(uintptr_t), buffer_size); \ - APPLY(code_object, linetable, sizeof(uintptr_t), buffer_size); \ - APPLY(code_object, firstlineno, sizeof(int), buffer_size); \ - APPLY(code_object, co_code_adaptive, sizeof(char), buffer_size); \ - APPLY(code_object, co_tlbc, sizeof(uintptr_t), buffer_size) + APPLY(code_object, qualname, sizeof(uintptr_t), _Alignof(uintptr_t), buffer_size); \ + APPLY(code_object, filename, sizeof(uintptr_t), _Alignof(uintptr_t), buffer_size); \ + APPLY(code_object, linetable, sizeof(uintptr_t), _Alignof(uintptr_t), buffer_size); \ + APPLY(code_object, firstlineno, sizeof(int), _Alignof(int), buffer_size); \ + APPLY(code_object, co_code_adaptive, sizeof(char), _Alignof(char), buffer_size); \ + APPLY(code_object, co_tlbc, sizeof(uintptr_t), _Alignof(uintptr_t), buffer_size) #define PY_REMOTE_DEBUG_SET_OBJECT_FIELDS(APPLY, buffer_size) \ - APPLY(set_object, used, sizeof(Py_ssize_t), buffer_size); \ - APPLY(set_object, mask, sizeof(Py_ssize_t), buffer_size); \ - APPLY(set_object, table, sizeof(uintptr_t), buffer_size) + APPLY(set_object, used, sizeof(Py_ssize_t), _Alignof(Py_ssize_t), buffer_size); \ + APPLY(set_object, mask, sizeof(Py_ssize_t), _Alignof(Py_ssize_t), buffer_size); \ + APPLY(set_object, table, sizeof(uintptr_t), _Alignof(uintptr_t), buffer_size) #define PY_REMOTE_DEBUG_LONG_OBJECT_FIELDS(APPLY, buffer_size) \ - APPLY(long_object, lv_tag, sizeof(uintptr_t), buffer_size); \ - APPLY(long_object, ob_digit, sizeof(digit), buffer_size) + APPLY(long_object, lv_tag, sizeof(uintptr_t), _Alignof(uintptr_t), buffer_size); \ + APPLY(long_object, ob_digit, sizeof(digit), _Alignof(digit), buffer_size) #define PY_REMOTE_DEBUG_BYTES_OBJECT_FIELDS(APPLY, buffer_size) \ - APPLY(bytes_object, ob_size, sizeof(Py_ssize_t), buffer_size); \ - APPLY(bytes_object, ob_sval, sizeof(char), buffer_size) + APPLY(bytes_object, ob_size, sizeof(Py_ssize_t), _Alignof(Py_ssize_t), buffer_size); \ + APPLY(bytes_object, ob_sval, sizeof(char), _Alignof(char), buffer_size) #define PY_REMOTE_DEBUG_UNICODE_OBJECT_FIELDS(APPLY, buffer_size) \ - APPLY(unicode_object, length, sizeof(Py_ssize_t), buffer_size); \ - APPLY(unicode_object, asciiobject_size, sizeof(char), buffer_size) + APPLY(unicode_object, length, sizeof(Py_ssize_t), _Alignof(Py_ssize_t), buffer_size); \ + APPLY(unicode_object, asciiobject_size, sizeof(char), _Alignof(char), buffer_size) #define PY_REMOTE_DEBUG_GEN_OBJECT_FIELDS(APPLY, buffer_size) \ - APPLY(gen_object, gi_frame_state, sizeof(int8_t), buffer_size); \ - APPLY(gen_object, gi_iframe, FIELD_SIZE(PyGenObject, gi_iframe), buffer_size) + APPLY(gen_object, gi_frame_state, sizeof(int8_t), _Alignof(int8_t), buffer_size); \ + APPLY(gen_object, gi_iframe, FIELD_SIZE(PyGenObject, gi_iframe), _Alignof(_PyInterpreterFrame), buffer_size) #define PY_REMOTE_DEBUG_TASK_OBJECT_FIELDS(APPLY, buffer_size) \ - APPLY(asyncio_task_object, task_name, sizeof(uintptr_t), buffer_size); \ - APPLY(asyncio_task_object, task_awaited_by, sizeof(uintptr_t), buffer_size); \ - APPLY(asyncio_task_object, task_is_task, sizeof(char), buffer_size); \ - APPLY(asyncio_task_object, task_awaited_by_is_set, sizeof(char), buffer_size); \ - APPLY(asyncio_task_object, task_coro, sizeof(uintptr_t), buffer_size); \ - APPLY(asyncio_task_object, task_node, SIZEOF_LLIST_NODE, buffer_size) + APPLY(asyncio_task_object, task_name, sizeof(uintptr_t), _Alignof(uintptr_t), buffer_size); \ + APPLY(asyncio_task_object, task_awaited_by, sizeof(uintptr_t), _Alignof(uintptr_t), buffer_size); \ + APPLY(asyncio_task_object, task_is_task, sizeof(char), _Alignof(char), buffer_size); \ + APPLY(asyncio_task_object, task_awaited_by_is_set, sizeof(char), _Alignof(char), buffer_size); \ + APPLY(asyncio_task_object, task_coro, sizeof(uintptr_t), _Alignof(uintptr_t), buffer_size); \ + APPLY(asyncio_task_object, task_node, SIZEOF_LLIST_NODE, _Alignof(struct llist_node), buffer_size) #define PY_REMOTE_DEBUG_ASYNC_INTERPRETER_STATE_FIELDS(APPLY, buffer_size) \ - APPLY(asyncio_interpreter_state, asyncio_tasks_head, SIZEOF_LLIST_NODE, buffer_size) + APPLY(asyncio_interpreter_state, asyncio_tasks_head, SIZEOF_LLIST_NODE, _Alignof(struct llist_node), buffer_size) #define PY_REMOTE_DEBUG_ASYNC_THREAD_STATE_FIELDS(APPLY, buffer_size) \ - APPLY(asyncio_thread_state, asyncio_running_loop, sizeof(uintptr_t), buffer_size); \ - APPLY(asyncio_thread_state, asyncio_running_task, sizeof(uintptr_t), buffer_size); \ - APPLY(asyncio_thread_state, asyncio_tasks_head, SIZEOF_LLIST_NODE, buffer_size) + APPLY(asyncio_thread_state, asyncio_running_loop, sizeof(uintptr_t), _Alignof(uintptr_t), buffer_size); \ + APPLY(asyncio_thread_state, asyncio_running_task, sizeof(uintptr_t), _Alignof(uintptr_t), buffer_size); \ + APPLY(asyncio_thread_state, asyncio_tasks_head, SIZEOF_LLIST_NODE, _Alignof(struct llist_node), buffer_size) /* Called once after reading _Py_DebugOffsets, before any hot-path use. */ static inline int @@ -321,6 +352,7 @@ _PyRemoteDebug_ValidateDebugOffsetsLayout(struct _Py_DebugOffsets *debug_offsets err_stackitem, exc_value, sizeof(uintptr_t), + _Alignof(uintptr_t), sizeof(_PyErr_StackItem)); PY_REMOTE_DEBUG_VALIDATE_NESTED_FIELD( thread_state, @@ -328,16 +360,23 @@ _PyRemoteDebug_ValidateDebugOffsetsLayout(struct _Py_DebugOffsets *debug_offsets err_stackitem, exc_value, sizeof(uintptr_t), + _Alignof(uintptr_t), SIZEOF_THREAD_STATE); PY_REMOTE_DEBUG_VALIDATE_READ_SECTION(gc, SIZEOF_GC_RUNTIME_STATE); - PY_REMOTE_DEBUG_VALIDATE_FIELD(gc, frame, sizeof(uintptr_t), SIZEOF_GC_RUNTIME_STATE); + PY_REMOTE_DEBUG_VALIDATE_FIELD( + gc, + frame, + sizeof(uintptr_t), + _Alignof(uintptr_t), + SIZEOF_GC_RUNTIME_STATE); PY_REMOTE_DEBUG_VALIDATE_NESTED_FIELD( interpreter_state, gc, gc, frame, sizeof(uintptr_t), + _Alignof(uintptr_t), INTERP_STATE_BUFFER_SIZE); PY_REMOTE_DEBUG_VALIDATE_SECTION(interpreter_frame); @@ -351,13 +390,19 @@ _PyRemoteDebug_ValidateDebugOffsetsLayout(struct _Py_DebugOffsets *debug_offsets SIZEOF_CODE_OBJ); PY_REMOTE_DEBUG_VALIDATE_SECTION(pyobject); - PY_REMOTE_DEBUG_VALIDATE_FIELD(pyobject, ob_type, sizeof(uintptr_t), SIZEOF_PYOBJECT); + PY_REMOTE_DEBUG_VALIDATE_FIELD( + pyobject, + ob_type, + sizeof(uintptr_t), + _Alignof(uintptr_t), + SIZEOF_PYOBJECT); PY_REMOTE_DEBUG_VALIDATE_SECTION(type_object); PY_REMOTE_DEBUG_VALIDATE_FIELD( type_object, tp_flags, sizeof(unsigned long), + _Alignof(unsigned long), SIZEOF_TYPE_OBJ); PY_REMOTE_DEBUG_VALIDATE_SECTION(set_object); @@ -389,6 +434,7 @@ _PyRemoteDebug_ValidateDebugOffsetsLayout(struct _Py_DebugOffsets *debug_offsets llist_node, next, sizeof(uintptr_t), + _Alignof(uintptr_t), SIZEOF_LLIST_NODE); return 0; From bff8f8a01211edc1c30eb5a9bd049c024206c306 Mon Sep 17 00:00:00 2001 From: maurycy <5383+maurycy@users.noreply.github.com> Date: Sun, 12 Apr 2026 21:23:41 +0200 Subject: [PATCH 7/8] Revert "bring back https://github.com/python/cpython/compare/f12f6c4f0467837e25a445339f2e5a24388a317d..b8dcd8fa2b3be846b1dad6d2afb1dccd968789b3" This reverts commit d3b0fb0abd799f061eccad063f99c78d8d731392. --- Lib/test/test_external_inspection.py | 86 ---------------------------- 1 file changed, 86 deletions(-) diff --git a/Lib/test/test_external_inspection.py b/Lib/test/test_external_inspection.py index 2bfcf1202b99dc..4924e8be71082d 100644 --- a/Lib/test/test_external_inspection.py +++ b/Lib/test/test_external_inspection.py @@ -1,6 +1,5 @@ import unittest import os -import json import textwrap import contextlib import importlib @@ -419,91 +418,6 @@ def _frame_to_lineno_tuple(frame): filename, location, funcname, opcode = frame return (filename, location.lineno, funcname, opcode) - @contextmanager - def _poisoned_debug_offsets_process(self, poison_code): - script = ( - textwrap.dedent( - """\ - import json - import os - import struct - import sys - import threading - import time - - cookie = b"xdebugpy" - pid = os.getpid() - - def find_debug_offsets(): - with open(f"/proc/{pid}/maps") as f: - for line in f: - parts = line.split() - if len(parts) < 2 or "rw" not in parts[1]: - continue - start, end = (int(x, 16) for x in parts[0].split("-")) - if end - start > 10_000_000: - continue - try: - fd = os.open(f"/proc/{pid}/mem", os.O_RDONLY) - os.lseek(fd, start, 0) - data = os.read(fd, end - start) - os.close(fd) - except OSError: - continue - off = data.find(cookie) - if off == -1: - continue - version = struct.unpack_from("> 24) & 0xFF) != sys.version_info.major: - continue - return start + off - raise RuntimeError("debug offsets not found") - - addr = find_debug_offsets() - """ - ) - + textwrap.dedent(poison_code) - + "\n" - + textwrap.dedent( - """\ - print( - json.dumps({"pid": pid, "native_tid": threading.get_native_id()}), - flush=True, - ) - time.sleep(60) - """ - ) - ) - - proc = subprocess.Popen( - [sys.executable, "-c", script], - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - text=True, - ) - try: - line = proc.stdout.readline() - if not line: - stderr = proc.stderr.read() - self.fail( - "poisoned child failed to initialize: " - f"{stderr.strip() or 'no stderr output'}" - ) - yield proc, json.loads(line) - finally: - try: - proc.terminate() - proc.wait(timeout=SHORT_TIMEOUT) - except subprocess.TimeoutExpired: - proc.kill() - proc.wait(timeout=SHORT_TIMEOUT) - except OSError: - pass - if proc.stdout is not None: - proc.stdout.close() - if proc.stderr is not None: - proc.stderr.close() - def _extract_coroutine_stacks_lineno_only(self, stack_trace): """Extract coroutine stacks with line numbers only (no column offsets). From 174853d2b8e469b4902fd61d1eb13c4c55696a70 Mon Sep 17 00:00:00 2001 From: maurycy <5383+maurycy@users.noreply.github.com> Date: Sun, 12 Apr 2026 21:27:33 +0200 Subject: [PATCH 8/8] Revert "demonstrate the bug?" This reverts commit dac7c756758b0ca7c414bf4225fbbfac54e96004. --- Lib/test/test_external_inspection.py | 31 ---------------------------- 1 file changed, 31 deletions(-) diff --git a/Lib/test/test_external_inspection.py b/Lib/test/test_external_inspection.py index 4924e8be71082d..ec7192b1b89184 100644 --- a/Lib/test/test_external_inspection.py +++ b/Lib/test/test_external_inspection.py @@ -3574,36 +3574,5 @@ def test_get_stats_disabled_raises(self): client_socket.sendall(b"done") -@requires_remote_subprocess_debugging() -@unittest.skipUnless(sys.platform == "linux", "requires /proc/self/mem") -@unittest.skipIf( - not PROCESS_VM_READV_SUPPORTED, - "Run on Linux with process_vm_readv support", -) -class TestInvalidDebugOffsets(RemoteInspectionTestBase): - @skip_if_not_supported - @unittest.skipUnless( - os.environ.get("PYTHON_REMOTE_DEBUG_UBSAN_PROBE") == "1", - "with UBSan probe", - ) - def test_ubsan_probe_misaligned_interpreter_state_id_offset(self): - poison_code = textwrap.dedent( - """\ - interpreter_state_offset = 8 + 8 + 8 + 3 * 8 - interpreter_id_offset = interpreter_state_offset + 8 - fd = os.open(f"/proc/{pid}/mem", os.O_WRONLY) - os.lseek(fd, addr + interpreter_id_offset, 0) - os.write(fd, struct.pack("