From 5455d2c741daa8e7b9db51ab43fbbd1c5eb773bc Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Sun, 18 Oct 2026 09:23:49 +0200 Subject: [PATCH] Dispatch async signals during PHP time_sleep_until() --- NEWS | 2 ++ UPGRADING | 5 ++++ ext/standard/basic_functions.c | 25 ++++++++++++---- .../misc/time_sleep_until_async_signals.phpt | 30 +++++++++++++++++++ ...e_sleep_until_async_signals_exception.phpt | 29 ++++++++++++++++++ .../time_sleep_until_async_signals_exit.phpt | 24 +++++++++++++++ .../misc/time_sleep_until_manual_signals.phpt | 27 +++++++++++++++++ 7 files changed, 136 insertions(+), 6 deletions(-) create mode 100644 ext/standard/tests/misc/time_sleep_until_async_signals.phpt create mode 100644 ext/standard/tests/misc/time_sleep_until_async_signals_exception.phpt create mode 100644 ext/standard/tests/misc/time_sleep_until_async_signals_exit.phpt create mode 100644 ext/standard/tests/misc/time_sleep_until_manual_signals.phpt diff --git a/NEWS b/NEWS index 4a9a2135ee29..5cbe2d62e197 100644 --- a/NEWS +++ b/NEWS @@ -123,6 +123,8 @@ PHP NEWS sockets. (David Carlier) - Standard: + . Dispatch asynchronous signal handlers while time_sleep_until() waits. + (Nicolas Grekas) . Fixed bug #60110 (fclose(), file_put_contents(), copy() do not return false properly). (Jakub Zelenka, Ilija Tovilo) . Fixed three Windows-only proc_open() defects: an uninitialized diff --git a/UPGRADING b/UPGRADING index 9442aad84504..a780f70903e8 100644 --- a/UPGRADING +++ b/UPGRADING @@ -37,6 +37,11 @@ PHP 8.7 UPGRADE NOTES application terminate one attribute and append further ones. - Standard: + . With pcntl_async_signals(true), time_sleep_until() now runs signal + handlers before the sleep ends. Previously, handlers waited until the + deadline. If a handler returns normally, the sleep resumes until the + original deadline. A handler can now interrupt the sleep by throwing an + exception or calling exit. . The number of filters that can be chained in a php://filter URL is limited to 16 by default. Set the stream context option max_filter_count to change this. diff --git a/ext/standard/basic_functions.c b/ext/standard/basic_functions.c index 1665feac37df..0d18c366371f 100644 --- a/ext/standard/basic_functions.c +++ b/ext/standard/basic_functions.c @@ -31,6 +31,7 @@ #include "ext/standard/info.h" #include "ext/session/php_session.h" #include "zend_exceptions.h" +#include "zend_hrtime.h" #include "zend_ini.h" #include "zend_operators.h" #include "ext/standard/php_dns.h" @@ -1206,8 +1207,9 @@ PHP_FUNCTION(time_sleep_until) { double target_secs; struct timeval tm; - struct timespec php_req, php_rem; + struct timespec php_req; uint64_t current_ns, target_ns, diff_ns; + zend_hrtime_t start; const uint64_t ns_per_sec = 1000000000; const double top_target_sec = (double)(UINT64_MAX / ns_per_sec); @@ -1218,6 +1220,7 @@ PHP_FUNCTION(time_sleep_until) if (gettimeofday((struct timeval *) &tm, NULL) != 0) { RETURN_FALSE; } + start = zend_hrtime(); if (UNEXPECTED(!(target_secs >= 0 && target_secs <= top_target_sec))) { zend_argument_value_error(1, "must be between 0 and %" PRIu64, (uint64_t)top_target_sec); @@ -1235,13 +1238,23 @@ PHP_FUNCTION(time_sleep_until) php_req.tv_sec = (time_t) (diff_ns / ns_per_sec); php_req.tv_nsec = (long) (diff_ns % ns_per_sec); - while (nanosleep(&php_req, &php_rem)) { - if (errno == EINTR) { - php_req.tv_sec = php_rem.tv_sec; - php_req.tv_nsec = php_rem.tv_nsec; - } else { + while (nanosleep(&php_req, NULL)) { + if (errno != EINTR) { RETURN_FALSE; } + if (UNEXPECTED(atomic_load(&EG(vm_interrupt)))) { + zend_fcall_interrupt(execute_data); + if (UNEXPECTED(EG(exception))) { + RETURN_THROWS(); + } + } + zend_hrtime_t elapsed = zend_hrtime() - start; + if (elapsed >= diff_ns) { + break; + } + uint64_t remaining_ns = diff_ns - elapsed; + php_req.tv_sec = (time_t) (remaining_ns / ns_per_sec); + php_req.tv_nsec = (long) (remaining_ns % ns_per_sec); } RETURN_TRUE; diff --git a/ext/standard/tests/misc/time_sleep_until_async_signals.phpt b/ext/standard/tests/misc/time_sleep_until_async_signals.phpt new file mode 100644 index 000000000000..51e6a1b4d31a --- /dev/null +++ b/ext/standard/tests/misc/time_sleep_until_async_signals.phpt @@ -0,0 +1,30 @@ +--TEST-- +time_sleep_until() dispatches async signals and keeps the original deadline +--EXTENSIONS-- +pcntl +--SKIPIF-- + +--FILE-- += $deadline); +var_dump(microtime(true) < $deadline + 0.8); +?> +--EXPECT-- +bool(true) +bool(true) +bool(true) +bool(true) diff --git a/ext/standard/tests/misc/time_sleep_until_async_signals_exception.phpt b/ext/standard/tests/misc/time_sleep_until_async_signals_exception.phpt new file mode 100644 index 000000000000..9ad0df39c4b6 --- /dev/null +++ b/ext/standard/tests/misc/time_sleep_until_async_signals_exception.phpt @@ -0,0 +1,29 @@ +--TEST-- +An async signal handler can throw during time_sleep_until() +--EXTENSIONS-- +pcntl +--SKIPIF-- + +--FILE-- +getMessage(), "\n"; +} +?> +--EXPECT-- +bool(true) +Interrupted diff --git a/ext/standard/tests/misc/time_sleep_until_async_signals_exit.phpt b/ext/standard/tests/misc/time_sleep_until_async_signals_exit.phpt new file mode 100644 index 000000000000..a0d879779db2 --- /dev/null +++ b/ext/standard/tests/misc/time_sleep_until_async_signals_exit.phpt @@ -0,0 +1,24 @@ +--TEST-- +An async signal handler can exit during time_sleep_until() +--EXTENSIONS-- +pcntl +--SKIPIF-- + +--FILE-- + +--EXPECT-- +bool(true) diff --git a/ext/standard/tests/misc/time_sleep_until_manual_signals.phpt b/ext/standard/tests/misc/time_sleep_until_manual_signals.phpt new file mode 100644 index 000000000000..aada70df57b4 --- /dev/null +++ b/ext/standard/tests/misc/time_sleep_until_manual_signals.phpt @@ -0,0 +1,27 @@ +--TEST-- +time_sleep_until() leaves manually dispatched signals pending +--EXTENSIONS-- +pcntl +--SKIPIF-- + +--FILE-- + +--EXPECT-- +bool(true) +bool(false) +bool(true)