Skip to content

Commit a8e8473

Browse files
zangjiuchengclaude
andcommitted
gh-152107: Guard GC untrack on untracked dict iterator under OOM
dictiter_new() allocates di->di_result via _PyTuple_FromPairSteal() for item iterators before calling _PyObject_GC_TRACK(di). When that allocation fails (OOM), the error path runs Py_DECREF(di) on the still-untracked iterator, and dictiter_dealloc() unconditionally called _PyObject_GC_UNTRACK(di) -- which asserts the object is GC-tracked. This aborts on a debug build (Objects/dictobject.c) and corrupts the GC list (later segfault) on a release build. Guard the untrack with _PyObject_GC_IS_TRACKED() so the never-tracked iterator is freed cleanly. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent a46db4f commit a8e8473

3 files changed

Lines changed: 35 additions & 3 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: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5539,8 +5539,13 @@ static void
55395539
dictiter_dealloc(PyObject *self)
55405540
{
55415541
dictiterobject *di = (dictiterobject *)self;
5542-
/* bpo-31095: UnTrack is needed before calling any callbacks */
5543-
_PyObject_GC_UNTRACK(di);
5542+
/* bpo-31095: UnTrack is needed before calling any callbacks.
5543+
The iterator may not be GC-tracked yet if dictiter_new() failed to
5544+
allocate di_result and decref'd the partially built iterator before
5545+
calling _PyObject_GC_TRACK(); guard against untracking it twice. */
5546+
if (_PyObject_GC_IS_TRACKED(di)) {
5547+
_PyObject_GC_UNTRACK(di);
5548+
}
55445549
Py_XDECREF(di->di_dict);
55455550
Py_XDECREF(di->di_result);
55465551
PyObject_GC_Del(di);

0 commit comments

Comments
 (0)