Script 'mail_helper' called by obssrc Hello community, here is the log from the commit of package python314 for openSUSE:Factory checked in at 2025-11-19 14:58:07 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Comparing /work/SRC/openSUSE:Factory/python314 (Old) and /work/SRC/openSUSE:Factory/.python314.new.2061 (New) ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "python314" Wed Nov 19 14:58:07 2025 rev:29 rq:1318505 version:3.14.0 Changes: -------- --- /work/SRC/openSUSE:Factory/python314/python314.changes 2025-10-17 17:26:08.774911851 +0200 +++ /work/SRC/openSUSE:Factory/.python314.new.2061/python314.changes 2025-11-19 15:02:10.209025736 +0100 @@ -1,0 +2,20 @@ +Thu Nov 13 17:13:03 UTC 2025 - Matej Cepl <[email protected]> + +- Add CVE-2025-6075-expandvars-perf-degrad.patch avoid simple + quadratic complexity vulnerabilities of os.path.expandvars() + (CVE-2025-6075, bsc#1252974). + +------------------------------------------------------------------- +Tue Nov 4 16:44:05 UTC 2025 - Matej Cepl <[email protected]> + +- Add CVE-2025-8291-consistency-zip64.patch which checks + consistency of the zip64 end of central directory record, and + preventing obfuscation of the payload, i.e., you scanning for + malicious content in a ZIP file with one ZIP parser (let's say + a Rust one) then unpack it in production with another (e.g., + the Python one) and get malicious content that the other parser + did not see (CVE-2025-8291, bsc#1251305) +- Remove subprocess-raise-timeout.patch, which seems irrelevant + now. + +------------------------------------------------------------------- Old: ---- subprocess-raise-timeout.patch New: ---- CVE-2025-6075-expandvars-perf-degrad.patch CVE-2025-8291-consistency-zip64.patch _scmsync.obsinfo build.specials.obscpio ----------(Old B)---------- Old: did not see (CVE-2025-8291, bsc#1251305) - Remove subprocess-raise-timeout.patch, which seems irrelevant now. ----------(Old E)---------- ----------(New B)---------- New: - Add CVE-2025-6075-expandvars-perf-degrad.patch avoid simple quadratic complexity vulnerabilities of os.path.expandvars() New: - Add CVE-2025-8291-consistency-zip64.patch which checks consistency of the zip64 end of central directory record, and ----------(New E)---------- ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Other differences: ------------------ ++++++ python314.spec ++++++ --- /var/tmp/diff_new_pack.mUowNH/_old 2025-11-19 15:02:13.269155031 +0100 +++ /var/tmp/diff_new_pack.mUowNH/_new 2025-11-19 15:02:13.273155200 +0100 @@ -203,8 +203,6 @@ Patch03: python-3.3.0b1-localpath.patch # replace DATE, TIME and COMPILER by fixed definitions to aid reproducible builds Patch04: python-3.3.0b1-fix_date_time_compiler.patch -# Raise timeout value for test_subprocess -Patch06: subprocess-raise-timeout.patch # PATCH-FEATURE-UPSTREAM bpo-31046_ensurepip_honours_prefix.patch bpo#31046 [email protected] # ensurepip should honour the value of $(prefix) Patch07: bpo-31046_ensurepip_honours_prefix.patch @@ -226,6 +224,12 @@ Patch44: gh138131-exclude-pycache-from-digest.patch # PATCH-FIX-OPENSUSE gh139257-Support-docutils-0.22.patch gh#python/cpython#139257 [email protected] Patch45: gh139257-Support-docutils-0.22.patch +# PATCH-FIX-UPSTREAM CVE-2025-8291-consistency-zip64.patch bsc#1251305 [email protected] +# Check consistency of the zip64 end of central directory record +Patch46: CVE-2025-8291-consistency-zip64.patch +# PATCH-FIX-UPSTREAM CVE-2025-6075-expandvars-perf-degrad.patch bsc#1252974 [email protected] +# Avoid potential quadratic complexity vulnerabilities in path modules +Patch47: CVE-2025-6075-expandvars-perf-degrad.patch #### Python 3.14 DEVELOPMENT PATCHES BuildRequires: autoconf-archive BuildRequires: automake ++++++ CVE-2025-6075-expandvars-perf-degrad.patch ++++++ >From 5c0bf5295a6a38ee7540e447bcdc4889d131e261 Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka <[email protected]> Date: Fri, 31 Oct 2025 15:49:51 +0200 Subject: [PATCH] [3.14] gh-136065: Fix quadratic complexity in os.path.expandvars() (GH-134952) (cherry picked from commit f029e8db626ddc6e3a3beea4eff511a71aaceb5c) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Serhiy Storchaka <[email protected]> Co-authored-by: Łukasz Langa <[email protected]> --- Lib/ntpath.py | 126 ++++++------------ Lib/posixpath.py | 43 +++--- Lib/test/test_genericpath.py | 21 ++- Lib/test/test_ntpath.py | 22 ++- ...-05-30-22-33-27.gh-issue-136065.bu337o.rst | 1 + 5 files changed, 96 insertions(+), 117 deletions(-) create mode 100644 Misc/NEWS.d/next/Security/2025-05-30-22-33-27.gh-issue-136065.bu337o.rst diff --git a/Lib/ntpath.py b/Lib/ntpath.py index 9cdc16480f9afe..01f060e70beed9 100644 --- a/Lib/ntpath.py +++ b/Lib/ntpath.py @@ -400,17 +400,23 @@ def expanduser(path): # XXX With COMMAND.COM you can use any characters in a variable name, # XXX except '^|<>='. +_varpattern = r"'[^']*'?|%(%|[^%]*%?)|\$(\$|[-\w]+|\{[^}]*\}?)" +_varsub = None +_varsubb = None + def expandvars(path): """Expand shell variables of the forms $var, ${var} and %var%. Unknown variables are left unchanged.""" path = os.fspath(path) + global _varsub, _varsubb if isinstance(path, bytes): if b'$' not in path and b'%' not in path: return path - import string - varchars = bytes(string.ascii_letters + string.digits + '_-', 'ascii') - quote = b'\'' + if not _varsubb: + import re + _varsubb = re.compile(_varpattern.encode(), re.ASCII).sub + sub = _varsubb percent = b'%' brace = b'{' rbrace = b'}' @@ -419,94 +425,44 @@ def expandvars(path): else: if '$' not in path and '%' not in path: return path - import string - varchars = string.ascii_letters + string.digits + '_-' - quote = '\'' + if not _varsub: + import re + _varsub = re.compile(_varpattern, re.ASCII).sub + sub = _varsub percent = '%' brace = '{' rbrace = '}' dollar = '$' environ = os.environ - res = path[:0] - index = 0 - pathlen = len(path) - while index < pathlen: - c = path[index:index+1] - if c == quote: # no expansion within single quotes - path = path[index + 1:] - pathlen = len(path) - try: - index = path.index(c) - res += c + path[:index + 1] - except ValueError: - res += c + path - index = pathlen - 1 - elif c == percent: # variable or '%' - if path[index + 1:index + 2] == percent: - res += c - index += 1 - else: - path = path[index+1:] - pathlen = len(path) - try: - index = path.index(percent) - except ValueError: - res += percent + path - index = pathlen - 1 - else: - var = path[:index] - try: - if environ is None: - value = os.fsencode(os.environ[os.fsdecode(var)]) - else: - value = environ[var] - except KeyError: - value = percent + var + percent - res += value - elif c == dollar: # variable or '$$' - if path[index + 1:index + 2] == dollar: - res += c - index += 1 - elif path[index + 1:index + 2] == brace: - path = path[index+2:] - pathlen = len(path) - try: - index = path.index(rbrace) - except ValueError: - res += dollar + brace + path - index = pathlen - 1 - else: - var = path[:index] - try: - if environ is None: - value = os.fsencode(os.environ[os.fsdecode(var)]) - else: - value = environ[var] - except KeyError: - value = dollar + brace + var + rbrace - res += value - else: - var = path[:0] - index += 1 - c = path[index:index + 1] - while c and c in varchars: - var += c - index += 1 - c = path[index:index + 1] - try: - if environ is None: - value = os.fsencode(os.environ[os.fsdecode(var)]) - else: - value = environ[var] - except KeyError: - value = dollar + var - res += value - if c: - index -= 1 + + def repl(m): + lastindex = m.lastindex + if lastindex is None: + return m[0] + name = m[lastindex] + if lastindex == 1: + if name == percent: + return name + if not name.endswith(percent): + return m[0] + name = name[:-1] else: - res += c - index += 1 - return res + if name == dollar: + return name + if name.startswith(brace): + if not name.endswith(rbrace): + return m[0] + name = name[1:-1] + + try: + if environ is None: + return os.fsencode(os.environ[os.fsdecode(name)]) + else: + return environ[name] + except KeyError: + return m[0] + + return sub(repl, path) # Normalize a path, e.g. A//B, A/./B and A/foo/../B all become A\B. diff --git a/Lib/posixpath.py b/Lib/posixpath.py index d38f3bd5872bcd..ad86cc06c017a0 100644 --- a/Lib/posixpath.py +++ b/Lib/posixpath.py @@ -284,42 +284,41 @@ def expanduser(path): # This expands the forms $variable and ${variable} only. # Non-existent variables are left unchanged. -_varprog = None -_varprogb = None +_varpattern = r'\$(\w+|\{[^}]*\}?)' +_varsub = None +_varsubb = None def expandvars(path): """Expand shell variables of form $var and ${var}. Unknown variables are left unchanged.""" path = os.fspath(path) - global _varprog, _varprogb + global _varsub, _varsubb if isinstance(path, bytes): if b'$' not in path: return path - if not _varprogb: + if not _varsubb: import re - _varprogb = re.compile(br'\$(\w+|\{[^}]*\})', re.ASCII) - search = _varprogb.search + _varsubb = re.compile(_varpattern.encode(), re.ASCII).sub + sub = _varsubb start = b'{' end = b'}' environ = getattr(os, 'environb', None) else: if '$' not in path: return path - if not _varprog: + if not _varsub: import re - _varprog = re.compile(r'\$(\w+|\{[^}]*\})', re.ASCII) - search = _varprog.search + _varsub = re.compile(_varpattern, re.ASCII).sub + sub = _varsub start = '{' end = '}' environ = os.environ - i = 0 - while True: - m = search(path, i) - if not m: - break - i, j = m.span(0) - name = m.group(1) - if name.startswith(start) and name.endswith(end): + + def repl(m): + name = m[1] + if name.startswith(start): + if not name.endswith(end): + return m[0] name = name[1:-1] try: if environ is None: @@ -327,13 +326,11 @@ def expandvars(path): else: value = environ[name] except KeyError: - i = j + return m[0] else: - tail = path[j:] - path = path[:i] + value - i = len(path) - path += tail - return path + return value + + return sub(repl, path) # Normalize a path, e.g. A//B, A/./B and A/foo/../B all become A/B. diff --git a/Lib/test/test_genericpath.py b/Lib/test/test_genericpath.py index df07af01fc7540..1a44cedcd360b1 100644 --- a/Lib/test/test_genericpath.py +++ b/Lib/test/test_genericpath.py @@ -7,9 +7,9 @@ import sys import unittest import warnings -from test.support import ( - is_apple, is_emscripten, os_helper, warnings_helper -) +from test import support +from test.support import os_helper +from test.support import warnings_helper from test.support.script_helper import assert_python_ok from test.support.os_helper import FakePath @@ -445,6 +445,19 @@ def check(value, expected): os.fsencode('$bar%s bar' % nonascii)) check(b'$spam}bar', os.fsencode('%s}bar' % nonascii)) + @support.requires_resource('cpu') + def test_expandvars_large(self): + expandvars = self.pathmodule.expandvars + with os_helper.EnvironmentVarGuard() as env: + env.clear() + env["A"] = "B" + n = 100_000 + self.assertEqual(expandvars('$A'*n), 'B'*n) + self.assertEqual(expandvars('${A}'*n), 'B'*n) + self.assertEqual(expandvars('$A!'*n), 'B!'*n) + self.assertEqual(expandvars('${A}A'*n), 'BA'*n) + self.assertEqual(expandvars('${'*10*n), '${'*10*n) + def test_abspath(self): self.assertIn("foo", self.pathmodule.abspath("foo")) with warnings.catch_warnings(): @@ -502,7 +515,7 @@ def test_nonascii_abspath(self): # directory (when the bytes name is used). and sys.platform not in { "win32", "emscripten", "wasi" - } and not is_apple + } and not support.is_apple ): name = os_helper.TESTFN_UNDECODABLE elif os_helper.TESTFN_NONASCII: diff --git a/Lib/test/test_ntpath.py b/Lib/test/test_ntpath.py index 03bfccf260b25d..9270f3257068d6 100644 --- a/Lib/test/test_ntpath.py +++ b/Lib/test/test_ntpath.py @@ -7,8 +7,7 @@ import unittest import warnings from test import support -from test.support import cpython_only, os_helper -from test.support import TestFailed +from test.support import os_helper from ntpath import ALLOW_MISSING from test.support.os_helper import FakePath from test import test_genericpath @@ -59,7 +58,7 @@ def tester(fn, wantResult): fn = fn.replace("\\", "\\\\") gotResult = eval(fn) if wantResult != gotResult and _norm(wantResult) != _norm(gotResult): - raise TestFailed("%s should return: %s but returned: %s" \ + raise support.TestFailed("%s should return: %s but returned: %s" \ %(str(fn), str(wantResult), str(gotResult))) # then with bytes @@ -75,7 +74,7 @@ def tester(fn, wantResult): warnings.simplefilter("ignore", DeprecationWarning) gotResult = eval(fn) if _norm(wantResult) != _norm(gotResult): - raise TestFailed("%s should return: %s but returned: %s" \ + raise support.TestFailed("%s should return: %s but returned: %s" \ %(str(fn), str(wantResult), repr(gotResult))) @@ -1022,6 +1021,19 @@ def check(value, expected): check('%spam%bar', '%sbar' % nonascii) check('%{}%bar'.format(nonascii), 'ham%sbar' % nonascii) + @support.requires_resource('cpu') + def test_expandvars_large(self): + expandvars = ntpath.expandvars + with os_helper.EnvironmentVarGuard() as env: + env.clear() + env["A"] = "B" + n = 100_000 + self.assertEqual(expandvars('%A%'*n), 'B'*n) + self.assertEqual(expandvars('%A%A'*n), 'BA'*n) + self.assertEqual(expandvars("''"*n + '%%'), "''"*n + '%') + self.assertEqual(expandvars("%%"*n), "%"*n) + self.assertEqual(expandvars("$$"*n), "$"*n) + def test_expanduser(self): tester('ntpath.expanduser("test")', 'test') @@ -1439,7 +1451,7 @@ def test_con_device(self): self.assertTrue(os.path.exists(r"\\.\CON")) @unittest.skipIf(sys.platform != 'win32', "Fast paths are only for win32") - @cpython_only + @support.cpython_only def test_fast_paths_in_use(self): # There are fast paths of these functions implemented in posixmodule.c. # Confirm that they are being used, and not the Python fallbacks in diff --git a/Misc/NEWS.d/next/Security/2025-05-30-22-33-27.gh-issue-136065.bu337o.rst b/Misc/NEWS.d/next/Security/2025-05-30-22-33-27.gh-issue-136065.bu337o.rst new file mode 100644 index 00000000000000..1d152bb5318380 --- /dev/null +++ b/Misc/NEWS.d/next/Security/2025-05-30-22-33-27.gh-issue-136065.bu337o.rst @@ -0,0 +1 @@ +Fix quadratic complexity in :func:`os.path.expandvars`. ++++++ CVE-2025-8291-consistency-zip64.patch ++++++ >From 5454f861e2b3c96fa1e6430dc952544670955f69 Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka <[email protected]> Date: Tue, 7 Oct 2025 20:15:26 +0300 Subject: [PATCH] gh-139700: Check consistency of the zip64 end of central directory record (GH-139702) Support records with "zip64 extensible data" if there are no bytes prepended to the ZIP file. (cherry picked from commit 162997bb70e067668c039700141770687bc8f267) Co-authored-by: Serhiy Storchaka <[email protected]> --- Lib/test/test_zipfile/test_core.py | 82 +++++++++- Lib/zipfile/__init__.py | 51 +++--- Misc/NEWS.d/next/Security/2025-10-07-19-31-34.gh-issue-139700.vNHU1O.rst | 3 3 files changed, 113 insertions(+), 23 deletions(-) create mode 100644 Misc/NEWS.d/next/Security/2025-10-07-19-31-34.gh-issue-139700.vNHU1O.rst Index: Python-3.14.0/Lib/test/test_zipfile/test_core.py =================================================================== --- Python-3.14.0.orig/Lib/test/test_zipfile/test_core.py 2025-11-12 01:03:54.947094045 +0100 +++ Python-3.14.0/Lib/test/test_zipfile/test_core.py 2025-11-12 01:04:11.766432381 +0100 @@ -898,6 +898,8 @@ self, file_size_64_set=False, file_size_extra=False, compress_size_64_set=False, compress_size_extra=False, header_offset_64_set=False, header_offset_extra=False, + extensible_data=b'', + end_of_central_dir_size=None, offset_to_end_of_central_dir=None, ): """Generate bytes sequence for a zip with (incomplete) zip64 data. @@ -951,6 +953,12 @@ central_dir_size = struct.pack('<Q', 58 + 8 * len(central_zip64_fields)) offset_to_central_dir = struct.pack('<Q', 50 + 8 * len(local_zip64_fields)) + if end_of_central_dir_size is None: + end_of_central_dir_size = 44 + len(extensible_data) + if offset_to_end_of_central_dir is None: + offset_to_end_of_central_dir = (108 + + 8 * len(local_zip64_fields) + + 8 * len(central_zip64_fields)) local_extra_length = struct.pack("<H", 4 + 8 * len(local_zip64_fields)) central_extra_length = struct.pack("<H", 4 + 8 * len(central_zip64_fields)) @@ -979,14 +987,17 @@ + filename + central_extra # Zip64 end of central directory - + b"PK\x06\x06,\x00\x00\x00\x00\x00\x00\x00-\x00-" - + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00" + + b"PK\x06\x06" + + struct.pack('<Q', end_of_central_dir_size) + + b"-\x00-\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00" + b"\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00" + central_dir_size + offset_to_central_dir + + extensible_data # Zip64 end of central directory locator - + b"PK\x06\x07\x00\x00\x00\x00l\x00\x00\x00\x00\x00\x00\x00\x01" - + b"\x00\x00\x00" + + b"PK\x06\x07\x00\x00\x00\x00" + + struct.pack('<Q', offset_to_end_of_central_dir) + + b"\x01\x00\x00\x00" # end of central directory + b"PK\x05\x06\x00\x00\x00\x00\x01\x00\x01\x00:\x00\x00\x002\x00" + b"\x00\x00\x00\x00" @@ -1017,6 +1028,7 @@ with self.assertRaises(zipfile.BadZipFile) as e: zipfile.ZipFile(io.BytesIO(missing_file_size_extra)) self.assertIn('file size', str(e.exception).lower()) + self.assertTrue(zipfile.is_zipfile(io.BytesIO(missing_file_size_extra))) # zip64 file size present, zip64 compress size present, one field in # extra, expecting two, equals missing compress size. @@ -1028,6 +1040,7 @@ with self.assertRaises(zipfile.BadZipFile) as e: zipfile.ZipFile(io.BytesIO(missing_compress_size_extra)) self.assertIn('compress size', str(e.exception).lower()) + self.assertTrue(zipfile.is_zipfile(io.BytesIO(missing_compress_size_extra))) # zip64 compress size present, no fields in extra, expecting one, # equals missing compress size. @@ -1037,6 +1050,7 @@ with self.assertRaises(zipfile.BadZipFile) as e: zipfile.ZipFile(io.BytesIO(missing_compress_size_extra)) self.assertIn('compress size', str(e.exception).lower()) + self.assertTrue(zipfile.is_zipfile(io.BytesIO(missing_compress_size_extra))) # zip64 file size present, zip64 compress size present, zip64 header # offset present, two fields in extra, expecting three, equals missing @@ -1051,6 +1065,7 @@ with self.assertRaises(zipfile.BadZipFile) as e: zipfile.ZipFile(io.BytesIO(missing_header_offset_extra)) self.assertIn('header offset', str(e.exception).lower()) + self.assertTrue(zipfile.is_zipfile(io.BytesIO(missing_header_offset_extra))) # zip64 compress size present, zip64 header offset present, one field # in extra, expecting two, equals missing header offset @@ -1063,6 +1078,7 @@ with self.assertRaises(zipfile.BadZipFile) as e: zipfile.ZipFile(io.BytesIO(missing_header_offset_extra)) self.assertIn('header offset', str(e.exception).lower()) + self.assertTrue(zipfile.is_zipfile(io.BytesIO(missing_header_offset_extra))) # zip64 file size present, zip64 header offset present, one field in # extra, expecting two, equals missing header offset @@ -1075,6 +1091,7 @@ with self.assertRaises(zipfile.BadZipFile) as e: zipfile.ZipFile(io.BytesIO(missing_header_offset_extra)) self.assertIn('header offset', str(e.exception).lower()) + self.assertTrue(zipfile.is_zipfile(io.BytesIO(missing_header_offset_extra))) # zip64 header offset present, no fields in extra, expecting one, # equals missing header offset @@ -1086,6 +1103,63 @@ with self.assertRaises(zipfile.BadZipFile) as e: zipfile.ZipFile(io.BytesIO(missing_header_offset_extra)) self.assertIn('header offset', str(e.exception).lower()) + self.assertTrue(zipfile.is_zipfile(io.BytesIO(missing_header_offset_extra))) + + def test_bad_zip64_end_of_central_dir(self): + zipdata = self.make_zip64_file(end_of_central_dir_size=0) + with self.assertRaisesRegex(zipfile.BadZipFile, 'Corrupt.*record'): + zipfile.ZipFile(io.BytesIO(zipdata)) + self.assertFalse(zipfile.is_zipfile(io.BytesIO(zipdata))) + + zipdata = self.make_zip64_file(end_of_central_dir_size=100) + with self.assertRaisesRegex(zipfile.BadZipFile, 'Corrupt.*record'): + zipfile.ZipFile(io.BytesIO(zipdata)) + self.assertFalse(zipfile.is_zipfile(io.BytesIO(zipdata))) + + zipdata = self.make_zip64_file(offset_to_end_of_central_dir=0) + with self.assertRaisesRegex(zipfile.BadZipFile, 'Corrupt.*record'): + zipfile.ZipFile(io.BytesIO(zipdata)) + self.assertFalse(zipfile.is_zipfile(io.BytesIO(zipdata))) + + zipdata = self.make_zip64_file(offset_to_end_of_central_dir=1000) + with self.assertRaisesRegex(zipfile.BadZipFile, 'Corrupt.*locator'): + zipfile.ZipFile(io.BytesIO(zipdata)) + self.assertFalse(zipfile.is_zipfile(io.BytesIO(zipdata))) + + def test_zip64_end_of_central_dir_record_not_found(self): + zipdata = self.make_zip64_file() + zipdata = zipdata.replace(b"PK\x06\x06", b'\x00'*4) + with self.assertRaisesRegex(zipfile.BadZipFile, 'record not found'): + zipfile.ZipFile(io.BytesIO(zipdata)) + self.assertFalse(zipfile.is_zipfile(io.BytesIO(zipdata))) + + zipdata = self.make_zip64_file( + extensible_data=b'\xca\xfe\x04\x00\x00\x00data') + zipdata = zipdata.replace(b"PK\x06\x06", b'\x00'*4) + with self.assertRaisesRegex(zipfile.BadZipFile, 'record not found'): + zipfile.ZipFile(io.BytesIO(zipdata)) + self.assertFalse(zipfile.is_zipfile(io.BytesIO(zipdata))) + + def test_zip64_extensible_data(self): + # These values are what is set in the make_zip64_file method. + expected_file_size = 8 + expected_compress_size = 8 + expected_header_offset = 0 + expected_content = b"test1234" + + zipdata = self.make_zip64_file( + extensible_data=b'\xca\xfe\x04\x00\x00\x00data') + with zipfile.ZipFile(io.BytesIO(zipdata)) as zf: + zinfo = zf.infolist()[0] + self.assertEqual(zinfo.file_size, expected_file_size) + self.assertEqual(zinfo.compress_size, expected_compress_size) + self.assertEqual(zinfo.header_offset, expected_header_offset) + self.assertEqual(zf.read(zinfo), expected_content) + self.assertTrue(zipfile.is_zipfile(io.BytesIO(zipdata))) + + with self.assertRaisesRegex(zipfile.BadZipFile, 'record not found'): + zipfile.ZipFile(io.BytesIO(b'prepended' + zipdata)) + self.assertFalse(zipfile.is_zipfile(io.BytesIO(b'prepended' + zipdata))) def test_generated_valid_zip64_extra(self): # These values are what is set in the make_zip64_file method. Index: Python-3.14.0/Lib/zipfile/__init__.py =================================================================== --- Python-3.14.0.orig/Lib/zipfile/__init__.py 2025-11-12 01:03:55.239347604 +0100 +++ Python-3.14.0/Lib/zipfile/__init__.py 2025-11-12 01:04:11.767105446 +0100 @@ -265,7 +265,7 @@ else: with open(filename, "rb") as fp: result = _check_zipfile(fp) - except OSError: + except (OSError, BadZipFile): pass return result @@ -275,9 +275,6 @@ # "concat" is zero, unless zip was concatenated to another file concat = endrec[_ECD_LOCATION] - size_cd - offset_cd - if endrec[_ECD_SIGNATURE] == stringEndArchive64: - # If Zip64 extension structures are present, account for them - concat -= (sizeEndCentDir64 + sizeEndCentDir64Locator) if debug > 2: inferred = concat + offset_cd @@ -289,16 +286,15 @@ """ Read the ZIP64 end-of-archive records and use that to update endrec """ - try: - fpin.seek(offset - sizeEndCentDir64Locator, 2) - except OSError: - # If the seek fails, the file is not large enough to contain a ZIP64 + offset -= sizeEndCentDir64Locator + if offset < 0: + # The file is not large enough to contain a ZIP64 # end-of-archive record, so just return the end record we were given. return endrec - + fpin.seek(offset) data = fpin.read(sizeEndCentDir64Locator) if len(data) != sizeEndCentDir64Locator: - return endrec + raise OSError("Unknown I/O error") sig, diskno, reloff, disks = struct.unpack(structEndArchive64Locator, data) if sig != stringEndArchive64Locator: return endrec @@ -306,16 +302,33 @@ if diskno != 0 or disks > 1: raise BadZipFile("zipfiles that span multiple disks are not supported") - # Assume no 'zip64 extensible data' - fpin.seek(offset - sizeEndCentDir64Locator - sizeEndCentDir64, 2) + offset -= sizeEndCentDir64 + if reloff > offset: + raise BadZipFile("Corrupt zip64 end of central directory locator") + # First, check the assumption that there is no prepended data. + fpin.seek(reloff) + extrasz = offset - reloff data = fpin.read(sizeEndCentDir64) if len(data) != sizeEndCentDir64: - return endrec + raise OSError("Unknown I/O error") + if not data.startswith(stringEndArchive64) and reloff != offset: + # Since we already have seen the Zip64 EOCD Locator, it's + # possible we got here because there is prepended data. + # Assume no 'zip64 extensible data' + fpin.seek(offset) + extrasz = 0 + data = fpin.read(sizeEndCentDir64) + if len(data) != sizeEndCentDir64: + raise OSError("Unknown I/O error") + if not data.startswith(stringEndArchive64): + raise BadZipFile("Zip64 end of central directory record not found") + sig, sz, create_version, read_version, disk_num, disk_dir, \ dircount, dircount2, dirsize, diroffset = \ struct.unpack(structEndArchive64, data) - if sig != stringEndArchive64: - return endrec + if (diroffset + dirsize != reloff or + sz + 12 != sizeEndCentDir64 + extrasz): + raise BadZipFile("Corrupt zip64 end of central directory record") # Update the original endrec using data from the ZIP64 record endrec[_ECD_SIGNATURE] = sig @@ -325,6 +338,7 @@ endrec[_ECD_ENTRIES_TOTAL] = dircount2 endrec[_ECD_SIZE] = dirsize endrec[_ECD_OFFSET] = diroffset + endrec[_ECD_LOCATION] = offset - extrasz return endrec @@ -358,7 +372,7 @@ endrec.append(filesize - sizeEndCentDir) # Try to read the "Zip64 end of central directory" structure - return _EndRecData64(fpin, -sizeEndCentDir, endrec) + return _EndRecData64(fpin, filesize - sizeEndCentDir, endrec) # Either this is not a ZIP file, or it is a ZIP file with an archive # comment. Search the end of the file for the "end of central directory" @@ -382,8 +396,7 @@ endrec.append(maxCommentStart + start) # Try to read the "Zip64 end of central directory" structure - return _EndRecData64(fpin, maxCommentStart + start - filesize, - endrec) + return _EndRecData64(fpin, maxCommentStart + start, endrec) # Unable to find a valid end of central directory structure return None @@ -2142,7 +2155,7 @@ " would require ZIP64 extensions") zip64endrec = struct.pack( structEndArchive64, stringEndArchive64, - 44, 45, 45, 0, 0, centDirCount, centDirCount, + sizeEndCentDir64 - 12, 45, 45, 0, 0, centDirCount, centDirCount, centDirSize, centDirOffset) self.fp.write(zip64endrec) Index: Python-3.14.0/Misc/NEWS.d/next/Security/2025-10-07-19-31-34.gh-issue-139700.vNHU1O.rst =================================================================== --- /dev/null 1970-01-01 00:00:00.000000000 +0000 +++ Python-3.14.0/Misc/NEWS.d/next/Security/2025-10-07-19-31-34.gh-issue-139700.vNHU1O.rst 2025-11-12 01:04:11.767493557 +0100 @@ -0,0 +1,3 @@ +Check consistency of the zip64 end of central directory record. Support +records with "zip64 extensible data" if there are no bytes prepended to the +ZIP file. ++++++ F00251-change-user-install-location.patch ++++++ --- /var/tmp/diff_new_pack.mUowNH/_old 2025-11-19 15:02:13.337157904 +0100 +++ /var/tmp/diff_new_pack.mUowNH/_new 2025-11-19 15:02:13.341158073 +0100 @@ -24,9 +24,9 @@ Co-authored-by: Michal Cyprian <[email protected]> Co-authored-by: Lumír Balhar <[email protected]> --- - Lib/sysconfig/__init__.py | 51 ++++++++++++++++++++++++++++++++++++++++++++- + Lib/sysconfig/__init__.py | 49 ++++++++++++++++++++++++++++++++++++++++++++- Lib/test/test_sysconfig.py | 17 +++++++++++++-- - 2 files changed, 65 insertions(+), 3 deletions(-) + 2 files changed, 63 insertions(+), 3 deletions(-) Index: Python-3.14.0/Lib/sysconfig/__init__.py =================================================================== ++++++ _scmsync.obsinfo ++++++ mtime: 1763502158 commit: a5bb2062897eab307408f4ab932f2270da844b16094e9ce72c21ac490e47f956 url: https://src.opensuse.org/python-interpreters/python314.git revision: a5bb2062897eab307408f4ab932f2270da844b16094e9ce72c21ac490e47f956 projectscmsync: https://src.opensuse.org/python-interpreters/_ObsPrj ++++++ build.specials.obscpio ++++++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/.gitignore new/.gitignore --- old/.gitignore 1970-01-01 01:00:00.000000000 +0100 +++ new/.gitignore 2025-11-18 22:43:11.000000000 +0100 @@ -0,0 +1,5 @@ +.osc +*.obscpio +_build.* +.pbuild +python314-*-build/ ++++++ fix-test-recursion-limit-15.6.patch ++++++ --- /var/tmp/diff_new_pack.mUowNH/_old 2025-11-19 15:02:13.541166524 +0100 +++ /var/tmp/diff_new_pack.mUowNH/_new 2025-11-19 15:02:13.545166692 +0100 @@ -2,10 +2,10 @@ Lib/test/test_compile.py | 5 +++++ 1 file changed, 5 insertions(+) -Index: Python-3.14.0b3/Lib/test/test_compile.py +Index: Python-3.14.0/Lib/test/test_compile.py =================================================================== ---- Python-3.14.0b3.orig/Lib/test/test_compile.py 2025-06-22 00:32:01.975698954 +0200 -+++ Python-3.14.0b3/Lib/test/test_compile.py 2025-06-22 00:32:07.740981155 +0200 +--- Python-3.14.0.orig/Lib/test/test_compile.py 2025-11-06 23:19:11.681015028 +0100 ++++ Python-3.14.0/Lib/test/test_compile.py 2025-11-06 23:22:47.971267371 +0100 @@ -24,6 +24,9 @@ from test.support.bytecode_helper import instructions_with_positions from test.support.os_helper import FakePath ++++++ python-3.3.0b1-fix_date_time_compiler.patch ++++++ --- /var/tmp/diff_new_pack.mUowNH/_old 2025-11-19 15:02:13.629170242 +0100 +++ /var/tmp/diff_new_pack.mUowNH/_new 2025-11-19 15:02:13.637170580 +0100 @@ -2,10 +2,10 @@ Makefile.pre.in | 5 +++++ 1 file changed, 5 insertions(+) -Index: Python-3.14.0rc1/Makefile.pre.in +Index: Python-3.14.0/Makefile.pre.in =================================================================== ---- Python-3.14.0rc1.orig/Makefile.pre.in 2025-07-22 18:42:44.000000000 +0200 -+++ Python-3.14.0rc1/Makefile.pre.in 2025-07-23 10:10:27.325708066 +0200 +--- Python-3.14.0.orig/Makefile.pre.in 2025-10-07 11:34:52.000000000 +0200 ++++ Python-3.14.0/Makefile.pre.in 2025-11-06 23:19:25.434995608 +0100 @@ -1910,6 +1910,11 @@ -DGITBRANCH="\"`LC_ALL=C $(GITBRANCH)`\"" \ -o $@ $(srcdir)/Modules/getbuildinfo.c
