Skip to content

Commit b9bf25c

Browse files
Vizonexbdraco
andauthored
fix ref bug with iter, views and istr (#1311)
Co-authored-by: J. Nick Koston <[email protected]> Co-authored-by: J. Nick Koston <[email protected]>
1 parent edb61e8 commit b9bf25c

7 files changed

Lines changed: 113 additions & 2 deletions

File tree

CHANGES/1311.bugfix.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Fixed reference leak in iterators, views and ``istr``
2+
-- by :user:`Vizonex`.

multidict/_multilib/istr.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,10 @@ PyDoc_STRVAR(istr__doc__, "istr class implementation");
2222
static inline void
2323
istr_dealloc(istrobject *self)
2424
{
25+
PyTypeObject *tp = Py_TYPE(self);
2526
Py_XDECREF(self->canonical);
2627
PyUnicode_Type.tp_dealloc((PyObject *)self);
28+
Py_DECREF(tp);
2729
}
2830

2931
static inline PyObject *

multidict/_multilib/iter.h

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -134,14 +134,17 @@ multidict_keys_iter_iternext(MultidictIter *self)
134134
static inline void
135135
multidict_iter_dealloc(MultidictIter *self)
136136
{
137+
PyTypeObject *tp = Py_TYPE(self);
137138
PyObject_GC_UnTrack(self);
138139
Py_XDECREF(self->md);
139-
PyObject_GC_Del(self);
140+
tp->tp_free(self);
141+
Py_DECREF(tp);
140142
}
141143

142144
static inline int
143145
multidict_iter_traverse(MultidictIter *self, visitproc visit, void *arg)
144146
{
147+
Py_VISIT(Py_TYPE(self));
145148
Py_VISIT(self->md);
146149
return 0;
147150
}

multidict/_multilib/views.h

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,15 +30,18 @@ _init_view(_Multidict_ViewObject *self, MultiDictObject *md)
3030
static inline void
3131
multidict_view_dealloc(_Multidict_ViewObject *self)
3232
{
33+
PyTypeObject *tp = Py_TYPE(self);
3334
PyObject_GC_UnTrack(self);
3435
Py_XDECREF(self->md);
35-
PyObject_GC_Del(self);
36+
tp->tp_free(self);
37+
Py_DECREF(tp);
3638
}
3739

3840
static inline int
3941
multidict_view_traverse(_Multidict_ViewObject *self, visitproc visit,
4042
void *arg)
4143
{
44+
Py_VISIT(Py_TYPE(self));
4245
Py_VISIT(self->md);
4346
return 0;
4447
}
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import gc
2+
import sys
3+
import sysconfig
4+
5+
from multidict import MultiDict, istr
6+
7+
# sys.getrefcount is not meaningful under the free-threaded build:
8+
# refcounts are biased per-thread and types may be immortalized, so
9+
# the simple baseline/after comparison below does not apply.
10+
FREETHREADED = bool(sysconfig.get_config_var("Py_GIL_DISABLED"))
11+
12+
13+
if __name__ == "__main__":
14+
if FREETHREADED:
15+
raise SystemExit(0)
16+
17+
md = MultiDict([("a", "1"), ("b", "2")])
18+
19+
# Iterator type leak
20+
iter_type = type(iter(md.keys()))
21+
gc.collect()
22+
baseline = sys.getrefcount(iter_type)
23+
for _ in range(1000):
24+
_it = iter(md.keys())
25+
list(_it)
26+
del _it
27+
gc.collect()
28+
after = sys.getrefcount(iter_type)
29+
assert after == baseline, f"iterator type leaked: {after - baseline}"
30+
31+
# View type leak
32+
view_type = type(md.keys())
33+
gc.collect()
34+
baseline = sys.getrefcount(view_type)
35+
for _ in range(1000):
36+
_v = md.keys()
37+
del _v
38+
gc.collect()
39+
after = sys.getrefcount(view_type)
40+
assert after == baseline, f"view type leaked: {after - baseline}"
41+
42+
# istr type leak
43+
gc.collect()
44+
baseline = sys.getrefcount(istr)
45+
for _ in range(1000):
46+
_s = istr("hello")
47+
del _s
48+
gc.collect()
49+
after = sys.getrefcount(istr)
50+
assert after == baseline, f"istr type leaked: {after - baseline}"
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import gc
2+
import sys
3+
import sysconfig
4+
5+
from multidict import MultiDict
6+
7+
# sys.getrefcount is not meaningful under the free-threaded build:
8+
# refcounts are biased per-thread and types may be immortalized, so
9+
# the simple baseline/after comparison below does not apply.
10+
FREETHREADED = bool(sysconfig.get_config_var("Py_GIL_DISABLED"))
11+
12+
13+
if __name__ == "__main__":
14+
if FREETHREADED:
15+
raise SystemExit(0)
16+
17+
md = MultiDict([("a", "1"), ("b", "2")])
18+
19+
# items() / values() iterators each have their own PyType_Spec
20+
# sharing multidict_iter_dealloc; test them independently so a
21+
# regression in just one spec's slot table is still caught.
22+
for view_name in ("items", "values"):
23+
get_view = getattr(md, view_name)
24+
iter_type = type(iter(get_view()))
25+
gc.collect()
26+
baseline = sys.getrefcount(iter_type)
27+
for _ in range(1000):
28+
_it = iter(get_view())
29+
list(_it)
30+
del _it
31+
gc.collect()
32+
after = sys.getrefcount(iter_type)
33+
assert after == baseline, (
34+
f"{view_name} iterator type leaked: {after - baseline}"
35+
)
36+
37+
# items() / values() views each have their own PyType_Spec
38+
# sharing multidict_view_dealloc; same rationale as above.
39+
for view_name in ("items", "values"):
40+
get_view = getattr(md, view_name)
41+
view_type = type(get_view())
42+
gc.collect()
43+
baseline = sys.getrefcount(view_type)
44+
for _ in range(1000):
45+
_v = get_view()
46+
del _v
47+
gc.collect()
48+
after = sys.getrefcount(view_type)
49+
assert after == baseline, f"{view_name} view type leaked: {after - baseline}"

tests/test_leaks.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@
1515
"multidict_extend_multidict.py",
1616
"multidict_extend_tuple.py",
1717
"multidict_update_multidict.py",
18+
"multidict_type_leak.py",
19+
"multidict_type_leak_items_values.py",
1820
"multidict_pop.py",
1921
),
2022
)

0 commit comments

Comments
 (0)