Skip to content

Commit ee12a34

Browse files
colesburyYhg1s
andauthored
[3.13] gh-129967: Fix race condition in repr(set) (gh-129978) (gh-130020)
The call to `PySequence_List()` could temporarily unlock and relock the set, allowing the items to be cleared and return the incorrect notation `{}` for a empty set (it should be `set()`). (cherry picked from commit a7427f2) Co-authored-by: T. Wouters <thomas@python.org>
1 parent 3679083 commit ee12a34

File tree

3 files changed

+54
-2
lines changed

3 files changed

+54
-2
lines changed
+41
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import unittest
2+
3+
from threading import Thread, Barrier
4+
from unittest import TestCase
5+
6+
from test.support import threading_helper
7+
8+
9+
@threading_helper.requires_working_threading()
10+
class TestSet(TestCase):
11+
def test_repr_clear(self):
12+
"""Test repr() of a set while another thread is calling clear()"""
13+
NUM_ITERS = 10
14+
NUM_REPR_THREADS = 10
15+
barrier = Barrier(NUM_REPR_THREADS + 1)
16+
s = {1, 2, 3, 4, 5, 6, 7, 8}
17+
18+
def clear_set():
19+
barrier.wait()
20+
s.clear()
21+
22+
def repr_set():
23+
barrier.wait()
24+
set_reprs.append(repr(s))
25+
26+
for _ in range(NUM_ITERS):
27+
set_reprs = []
28+
threads = [Thread(target=clear_set)]
29+
for _ in range(NUM_REPR_THREADS):
30+
threads.append(Thread(target=repr_set))
31+
for t in threads:
32+
t.start()
33+
for t in threads:
34+
t.join()
35+
36+
for set_repr in set_reprs:
37+
self.assertIn(set_repr, ("set()", "{1, 2, 3, 4, 5, 6, 7, 8}"))
38+
39+
40+
if __name__ == "__main__":
41+
unittest.main()
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Fix a race condition in the :term:`free threading` build when ``repr(set)``
2+
is called concurrently with ``set.clear()``.

Objects/setobject.c

+11-2
Original file line numberDiff line numberDiff line change
@@ -531,9 +531,18 @@ set_repr_lock_held(PySetObject *so)
531531
return PyUnicode_FromFormat("%s()", Py_TYPE(so)->tp_name);
532532
}
533533

534-
keys = PySequence_List((PyObject *)so);
535-
if (keys == NULL)
534+
// gh-129967: avoid PySequence_List because it might re-lock the object
535+
// lock or the GIL and allow something to clear the set from underneath us.
536+
keys = PyList_New(so->used);
537+
if (keys == NULL) {
536538
goto done;
539+
}
540+
541+
Py_ssize_t pos = 0, idx = 0;
542+
setentry *entry;
543+
while (set_next(so, &pos, &entry)) {
544+
PyList_SET_ITEM(keys, idx++, Py_NewRef(entry->key));
545+
}
537546

538547
/* repr(keys)[1:-1] */
539548
listrepr = PyObject_Repr(keys);

0 commit comments

Comments
 (0)