Skip to content

Commit 9f894f4

Browse files
zangjiuchengclaude
andcommitted
gh-152107: Fix crash creating a dict item iterator under MemoryError
dictiter_new() for item iterators allocates di_result via _PyTuple_FromPairSteal() before calling _PyObject_GC_TRACK(di). If that allocation fails (OOM), the error path runs Py_DECREF(di) on the not-yet-tracked iterator and dictiter_dealloc()'s _PyObject_GC_UNTRACK(di) asserts the object is GC-tracked -- aborting on debug builds and corrupting the GC list (later segfault) on release builds. An iterator with di_result == NULL is already in a valid state, so track it right after initialization, before the only fallible allocation. The OOM error path then deallocates a properly tracked object. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent a46db4f commit 9f894f4

3 files changed

Lines changed: 33 additions & 5 deletions

File tree

Lib/test/test_dict.py

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
import unittest
1010
import weakref
1111
from test import support
12-
from test.support import import_helper
12+
from test.support import import_helper, script_helper
1313

1414

1515
class CustomHash:
@@ -116,6 +116,30 @@ def test_items(self):
116116
self.assertRaises(TypeError, d.items, None)
117117
self.assertEqual(repr(dict(a=1).items()), "dict_items([('a', 1)])")
118118

119+
@support.cpython_only
120+
def test_item_iterator_no_memory(self):
121+
# gh-152107: dictiter_new() for a dict item-iterator allocates
122+
# di_result via _PyTuple_FromPairSteal() before _PyObject_GC_TRACK().
123+
# When that allocation fails under OOM the error path decref'd the
124+
# still-untracked iterator, and dictiter_dealloc() unconditionally
125+
# untracked it -- asserting on a debug build and corrupting the GC
126+
# list (later segfault) on a release build. Make sure it doesn't crash.
127+
import_helper.import_module("_testcapi")
128+
# _strptime() builds dict item-iterators while the size-2 tuple
129+
# freelist is drained, so _PyTuple_FromPairSteal() in dictiter_new()
130+
# actually allocates and can fail under the set_nomemory() sweep.
131+
code = """if 1:
132+
import _strptime
133+
from _testcapi import set_nomemory
134+
for start in range(60):
135+
set_nomemory(start)
136+
try:
137+
_strptime._strptime("", "")
138+
except BaseException:
139+
pass
140+
"""
141+
script_helper.assert_python_ok("-c", code)
142+
119143
def test_views_mapping(self):
120144
mappingproxy = type(type.__dict__)
121145
class Dict(dict):
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
Fix a crash when creating a :class:`dict` item iterator (for example
2+
``iter(d.items())`` or ``reversed(d.items())``) under a memory-allocation
3+
failure. Patch by Jiucheng Zang.

Objects/dictobject.c

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5520,6 +5520,11 @@ dictiter_new(PyDictObject *dict, PyTypeObject *itertype)
55205520
else {
55215521
di->di_pos = 0;
55225522
}
5523+
di->di_result = NULL;
5524+
/* An iterator with di_result == NULL is in a valid state, so track it
5525+
now -- before the only fallible allocation below. This keeps the OOM
5526+
error path from deallocating a not-yet-tracked iterator. */
5527+
_PyObject_GC_TRACK(di);
55235528
if (itertype == &PyDictIterItem_Type ||
55245529
itertype == &PyDictRevIterItem_Type) {
55255530
di->di_result = _PyTuple_FromPairSteal(Py_None, Py_None);
@@ -5528,10 +5533,6 @@ dictiter_new(PyDictObject *dict, PyTypeObject *itertype)
55285533
return NULL;
55295534
}
55305535
}
5531-
else {
5532-
di->di_result = NULL;
5533-
}
5534-
_PyObject_GC_TRACK(di);
55355536
return (PyObject *)di;
55365537
}
55375538

0 commit comments

Comments
 (0)