https://github.com/python/cpython/commit/ec8a4201f804e69d4630cc1c74ff44db8c6d6efa
commit: ec8a4201f804e69d4630cc1c74ff44db8c6d6efa
branch: main
author: Thomas Kowalski <[email protected]>
committer: serhiy-storchaka <[email protected]>
date: 2026-08-30T10:29:31+03:00
summary:
gh-148442: Eliminate race condition in list rich comparison (GH-148531)
files:
A
Misc/NEWS.d/next/Core_and_Builtins/2026-04-13-00-00-00.gh-issue-148442.aBcDeF.rst
M Lib/test/test_list.py
M Objects/listobject.c
diff --git a/Lib/test/test_list.py b/Lib/test/test_list.py
index 44ecd62d6735def..1b0724c2b1c99dc 100644
--- a/Lib/test/test_list.py
+++ b/Lib/test/test_list.py
@@ -244,7 +244,7 @@ def __eq__(self, other):
list1 = [X()]
list2 = [Y()]
- self.assertTrue(list1 == list2)
+ self.assertFalse(list1 == list2)
list3 = [Z()]
list4 = [1]
@@ -261,6 +261,54 @@ def __lt__(self, other):
with self.assertRaises(TypeError):
a[0] < a
+ def test_richcompare_stale_element_list_vitem(self):
+ # gh-148442: list_richcompare_impl must use the captured vitem for
+ # the final ordering comparison, not re-read list1's slot after __eq__
+ # may have mutated it.
+ #
+ # x.__eq__(0) puts AlwaysLT() into list1[0] and returns False.
+ class AlwaysLT:
+ def __eq__(self, other: object) -> bool:
+ return False
+
+ def __gt__(self, other: object) -> bool:
+ return False
+
+ class Mutating:
+ def __eq__(self, other: object) -> bool:
+ list1[0] = AlwaysLT()
+ return False
+
+ def __gt__(self, other: object) -> bool:
+ return True
+
+ list1 = [Mutating(), 0]
+ list2 = [0, 0]
+ self.assertTrue(list1 > list2)
+
+ def test_richcompare_stale_element_list_witem(self):
+ # gh-148442: list_richcompare_impl must use the captured witem for
+ # the final ordering comparison, not re-read list2's slot after __eq__
+ # may have mutated it.
+ #
+ # x.__eq__(0) puts AlwaysGT() into list2[0] and returns False.
+ class AlwaysGT:
+ pass
+
+ class Mutating:
+ def __eq__(self, other: object) -> bool:
+ list2[0] = AlwaysGT()
+ return False
+
+ def __gt__(self, other: object) -> bool:
+ if isinstance(other, AlwaysGT):
+ return False # pretend AlwaysGT beats us
+ return True # beat everything else (including 0)
+
+ list1 = [Mutating(), 0]
+ list2 = [0, 0]
+ self.assertTrue(list1 > list2)
+
def test_list_index_modifing_operand(self):
# See gh-120384
class evil:
diff --git
a/Misc/NEWS.d/next/Core_and_Builtins/2026-04-13-00-00-00.gh-issue-148442.aBcDeF.rst
b/Misc/NEWS.d/next/Core_and_Builtins/2026-04-13-00-00-00.gh-issue-148442.aBcDeF.rst
new file mode 100644
index 000000000000000..e30a3dee6f54dd7
--- /dev/null
+++
b/Misc/NEWS.d/next/Core_and_Builtins/2026-04-13-00-00-00.gh-issue-148442.aBcDeF.rst
@@ -0,0 +1,4 @@
+Fix a race condition in ``list.__lt__``, ``list.__le__``, ``list.__gt__``,
+and ``list.__ge__``. Previously, under concurrent list mutation, the items
+being compared could be replaced with other objects before the final
comparison,
+causing the final comparison result to be incorrect.
diff --git a/Objects/listobject.c b/Objects/listobject.c
index 8a9c9bda68269b8..81eb3e1102159b5 100644
--- a/Objects/listobject.c
+++ b/Objects/listobject.c
@@ -3454,7 +3454,10 @@ list_richcompare_impl(PyObject *v, PyObject *w, int op)
Py_RETURN_TRUE;
}
- /* Search for the first index where items are different */
+ /* Search for the first index where items are different.
+ * We incref vitem/witem before calling PyObject_RichCompareBool, which may
+ * release the GIL and allow the list to be mutated in the meantime.
+ */
for (i = 0; i < Py_SIZE(vl) && i < Py_SIZE(wl); i++) {
PyObject *vitem = vl->ob_item[i];
PyObject *witem = wl->ob_item[i];
@@ -3465,36 +3468,36 @@ list_richcompare_impl(PyObject *v, PyObject *w, int op)
Py_INCREF(vitem);
Py_INCREF(witem);
int k = PyObject_RichCompareBool(vitem, witem, Py_EQ);
- Py_DECREF(vitem);
- Py_DECREF(witem);
- if (k < 0)
+ if (k < 0) {
+ Py_DECREF(vitem);
+ Py_DECREF(witem);
return NULL;
- if (!k)
- break;
- }
-
- if (i >= Py_SIZE(vl) || i >= Py_SIZE(wl)) {
- /* No more items to compare -- compare sizes */
- Py_RETURN_RICHCOMPARE(Py_SIZE(vl), Py_SIZE(wl), op);
- }
+ }
+ if (!k) {
+ /* We have a differing item -- shortcuts for EQ/NE */
+ if (op == Py_EQ) {
+ Py_DECREF(vitem);
+ Py_DECREF(witem);
+ Py_RETURN_FALSE;
+ }
+ if (op == Py_NE) {
+ Py_DECREF(vitem);
+ Py_DECREF(witem);
+ Py_RETURN_TRUE;
+ }
+ /* Compare the differing items using the proper operator */
+ PyObject *result = PyObject_RichCompare(vitem, witem, op);
+ Py_DECREF(vitem);
+ Py_DECREF(witem);
+ return result;
+ }
- /* We have an item that differs -- shortcuts for EQ/NE */
- if (op == Py_EQ) {
- Py_RETURN_FALSE;
- }
- if (op == Py_NE) {
- Py_RETURN_TRUE;
+ Py_DECREF(vitem);
+ Py_DECREF(witem);
}
- /* Compare the final item again using the proper operator */
- PyObject *vitem = vl->ob_item[i];
- PyObject *witem = wl->ob_item[i];
- Py_INCREF(vitem);
- Py_INCREF(witem);
- PyObject *result = PyObject_RichCompare(vl->ob_item[i], wl->ob_item[i],
op);
- Py_DECREF(vitem);
- Py_DECREF(witem);
- return result;
+ /* All compared elements were equal -- compare sizes */
+ Py_RETURN_RICHCOMPARE(Py_SIZE(vl), Py_SIZE(wl), op);
}
static PyObject *
_______________________________________________
Python-checkins mailing list -- [email protected]
To unsubscribe send an email to [email protected]
https://mail.python.org/mailman3//lists/python-checkins.python.org
Member address: [email protected]