Skip to content

Commit 64d2dc9

Browse files
bdracoasvetlov
andauthored
Fix memory leak on objects passed to init, update, or extend (#1121)
fixes #1117 --------- Co-authored-by: Andrew Svetlov <andrew.svetlov@gmail.com>
1 parent 9a32040 commit 64d2dc9

5 files changed

Lines changed: 65 additions & 12 deletions

File tree

CHANGES/1121.bugfix.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Resolved a memory leak by ensuring proper reference count decrementation -- by :user:`asvetlov` and :user:`bdraco`.

docs/spelling_wordlist.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ Ctrl
2626
cython
2727
Deprecations
2828
deallocation
29+
decrementation
2930
dev
3031
dict
3132
docstrings

multidict/_multidict.c

Lines changed: 26 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -505,15 +505,19 @@ multidict_tp_init(MultiDictObject *self, PyObject *args, PyObject *kwds)
505505
PyObject *arg = NULL;
506506
Py_ssize_t size = _multidict_extend_parse_args(args, kwds, "MultiDict", &arg);
507507
if (size < 0) {
508-
return -1;
508+
goto fail;
509509
}
510510
if (pair_list_init(&self->pairs, size) < 0) {
511-
return -1;
511+
goto fail;
512512
}
513513
if (_multidict_extend(self, arg, kwds, "MultiDict", 1) < 0) {
514-
return -1;
514+
goto fail;
515515
}
516+
Py_CLEAR(arg);
516517
return 0;
518+
fail:
519+
Py_CLEAR(arg);
520+
return -1;
517521
}
518522

519523
static inline PyObject *
@@ -544,14 +548,17 @@ multidict_extend(MultiDictObject *self, PyObject *args, PyObject *kwds)
544548
PyObject *arg = NULL;
545549
Py_ssize_t size = _multidict_extend_parse_args(args, kwds, "extend", &arg);
546550
if (size < 0) {
547-
return NULL;
551+
goto fail;
548552
}
549553
pair_list_grow(&self->pairs, size);
550554
if (_multidict_extend(self, arg, kwds, "extend", 1) < 0) {
551-
return NULL;
555+
goto fail;
552556
}
553-
557+
Py_CLEAR(arg);
554558
Py_RETURN_NONE;
559+
fail:
560+
Py_CLEAR(arg);
561+
return NULL;
555562
}
556563

557564
static inline PyObject *
@@ -692,12 +699,16 @@ multidict_update(MultiDictObject *self, PyObject *args, PyObject *kwds)
692699
{
693700
PyObject *arg = NULL;
694701
if (_multidict_extend_parse_args(args, kwds, "update", &arg) < 0) {
695-
return NULL;
702+
goto fail;
696703
}
697704
if (_multidict_extend(self, arg, kwds, "update", 0) < 0) {
698-
return NULL;
705+
goto fail;
699706
}
707+
Py_CLEAR(arg);
700708
Py_RETURN_NONE;
709+
fail:
710+
Py_CLEAR(arg);
711+
return NULL;
701712
}
702713

703714
PyDoc_STRVAR(multidict_add_doc,
@@ -917,17 +928,21 @@ cimultidict_tp_init(MultiDictObject *self, PyObject *args, PyObject *kwds)
917928
PyObject *arg = NULL;
918929
Py_ssize_t size = _multidict_extend_parse_args(args, kwds, "CIMultiDict", &arg);
919930
if (size < 0) {
920-
return -1;
931+
goto fail;
921932
}
922933

923934
if (ci_pair_list_init(&self->pairs, size) < 0) {
924-
return -1;
935+
goto fail;
925936
}
926937

927938
if (_multidict_extend(self, arg, kwds, "CIMultiDict", 1) < 0) {
928-
return -1;
939+
goto fail;
929940
}
941+
Py_CLEAR(arg);
930942
return 0;
943+
fail:
944+
Py_CLEAR(arg);
945+
return -1;
931946
}
932947

933948

tests/test_leaks.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@
1919
)
2020
@pytest.mark.leaks
2121
@pytest.mark.skipif(IS_PYPY, reason="leak testing is not supported on PyPy")
22-
@pytest.mark.xfail(reason="memory leak https://github.com/aio-libs/multidict/issues/1117")
2322
def test_leak(script: str) -> None:
2423
"""Run isolated leak test script and check for leaks."""
2524
leak_test_script = pathlib.Path(__file__).parent.joinpath("isolated", script)

tests/test_multidict.py

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import gc
44
import operator
5+
import platform
56
import sys
67
import weakref
78
from collections import deque
@@ -22,6 +23,7 @@
2223
)
2324

2425
_T = TypeVar("_T")
26+
IS_PYPY = platform.python_implementation() == "PyPy"
2527

2628

2729
def chained_callable(
@@ -1272,3 +1274,38 @@ def test_convert_multidict_to_cimultidict_eq(
12721274
) == case_insensitive_multidict_class(
12731275
[("H1", "header1"), ("H2", "header2"), ("H3", "header3")]
12741276
)
1277+
1278+
1279+
@pytest.mark.skipif(IS_PYPY, reason="getrefcount is not supported on PyPy")
1280+
def test_extend_does_not_alter_refcount(
1281+
case_sensitive_multidict_class: type[MultiDict[str]],
1282+
) -> None:
1283+
"""Test that extending a MultiDict with a MultiDict does not alter the refcount of the original."""
1284+
original = case_sensitive_multidict_class([("h1", "header1")])
1285+
new = case_sensitive_multidict_class([("h2", "header2")])
1286+
original_refcount = sys.getrefcount(original)
1287+
new.extend(original)
1288+
assert sys.getrefcount(original) == original_refcount
1289+
1290+
1291+
@pytest.mark.skipif(IS_PYPY, reason="getrefcount is not supported on PyPy")
1292+
def test_update_does_not_alter_refcount(
1293+
case_sensitive_multidict_class: type[MultiDict[str]],
1294+
) -> None:
1295+
"""Test that updating a MultiDict with a MultiDict does not alter the refcount of the original."""
1296+
original = case_sensitive_multidict_class([("h1", "header1")])
1297+
new = case_sensitive_multidict_class([("h2", "header2")])
1298+
original_refcount = sys.getrefcount(original)
1299+
new.update(original)
1300+
assert sys.getrefcount(original) == original_refcount
1301+
1302+
1303+
@pytest.mark.skipif(IS_PYPY, reason="getrefcount is not supported on PyPy")
1304+
def test_init_does_not_alter_refcount(
1305+
case_sensitive_multidict_class: type[MultiDict[str]],
1306+
) -> None:
1307+
"""Test that initializing a MultiDict with a MultiDict does not alter the refcount of the original."""
1308+
original = case_sensitive_multidict_class([("h1", "header1")])
1309+
original_refcount = sys.getrefcount(original)
1310+
case_sensitive_multidict_class(original)
1311+
assert sys.getrefcount(original) == original_refcount

0 commit comments

Comments
 (0)