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

Bug#1091325: marked as done (bookworm-pu: package pypy3/7.3.11+dfsg-2+deb12u3)



Your message dated Sat, 11 Jan 2025 11:03:09 +0000
with message-id <E1tWZGn-009jaQ-BQ@coccia.debian.org>
and subject line Close 1091325
has caused the Debian Bug report #1091325,
regarding bookworm-pu: package pypy3/7.3.11+dfsg-2+deb12u3
to be marked as done.

This means that you claim that the problem has been dealt with.
If this is not the case it is now your responsibility to reopen the
Bug report if necessary, and/or fix the problem forthwith.

(NB: If you are a system administrator and have no idea what this
message is talking about, this may indicate a serious mail system
misconfiguration somewhere. Please contact owner@bugs.debian.org
immediately.)


-- 
1091325: https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=1091325
Debian Bug Tracking System
Contact owner@bugs.debian.org with problems
--- Begin Message ---
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

--- End Message ---
--- Begin Message ---
Version: 12.9
This update has been released as part of 12.9. Thank you for your contribution.

--- End Message ---

Reply to: