From ab9f2f4cc912e0bdd7b24a2139cd01d026e31096 Mon Sep 17 00:00:00 2001 From: Thomas Kowalski Date: Mon, 11 May 2026 14:46:11 +0200 Subject: [PATCH 1/7] fix: properly initialise frozendict's hash value --- Objects/dictobject.c | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/Objects/dictobject.c b/Objects/dictobject.c index 42bc63acd9049c..fea65129dab55a 100644 --- a/Objects/dictobject.c +++ b/Objects/dictobject.c @@ -940,7 +940,13 @@ new_frozendict(PyDictKeysObject *keys, PyDictValues *values, Py_ssize_t used, int free_values_on_failure) { PyDictObject *mp = PyObject_GC_New(PyDictObject, &PyFrozenDict_Type); - return new_dict_impl(mp, keys, values, used, free_values_on_failure); + PyObject *result = new_dict_impl(mp, keys, values, used, free_values_on_failure); + if (result != NULL) { + /* ma_hash must be -1 (sentinel for "not computed") since PyObject_GC_New + does not zero-initialize memory and new_dict_impl does not touch ma_hash. */ + _PyFrozenDictObject_CAST(result)->ma_hash = -1; + } + return result; } static PyObject * From 0b39d23b0ff606b19fa473a72af5a2375f87b773 Mon Sep 17 00:00:00 2001 From: Thomas Kowalski Date: Mon, 11 May 2026 14:49:30 +0200 Subject: [PATCH 2/7] misc: add news entry --- .../2026-05-11-14-48-56.gh-issue-149676.6aTrw1.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 Misc/NEWS.d/next/Core_and_Builtins/2026-05-11-14-48-56.gh-issue-149676.6aTrw1.rst diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2026-05-11-14-48-56.gh-issue-149676.6aTrw1.rst b/Misc/NEWS.d/next/Core_and_Builtins/2026-05-11-14-48-56.gh-issue-149676.6aTrw1.rst new file mode 100644 index 00000000000000..bb5e83aa8a1b91 --- /dev/null +++ b/Misc/NEWS.d/next/Core_and_Builtins/2026-05-11-14-48-56.gh-issue-149676.6aTrw1.rst @@ -0,0 +1 @@ +Fix :class:`frozendict` instances being created with an uninitialised hash value, which could cause incorrect hash lookups or crashes. From 4ca9e476b74f025a6a48d58a21acac28d6331ae4 Mon Sep 17 00:00:00 2001 From: Thomas Kowalski Date: Mon, 11 May 2026 16:44:42 +0200 Subject: [PATCH 3/7] test: add regression test --- Lib/test/test_dict.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/Lib/test/test_dict.py b/Lib/test/test_dict.py index b2f4363b23e748..d37b47a5432826 100644 --- a/Lib/test/test_dict.py +++ b/Lib/test/test_dict.py @@ -1902,6 +1902,17 @@ def test_hash(self): with self.assertRaisesRegex(TypeError, "unhashable type: 'list'"): hash(fd) + def test_hash_pipe_operator(self): + # gh-149676: frozendict created via | must have the same hash as one + # created directly with the same contents (ma_hash must be initialised + # to -1 so that the hash is computed on first call, not left as garbage) + a = frozendict({"a": 1}) + b = frozendict({"b": 2}) + c = frozendict({"a": 1, "b": 2}) + c_union = a | b + self.assertEqual(c, c_union) + self.assertEqual(hash(c), hash(c_union)) + def test_fromkeys(self): self.assertEqual(frozendict.fromkeys('abc'), frozendict(a=None, b=None, c=None)) From b7c1ce6985183f423869d4c212b6a88e132b3e31 Mon Sep 17 00:00:00 2001 From: Thomas Kowalski Date: Tue, 12 May 2026 10:42:37 +0200 Subject: [PATCH 4/7] review: update regression test Co-authored-by: Victor Stinner --- Lib/test/test_dict.py | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/Lib/test/test_dict.py b/Lib/test/test_dict.py index d37b47a5432826..d73253e02ddbaf 100644 --- a/Lib/test/test_dict.py +++ b/Lib/test/test_dict.py @@ -1903,15 +1903,10 @@ def test_hash(self): hash(fd) def test_hash_pipe_operator(self): - # gh-149676: frozendict created via | must have the same hash as one - # created directly with the same contents (ma_hash must be initialised - # to -1 so that the hash is computed on first call, not left as garbage) + # gh-149676: Test hash(frozendict | frozendict) a = frozendict({"a": 1}) b = frozendict({"b": 2}) - c = frozendict({"a": 1, "b": 2}) - c_union = a | b - self.assertEqual(c, c_union) - self.assertEqual(hash(c), hash(c_union)) + self.assertEqual(hash(a | b), hash(frozendict({"a": 1, "b": 2}))) def test_fromkeys(self): self.assertEqual(frozendict.fromkeys('abc'), From bd77be2b1867349f67dc8ae4a699f445268e36ac Mon Sep 17 00:00:00 2001 From: Thomas Kowalski Date: Tue, 12 May 2026 10:43:54 +0200 Subject: [PATCH 5/7] news: update news entry --- .../2026-05-11-14-48-56.gh-issue-149676.6aTrw1.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2026-05-11-14-48-56.gh-issue-149676.6aTrw1.rst b/Misc/NEWS.d/next/Core_and_Builtins/2026-05-11-14-48-56.gh-issue-149676.6aTrw1.rst index bb5e83aa8a1b91..4b01f0af46cb76 100644 --- a/Misc/NEWS.d/next/Core_and_Builtins/2026-05-11-14-48-56.gh-issue-149676.6aTrw1.rst +++ b/Misc/NEWS.d/next/Core_and_Builtins/2026-05-11-14-48-56.gh-issue-149676.6aTrw1.rst @@ -1 +1 @@ -Fix :class:`frozendict` instances being created with an uninitialised hash value, which could cause incorrect hash lookups or crashes. +Fix frozendict | frozendict hash. From 06ae01dfe19820aef9d62993dec9b2cbce8b39e3 Mon Sep 17 00:00:00 2001 From: Thomas Kowalski Date: Tue, 12 May 2026 10:47:55 +0200 Subject: [PATCH 6/7] refactor: add frozendict parameter to new_dict_impl --- Lib/test/test_dict.py | 11 +++++------ Objects/dictobject.c | 15 ++++++--------- 2 files changed, 11 insertions(+), 15 deletions(-) diff --git a/Lib/test/test_dict.py b/Lib/test/test_dict.py index d73253e02ddbaf..4efb066d4fd01c 100644 --- a/Lib/test/test_dict.py +++ b/Lib/test/test_dict.py @@ -1868,6 +1868,11 @@ def test_merge(self): self.assertEqual(fd | {}, fd) self.assertEqual(frozendict() | fd, fd) + # gh-149676: Test hash(frozendict | frozendict) + a = frozendict({"a": 1}) + b = frozendict({"b": 2}) + self.assertEqual(hash(a | b), hash(frozendict({"a": 1, "b": 2}))) + def test_update(self): # test "a |= b" operator d = frozendict(x=1) @@ -1902,12 +1907,6 @@ def test_hash(self): with self.assertRaisesRegex(TypeError, "unhashable type: 'list'"): hash(fd) - def test_hash_pipe_operator(self): - # gh-149676: Test hash(frozendict | frozendict) - a = frozendict({"a": 1}) - b = frozendict({"b": 2}) - self.assertEqual(hash(a | b), hash(frozendict({"a": 1, "b": 2}))) - def test_fromkeys(self): self.assertEqual(frozendict.fromkeys('abc'), frozendict(a=None, b=None, c=None)) diff --git a/Objects/dictobject.c b/Objects/dictobject.c index fea65129dab55a..fe1f3b515b537e 100644 --- a/Objects/dictobject.c +++ b/Objects/dictobject.c @@ -900,7 +900,7 @@ free_values(PyDictValues *values, bool use_qsbr) static inline PyObject * new_dict_impl(PyDictObject *mp, PyDictKeysObject *keys, PyDictValues *values, Py_ssize_t used, - int free_values_on_failure) + int free_values_on_failure, int frozendict) { assert(keys != NULL); if (mp == NULL) { @@ -915,6 +915,9 @@ new_dict_impl(PyDictObject *mp, PyDictKeysObject *keys, mp->ma_values = values; mp->ma_used = used; mp->_ma_watcher_tag = 0; + if (frozendict) { + ((PyFrozenDictObject *)mp)->ma_hash = -1; + } ASSERT_CONSISTENT(mp); _PyObject_GC_TRACK(mp); return (PyObject *)mp; @@ -931,7 +934,7 @@ new_dict(PyDictKeysObject *keys, PyDictValues *values, } assert(mp == NULL || Py_IS_TYPE(mp, &PyDict_Type)); - return new_dict_impl(mp, keys, values, used, free_values_on_failure); + return new_dict_impl(mp, keys, values, used, free_values_on_failure, 0); } /* Consumes a reference to the keys object */ @@ -940,13 +943,7 @@ new_frozendict(PyDictKeysObject *keys, PyDictValues *values, Py_ssize_t used, int free_values_on_failure) { PyDictObject *mp = PyObject_GC_New(PyDictObject, &PyFrozenDict_Type); - PyObject *result = new_dict_impl(mp, keys, values, used, free_values_on_failure); - if (result != NULL) { - /* ma_hash must be -1 (sentinel for "not computed") since PyObject_GC_New - does not zero-initialize memory and new_dict_impl does not touch ma_hash. */ - _PyFrozenDictObject_CAST(result)->ma_hash = -1; - } - return result; + return new_dict_impl(mp, keys, values, used, free_values_on_failure, 1); } static PyObject * From a67ee6f829c3c0117c139989b170138daeb106a6 Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Tue, 12 May 2026 12:18:51 +0200 Subject: [PATCH 7/7] Update Misc/NEWS.d/next/Core_and_Builtins/2026-05-11-14-48-56.gh-issue-149676.6aTrw1.rst --- .../2026-05-11-14-48-56.gh-issue-149676.6aTrw1.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2026-05-11-14-48-56.gh-issue-149676.6aTrw1.rst b/Misc/NEWS.d/next/Core_and_Builtins/2026-05-11-14-48-56.gh-issue-149676.6aTrw1.rst index 4b01f0af46cb76..96f407cf5ad25a 100644 --- a/Misc/NEWS.d/next/Core_and_Builtins/2026-05-11-14-48-56.gh-issue-149676.6aTrw1.rst +++ b/Misc/NEWS.d/next/Core_and_Builtins/2026-05-11-14-48-56.gh-issue-149676.6aTrw1.rst @@ -1 +1 @@ -Fix frozendict | frozendict hash. +Fix ``frozendict | frozendict`` hash.