Skip to content

Commit 6fd9608

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 74e3a69 commit 6fd9608

4 files changed

Lines changed: 146 additions & 11 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
- SPL:
1519
. Fixed bug GH-23385 (SplDoublyLinkedList::serialize() use-after-free when
1620
__serialize() removes an element). (David Carlier)

‎ext/pcntl/pcntl.c‎

Lines changed: 51 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
#include "ext/standard/info.h"
3030
#include "php_signal.h"
3131
#include "php_ticks.h"
32+
#include "zend_exceptions.h"
3233
#include "zend_fibers.h"
3334
#include "main/php_main.h"
3435

@@ -1352,6 +1353,8 @@ void pcntl_signal_dispatch(void)
13521353
{
13531354
zval params[2], *handle, retval;
13541355
struct php_pcntl_pending_signal *queue, *next;
1356+
zend_object *old_exception;
1357+
const zend_op *old_opline_before_exception = NULL;
13551358
sigset_t mask;
13561359
sigset_t old_mask;
13571360

@@ -1379,8 +1382,21 @@ void pcntl_signal_dispatch(void)
13791382
PCNTL_G(head) = NULL; /* simple stores are atomic */
13801383
PCNTL_G(tail) = NULL;
13811384

1385+
/* Dispatching can happen while an exception is propagating, typically from the interrupt
1386+
* check that runs right after an internal function returned with an exception pending.
1387+
* Handlers cannot be called in that state, so set the exception aside while they run,
1388+
* the way destructors are called during unwinding. */
1389+
old_exception = EG(exception);
1390+
if (old_exception && EG(current_execute_data)) {
1391+
EG(current_execute_data)->opline = EG(opline_before_exception);
1392+
old_opline_before_exception = EG(opline_before_exception);
1393+
}
1394+
EG(exception) = NULL;
1395+
13821396
/* Allocate */
13831397
while (queue) {
1398+
bool handler_threw = false;
1399+
13841400
if ((handle = zend_hash_index_find(&PCNTL_G(php_signal_table), queue->signo)) != NULL) {
13851401
if (Z_TYPE_P(handle) != IS_LONG) {
13861402
ZVAL_LONG(&params[0], queue->signo);
@@ -1391,28 +1407,52 @@ void pcntl_signal_dispatch(void)
13911407
call_user_function(NULL, NULL, handle, &retval, 2, params);
13921408
zval_ptr_dtor(&retval);
13931409
zval_ptr_dtor(&params[1]);
1394-
1395-
if (EG(exception)) {
1396-
break;
1397-
}
1410+
handler_threw = NULL != EG(exception);
13981411
}
13991412
}
14001413

14011414
next = queue->next;
14021415
queue->next = PCNTL_G(spares);
14031416
PCNTL_G(spares) = queue;
14041417
queue = next;
1418+
1419+
/* No other handler can be called while the exception propagates */
1420+
if (handler_threw) {
1421+
break;
1422+
}
14051423
}
14061424

1407-
/* drain the remaining in case of exception thrown */
1408-
while (queue) {
1409-
next = queue->next;
1410-
queue->next = PCNTL_G(spares);
1411-
PCNTL_G(spares) = queue;
1412-
queue = next;
1425+
if (old_exception) {
1426+
if (EG(current_execute_data)) {
1427+
EG(current_execute_data)->opline = EG(exception_op);
1428+
EG(opline_before_exception) = old_opline_before_exception;
1429+
}
1430+
if (EG(exception)) {
1431+
zend_exception_set_previous(EG(exception), old_exception);
1432+
} else {
1433+
EG(exception) = old_exception;
1434+
}
14131435
}
14141436

1415-
PCNTL_G(pending_signals) = false;
1437+
if (UNEXPECTED(queue)) {
1438+
/* The signals a throwing handler left behind go back to the queue instead of being
1439+
* dropped, and the engine is asked to dispatch again once that exception is handled.
1440+
* Signals are still blocked here, so PCNTL_G(head) cannot have been repopulated. */
1441+
next = queue;
1442+
1443+
while (next->next) {
1444+
next = next->next;
1445+
}
1446+
1447+
PCNTL_G(head) = queue;
1448+
PCNTL_G(tail) = next;
1449+
1450+
if (PCNTL_G(async_signals)) {
1451+
zend_atomic_bool_store_ex(&EG(vm_interrupt), true);
1452+
}
1453+
} else {
1454+
PCNTL_G(pending_signals) = false;
1455+
}
14161456

14171457
/* Re-enable queue */
14181458
PCNTL_G(processing_signal_queue) = false;
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)