]> git.ipfire.org Git - thirdparty/Python/cpython.git/commitdiff
gh-129726: Break `gzip.GzipFile` reference loop (#130055)
authorCody Maloney <cmaloney@users.noreply.github.com>
Fri, 28 Feb 2025 08:09:24 +0000 (00:09 -0800)
committerGitHub <noreply@github.com>
Fri, 28 Feb 2025 08:09:24 +0000 (09:09 +0100)
A reference loop was resulting in the `fileobj` held by the `GzipFile`
being closed before the `GzipFile`.

The issue started with gh-89550 in 3.12, but was hidden in most cases
until 3.13 when gh-62948 made it more visible.

Lib/gzip.py
Lib/test/test_gzip.py
Misc/NEWS.d/next/Library/2025-02-12-12-38-24.gh-issue-129726.jB0sxu.rst [new file with mode: 0644]

index 1a3c82ce7e0711ad18928fb55d9e68d29b17df2d..7e384f8a568c1cf9cb77a164f2aec86b80977224 100644 (file)
@@ -5,11 +5,15 @@ but random access is not allowed."""
 
 # based on Andrew Kuchling's minigzip.py distributed with the zlib module
 
-import struct, sys, time, os
-import zlib
+import _compression
 import builtins
 import io
-import _compression
+import os
+import struct
+import sys
+import time
+import weakref
+import zlib
 
 __all__ = ["BadGzipFile", "GzipFile", "open", "compress", "decompress"]
 
@@ -125,10 +129,13 @@ class BadGzipFile(OSError):
 class _WriteBufferStream(io.RawIOBase):
     """Minimal object to pass WriteBuffer flushes into GzipFile"""
     def __init__(self, gzip_file):
-        self.gzip_file = gzip_file
+        self.gzip_file = weakref.ref(gzip_file)
 
     def write(self, data):
-        return self.gzip_file._write_raw(data)
+        gzip_file = self.gzip_file()
+        if gzip_file is None:
+            raise RuntimeError("lost gzip_file")
+        return gzip_file._write_raw(data)
 
     def seekable(self):
         return False
index bf6e1703db84510b38e08d4d128161e60a4443f9..0940bb114df625d2473c22ba9ce46b2dc239ab6a 100644 (file)
@@ -3,12 +3,14 @@
 
 import array
 import functools
+import gc
 import io
 import os
 import struct
 import sys
 import unittest
 from subprocess import PIPE, Popen
+from test.support import catch_unraisable_exception
 from test.support import import_helper
 from test.support import os_helper
 from test.support import _4G, bigmemtest, requires_subprocess
@@ -859,6 +861,17 @@ class TestGzip(BaseTest):
         self.assertEqual(gzip.decompress(data), message * 2)
 
 
+    def test_refloop_unraisable(self):
+        # Ensure a GzipFile referring to a temporary fileobj deletes cleanly.
+        # Previously an unraisable exception would occur on close because the
+        # fileobj would be closed before the GzipFile as the result of a
+        # reference loop. See issue gh-129726
+        with catch_unraisable_exception() as cm:
+            gzip.GzipFile(fileobj=io.BytesIO(), mode="w")
+            gc.collect()
+            self.assertIsNone(cm.unraisable)
+
+
 class TestOpen(BaseTest):
     def test_binary_modes(self):
         uncompressed = data1 * 50
diff --git a/Misc/NEWS.d/next/Library/2025-02-12-12-38-24.gh-issue-129726.jB0sxu.rst b/Misc/NEWS.d/next/Library/2025-02-12-12-38-24.gh-issue-129726.jB0sxu.rst
new file mode 100644 (file)
index 0000000..31032b5
--- /dev/null
@@ -0,0 +1,3 @@
+Fix :class:`gzip.GzipFile` raising an unraisable exception during garbage
+collection when referring to a temporary object by breaking the reference
+loop with :mod:`weakref`.