Skip to content

Commit c7cb736

Browse files
committed
Initial commit
1 parent 517d3d2 commit c7cb736

2 files changed

Lines changed: 57 additions & 0 deletions

File tree

Lib/test/test_thread.py

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -345,6 +345,48 @@ def func():
345345
handle = thread.start_joinable_thread(func, handle=None)
346346
handle.join()
347347

348+
class StartNewThreadKwargsRace(unittest.TestCase):
349+
350+
@unittest.skipUnless(support.Py_GIL_DISABLED, "GIL must be disabled")
351+
def test_dict_growsup_when_thread_start(self):
352+
# See gh-149816 - (62) Concurrent kwargs growth causes heap overwrite
353+
# This test is meant to be run under a free-threaded build, where the GIL is
354+
# disabled and concurrent mutations of the same dict can cause heap
355+
# corruption.
356+
results = []
357+
def mutator(shared, stop, prefix, burst):
358+
i = 0
359+
while not stop.locked():
360+
for _ in range(burst):
361+
shared[f"{prefix}_{i}"] = i
362+
i += 1
363+
time.sleep(0)
364+
results.append(prefix)
365+
366+
def nop(i, **kwargs):
367+
pass
368+
369+
DELAY = 1.0
370+
stop = thread.lock()
371+
shared = {f"base_{i}": i for i in range(20000)}
372+
n = 4
373+
for i in range(n):
374+
args=(shared, stop, f"dynamic_{i}", 1000)
375+
thread.start_new_thread(mutator, args)
376+
377+
snt = 32
378+
for i in range(snt):
379+
try:
380+
thread.start_new_thread(nop, (i,), shared)
381+
except RuntimeError:
382+
break
383+
384+
stop.acquire()
385+
# wait for all mutator threads stop.
386+
wait_t = time.monotonic()
387+
while len(results) < n and time.monotonic() - wait_t < DELAY:
388+
time.sleep(0.01)
389+
348390

349391
class Barrier:
350392
def __init__(self, num_threads):

Modules/_threadmodule.c

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -385,6 +385,21 @@ thread_run(void *boot_raw)
385385
PyEval_AcquireThread(tstate);
386386
_Py_atomic_add_ssize(&tstate->interp->threads.count, 1);
387387

388+
#ifdef Py_GIL_DISABLED
389+
// See gh-149816 - (62) Concurrent kwargs growth causes heap overwrite
390+
// So duplicate boot->kwargs to ensure that it won't be mutated concurrently
391+
// by the caller.
392+
if (boot->kwargs != NULL) {
393+
PyObject *n_kwargs = PyDict_Copy(boot->kwargs);
394+
if (n_kwargs == NULL) {
395+
thread_bootstate_free(boot, 1);
396+
goto exit;
397+
}
398+
Py_DECREF(boot->kwargs); // I am not pretty sure about this.
399+
boot->kwargs = n_kwargs;
400+
}
401+
#endif /* Py_GIL_DISABLED */
402+
388403
PyObject *res = PyObject_Call(boot->func, boot->args, boot->kwargs);
389404
if (res == NULL) {
390405
if (PyErr_ExceptionMatches(PyExc_SystemExit))

0 commit comments

Comments
 (0)