|
5 | 5 |
|
6 | 6 | from ast import Or |
7 | 7 | from functools import partial |
8 | | -from threading import Barrier, Thread |
| 8 | +from threading import Barrier, Event, Thread |
9 | 9 | from unittest import TestCase |
10 | 10 |
|
11 | 11 | try: |
@@ -336,5 +336,50 @@ def reader(): |
336 | 336 | with threading_helper.start_threads([t1, t2]): |
337 | 337 | pass |
338 | 338 |
|
| 339 | + def test_racing_frozendict_reads_during_construction(self): |
| 340 | + # gh-151722: a frozendict is GC-tracked before it is fully |
| 341 | + # populated, so a half-built instance is observable from other |
| 342 | + # threads. Reading it with len()/repr()/hash() must not race |
| 343 | + # with the construction-time table and ma_used writes. |
| 344 | + NUM_KEYS = 8192 |
| 345 | + NUM_ROUNDS = 40 |
| 346 | + |
| 347 | + latest = [frozendict()] # main -> reader handoff, never empty |
| 348 | + done = Event() |
| 349 | + |
| 350 | + class Evil: |
| 351 | + def keys(self): |
| 352 | + return [f"k{i}" for i in range(NUM_KEYS)] |
| 353 | + |
| 354 | + def __getitem__(self, key): |
| 355 | + if latest[0] is empty: |
| 356 | + for obj in gc.get_objects(): |
| 357 | + if (isinstance(obj, frozendict) |
| 358 | + and 0 < len(obj) < NUM_KEYS): |
| 359 | + latest[0] = obj # leak the half-built object |
| 360 | + break |
| 361 | + return 1 |
| 362 | + |
| 363 | + empty = latest[0] |
| 364 | + |
| 365 | + def reader(): |
| 366 | + while not done.is_set(): |
| 367 | + fd = latest[0] |
| 368 | + len(fd) |
| 369 | + repr(fd) |
| 370 | + hash(fd) |
| 371 | + |
| 372 | + readers = [Thread(target=reader) for _ in range(3)] |
| 373 | + for t in readers: |
| 374 | + t.start() |
| 375 | + try: |
| 376 | + for _ in range(NUM_ROUNDS): |
| 377 | + latest[0] = empty |
| 378 | + frozendict(Evil()) |
| 379 | + finally: |
| 380 | + done.set() |
| 381 | + for t in readers: |
| 382 | + t.join() |
| 383 | + |
339 | 384 | if __name__ == "__main__": |
340 | 385 | unittest.main() |
0 commit comments