Skip to content

Commit 41a4c86

Browse files
pablogsalmiss-islington
authored andcommitted
bpo-33613, test_semaphore_tracker_sigint: fix race condition (pythonGH-7850)
Fail `test_semaphore_tracker_sigint` if no warnings are expected and one is received. Fix race condition when the child receives SIGINT before it can register signal handlers for it. The race condition occurs when the parent calls `_semaphore_tracker.ensure_running()` (which in turn spawns the semaphore_tracker using `_posixsubprocess.fork_exec`), the child registers the signal handlers and the parent tries to kill the child. What seem to happen is that in some slow systems, the parent sends the signal to kill the child before the child protects against the signal. (cherry picked from commit ec74d18) Co-authored-by: Pablo Galindo <[email protected]>
1 parent 1e92123 commit 41a4c86

File tree

3 files changed

+52
-12
lines changed

3 files changed

+52
-12
lines changed

Lib/multiprocessing/semaphore_tracker.py

Lines changed: 28 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,9 @@
2323

2424
__all__ = ['ensure_running', 'register', 'unregister']
2525

26+
_HAVE_SIGMASK = hasattr(signal, 'pthread_sigmask')
27+
_IGNORED_SIGNALS = (signal.SIGINT, signal.SIGTERM)
28+
2629

2730
class SemaphoreTracker(object):
2831

@@ -43,10 +46,16 @@ def ensure_running(self):
4346
with self._lock:
4447
if self._pid is not None:
4548
# semaphore tracker was launched before, is it still running?
46-
pid, status = os.waitpid(self._pid, os.WNOHANG)
47-
if not pid:
48-
# => still alive
49-
return
49+
try:
50+
pid, _ = os.waitpid(self._pid, os.WNOHANG)
51+
except ChildProcessError:
52+
# The process terminated
53+
pass
54+
else:
55+
if not pid:
56+
# => still alive
57+
return
58+
5059
# => dead, launch it again
5160
os.close(self._fd)
5261
self._fd = None
@@ -68,7 +77,19 @@ def ensure_running(self):
6877
exe = spawn.get_executable()
6978
args = [exe] + util._args_from_interpreter_flags()
7079
args += ['-c', cmd % r]
71-
pid = util.spawnv_passfds(exe, args, fds_to_pass)
80+
# bpo-33613: Register a signal mask that will block the signals.
81+
# This signal mask will be inherited by the child that is going
82+
# to be spawned and will protect the child from a race condition
83+
# that can make the child die before it registers signal handlers
84+
# for SIGINT and SIGTERM. The mask is unregistered after spawning
85+
# the child.
86+
try:
87+
if _HAVE_SIGMASK:
88+
signal.pthread_sigmask(signal.SIG_BLOCK, _IGNORED_SIGNALS)
89+
pid = util.spawnv_passfds(exe, args, fds_to_pass)
90+
finally:
91+
if _HAVE_SIGMASK:
92+
signal.pthread_sigmask(signal.SIG_UNBLOCK, _IGNORED_SIGNALS)
7293
except:
7394
os.close(w)
7495
raise
@@ -104,12 +125,13 @@ def _send(self, cmd, name):
104125
unregister = _semaphore_tracker.unregister
105126
getfd = _semaphore_tracker.getfd
106127

107-
108128
def main(fd):
109129
'''Run semaphore tracker.'''
110130
# protect the process from ^C and "killall python" etc
111131
signal.signal(signal.SIGINT, signal.SIG_IGN)
112132
signal.signal(signal.SIGTERM, signal.SIG_IGN)
133+
if _HAVE_SIGMASK:
134+
signal.pthread_sigmask(signal.SIG_UNBLOCK, _IGNORED_SIGNALS)
113135

114136
for f in (sys.stdin, sys.stdout):
115137
try:

Lib/test/_test_multiprocessing.py

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
import struct
2121
import operator
2222
import weakref
23+
import warnings
2324
import test.support
2425
import test.support.script_helper
2526
from test import support
@@ -4482,17 +4483,19 @@ def check_semaphore_tracker_death(self, signum, should_die):
44824483
# bpo-31310: if the semaphore tracker process has died, it should
44834484
# be restarted implicitly.
44844485
from multiprocessing.semaphore_tracker import _semaphore_tracker
4485-
_semaphore_tracker.ensure_running()
44864486
pid = _semaphore_tracker._pid
4487+
if pid is not None:
4488+
os.kill(pid, signal.SIGKILL)
4489+
os.waitpid(pid, 0)
4490+
with warnings.catch_warnings(record=True) as all_warn:
4491+
_semaphore_tracker.ensure_running()
4492+
pid = _semaphore_tracker._pid
4493+
44874494
os.kill(pid, signum)
44884495
time.sleep(1.0) # give it time to die
44894496

44904497
ctx = multiprocessing.get_context("spawn")
4491-
with contextlib.ExitStack() as stack:
4492-
if should_die:
4493-
stack.enter_context(self.assertWarnsRegex(
4494-
UserWarning,
4495-
"semaphore_tracker: process died"))
4498+
with warnings.catch_warnings(record=True) as all_warn:
44964499
sem = ctx.Semaphore()
44974500
sem.acquire()
44984501
sem.release()
@@ -4502,11 +4505,23 @@ def check_semaphore_tracker_death(self, signum, should_die):
45024505
del sem
45034506
gc.collect()
45044507
self.assertIsNone(wr())
4508+
if should_die:
4509+
self.assertEqual(len(all_warn), 1)
4510+
the_warn = all_warn[0]
4511+
issubclass(the_warn.category, UserWarning)
4512+
self.assertTrue("semaphore_tracker: process died"
4513+
in str(the_warn.message))
4514+
else:
4515+
self.assertEqual(len(all_warn), 0)
45054516

45064517
def test_semaphore_tracker_sigint(self):
45074518
# Catchable signal (ignored by semaphore tracker)
45084519
self.check_semaphore_tracker_death(signal.SIGINT, False)
45094520

4521+
def test_semaphore_tracker_sigterm(self):
4522+
# Catchable signal (ignored by semaphore tracker)
4523+
self.check_semaphore_tracker_death(signal.SIGTERM, False)
4524+
45104525
def test_semaphore_tracker_sigkill(self):
45114526
# Uncatchable signal.
45124527
self.check_semaphore_tracker_death(signal.SIGKILL, True)
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
Fix a race condition in ``multiprocessing.semaphore_tracker`` when the
2+
tracker receives SIGINT before it can register signal handlers for ignoring
3+
it.

0 commit comments

Comments
 (0)