Skip to content

Commit ec74d18

Browse files
pablogsalpitrou
authored andcommitted
bpo-33613, test_semaphore_tracker_sigint: fix race condition (#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.
1 parent e9ba370 commit ec74d18

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
@@ -4517,17 +4518,19 @@ def check_semaphore_tracker_death(self, signum, should_die):
45174518
# bpo-31310: if the semaphore tracker process has died, it should
45184519
# be restarted implicitly.
45194520
from multiprocessing.semaphore_tracker import _semaphore_tracker
4520-
_semaphore_tracker.ensure_running()
45214521
pid = _semaphore_tracker._pid
4522+
if pid is not None:
4523+
os.kill(pid, signal.SIGKILL)
4524+
os.waitpid(pid, 0)
4525+
with warnings.catch_warnings(record=True) as all_warn:
4526+
_semaphore_tracker.ensure_running()
4527+
pid = _semaphore_tracker._pid
4528+
45224529
os.kill(pid, signum)
45234530
time.sleep(1.0) # give it time to die
45244531

45254532
ctx = multiprocessing.get_context("spawn")
4526-
with contextlib.ExitStack() as stack:
4527-
if should_die:
4528-
stack.enter_context(self.assertWarnsRegex(
4529-
UserWarning,
4530-
"semaphore_tracker: process died"))
4533+
with warnings.catch_warnings(record=True) as all_warn:
45314534
sem = ctx.Semaphore()
45324535
sem.acquire()
45334536
sem.release()
@@ -4537,11 +4540,23 @@ def check_semaphore_tracker_death(self, signum, should_die):
45374540
del sem
45384541
gc.collect()
45394542
self.assertIsNone(wr())
4543+
if should_die:
4544+
self.assertEqual(len(all_warn), 1)
4545+
the_warn = all_warn[0]
4546+
issubclass(the_warn.category, UserWarning)
4547+
self.assertTrue("semaphore_tracker: process died"
4548+
in str(the_warn.message))
4549+
else:
4550+
self.assertEqual(len(all_warn), 0)
45404551

45414552
def test_semaphore_tracker_sigint(self):
45424553
# Catchable signal (ignored by semaphore tracker)
45434554
self.check_semaphore_tracker_death(signal.SIGINT, False)
45444555

4556+
def test_semaphore_tracker_sigterm(self):
4557+
# Catchable signal (ignored by semaphore tracker)
4558+
self.check_semaphore_tracker_death(signal.SIGTERM, False)
4559+
45454560
def test_semaphore_tracker_sigkill(self):
45464561
# Uncatchable signal.
45474562
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)