From 946b6c57de001c808c95ca5c0f5ba851fd07f0bf Mon Sep 17 00:00:00 2001 From: jlaportebot Date: Sun, 24 May 2026 13:02:44 -0400 Subject: [PATCH] fix(multiprocessing): add lock to prevent waitpid race across threads (gh-148318) When multiple threads join() fork-based child processes concurrently, os.waitpid() can be called by one thread for a PID that another thread is also waiting on. Since waitpid() reaps the child process-wide, the second thread's call raises OSError(ECHILD), leaving Process.exitcode stuck at None. Add a class-level threading.Lock around the os.waitpid() call in Popen.poll() so that only one thread performs waitpid at a time, with a double-check of self.returncode inside the lock to avoid missing a result set by another thread. Fixes gh-148318. --- Lib/multiprocessing/popen_fork.py | 31 ++++++++++++++++++++++--------- 1 file changed, 22 insertions(+), 9 deletions(-) diff --git a/Lib/multiprocessing/popen_fork.py b/Lib/multiprocessing/popen_fork.py index a02a53b6a176da..dcf68527e6d259 100644 --- a/Lib/multiprocessing/popen_fork.py +++ b/Lib/multiprocessing/popen_fork.py @@ -1,6 +1,7 @@ import atexit import os import signal +import threading from . import util @@ -13,6 +14,13 @@ class Popen(object): method = 'fork' + # Lock to protect os.waitpid() calls from concurrent threads. + # Without this lock, a thread can reap a child process that + # another thread is also trying to wait on, causing the second + # thread's waitpid() to raise OSError(ECHILD) and leaving + # the Process.exitcode stuck at None. See gh-148318. + _waitpid_lock = threading.Lock() + def __init__(self, process_obj): util._flush_std_streams() self.returncode = None @@ -24,15 +32,20 @@ def duplicate_for_child(self, fd): def poll(self, flag=os.WNOHANG): if self.returncode is None: - try: - pid, sts = os.waitpid(self.pid, flag) - except OSError: - # Child process not yet created. See #1731717 - # e.errno == errno.ECHILD == 10 - return None - if pid == self.pid: - self.returncode = os.waitstatus_to_exitcode(sts) - return self.returncode + with self._waitpid_lock: + # Another thread may have set returncode while we + # waited for the lock. + if self.returncode is not None: + return self.returncode + try: + pid, sts = os.waitpid(self.pid, flag) + except OSError: + # Child process not yet created. See #1731717 + # e.errno == errno.ECHILD == 10 + return None + if pid == self.pid: + self.returncode = os.waitstatus_to_exitcode(sts) + return self.returncode def wait(self, timeout=None): if self.returncode is None: