Skip to content

Commit 23bfbd0

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 zend_lookup_class_ex() does around autoloading, lets the handlers run. 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, so the next dispatch delivers them. 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 7ef3bfe commit 23bfbd0

3 files changed

Lines changed: 77 additions & 11 deletions

File tree

‎NEWS‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@ PHP NEWS
22
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
33
?? ??? ????, PHP 8.4.27
44

5+
- PCNTL:
6+
. Fixed pcntl_signal_dispatch() dropping queued signals when it runs while an
7+
exception is pending. (nicolas-grekas)
8+
59

610
24 Sep 2026, PHP 8.4.26
711

‎ext/pcntl/pcntl.c‎

Lines changed: 29 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1345,8 +1345,15 @@ void pcntl_signal_dispatch(void)
13451345
PCNTL_G(head) = NULL; /* simple stores are atomic */
13461346
PCNTL_G(tail) = NULL;
13471347

1348+
/* Dispatching can happen while an exception is propagating, typically from the interrupt
1349+
* handler that runs right after an internal function returned with an exception pending.
1350+
* Handlers cannot be called in that state, so set the exception aside for the time being. */
1351+
zend_exception_save();
1352+
13481353
/* Allocate */
13491354
while (queue) {
1355+
bool handler_threw = false;
1356+
13501357
if ((handle = zend_hash_index_find(&PCNTL_G(php_signal_table), queue->signo)) != NULL) {
13511358
if (Z_TYPE_P(handle) != IS_LONG) {
13521359
ZVAL_NULL(&retval);
@@ -1365,27 +1372,38 @@ void pcntl_signal_dispatch(void)
13651372
#ifdef HAVE_STRUCT_SIGINFO_T
13661373
zval_ptr_dtor(&params[1]);
13671374
#endif
1368-
if (EG(exception)) {
1369-
break;
1370-
}
1375+
handler_threw = NULL != EG(exception);
13711376
}
13721377
}
13731378

13741379
next = queue->next;
13751380
queue->next = PCNTL_G(spares);
13761381
PCNTL_G(spares) = queue;
13771382
queue = next;
1378-
}
13791383

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;
1384+
/* No other handler can be called while the exception propagates */
1385+
if (handler_threw) {
1386+
break;
1387+
}
13861388
}
13871389

1388-
PCNTL_G(pending_signals) = 0;
1390+
zend_exception_restore();
1391+
1392+
if (UNEXPECTED(queue)) {
1393+
/* The signals a throwing handler left behind go back to the queue instead of being
1394+
* dropped, so that the next dispatch delivers them. Signals are still blocked here, so
1395+
* PCNTL_G(head) cannot have been repopulated in the meantime. */
1396+
next = queue;
1397+
1398+
while (next->next) {
1399+
next = next->next;
1400+
}
1401+
1402+
PCNTL_G(head) = queue;
1403+
PCNTL_G(tail) = next;
1404+
} else {
1405+
PCNTL_G(pending_signals) = 0;
1406+
}
13891407

13901408
/* Re-enable queue */
13911409
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

0 commit comments

Comments
 (0)