Commit 294f50b527c for php
commit 294f50b527ce4c3772ff78d395af28754a2379eb
Author: Nicolas Grekas <nicolas.grekas@gmail.com>
Date: Tue Oct 20 18:16:55 2026 +0200
Make usleep() and time_nanosleep() alertable on Windows (#24399)
diff --git a/NEWS b/NEWS
index d4325d008a6..48afd654b2e 100644
--- a/NEWS
+++ b/NEWS
@@ -106,6 +106,8 @@ PHP NEWS
file fails after it was already truncated. (Ilia Alshanetsky)
- Standard:
+ . Fixed bug GH-24399 (usleep() and time_nanosleep() ignore queued APCs on
+ Windows). (Nicolas Grekas)
. Fixed chown() and lchown() failing to resolve user names in ZTS builds
when getpwnam_r() needs a larger buffer. (Ilia Alshanetsky)
. Fixed password_get_info() and password_needs_rehash() accepting malformed
diff --git a/ext/standard/basic_functions.c b/ext/standard/basic_functions.c
index ae1a1ef8666..f9ab71cf6d7 100644
--- a/ext/standard/basic_functions.c
+++ b/ext/standard/basic_functions.c
@@ -36,6 +36,7 @@
#include "zend_attributes.h"
#include "zend_enum.h"
#include "zend_ini.h"
+#include "zend_hrtime.h"
#include "zend_operators.h"
#include "ext/standard/php_dns.h"
#include "ext/standard/php_uuencode.h"
@@ -1200,8 +1201,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);
@@ -1212,6 +1214,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);
@@ -1229,13 +1232,17 @@ 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;
}
+ 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/general_functions/sleep_alertable_win32.phpt b/ext/standard/tests/general_functions/sleep_alertable_win32.phpt
new file mode 100644
index 00000000000..55da154ccf7
--- /dev/null
+++ b/ext/standard/tests/general_functions/sleep_alertable_win32.phpt
@@ -0,0 +1,78 @@
+--TEST--
+Sleep functions process queued APCs on Windows
+--EXTENSIONS--
+ffi
+--SKIPIF--
+<?php
+if (PHP_OS_FAMILY !== 'Windows') die('skip Windows only');
+?>
+--INI--
+ffi.enable=1
+--FILE--
+<?php
+$callconv = PHP_INT_SIZE === 4 ? '__stdcall' : '';
+// kernel32.dll exports undecorated names: resolve them with the default
+// calling convention, then cast them to their actual calling convention
+$kernel32 = FFI::cdef(<<<C
+ void *GetCurrentThread(void);
+ unsigned long QueueUserAPC(void *callback, void *thread, uintptr_t data);
+ unsigned long SleepEx(unsigned long milliseconds, int alertable);
+C, 'kernel32.dll');
+$ffi = FFI::cdef(<<<C
+ typedef void ($callconv *PAPCFUNC)(uintptr_t);
+ typedef void *($callconv *GetCurrentThread_t)(void);
+ typedef unsigned long ($callconv *QueueUserAPC_t)(PAPCFUNC callback, void *thread, uintptr_t data);
+ typedef unsigned long ($callconv *SleepEx_t)(unsigned long milliseconds, int alertable);
+C);
+$GetCurrentThread = $ffi->cast('GetCurrentThread_t', $kernel32->GetCurrentThread);
+$QueueUserAPC = $ffi->cast('QueueUserAPC_t', $kernel32->QueueUserAPC);
+$SleepEx = $ffi->cast('SleepEx_t', $kernel32->SleepEx);
+
+$called = 0;
+$callback = static function (int $data) use (&$called): void {
+ $called = $data;
+ // The time spent in APCs is not time slept
+ usleep(300000);
+};
+
+echo "usleep()\n";
+var_dump($QueueUserAPC($callback, $GetCurrentThread(), 1) !== 0);
+var_dump(usleep(1000000));
+var_dump($called === 1);
+
+echo "time_nanosleep()\n";
+var_dump($QueueUserAPC($callback, $GetCurrentThread(), 2) !== 0);
+var_dump(time_nanosleep(2, 0));
+var_dump($called === 2);
+
+echo "time_sleep_until()\n";
+var_dump($QueueUserAPC($callback, $GetCurrentThread(), 3) !== 0);
+$deadline = microtime(true) + 0.5;
+var_dump(time_sleep_until($deadline));
+var_dump($called === 3);
+$now = microtime(true);
+var_dump($now + 0.002 >= $deadline);
+var_dump($now < $deadline + 0.25);
+
+$SleepEx(0, 1);
+?>
+--EXPECT--
+usleep()
+bool(true)
+NULL
+bool(true)
+time_nanosleep()
+bool(true)
+array(2) {
+ ["seconds"]=>
+ int(2)
+ ["nanoseconds"]=>
+ int(0)
+}
+bool(true)
+time_sleep_until()
+bool(true)
+bool(true)
+bool(true)
+bool(true)
+bool(true)
diff --git a/win32/time.c b/win32/time.c
index ac1abbc9e9c..192d2fa3401 100644
--- a/win32/time.c
+++ b/win32/time.c
@@ -22,6 +22,7 @@
#include <mmsystem.h>
#include <errno.h>
#include "php_win32_globals.h"
+#include "zend_hrtime.h"
static zend_always_inline void getfilesystemtime(struct timeval *tv)
{/*{{{*/
@@ -63,17 +64,44 @@ PHPAPI int gettimeofday(struct timeval *time_Info, struct timezone *timezone_Inf
return 0;
}/*}}}*/
+static DWORD wait_timer_alertable(HANDLE timer)
+{/*{{{*/
+ DWORD result;
+
+#ifdef ZTS
+ /* Threads that PHP does not manage have no executor globals, and their APCs cannot run PHP code */
+ if (!tsrm_is_managed_thread()) {
+ return WaitForSingleObjectEx(timer, INFINITE, TRUE);
+ }
+#endif
+
+ /* APCs can run PHP code that bails out. Close the timer before propagating the bailout. */
+ zend_try {
+ result = WaitForSingleObjectEx(timer, INFINITE, TRUE);
+ } zend_catch {
+ CloseHandle(timer);
+ zend_bailout();
+ } zend_end_try();
+
+ return result;
+}/*}}}*/
+
PHPAPI int usleep(unsigned int useconds)
{/*{{{*/
HANDLE timer;
LARGE_INTEGER due;
+ DWORD result;
due.QuadPart = -(10 * (__int64)useconds);
timer = CreateWaitableTimer(NULL, TRUE, NULL);
SetWaitableTimer(timer, &due, 0, NULL, NULL, 0);
- WaitForSingleObject(timer, INFINITE);
+ result = wait_timer_alertable(timer);
CloseHandle(timer);
+ if (result == WAIT_IO_COMPLETION) {
+ errno = EINTR;
+ return -1;
+ }
return 0;
}/*}}}*/
@@ -84,5 +112,30 @@ PHPAPI int nanosleep( const struct timespec * rqtp, struct timespec * rmtp )
errno = EINVAL;
return -1;
}
- return usleep( rqtp->tv_sec * 1000000 + rqtp->tv_nsec / 1000 );
+ /* Run already queued APCs first, so that their runtime doesn't count as time slept */
+ if (SleepEx(0, TRUE) == WAIT_IO_COMPLETION) {
+ if (rmtp != NULL) {
+ *rmtp = *rqtp;
+ }
+ errno = EINTR;
+ return -1;
+ }
+ zend_hrtime_t start = zend_hrtime();
+ int result = usleep(rqtp->tv_sec * 1000000 + rqtp->tv_nsec / 1000);
+ if (result == -1 && errno == EINTR && rmtp != NULL) {
+ zend_hrtime_t elapsed = zend_hrtime() - start;
+ time_t seconds = rqtp->tv_sec - (time_t) (elapsed / ZEND_NANO_IN_SEC);
+ long nanoseconds = rqtp->tv_nsec - (long) (elapsed % ZEND_NANO_IN_SEC);
+ if (nanoseconds < 0) {
+ seconds--;
+ nanoseconds += (long) ZEND_NANO_IN_SEC;
+ }
+ if (seconds < 0) {
+ seconds = 0;
+ nanoseconds = 0;
+ }
+ rmtp->tv_sec = seconds;
+ rmtp->tv_nsec = nanoseconds;
+ }
+ return result;
}/*}}}*/