Skip to content

Commit 31f63e0

Browse files
authored
[mypyc] Fix reference leak on failure in specialized dict methods (#22233)
The specializations for `list(subdict.m())` where `m` is `keys`, `values`, or `items` create an empty list to extend with the result of the method. If the method raises, the list is not cleaned up. Use `PySequence_List` instead which does the same thing with correct memory handling.
1 parent 45992fc commit 31f63e0

2 files changed

Lines changed: 78 additions & 35 deletions

File tree

‎mypyc/lib-rt/dict_ops.c‎

Lines changed: 12 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -212,58 +212,35 @@ PyObject *CPyDict_ItemsView(PyObject *dict) {
212212
return PyObject_CallMethodNoArgs(dict, mypyc_interned_str.items);
213213
}
214214

215-
PyObject *CPyDict_Keys(PyObject *dict) {
216-
if (PyDict_CheckExact(dict)) {
217-
return PyDict_Keys(dict);
218-
}
219-
// Inline generic fallback logic to also return a list.
220-
PyObject *list = PyList_New(0);
221-
PyObject *view = PyObject_CallMethodNoArgs(dict, mypyc_interned_str.keys);
215+
static inline PyObject *dict_view_as_list(PyObject *dict, PyObject *method) {
216+
PyObject *view = PyObject_CallMethodNoArgs(dict, method);
222217
if (view == NULL) {
223218
return NULL;
224219
}
225-
int res = PyList_Extend(list, view);
220+
PyObject *list = PySequence_List(view);
226221
Py_DECREF(view);
227-
if (res < 0) {
228-
return NULL;
229-
}
230222
return list;
231223
}
232224

225+
PyObject *CPyDict_Keys(PyObject *dict) {
226+
if (PyDict_CheckExact(dict)) {
227+
return PyDict_Keys(dict);
228+
}
229+
return dict_view_as_list(dict, mypyc_interned_str.keys);
230+
}
231+
233232
PyObject *CPyDict_Values(PyObject *dict) {
234233
if (PyDict_CheckExact(dict)) {
235234
return PyDict_Values(dict);
236235
}
237-
// Inline generic fallback logic to also return a list.
238-
PyObject *list = PyList_New(0);
239-
PyObject *view = PyObject_CallMethodNoArgs(dict, mypyc_interned_str.values);
240-
if (view == NULL) {
241-
return NULL;
242-
}
243-
int res = PyList_Extend(list, view);
244-
Py_DECREF(view);
245-
if (res < 0) {
246-
return NULL;
247-
}
248-
return list;
236+
return dict_view_as_list(dict, mypyc_interned_str.values);
249237
}
250238

251239
PyObject *CPyDict_Items(PyObject *dict) {
252240
if (PyDict_CheckExact(dict)) {
253241
return PyDict_Items(dict);
254242
}
255-
// Inline generic fallback logic to also return a list.
256-
PyObject *list = PyList_New(0);
257-
PyObject *view = PyObject_CallMethodNoArgs(dict, mypyc_interned_str.items);
258-
if (view == NULL) {
259-
return NULL;
260-
}
261-
int res = PyList_Extend(list, view);
262-
Py_DECREF(view);
263-
if (res < 0) {
264-
return NULL;
265-
}
266-
return list;
243+
return dict_view_as_list(dict, mypyc_interned_str.items);
267244
}
268245

269246
char CPyDict_Clear(PyObject *dict) {

‎mypyc/test-data/run-dicts.test‎

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -513,3 +513,69 @@ test_sub_cyclic_dict_cleanup()
513513
test_cyclic_dict_cleanup_interpreted()
514514
test_sub_cyclic_dict_cleanup_interpreted()
515515
test_allow_interpreted_subclass_cyclic_dict_cleanup()
516+
517+
[case testDictListErrorOwnership]
518+
from typing import Any, Dict, List
519+
520+
def keys(d: Dict[Any, Any]) -> List[Any]:
521+
return list(d.keys())
522+
def values(d: Dict[Any, Any]) -> List[Any]:
523+
return list(d.values())
524+
def items(d: Dict[Any, Any]) -> List[Any]:
525+
return list(d.items())
526+
527+
[file driver.py]
528+
import gc
529+
import weakref
530+
from native import keys, values, items
531+
from testutil import assertRaises
532+
533+
class Sentinel:
534+
pass
535+
536+
refs = []
537+
def failing_iterator():
538+
value = Sentinel()
539+
refs.append(weakref.ref(value))
540+
yield value
541+
raise RuntimeError("iteration failed")
542+
543+
class BadDict(dict):
544+
def keys(self):
545+
return failing_iterator()
546+
values = keys
547+
items = keys
548+
549+
class BadMethod(dict):
550+
def keys(self):
551+
raise RuntimeError("method failed")
552+
values = keys
553+
items = keys
554+
555+
class BadIter:
556+
def __iter__(self):
557+
raise RuntimeError("iter failed")
558+
559+
class BadLength:
560+
def __iter__(self):
561+
return self
562+
def __next__(self):
563+
return None
564+
def __length_hint__(self):
565+
raise RuntimeError("length failed")
566+
567+
class CustomDict(dict):
568+
def __init__(self, result):
569+
self.result = result
570+
def keys(self):
571+
return self.result
572+
values = keys
573+
items = keys
574+
575+
for convert in (keys, values, items):
576+
for d in (BadDict(), BadMethod(), CustomDict(BadIter()), CustomDict(BadLength())):
577+
with assertRaises(RuntimeError):
578+
convert(d)
579+
gc.collect()
580+
assert all(ref() is None for ref in refs)
581+
assert convert(CustomDict(iter([1, 2]))) == [1, 2]

0 commit comments

Comments
 (0)