From f6d8284f5ac856954149d9302e349e9e52a9f1b0 Mon Sep 17 00:00:00 2001 From: Bhuvansh Kataria Date: Sun, 26 Jul 2026 09:02:23 +0000 Subject: [PATCH 1/5] gh-154709: Fix out-of-bounds access in dict reverse iterator --- Lib/test/test_dict.py | 15 +++++++++++++++ ...2026-07-26-09-07-41.gh-issue-154709.M2uZ76.rst | 2 ++ Objects/dictobject.c | 10 ++++++++++ 3 files changed, 27 insertions(+) create mode 100644 Misc/NEWS.d/next/Core_and_Builtins/2026-07-26-09-07-41.gh-issue-154709.M2uZ76.rst diff --git a/Lib/test/test_dict.py b/Lib/test/test_dict.py index dc31d403b837adb..065f5672681c6ae 100644 --- a/Lib/test/test_dict.py +++ b/Lib/test/test_dict.py @@ -1403,6 +1403,21 @@ def __init__(self, x, y): self.assertEqual(list(reversed(A(1, 0).__dict__)), ['x']) self.assertEqual(list(reversed(A(0, 1).__dict__)), ['y']) + def test_reversed_dict_after_clear_and_restore(self): + d = {} + for i in range(1000): + d[f"k{i}"] = i + + for i in range(1, 1000): + del d[f"k{i}"] + + it = reversed(d) + + d.clear() + d["k0"] = 0 + + self.assertEqual(list(it), []) + def test_dict_copy_order(self): # bpo-34320 od = collections.OrderedDict([('a', 1), ('b', 2)]) diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2026-07-26-09-07-41.gh-issue-154709.M2uZ76.rst b/Misc/NEWS.d/next/Core_and_Builtins/2026-07-26-09-07-41.gh-issue-154709.M2uZ76.rst new file mode 100644 index 000000000000000..6eb1ce6a8b6970b --- /dev/null +++ b/Misc/NEWS.d/next/Core_and_Builtins/2026-07-26-09-07-41.gh-issue-154709.M2uZ76.rst @@ -0,0 +1,2 @@ +Fix an out-of-bounds access in reverse dictionary iterators when the +underlying dictionary is cleared and modified after the iterator is created. diff --git a/Objects/dictobject.c b/Objects/dictobject.c index c650aa456d2cc9d..8bb564a499be403 100644 --- a/Objects/dictobject.c +++ b/Objects/dictobject.c @@ -6272,13 +6272,23 @@ dictreviter_iter_lock_held(PyDictObject *d, PyObject *self) if (i < 0) { goto fail; } + if (_PyDict_HasSplitTable(d)) { + if (i >= d->ma_used) { + goto fail; + } + int index = get_index_from_order(d, i); key = LOAD_SHARED_KEY(DK_UNICODE_ENTRIES(k)[index].me_key); value = d->ma_values->values[index]; assert (value != NULL); } else { + Py_ssize_t n = k->dk_nentries; + if (i >= n) { + goto fail; + } + if (DK_IS_UNICODE(k)) { PyDictUnicodeEntry *entry_ptr = &DK_UNICODE_ENTRIES(k)[i]; while (entry_ptr->me_value == NULL) { From 2433f378f0567ba11b00023f1e7888e8f7146f1f Mon Sep 17 00:00:00 2001 From: Bhuvansh Kataria Date: Wed, 29 Jul 2026 13:44:23 +0000 Subject: [PATCH 2/5] gh-154709: Address review comments --- Objects/dictobject.c | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/Objects/dictobject.c b/Objects/dictobject.c index 8bb564a499be403..93cd3dadbe59171 100644 --- a/Objects/dictobject.c +++ b/Objects/dictobject.c @@ -6284,11 +6284,9 @@ dictreviter_iter_lock_held(PyDictObject *d, PyObject *self) assert (value != NULL); } else { - Py_ssize_t n = k->dk_nentries; - if (i >= n) { + if (i >= k->dk_nentries) { goto fail; } - if (DK_IS_UNICODE(k)) { PyDictUnicodeEntry *entry_ptr = &DK_UNICODE_ENTRIES(k)[i]; while (entry_ptr->me_value == NULL) { From 19c842c679cf24b7e332588de441872a8883fc24 Mon Sep 17 00:00:00 2001 From: Bhuvansh Kataria Date: Wed, 29 Jul 2026 14:41:48 +0000 Subject: [PATCH 3/5] gh-154709: Remove redundant reverse iterator check --- Objects/dictobject.c | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/Objects/dictobject.c b/Objects/dictobject.c index 93cd3dadbe59171..9b5b84aaf0c6684 100644 --- a/Objects/dictobject.c +++ b/Objects/dictobject.c @@ -6274,14 +6274,10 @@ dictreviter_iter_lock_held(PyDictObject *d, PyObject *self) } if (_PyDict_HasSplitTable(d)) { - if (i >= d->ma_used) { - goto fail; - } - int index = get_index_from_order(d, i); key = LOAD_SHARED_KEY(DK_UNICODE_ENTRIES(k)[index].me_key); value = d->ma_values->values[index]; - assert (value != NULL); + assert(value != NULL); } else { if (i >= k->dk_nentries) { From 2929ecafc3d00a48cf54b9fdd2abc7cb2889bdf5 Mon Sep 17 00:00:00 2001 From: Bhuvansh Kataria Date: Wed, 29 Jul 2026 14:45:51 +0000 Subject: [PATCH 4/5] gh-154709: Test reversed dict views --- Lib/test/test_dict.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/Lib/test/test_dict.py b/Lib/test/test_dict.py index 065f5672681c6ae..1e665c86303078c 100644 --- a/Lib/test/test_dict.py +++ b/Lib/test/test_dict.py @@ -1411,12 +1411,18 @@ def test_reversed_dict_after_clear_and_restore(self): for i in range(1, 1000): del d[f"k{i}"] - it = reversed(d) + iterators = ( + reversed(d), + reversed(d.keys()), + reversed(d.values()), + reversed(d.items()), + ) d.clear() d["k0"] = 0 - self.assertEqual(list(it), []) + for it in iterators: + self.assertEqual(list(it), []) def test_dict_copy_order(self): # bpo-34320 From f89a81d4dfb2372136d6a3722b75e5e0ca220682 Mon Sep 17 00:00:00 2001 From: Bhuvansh Date: Wed, 29 Jul 2026 20:25:14 +0530 Subject: [PATCH 5/5] Update Objects/dictobject.c Co-authored-by: Serhiy Storchaka --- Objects/dictobject.c | 1 - 1 file changed, 1 deletion(-) diff --git a/Objects/dictobject.c b/Objects/dictobject.c index 9b5b84aaf0c6684..74b6d5d779a064c 100644 --- a/Objects/dictobject.c +++ b/Objects/dictobject.c @@ -6272,7 +6272,6 @@ dictreviter_iter_lock_held(PyDictObject *d, PyObject *self) if (i < 0) { goto fail; } - if (_PyDict_HasSplitTable(d)) { int index = get_index_from_order(d, i); key = LOAD_SHARED_KEY(DK_UNICODE_ENTRIES(k)[index].me_key);