Skip to content

Commit 7178c06

Browse files
cmaloneymiss-islington
authored andcommitted
pythongh-129726: Break gzip.GzipFile reference loop (pythonGH-130055)
A reference loop was resulting in the `fileobj` held by the `GzipFile` being closed before the `GzipFile`. The issue started with pythongh-89550 in 3.12, but was hidden in most cases until 3.13 when pythongh-62948 made it more visible. (cherry picked from commit 7f39137) Co-authored-by: Cody Maloney <cmaloney@users.noreply.github.com>
1 parent 797aac7 commit 7178c06

File tree

3 files changed

+28
-5
lines changed

3 files changed

+28
-5
lines changed

Lib/gzip.py

+12-5
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,15 @@
55

66
# based on Andrew Kuchling's minigzip.py distributed with the zlib module
77

8-
import struct, sys, time, os
9-
import zlib
8+
import _compression
109
import builtins
1110
import io
12-
import _compression
11+
import os
12+
import struct
13+
import sys
14+
import time
15+
import weakref
16+
import zlib
1317

1418
__all__ = ["BadGzipFile", "GzipFile", "open", "compress", "decompress"]
1519

@@ -125,10 +129,13 @@ class BadGzipFile(OSError):
125129
class _WriteBufferStream(io.RawIOBase):
126130
"""Minimal object to pass WriteBuffer flushes into GzipFile"""
127131
def __init__(self, gzip_file):
128-
self.gzip_file = gzip_file
132+
self.gzip_file = weakref.ref(gzip_file)
129133

130134
def write(self, data):
131-
return self.gzip_file._write_raw(data)
135+
gzip_file = self.gzip_file()
136+
if gzip_file is None:
137+
raise RuntimeError("lost gzip_file")
138+
return gzip_file._write_raw(data)
132139

133140
def seekable(self):
134141
return False

Lib/test/test_gzip.py

+13
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,14 @@
33

44
import array
55
import functools
6+
import gc
67
import io
78
import os
89
import struct
910
import sys
1011
import unittest
1112
from subprocess import PIPE, Popen
13+
from test.support import catch_unraisable_exception
1214
from test.support import import_helper
1315
from test.support import os_helper
1416
from test.support import _4G, bigmemtest, requires_subprocess
@@ -848,6 +850,17 @@ def test_write_seek_write(self):
848850
self.assertEqual(gzip.decompress(data), message * 2)
849851

850852

853+
def test_refloop_unraisable(self):
854+
# Ensure a GzipFile referring to a temporary fileobj deletes cleanly.
855+
# Previously an unraisable exception would occur on close because the
856+
# fileobj would be closed before the GzipFile as the result of a
857+
# reference loop. See issue gh-129726
858+
with catch_unraisable_exception() as cm:
859+
gzip.GzipFile(fileobj=io.BytesIO(), mode="w")
860+
gc.collect()
861+
self.assertIsNone(cm.unraisable)
862+
863+
851864
class TestOpen(BaseTest):
852865
def test_binary_modes(self):
853866
uncompressed = data1 * 50
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
Fix :class:`gzip.GzipFile` raising an unraisable exception during garbage
2+
collection when referring to a temporary object by breaking the reference
3+
loop with :mod:`weakref`.

0 commit comments

Comments
 (0)