Skip to content

Do not let PHP-FPM children miss SIGTERM, SIGQUIT #4836

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 5 commits into from
Closed
Show file tree
Hide file tree
Changes from 2 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
9 changes: 9 additions & 0 deletions sapi/fpm/fpm/fpm_children.c
Original file line number Diff line number Diff line change
Expand Up @@ -404,6 +404,11 @@ int fpm_children_make(struct fpm_worker_pool_s *wp, int in_event_loop, int nb_to
return 2;
}

zlog(ZLOG_DEBUG, "blocking signals before child birth");
if (0 > fpm_signals_child_block()) {
zlog(ZLOG_WARNING, "child may miss signals");
}

pid = fork();

switch (pid) {
Expand All @@ -415,12 +420,16 @@ int fpm_children_make(struct fpm_worker_pool_s *wp, int in_event_loop, int nb_to
return 0;

case -1 :
zlog(ZLOG_DEBUG, "unblocking signals");
fpm_signals_unblock();
zlog(ZLOG_SYSERROR, "fork() failed");

fpm_resources_discard(child);
return 2;

default :
zlog(ZLOG_DEBUG, "unblocking signals, child born");
fpm_signals_unblock();
child->pid = pid;
fpm_clock_get(&child->started);
fpm_parent_resources_use(child);
Expand Down
24 changes: 23 additions & 1 deletion sapi/fpm/fpm/fpm_signals.c
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@

static int sp[2];
static sigset_t block_sigset;
static sigset_t child_block_sigset;

const char *fpm_signal_names[NSIG + 1] = {
#ifdef SIGHUP
Expand Down Expand Up @@ -167,7 +168,8 @@ static void sig_handler(int signo) /* {{{ */

if (fpm_globals.parent_pid != getpid()) {
/* prevent a signal race condition when child process
have not set up it's own signal handler yet */
do not set up it's own sigprocmask for some reason,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

few typos (grammar mistakes) in here. Maybe something like:

prevent a signal race condition if a child process does not set 
its own sigprocmask up which leads to #76601.

leads to #76601 in such cases */
return;
}

Expand Down Expand Up @@ -247,6 +249,10 @@ int fpm_signals_init_child() /* {{{ */
}

zend_signal_init();

if (0 > fpm_signals_unblock()) {
return -1;
}
return 0;
}
/* }}} */
Expand Down Expand Up @@ -276,6 +282,12 @@ int fpm_signals_init_mask(int *signum_array, size_t size) /* {{{ */
return -1;
}
}
memcpy(&child_block_sigset, &block_sigset, sizeof(block_sigset));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is technically undefined (or at least not recommended for portability) as sigset_t should be used only with specified operations. I would prefer to set it in the loop above instead - using the same operations as for block_sigset.

if (0 > sigaddset(&child_block_sigset, SIGTERM) ||
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's the actually the reason specify one part (block_sigset) from parameter provided in fpm_main.c and another part (child_block_sigset extra signals) specified directly in this function? I think we should either have everything in the parameters (meaning extra child signals to block array paramter) or just have everything defined in fpm_signals which might make more sense.

0 > sigaddset(&child_block_sigset, SIGQUIT)) {
zlog(ZLOG_SYSERROR, "failed to prepare child signal block mask: sigaddset()");
return -1;
}
return 0;
}
/* }}} */
Expand All @@ -290,6 +302,16 @@ int fpm_signals_block() /* {{{ */
}
/* }}} */

int fpm_signals_child_block() /* {{{ */
{
if (0 > sigprocmask(SIG_BLOCK, &child_block_sigset, NULL)) {
zlog(ZLOG_SYSERROR, "failed to block child signals");
return -1;
}
return 0;
}
/* }}} */

int fpm_signals_unblock() /* {{{ */
{
/* Ensure that during reload after upgrade all signals are unblocked.
Expand Down
1 change: 1 addition & 0 deletions sapi/fpm/fpm/fpm_signals.h
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ int fpm_signals_init_child();
int fpm_signals_get_fd();
int fpm_signals_init_mask(int *signum_array, size_t size);
int fpm_signals_block();
int fpm_signals_child_block();
int fpm_signals_unblock();

extern const char *fpm_signal_names[NSIG + 1];
Expand Down
92 changes: 92 additions & 0 deletions sapi/fpm/tests/bug76601-reload-child-signals.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
--TEST--
FPM: bug76601 children should not ignore signals during concurrent reloads
--SKIPIF--
<?php
include "skipif.inc";
if (getenv("SKIP_SLOW_TESTS")) die("skip slow test");
?>
--FILE--
<?php

require_once "tester.inc";

$cfg = <<<EOT
[global]
error_log = {{FILE:LOG}}
pid = {{FILE:PID}}
; some value twice greater than tester->getLogLines() timeout
process_control_timeout=10
[unconfined]
listen = {{ADDR}}
; spawn children immediately after reload
pm = static
pm.max_children = 10
EOT;

$code = <<<EOT
<?php
/* empty */
EOT;

/*
* If a child miss SIGQUIT then reload process should stuck
* for at least process_control_timeout that is set greater
* than timout in log reading functions.
*
* Alternative way is to set log_level=debug and filter result of
* $tester->getLogLines(2000) for lines containing SIGKILL
*
* [22-Oct-2019 03:28:19.532703] DEBUG: pid 21315, fpm_pctl_kill_all(), line 161: [pool unconfined] sending signal 9 SIGKILL to child 21337
* [22-Oct-2019 03:28:19.533471] DEBUG: pid 21315, fpm_children_bury(), line 259: [pool unconfined] child 21337 exited on signal 9 (SIGKILL) after 1.003055 seconds from start
*
* but it has less probability of failure detection. Additionally it requires more
* $tester->expectLogNotice() around last reload due to presence of debug messages.
*/

$tester = new FPM\Tester($cfg, $code);
$tester->start();
$tester->expectLogStartNotices();

/* Vary interval between concurrent reload requests
since performance of test instance is not known in advance */
$max_interval = 25000;
$step = 1000;
$pid = $tester->getPid();
for ($interval = 0; $interval < $max_interval; $interval += $step) {
exec("kill -USR2 $pid", $out, $killExitCode);
if ($killExitCode) {
echo "ERROR: master process is dead\n";
break;
}
usleep($interval);
}
echo "Reached interval $interval us with $step us steps\n";
$tester->expectLogNotice('Reloading in progress ...');
/* Consume mix of 'Reloading in progress ...' and 'reloading: .*' */
$skipped = $tester->getLogLines(2000);

$tester->signal('USR2');
$tester->expectLogNotice('Reloading in progress ...');
/* When a child ignores SIGQUIT, the following expectation fails due to timeout. */
if (!$tester->expectLogNotice('reloading: .*')) {
/* for troubleshooting */
echo "Skipped messages\n";
echo implode('', $skipped);
}
$tester->expectLogNotice('using inherited socket fd=\d+, "127.0.0.1:\d+"');
$tester->expectLogStartNotices();

$tester->terminate();
$tester->expectLogTerminatingNotices();
$tester->close();

?>
Done
--EXPECT--
Reached interval 25000 us with 1000 us steps
Done
--CLEAN--
<?php
require_once "tester.inc";
FPM\Tester::clean();
?>