Script 'mail_helper' called by obssrc
Hello community,

here is the log from the commit of package python313 for openSUSE:Factory 
checked in at 2026-10-01 16:41:18
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/python313 (Old)
 and      /work/SRC/openSUSE:Factory/.python313.new.1253 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Package is "python313"

Thu Oct  1 16:41:18 2026 rev:46 rq:1381538 version:3.13.15

Changes:
--------
--- /work/SRC/openSUSE:Factory/python313/python313.changes      2026-09-18 
22:04:21.795123481 +0200
+++ /work/SRC/openSUSE:Factory/.python313.new.1253/python313.changes    
2026-10-01 16:41:27.654996286 +0200
@@ -1,0 +2,7 @@
+Thu Sep 17 21:24:30 UTC 2026 - Matej Cepl <[email protected]>
+
+- CVE-2026-15310: bound zipfile decompression for
+  bzip2/LZMA/Zstandard (bsc#1277111, gh#python/cpython#156002)
+  CVE-2026-15310-bound-zipfile-decompression.patch
+
+-------------------------------------------------------------------

New:
----
  CVE-2026-15310-bound-zipfile-decompression.patch

----------(New B)----------
  New:  bzip2/LZMA/Zstandard (bsc#1277111, gh#python/cpython#156002)
  CVE-2026-15310-bound-zipfile-decompression.patch
----------(New E)----------

++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Other differences:
------------------
++++++ python313.spec ++++++
--- /var/tmp/diff_new_pack.gGMAlA/_old  2026-10-01 16:41:29.018053421 +0200
+++ /var/tmp/diff_new_pack.gGMAlA/_new  2026-10-01 16:41:29.020053505 +0200
@@ -251,6 +251,9 @@
 # PATCH-FIX-UPSTREAM CVE-2026-19672-tarfile-outside-dirs.patch bsc#1276227 
[email protected]
 # in tarfile, handle a member that leaves the destination and comes back
 Patch57:        CVE-2026-19672-tarfile-outside-dirs.patch
+# PATCH-FIX-UPSTREAM CVE-2026-15310-bound-zipfile-decompression.patch 
bsc#1277111 [email protected]
+# Bound zipfile decompression for bzip2/LZMA/Zstandard 
(gh#python/cpython!156003)
+Patch58:         CVE-2026-15310-bound-zipfile-decompression.patch
 #### END OF PATCHES
 BuildRequires:  autoconf-archive
 BuildRequires:  automake

++++++ CVE-2026-15310-bound-zipfile-decompression.patch ++++++
>From 7a2b7388233ea0c647168c5792dc31f3051fe177 Mon Sep 17 00:00:00 2001
From: Petr Viktorin <[email protected]>
Date: Mon, 31 Aug 2026 21:04:04 +0200
Subject: [PATCH 1/3] [3.15] gh-156002: Bound zipfile decompression for
 bzip2/LZMA/Zstandard (GH-156003) (GH-156362)

Patch by @tonghuaroot.

zipfile.ZipExtFile._read1() bounds the output of each decompress() call
for DEFLATE members by passing a max_length to zlib, but for bzip2, LZMA,
and Zstandard members it called decompress() with no bound. A whole
compressed chunk was therefore expanded into a single allocation before
the data[:self._left] clip ran, so a consumer that deliberately reads in
small chunks to limit memory (for example zf.open(name).read(8192)) was
silently unprotected for non-DEFLATE members. A small, spec-conformant
archive member declaring a large uncompressed size could drive multi-GB
peak memory.

_read1() now passes a per-call bound to the non-DEFLATE decompress()
(mirroring the DEFLATE branch) and drains the decompressor's internal
buffer across calls by checking needs_input before reading more
compressed input. zipfile's LZMADecompressor wrapper forwards max_length
and exposes needs_input so the bound also holds for LZMA members.

(cherry picked from commit f897dbf2f36a5935700b7c2d94d4681d2136b7d4)
(cherry picked from commit 1b424c0178a01e155fd0267dc28a8fc1159b33a8)

Co-authored-by: Petr Viktorin <[email protected]>
Co-authored-by: tonghuaroot <[email protected]>
---
 Lib/test/test_zipfile/test_core.py            | 42 +++++++++++++++++++
 Lib/zipfile/__init__.py                       | 38 ++++++++++++++---
 ...-08-18-13-54-05.gh-issue-156002.CcWXPP.rst |  4 ++
 3 files changed, 79 insertions(+), 5 deletions(-)
 create mode 100644 
Misc/NEWS.d/next/Security/2026-08-18-13-54-05.gh-issue-156002.CcWXPP.rst

diff --git a/Lib/test/test_zipfile/test_core.py 
b/Lib/test/test_zipfile/test_core.py
index 57bd71481a37626..b04ba512b8e34e7 100644
--- a/Lib/test/test_zipfile/test_core.py
+++ b/Lib/test/test_zipfile/test_core.py
@@ -2619,6 +2619,48 @@ def tearDown(self):
         unlink(TESTFN2)
 
 
+class AbstractBoundedDecompressTests:
+    # ZipExtFile._read1() bounds the output of each decompress() call so that a
+    # small member declaring a large uncompressed size cannot expand into one
+    # unbounded read.
+    def test_read1_output_is_bounded(self):
+        buf = io.BytesIO()
+        with zipfile.ZipFile(buf, "w", compression=self.compression) as zf:
+            zf.writestr("big", b"\0" * (4 * 1024 * 1024))
+        with zipfile.ZipFile(io.BytesIO(buf.getvalue())) as zf:
+            with zf.open("big") as f:
+                self.assertLessEqual(len(f._read1(100)), f.MIN_READ_SIZE)
+
+
+class StoredBoundedDecompressTests(AbstractBoundedDecompressTests,
+                                   unittest.TestCase):
+    compression = zipfile.ZIP_STORED
+
+
+@requires_zlib()
+class DeflateBoundedDecompressTests(AbstractBoundedDecompressTests,
+                                    unittest.TestCase):
+    compression = zipfile.ZIP_DEFLATED
+
+
+@requires_bz2()
+class Bzip2BoundedDecompressTests(AbstractBoundedDecompressTests,
+                                  unittest.TestCase):
+    compression = zipfile.ZIP_BZIP2
+
+
+@requires_lzma()
+class LzmaBoundedDecompressTests(AbstractBoundedDecompressTests,
+                                 unittest.TestCase):
+    compression = zipfile.ZIP_LZMA
+
+
+@requires_zstd()
+class ZstdBoundedDecompressTests(AbstractBoundedDecompressTests,
+                                 unittest.TestCase):
+    compression = zipfile.ZIP_ZSTANDARD
+
+
 class AbstractBadCrcTests:
     def test_testzip_with_bad_crc(self):
         """Tests that files with bad CRCs return their name from testzip."""
diff --git a/Lib/zipfile/__init__.py b/Lib/zipfile/__init__.py
index 37555d32d997307..8753c07f61e35e8 100644
--- a/Lib/zipfile/__init__.py
+++ b/Lib/zipfile/__init__.py
@@ -726,7 +726,16 @@ def __init__(self):
         self._unconsumed = b''
         self.eof = False
 
-    def decompress(self, data):
+    @property
+    def _needs_input(self):
+        # While the LZMA properties header is still being buffered, more input
+        # is required; afterwards defer to the wrapped decompressor so a 
bounded
+        # decompress() call can be drained across reads.
+        if self._decomp is None:
+            return True
+        return self._decomp.needs_input
+
+    def decompress(self, data, max_length=-1):
         if self._decomp is None:
             self._unconsumed += data
             if len(self._unconsumed) <= 4:
@@ -742,7 +751,7 @@ def decompress(self, data):
             data = self._unconsumed[4 + psize:]
             del self._unconsumed
 
-        result = self._decomp.decompress(data)
+        result = self._decomp.decompress(data, max_length)
         self.eof = self._decomp.eof
         return result
 
@@ -802,6 +811,13 @@ def _get_compressor(compress_type, compresslevel=None):
         return None
 
 
+def _decompressor_needs_input(decompressor):
+    # bz2/zstd expose the stdlib decompressor's public needs_input; the LZMA
+    # wrapper keeps it private (_needs_input) to avoid adding public API.
+    needs_input = getattr(decompressor, "needs_input", None)
+    return decompressor._needs_input if needs_input is None else needs_input
+
+
 def _get_decompressor(compress_type):
     _check_compression(compress_type)
     if compress_type == ZIP_STORED:
@@ -1102,8 +1118,15 @@ def _read1(self, n):
             data = self._decompressor.unconsumed_tail
             if n > len(data):
                 data += self._read2(n - len(data))
-        else:
+        elif self._compress_type == ZIP_STORED:
             data = self._read2(n)
+        else:
+            # bzip2/lzma/zstd: a bounded decompress() call may leave input
+            # buffered inside the decompressor; drain that before reading more.
+            if _decompressor_needs_input(self._decompressor):
+                data = self._read2(n)
+            else:
+                data = b''
 
         if self._compress_type == ZIP_STORED:
             self._eof = self._compress_left <= 0
@@ -1116,8 +1139,13 @@ def _read1(self, n):
             if self._eof:
                 data += self._decompressor.flush()
         else:
-            data = self._decompressor.decompress(data)
-            self._eof = self._decompressor.eof or self._compress_left <= 0
+            # Bound the output of a single decompress() call (mirroring the
+            # DEFLATE path above) so that a small compressed member cannot
+            # expand into one unbounded read.
+            data = self._decompressor.decompress(data, max(n, 
self.MIN_READ_SIZE))
+            self._eof = (self._decompressor.eof or
+                         self._compress_left <= 0 and
+                         _decompressor_needs_input(self._decompressor))
 
         data = data[:self._left]
         self._left -= len(data)
diff --git 
a/Misc/NEWS.d/next/Security/2026-08-18-13-54-05.gh-issue-156002.CcWXPP.rst 
b/Misc/NEWS.d/next/Security/2026-08-18-13-54-05.gh-issue-156002.CcWXPP.rst
new file mode 100644
index 000000000000000..4e49ad5ce8fa00a
--- /dev/null
+++ b/Misc/NEWS.d/next/Security/2026-08-18-13-54-05.gh-issue-156002.CcWXPP.rst
@@ -0,0 +1,4 @@
+Bound the amount of data :mod:`zipfile` decompresses per read for members
+compressed with bzip2, LZMA, or Zstandard, matching the existing limit for
+deflate. A small archive member could previously expand into an unbounded
+allocation even when read in small chunks.

>From d53a41e8cf0148af073c46abc36c2969adcb70c2 Mon Sep 17 00:00:00 2001
From: Petr Viktorin <[email protected]>
Date: Thu, 3 Sep 2026 16:39:39 +0200
Subject: [PATCH 2/3] Remove zstd test (3.14+)

---
 Lib/test/test_zipfile/test_core.py | 6 ------
 1 file changed, 6 deletions(-)

diff --git a/Lib/test/test_zipfile/test_core.py 
b/Lib/test/test_zipfile/test_core.py
index b04ba512b8e34e7..4e4a74b2df42ec8 100644
--- a/Lib/test/test_zipfile/test_core.py
+++ b/Lib/test/test_zipfile/test_core.py
@@ -2655,12 +2655,6 @@ class 
LzmaBoundedDecompressTests(AbstractBoundedDecompressTests,
     compression = zipfile.ZIP_LZMA
 
 
-@requires_zstd()
-class ZstdBoundedDecompressTests(AbstractBoundedDecompressTests,
-                                 unittest.TestCase):
-    compression = zipfile.ZIP_ZSTANDARD
-
-
 class AbstractBadCrcTests:
     def test_testzip_with_bad_crc(self):
         """Tests that files with bad CRCs return their name from testzip."""

>From f3f5234facd2ba491b3bff9e0ac9bd54378314ce Mon Sep 17 00:00:00 2001
From: "Miss Islington (bot)"
 <[email protected]>
Date: Wed, 16 Sep 2026 06:52:44 -0700
Subject: [PATCH 3/3] gh-156002: Keep reading through monkey-patched zipfile
 decompressors (GH-157180) (GH-157557)

(cherry picked from commit f507e6946a3194e83e1d7b8ee6e14567175e46de)

Co-authored-by: Petr Viktorin <[email protected]>
Co-authored-by: rasmusfaber <[email protected]>
---
 Lib/test/test_zipfile/test_core.py            | 68 +++++++++++++++++++
 Lib/zipfile/__init__.py                       | 19 +++---
 ...-09-08-13-06-29.gh-issue-156002.vmOC8T.rst |  5 ++
 3 files changed, 81 insertions(+), 11 deletions(-)
 create mode 100644 
Misc/NEWS.d/next/Library/2026-09-08-13-06-29.gh-issue-156002.vmOC8T.rst

diff --git a/Lib/test/test_zipfile/test_core.py 
b/Lib/test/test_zipfile/test_core.py
index 4e4a74b2df42ec8..05e39faa1045551 100644
--- a/Lib/test/test_zipfile/test_core.py
+++ b/Lib/test/test_zipfile/test_core.py
@@ -2655,6 +2655,74 @@ class 
LzmaBoundedDecompressTests(AbstractBoundedDecompressTests,
     compression = zipfile.ZIP_LZMA
 
 
+
+class MonkeypatchedDecompressorTests(unittest.TestCase):
+    # Some third-party projects monkey-patch _get_decompressor() to add
+    # additional compression schemes. This can break at any time as the
+    # internal compressor objects change.
+    # To protect users, we try to keep this case working.
+    # See also: GH-156002 and GH-113767.
+    COMPRESSION = 99
+
+    class Compressor:
+        """Compressor with only the original BZ2Compressor API"""
+        def compress(self, data):
+            return data.swapcase()
+
+        def flush(self):
+            return b''
+
+    class Decompressor:
+        """Decompressor with only the 3.3+ BZ2Decompressor API"""
+        eof = False
+
+        def decompress(self, data):
+            return data.swapcase()
+
+    def setUp(self):
+        orig_check_compression = zipfile._check_compression
+        orig_get_compressor = zipfile._get_compressor
+        orig_get_decompressor = zipfile._get_decompressor
+
+        def check_compression(compression):
+            if compression != self.COMPRESSION:
+                orig_check_compression(compression)
+
+        def get_compressor(compress_type, compresslevel=None):
+            if compress_type == self.COMPRESSION:
+                return self.Compressor()
+            return orig_get_compressor(compress_type, compresslevel)
+
+        def get_decompressor(compress_type):
+            if compress_type == self.COMPRESSION:
+                return self.Decompressor()
+            return orig_get_decompressor(compress_type)
+
+        self.enterContext(mock.patch.object(
+            zipfile, '_check_compression', check_compression))
+        self.enterContext(mock.patch.object(
+            zipfile, '_get_compressor', get_compressor))
+        self.enterContext(mock.patch.object(
+            zipfile, '_get_decompressor', get_decompressor))
+
+    def test_roundtrip_monkeypatched_decompressor(self):
+        data = bytes(range(256)) * 8
+        buf = io.BytesIO()
+        with zipfile.ZipFile(buf, "w", compression=self.COMPRESSION) as zf:
+            zf.writestr("member", data)
+        self.assertIn(data.swapcase(), buf.getvalue())
+        with zipfile.ZipFile(io.BytesIO(buf.getvalue())) as zf:
+            self.assertEqual(zf.read("member"), data)
+            with zf.open("member") as f:
+                self.assertEqual(f.read(100), data[:100])
+                self.assertEqual(f.read1(100), data[100:200])
+                f.seek(-100, os.SEEK_END)
+                self.assertEqual(f.read(), data[-100:])
+                # Rewinding past the read buffer re-creates the decompressor.
+                f.seek(0)
+                self.assertEqual(f.read(), data)
+
+
 class AbstractBadCrcTests:
     def test_testzip_with_bad_crc(self):
         """Tests that files with bad CRCs return their name from testzip."""
diff --git a/Lib/zipfile/__init__.py b/Lib/zipfile/__init__.py
index 8753c07f61e35e8..a64f831ecddac34 100644
--- a/Lib/zipfile/__init__.py
+++ b/Lib/zipfile/__init__.py
@@ -727,7 +727,7 @@ def __init__(self):
         self.eof = False
 
     @property
-    def _needs_input(self):
+    def needs_input(self):
         # While the LZMA properties header is still being buffered, more input
         # is required; afterwards defer to the wrapped decompressor so a 
bounded
         # decompress() call can be drained across reads.
@@ -811,13 +811,6 @@ def _get_compressor(compress_type, compresslevel=None):
         return None
 
 
-def _decompressor_needs_input(decompressor):
-    # bz2/zstd expose the stdlib decompressor's public needs_input; the LZMA
-    # wrapper keeps it private (_needs_input) to avoid adding public API.
-    needs_input = getattr(decompressor, "needs_input", None)
-    return decompressor._needs_input if needs_input is None else needs_input
-
-
 def _get_decompressor(compress_type):
     _check_compression(compress_type)
     if compress_type == ZIP_STORED:
@@ -1123,7 +1116,7 @@ def _read1(self, n):
         else:
             # bzip2/lzma/zstd: a bounded decompress() call may leave input
             # buffered inside the decompressor; drain that before reading more.
-            if _decompressor_needs_input(self._decompressor):
+            if getattr(self._decompressor, "needs_input", True):
                 data = self._read2(n)
             else:
                 data = b''
@@ -1142,10 +1135,14 @@ def _read1(self, n):
             # Bound the output of a single decompress() call (mirroring the
             # DEFLATE path above) so that a small compressed member cannot
             # expand into one unbounded read.
-            data = self._decompressor.decompress(data, max(n, 
self.MIN_READ_SIZE))
+            try:
+                data = self._decompressor.decompress(data, max(n, 
self.MIN_READ_SIZE))
+            except TypeError:
+                # See MonkeypatchedDecompressorTests in test_core.py
+                data = self._decompressor.decompress(data)
             self._eof = (self._decompressor.eof or
                          self._compress_left <= 0 and
-                         _decompressor_needs_input(self._decompressor))
+                         getattr(self._decompressor, "needs_input", True))
 
         data = data[:self._left]
         self._left -= len(data)
diff --git 
a/Misc/NEWS.d/next/Library/2026-09-08-13-06-29.gh-issue-156002.vmOC8T.rst 
b/Misc/NEWS.d/next/Library/2026-09-08-13-06-29.gh-issue-156002.vmOC8T.rst
new file mode 100644
index 000000000000000..a21386803cca0f4
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2026-09-08-13-06-29.gh-issue-156002.vmOC8T.rst
@@ -0,0 +1,5 @@
+:mod:`zipfile` again reads members through a third-party decompressor
+installed by monkey-patching the private ``_get_decompressor()`` to return an
+object that only implements old BZ2Decompressor API from Python 3.3.
+Note that decompressors without ``needs_input`` and two-argument
+``decompress()`` are vulnerable to :cve:`2026-15310`.

++++++ _scmsync.obsinfo ++++++
--- /var/tmp/diff_new_pack.gGMAlA/_old  2026-10-01 16:41:29.155059164 +0200
+++ /var/tmp/diff_new_pack.gGMAlA/_new  2026-10-01 16:41:29.158059290 +0200
@@ -1,6 +1,6 @@
-mtime: 1789560642
-commit: 27f9421b997f631c957f3dde9fc1a6d31f042d82aa439d9892675ad26e97400e
+mtime: 1789682175
+commit: a4015331750593d33e50b9ad172e8330b02afd3d9ecd2df7359a8f4ba5f65185
 url: https://src.opensuse.org/python-interpreters/python313
-revision: 27f9421b997f631c957f3dde9fc1a6d31f042d82aa439d9892675ad26e97400e
+revision: a4015331750593d33e50b9ad172e8330b02afd3d9ecd2df7359a8f4ba5f65185
 projectscmsync: https://src.opensuse.org/python-interpreters/_ObsPrj
 

++++++ build.specials.obscpio ++++++

++++++ 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      2026-09-17 23:56:15.000000000 +0200
@@ -0,0 +1,6 @@
+.osc
+*.obscpio
+*.osc
+_build.*
+.pbuild
+python313-*-build/

Reply via email to