[3.12] gh-129726: Break gzip.GzipFile reference loop (GH-130055) (#130670)

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 7f39137662)

Co-authored-by: Cody Maloney <cmaloney@users.noreply.github.com>
This commit is contained in:
Miss Islington (bot) 2025-02-28 09:28:14 +01:00 committed by GitHub
parent 107e08dfb1
commit 500ea3b0ee
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 28 additions and 5 deletions

View file

@ -5,11 +5,15 @@ but random access is not allowed."""
# based on Andrew Kuchling's minigzip.py distributed with the zlib module # based on Andrew Kuchling's minigzip.py distributed with the zlib module
import struct, sys, time, os import _compression
import zlib
import builtins import builtins
import io import io
import _compression import os
import struct
import sys
import time
import weakref
import zlib
__all__ = ["BadGzipFile", "GzipFile", "open", "compress", "decompress"] __all__ = ["BadGzipFile", "GzipFile", "open", "compress", "decompress"]
@ -124,10 +128,13 @@ class BadGzipFile(OSError):
class _WriteBufferStream(io.RawIOBase): class _WriteBufferStream(io.RawIOBase):
"""Minimal object to pass WriteBuffer flushes into GzipFile""" """Minimal object to pass WriteBuffer flushes into GzipFile"""
def __init__(self, gzip_file): def __init__(self, gzip_file):
self.gzip_file = gzip_file self.gzip_file = weakref.ref(gzip_file)
def write(self, data): 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): def seekable(self):
return False return False

View file

@ -3,12 +3,14 @@
import array import array
import functools import functools
import gc
import io import io
import os import os
import struct import struct
import sys import sys
import unittest import unittest
from subprocess import PIPE, Popen from subprocess import PIPE, Popen
from test.support import catch_unraisable_exception
from test.support import import_helper from test.support import import_helper
from test.support import os_helper from test.support import os_helper
from test.support import _4G, bigmemtest, requires_subprocess from test.support import _4G, bigmemtest, requires_subprocess
@ -836,6 +838,17 @@ class TestGzip(BaseTest):
self.assertEqual(gzip.decompress(data), message * 2) 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): class TestOpen(BaseTest):
def test_binary_modes(self): def test_binary_modes(self):
uncompressed = data1 * 50 uncompressed = data1 * 50

View file

@ -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`.