Skip to content

Commit 86e49a5

Browse files
[3.10] gh-94207: Fix struct module leak (GH-94239) (GH-94266)
* gh-94207: Fix struct module leak (GH-94239) Make _struct.Struct a GC type This fixes a memory leak in the _struct module, where as soon as a Struct object is stored in the cache, there's a cycle from the _struct module to the cache to Struct objects to the Struct type back to the module. If _struct.Struct is not gc-tracked, that cycle is never collected. This PR makes _struct.Struct GC-tracked, and adds a regression test. (cherry picked from commit 6b86534) Co-authored-by: Mark Dickinson <dickinsm@gmail.com>
1 parent 1494382 commit 86e49a5

File tree

3 files changed

+40
-2
lines changed

3 files changed

+40
-2
lines changed

Lib/test/test_struct.py

+18
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,15 @@
11
from collections import abc
22
import array
3+
import gc
34
import math
45
import operator
56
import unittest
67
import struct
78
import sys
9+
import weakref
810

911
from test import support
12+
from test.support import import_helper
1013
from test.support.script_helper import assert_python_ok
1114

1215
ISBIGENDIAN = sys.byteorder == "big"
@@ -671,6 +674,21 @@ def __del__(self):
671674
self.assertIn(b"Exception ignored in:", stderr)
672675
self.assertIn(b"C.__del__", stderr)
673676

677+
def test__struct_reference_cycle_cleaned_up(self):
678+
# Regression test for python/cpython#94207.
679+
680+
# When we create a new struct module, trigger use of its cache,
681+
# and then delete it ...
682+
_struct_module = import_helper.import_fresh_module("_struct")
683+
module_ref = weakref.ref(_struct_module)
684+
_struct_module.calcsize("b")
685+
del _struct_module
686+
687+
# Then the module should have been garbage collected.
688+
gc.collect()
689+
self.assertIsNone(
690+
module_ref(), "_struct module was not garbage collected")
691+
674692
def test_issue35714(self):
675693
# Embedded null characters should not be allowed in format strings.
676694
for s in '\0', '2\0i', b'\0':
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Made :class:`_struct.Struct` GC-tracked in order to fix a reference leak in
2+
the :mod:`_struct` module.

Modules/_struct.c

+20-2
Original file line numberDiff line numberDiff line change
@@ -1495,10 +1495,26 @@ Struct___init___impl(PyStructObject *self, PyObject *format)
14951495
return ret;
14961496
}
14971497

1498+
static int
1499+
s_clear(PyStructObject *s)
1500+
{
1501+
Py_CLEAR(s->s_format);
1502+
return 0;
1503+
}
1504+
1505+
static int
1506+
s_traverse(PyStructObject *s, visitproc visit, void *arg)
1507+
{
1508+
Py_VISIT(Py_TYPE(s));
1509+
Py_VISIT(s->s_format);
1510+
return 0;
1511+
}
1512+
14981513
static void
14991514
s_dealloc(PyStructObject *s)
15001515
{
15011516
PyTypeObject *tp = Py_TYPE(s);
1517+
PyObject_GC_UnTrack(s);
15021518
if (s->weakreflist != NULL)
15031519
PyObject_ClearWeakRefs((PyObject *)s);
15041520
if (s->s_codes != NULL) {
@@ -2079,21 +2095,23 @@ static PyType_Slot PyStructType_slots[] = {
20792095
{Py_tp_getattro, PyObject_GenericGetAttr},
20802096
{Py_tp_setattro, PyObject_GenericSetAttr},
20812097
{Py_tp_doc, (void*)s__doc__},
2098+
{Py_tp_traverse, s_traverse},
2099+
{Py_tp_clear, s_clear},
20822100
{Py_tp_methods, s_methods},
20832101
{Py_tp_members, s_members},
20842102
{Py_tp_getset, s_getsetlist},
20852103
{Py_tp_init, Struct___init__},
20862104
{Py_tp_alloc, PyType_GenericAlloc},
20872105
{Py_tp_new, s_new},
2088-
{Py_tp_free, PyObject_Del},
2106+
{Py_tp_free, PyObject_GC_Del},
20892107
{0, 0},
20902108
};
20912109

20922110
static PyType_Spec PyStructType_spec = {
20932111
"_struct.Struct",
20942112
sizeof(PyStructObject),
20952113
0,
2096-
Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE,
2114+
Py_TPFLAGS_DEFAULT | Py_TPFLAGS_HAVE_GC | Py_TPFLAGS_BASETYPE,
20972115
PyStructType_slots
20982116
};
20992117

0 commit comments

Comments
 (0)