Skip to content

Commit 8cb57d2

Browse files
ext/pcntl: do not drop queued signals when an exception is pending (#23624)
* ext/pcntl: run signal handlers when dispatch happens with an exception pending ZEND_DO_FCALL runs its interrupt check right after an internal function returns, before the pending exception is handled, and zend_call_function() does the same for the calls it makes, so pcntl_interrupt_function() can reach the dispatcher with EG(exception) set. call_user_function() returns without calling anything in that state, the "if (EG(exception)) break" added by 296fad1 fires on the first entry, and the drain loop then recycles the whole queue without a single handler having run. The signal is destroyed rather than delayed: a later pcntl_signal_dispatch() finds nothing left. Set the exception aside while the handlers run and chain it back afterwards. The frame is left as found, EG(opline_before_exception) included: depending on the caller, the exception may not be registered on it yet, or may already be on its way to a catch block, and the caller finishes the job once we return. zend_test_raise_and_throw() and the user frame test are from Arnaud Le Blanc. Any long blocking internal call that throws on timeout reaches this. pecl/amqp throws "Consumer timeout exceed" out of AMQPQueue::consume(), which makes a Symfony messenger worker miss every SIGTERM whatever the timeout is. PDO/SQLite throws "database is locked" once busy_timeout expires, which kills a keepalive SIGALRM for the rest of the process's life. * ext/pcntl: keep the signals a throwing handler left in the queue When a handler threw, the signals queued behind it were recycled without ever being delivered. Put them back on the queue instead, and re-arm the interrupt so that the engine dispatches them once the exception has been handled, rather than leaving them to wait for another signal to come in. Not calling further handlers while the exception propagates is unchanged.
1 parent fa7734f commit 8cb57d2

9 files changed

Lines changed: 235 additions & 11 deletions

‎NEWS‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,12 @@ PHP NEWS
6565
is_cacheable_stream_path()). (ndossche)
6666
. Fix zend_analyze_calls() call_stack buffer overrun. (Mrmaxmeier)
6767

68+
- PCNTL:
69+
. Fixed pcntl_signal_dispatch() dropping the queued signals when it runs while
70+
an exception is pending. (nicolas-grekas)
71+
. Fixed pcntl_signal_dispatch() dropping the signals queued behind a handler
72+
that throws. (nicolas-grekas)
73+
6874
- PDO:
6975
. Fixed PDOStatement::getColumnMeta() reading out of bounds for an invalid
7076
column index. (Ilia Alshanetsky)

‎ext/pcntl/pcntl.c‎

Lines changed: 55 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
#include "ext/standard/info.h"
3232
#include "php_signal.h"
3333
#include "php_ticks.h"
34+
#include "zend_exceptions.h"
3435
#include "zend_fibers.h"
3536

