From dffe371fef7d43a39cfccee1d8640270780935c1 Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Sun, 18 Oct 2026 09:19:39 +0200 Subject: [PATCH 1/3] Make PHP usleep() and time_nanosleep() alertable on Windows --- NEWS | 2 + .../sleep_alertable_win32.phpt | 68 +++++++++++++++++++ win32/time.c | 27 +++++++- 3 files changed, 95 insertions(+), 2 deletions(-) create mode 100644 ext/standard/tests/general_functions/sleep_alertable_win32.phpt diff --git a/NEWS b/NEWS index aa752e0af386..b5fc9501d343 100644 --- a/NEWS +++ b/NEWS @@ -91,6 +91,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/tests/general_functions/sleep_alertable_win32.phpt b/ext/standard/tests/general_functions/sleep_alertable_win32.phpt new file mode 100644 index 000000000000..035c3ccfc03d --- /dev/null +++ b/ext/standard/tests/general_functions/sleep_alertable_win32.phpt @@ -0,0 +1,68 @@ +--TEST-- +Sleep functions process queued APCs on Windows +--EXTENSIONS-- +ffi +--SKIPIF-- + +--INI-- +ffi.enable=1 +--FILE-- +QueueUserAPC($callback, $ffi->GetCurrentThread(), 1) !== 0); +var_dump(usleep(1000000)); +var_dump($called === 1); + +echo "time_nanosleep()\n"; +var_dump($ffi->QueueUserAPC($callback, $ffi->GetCurrentThread(), 2) !== 0); +$remaining = time_nanosleep(2, 0); +var_dump($called === 2); +var_dump(is_array($remaining)); +if (is_array($remaining)) { + var_dump($remaining['seconds'] >= 0 && $remaining['seconds'] <= 2); + var_dump($remaining['nanoseconds'] >= 0 && $remaining['nanoseconds'] < 1000000000); + var_dump($remaining['seconds'] > 0 || $remaining['nanoseconds'] > 0); + var_dump($remaining['seconds'] < 2 || $remaining['nanoseconds'] === 0); +} + +echo "time_sleep_until()\n"; +var_dump($ffi->QueueUserAPC($callback, $ffi->GetCurrentThread(), 3) !== 0); +$deadline = microtime(true) + 0.1; +var_dump(time_sleep_until($deadline)); +var_dump($called === 3); +var_dump(microtime(true) + 0.002 >= $deadline); + +$ffi->SleepEx(0, 1); +?> +--EXPECT-- +usleep() +bool(true) +NULL +bool(true) +time_nanosleep() +bool(true) +bool(true) +bool(true) +bool(true) +bool(true) +bool(true) +bool(true) +time_sleep_until() +bool(true) +bool(true) +bool(true) +bool(true) diff --git a/win32/time.c b/win32/time.c index ac1abbc9e9c7..29178b408b01 100644 --- a/win32/time.c +++ b/win32/time.c @@ -22,6 +22,7 @@ #include #include #include "php_win32_globals.h" +#include "zend_hrtime.h" static zend_always_inline void getfilesystemtime(struct timeval *tv) {/*{{{*/ @@ -67,13 +68,18 @@ 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 = WaitForSingleObjectEx(timer, INFINITE, TRUE); CloseHandle(timer); + if (result == WAIT_IO_COMPLETION) { + errno = EINTR; + return -1; + } return 0; }/*}}}*/ @@ -84,5 +90,22 @@ PHPAPI int nanosleep( const struct timespec * rqtp, struct timespec * rmtp ) errno = EINVAL; return -1; } - return usleep( rqtp->tv_sec * 1000000 + rqtp->tv_nsec / 1000 ); + 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; }/*}}}*/ From 26d3427937e903e94cfac2bc81ec4bc0ffbe3eb4 Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Sun, 18 Oct 2026 11:01:37 +0200 Subject: [PATCH 2/3] Fix Windows APC test calling conventions on x64 --- .../general_functions/sleep_alertable_win32.phpt | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/ext/standard/tests/general_functions/sleep_alertable_win32.phpt b/ext/standard/tests/general_functions/sleep_alertable_win32.phpt index 035c3ccfc03d..5e949bc8f40a 100644 --- a/ext/standard/tests/general_functions/sleep_alertable_win32.phpt +++ b/ext/standard/tests/general_functions/sleep_alertable_win32.phpt @@ -10,11 +10,12 @@ if (PHP_OS_FAMILY !== 'Windows') die('skip Windows only'); ffi.enable=1 --FILE-- Date: Mon, 19 Oct 2026 14:47:37 +0200 Subject: [PATCH 3/3] Close the timer when an APC bails out and don't count queued APCs as time slept time_sleep_until() now resumes against its deadline. The test resolves undecorated kernel32.dll exports, so that it also works on x86. --- ext/standard/basic_functions.c | 19 +++++-- .../sleep_alertable_win32.phpt | 55 +++++++++++-------- win32/time.c | 32 ++++++++++- 3 files changed, 76 insertions(+), 30 deletions(-) diff --git a/ext/standard/basic_functions.c b/ext/standard/basic_functions.c index ae1a1ef8666f..f9ab71cf6d70 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 index 5e949bc8f40a..55da154ccf75 100644 --- a/ext/standard/tests/general_functions/sleep_alertable_win32.phpt +++ b/ext/standard/tests/general_functions/sleep_alertable_win32.phpt @@ -11,43 +11,50 @@ ffi.enable=1 --FILE-- 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($ffi->QueueUserAPC($callback, $ffi->GetCurrentThread(), 1) !== 0); +var_dump($QueueUserAPC($callback, $GetCurrentThread(), 1) !== 0); var_dump(usleep(1000000)); var_dump($called === 1); echo "time_nanosleep()\n"; -var_dump($ffi->QueueUserAPC($callback, $ffi->GetCurrentThread(), 2) !== 0); -$remaining = time_nanosleep(2, 0); +var_dump($QueueUserAPC($callback, $GetCurrentThread(), 2) !== 0); +var_dump(time_nanosleep(2, 0)); var_dump($called === 2); -var_dump(is_array($remaining)); -if (is_array($remaining)) { - var_dump($remaining['seconds'] >= 0 && $remaining['seconds'] <= 2); - var_dump($remaining['nanoseconds'] >= 0 && $remaining['nanoseconds'] < 1000000000); - var_dump($remaining['seconds'] > 0 || $remaining['nanoseconds'] > 0); - var_dump($remaining['seconds'] < 2 || $remaining['nanoseconds'] === 0); -} echo "time_sleep_until()\n"; -var_dump($ffi->QueueUserAPC($callback, $ffi->GetCurrentThread(), 3) !== 0); -$deadline = microtime(true) + 0.1; +var_dump($QueueUserAPC($callback, $GetCurrentThread(), 3) !== 0); +$deadline = microtime(true) + 0.5; var_dump(time_sleep_until($deadline)); var_dump($called === 3); -var_dump(microtime(true) + 0.002 >= $deadline); +$now = microtime(true); +var_dump($now + 0.002 >= $deadline); +var_dump($now < $deadline + 0.25); -$ffi->SleepEx(0, 1); +$SleepEx(0, 1); ?> --EXPECT-- usleep() @@ -56,14 +63,16 @@ NULL bool(true) time_nanosleep() bool(true) -bool(true) -bool(true) -bool(true) -bool(true) -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 29178b408b01..192d2fa34019 100644 --- a/win32/time.c +++ b/win32/time.c @@ -64,6 +64,28 @@ 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; @@ -74,7 +96,7 @@ PHPAPI int usleep(unsigned int useconds) timer = CreateWaitableTimer(NULL, TRUE, NULL); SetWaitableTimer(timer, &due, 0, NULL, NULL, 0); - result = WaitForSingleObjectEx(timer, INFINITE, TRUE); + result = wait_timer_alertable(timer); CloseHandle(timer); if (result == WAIT_IO_COMPLETION) { errno = EINTR; @@ -90,6 +112,14 @@ PHPAPI int nanosleep( const struct timespec * rqtp, struct timespec * rmtp ) errno = EINVAL; return -1; } + /* 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) {