From: Miss Islington (bot) <31488909+miss-islington@users.noreply.github.com> Date: Fri, 28 Feb 2025 08:32:44 +0000 (+0100) Subject: [3.13] gh-129726: Break `gzip.GzipFile` reference loop (GH-130055) (#130669) X-Git-Tag: v3.13.3~195 X-Git-Url: http://git.ipfire.org/gitweb.cgi?a=commitdiff_plain;h=ad97027e9b010d07272aeb8b3151bc59f710f367;p=thirdparty%2FPython%2Fcpython.git [3.13] gh-129726: Break `gzip.GzipFile` reference loop (GH-130055) (#130669) gh-129726: Break `gzip.GzipFile` reference loop (GH-130055) 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. (cherry picked from commit 7f39137662f637518a74228286e7ec675fa4e27d) Co-authored-by: Cody Maloney --- diff --git a/Lib/gzip.py b/Lib/gzip.py index ba753ce3050d..447fa2b0494f 100644 --- a/Lib/gzip.py +++ b/Lib/gzip.py @@ -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 diff --git a/Lib/test/test_gzip.py b/Lib/test/test_gzip.py index ae384c3849d4..062c5ff47d46 100644 --- a/Lib/test/test_gzip.py +++ b/Lib/test/test_gzip.py @@ -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 @@ -848,6 +850,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 index 000000000000..31032b59b5ea --- /dev/null +++ b/Misc/NEWS.d/next/Library/2025-02-12-12-38-24.gh-issue-129726.jB0sxu.rst @@ -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`.