Submitted By:            Joe Locash
Date:                    2026-04-13
Update Date:             2026-04-15
Initial Package Version: 3.14.4
Upstream Status:         Applied
Origin:                  Upstream:
                           https://github.com/python/cpython/pull/148342
                           https://github.com/python/cpython/pull/148480
                           https://github.com/python/cpython/pull/148516
                           https://github.com/python/cpython/pull/148577
Description:             Upstream fixes for CVE-2026-1502, CVE-2026-6100,
                         CVE-2026-4786, and CVE-2026-5713.

From afdd351544e8112d4070a31f2218f99256697472 Mon Sep 17 00:00:00 2001
From: Seth Larson <seth@python.org>
Date: Fri, 10 Apr 2026 10:21:42 -0500
Subject: [PATCH] gh-146211: Reject CR/LF in HTTP tunnel request headers
 (GH-146212) (cherry picked from commit
 05ed7ce7ae9e17c23a04085b2539fe6d6d3cef69)

Co-authored-by: Seth Larson <seth@python.org>
Co-authored-by: Illia Volochii <illia.volochii@gmail.com>
---
 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 c8d8173c4b06d06902c99ec010ad785a30952880 Mon Sep 17 00:00:00 2001
From: Stan Ulbrych <stan@python.org>
Date: Mon, 13 Apr 2026 02:14:54 +0100
Subject: [PATCH] gh-148395: Fix a possible UAF in
 `{LZMA,BZ2,_Zlib}Decompressor` (GH-148396)

Fix dangling input pointer after `MemoryError` in _lzma/_bz2/_ZlibDecompressor.decompress
(cherry picked from commit 8fc66aef6d7b3ae58f43f5c66f9366cc8cbbfcd2)

Co-authored-by: Stan Ulbrych <stan@python.org>
---
 .../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;
 }
From f529b9470752c28ab69c96f31b0dbc10db69b404 Mon Sep 17 00:00:00 2001
From: Stan Ulbrych <stan@python.org>
Date: Mon, 13 Apr 2026 20:02:52 +0100
Subject: [PATCH] gh-148169: Fix webbrowser `%action` substitution bypass of
 dash-prefix check (GH-148170) (cherry picked from commit
 d22922c8a7958353689dc4763dd72da2dea03fff)

Co-authored-by: Stan Ulbrych <stan@python.org>
---
 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 6e05c08fb730d0ef5340478546e3a9e88601f1ec Mon Sep 17 00:00:00 2001
From: Pablo Galindo Salgado <Pablogsal@gmail.com>
Date: Mon, 13 Apr 2026 23:22:23 +0100
Subject: [PATCH] [3.14] gh-148178: Validate remote debug offset tables on load
 (GH-148187)

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.
(cherry picked from commit 289fd2c97a7e5aecb8b69f94f5e838ccfeee7e67)

Co-authored-by: Pablo Galindo Salgado <Pablogsal@gmail.com>
---
 ...-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 <internal/pycore_interpframe.h>    // FRAME_OWNED_BY_CSTACK
 #include <internal/pycore_llist.h>          // struct llist_node
 #include <internal/pycore_stackref.h>       // Py_TAG_BITS
+#include <internal/pycore_tstate.h>         // _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;
