Skip to content

Commit 8a4795a

Browse files
committed
gh-151722: Add free-threading regression test for frozendict read race
1 parent 675a368 commit 8a4795a

1 file changed

Lines changed: 57 additions & 0 deletions

File tree

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import gc
2+
import unittest
3+
from threading import Event, Thread
4+
5+
from test.support import threading_helper
6+
7+
8+
@threading_helper.requires_working_threading()
9+
class TestFrozenDict(unittest.TestCase):
10+
def test_racing_reads_during_construction(self):
11+
# gh-151722: a frozendict is GC-tracked before it is fully
12+
# populated, so a half-built instance is observable from other
13+
# threads. Reading it with len()/repr()/hash() must not race
14+
# with the construction-time table and ma_used writes.
15+
NUM_KEYS = 8192
16+
NUM_ROUNDS = 40
17+
18+
latest = [frozendict()] # main -> reader handoff, never empty
19+
done = Event()
20+
21+
class Evil:
22+
def keys(self):
23+
return [f"k{i}" for i in range(NUM_KEYS)]
24+
25+
def __getitem__(self, key):
26+
if latest[0] is empty:
27+
for obj in gc.get_objects():
28+
if (isinstance(obj, frozendict)
29+
and 0 < len(obj) < NUM_KEYS):
30+
latest[0] = obj # leak the half-built object
31+
break
32+
return 1
33+
34+
empty = latest[0]
35+
36+
def reader():
37+
while not done.is_set():
38+
fd = latest[0]
39+
len(fd)
40+
repr(fd)
41+
hash(fd)
42+
43+
readers = [Thread(target=reader) for _ in range(3)]
44+
for t in readers:
45+
t.start()
46+
try:
47+
for _ in range(NUM_ROUNDS):
48+
latest[0] = empty
49+
frozendict(Evil())
50+
finally:
51+
done.set()
52+
for t in readers:
53+
t.join()
54+
55+
56+
if __name__ == "__main__":
57+
unittest.main()

0 commit comments

Comments
 (0)