diff --git a/Lib/test/test_thread.py b/Lib/test/test_thread.py index ac924728febc991..3776371ee21b63f 100644 --- a/Lib/test/test_thread.py +++ b/Lib/test/test_thread.py @@ -345,6 +345,48 @@ def func(): handle = thread.start_joinable_thread(func, handle=None) handle.join() +class StartNewThreadKwargsRace(unittest.TestCase): + + @unittest.skipUnless(support.Py_GIL_DISABLED, "GIL must be disabled") + def test_dict_growsup_when_thread_start(self): + # See gh-149816 - (62) Concurrent kwargs growth causes heap overwrite + # This test is meant to be run under a free-threaded build, where the GIL is + # disabled and concurrent mutations of the same dict can cause heap + # corruption. + results = [] + def mutator(shared, stop, prefix, burst): + i = 0 + while not stop.locked(): + for _ in range(burst): + shared[f"{prefix}_{i}"] = i + i += 1 + time.sleep(0) + results.append(prefix) + + def nop(i, **kwargs): + pass + + DELAY = 1.0 + stop = thread.lock() + shared = {f"base_{i}": i for i in range(20000)} + n = 4 + for i in range(n): + args=(shared, stop, f"dynamic_{i}", 1000) + thread.start_new_thread(mutator, args) + + snt = 32 + for i in range(snt): + try: + thread.start_new_thread(nop, (i,), shared) + except RuntimeError: + break + + stop.acquire() + # wait for all mutator threads stop. + wait_t = time.monotonic() + while len(results) < n and time.monotonic() - wait_t < DELAY: + time.sleep(0.01) + class Barrier: def __init__(self, num_threads): diff --git a/Misc/NEWS.d/next/Library/2026-05-20-19-10-09.gh-issue-149816.g_ycIN.rst b/Misc/NEWS.d/next/Library/2026-05-20-19-10-09.gh-issue-149816.g_ycIN.rst new file mode 100644 index 000000000000000..50dc74c8beeaca2 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-05-20-19-10-09.gh-issue-149816.g_ycIN.rst @@ -0,0 +1 @@ +Fix a race condition on ``kwargs`` in ``PyStack_UnpackDict`` by duplicating the ``kwargs`` argument in the ``thread.new_start_thread`` function. diff --git a/Modules/_threadmodule.c b/Modules/_threadmodule.c index 135b53111014d1c..c70c454e171868e 100644 --- a/Modules/_threadmodule.c +++ b/Modules/_threadmodule.c @@ -385,6 +385,21 @@ thread_run(void *boot_raw) PyEval_AcquireThread(tstate); _Py_atomic_add_ssize(&tstate->interp->threads.count, 1); +#ifdef Py_GIL_DISABLED + // See gh-149816 - (62) Concurrent kwargs growth causes heap overwrite + // So duplicate boot->kwargs to ensure that it won't be mutated concurrently + // by the caller. + if (boot->kwargs != NULL) { + PyObject *n_kwargs = PyDict_Copy(boot->kwargs); + if (n_kwargs == NULL) { + thread_bootstate_free(boot, 1); + goto exit; + } + Py_DECREF(boot->kwargs); // I am not pretty sure about this. + boot->kwargs = n_kwargs; + } +#endif /* Py_GIL_DISABLED */ + PyObject *res = PyObject_Call(boot->func, boot->args, boot->kwargs); if (res == NULL) { if (PyErr_ExceptionMatches(PyExc_SystemExit))