Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 8 additions & 2 deletions Lib/multiprocessing/forkserver.py
Original file line number Diff line number Diff line change
Expand Up @@ -320,8 +320,14 @@ def sigchld_handler(*_unused):
# protect the process from ^C
signal.SIGINT: signal.SIG_IGN,
}
old_handlers = {sig: signal.signal(sig, val)
for (sig, val) in handlers.items()}
old_handlers = {}
for sig, val in handlers.items():
old_handler = signal.signal(sig, val)
if old_handler is None:
# gh-159418: a handler installed outside of Python (e.g. from C)
# cannot be restored from Python, so use the default action.
old_handler = signal.SIG_DFL
old_handlers[sig] = old_handler

# calling os.write() in the Python signal handler is racy
signal.set_wakeup_fd(sig_w)
Expand Down
38 changes: 38 additions & 0 deletions Lib/test/_test_multiprocessing.py
Original file line number Diff line number Diff line change
Expand Up @@ -7610,6 +7610,44 @@ def test_forkserver_preload_imports_sys_path(self):
q.close()
self.assertIn(self._mod_name, child_imported_modules)

@staticmethod
def send_child_sigint_state(conn, mod_name):
conn.send((mod_name in sys.modules, signal.getsignal(signal.SIGINT)))

def test_forkserver_preload_foreign_sigint_handler(self):
# gh-159418: signal.signal() returns None when the handler was already
# installed outside Python before the interpreter started, for example
# by a library loaded with LD_PRELOAD. Simulate that in the forkserver
# with a preloaded module.
if self._ctx.get_start_method() != "forkserver":
self.skipTest("forkserver specific test.")
mod_name = "foreign_sigint_test_mod"
mod_path = os.path.join(self._temp_dir, f"{mod_name}.py")
with open(mod_path, "w", encoding="utf-8") as mod:
mod.write(textwrap.dedent("""\
import signal
_signal = signal.signal
def _fake_signal(signum, handler):
old_handler = _signal(signum, handler)
return None if signum == signal.SIGINT else old_handler
signal.signal = _fake_signal
"""))
forkserver = multiprocessing.forkserver._forkserver
self.addCleanup(self._ctx.set_forkserver_preload,
forkserver._preload_modules)
self.addCleanup(forkserver._stop) # Don't reuse the patched server.
forkserver._stop() # Must be fresh.
self._ctx.set_forkserver_preload([mod_name])
r, w = self._ctx.Pipe(duplex=False)
self.addCleanup(r.close)
proc = self._ctx.Process(
target=self.send_child_sigint_state, args=(w, mod_name))
proc.start()
w.close()
proc.join()
self.assertEqual(proc.exitcode, 0)
self.assertEqual(r.recv(), (True, signal.SIG_DFL))

@staticmethod
def enq_sys_path_and_import(queue, mod_name):
queue.put(sys.path)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Fix :mod:`multiprocessing` ``forkserver`` child processes exiting with a
:exc:`TypeError` when :data:`~signal.SIGINT` had a handler installed outside
of Python, for example by a native library loaded with ``LD_PRELOAD``. Such
a signal is now reset to :data:`~signal.SIG_DFL` in the child.
Loading