summaryrefslogtreecommitdiff
path: root/Lib/subprocess.py
diff options
context:
space:
mode:
authorVictor Stinner <vstinner@python.org>2020-01-15 17:38:55 +0100
committerGitHub <noreply@github.com>2020-01-15 17:38:55 +0100
commite85a305503bf83c5a8ffb3a988dfe7b67461cbee (patch)
tree6bf49817955b9bba97c35a65bcaaa8c0e05ddb96 /Lib/subprocess.py
parented154c387efc5f978ec97900ec9e0ec6631d5498 (diff)
downloadcpython-git-e85a305503bf83c5a8ffb3a988dfe7b67461cbee.tar.gz
bpo-38630: Fix subprocess.Popen.send_signal() race condition (GH-16984)
On Unix, subprocess.Popen.send_signal() now polls the process status. Polling reduces the risk of sending a signal to the wrong process if the process completed, the Popen.returncode attribute is still None, and the pid has been reassigned (recycled) to a new different process.
Diffstat (limited to 'Lib/subprocess.py')
-rw-r--r--Lib/subprocess.py28
1 files changed, 25 insertions, 3 deletions
diff --git a/Lib/subprocess.py b/Lib/subprocess.py
index 30f0d1be15..79dffd349a 100644
--- a/Lib/subprocess.py
+++ b/Lib/subprocess.py
@@ -2061,9 +2061,31 @@ class Popen(object):
def send_signal(self, sig):
"""Send a signal to the process."""
- # Skip signalling a process that we know has already died.
- if self.returncode is None:
- os.kill(self.pid, sig)
+ # bpo-38630: Polling reduces the risk of sending a signal to the
+ # wrong process if the process completed, the Popen.returncode
+ # attribute is still None, and the pid has been reassigned
+ # (recycled) to a new different process. This race condition can
+ # happens in two cases.
+ #
+ # Case 1. Thread A calls Popen.poll(), thread B calls
+ # Popen.send_signal(). In thread A, waitpid() succeed and returns
+ # the exit status. Thread B calls kill() because poll() in thread A
+ # did not set returncode yet. Calling poll() in thread B prevents
+ # the race condition thanks to Popen._waitpid_lock.
+ #
+ # Case 2. waitpid(pid, 0) has been called directly, without
+ # using Popen methods: returncode is still None is this case.
+ # Calling Popen.poll() will set returncode to a default value,
+ # since waitpid() fails with ProcessLookupError.
+ self.poll()
+ if self.returncode is not None:
+ # Skip signalling a process that we know has already died.
+ return
+
+ # The race condition can still happen if the race condition
+ # described above happens between the returncode test
+ # and the kill() call.
+ os.kill(self.pid, sig)
def terminate(self):
"""Terminate the process with SIGTERM