Skip to content

Commit 4bc5f9f

Browse files
gh-94207: Fix struct module leak (GH-94239) (GH-94265)
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 <mdickinson@enthought.com>
1 parent 89ba660 commit 4bc5f9f

3 files changed

Lines changed: 39 additions & 2 deletions

File tree

‎Lib/test/test_struct.py‎

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
fromcollectionsimportabc
22
importarray
3+
importgc
34
importmath
45
importoperator
56
importunittest
67
importstruct
78
importsys
9+
importweakref
810

911
fromtestimportsupport
1012
fromtest.supportimportimport_helper
@@ -672,6 +674,21 @@ def __del__(self):
672674
self.assertIn(b"Exception ignored in:", stderr)
673675
self.assertIn(b"C.__del__", stderr)
674676

677+
deftest__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+
675692
deftest_issue35714(self):
676693
# Embedded null characters should not be allowed in format strings.
677694
forsin'\0', '2\0i', b'\0':
Lines changed: 2 additions & 0 deletions
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‎

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1496,10 +1496,26 @@ Struct___init___impl(PyStructObject *self, PyObject *format)
14961496
returnret;
14971497
}
14981498

1499+
staticint
1500+
s_clear(PyStructObject*s)
1501+
{
1502+
Py_CLEAR(s->s_format);
1503+
return0;
1504+
}
1505+
1506+
staticint
1507+
s_traverse(PyStructObject*s, visitprocvisit, void*arg)
1508+
{
1509+
Py_VISIT(Py_TYPE(s));
1510+
Py_VISIT(s->s_format);
1511+
return0;
1512+
}
1513+
14991514
staticvoid
15001515
s_dealloc(PyStructObject*s)
15011516
{
15021517
PyTypeObject*tp=Py_TYPE(s);
1518+
PyObject_GC_UnTrack(s);
15031519
if (s->weakreflist!=NULL)
15041520
PyObject_ClearWeakRefs((PyObject*)s);
15051521
if (s->s_codes!=NULL) {
@@ -2078,21 +2094,23 @@ static PyType_Slot PyStructType_slots[] = {
20782094
{Py_tp_getattro, PyObject_GenericGetAttr},
20792095
{Py_tp_setattro, PyObject_GenericSetAttr},
20802096
{Py_tp_doc, (void*)s__doc__},
2097+
{Py_tp_traverse, s_traverse},
2098+
{Py_tp_clear, s_clear},
20812099
{Py_tp_methods, s_methods},
20822100
{Py_tp_members, s_members},
20832101
{Py_tp_getset, s_getsetlist},
20842102
{Py_tp_init, Struct___init__},
20852103
{Py_tp_alloc, PyType_GenericAlloc},
20862104
{Py_tp_new, s_new},
2087-
{Py_tp_free, PyObject_Del},
2105+
{Py_tp_free, PyObject_GC_Del},
20882106
{0, 0},
20892107
};
20902108

20912109
staticPyType_SpecPyStructType_spec= {
20922110
"_struct.Struct",
20932111
sizeof(PyStructObject),
20942112
0,
2095-
Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE,
2113+
Py_TPFLAGS_DEFAULT | Py_TPFLAGS_HAVE_GC | Py_TPFLAGS_BASETYPE,
20962114
PyStructType_slots
20972115
};
20982116

0 commit comments

Comments
 (0)