From cfdeb5c14589224215a6455674ebf9261defd13a Mon Sep 17 00:00:00 2001 From: Keming Date: Thu, 27 Nov 2025 17:42:43 +0800 Subject: [PATCH 1/3] fix: avoid increase the set->used twice when the __eq__ trigger another add Signed-off-by: Keming --- .../2025-11-27-17-47-09.gh-issue-141805.TT0biF.rst | 2 ++ Objects/setobject.c | 1 + 2 files changed, 3 insertions(+) create mode 100644 Misc/NEWS.d/next/Core_and_Builtins/2025-11-27-17-47-09.gh-issue-141805.TT0biF.rst diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2025-11-27-17-47-09.gh-issue-141805.TT0biF.rst b/Misc/NEWS.d/next/Core_and_Builtins/2025-11-27-17-47-09.gh-issue-141805.TT0biF.rst new file mode 100644 index 00000000000000..72ffbcf83ba48e --- /dev/null +++ b/Misc/NEWS.d/next/Core_and_Builtins/2025-11-27-17-47-09.gh-issue-141805.TT0biF.rst @@ -0,0 +1,2 @@ +Avoid increase the PySetOjbect->used twice when adding one element triggers +__eq__ that cause another set.add diff --git a/Objects/setobject.c b/Objects/setobject.c index 85f4d7d403178a..3a8a5e3e7ecd56 100644 --- a/Objects/setobject.c +++ b/Objects/setobject.c @@ -180,6 +180,7 @@ set_add_entry_takeref(PySetObject *so, PyObject *key, Py_hash_t hash) else if (entry->hash == -1) { assert (entry->key == dummy); freeslot = entry; + goto found_unused_or_dummy; } entry++; } while (probes--); From 4a2440db3cf5763dc6bd60e7f47e157257e7e7ea Mon Sep 17 00:00:00 2001 From: Keming Date: Thu, 27 Nov 2025 18:11:29 +0800 Subject: [PATCH 2/3] add testcase for dummy slot Signed-off-by: Keming --- Lib/test/test_set.py | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/Lib/test/test_set.py b/Lib/test/test_set.py index c0df9507bd7f5e..951225ddc24f2b 100644 --- a/Lib/test/test_set.py +++ b/Lib/test/test_set.py @@ -675,6 +675,33 @@ def __hash__(self): with self.assertRaises(KeyError): myset.discard(elem2) + def test_set_add_to_dummy_slot(self): + # gh-141805 + tasks = set() + + class Dummy: + def __hash__(self): + return 0 + + class CorruptTrigger: + triggered = False + + def __hash__(self): + return 0 + + def __eq__(self, value): + if not self.triggered: + self.triggered = True + tasks.add(self) + return False + + tasks.add(Dummy()) + tasks.add(Dummy()) + tasks.pop() + self.assertEqual(len(tasks), 1) + tasks.add(CorruptTrigger()) + self.assertEqual(len(tasks), 2) + class SetSubclass(set): pass From 08f7e68ab3c01971005d38657131d0db5e10498f Mon Sep 17 00:00:00 2001 From: Keming Date: Fri, 28 Nov 2025 08:13:02 +0800 Subject: [PATCH 3/3] polish the NEWS to make it easy to understand for users Signed-off-by: Keming --- .../2025-11-27-17-47-09.gh-issue-141805.TT0biF.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2025-11-27-17-47-09.gh-issue-141805.TT0biF.rst b/Misc/NEWS.d/next/Core_and_Builtins/2025-11-27-17-47-09.gh-issue-141805.TT0biF.rst index 72ffbcf83ba48e..9b4f32508f1dd2 100644 --- a/Misc/NEWS.d/next/Core_and_Builtins/2025-11-27-17-47-09.gh-issue-141805.TT0biF.rst +++ b/Misc/NEWS.d/next/Core_and_Builtins/2025-11-27-17-47-09.gh-issue-141805.TT0biF.rst @@ -1,2 +1,2 @@ -Avoid increase the PySetOjbect->used twice when adding one element triggers -__eq__ that cause another set.add +Fix set length corruption when set additions occur within +element comparison (:meth:`object.__eq__`) methods.