[Date Prev][Date Next] [Thread Prev][Thread Next] [Date Index] [Thread Index]

Bug#1091325: bookworm-pu: package pypy3/7.3.11+dfsg-2+deb12u3



Package: release.debian.org
Severity: normal
Tags: bookworm
X-Debbugs-Cc: pypy3@packages.debian.org
Control: affects -1 + src:pypy3
User: release.debian.org@packages.debian.org
Usertags: pu

Update pypy3 for the same security issues as cpython3 in bookworm:

* Security patches to the standard library:
 - CVE-2023-27043: Parse email addresses with special characters,
   correctly.
 - CVE-2024-9287: Quote path names in venv activation scripts.
 - CVE-2024-4032: Fix private IP address ranges.
 - CVE-2024-6232: Fix ReDoS when parsing tarfile headers.
 - CVE-2024-8088: Avoid infinite loop in zip file parsing.
 - CVE-2024-6923: Encode newlines in headers in the email module.
 - CVE-2024-7592: Quadratic complexity parsing cookies with backslashes.
 - CVE-2024-11168: Ensure addresses in brackets are valid IPv6 addresses.

And some housekeeping:

* Clean the python 2.7 source tree.
* Clean cffi modules C source, lex and yacc tabs.

[ Reason ]
Security updates that the security team doesn't consider urgent enough
to warrant a DSA for.

[ Impact ]
Of these, CVE-2024-8088 is labelled HIGH in its description. I see we
also issued a DSA for CVE-2024-4032 in cpython.

[ Tests ]
The patches all come from cPython, so we've already got some confidence
in them. cPython is pretty good about including regression tests in
everything. I've test-built pypy3 and checked that all the new tests are
passing (pypy3 always has a lot of failing tests, so we ignore results,
sorry about that).

[ Risks ]
If they weren't an issue for cPython, they're almost certainly not for
PyPy, the user-base is much smaller.

[ Checklist ]
  [x] *all* changes are documented in the d/changelog
  [x] I reviewed all changes and I approve them
  [x] attach debdiff against the package in (old)stable
  [x] the issue is verified as fixed in unstable

[ Changes ]
See the changelog above.

[ Other info ]
Uploaded to bookworm.
diff -Nru pypy3-7.3.11+dfsg/debian/changelog pypy3-7.3.11+dfsg/debian/changelog
--- pypy3-7.3.11+dfsg/debian/changelog	2024-05-01 20:39:38.000000000 -0400
+++ pypy3-7.3.11+dfsg/debian/changelog	2024-12-23 16:22:45.000000000 -0400
@@ -1,3 +1,20 @@
+pypy3 (7.3.11+dfsg-2+deb12u3) bookworm; urgency=medium
+
+  * Security patches to the standard library:
+    - CVE-2023-27043: Parse email addresses with special characters,
+      correctly.
+    - CVE-2024-9287: Quote path names in venv activation scripts.
+    - CVE-2024-4032: Fix private IP address ranges.
+    - CVE-2024-6232: Fix ReDoS when parsing tarfile headers.
+    - CVE-2024-8088: Avoid infinite loop in zip file parsing.
+    - CVE-2024-6923: Encode newlines in headers in the email module.
+    - CVE-2024-7592: Quadratic complexity parsing cookies with backslashes.
+    - CVE-2024-11168: Ensure addresses in brackets are valid IPv6 addresses.
+  * Clean the python 2.7 source tree.
+  * Clean cffi modules C source, lex and yacc tabs.
+
+ -- Stefano Rivera <stefanor@debian.org>  Mon, 23 Dec 2024 16:22:45 -0400
+
 pypy3 (7.3.11+dfsg-2+deb12u2) bookworm; urgency=medium
 
   * Security patches to the standard library:
diff -Nru pypy3-7.3.11+dfsg/debian/clean pypy3-7.3.11+dfsg/debian/clean
--- pypy3-7.3.11+dfsg/debian/clean	2024-05-01 20:39:38.000000000 -0400
+++ pypy3-7.3.11+dfsg/debian/clean	2024-12-23 16:22:45.000000000 -0400
@@ -24,6 +24,16 @@
              -a ! -path 'lib_pypy/_cffi_ssl/_cffi_src/*' \
              -a ! -path 'lib_pypy/_libmpdec/*' \
              -a ! -path 'lib_pypy/_sha3/*'
+echo lib_pypy/_blake2/_blake2b_cffi.c
+echo lib_pypy/_blake2/_blake2s_cffi.c
+echo lib_pypy/_sha3/_sha3_cffi.c
+echo pypy/goal/lextab.py
+echo pypy/goal/yacctab.py
+echo pypy/lextab.py
+echo pypy/yacctab.py
+
+# Python 2.7
+echo cpython27/python
 
 # Tests
 echo pypy/test.db