3637
#if defined(HAVE_GETPRIORITY) || defined(HAVE_SETPRIORITY) || defined(HAVE_WAIT3)
@@ -1318,6 +1319,9 @@ void pcntl_signal_dispatch(void)
13181319
{
13191320
zval params[2], *handle, retval;
13201321
struct php_pcntl_pending_signal *queue, *next;
1322+
zend_object *old_exception;
1323+
const zend_op *old_opline_before_exception = NULL;
1324+
const zend_op *old_opline = NULL;
13211325
sigset_t mask;
13221326
sigset_t old_mask;
13231327

@@ -1345,8 +1349,24 @@ void pcntl_signal_dispatch(void)
13451349
PCNTL_G(head) = NULL; /* simple stores are atomic */
13461350
PCNTL_G(tail) = NULL;
13471351

1352+
/* Dispatching can happen with an exception pending, e.g. from the interrupt check that runs
1353+
* right after an internal function threw. call_user_function() does nothing in that state,
1354+
* so set the exception aside while the handlers run. The frame is left as found: depending
1355+
* on the caller, the exception may not be registered on it yet, or may already be on its
1356+
* way to a catch block, and the caller takes it from there once we return. */
1357+
old_exception = EG(exception);
1358+
if (old_exception) {
1359+
if (EG(current_execute_data)) {
1360+
old_opline = EG(current_execute_data)->opline;
1361+
}
1362+
old_opline_before_exception = EG(opline_before_exception);
1363+
EG(exception) = NULL;
1364+
}
1365+
13481366
/* Allocate */
13491367
while (queue) {
1368+
bool handler_threw = false;
1369+
13501370
if ((handle = zend_hash_index_find(&PCNTL_G(php_signal_table), queue->signo)) != NULL) {
13511371
if (Z_TYPE_P(handle) != IS_LONG) {
13521372
ZVAL_NULL(&retval);
@@ -1365,27 +1385,52 @@ void pcntl_signal_dispatch(void)
13651385
#ifdef HAVE_STRUCT_SIGINFO_T
13661386
zval_ptr_dtor(&params[1]);
13671387
#endif
1368-
if (EG(exception)) {
1369-
break;
1370-
}
1388+
handler_threw = NULL != EG(exception);
13711389
}
13721390
}
13731391

13741392
next = queue->next;
13751393
queue->next = PCNTL_G(spares);
13761394
PCNTL_G(spares) = queue;
13771395
queue = next;
1396+
1397+
/* No other handler can be called while the exception propagates */
1398+
if (handler_threw) {
1399+
break;
1400+
}
13781401
}
13791402

1380-
/* drain the remaining in case of exception thrown */
1381-
while (queue) {
1382-
next = queue->next;
1383-
queue->next = PCNTL_G(spares);
1384-
PCNTL_G(spares) = queue;
1385-
queue = next;
1403+
if (old_exception) {
1404+
if (EG(current_execute_data)) {
1405+
EG(current_execute_data)->opline = old_opline;
1406+
}
1407+
EG(opline_before_exception) = old_opline_before_exception;
1408+
if (EG(exception)) {
1409+
zend_exception_set_previous(EG(exception), old_exception);
1410+
} else {
1411+
EG(exception) = old_exception;
1412+
}
13861413
}
13871414

1388-
PCNTL_G(pending_signals) = 0;
1415+
if (UNEXPECTED(queue)) {
1416+
/* Put back what the throwing handler did not get to, instead of dropping it, and ask
1417+
* the engine to come back once the exception has been handled. Signals are still
1418+
* blocked here, so PCNTL_G(head) cannot have been repopulated in the meantime. */
1419+
next = queue;
1420+
1421+
while (next->next) {
1422+
next = next->next;
1423+
}
1424+
1425+
PCNTL_G(head) = queue;
1426+
PCNTL_G(tail) = next;
1427+
1428+
if (PCNTL_G(async_signals)) {
1429+
zend_atomic_bool_store_ex(&EG(vm_interrupt), true);
1430+
}
1431+
} else {
1432+
PCNTL_G(pending_signals) = 0;
1433+
}
13891434

13901435
/* Re-enable queue */
13911436
PCNTL_G(processing_signal_queue) = 0;
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
--TEST--
2+
pcntl_signal_dispatch() keeps the signals left in the queue by a throwing handler
3+
--EXTENSIONS--
4+
pcntl
5+
posix
6+
--FILE--
7+
<?php
8+
9+
$called = [];
10+
11+
pcntl_signal(SIGUSR1, function ($signo) use (&$called) {
12+
$called[] = 'SIGUSR1';
13+
throw new \Exception('Exception in signal handler');
14+
});
15+
16+
pcntl_signal(SIGUSR2, function ($signo) use (&$called) {
17+
$called[] = 'SIGUSR2';
18+
});
19+
20+
pcntl_signal(SIGHUP, function ($signo) use (&$called) {
21+
$called[] = 'SIGHUP';
22+
});
23+
24+
posix_kill(posix_getpid(), SIGUSR1);
25+
posix_kill(posix_getpid(), SIGUSR2);
26+
posix_kill(posix_getpid(), SIGHUP);
27+
28+
try {
29+
pcntl_signal_dispatch();
30+
} catch (\Exception $e) {
31+
echo $e->getMessage() . "\n";
32+
}
33+
34+
echo "Handlers called: " . implode(', ', $called) . "\n";
35+
36+
pcntl_signal_dispatch();
37+
38+
echo "Handlers called: " . implode(', ', $called) . "\n";
39+
40+
?>
41+
--EXPECT--
42+
Exception in signal handler
43+
Handlers called: SIGUSR1
44+
Handlers called: SIGUSR1, SIGUSR2, SIGHUP
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
--TEST--
2+
pcntl_signal_dispatch() delivers the signals a throwing handler left behind once its exception is handled
3+
--EXTENSIONS--
4+
pcntl
5+
posix
6+
--FILE--
7+
<?php
8+
9+
$called = [];
10+
11+
pcntl_signal(SIGUSR1, function ($signo) use (&$called) {
12+
$called[] = 'SIGUSR1';
13+
throw new \Exception('Exception in signal handler');
14+
});
15+
16+
pcntl_signal(SIGUSR2, function ($signo) use (&$called) {
17+
$called[] = 'SIGUSR2';
18+
});
19+
20+
pcntl_signal(SIGHUP, function ($signo) use (&$called) {
21+
$called[] = 'SIGHUP';
22+
});
23+
24+
// Queued, not dispatched: asynchronous signals are off
25+
posix_kill(posix_getpid(), SIGUSR1);
26+
posix_kill(posix_getpid(), SIGUSR2);
27+
28+
pcntl_async_signals(true);
29+
30+
try {
31+
// Delivered asynchronously, so the whole queue is dispatched
32+
posix_kill(posix_getpid(), SIGHUP);
33+
echo "Not reached\n";
34+
} catch (\Exception $e) {
35+
echo $e->getMessage() . "\n";
36+
}
37+
38+
// No explicit dispatch: the engine delivers what the throwing handler left behind
39+
// on its own, as soon as the exception has been handled
40+
usleep(1000);
41+
42+
echo "Handlers called: " . implode(', ', $called) . "\n";
43+
44+
?>
45+
--EXPECT--
46+
Exception in signal handler
47+
Handlers called: SIGUSR1, SIGUSR2, SIGHUP
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
--TEST--
2+
pcntl_signal_dispatch() runs the handlers of the signals raised while an internal function ran and then threw
3+
--EXTENSIONS--
4+
pcntl
5+
zend_test
6+
--FILE--
7+
<?php
8+
9+
pcntl_async_signals(true);
10+
11+
pcntl_signal(SIGUSR1, function ($signo) {
12+
echo "Handler called\n";
13+
});
14+
15+
try {
16+
zend_test_raise_and_throw(SIGUSR1);
17+
} catch (\Exception $e) {
18+
echo $e->getMessage(), "\n";
19+
}
20+
21+
?>
22+
--EXPECT--
23+
Handler called
24+
Exception after raise()
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
--TEST--
2+
pcntl_signal_dispatch() with an exception pending after an internal function called from a user frame
3+
--EXTENSIONS--
4+
pcntl
5+
zend_test
6+
--FILE--
7+
<?php
8+
9+
pcntl_signal(SIGUSR1, function ($signo) {
10+
echo "Handler called from ", debug_backtrace()[1]['function'], "()\n";
11+
});
12+
13+
pcntl_async_signals(true);
14+
15+
function test() {
16+
declare(ticks=1) {
17+
register_tick_function('zend_test_raise_and_throw', SIGUSR1);
18+
}
19+
unregister_tick_function('zend_test_raise_and_throw');
20+
}
21+
22+
test();
23+
24+
?>
25+
--EXPECTF--
26+
Handler called from test()
27+
28+
Fatal error: Uncaught Exception: Exception after raise() in %s:%d
29+
Stack trace:
30+
#0 %s(%d): zend_test_raise_and_throw(%d)
31+
#1 %s(%d): test()
32+
#2 {main}
33+
thrown in %s on line %d

‎ext/zend_test/test.c‎

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737
#include "zend_call_stack.h"
3838
#include "zend_exceptions.h"
3939
#include "zend_mm_custom_handlers.h"
40+
#include <signal.h>
4041

4142
// `php.h` sets `NDEBUG` when not `PHP_DEBUG` which will make `assert()` from
4243
// assert.h a no-op. In order to have `assert()` working on NDEBUG builds, we
@@ -672,6 +673,22 @@ static ZEND_FUNCTION(zend_test_crash)
672673
php_printf("%s", invalid);
673674
}
674675

676+
static ZEND_FUNCTION(zend_test_raise_and_throw)
677+
{
678+
zend_long signo;
679+
680+
ZEND_PARSE_PARAMETERS_START(1, 1)
681+
Z_PARAM_LONG(signo)
682+
ZEND_PARSE_PARAMETERS_END();
683+
684+
if (raise((int) signo) != 0) {
685+
zend_throw_error(NULL, "raise() failed");
686+
RETURN_THROWS();
687+
}
688+
689+
zend_throw_exception(NULL, "Exception after raise()", 0);
690+
}
691+
675692
static bool has_opline(zend_execute_data *execute_data)
676693
{
677694
return execute_data

‎ext/zend_test/test.stub.php‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -298,6 +298,8 @@ function zend_get_map_ptr_last(): int {}
298298

299299
function zend_test_crash(?string $message = null): void {}
300300

301+
function zend_test_raise_and_throw(int $signal): void {}
302+
301303
function zend_test_fill_packed_array(array &$array): void {}
302304

303305
/** @return resource */

‎ext/zend_test/test_arginfo.h‎

Lines changed: 7 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)