Skip to content

Commit da5fb61

Browse files
ext/pcntl: do not drop queued signals when an exception is pending
pcntl_signal_dispatch() takes the whole queue out of PCNTL_G(head) before it starts calling handlers, and recycles every entry it walks over. Two paths let signals disappear that way. The first one is the interrupt handler. ZEND_VM_FCALL_INTERRUPT_CHECK() runs right after an internal function returns, before the pending exception is handled, so pcntl_interrupt_function() reaches 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 entire queue without a single handler having run. Setting the exception aside for the duration of the dispatch, the way destructors are called during unwinding, lets the handlers run. zend_exception_save() is not usable for that: it goes through the single EG(prev_exception) slot, so a dispatch happening inside an autoloader called with an exception set aside would hand that exception back too early. The second one is a handler that throws while other signals are queued behind it: those were recycled too. They now go back to the queue, and with asynchronous signals the interrupt is re-armed, so that the engine delivers them on its own once the exception is handled instead of waiting for another signal to come in. This is reachable from any long blocking internal call that throws on timeout. pecl/amqp is one: AMQPQueue::consume() throws "Consumer timeout exceed" when the read timeout expires, which made a Symfony messenger worker built on it miss every SIGTERM, whatever the timeout was.
1 parent 214ed7e commit da5fb61

4 files changed

Lines changed: 146 additions & 10 deletions

File tree

‎NEWS‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,10 @@ PHP NEWS
1111
registrations are freed while still reachable from the cycle collector.
1212
(Ilia Alshanetsky)
1313

14+
- PCNTL:
15+
. Fixed pcntl_signal_dispatch() dropping queued signals when it runs while an
16+
exception is pending. (nicolas-grekas)
17+
1418
- Zip:
1519
. Fixed ZipArchive::extractTo() ignoring files given in a non-list array.
1620
(David Carlier)

‎ext/pcntl/pcntl.c‎

Lines changed: 51 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,8 @@ 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;
13211324
sigset_t mask;
13221325
sigset_t old_mask;
13231326

@@ -1345,8 +1348,21 @@ void pcntl_signal_dispatch(void)
13451348
PCNTL_G(head) = NULL; /* simple stores are atomic */
13461349
PCNTL_G(tail) = NULL;
13471350

1351+
/* Dispatching can happen while an exception is propagating, typically from the interrupt
1352+
* check that runs right after an internal function returned with an exception pending.
1353+
* Handlers cannot be called in that state, so set the exception aside while they run,
1354+
* the way destructors are called during unwinding. */
1355+
old_exception = EG(exception);
1356+
if (old_exception && EG(current_execute_data)) {
1357+
EG(current_execute_data)->opline = EG(opline_before_exception);
1358+
old_opline_before_exception = EG(opline_before_exception);
1359+
}
1360+
EG(exception) = NULL;
1361+
13481362
/* Allocate */
13491363
while (queue) {
1364+
bool handler_threw = false;
1365+
13501366
if ((handle = zend_hash_index_find(&PCNTL_G(php_signal_table), queue->signo)) != NULL) {
13511367
if (Z_TYPE_P(handle) != IS_LONG) {
13521368
ZVAL_NULL(&retval);
@@ -1365,27 +1381,52 @@ void pcntl_signal_dispatch(void)
13651381
#ifdef HAVE_STRUCT_SIGINFO_T
13661382
zval_ptr_dtor(&params[1]);
13671383
#endif
1368-
if (EG(exception)) {
1369-
break;
1370-
}
1384+
handler_threw = NULL != EG(exception);
13711385
}
13721386
}
13731387

13741388
next = queue->next;
13751389
queue->next = PCNTL_G(spares);
13761390
PCNTL_G(spares) = queue;
13771391
queue = next;
1392+
1393+
/* No other handler can be called while the exception propagates */
1394+
if (handler_threw) {
1395+
break;
1396+
}
13781397
}
13791398

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;
1399+
if (old_exception) {
1400+
if (EG(current_execute_data)) {
1401+
EG(current_execute_data)->opline = EG(exception_op);
1402+
EG(opline_before_exception) = old_opline_before_exception;
1403+
}
1404+
if (EG(exception)) {
1405+
zend_exception_set_previous(EG(exception), old_exception);
1406+
} else {
1407+
EG(exception) = old_exception;
1408+
}
13861409
}
13871410

1388-
PCNTL_G(pending_signals) = 0;
1411+
if (UNEXPECTED(queue)) {
1412+
/* The signals a throwing handler left behind go back to the queue instead of being
1413+
* dropped, and the engine is asked to dispatch again once that exception is handled.
1414+
* Signals are still blocked here, so PCNTL_G(head) cannot have been repopulated. */
1415+
next = queue;
1416+
1417+
while (next->next) {
1418+
next = next->next;
1419+
}
1420+
1421+
PCNTL_G(head) = queue;
1422+
PCNTL_G(tail) = next;
1423+
1424+
if (PCNTL_G(async_signals)) {
1425+
zend_atomic_bool_store_ex(&EG(vm_interrupt), true);
1426+
}
1427+
} else {
1428+
PCNTL_G(pending_signals) = 0;
1429+
}
13891430

13901431
/* Re-enable queue */
13911432
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

0 commit comments

Comments
 (0)