Skip to content

Commit 0b4037e

Browse files
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.
1 parent 6eb8d06 commit 0b4037e

7 files changed

Lines changed: 117 additions & 1 deletion

File tree

‎NEWS‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,10 @@ 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+
6872
- PDO:
6973
. Fixed PDOStatement::getColumnMeta() reading out of bounds for an invalid
7074
column index. (Ilia Alshanetsky)

‎ext/pcntl/pcntl.c‎

Lines changed: 30 additions & 0 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,6 +1349,20 @@ 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) {
13501368
if ((handle = zend_hash_index_find(&PCNTL_G(php_signal_table), queue->signo)) != NULL) {
@@ -1385,6 +1403,18 @@ void pcntl_signal_dispatch(void)
13851403
queue = next;
13861404
}
13871405

1406+
if (old_exception) {
1407+
if (EG(current_execute_data)) {
1408+
EG(current_execute_data)->opline = old_opline;
1409+
}
1410+
EG(opline_before_exception) = old_opline_before_exception;
1411+
if (EG(exception)) {
1412+
zend_exception_set_previous(EG(exception), old_exception);
1413+
} else {
1414+
EG(exception) = old_exception;
1415+
}
1416+
}
1417+
13881418
PCNTL_G(pending_signals) = 0;
13891419

13901420
/* Re-enable queue */
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)