From 3d0999cf71fcb0398920d2dcd9f136749eb13982 Mon Sep 17 00:00:00 2001 From: Seth Larson Date: Fri, 10 Apr 2026 10:21:42 -0500 Subject: [PATCH 1/4] 00479: CVE-2026-1502 Reject CR/LF in HTTP tunnel request headers Co-authored-by: Illia Volochii --- Lib/http/client.py | 11 ++++- Lib/test/test_httplib.py | 45 +++++++++++++++++++ ...-03-20-09-29-42.gh-issue-146211.PQVbs7.rst | 2 + 3 files changed, 57 insertions(+), 1 deletion(-) create mode 100644 Misc/NEWS.d/next/Security/2026-03-20-09-29-42.gh-issue-146211.PQVbs7.rst diff --git a/Lib/http/client.py b/Lib/http/client.py index 77f8d26291dfc2..6fb7d254ea9c27 100644 --- a/Lib/http/client.py +++ b/Lib/http/client.py @@ -972,13 +972,22 @@ def _wrap_ipv6(self, ip): return ip def _tunnel(self): + if _contains_disallowed_url_pchar_re.search(self._tunnel_host): + raise ValueError('Tunnel host can\'t contain control characters %r' + % (self._tunnel_host,)) connect = b"CONNECT %s:%d %s\r\n" % ( self._wrap_ipv6(self._tunnel_host.encode("idna")), self._tunnel_port, self._http_vsn_str.encode("ascii")) headers = [connect] for header, value in self._tunnel_headers.items(): - headers.append(f"{header}: {value}\r\n".encode("latin-1")) + header_bytes = header.encode("latin-1") + value_bytes = value.encode("latin-1") + if not _is_legal_header_name(header_bytes): + raise ValueError('Invalid header name %r' % (header_bytes,)) + if _is_illegal_header_value(value_bytes): + raise ValueError('Invalid header value %r' % (value_bytes,)) + headers.append(b"%s: %s\r\n" % (header_bytes, value_bytes)) headers.append(b"\r\n") # Making a single send() call instead of one per line encourages # the host OS to use a more optimal packet size instead of diff --git a/Lib/test/test_httplib.py b/Lib/test/test_httplib.py index bcb828edec7c39..6f3eac6b98a4de 100644 --- a/Lib/test/test_httplib.py +++ b/Lib/test/test_httplib.py @@ -369,6 +369,51 @@ def test_invalid_headers(self): with self.assertRaisesRegex(ValueError, 'Invalid header'): conn.putheader(name, value) + def test_invalid_tunnel_headers(self): + cases = ( + ('Invalid\r\nName', 'ValidValue'), + ('Invalid\rName', 'ValidValue'), + ('Invalid\nName', 'ValidValue'), + ('\r\nInvalidName', 'ValidValue'), + ('\rInvalidName', 'ValidValue'), + ('\nInvalidName', 'ValidValue'), + (' InvalidName', 'ValidValue'), + ('\tInvalidName', 'ValidValue'), + ('Invalid:Name', 'ValidValue'), + (':InvalidName', 'ValidValue'), + ('ValidName', 'Invalid\r\nValue'), + ('ValidName', 'Invalid\rValue'), + ('ValidName', 'Invalid\nValue'), + ('ValidName', 'InvalidValue\r\n'), + ('ValidName', 'InvalidValue\r'), + ('ValidName', 'InvalidValue\n'), + ) + for name, value in cases: + with self.subTest((name, value)): + conn = client.HTTPConnection('example.com') + conn.set_tunnel('tunnel', headers={ + name: value + }) + conn.sock = FakeSocket('') + with self.assertRaisesRegex(ValueError, 'Invalid header'): + conn._tunnel() # Called in .connect() + + def test_invalid_tunnel_host(self): + cases = ( + 'invalid\r.host', + '\ninvalid.host', + 'invalid.host\r\n', + 'invalid.host\x00', + 'invalid host', + ) + for tunnel_host in cases: + with self.subTest(tunnel_host): + conn = client.HTTPConnection('example.com') + conn.set_tunnel(tunnel_host) + conn.sock = FakeSocket('') + with self.assertRaisesRegex(ValueError, 'Tunnel host can\'t contain control characters'): + conn._tunnel() # Called in .connect() + def test_headers_debuglevel(self): body = ( b'HTTP/1.1 200 OK\r\n' diff --git a/Misc/NEWS.d/next/Security/2026-03-20-09-29-42.gh-issue-146211.PQVbs7.rst b/Misc/NEWS.d/next/Security/2026-03-20-09-29-42.gh-issue-146211.PQVbs7.rst new file mode 100644 index 00000000000000..4993633b8ebebb --- /dev/null +++ b/Misc/NEWS.d/next/Security/2026-03-20-09-29-42.gh-issue-146211.PQVbs7.rst @@ -0,0 +1,2 @@ +Reject CR/LF characters in tunnel request headers for the +HTTPConnection.set_tunnel() method. From 63e2444fbf941669a088dba2bc9a17cc787d308c Mon Sep 17 00:00:00 2001 From: Stan Ulbrych Date: Mon, 13 Apr 2026 20:02:52 +0100 Subject: [PATCH 2/4] 00480: CVE-2026-4786 Fix webbrowser `%action` substitution bypass of dash-prefix check --- Lib/test/test_webbrowser.py | 9 +++++++++ Lib/webbrowser.py | 5 +++-- .../2026-03-31-09-15-51.gh-issue-148169.EZJzz2.rst | 2 ++ 3 files changed, 14 insertions(+), 2 deletions(-) create mode 100644 Misc/NEWS.d/next/Security/2026-03-31-09-15-51.gh-issue-148169.EZJzz2.rst diff --git a/Lib/test/test_webbrowser.py b/Lib/test/test_webbrowser.py index 404b3a31a5d2c9..bfbcf112b0b085 100644 --- a/Lib/test/test_webbrowser.py +++ b/Lib/test/test_webbrowser.py @@ -119,6 +119,15 @@ def test_open_bad_new_parameter(self): arguments=[URL], kw=dict(new=999)) + def test_reject_action_dash_prefixes(self): + browser = self.browser_class(name=CMD_NAME) + with self.assertRaises(ValueError): + browser.open('%action--incognito') + # new=1: action is "--new-window", so "%action" itself expands to + # a dash-prefixed flag even with no dash in the original URL. + with self.assertRaises(ValueError): + browser.open('%action', new=1) + class EdgeCommandTest(CommandTestMixin, unittest.TestCase): diff --git a/Lib/webbrowser.py b/Lib/webbrowser.py index 0e0b5034e5f53d..97aad6eea509eb 100644 --- a/Lib/webbrowser.py +++ b/Lib/webbrowser.py @@ -274,7 +274,6 @@ def _invoke(self, args, remote, autoraise, url=None): def open(self, url, new=0, autoraise=True): sys.audit("webbrowser.open", url) - self._check_url(url) if new == 0: action = self.remote_action elif new == 1: @@ -288,7 +287,9 @@ def open(self, url, new=0, autoraise=True): raise Error("Bad 'new' parameter to open(); " f"expected 0, 1, or 2, got {new}") - args = [arg.replace("%s", url).replace("%action", action) + self._check_url(url.replace("%action", action)) + + args = [arg.replace("%action", action).replace("%s", url) for arg in self.remote_args] args = [arg for arg in args if arg] success = self._invoke(args, True, autoraise, url) diff --git a/Misc/NEWS.d/next/Security/2026-03-31-09-15-51.gh-issue-148169.EZJzz2.rst b/Misc/NEWS.d/next/Security/2026-03-31-09-15-51.gh-issue-148169.EZJzz2.rst new file mode 100644 index 00000000000000..45cdeebe1b6d64 --- /dev/null +++ b/Misc/NEWS.d/next/Security/2026-03-31-09-15-51.gh-issue-148169.EZJzz2.rst @@ -0,0 +1,2 @@ +A bypass in :mod:`webbrowser` allowed URLs prefixed with ``%action`` to pass +the dash-prefix safety check. From 6673d6acb50f4ab7ee6454fef4d7988808037dd5 Mon Sep 17 00:00:00 2001 From: Pablo Galindo Salgado Date: Mon, 13 Apr 2026 23:22:23 +0100 Subject: [PATCH 3/4] 00481: CVE-2026-5713 Validate remote debug offset tables on load --- ...-04-06-13-55-00.gh-issue-148178.Rs7kLm.rst | 2 + Modules/_remote_debugging_module.c | 509 +++++++++++++++++- 2 files changed, 505 insertions(+), 6 deletions(-) create mode 100644 Misc/NEWS.d/next/Security/2026-04-06-13-55-00.gh-issue-148178.Rs7kLm.rst 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_module.c b/Modules/_remote_debugging_module.c index a32777225817ea..d756ac326ff173 100644 --- a/Modules/_remote_debugging_module.c +++ b/Modules/_remote_debugging_module.c @@ -20,6 +20,7 @@ #include // FRAME_OWNED_BY_CSTACK #include // struct llist_node #include // Py_TAG_BITS +#include // _PyThreadStateImpl #include "../Python/remote_debug.h" // gh-141784: Python.h header must be included first, before system headers. @@ -41,9 +42,11 @@ * TYPE DEFINITIONS AND STRUCTURES * ============================================================================ */ -#define GET_MEMBER(type, obj, offset) (*(type*)((char*)(obj) + (offset))) +#define GET_MEMBER(type, obj, offset) \ + (*(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) @@ -107,6 +110,486 @@ struct _Py_AsyncioModuleDebugOffsets { } asyncio_thread_state; }; +/* Treat the remote debug tables as untrusted input and validate every + * size/offset we later dereference against a fixed local buffer or object + * layout before the unwinder starts using them. */ +#define FIELD_SIZE(type, member) sizeof(((type *)0)->member) +#define PY_REMOTE_DEBUG_INVALID_ASYNC_DEBUG_OFFSETS (-2) + +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_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 offset, + uint64_t reported_size, + 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; + } + return validate_span(field_name, offset, width, buffer_size, "local buffer size"); +} + +static inline int +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"); +} + +#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, field_alignment, buffer_size) \ + do { \ + if (validate_field( \ + #section "." #field, \ + debug_offsets->section.field, \ + debug_offsets->section.size, \ + field_size, \ + field_alignment, \ + buffer_size) < 0) { \ + return -1; \ + } \ + } while (0) + +#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; \ + } \ + } while (0) + +static inline int +validate_debug_offsets_layout(struct _Py_DebugOffsets *debug_offsets) +{ + PY_REMOTE_DEBUG_VALIDATE_SECTION(runtime_state); + PY_REMOTE_DEBUG_VALIDATE_FIELD( + runtime_state, + interpreters_head, + sizeof(uintptr_t), + _Alignof(uintptr_t), + sizeof(_PyRuntimeState)); + + PY_REMOTE_DEBUG_VALIDATE_SECTION(interpreter_state); + PY_REMOTE_DEBUG_VALIDATE_FIELD( + interpreter_state, + threads_head, + sizeof(uintptr_t), + _Alignof(uintptr_t), + INTERP_STATE_BUFFER_SIZE); + PY_REMOTE_DEBUG_VALIDATE_FIELD( + interpreter_state, + threads_main, + sizeof(uintptr_t), + _Alignof(uintptr_t), + INTERP_STATE_BUFFER_SIZE); + PY_REMOTE_DEBUG_VALIDATE_FIELD( + interpreter_state, + gil_runtime_state_locked, + sizeof(int), + _Alignof(int), + INTERP_STATE_BUFFER_SIZE); + PY_REMOTE_DEBUG_VALIDATE_FIELD( + interpreter_state, + gil_runtime_state_holder, + sizeof(PyThreadState *), + _Alignof(PyThreadState *), + INTERP_STATE_BUFFER_SIZE); + PY_REMOTE_DEBUG_VALIDATE_FIELD( + interpreter_state, + code_object_generation, + sizeof(uint64_t), + _Alignof(uint64_t), + INTERP_STATE_BUFFER_SIZE); + PY_REMOTE_DEBUG_VALIDATE_FIELD( + interpreter_state, + tlbc_generation, + sizeof(uint32_t), + _Alignof(uint32_t), + INTERP_STATE_BUFFER_SIZE); + + PY_REMOTE_DEBUG_VALIDATE_READ_SECTION(thread_state, SIZEOF_THREAD_STATE); + PY_REMOTE_DEBUG_VALIDATE_FIELD( + thread_state, + next, + sizeof(uintptr_t), + _Alignof(uintptr_t), + SIZEOF_THREAD_STATE); + PY_REMOTE_DEBUG_VALIDATE_FIELD( + thread_state, + current_frame, + sizeof(uintptr_t), + _Alignof(uintptr_t), + SIZEOF_THREAD_STATE); + PY_REMOTE_DEBUG_VALIDATE_FIELD( + thread_state, + native_thread_id, + sizeof(unsigned long), + _Alignof(unsigned long), + SIZEOF_THREAD_STATE); + PY_REMOTE_DEBUG_VALIDATE_FIELD( + thread_state, + datastack_chunk, + sizeof(uintptr_t), + _Alignof(uintptr_t), + SIZEOF_THREAD_STATE); + + PY_REMOTE_DEBUG_VALIDATE_SECTION(interpreter_frame); + PY_REMOTE_DEBUG_VALIDATE_FIELD( + interpreter_frame, + previous, + sizeof(uintptr_t), + _Alignof(uintptr_t), + SIZEOF_INTERP_FRAME); + PY_REMOTE_DEBUG_VALIDATE_FIELD( + interpreter_frame, + executable, + sizeof(uintptr_t), + _Alignof(uintptr_t), + SIZEOF_INTERP_FRAME); + PY_REMOTE_DEBUG_VALIDATE_FIELD( + interpreter_frame, + instr_ptr, + sizeof(uintptr_t), + _Alignof(uintptr_t), + SIZEOF_INTERP_FRAME); + PY_REMOTE_DEBUG_VALIDATE_FIELD( + interpreter_frame, + owner, + sizeof(char), + _Alignof(char), + SIZEOF_INTERP_FRAME); + PY_REMOTE_DEBUG_VALIDATE_FIELD( + interpreter_frame, + stackpointer, + sizeof(uintptr_t), + _Alignof(uintptr_t), + SIZEOF_INTERP_FRAME); + PY_REMOTE_DEBUG_VALIDATE_FIELD( + interpreter_frame, + tlbc_index, + sizeof(int32_t), + _Alignof(int32_t), + SIZEOF_INTERP_FRAME); + + PY_REMOTE_DEBUG_VALIDATE_SECTION(code_object); + PY_REMOTE_DEBUG_VALIDATE_FIELD( + code_object, + qualname, + sizeof(uintptr_t), + _Alignof(uintptr_t), + SIZEOF_CODE_OBJ); + PY_REMOTE_DEBUG_VALIDATE_FIELD( + code_object, + filename, + sizeof(uintptr_t), + _Alignof(uintptr_t), + SIZEOF_CODE_OBJ); + PY_REMOTE_DEBUG_VALIDATE_FIELD( + code_object, + linetable, + sizeof(uintptr_t), + _Alignof(uintptr_t), + SIZEOF_CODE_OBJ); + PY_REMOTE_DEBUG_VALIDATE_FIELD( + code_object, + firstlineno, + sizeof(int), + _Alignof(int), + SIZEOF_CODE_OBJ); + PY_REMOTE_DEBUG_VALIDATE_FIELD( + code_object, + co_code_adaptive, + sizeof(char), + _Alignof(char), + SIZEOF_CODE_OBJ); + PY_REMOTE_DEBUG_VALIDATE_FIELD( + code_object, + co_tlbc, + sizeof(uintptr_t), + _Alignof(uintptr_t), + SIZEOF_CODE_OBJ); + + PY_REMOTE_DEBUG_VALIDATE_SECTION(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); + PY_REMOTE_DEBUG_VALIDATE_FIELD( + set_object, + used, + sizeof(Py_ssize_t), + _Alignof(Py_ssize_t), + SIZEOF_SET_OBJ); + PY_REMOTE_DEBUG_VALIDATE_FIELD( + set_object, + mask, + sizeof(Py_ssize_t), + _Alignof(Py_ssize_t), + SIZEOF_SET_OBJ); + PY_REMOTE_DEBUG_VALIDATE_FIELD( + set_object, + table, + sizeof(uintptr_t), + _Alignof(uintptr_t), + SIZEOF_SET_OBJ); + + PY_REMOTE_DEBUG_VALIDATE_READ_SECTION(long_object, SIZEOF_LONG_OBJ); + PY_REMOTE_DEBUG_VALIDATE_FIELD( + long_object, + lv_tag, + sizeof(uintptr_t), + _Alignof(uintptr_t), + SIZEOF_LONG_OBJ); + PY_REMOTE_DEBUG_VALIDATE_FIELD( + long_object, + ob_digit, + sizeof(digit), + _Alignof(digit), + SIZEOF_LONG_OBJ); + + PY_REMOTE_DEBUG_VALIDATE_SECTION(bytes_object); + PY_REMOTE_DEBUG_VALIDATE_FIELD( + bytes_object, + ob_size, + sizeof(Py_ssize_t), + _Alignof(Py_ssize_t), + SIZEOF_BYTES_OBJ); + PY_REMOTE_DEBUG_VALIDATE_FIELD( + bytes_object, + ob_sval, + sizeof(char), + _Alignof(char), + SIZEOF_BYTES_OBJ); + + PY_REMOTE_DEBUG_VALIDATE_SECTION(unicode_object); + PY_REMOTE_DEBUG_VALIDATE_FIELD( + unicode_object, + length, + sizeof(Py_ssize_t), + _Alignof(Py_ssize_t), + SIZEOF_UNICODE_OBJ); + PY_REMOTE_DEBUG_VALIDATE_FIELD( + unicode_object, + asciiobject_size, + sizeof(char), + _Alignof(char), + SIZEOF_UNICODE_OBJ); + + PY_REMOTE_DEBUG_VALIDATE_SECTION(gen_object); + PY_REMOTE_DEBUG_VALIDATE_FIELD( + gen_object, + gi_frame_state, + sizeof(int8_t), + _Alignof(int8_t), + SIZEOF_GEN_OBJ); + PY_REMOTE_DEBUG_VALIDATE_FIELD( + gen_object, + gi_iframe, + FIELD_SIZE(PyGenObject, gi_iframe), + _Alignof(_PyInterpreterFrame), + SIZEOF_GEN_OBJ); + + PY_REMOTE_DEBUG_VALIDATE_FIXED_FIELD( + llist_node, + next, + sizeof(uintptr_t), + _Alignof(uintptr_t), + SIZEOF_LLIST_NODE); + + return 0; +} + +static inline int +validate_async_debug_offsets_layout(struct _Py_AsyncioModuleDebugOffsets *debug_offsets) +{ + PY_REMOTE_DEBUG_VALIDATE_READ_SECTION(asyncio_task_object, SIZEOF_TASK_OBJ); + PY_REMOTE_DEBUG_VALIDATE_FIELD( + asyncio_task_object, + task_name, + sizeof(uintptr_t), + _Alignof(uintptr_t), + SIZEOF_TASK_OBJ); + PY_REMOTE_DEBUG_VALIDATE_FIELD( + asyncio_task_object, + task_awaited_by, + sizeof(uintptr_t), + _Alignof(uintptr_t), + SIZEOF_TASK_OBJ); + PY_REMOTE_DEBUG_VALIDATE_FIELD( + asyncio_task_object, + task_is_task, + sizeof(char), + _Alignof(char), + SIZEOF_TASK_OBJ); + PY_REMOTE_DEBUG_VALIDATE_FIELD( + asyncio_task_object, + task_awaited_by_is_set, + sizeof(char), + _Alignof(char), + SIZEOF_TASK_OBJ); + PY_REMOTE_DEBUG_VALIDATE_FIELD( + asyncio_task_object, + task_coro, + sizeof(uintptr_t), + _Alignof(uintptr_t), + SIZEOF_TASK_OBJ); + PY_REMOTE_DEBUG_VALIDATE_FIELD( + asyncio_task_object, + task_node, + SIZEOF_LLIST_NODE, + _Alignof(struct llist_node), + SIZEOF_TASK_OBJ); + + PY_REMOTE_DEBUG_VALIDATE_SECTION(asyncio_interpreter_state); + PY_REMOTE_DEBUG_VALIDATE_FIELD( + asyncio_interpreter_state, + asyncio_tasks_head, + SIZEOF_LLIST_NODE, + _Alignof(struct llist_node), + sizeof(PyInterpreterState)); + + PY_REMOTE_DEBUG_VALIDATE_SECTION(asyncio_thread_state); + PY_REMOTE_DEBUG_VALIDATE_FIELD( + asyncio_thread_state, + asyncio_running_loop, + sizeof(uintptr_t), + _Alignof(uintptr_t), + sizeof(_PyThreadStateImpl)); + PY_REMOTE_DEBUG_VALIDATE_FIELD( + asyncio_thread_state, + asyncio_running_task, + sizeof(uintptr_t), + _Alignof(uintptr_t), + sizeof(_PyThreadStateImpl)); + PY_REMOTE_DEBUG_VALIDATE_FIELD( + asyncio_thread_state, + asyncio_tasks_head, + SIZEOF_LLIST_NODE, + _Alignof(struct llist_node), + 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_FIXED_FIELD +#undef FIELD_SIZE + /* ============================================================================ * STRUCTSEQ TYPE DEFINITIONS * ============================================================================ */ @@ -434,7 +917,7 @@ validate_debug_offsets(struct _Py_DebugOffsets *debug_offsets) return -1; } - return 0; + return validate_debug_offsets_layout(debug_offsets); } // Generic function to iterate through all threads @@ -877,8 +1360,13 @@ read_async_debug( 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 (validate_async_debug_offsets_layout(&unwinder->async_debug_offsets) < 0) { + set_exception_cause(unwinder, PyExc_RuntimeError, "Invalid AsyncioDebug offsets"); + return PY_REMOTE_DEBUG_INVALID_ASYNC_DEBUG_OFFSETS; + } + return 0; } /* ============================================================================ @@ -2054,10 +2542,15 @@ static void * find_frame_in_chunks(StackChunkList *chunks, uintptr_t remote_ptr) { for (size_t i = 0; i < chunks->count; ++i) { + if (chunks->chunks[i].size <= offsetof(_PyStackChunk, data)) { + continue; + } 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); } } @@ -2624,7 +3117,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 d762602459536ceea7bf36fb86ee6726c5fbdf34 Mon Sep 17 00:00:00 2001 From: "Miss Islington (bot)" <31488909+miss-islington@users.noreply.github.com> Date: Mon, 13 Apr 2026 03:40:54 +0200 Subject: [PATCH 4/4] 00482: CVE-2026-6100 Fix a possible UAF in {LZMA,BZ2,_Zlib}Decompressor Co-authored-by: Stan Ulbrych --- .../Security/2026-04-10-16-28-21.gh-issue-148395.kfzm0G.rst | 5 +++++ Modules/_bz2module.c | 1 + Modules/_lzmamodule.c | 1 + Modules/zlibmodule.c | 1 + 4 files changed, 8 insertions(+) create mode 100644 Misc/NEWS.d/next/Security/2026-04-10-16-28-21.gh-issue-148395.kfzm0G.rst diff --git a/Misc/NEWS.d/next/Security/2026-04-10-16-28-21.gh-issue-148395.kfzm0G.rst b/Misc/NEWS.d/next/Security/2026-04-10-16-28-21.gh-issue-148395.kfzm0G.rst new file mode 100644 index 00000000000000..9502189ab199c1 --- /dev/null +++ b/Misc/NEWS.d/next/Security/2026-04-10-16-28-21.gh-issue-148395.kfzm0G.rst @@ -0,0 +1,5 @@ +Fix a dangling input pointer in :class:`lzma.LZMADecompressor`, +:class:`bz2.BZ2Decompressor`, and internal :class:`!zlib._ZlibDecompressor` +when memory allocation fails with :exc:`MemoryError`, which could let a +subsequent :meth:`!decompress` call read or write through a stale pointer to +the already-released caller buffer. diff --git a/Modules/_bz2module.c b/Modules/_bz2module.c index 9e85e0de42cd8d..055ce82e7d2863 100644 --- a/Modules/_bz2module.c +++ b/Modules/_bz2module.c @@ -593,6 +593,7 @@ decompress(BZ2Decompressor *d, char *data, size_t len, Py_ssize_t max_length) return result; error: + bzs->next_in = NULL; Py_XDECREF(result); return NULL; } diff --git a/Modules/_lzmamodule.c b/Modules/_lzmamodule.c index 462c2181fa6036..6785dc56730c5c 100644 --- a/Modules/_lzmamodule.c +++ b/Modules/_lzmamodule.c @@ -1120,6 +1120,7 @@ decompress(Decompressor *d, uint8_t *data, size_t len, Py_ssize_t max_length) return result; error: + lzs->next_in = NULL; Py_XDECREF(result); return NULL; } diff --git a/Modules/zlibmodule.c b/Modules/zlibmodule.c index 5b6b0c5cac864a..a86aa5fdbb576c 100644 --- a/Modules/zlibmodule.c +++ b/Modules/zlibmodule.c @@ -1675,6 +1675,7 @@ decompress(ZlibDecompressor *self, uint8_t *data, return result; error: + self->zst.next_in = NULL; Py_XDECREF(result); return NULL; }