Commit c1773a1da24 for php.net
commit c1773a1da24b840b7fe53d24c460fc4bf99fb387
Author: Ilia Alshanetsky <ilia@ilia.ws>
Date: Thu Jul 16 08:45:52 2026 -0400
ext/ftp: apply the connect timeout ceiling to the remaining entry points
GH-20601 capped the timeout in ftp_connect() so a value that overflows the
timeval conversion cannot reach php_network_set_limit_time(). ftp_ssl_connect()
and ftp_set_option(FTP_TIMEOUT_SEC) kept rejecting only non-positive values, so
PHP_INT_MAX still reached ftp_open() and my_poll() through them.
Apply the same bound to both, factored into a shared PHP_FTP_TIMEOUT_SEC_MAX
macro. ftp_ssl_connect() with PHP_INT_MAX hung before this rather than
reporting the bad argument.
Closes GH-22767
diff --git a/ext/ftp/php_ftp.c b/ext/ftp/php_ftp.c
index 432270cff19..5cb7fff4886 100644
--- a/ext/ftp/php_ftp.c
+++ b/ext/ftp/php_ftp.c
@@ -36,6 +36,8 @@
#include "ftp.h"
#include "ftp_arginfo.h"
+#define PHP_FTP_TIMEOUT_SEC_MAX ((uint64_t)((double) PHP_TIMEOUT_ULL_MAX / 1000000.0))
+
static zend_class_entry *php_ftp_ce = NULL;
static zend_object_handlers ftp_object_handlers;
@@ -147,15 +149,13 @@ PHP_FUNCTION(ftp_connect)
RETURN_THROWS();
}
- const uint64_t timeoutmax = (uint64_t)((double) PHP_TIMEOUT_ULL_MAX / 1000000.0);
-
if (timeout_sec <= 0) {
zend_argument_value_error(3, "must be greater than 0");
RETURN_THROWS();
}
- if (timeout_sec >= timeoutmax) {
- zend_argument_value_error(3, "must be less than " ZEND_ULONG_FMT, timeoutmax);
+ if (timeout_sec >= PHP_FTP_TIMEOUT_SEC_MAX) {
+ zend_argument_value_error(3, "must be less than " ZEND_ULONG_FMT, PHP_FTP_TIMEOUT_SEC_MAX);
RETURN_THROWS();
}
@@ -196,6 +196,11 @@ PHP_FUNCTION(ftp_ssl_connect)
RETURN_THROWS();
}
+ if (timeout_sec >= PHP_FTP_TIMEOUT_SEC_MAX) {
+ zend_argument_value_error(3, "must be less than " ZEND_ULONG_FMT, PHP_FTP_TIMEOUT_SEC_MAX);
+ RETURN_THROWS();
+ }
+
/* connect */
if (!(ftp = ftp_open(host, (short)port, timeout_sec))) {
RETURN_FALSE;
@@ -1275,6 +1280,10 @@ PHP_FUNCTION(ftp_set_option)
zend_argument_value_error(3, "must be greater than 0 for the FTP_TIMEOUT_SEC option");
RETURN_THROWS();
}
+ if ((uint64_t) Z_LVAL_P(z_value) >= PHP_FTP_TIMEOUT_SEC_MAX) {
+ zend_argument_value_error(3, "must be less than " ZEND_ULONG_FMT " for the FTP_TIMEOUT_SEC option", PHP_FTP_TIMEOUT_SEC_MAX);
+ RETURN_THROWS();
+ }
ftp->timeout_sec = Z_LVAL_P(z_value);
RETURN_TRUE;
break;
diff --git a/ext/ftp/tests/gh20601_set_option.phpt b/ext/ftp/tests/gh20601_set_option.phpt
new file mode 100644
index 00000000000..3317d4b5f2a
--- /dev/null
+++ b/ext/ftp/tests/gh20601_set_option.phpt
@@ -0,0 +1,26 @@
+--TEST--
+GH-20601 (ftp_set_option FTP_TIMEOUT_SEC timeout overflow)
+--EXTENSIONS--
+ftp
+pcntl
+--SKIPIF--
+<?php
+if (PHP_INT_SIZE != 8) die("skip: 64-bit only");
+if (PHP_OS_FAMILY === 'Windows') die("skip not for windows");
+?>
+--FILE--
+<?php
+require 'server.inc';
+
+$ftp = ftp_connect('127.0.0.1', $port);
+$ftp or die("Couldn't connect to the server");
+ftp_login($ftp, 'user', 'pass');
+
+try {
+ ftp_set_option($ftp, FTP_TIMEOUT_SEC, PHP_INT_MAX);
+} catch (\ValueError $e) {
+ echo $e->getMessage();
+}
+?>
+--EXPECTF--
+ftp_set_option(): Argument #3 ($value) must be less than %d for the FTP_TIMEOUT_SEC option
diff --git a/ext/ftp/tests/gh20601_ssl.phpt b/ext/ftp/tests/gh20601_ssl.phpt
new file mode 100644
index 00000000000..005f866e18e
--- /dev/null
+++ b/ext/ftp/tests/gh20601_ssl.phpt
@@ -0,0 +1,21 @@
+--TEST--
+GH-20601 (ftp_ssl_connect timeout overflow)
+--EXTENSIONS--
+ftp
+openssl
+--SKIPIF--
+<?php
+if (!function_exists("ftp_ssl_connect")) die("skip ftp_ssl is disabled");
+if (PHP_INT_SIZE != 8) die("skip: 64-bit only");
+if (PHP_OS_FAMILY === 'Windows') die("skip not for windows");
+?>
+--FILE--
+<?php
+try {
+ ftp_ssl_connect('127.0.0.1', 1024, PHP_INT_MAX);
+} catch (\ValueError $e) {
+ echo $e->getMessage();
+}
+?>
+--EXPECTF--
+ftp_ssl_connect(): Argument #3 ($timeout) must be less than %d