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;
 }/*}}}*/