From 7cead0e211446689171c05589352af14ec7a79ea Mon Sep 17 00:00:00 2001 From: Dmitry Voropaev Date: Thu, 24 Sep 2026 19:44:12 +0300 Subject: [PATCH 1/2] gh-116612: Fork before shutdown in test_3_join_in_forked_from_thread The test script ended at ``w.start()``, so the ``os.fork()`` in ``worker()`` raced the interpreter shutdown that begins as soon as the main thread falls off the end of the script. When the fork loses that race it happens inside ``threading._shutdown()``, which marks the main thread as stopped before joining non-daemon threads. The child then inherits a main thread that was already marked as stopped in the parent, so ``mainthread.join()`` in ``joiningfunc()`` returns for the wrong reason and the test stops checking what its own comment describes: "In the forked process, the main Thread object must be marked as stopped." Join ``w`` so that the main thread stays alive until the fork has happened, and check in ``worker()`` that the main thread really is still alive before forking. Without that check the test passes either way, so losing the join again would go unnoticed. With ``time.sleep(0.1)`` added at the start of ``worker()`` (the reproducer from the issue), the fork previously saw ``threading._SHUTTING_DOWN`` true and the main thread already marked done; it now sees both false, and the main thread handle is marked done only in the child. --- Lib/test/test_threading.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/Lib/test/test_threading.py b/Lib/test/test_threading.py index 96b43936be92cd..d1e88b91646066 100644 --- a/Lib/test/test_threading.py +++ b/Lib/test/test_threading.py @@ -1580,6 +1580,14 @@ def test_3_join_in_forked_from_thread(self): main_thread = threading.current_thread() def worker(): + # fork() must happen before the interpreter starts shutting + # down: otherwise the main thread is already marked as + # stopped in the parent, the child inherits that state, and + # the test below succeeds without testing anything. + if not main_thread.is_alive(): + print('main thread stopped before fork()', + file=sys.stderr, flush=True) + os._exit(3) childpid = os.fork() if childpid != 0: # parent process @@ -1595,6 +1603,9 @@ def worker(): w = threading.Thread(target=worker) w.start() + # Keep the main thread alive until worker() has forked, so that + # the check above cannot be lost to interpreter shutdown. + w.join() """ self._run_and_join(script) From 9ef91ca8908e1c12bf4fe31260e0e8d8a6e6d52d Mon Sep 17 00:00:00 2001 From: Dmitry Voropaev Date: Mon, 28 Sep 2026 18:21:42 +0300 Subject: [PATCH 2/2] gh-116612: Add a NEWS entry --- .../next/Tests/2026-09-28-12-05-10.gh-issue-116612.Rn4kLp.rst | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 Misc/NEWS.d/next/Tests/2026-09-28-12-05-10.gh-issue-116612.Rn4kLp.rst diff --git a/Misc/NEWS.d/next/Tests/2026-09-28-12-05-10.gh-issue-116612.Rn4kLp.rst b/Misc/NEWS.d/next/Tests/2026-09-28-12-05-10.gh-issue-116612.Rn4kLp.rst new file mode 100644 index 00000000000000..650c51d19f96aa --- /dev/null +++ b/Misc/NEWS.d/next/Tests/2026-09-28-12-05-10.gh-issue-116612.Rn4kLp.rst @@ -0,0 +1,3 @@ +Make ``test_3_join_in_forked_from_thread`` in ``test_threading`` wait for the +forking thread, so that the fork happens while the main thread is still +running rather than racing interpreter shutdown.