Skip to content

Commit 7f39137

Browse files
authored
gh-129726: Break gzip.GzipFile reference loop (#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.
1 parent e419817 commit 7f39137

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
@@ -859,6 +861,17 @@ def test_write_seek_write(self):
859861
self.assertEqual(gzip.decompress(data), message * 2)
860862

861863

864+
def test_refloop_unraisable(self):
865+
# Ensure a GzipFile referring to a temporary fileobj deletes cleanly.
866+
# Previously an unraisable exception would occur on close because the
867+
# fileobj would be closed before the GzipFile as the result of a
868+
# reference loop. See issue gh-129726
869+
with catch_unraisable_exception() as cm:
870+
gzip.GzipFile(fileobj=io.BytesIO(), mode="w")
871+
gc.collect()
872+
self.assertIsNone(cm.unraisable)
873+
874+
862875
class TestOpen(BaseTest):
863876
def test_binary_modes(self):
864877
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)