Skip to content

Commit ec34427

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 b8f2ff0 commit ec34427

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

@@ -124,10 +128,13 @@ class BadGzipFile(OSError):
124128
class _WriteBufferStream(io.RawIOBase):
125129
"""Minimal object to pass WriteBuffer flushes into GzipFile"""
126130
def __init__(self, gzip_file):
127-
self.gzip_file = gzip_file
131+
self.gzip_file = weakref.ref(gzip_file)
128132

129133
def write(self, data):
130-
return self.gzip_file._write_raw(data)
134+
gzip_file = self.gzip_file()
135+
if gzip_file is None:
136+
raise RuntimeError("lost gzip_file")
137+
return gzip_file._write_raw(data)
131138

132139
def seekable(self):
133140
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
@@ -836,6 +838,17 @@ def test_write_seek_write(self):
836838
self.assertEqual(gzip.decompress(data), message * 2)
837839

838840

841+
def test_refloop_unraisable(self):
842+
# Ensure a GzipFile referring to a temporary fileobj deletes cleanly.
843+
# Previously an unraisable exception would occur on close because the
844+
# fileobj would be closed before the GzipFile as the result of a
845+
# reference loop. See issue gh-129726
846+
with catch_unraisable_exception() as cm:
847+
gzip.GzipFile(fileobj=io.BytesIO(), mode="w")
848+
gc.collect()
849+
self.assertIsNone(cm.unraisable)
850+
851+
839852
class TestOpen(BaseTest):
840853
def test_binary_modes(self):
841854
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)