diff -Nru pypy3-7.3.11+dfsg/debian/patches/CVE-2023-27043-email-parseaddr pypy3-7.3.11+dfsg/debian/patches/CVE-2023-27043-email-parseaddr
--- pypy3-7.3.11+dfsg/debian/patches/CVE-2023-27043-email-parseaddr	1969-12-31 20:00:00.000000000 -0400
+++ pypy3-7.3.11+dfsg/debian/patches/CVE-2023-27043-email-parseaddr	2024-12-23 16:22:45.000000000 -0400
@@ -0,0 +1,434 @@
+From: Petr Viktorin <encukou@gmail.com>
+Date: Fri, 6 Sep 2024 13:14:22 +0200
+Subject: gh-102988: Reject malformed addresses in email.parseaddr()
+ (GH-111116) (#123768)
+
+Detect email address parsing errors and return empty tuple to
+indicate the parsing error (old API). Add an optional 'strict'
+parameter to getaddresses() and parseaddr() functions. Patch by
+Thomas Dwyer.
+
+(cherry picked from commit 4a153a1d3b18803a684cd1bcc2cdf3ede3dbae19)
+
+Co-authored-by: Victor Stinner <vstinner@python.org>
+Co-Authored-By: Thomas Dwyer <github@tomd.tel>
+
+Origin: upstream, https://github.com/python/cpython/commit/2a9273a0e4466e2f057f9ce6fe98cd8ce570331b
+---
+ lib-python/3/email/utils.py                | 151 +++++++++++++++++++--
+ lib-python/3/test/test_email/test_email.py | 204 +++++++++++++++++++++++++++--
+ 2 files changed, 338 insertions(+), 17 deletions(-)
+
+diff --git a/lib-python/3/email/utils.py b/lib-python/3/email/utils.py
+index 48d3016..7ca7a7c 100644
+--- a/lib-python/3/email/utils.py
++++ b/lib-python/3/email/utils.py
+@@ -48,6 +48,7 @@ TICK = "'"
+ specialsre = re.compile(r'[][\\()<>@,:;".]')
+ escapesre = re.compile(r'[\\"]')
+ 
++
+ def _has_surrogates(s):
+     """Return True if s contains surrogate-escaped binary data."""
+     # This check is based on the fact that unless there are surrogates, utf8
+@@ -106,12 +107,127 @@ def formataddr(pair, charset='utf-8'):
+     return address
+ 
+ 
++def _iter_escaped_chars(addr):
++    pos = 0
++    escape = False
++    for pos, ch in enumerate(addr):
++        if escape:
++            yield (pos, '\\' + ch)
++            escape = False
++        elif ch == '\\':
++            escape = True
++        else:
++            yield (pos, ch)
++    if escape:
++        yield (pos, '\\')
++
++
++def _strip_quoted_realnames(addr):
++    """Strip real names between quotes."""
++    if '"' not in addr:
++        # Fast path
++        return addr
++
++    start = 0
++    open_pos = None
++    result = []
++    for pos, ch in _iter_escaped_chars(addr):
++        if ch == '"':
++            if open_pos is None:
++                open_pos = pos
++            else:
++                if start != open_pos:
++                    result.append(addr[start:open_pos])
++                start = pos + 1
++                open_pos = None
++
++    if start < len(addr):
++        result.append(addr[start:])
++
++    return ''.join(result)
+ 
+-def getaddresses(fieldvalues):
+-    """Return a list of (REALNAME, EMAIL) for each fieldvalue."""
+-    all = COMMASPACE.join(str(v) for v in fieldvalues)
+-    a = _AddressList(all)
+-    return a.addresslist
++
++supports_strict_parsing = True
++
++def getaddresses(fieldvalues, *, strict=True):
++    """Return a list of (REALNAME, EMAIL) or ('','') for each fieldvalue.
++
++    When parsing fails for a fieldvalue, a 2-tuple of ('', '') is returned in
++    its place.
++
++    If strict is true, use a strict parser which rejects malformed inputs.
++    """
++
++    # If strict is true, if the resulting list of parsed addresses is greater
++    # than the number of fieldvalues in the input list, a parsing error has
++    # occurred and consequently a list containing a single empty 2-tuple [('',
++    # '')] is returned in its place. This is done to avoid invalid output.
++    #
++    # Malformed input: getaddresses(['alice@example.com <bob@example.com>'])
++    # Invalid output: [('', 'alice@example.com'), ('', 'bob@example.com')]
++    # Safe output: [('', '')]
++
++    if not strict:
++        all = COMMASPACE.join(str(v) for v in fieldvalues)
++        a = _AddressList(all)
++        return a.addresslist
++
++    fieldvalues = [str(v) for v in fieldvalues]
++    fieldvalues = _pre_parse_validation(fieldvalues)
++    addr = COMMASPACE.join(fieldvalues)
++    a = _AddressList(addr)
++    result = _post_parse_validation(a.addresslist)
++
++    # Treat output as invalid if the number of addresses is not equal to the
++    # expected number of addresses.
++    n = 0
++    for v in fieldvalues:
++        # When a comma is used in the Real Name part it is not a deliminator.
++        # So strip those out before counting the commas.
++        v = _strip_quoted_realnames(v)
++        # Expected number of addresses: 1 + number of commas
++        n += 1 + v.count(',')
++    if len(result) != n:
++        return [('', '')]
++
++    return result
++
++
++def _check_parenthesis(addr):
++    # Ignore parenthesis in quoted real names.
++    addr = _strip_quoted_realnames(addr)
++
++    opens = 0
++    for pos, ch in _iter_escaped_chars(addr):
++        if ch == '(':
++            opens += 1
++        elif ch == ')':
++            opens -= 1
++            if opens < 0:
++                return False
++    return (opens == 0)
++
++
++def _pre_parse_validation(email_header_fields):
++    accepted_values = []
++    for v in email_header_fields:
++        if not _check_parenthesis(v):
++            v = "('', '')"
++        accepted_values.append(v)
++
++    return accepted_values
++
++
++def _post_parse_validation(parsed_email_header_tuples):
++    accepted_values = []
++    # The parser would have parsed a correctly formatted domain-literal
++    # The existence of an [ after parsing indicates a parsing failure
++    for v in parsed_email_header_tuples:
++        if '[' in v[1]:
++            v = ('', '')
++        accepted_values.append(v)
++
++    return accepted_values
+ 
+ 
+ def _format_timetuple_and_zone(timetuple, zone):
+@@ -202,16 +318,33 @@ def parsedate_to_datetime(data):
+             tzinfo=datetime.timezone(datetime.timedelta(seconds=tz)))
+ 
+ 
+-def parseaddr(addr):
++def parseaddr(addr, *, strict=True):
+     """
+     Parse addr into its constituent realname and email address parts.
+ 
+     Return a tuple of realname and email address, unless the parse fails, in
+     which case return a 2-tuple of ('', '').
++
++    If strict is True, use a strict parser which rejects malformed inputs.
+     """
+-    addrs = _AddressList(addr).addresslist
+-    if not addrs:
+-        return '', ''
++    if not strict:
++        addrs = _AddressList(addr).addresslist
++        if not addrs:
++            return ('', '')
++        return addrs[0]
++
++    if isinstance(addr, list):
++        addr = addr[0]
++
++    if not isinstance(addr, str):
++        return ('', '')
++
++    addr = _pre_parse_validation([addr])[0]
++    addrs = _post_parse_validation(_AddressList(addr).addresslist)
++
++    if not addrs or len(addrs) > 1:
++        return ('', '')
++
+     return addrs[0]
+ 
+ 
+diff --git a/lib-python/3/test/test_email/test_email.py b/lib-python/3/test/test_email/test_email.py
+index 761ea90..0c68964 100644
+--- a/lib-python/3/test/test_email/test_email.py
++++ b/lib-python/3/test/test_email/test_email.py
+@@ -16,6 +16,7 @@ from unittest.mock import patch
+ 
+ import email
+ import email.policy
++import email.utils
+ 
+ from email.charset import Charset
+ from email.header import Header, decode_header, make_header
+@@ -3263,15 +3264,154 @@ Foo
+            [('Al Person', 'aperson@dom.ain'),
+             ('Bud Person', 'bperson@dom.ain')])
+ 
++    def test_getaddresses_comma_in_name(self):
++        """GH-106669 regression test."""
++        self.assertEqual(
++            utils.getaddresses(
++                [
++                    '"Bud, Person" <bperson@dom.ain>',
++                    'aperson@dom.ain (Al Person)',
++                    '"Mariusz Felisiak" <to@example.com>',
++                ]
++            ),
++            [
++                ('Bud, Person', 'bperson@dom.ain'),
++                ('Al Person', 'aperson@dom.ain'),
++                ('Mariusz Felisiak', 'to@example.com'),
++            ],
++        )
++
++    def test_parsing_errors(self):
++        """Test for parsing errors from CVE-2023-27043 and CVE-2019-16056"""
++        alice = 'alice@example.org'
++        bob = 'bob@example.com'
++        empty = ('', '')
++
++        # Test utils.getaddresses() and utils.parseaddr() on malformed email
++        # addresses: default behavior (strict=True) rejects malformed address,
++        # and strict=False which tolerates malformed address.
++        for invalid_separator, expected_non_strict in (
++            ('(', [(f'<{bob}>', alice)]),
++            (')', [('', alice), empty, ('', bob)]),
++            ('<', [('', alice), empty, ('', bob), empty]),
++            ('>', [('', alice), empty, ('', bob)]),
++            ('[', [('', f'{alice}[<{bob}>]')]),
++            (']', [('', alice), empty, ('', bob)]),
++            ('@', [empty, empty, ('', bob)]),
++            (';', [('', alice), empty, ('', bob)]),
++            (':', [('', alice), ('', bob)]),
++            ('.', [('', alice + '.'), ('', bob)]),
++            ('"', [('', alice), ('', f'<{bob}>')]),
++        ):
++            address = f'{alice}{invalid_separator}<{bob}>'
++            with self.subTest(address=address):
++                self.assertEqual(utils.getaddresses([address]),
++                                 [empty])
++                self.assertEqual(utils.getaddresses([address], strict=False),
++                                 expected_non_strict)
++
++                self.assertEqual(utils.parseaddr([address]),
++                                 empty)
++                self.assertEqual(utils.parseaddr([address], strict=False),
++                                 ('', address))
++
++        # Comma (',') is treated differently depending on strict parameter.
++        # Comma without quotes.
++        address = f'{alice},<{bob}>'
++        self.assertEqual(utils.getaddresses([address]),
++                         [('', alice), ('', bob)])
++        self.assertEqual(utils.getaddresses([address], strict=False),
++                         [('', alice), ('', bob)])
++        self.assertEqual(utils.parseaddr([address]),
++                         empty)
++        self.assertEqual(utils.parseaddr([address], strict=False),
++                         ('', address))
++
++        # Real name between quotes containing comma.
++        address = '"Alice, alice@example.org" <bob@example.com>'
++        expected_strict = ('Alice, alice@example.org', 'bob@example.com')
++        self.assertEqual(utils.getaddresses([address]), [expected_strict])
++        self.assertEqual(utils.getaddresses([address], strict=False), [expected_strict])
++        self.assertEqual(utils.parseaddr([address]), expected_strict)
++        self.assertEqual(utils.parseaddr([address], strict=False),
++                         ('', address))
++
++        # Valid parenthesis in comments.
++        address = 'alice@example.org (Alice)'
++        expected_strict = ('Alice', 'alice@example.org')
++        self.assertEqual(utils.getaddresses([address]), [expected_strict])
++        self.assertEqual(utils.getaddresses([address], strict=False), [expected_strict])
++        self.assertEqual(utils.parseaddr([address]), expected_strict)
++        self.assertEqual(utils.parseaddr([address], strict=False),
++                         ('', address))
++
++        # Invalid parenthesis in comments.
++        address = 'alice@example.org )Alice('
++        self.assertEqual(utils.getaddresses([address]), [empty])
++        self.assertEqual(utils.getaddresses([address], strict=False),
++                         [('', 'alice@example.org'), ('', ''), ('', 'Alice')])
++        self.assertEqual(utils.parseaddr([address]), empty)
++        self.assertEqual(utils.parseaddr([address], strict=False),
++                         ('', address))
++
++        # Two addresses with quotes separated by comma.
++        address = '"Jane Doe" <jane@example.net>, "John Doe" <john@example.net>'
++        self.assertEqual(utils.getaddresses([address]),
++                         [('Jane Doe', 'jane@example.net'),
++                          ('John Doe', 'john@example.net')])
++        self.assertEqual(utils.getaddresses([address], strict=False),
++                         [('Jane Doe', 'jane@example.net'),
++                          ('John Doe', 'john@example.net')])
++        self.assertEqual(utils.parseaddr([address]), empty)
++        self.assertEqual(utils.parseaddr([address], strict=False),
++                         ('', address))
++
++        # Test email.utils.supports_strict_parsing attribute
++        self.assertEqual(email.utils.supports_strict_parsing, True)
++
+     def test_getaddresses_nasty(self):
+-        eq = self.assertEqual
+-        eq(utils.getaddresses(['foo: ;']), [('', '')])
+-        eq(utils.getaddresses(
+-           ['[]*-- =~$']),
+-           [('', ''), ('', ''), ('', '*--')])
+-        eq(utils.getaddresses(
+-           ['foo: ;', '"Jason R. Mastaler" <jason@dom.ain>']),
+-           [('', ''), ('Jason R. Mastaler', 'jason@dom.ain')])
++        for addresses, expected in (
++            (['"Sürname, Firstname" <to@example.com>'],
++             [('Sürname, Firstname', 'to@example.com')]),
++
++            (['foo: ;'],
++             [('', '')]),
++
++            (['foo: ;', '"Jason R. Mastaler" <jason@dom.ain>'],
++             [('', ''), ('Jason R. Mastaler', 'jason@dom.ain')]),
++
++            ([r'Pete(A nice \) chap) <pete(his account)@silly.test(his host)>'],
++             [('Pete (A nice ) chap his account his host)', 'pete@silly.test')]),
++
++            (['(Empty list)(start)Undisclosed recipients  :(nobody(I know))'],
++             [('', '')]),
++
++            (['Mary <@machine.tld:mary@example.net>, , jdoe@test   . example'],
++             [('Mary', 'mary@example.net'), ('', ''), ('', 'jdoe@test.example')]),
++
++            (['John Doe <jdoe@machine(comment).  example>'],
++             [('John Doe (comment)', 'jdoe@machine.example')]),
++
++            (['"Mary Smith: Personal Account" <smith@home.example>'],
++             [('Mary Smith: Personal Account', 'smith@home.example')]),
++
++            (['Undisclosed recipients:;'],
++             [('', '')]),
++
++            ([r'<boss@nil.test>, "Giant; \"Big\" Box" <bob@example.net>'],
++             [('', 'boss@nil.test'), ('Giant; "Big" Box', 'bob@example.net')]),
++        ):
++            with self.subTest(addresses=addresses):
++                self.assertEqual(utils.getaddresses(addresses),
++                                 expected)
++                self.assertEqual(utils.getaddresses(addresses, strict=False),
++                                 expected)
++
++        addresses = ['[]*-- =~$']
++        self.assertEqual(utils.getaddresses(addresses),
++                         [('', '')])
++        self.assertEqual(utils.getaddresses(addresses, strict=False),
++                         [('', ''), ('', ''), ('', '*--')])
+ 
+     def test_getaddresses_embedded_comment(self):
+         """Test proper handling of a nested comment"""
+@@ -3460,6 +3600,54 @@ multipart/report
+                 m = cls(*constructor, policy=email.policy.default)
+                 self.assertIs(m.policy, email.policy.default)
+ 
++    def test_iter_escaped_chars(self):
++        self.assertEqual(list(utils._iter_escaped_chars(r'a\\b\"c\\"d')),
++                         [(0, 'a'),
++                          (2, '\\\\'),
++                          (3, 'b'),
++                          (5, '\\"'),
++                          (6, 'c'),
++                          (8, '\\\\'),
++                          (9, '"'),
++                          (10, 'd')])
++        self.assertEqual(list(utils._iter_escaped_chars('a\\')),
++                         [(0, 'a'), (1, '\\')])
++
++    def test_strip_quoted_realnames(self):
++        def check(addr, expected):
++            self.assertEqual(utils._strip_quoted_realnames(addr), expected)
++
++        check('"Jane Doe" <jane@example.net>, "John Doe" <john@example.net>',
++              ' <jane@example.net>,  <john@example.net>')
++        check(r'"Jane \"Doe\"." <jane@example.net>',
++              ' <jane@example.net>')
++
++        # special cases
++        check(r'before"name"after', 'beforeafter')
++        check(r'before"name"', 'before')
++        check(r'b"name"', 'b')  # single char
++        check(r'"name"after', 'after')
++        check(r'"name"a', 'a')  # single char
++        check(r'"name"', '')
++
++        # no change
++        for addr in (
++            'Jane Doe <jane@example.net>, John Doe <john@example.net>',
++            'lone " quote',
++        ):
++            self.assertEqual(utils._strip_quoted_realnames(addr), addr)
++
++
++    def test_check_parenthesis(self):
++        addr = 'alice@example.net'
++        self.assertTrue(utils._check_parenthesis(f'{addr} (Alice)'))
++        self.assertFalse(utils._check_parenthesis(f'{addr} )Alice('))
++        self.assertFalse(utils._check_parenthesis(f'{addr} (Alice))'))
++        self.assertFalse(utils._check_parenthesis(f'{addr} ((Alice)'))
++
++        # Ignore real name between quotes
++        self.assertTrue(utils._check_parenthesis(f'")Alice((" {addr}'))
++
+ 
+ # Test the iterator/generators
+ class TestIterators(TestEmailBase):
diff -Nru pypy3-7.3.11+dfsg/debian/patches/CVE-2024-11168-urllib-parse-bracketed-names pypy3-7.3.11+dfsg/debian/patches/CVE-2024-11168-urllib-parse-bracketed-names
--- pypy3-7.3.11+dfsg/debian/patches/CVE-2024-11168-urllib-parse-bracketed-names	1969-12-31 20:00:00.000000000 -0400
+++ pypy3-7.3.11+dfsg/debian/patches/CVE-2024-11168-urllib-parse-bracketed-names	2024-12-23 16:22:45.000000000 -0400
@@ -0,0 +1,99 @@
+From: Victor Stinner <vstinner@python.org>
+Date: Mon, 2 Dec 2024 13:36:46 +0100
+Subject: gh-103848: Adds checks to ensure that bracketed hosts found by
+ urlsplit are of IPv6 or IPvFuture format (#103849) (#126976)
+
+Co-authored-by: Gregory P. Smith <greg@krypto.org>
+(cherry picked from commit 29f348e232e82938ba2165843c448c2b291504c5)
+
+Co-authored-by: JohnJamesUtley <81572567+JohnJamesUtley@users.noreply.github.com>
+
+Origin: cpython, https://github.com/python/cpython/commit/ddca2953191c67a12b1f19d6bca41016c6ae7132
+---
+ lib-python/3/test/test_urlparse.py | 26 ++++++++++++++++++++++++++
+ lib-python/3/urllib/parse.py       | 16 +++++++++++++++-
+ 2 files changed, 41 insertions(+), 1 deletion(-)
+
+diff --git a/lib-python/3/test/test_urlparse.py b/lib-python/3/test/test_urlparse.py
+index 574da5b..c84df23 100644
+--- a/lib-python/3/test/test_urlparse.py
++++ b/lib-python/3/test/test_urlparse.py
+@@ -1071,6 +1071,32 @@ class UrlParseTestCase(unittest.TestCase):
+         self.assertEqual(p2.scheme, 'tel')
+         self.assertEqual(p2.path, '+31641044153')
+ 
++    def test_invalid_bracketed_hosts(self):
++        self.assertRaises(ValueError, urllib.parse.urlsplit, 'Scheme://user@[192.0.2.146]/Path?Query')
++        self.assertRaises(ValueError, urllib.parse.urlsplit, 'Scheme://user@[important.com:8000]/Path?Query')
++        self.assertRaises(ValueError, urllib.parse.urlsplit, 'Scheme://user@[v123r.IP]/Path?Query')
++        self.assertRaises(ValueError, urllib.parse.urlsplit, 'Scheme://user@[v12ae]/Path?Query')
++        self.assertRaises(ValueError, urllib.parse.urlsplit, 'Scheme://user@[v.IP]/Path?Query')
++        self.assertRaises(ValueError, urllib.parse.urlsplit, 'Scheme://user@[v123.]/Path?Query')
++        self.assertRaises(ValueError, urllib.parse.urlsplit, 'Scheme://user@[v]/Path?Query')
++        self.assertRaises(ValueError, urllib.parse.urlsplit, 'Scheme://user@[0439:23af::2309::fae7:1234]/Path?Query')
++        self.assertRaises(ValueError, urllib.parse.urlsplit, 'Scheme://user@[0439:23af:2309::fae7:1234:2342:438e:192.0.2.146]/Path?Query')
++        self.assertRaises(ValueError, urllib.parse.urlsplit, 'Scheme://user@]v6a.ip[/Path')
++
++    def test_splitting_bracketed_hosts(self):
++        p1 = urllib.parse.urlsplit('scheme://user@[v6a.ip]/path?query')
++        self.assertEqual(p1.hostname, 'v6a.ip')
++        self.assertEqual(p1.username, 'user')
++        self.assertEqual(p1.path, '/path')
++        p2 = urllib.parse.urlsplit('scheme://user@[0439:23af:2309::fae7%test]/path?query')
++        self.assertEqual(p2.hostname, '0439:23af:2309::fae7%test')
++        self.assertEqual(p2.username, 'user')
++        self.assertEqual(p2.path, '/path')
++        p3 = urllib.parse.urlsplit('scheme://user@[0439:23af:2309::fae7:1234:192.0.2.146%test]/path?query')
++        self.assertEqual(p3.hostname, '0439:23af:2309::fae7:1234:192.0.2.146%test')
++        self.assertEqual(p3.username, 'user')
++        self.assertEqual(p3.path, '/path')
++
+     def test_port_casting_failure_message(self):
+         message = "Port could not be cast to integer value as 'oracle'"
+         p1 = urllib.parse.urlparse('http://Server=sde; Service=sde:oracle')
+diff --git a/lib-python/3/urllib/parse.py b/lib-python/3/urllib/parse.py
+index f5d3662..39b0249 100644
+--- a/lib-python/3/urllib/parse.py
++++ b/lib-python/3/urllib/parse.py
+@@ -36,6 +36,7 @@ import sys
+ import types
+ import collections
+ import warnings
++import ipaddress
+ 
+ __all__ = ["urlparse", "urlunparse", "urljoin", "urldefrag",
+            "urlsplit", "urlunsplit", "urlencode", "parse_qs",
+@@ -442,6 +443,17 @@ def _checknetloc(netloc):
+             raise ValueError("netloc '" + netloc + "' contains invalid " +
+                              "characters under NFKC normalization")
+ 
++# Valid bracketed hosts are defined in
++# https://www.rfc-editor.org/rfc/rfc3986#page-49 and https://url.spec.whatwg.org/
++def _check_bracketed_host(hostname):
++    if hostname.startswith('v'):
++        if not re.match(r"\Av[a-fA-F0-9]+\..+\Z", hostname):
++            raise ValueError(f"IPvFuture address is invalid")
++    else:
++        ip = ipaddress.ip_address(hostname) # Throws Value Error if not IPv6 or IPv4
++        if isinstance(ip, ipaddress.IPv4Address):
++            raise ValueError(f"An IPv4 address cannot be in brackets")
++
+ def urlsplit(url, scheme='', allow_fragments=True):
+     """Parse a URL into 5 components:
+     <scheme>://<netloc>/<path>?<query>#<fragment>
+@@ -488,12 +500,14 @@ def urlsplit(url, scheme='', allow_fragments=True):
+                 break
+         else:
+             scheme, url = url[:i].lower(), url[i+1:]
+-
+     if url[:2] == '//':
+         netloc, url = _splitnetloc(url, 2)
+         if (('[' in netloc and ']' not in netloc) or
+                 (']' in netloc and '[' not in netloc)):
+             raise ValueError("Invalid IPv6 URL")
++        if '[' in netloc and ']' in netloc:
++            bracketed_host = netloc.partition('[')[2].partition(']')[0]
++            _check_bracketed_host(bracketed_host)
+     if allow_fragments and '#' in url:
+         url, fragment = url.split('#', 1)
+     if '?' in url:
diff -Nru pypy3-7.3.11+dfsg/debian/patches/CVE-2024-4032-private-ip-ranges pypy3-7.3.11+dfsg/debian/patches/CVE-2024-4032-private-ip-ranges
--- pypy3-7.3.11+dfsg/debian/patches/CVE-2024-4032-private-ip-ranges	1969-12-31 20:00:00.000000000 -0400
+++ pypy3-7.3.11+dfsg/debian/patches/CVE-2024-4032-private-ip-ranges	2024-12-23 16:22:45.000000000 -0400
@@ -0,0 +1,282 @@
+From: Petr Viktorin <encukou@gmail.com>
+Date: Tue, 7 May 2024 11:57:58 +0200
+Subject: gh-113171: gh-65056: Fix "private" (non-global) IP address ranges
+ (GH-113179) (GH-113186) (GH-118177) (GH-118472)
+
+The _private_networks variables, used by various is_private
+implementations, were missing some ranges and at the same time had
+overly strict ranges (where there are more specific ranges considered
+globally reachable by the IANA registries).
+
+This patch updates the ranges with what was missing or otherwise
+incorrect.
+
+100.64.0.0/10 is left alone, for now, as it's been made special in [1].
+
+The _address_exclude_many() call returns 8 networks for IPv4, 121
+networks for IPv6.
+
+[1] https://github.com/python/cpython/issues/61602
+
+In 3.10 and below, is_private checks whether the network and broadcast
+address are both private.
+In later versions (where the test wss backported from), it checks
+whether they both are in the same private network.
+
+For 0.0.0.0/0, both 0.0.0.0 and 255.225.255.255 are private,
+but one is in 0.0.0.0/8 ("This network") and the other in
+255.255.255.255/32 ("Limited broadcast").
+
+---------
+
+Co-authored-by: Jakub Stasiak <jakub@stasiak.at>
+
+Origin: cpython, https://github.com/python/cpython/commit/22adf29da8d99933ffed8647d3e0726edd16f7f8
+---
+ lib-python/3/ipaddress.py           | 95 ++++++++++++++++++++++++++++++-------
+ lib-python/3/test/test_ipaddress.py | 52 ++++++++++++++++++++
+ 2 files changed, 130 insertions(+), 17 deletions(-)
+
+diff --git a/lib-python/3/ipaddress.py b/lib-python/3/ipaddress.py
+index 25f373a..9b35340 100644
+--- a/lib-python/3/ipaddress.py
++++ b/lib-python/3/ipaddress.py
+@@ -1322,18 +1322,41 @@ class IPv4Address(_BaseV4, _BaseAddress):
+     @property
+     @functools.lru_cache()
+     def is_private(self):
+-        """Test if this address is allocated for private networks.
++        """``True`` if the address is defined as not globally reachable by
++        iana-ipv4-special-registry_ (for IPv4) or iana-ipv6-special-registry_
++        (for IPv6) with the following exceptions:
+ 
+-        Returns:
+-            A boolean, True if the address is reserved per
+-            iana-ipv4-special-registry.
++        * ``is_private`` is ``False`` for ``100.64.0.0/10``
++        * For IPv4-mapped IPv6-addresses the ``is_private`` value is determined by the
++            semantics of the underlying IPv4 addresses and the following condition holds
++            (see :attr:`IPv6Address.ipv4_mapped`)::
++
++                address.is_private == address.ipv4_mapped.is_private
+ 
++        ``is_private`` has value opposite to :attr:`is_global`, except for the ``100.64.0.0/10``
++        IPv4 range where they are both ``False``.
+         """
+-        return any(self in net for net in self._constants._private_networks)
++        return (
++            any(self in net for net in self._constants._private_networks)
++            and all(self not in net for net in self._constants._private_networks_exceptions)
++        )
+ 
+     @property
+     @functools.lru_cache()
+     def is_global(self):
++        """``True`` if the address is defined as globally reachable by
++        iana-ipv4-special-registry_ (for IPv4) or iana-ipv6-special-registry_
++        (for IPv6) with the following exception:
++
++        For IPv4-mapped IPv6-addresses the ``is_private`` value is determined by the
++        semantics of the underlying IPv4 addresses and the following condition holds
++        (see :attr:`IPv6Address.ipv4_mapped`)::
++
++            address.is_global == address.ipv4_mapped.is_global
++
++        ``is_global`` has value opposite to :attr:`is_private`, except for the ``100.64.0.0/10``
++        IPv4 range where they are both ``False``.
++        """
+         return self not in self._constants._public_network and not self.is_private
+ 
+     @property
+@@ -1537,13 +1560,15 @@ class _IPv4Constants:
+ 
+     _public_network = IPv4Network('100.64.0.0/10')
+ 
++    # Not globally reachable address blocks listed on
++    # https://www.iana.org/assignments/iana-ipv4-special-registry/iana-ipv4-special-registry.xhtml
+     _private_networks = [
+         IPv4Network('0.0.0.0/8'),
+         IPv4Network('10.0.0.0/8'),
+         IPv4Network('127.0.0.0/8'),
+         IPv4Network('169.254.0.0/16'),
+         IPv4Network('172.16.0.0/12'),
+-        IPv4Network('192.0.0.0/29'),
++        IPv4Network('192.0.0.0/24'),
+         IPv4Network('192.0.0.170/31'),
+         IPv4Network('192.0.2.0/24'),
+         IPv4Network('192.168.0.0/16'),
+@@ -1554,6 +1579,11 @@ class _IPv4Constants:
+         IPv4Network('255.255.255.255/32'),
+         ]
+ 
++    _private_networks_exceptions = [
++        IPv4Network('192.0.0.9/32'),
++        IPv4Network('192.0.0.10/32'),
++    ]
++
+     _reserved_network = IPv4Network('240.0.0.0/4')
+ 
+     _unspecified_address = IPv4Address('0.0.0.0')
+@@ -1995,23 +2025,42 @@ class IPv6Address(_BaseV6, _BaseAddress):
+     @property
+     @functools.lru_cache()
+     def is_private(self):
+-        """Test if this address is allocated for private networks.
++        """``True`` if the address is defined as not globally reachable by
++        iana-ipv4-special-registry_ (for IPv4) or iana-ipv6-special-registry_
++        (for IPv6) with the following exceptions:
+ 
+-        Returns:
+-            A boolean, True if the address is reserved per
+-            iana-ipv6-special-registry.
++        * ``is_private`` is ``False`` for ``100.64.0.0/10``
++        * For IPv4-mapped IPv6-addresses the ``is_private`` value is determined by the
++            semantics of the underlying IPv4 addresses and the following condition holds
++            (see :attr:`IPv6Address.ipv4_mapped`)::
++
++                address.is_private == address.ipv4_mapped.is_private
+ 
++        ``is_private`` has value opposite to :attr:`is_global`, except for the ``100.64.0.0/10``
++        IPv4 range where they are both ``False``.
+         """
+-        return any(self in net for net in self._constants._private_networks)
++        ipv4_mapped = self.ipv4_mapped
++        if ipv4_mapped is not None:
++            return ipv4_mapped.is_private
++        return (
++            any(self in net for net in self._constants._private_networks)
++            and all(self not in net for net in self._constants._private_networks_exceptions)
++        )
+ 
+     @property
+     def is_global(self):
+-        """Test if this address is allocated for public networks.
++        """``True`` if the address is defined as globally reachable by
++        iana-ipv4-special-registry_ (for IPv4) or iana-ipv6-special-registry_
++        (for IPv6) with the following exception:
+ 
+-        Returns:
+-            A boolean, true if the address is not reserved per
+-            iana-ipv6-special-registry.
++        For IPv4-mapped IPv6-addresses the ``is_private`` value is determined by the
++        semantics of the underlying IPv4 addresses and the following condition holds
++        (see :attr:`IPv6Address.ipv4_mapped`)::
++
++            address.is_global == address.ipv4_mapped.is_global
+ 
++        ``is_global`` has value opposite to :attr:`is_private`, except for the ``100.64.0.0/10``
++        IPv4 range where they are both ``False``.
+         """
+         return not self.is_private
+ 
+@@ -2252,19 +2301,31 @@ class _IPv6Constants:
+ 
+     _multicast_network = IPv6Network('ff00::/8')
+ 
++    # Not globally reachable address blocks listed on
++    # https://www.iana.org/assignments/iana-ipv6-special-registry/iana-ipv6-special-registry.xhtml
+     _private_networks = [
+         IPv6Network('::1/128'),
+         IPv6Network('::/128'),
+         IPv6Network('::ffff:0:0/96'),
++        IPv6Network('64:ff9b:1::/48'),
+         IPv6Network('100::/64'),
+         IPv6Network('2001::/23'),
+-        IPv6Network('2001:2::/48'),
+         IPv6Network('2001:db8::/32'),
+-        IPv6Network('2001:10::/28'),
++        # IANA says N/A, let's consider it not globally reachable to be safe
++        IPv6Network('2002::/16'),
+         IPv6Network('fc00::/7'),
+         IPv6Network('fe80::/10'),
+         ]
+ 
++    _private_networks_exceptions = [
++        IPv6Network('2001:1::1/128'),
++        IPv6Network('2001:1::2/128'),
++        IPv6Network('2001:3::/32'),
++        IPv6Network('2001:4:112::/48'),
++        IPv6Network('2001:20::/28'),
++        IPv6Network('2001:30::/28'),
++    ]
++
+     _reserved_networks = [
+         IPv6Network('::/8'), IPv6Network('100::/8'),
+         IPv6Network('200::/7'), IPv6Network('400::/6'),
+diff --git a/lib-python/3/test/test_ipaddress.py b/lib-python/3/test/test_ipaddress.py
+index 90897f6..bd14f04 100644
+--- a/lib-python/3/test/test_ipaddress.py
++++ b/lib-python/3/test/test_ipaddress.py
+@@ -2263,6 +2263,10 @@ class IpaddrUnitTest(unittest.TestCase):
+         self.assertEqual(True, ipaddress.ip_address(
+                 '172.31.255.255').is_private)
+         self.assertEqual(False, ipaddress.ip_address('172.32.0.0').is_private)
++        self.assertFalse(ipaddress.ip_address('192.0.0.0').is_global)
++        self.assertTrue(ipaddress.ip_address('192.0.0.9').is_global)
++        self.assertTrue(ipaddress.ip_address('192.0.0.10').is_global)
++        self.assertFalse(ipaddress.ip_address('192.0.0.255').is_global)
+ 
+         self.assertEqual(True,
+                          ipaddress.ip_address('169.254.100.200').is_link_local)
+@@ -2278,6 +2282,40 @@ class IpaddrUnitTest(unittest.TestCase):
+         self.assertEqual(False, ipaddress.ip_address('128.0.0.0').is_loopback)
+         self.assertEqual(True, ipaddress.ip_network('0.0.0.0').is_unspecified)
+ 
++    def testPrivateNetworks(self):
++        self.assertEqual(True, ipaddress.ip_network("0.0.0.0/0").is_private)
++        self.assertEqual(False, ipaddress.ip_network("1.0.0.0/8").is_private)
++
++        self.assertEqual(True, ipaddress.ip_network("0.0.0.0/8").is_private)
++        self.assertEqual(True, ipaddress.ip_network("10.0.0.0/8").is_private)
++        self.assertEqual(True, ipaddress.ip_network("127.0.0.0/8").is_private)
++        self.assertEqual(True, ipaddress.ip_network("169.254.0.0/16").is_private)
++        self.assertEqual(True, ipaddress.ip_network("172.16.0.0/12").is_private)
++        self.assertEqual(True, ipaddress.ip_network("192.0.0.0/29").is_private)
++        self.assertEqual(False, ipaddress.ip_network("192.0.0.9/32").is_private)
++        self.assertEqual(True, ipaddress.ip_network("192.0.0.170/31").is_private)
++        self.assertEqual(True, ipaddress.ip_network("192.0.2.0/24").is_private)
++        self.assertEqual(True, ipaddress.ip_network("192.168.0.0/16").is_private)
++        self.assertEqual(True, ipaddress.ip_network("198.18.0.0/15").is_private)
++        self.assertEqual(True, ipaddress.ip_network("198.51.100.0/24").is_private)
++        self.assertEqual(True, ipaddress.ip_network("203.0.113.0/24").is_private)
++        self.assertEqual(True, ipaddress.ip_network("240.0.0.0/4").is_private)
++        self.assertEqual(True, ipaddress.ip_network("255.255.255.255/32").is_private)
++
++        self.assertEqual(False, ipaddress.ip_network("::/0").is_private)
++        self.assertEqual(False, ipaddress.ip_network("::ff/128").is_private)
++
++        self.assertEqual(True, ipaddress.ip_network("::1/128").is_private)
++        self.assertEqual(True, ipaddress.ip_network("::/128").is_private)
++        self.assertEqual(True, ipaddress.ip_network("::ffff:0:0/96").is_private)
++        self.assertEqual(True, ipaddress.ip_network("100::/64").is_private)
++        self.assertEqual(True, ipaddress.ip_network("2001:2::/48").is_private)
++        self.assertEqual(False, ipaddress.ip_network("2001:3::/48").is_private)
++        self.assertEqual(True, ipaddress.ip_network("2001:db8::/32").is_private)
++        self.assertEqual(True, ipaddress.ip_network("2001:10::/28").is_private)
++        self.assertEqual(True, ipaddress.ip_network("fc00::/7").is_private)
++        self.assertEqual(True, ipaddress.ip_network("fe80::/10").is_private)
++
+     def testReservedIpv6(self):
+ 
+         self.assertEqual(True, ipaddress.ip_network('ffff::').is_multicast)
+@@ -2351,6 +2389,20 @@ class IpaddrUnitTest(unittest.TestCase):
+         self.assertEqual(True, ipaddress.ip_address('0::0').is_unspecified)
+         self.assertEqual(False, ipaddress.ip_address('::1').is_unspecified)
+ 
++        self.assertFalse(ipaddress.ip_address('64:ff9b:1::').is_global)
++        self.assertFalse(ipaddress.ip_address('2001::').is_global)
++        self.assertTrue(ipaddress.ip_address('2001:1::1').is_global)
++        self.assertTrue(ipaddress.ip_address('2001:1::2').is_global)
++        self.assertFalse(ipaddress.ip_address('2001:2::').is_global)
++        self.assertTrue(ipaddress.ip_address('2001:3::').is_global)
++        self.assertFalse(ipaddress.ip_address('2001:4::').is_global)
++        self.assertTrue(ipaddress.ip_address('2001:4:112::').is_global)
++        self.assertFalse(ipaddress.ip_address('2001:10::').is_global)
++        self.assertTrue(ipaddress.ip_address('2001:20::').is_global)
++        self.assertTrue(ipaddress.ip_address('2001:30::').is_global)
++        self.assertFalse(ipaddress.ip_address('2001:40::').is_global)
++        self.assertFalse(ipaddress.ip_address('2002::').is_global)
++
+         # some generic IETF reserved addresses
+         self.assertEqual(True, ipaddress.ip_address('100::').is_reserved)
+         self.assertEqual(True, ipaddress.ip_network('4000::1/128').is_reserved)
diff -Nru pypy3-7.3.11+dfsg/debian/patches/CVE-2024-6232-tarfile-redos pypy3-7.3.11+dfsg/debian/patches/CVE-2024-6232-tarfile-redos
--- pypy3-7.3.11+dfsg/debian/patches/CVE-2024-6232-tarfile-redos	1969-12-31 20:00:00.000000000 -0400
+++ pypy3-7.3.11+dfsg/debian/patches/CVE-2024-6232-tarfile-redos	2024-12-23 16:22:45.000000000 -0400
@@ -0,0 +1,236 @@
+From: Seth Michael Larson <seth@python.org>
+Date: Wed, 4 Sep 2024 10:46:01 -0500
+Subject: gh-121285: Remove backtracking when parsing tarfile headers
+ (GH-121286) (#123641)
+
+* Remove backtracking when parsing tarfile headers
+* Rewrite PAX header parsing to be stricter
+* Optimize parsing of GNU extended sparse headers v0.0
+
+(cherry picked from commit 34ddb64d088dd7ccc321f6103d23153256caa5d4)
+
+Co-authored-by: Seth Michael Larson <seth@python.org>
+Co-authored-by: Kirill Podoprigora <kirill.bast9@mail.ru>
+Co-authored-by: Gregory P. Smith <greg@krypto.org>
+
+Origin: cpython, https://github.com/python/cpython/commit/b4225ca91547aa97ed3aca391614afbb255bc877
+---
+ lib-python/3/tarfile.py           | 105 ++++++++++++++++++++++++--------------
+ lib-python/3/test/test_tarfile.py |  42 +++++++++++++++
+ 2 files changed, 109 insertions(+), 38 deletions(-)
+
+diff --git a/lib-python/3/tarfile.py b/lib-python/3/tarfile.py
+index 9438b08..a0300e7 100644
+--- a/lib-python/3/tarfile.py
++++ b/lib-python/3/tarfile.py
+@@ -708,6 +708,9 @@ class ExFileObject(io.BufferedReader):
+         super().__init__(fileobj)
+ #class ExFileObject
+ 
++# Header length is digits followed by a space.
++_header_length_prefix_re = re.compile(br"([0-9]{1,20}) ")
++
+ #------------------
+ # Exported Classes
+ #------------------
+@@ -1229,41 +1232,59 @@ class TarInfo(object):
+         else:
+             pax_headers = tarfile.pax_headers.copy()
+ 
+-        # Check if the pax header contains a hdrcharset field. This tells us
+-        # the encoding of the path, linkpath, uname and gname fields. Normally,
+-        # these fields are UTF-8 encoded but since POSIX.1-2008 tar
+-        # implementations are allowed to store them as raw binary strings if
+-        # the translation to UTF-8 fails.
+-        match = re.search(br"\d+ hdrcharset=([^\n]+)\n", buf)
+-        if match is not None:
+-            pax_headers["hdrcharset"] = match.group(1).decode("utf-8")
+-
+-        # For the time being, we don't care about anything other than "BINARY".
+-        # The only other value that is currently allowed by the standard is
+-        # "ISO-IR 10646 2000 UTF-8" in other words UTF-8.
+-        hdrcharset = pax_headers.get("hdrcharset")
+-        if hdrcharset == "BINARY":
+-            encoding = tarfile.encoding
+-        else:
+-            encoding = "utf-8"
+-
+         # Parse pax header information. A record looks like that:
+         # "%d %s=%s\n" % (length, keyword, value). length is the size
+         # of the complete record including the length field itself and
+-        # the newline. keyword and value are both UTF-8 encoded strings.
+-        regex = re.compile(br"(\d+) ([^=]+)=")
++        # the newline.
+         pos = 0
+-        while True:
+-            match = regex.match(buf, pos)
+-            if not match:
+-                break
++        encoding = None
++        raw_headers = []
++        while len(buf) > pos and buf[pos] != 0x00:
++            if not (match := _header_length_prefix_re.match(buf, pos)):
++                raise InvalidHeaderError("invalid header")
++            try:
++                length = int(match.group(1))
++            except ValueError:
++                raise InvalidHeaderError("invalid header")
++            # Headers must be at least 5 bytes, shortest being '5 x=\n'.
++            # Value is allowed to be empty.
++            if length < 5:
++                raise InvalidHeaderError("invalid header")
++            if pos + length > len(buf):
++                raise InvalidHeaderError("invalid header")
++
++            header_value_end_offset = match.start(1) + length - 1  # Last byte of the header
++            keyword_and_value = buf[match.end(1) + 1:header_value_end_offset]
++            raw_keyword, equals, raw_value = keyword_and_value.partition(b"=")
+ 
+-            length, keyword = match.groups()
+-            length = int(length)
+-            if length == 0:
++            # Check the framing of the header. The last character must be '\n' (0x0A)
++            if not raw_keyword or equals != b"=" or buf[header_value_end_offset] != 0x0A:
+                 raise InvalidHeaderError("invalid header")
+-            value = buf[match.end(2) + 1:match.start(1) + length - 1]
++            raw_headers.append((length, raw_keyword, raw_value))
++
++            # Check if the pax header contains a hdrcharset field. This tells us
++            # the encoding of the path, linkpath, uname and gname fields. Normally,
++            # these fields are UTF-8 encoded but since POSIX.1-2008 tar
++            # implementations are allowed to store them as raw binary strings if
++            # the translation to UTF-8 fails. For the time being, we don't care about
++            # anything other than "BINARY". The only other value that is currently
++            # allowed by the standard is "ISO-IR 10646 2000 UTF-8" in other words UTF-8.
++            # Note that we only follow the initial 'hdrcharset' setting to preserve
++            # the initial behavior of the 'tarfile' module.
++            if raw_keyword == b"hdrcharset" and encoding is None:
++                if raw_value == b"BINARY":
++                    encoding = tarfile.encoding
++                else:  # This branch ensures only the first 'hdrcharset' header is used.
++                    encoding = "utf-8"
+ 
++            pos += length
++
++        # If no explicit hdrcharset is set, we use UTF-8 as a default.
++        if encoding is None:
++            encoding = "utf-8"
++
++        # After parsing the raw headers we can decode them to text.
++        for length, raw_keyword, raw_value in raw_headers:
+             # Normally, we could just use "utf-8" as the encoding and "strict"
+             # as the error handler, but we better not take the risk. For
+             # example, GNU tar <= 1.23 is known to store filenames it cannot
+@@ -1271,17 +1292,16 @@ class TarInfo(object):
+             # hdrcharset=BINARY header).
+             # We first try the strict standard encoding, and if that fails we
+             # fall back on the user's encoding and error handler.
+-            keyword = self._decode_pax_field(keyword, "utf-8", "utf-8",
++            keyword = self._decode_pax_field(raw_keyword, "utf-8", "utf-8",
+                     tarfile.errors)
+             if keyword in PAX_NAME_FIELDS:
+-                value = self._decode_pax_field(value, encoding, tarfile.encoding,
++                value = self._decode_pax_field(raw_value, encoding, tarfile.encoding,
+                         tarfile.errors)
+             else:
+-                value = self._decode_pax_field(value, "utf-8", "utf-8",
++                value = self._decode_pax_field(raw_value, "utf-8", "utf-8",
+                         tarfile.errors)
+ 
+             pax_headers[keyword] = value
+-            pos += length
+ 
+         # Fetch the next header.
+         try:
+@@ -1296,7 +1316,7 @@ class TarInfo(object):
+ 
+         elif "GNU.sparse.size" in pax_headers:
+             # GNU extended sparse format version 0.0.
+-            self._proc_gnusparse_00(next, pax_headers, buf)
++            self._proc_gnusparse_00(next, raw_headers)
+ 
+         elif pax_headers.get("GNU.sparse.major") == "1" and pax_headers.get("GNU.sparse.minor") == "0":
+             # GNU extended sparse format version 1.0.
+@@ -1318,15 +1338,24 @@ class TarInfo(object):
+ 
+         return next
+ 
+-    def _proc_gnusparse_00(self, next, pax_headers, buf):
++    def _proc_gnusparse_00(self, next, raw_headers):
+         """Process a GNU tar extended sparse header, version 0.0.
+         """
+         offsets = []
+-        for match in re.finditer(br"\d+ GNU.sparse.offset=(\d+)\n", buf):
+-            offsets.append(int(match.group(1)))
+         numbytes = []
+-        for match in re.finditer(br"\d+ GNU.sparse.numbytes=(\d+)\n", buf):
+-            numbytes.append(int(match.group(1)))
++        for _, keyword, value in raw_headers:
++            if keyword == b"GNU.sparse.offset":
++                try:
++                    offsets.append(int(value.decode()))
++                except ValueError:
++                    raise InvalidHeaderError("invalid header")
++
++            elif keyword == b"GNU.sparse.numbytes":
++                try:
++                    numbytes.append(int(value.decode()))
++                except ValueError:
++                    raise InvalidHeaderError("invalid header")
++
+         next.sparse = list(zip(offsets, numbytes))
+ 
+     def _proc_gnusparse_01(self, next, pax_headers):
+diff --git a/lib-python/3/test/test_tarfile.py b/lib-python/3/test/test_tarfile.py
+index df634bc..28b909d 100644
+--- a/lib-python/3/test/test_tarfile.py
++++ b/lib-python/3/test/test_tarfile.py
+@@ -1109,6 +1109,48 @@ class PaxReadTest(LongnameTest, ReadTest, unittest.TestCase):
+         finally:
+             tar.close()
+ 
++    def test_pax_header_bad_formats(self):
++        # The fields from the pax header have priority over the
++        # TarInfo.
++        pax_header_replacements = (
++            b" foo=bar\n",
++            b"0 \n",
++            b"1 \n",
++            b"2 \n",
++            b"3 =\n",
++            b"4 =a\n",
++            b"1000000 foo=bar\n",
++            b"0 foo=bar\n",
++            b"-12 foo=bar\n",
++            b"000000000000000000000000036 foo=bar\n",
++        )
++        pax_headers = {"foo": "bar"}
++
++        for replacement in pax_header_replacements:
++            with self.subTest(header=replacement):
++                tar = tarfile.open(tmpname, "w", format=tarfile.PAX_FORMAT,
++                                   encoding="iso8859-1")
++                try:
++                    t = tarfile.TarInfo()
++                    t.name = "pax"  # non-ASCII
++                    t.uid = 1
++                    t.pax_headers = pax_headers
++                    tar.addfile(t)
++                finally:
++                    tar.close()
++
++                with open(tmpname, "rb") as f:
++                    data = f.read()
++                    self.assertIn(b"11 foo=bar\n", data)
++                    data = data.replace(b"11 foo=bar\n", replacement)
++
++                with open(tmpname, "wb") as f:
++                    f.truncate()
++                    f.write(data)
++
++                with self.assertRaisesRegex(tarfile.ReadError, r"file could not be opened successfully"):
++                    tarfile.open(tmpname, encoding="iso8859-1")
++
+ 
+ class WriteTestBase(TarTest):
+     # Put all write tests in here that are supposed to be tested
diff -Nru pypy3-7.3.11+dfsg/debian/patches/CVE-2024-6923-encode-email-header-newlines pypy3-7.3.11+dfsg/debian/patches/CVE-2024-6923-encode-email-header-newlines
--- pypy3-7.3.11+dfsg/debian/patches/CVE-2024-6923-encode-email-header-newlines	1969-12-31 20:00:00.000000000 -0400
+++ pypy3-7.3.11+dfsg/debian/patches/CVE-2024-6923-encode-email-header-newlines	2024-12-23 16:22:45.000000000 -0400
@@ -0,0 +1,275 @@
+From: =?utf-8?q?=C5=81ukasz_Langa?= <lukasz@langa.pl>
+Date: Wed, 4 Sep 2024 17:39:02 +0200
+Subject: gh-121650: Encode newlines in headers,
+ and verify headers are sound (GH-122233) (#122610)
+
+Per RFC 2047:
+
+> [...] these encoding schemes allow the
+> encoding of arbitrary octet values, mail readers that implement this
+> decoding should also ensure that display of the decoded data on the
+> recipient's terminal will not cause unwanted side-effects
+
+It seems that the "quoted-word" scheme is a valid way to include
+a newline character in a header value, just like we already allow
+undecodable bytes or control characters.
+They do need to be properly quoted when serialized to text, though.
+
+This should fail for custom fold() implementations that aren't careful
+about newlines.
+
+(cherry picked from commit 097633981879b3c9de9a1dd120d3aa585ecc2384)
+
+Co-authored-by: Petr Viktorin <encukou@gmail.com>
+Co-authored-by: Bas Bloemsaat <bas@bloemsaat.org>
+Co-authored-by: Serhiy Storchaka <storchaka@gmail.com>
+
+Origin: cpython, https://github.com/python/cpython/commit/f7be505d137a22528cb0fc004422c0081d5d90e6
+---
+ lib-python/3/email/_header_value_parser.py     | 12 +++--
+ lib-python/3/email/_policybase.py              |  8 ++++
+ lib-python/3/email/errors.py                   |  4 ++
+ lib-python/3/email/generator.py                | 13 +++++-
+ lib-python/3/test/test_email/test_generator.py | 62 ++++++++++++++++++++++++++
+ lib-python/3/test/test_email/test_policy.py    | 26 +++++++++++
+ 6 files changed, 121 insertions(+), 4 deletions(-)
+
+diff --git a/lib-python/3/email/_header_value_parser.py b/lib-python/3/email/_header_value_parser.py
+index 8a8fb8b..e394cfd 100644
+--- a/lib-python/3/email/_header_value_parser.py
++++ b/lib-python/3/email/_header_value_parser.py
+@@ -92,6 +92,8 @@ TOKEN_ENDS = TSPECIALS | WSP
+ ASPECIALS = TSPECIALS | set("*'%")
+ ATTRIBUTE_ENDS = ASPECIALS | WSP
+ EXTENDED_ATTRIBUTE_ENDS = ATTRIBUTE_ENDS - set('%')
++NLSET = {'\n', '\r'}
++SPECIALSNL = SPECIALS | NLSET
+ 
+ def quote_string(value):
+     return '"'+str(value).replace('\\', '\\\\').replace('"', r'\"')+'"'
+@@ -2778,9 +2780,13 @@ def _refold_parse_tree(parse_tree, *, policy):
+             wrap_as_ew_blocked -= 1
+             continue
+         tstr = str(part)
+-        if part.token_type == 'ptext' and set(tstr) & SPECIALS:
+-            # Encode if tstr contains special characters.
+-            want_encoding = True
++        if not want_encoding:
++            if part.token_type == 'ptext':
++                # Encode if tstr contains special characters.
++                want_encoding = not SPECIALSNL.isdisjoint(tstr)
++            else:
++                # Encode if tstr contains newlines.
++                want_encoding = not NLSET.isdisjoint(tstr)
+         try:
+             tstr.encode(encoding)
+             charset = encoding
+diff --git a/lib-python/3/email/_policybase.py b/lib-python/3/email/_policybase.py
+index c9cbadd..d1f4821 100644
+--- a/lib-python/3/email/_policybase.py
++++ b/lib-python/3/email/_policybase.py
+@@ -157,6 +157,13 @@ class Policy(_PolicyBase, metaclass=abc.ABCMeta):
+     message_factory     -- the class to use to create new message objects.
+                            If the value is None, the default is Message.
+ 
++    verify_generated_headers
++                        -- if true, the generator verifies that each header
++                           they are properly folded, so that a parser won't
++                           treat it as multiple headers, start-of-body, or
++                           part of another header.
++                           This is a check against custom Header & fold()
++                           implementations.
+     """
+ 
+     raise_on_defect = False
+@@ -165,6 +172,7 @@ class Policy(_PolicyBase, metaclass=abc.ABCMeta):
+     max_line_length = 78
+     mangle_from_ = False
+     message_factory = None
++    verify_generated_headers = True
+ 
+     def handle_defect(self, obj, defect):
+         """Based on policy, either raise defect or call register_defect.
+diff --git a/lib-python/3/email/errors.py b/lib-python/3/email/errors.py
+index d28a680..1a0d5c6 100644
+--- a/lib-python/3/email/errors.py
++++ b/lib-python/3/email/errors.py
+@@ -29,6 +29,10 @@ class CharsetError(MessageError):
+     """An illegal charset was given."""
+ 
+ 
++class HeaderWriteError(MessageError):
++    """Error while writing headers."""
++
++
+ # These are parsing defects which the parser was able to work around.
+ class MessageDefect(ValueError):
+     """Base class for a message defect."""
+diff --git a/lib-python/3/email/generator.py b/lib-python/3/email/generator.py
+index c9b1216..89224ae 100644
+--- a/lib-python/3/email/generator.py
++++ b/lib-python/3/email/generator.py
+@@ -14,12 +14,14 @@ import random
+ from copy import deepcopy
+ from io import StringIO, BytesIO
+ from email.utils import _has_surrogates
++from email.errors import HeaderWriteError
+ 
+ UNDERSCORE = '_'
+ NL = '\n'  # XXX: no longer used by the code below.
+ 
+ NLCRE = re.compile(r'\r\n|\r|\n')
+ fcre = re.compile(r'^From ', re.MULTILINE)
++NEWLINE_WITHOUT_FWSP = re.compile(r'\r\n[^ \t]|\r[^ \n\t]|\n[^ \t]')
+ 
+ 
+ 
+@@ -223,7 +225,16 @@ class Generator:
+ 
+     def _write_headers(self, msg):
+         for h, v in msg.raw_items():
+-            self.write(self.policy.fold(h, v))
++            folded = self.policy.fold(h, v)
++            if self.policy.verify_generated_headers:
++                linesep = self.policy.linesep
++                if not folded.endswith(self.policy.linesep):
++                    raise HeaderWriteError(
++                        f'folded header does not end with {linesep!r}: {folded!r}')
++                if NEWLINE_WITHOUT_FWSP.search(folded.removesuffix(linesep)):
++                    raise HeaderWriteError(
++                        f'folded header contains newline: {folded!r}')
++            self.write(folded)
+         # A blank line always separates headers from body
+         self.write(self._NL)
+ 
+diff --git a/lib-python/3/test/test_email/test_generator.py b/lib-python/3/test/test_email/test_generator.py
+index 89e7ede..d29400f 100644
+--- a/lib-python/3/test/test_email/test_generator.py
++++ b/lib-python/3/test/test_email/test_generator.py
+@@ -6,6 +6,7 @@ from email.message import EmailMessage
+ from email.generator import Generator, BytesGenerator
+ from email.headerregistry import Address
+ from email import policy
++import email.errors
+ from test.test_email import TestEmailBase, parameterize
+ 
+ 
+@@ -216,6 +217,44 @@ class TestGeneratorBase:
+         g.flatten(msg)
+         self.assertEqual(s.getvalue(), self.typ(expected))
+ 
++    def test_keep_encoded_newlines(self):
++        msg = self.msgmaker(self.typ(textwrap.dedent("""\
++            To: nobody
++            Subject: Bad subject=?UTF-8?Q?=0A?=Bcc: injection@example.com
++
++            None
++            """)))
++        expected = textwrap.dedent("""\
++            To: nobody
++            Subject: Bad subject=?UTF-8?Q?=0A?=Bcc: injection@example.com
++
++            None
++            """)
++        s = self.ioclass()
++        g = self.genclass(s, policy=self.policy.clone(max_line_length=80))
++        g.flatten(msg)
++        self.assertEqual(s.getvalue(), self.typ(expected))
++
++    def test_keep_long_encoded_newlines(self):
++        msg = self.msgmaker(self.typ(textwrap.dedent("""\
++            To: nobody
++            Subject: Bad subject=?UTF-8?Q?=0A?=Bcc: injection@example.com
++
++            None
++            """)))
++        expected = textwrap.dedent("""\
++            To: nobody
++            Subject: Bad subject
++             =?utf-8?q?=0A?=Bcc:
++             injection@example.com
++
++            None
++            """)
++        s = self.ioclass()
++        g = self.genclass(s, policy=self.policy.clone(max_line_length=30))
++        g.flatten(msg)
++        self.assertEqual(s.getvalue(), self.typ(expected))
++
+ 
+ class TestGenerator(TestGeneratorBase, TestEmailBase):
+ 
+@@ -224,6 +263,29 @@ class TestGenerator(TestGeneratorBase, TestEmailBase):
+     ioclass = io.StringIO
+     typ = str
+ 
++    def test_verify_generated_headers(self):
++        """gh-121650: by default the generator prevents header injection"""
++        class LiteralHeader(str):
++            name = 'Header'
++            def fold(self, **kwargs):
++                return self
++
++        for text in (
++            'Value\r\nBad Injection\r\n',
++            'NoNewLine'
++        ):
++            with self.subTest(text=text):
++                message = message_from_string(
++                    "Header: Value\r\n\r\nBody",
++                    policy=self.policy,
++                )
++
++                del message['Header']
++                message['Header'] = LiteralHeader(text)
++
++                with self.assertRaises(email.errors.HeaderWriteError):
++                    message.as_string()
++
+ 
+ class TestBytesGenerator(TestGeneratorBase, TestEmailBase):
+ 
+diff --git a/lib-python/3/test/test_email/test_policy.py b/lib-python/3/test/test_email/test_policy.py
+index e87c275..ff1ddf7 100644
+--- a/lib-python/3/test/test_email/test_policy.py
++++ b/lib-python/3/test/test_email/test_policy.py
+@@ -26,6 +26,7 @@ class PolicyAPITests(unittest.TestCase):
+         'raise_on_defect':          False,
+         'mangle_from_':             True,
+         'message_factory':          None,
++        'verify_generated_headers': True,
+         }
+     # These default values are the ones set on email.policy.default.
+     # If any of these defaults change, the docs must be updated.
+@@ -277,6 +278,31 @@ class PolicyAPITests(unittest.TestCase):
+                 with self.assertRaises(email.errors.HeaderParseError):
+                     policy.fold("Subject", subject)
+ 
++    def test_verify_generated_headers(self):
++        """Turning protection off allows header injection"""
++        policy = email.policy.default.clone(verify_generated_headers=False)
++        for text in (
++            'Header: Value\r\nBad: Injection\r\n',
++            'Header: NoNewLine'
++        ):
++            with self.subTest(text=text):
++                message = email.message_from_string(
++                    "Header: Value\r\n\r\nBody",
++                    policy=policy,
++                )
++                class LiteralHeader(str):
++                    name = 'Header'
++                    def fold(self, **kwargs):
++                        return self
++
++                del message['Header']
++                message['Header'] = LiteralHeader(text)
++
++                self.assertEqual(
++                    message.as_string(),
++                    f"{text}\nBody",
++                )
++
+     # XXX: Need subclassing tests.
+     # For adding subclassed objects, make sure the usual rules apply (subclass
+     # wins), but that the order still works (right overrides left).
diff -Nru pypy3-7.3.11+dfsg/debian/patches/CVE-2024-7592-http-cookie-quadratic-complexity pypy3-7.3.11+dfsg/debian/patches/CVE-2024-7592-http-cookie-quadratic-complexity
--- pypy3-7.3.11+dfsg/debian/patches/CVE-2024-7592-http-cookie-quadratic-complexity	1969-12-31 20:00:00.000000000 -0400
+++ pypy3-7.3.11+dfsg/debian/patches/CVE-2024-7592-http-cookie-quadratic-complexity	2024-12-23 16:22:45.000000000 -0400
@@ -0,0 +1,125 @@
+From: "Miss Islington (bot)"
+ <31488909+miss-islington@users.noreply.github.com>
+Date: Wed, 4 Sep 2024 17:49:40 +0200
+Subject: gh-123067: Fix quadratic complexity in parsing "-quoted cookie
+ values with backslashes (GH-123075) (#123107)
+
+This fixes CVE-2024-7592.
+(cherry picked from commit 44e458357fca05ca0ae2658d62c8c595b048b5ef)
+
+Co-authored-by: Serhiy Storchaka <storchaka@gmail.com>
+
+Origin: cpython, https://github.com/python/cpython/commit/d662e2db2605515a767f88ad48096b8ac623c774
+---
+ lib-python/3/http/cookies.py           | 34 +++++++-----------------------
+ lib-python/3/test/test_http_cookies.py | 38 ++++++++++++++++++++++++++++++++++
+ 2 files changed, 46 insertions(+), 26 deletions(-)
+
+diff --git a/lib-python/3/http/cookies.py b/lib-python/3/http/cookies.py
+index 35ac2dc..2c1f021 100644
+--- a/lib-python/3/http/cookies.py
++++ b/lib-python/3/http/cookies.py
+@@ -184,8 +184,13 @@ def _quote(str):
+         return '"' + str.translate(_Translator) + '"'
+ 
+ 
+-_OctalPatt = re.compile(r"\\[0-3][0-7][0-7]")
+-_QuotePatt = re.compile(r"[\\].")
++_unquote_sub = re.compile(r'\\(?:([0-3][0-7][0-7])|(.))').sub
++
++def _unquote_replace(m):
++    if m[1]:
++        return chr(int(m[1], 8))
++    else:
++        return m[2]
+ 
+ def _unquote(str):
+     # If there aren't any doublequotes,
+@@ -205,30 +210,7 @@ def _unquote(str):
+     #    \012 --> \n
+     #    \"   --> "
+     #
+-    i = 0
+-    n = len(str)
+-    res = []
+-    while 0 <= i < n:
+-        o_match = _OctalPatt.search(str, i)
+-        q_match = _QuotePatt.search(str, i)
+-        if not o_match and not q_match:              # Neither matched
+-            res.append(str[i:])
+-            break
+-        # else:
+-        j = k = -1
+-        if o_match:
+-            j = o_match.start(0)
+-        if q_match:
+-            k = q_match.start(0)
+-        if q_match and (not o_match or k < j):     # QuotePatt matched
+-            res.append(str[i:k])
+-            res.append(str[k+1])
+-            i = k + 2
+-        else:                                      # OctalPatt matched
+-            res.append(str[i:j])
+-            res.append(chr(int(str[j+1:j+4], 8)))
+-            i = j + 4
+-    return _nulljoin(res)
++    return _unquote_sub(_unquote_replace, str)
+ 
+ # The _getdate() routine is used to set the expiration time in the cookie's HTTP
+ # header.  By default, _getdate() returns the current time in the appropriate
+diff --git a/lib-python/3/test/test_http_cookies.py b/lib-python/3/test/test_http_cookies.py
+index 6072c7e..644e75c 100644
+--- a/lib-python/3/test/test_http_cookies.py
++++ b/lib-python/3/test/test_http_cookies.py
+@@ -5,6 +5,7 @@ from test.support import run_unittest, run_doctest
+ import unittest
+ from http import cookies
+ import pickle
++from test import support
+ 
+ 
+ class CookieTests(unittest.TestCase):
+@@ -58,6 +59,43 @@ class CookieTests(unittest.TestCase):
+             for k, v in sorted(case['dict'].items()):
+                 self.assertEqual(C[k].value, v)
+ 
++    def test_unquote(self):
++        cases = [
++            (r'a="b=\""', 'b="'),
++            (r'a="b=\\"', 'b=\\'),
++            (r'a="b=\="', 'b=='),
++            (r'a="b=\n"', 'b=n'),
++            (r'a="b=\042"', 'b="'),
++            (r'a="b=\134"', 'b=\\'),
++            (r'a="b=\377"', 'b=\xff'),
++            (r'a="b=\400"', 'b=400'),
++            (r'a="b=\42"', 'b=42'),
++            (r'a="b=\\042"', 'b=\\042'),
++            (r'a="b=\\134"', 'b=\\134'),
++            (r'a="b=\\\""', 'b=\\"'),
++            (r'a="b=\\\042"', 'b=\\"'),
++            (r'a="b=\134\""', 'b=\\"'),
++            (r'a="b=\134\042"', 'b=\\"'),
++        ]
++        for encoded, decoded in cases:
++            with self.subTest(encoded):
++                C = cookies.SimpleCookie()
++                C.load(encoded)
++                self.assertEqual(C['a'].value, decoded)
++
++    @support.requires_resource('cpu')
++    def test_unquote_large(self):
++        n = 10**6
++        for encoded in r'\\', r'\134':
++            with self.subTest(encoded):
++                data = 'a="b=' + encoded*n + ';"'
++                C = cookies.SimpleCookie()
++                C.load(data)
++                value = C['a'].value
++                self.assertEqual(value[:3], 'b=\\')
++                self.assertEqual(value[-2:], '\\;')
++                self.assertEqual(len(value), n + 3)
++
+     def test_load(self):
+         C = cookies.SimpleCookie()
+         C.load('Customer="WILE_E_COYOTE"; Version=1; Path=/acme')
diff -Nru pypy3-7.3.11+dfsg/debian/patches/CVE-2024-8088-zipfile-loop-dos pypy3-7.3.11+dfsg/debian/patches/CVE-2024-8088-zipfile-loop-dos
--- pypy3-7.3.11+dfsg/debian/patches/CVE-2024-8088-zipfile-loop-dos	1969-12-31 20:00:00.000000000 -0400
+++ pypy3-7.3.11+dfsg/debian/patches/CVE-2024-8088-zipfile-loop-dos	2024-12-23 16:22:45.000000000 -0400
@@ -0,0 +1,134 @@
+From: "Jason R. Coombs" <jaraco@jaraco.com>
+Date: Wed, 4 Sep 2024 11:46:48 -0400
+Subject: gh-123270: Replaced SanitizedNames with a more surgical fix.
+ (GH-123354) (#123432)
+
+Applies changes from zipp 3.20.1 and jaraco/zippGH-124
+(cherry picked from commit 2231286d78d328c2f575e0b05b16fe447d1656d6)
+(cherry picked from commit 17b77bb41409259bad1cd6c74761c18b6ab1e860)
+
+Co-authored-by: Jason R. Coombs <jaraco@jaraco.com>
+
+Origin: cpython, https://github.com/python/cpython/commit/962055268ed4f2ca1d717bfc8b6385de50a23ab7
+---
+ lib-python/3/test/test_zipfile.py | 77 +++++++++++++++++++++++++++++++++++++++
+ lib-python/3/zipfile.py           |  9 ++++-
+ 2 files changed, 84 insertions(+), 2 deletions(-)
+
+diff --git a/lib-python/3/test/test_zipfile.py b/lib-python/3/test/test_zipfile.py
+index f09c82c..26cf5fa 100644
+--- a/lib-python/3/test/test_zipfile.py
++++ b/lib-python/3/test/test_zipfile.py
+@@ -3056,6 +3056,83 @@ class TestPath(unittest.TestCase):
+         data = ['/'.join(string.ascii_lowercase + str(n)) for n in range(10000)]
+         zipfile.CompleteDirs._implied_dirs(data)
+ 
++    def test_malformed_paths(self):
++        """
++        Path should handle malformed paths gracefully.
++
++        Paths with leading slashes are not visible.
++
++        Paths with dots are treated like regular files.
++        """
++        data = io.BytesIO()
++        zf = zipfile.ZipFile(data, "w")
++        zf.writestr("/one-slash.txt", b"content")
++        zf.writestr("//two-slash.txt", b"content")
++        zf.writestr("../parent.txt", b"content")
++        zf.filename = ''
++        root = zipfile.Path(zf)
++        assert list(map(str, root.iterdir())) == ['../']
++        assert root.joinpath('..').joinpath('parent.txt').read_bytes() == b'content'
++
++    def test_unsupported_names(self):
++        """
++        Path segments with special characters are readable.
++
++        On some platforms or file systems, characters like
++        ``:`` and ``?`` are not allowed, but they are valid
++        in the zip file.
++        """
++        data = io.BytesIO()
++        zf = zipfile.ZipFile(data, "w")
++        zf.writestr("path?", b"content")
++        zf.writestr("V: NMS.flac", b"fLaC...")
++        zf.filename = ''
++        root = zipfile.Path(zf)
++        contents = root.iterdir()
++        assert next(contents).name == 'path?'
++        assert next(contents).name == 'V: NMS.flac'
++        assert root.joinpath('V: NMS.flac').read_bytes() == b"fLaC..."
++
++    def test_backslash_not_separator(self):
++        """
++        In a zip file, backslashes are not separators.
++        """
++        data = io.BytesIO()
++        zf = zipfile.ZipFile(data, "w")
++        zf.writestr(DirtyZipInfo.for_name("foo\\bar", zf), b"content")
++        zf.filename = ''
++        root = zipfile.Path(zf)
++        (first,) = root.iterdir()
++        assert not first.is_dir()
++        assert first.name == 'foo\\bar'
++
++
++class DirtyZipInfo(zipfile.ZipInfo):
++    """
++    Bypass name sanitization.
++    """
++
++    def __init__(self, filename, *args, **kwargs):
++        super().__init__(filename, *args, **kwargs)
++        self.filename = filename
++
++    @classmethod
++    def for_name(cls, name, archive):
++        """
++        Construct the same way that ZipFile.writestr does.
++
++        TODO: extract this functionality and re-use
++        """
++        self = cls(filename=name, date_time=time.localtime(time.time())[:6])
++        self.compress_type = archive.compression
++        self.compress_level = archive.compresslevel
++        if self.filename.endswith('/'):  # pragma: no cover
++            self.external_attr = 0o40775 << 16  # drwxrwxr-x
++            self.external_attr |= 0x10  # MS-DOS directory flag
++        else:
++            self.external_attr = 0o600 << 16  # ?rw-------
++        return self
++
+ 
+ if __name__ == "__main__":
+     unittest.main()
+diff --git a/lib-python/3/zipfile.py b/lib-python/3/zipfile.py
+index 95f95ee..68d643d 100644
+--- a/lib-python/3/zipfile.py
++++ b/lib-python/3/zipfile.py
+@@ -2146,7 +2146,7 @@ def _parents(path):
+ def _ancestry(path):
+     """
+     Given a path with elements separated by
+-    posixpath.sep, generate all elements of that path
++    posixpath.sep, generate all elements of that path.
+ 
+     >>> list(_ancestry('b/d'))
+     ['b/d', 'b']
+@@ -2158,9 +2158,14 @@ def _ancestry(path):
+     ['b']
+     >>> list(_ancestry(''))
+     []
++
++    Multiple separators are treated like a single.
++
++    >>> list(_ancestry('//b//d///f//'))
++    ['//b//d///f', '//b//d', '//b']
+     """
+     path = path.rstrip(posixpath.sep)
+-    while path and path != posixpath.sep:
++    while path.rstrip(posixpath.sep):
+         yield path
+         path, tail = posixpath.split(path)
+ 
diff -Nru pypy3-7.3.11+dfsg/debian/patches/CVE-2024-9287-venv-activation-templates pypy3-7.3.11+dfsg/debian/patches/CVE-2024-9287-venv-activation-templates
--- pypy3-7.3.11+dfsg/debian/patches/CVE-2024-9287-venv-activation-templates	1969-12-31 20:00:00.000000000 -0400
+++ pypy3-7.3.11+dfsg/debian/patches/CVE-2024-9287-venv-activation-templates	2024-12-23 16:22:45.000000000 -0400
@@ -0,0 +1,289 @@
+From: Victor Stinner <vstinner@python.org>
+Date: Mon, 4 Nov 2024 16:16:17 +0100
+Subject: gh-124651: Quote template strings in `venv` activation scripts
+ (GH-124712) (GH-126185) (GH-126269) (GH-126300)
+
+(cherry picked from commit ae961ae94bf19c8f8c7fbea3d1c25cc55ce8ae97)
+
+Origin: https://github.com/python/cpython/pull/126300
+---
+ lib-python/3/test/test_venv.py                | 81 +++++++++++++++++++++++++++
+ lib-python/3/venv/__init__.py                 | 42 ++++++++++++--
+ lib-python/3/venv/scripts/common/activate     |  6 +-
+ lib-python/3/venv/scripts/nt/activate.bat     |  4 +-
+ lib-python/3/venv/scripts/posix/activate.csh  |  6 +-
+ lib-python/3/venv/scripts/posix/activate.fish |  6 +-
+ 6 files changed, 129 insertions(+), 16 deletions(-)
+
+diff --git a/lib-python/3/test/test_venv.py b/lib-python/3/test/test_venv.py
+index 30c608e..5f7f2b2 100644
+--- a/lib-python/3/test/test_venv.py
++++ b/lib-python/3/test/test_venv.py
+@@ -14,6 +14,7 @@ import struct
+ import subprocess
+ import sys
+ import tempfile
++import shlex
+ from test.support import (captured_stdout, captured_stderr, requires_zlib,
+                           can_symlink, EnvironmentVarGuard, rmtree,
+                           import_module,
+@@ -85,6 +86,10 @@ class BaseTest(unittest.TestCase):
+             result = f.read()
+         return result
+ 
++    def assertEndsWith(self, string, tail):
++        if not string.endswith(tail):
++            self.fail(f"String {string!r} does not end with {tail!r}")
++
+ class BasicTest(BaseTest):
+     """Test venv module functionality."""
+ 
+@@ -342,6 +347,82 @@ class BasicTest(BaseTest):
+             'import sys; print(sys.executable)'])
+         self.assertEqual(out.strip(), envpy.encode())
+ 
++    # gh-124651: test quoted strings
++    @unittest.skipIf(os.name == 'nt', 'contains invalid characters on Windows')
++    def test_special_chars_bash(self):
++        """
++        Test that the template strings are quoted properly (bash)
++        """
++        rmtree(self.env_dir)
++        bash = shutil.which('bash')
++        if bash is None:
++            self.skipTest('bash required for this test')
++        env_name = '"\';&&$e|\'"'
++        env_dir = os.path.join(os.path.realpath(self.env_dir), env_name)
++        builder = venv.EnvBuilder(clear=True)
++        builder.create(env_dir)
++        activate = os.path.join(env_dir, self.bindir, 'activate')
++        test_script = os.path.join(self.env_dir, 'test_special_chars.sh')
++        with open(test_script, "w") as f:
++            f.write(f'source {shlex.quote(activate)}\n'
++                    'python -c \'import sys; print(sys.executable)\'\n'
++                    'python -c \'import os; print(os.environ["VIRTUAL_ENV"])\'\n'
++                    'deactivate\n')
++        out, err = check_output([bash, test_script])
++        lines = out.splitlines()
++        self.assertTrue(env_name.encode() in lines[0])
++        self.assertEndsWith(lines[1], env_name.encode())
++
++    # gh-124651: test quoted strings
++    @unittest.skipIf(os.name == 'nt', 'contains invalid characters on Windows')
++    def test_special_chars_csh(self):
++        """
++        Test that the template strings are quoted properly (csh)
++        """
++        rmtree(self.env_dir)
++        csh = shutil.which('tcsh') or shutil.which('csh')
++        if csh is None:
++            self.skipTest('csh required for this test')
++        env_name = '"\';&&$e|\'"'
++        env_dir = os.path.join(os.path.realpath(self.env_dir), env_name)
++        builder = venv.EnvBuilder(clear=True)
++        builder.create(env_dir)
++        activate = os.path.join(env_dir, self.bindir, 'activate.csh')
++        test_script = os.path.join(self.env_dir, 'test_special_chars.csh')
++        with open(test_script, "w") as f:
++            f.write(f'source {shlex.quote(activate)}\n'
++                    'python -c \'import sys; print(sys.executable)\'\n'
++                    'python -c \'import os; print(os.environ["VIRTUAL_ENV"])\'\n'
++                    'deactivate\n')
++        out, err = check_output([csh, test_script])
++        lines = out.splitlines()
++        self.assertTrue(env_name.encode() in lines[0])
++        self.assertEndsWith(lines[1], env_name.encode())
++
++    # gh-124651: test quoted strings on Windows
++    @unittest.skipUnless(os.name == 'nt', 'only relevant on Windows')
++    def test_special_chars_windows(self):
++        """
++        Test that the template strings are quoted properly on Windows
++        """
++        rmtree(self.env_dir)
++        env_name = "'&&^$e"
++        env_dir = os.path.join(os.path.realpath(self.env_dir), env_name)
++        builder = venv.EnvBuilder(clear=True)
++        builder.create(env_dir)
++        activate = os.path.join(env_dir, self.bindir, 'activate.bat')
++        test_batch = os.path.join(self.env_dir, 'test_special_chars.bat')
++        with open(test_batch, "w") as f:
++            f.write('@echo off\n'
++                    f'"{activate}" & '
++                    f'{self.exe} -c "import sys; print(sys.executable)" & '
++                    f'{self.exe} -c "import os; print(os.environ[\'VIRTUAL_ENV\'])" & '
++                    'deactivate')
++        out, err = check_output([test_batch])
++        lines = out.splitlines()
++        self.assertTrue(env_name.encode() in lines[0])
++        self.assertEndsWith(lines[1], env_name.encode())
++
+     @unittest.skipUnless(os.name == 'nt', 'only relevant on Windows')
+     def test_unicode_in_batch_file(self):
+         """
+diff --git a/lib-python/3/venv/__init__.py b/lib-python/3/venv/__init__.py
+index 83cbf73..7128da3 100644
+--- a/lib-python/3/venv/__init__.py
++++ b/lib-python/3/venv/__init__.py
+@@ -11,6 +11,7 @@ import subprocess
+ import sys
+ import sysconfig
+ import types
++import shlex
+ 
+ 
+ CORE_VENV_DEPS = ('pip', 'setuptools')
+@@ -405,11 +406,41 @@ Failing command: {}
+         :param context: The information for the environment creation request
+                         being processed.
+         """
+-        text = text.replace('__VENV_DIR__', context.env_dir)
+-        text = text.replace('__VENV_NAME__', context.env_name)
+-        text = text.replace('__VENV_PROMPT__', context.prompt)
+-        text = text.replace('__VENV_BIN_NAME__', context.bin_name)
+-        text = text.replace('__VENV_PYTHON__', context.env_exe)
++        replacements = {
++            '__VENV_DIR__': context.env_dir,
++            '__VENV_NAME__': context.env_name,
++            '__VENV_PROMPT__': context.prompt,
++            '__VENV_BIN_NAME__': context.bin_name,
++            '__VENV_PYTHON__': context.env_exe,
++        }
++
++        def quote_ps1(s):
++            """
++            This should satisfy PowerShell quoting rules [1], unless the quoted
++            string is passed directly to Windows native commands [2].
++            [1]: https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_quoting_rules
++            [2]: https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_parsing#passing-arguments-that-contain-quote-characters
++            """
++            s = s.replace("'", "''")
++            return f"'{s}'"
++
++        def quote_bat(s):
++            return s
++
++        # gh-124651: need to quote the template strings properly
++        quote = shlex.quote
++        script_path = context.script_path
++        if script_path.endswith('.ps1'):
++            quote = quote_ps1
++        elif script_path.endswith('.bat'):
++            quote = quote_bat
++        else:
++            # fallbacks to POSIX shell compliant quote
++            quote = shlex.quote
++
++        replacements = {key: quote(s) for key, s in replacements.items()}
++        for key, quoted in replacements.items():
++            text = text.replace(key, quoted)
+         return text
+ 
+     def install_scripts(self, context, path):
+@@ -449,6 +480,7 @@ Failing command: {}
+                 with open(srcfile, 'rb') as f:
+                     data = f.read()
+                 if not srcfile.endswith(('.exe', '.pdb')):
++                    context.script_path = srcfile
+                     try:
+                         data = data.decode('utf-8')
+                         data = self.replace_variables(data, context)
+diff --git a/lib-python/3/venv/scripts/common/activate b/lib-python/3/venv/scripts/common/activate
+index 45af353..1d116ca 100644
+--- a/lib-python/3/venv/scripts/common/activate
++++ b/lib-python/3/venv/scripts/common/activate
+@@ -37,11 +37,11 @@ deactivate () {
+ # unset irrelevant variables
+ deactivate nondestructive
+ 
+-VIRTUAL_ENV="__VENV_DIR__"
++VIRTUAL_ENV=__VENV_DIR__
+ export VIRTUAL_ENV
+ 
+ _OLD_VIRTUAL_PATH="$PATH"
+-PATH="$VIRTUAL_ENV/__VENV_BIN_NAME__:$PATH"
++PATH="$VIRTUAL_ENV/"__VENV_BIN_NAME__":$PATH"
+ export PATH
+ 
+ # unset PYTHONHOME if set
+@@ -54,7 +54,7 @@ fi
+ 
+ if [ -z "${VIRTUAL_ENV_DISABLE_PROMPT:-}" ] ; then
+     _OLD_VIRTUAL_PS1="${PS1:-}"
+-    PS1="__VENV_PROMPT__${PS1:-}"
++    PS1=__VENV_PROMPT__"${PS1:-}"
+     export PS1
+ fi
+ 
+diff --git a/lib-python/3/venv/scripts/nt/activate.bat b/lib-python/3/venv/scripts/nt/activate.bat
+index f61413e..11210c7 100644
+--- a/lib-python/3/venv/scripts/nt/activate.bat
++++ b/lib-python/3/venv/scripts/nt/activate.bat
+@@ -8,7 +8,7 @@ if defined _OLD_CODEPAGE (
+     "%SystemRoot%\System32\chcp.com" 65001 > nul
+ )
+ 
+-set VIRTUAL_ENV=__VENV_DIR__
++set "VIRTUAL_ENV=__VENV_DIR__"
+ 
+ if not defined PROMPT set PROMPT=$P$G
+ 
+@@ -24,7 +24,7 @@ set PYTHONHOME=
+ if defined _OLD_VIRTUAL_PATH set PATH=%_OLD_VIRTUAL_PATH%
+ if not defined _OLD_VIRTUAL_PATH set _OLD_VIRTUAL_PATH=%PATH%
+ 
+-set PATH=%VIRTUAL_ENV%\__VENV_BIN_NAME__;%PATH%
++set "PATH=%VIRTUAL_ENV%\__VENV_BIN_NAME__;%PATH%"
+ 
+ :END
+ if defined _OLD_CODEPAGE (
+diff --git a/lib-python/3/venv/scripts/posix/activate.csh b/lib-python/3/venv/scripts/posix/activate.csh
+index 68a0dc7..5130113 100644
+--- a/lib-python/3/venv/scripts/posix/activate.csh
++++ b/lib-python/3/venv/scripts/posix/activate.csh
+@@ -8,16 +8,16 @@ alias deactivate 'test $?_OLD_VIRTUAL_PATH != 0 && setenv PATH "$_OLD_VIRTUAL_PA
+ # Unset irrelevant variables.
+ deactivate nondestructive
+ 
+-setenv VIRTUAL_ENV "__VENV_DIR__"
++setenv VIRTUAL_ENV __VENV_DIR__
+ 
+ set _OLD_VIRTUAL_PATH="$PATH"
+-setenv PATH "$VIRTUAL_ENV/__VENV_BIN_NAME__:$PATH"
++setenv PATH "$VIRTUAL_ENV/"__VENV_BIN_NAME__":$PATH"
+ 
+ 
+ set _OLD_VIRTUAL_PROMPT="$prompt"
+ 
+ if (! "$?VIRTUAL_ENV_DISABLE_PROMPT") then
+-    set prompt = "__VENV_PROMPT__$prompt"
++    set prompt = __VENV_PROMPT__"$prompt"
+ endif
+ 
+ alias pydoc python -m pydoc
+diff --git a/lib-python/3/venv/scripts/posix/activate.fish b/lib-python/3/venv/scripts/posix/activate.fish
+index 54b9ea5..62ab531 100644
+--- a/lib-python/3/venv/scripts/posix/activate.fish
++++ b/lib-python/3/venv/scripts/posix/activate.fish
+@@ -29,10 +29,10 @@ end
+ # Unset irrelevant variables.
+ deactivate nondestructive
+ 
+-set -gx VIRTUAL_ENV "__VENV_DIR__"
++set -gx VIRTUAL_ENV __VENV_DIR__
+ 
+ set -gx _OLD_VIRTUAL_PATH $PATH
+-set -gx PATH "$VIRTUAL_ENV/__VENV_BIN_NAME__" $PATH
++set -gx PATH "$VIRTUAL_ENV/"__VENV_BIN_NAME__ $PATH
+ 
+ # Unset PYTHONHOME if set.
+ if set -q PYTHONHOME
+@@ -52,7 +52,7 @@ if test -z "$VIRTUAL_ENV_DISABLE_PROMPT"
+         set -l old_status $status
+ 
+         # Output the venv prompt; color taken from the blue of the Python logo.
+-        printf "%s%s%s" (set_color 4B8BBE) "__VENV_PROMPT__" (set_color normal)
++        printf "%s%s%s" (set_color 4B8BBE) __VENV_PROMPT__ (set_color normal)
+ 
+         # Restore the return status of the previous command.
+         echo "exit $old_status" | .
diff -Nru pypy3-7.3.11+dfsg/debian/patches/series pypy3-7.3.11+dfsg/debian/patches/series
--- pypy3-7.3.11+dfsg/debian/patches/series	2024-05-01 20:39:38.000000000 -0400
+++ pypy3-7.3.11+dfsg/debian/patches/series	2024-12-23 16:22:45.000000000 -0400
@@ -26,3 +26,11 @@
 CVE-2023-40217-test-reliability.patch
 CVE-2023-6597-tempfile-symlink.patch
 CVE-2024-0450-zipfile-quoted-overlap.patch
+CVE-2023-27043-email-parseaddr
+CVE-2024-9287-venv-activation-templates
+CVE-2024-4032-private-ip-ranges
+CVE-2024-6232-tarfile-redos
+CVE-2024-8088-zipfile-loop-dos
+CVE-2024-6923-encode-email-header-newlines
+CVE-2024-7592-http-cookie-quadratic-complexity
+CVE-2024-11168-urllib-parse-bracketed-names
diff -Nru pypy3-7.3.11+dfsg/debian/rules pypy3-7.3.11+dfsg/debian/rules
--- pypy3-7.3.11+dfsg/debian/rules	2024-05-01 20:39:38.000000000 -0400
+++ pypy3-7.3.11+dfsg/debian/rules	2024-12-23 16:22:45.000000000 -0400
@@ -61,6 +61,10 @@
 override_dh_auto_install:
 	debian/scripts/gen-backend-versions.py
 
+override_dh_auto_clean:
+	sed 's/^@/#/' cpython27/Makefile.pre.in | $(MAKE) -C cpython27 -f - srcdir=. distclean
+	dh_auto_clean
+
 override_dh_fixperms-arch:
 	debian/scripts/cleanup-lib.sh pypy3-lib
 	find debian/pypy3-tk \( -name '*.pyc' -o -name '__pycache__' \) -delete

Reply to: