Commit 46e55dd97ce for php.net

commit 46e55dd97ceea10267fe935c2c5abc2a05c8130b
Author: James Lucas <james@lucas.net.au>
Date:   Mon Dec 15 21:23:44 2025 +1100

    ext/sockets: adding Linux's TCP_USER_TIMEOUT constant.

    Set TCP_USER_TIMEOUT to cap (ms) how long TCP will wait for ACKs on in-flight data
    before aborting the connection; prevents stuck/half-open sessions and
    enables faster failover vs default retransmission timeouts.

    Co-authored-by: David Carlier <devnexen@gmail.com>

    close GH-20708

diff --git a/NEWS b/NEWS
index cea609f1d72..12740c65c3c 100644
--- a/NEWS
+++ b/NEWS
@@ -57,6 +57,10 @@ PHP                                                                        NEWS
   . Soap::__setCookie() when cookie name is a digit is now not stored and represented
     as a string anymore but a int. (David Carlier)

+- Sockets:
+  . Added the TCP_USER_TIMEOUT constant for Linux to set the maximum time in milliseconds
+        transmitted data can remain unacknowledged. (James Lucas)
+
 - SPL:
   . DirectoryIterator key can now work better with filesystem supporting larger
     directory indexing. (David Carlier)
diff --git a/UPGRADING b/UPGRADING
index 9a2159a33e7..d52827bf961 100644
--- a/UPGRADING
+++ b/UPGRADING
@@ -94,6 +94,9 @@ PHP 8.6 UPGRADE NOTES
 10. New Global Constants
 ========================================

+- Sockets:
+  . TCP_USER_TIMEOUT (Linux only).
+
 ========================================
 11. Changes to INI File Handling
 ========================================
diff --git a/ext/sockets/sockets.c b/ext/sockets/sockets.c
index 4a9332498c3..b76818830fc 100644
--- a/ext/sockets/sockets.c
+++ b/ext/sockets/sockets.c
@@ -1773,7 +1773,7 @@ PHP_FUNCTION(socket_sendto)
 				RETURN_THROWS();
 			}

-			memset(&sll, 0, sizeof(sll));
+			memset(&sll, 0, sizeof(sll));
 			sll.sll_family = AF_PACKET;
 			sll.sll_ifindex = port;

@@ -2130,6 +2130,28 @@ PHP_FUNCTION(socket_set_option)
 		}
 #endif

+#if defined(TCP_USER_TIMEOUT)
+		case TCP_USER_TIMEOUT: {
+			zend_long timeout = zval_get_long(arg4);
+
+			// TCP_USER_TIMEOUT unsigned int
+			if (timeout < 0 || timeout > UINT_MAX) {
+				zend_argument_value_error(4, "must be of between 0 and %u", UINT_MAX);
+				RETURN_THROWS();
+			}
+
+			unsigned int val = (unsigned int)timeout;
+			optlen = sizeof(val);
+			opt_ptr = &val;
+			if (setsockopt(php_sock->bsd_socket, level, optname, opt_ptr, optlen) != 0) {
+				PHP_SOCKET_ERROR(php_sock, "Unable to set socket option", errno);
+				RETURN_FALSE;
+			}
+
+			RETURN_TRUE;
+		}
+#endif
+
 		}
 	}

diff --git a/ext/sockets/sockets.stub.php b/ext/sockets/sockets.stub.php
index 3df9b598a1e..04fb702807e 100644
--- a/ext/sockets/sockets.stub.php
+++ b/ext/sockets/sockets.stub.php
@@ -602,6 +602,13 @@
  */
 const TCP_SYNCNT = UNKNOWN;
 #endif
+#ifdef TCP_USER_TIMEOUT
+/**
+ * @var int
+ * @cvalue TCP_USER_TIMEOUT
+ */
+const TCP_USER_TIMEOUT = UNKNOWN;
+#endif
 #ifdef SO_ZEROCOPY
 /**
  * @var int
diff --git a/ext/sockets/sockets_arginfo.h b/ext/sockets/sockets_arginfo.h
index edfc344ff8c..0145d012528 100644
Binary files a/ext/sockets/sockets_arginfo.h and b/ext/sockets/sockets_arginfo.h differ
diff --git a/ext/sockets/tests/socket_setoption_tcpusertimeout_32bit.phpt b/ext/sockets/tests/socket_setoption_tcpusertimeout_32bit.phpt
new file mode 100644
index 00000000000..3f12120e54d
--- /dev/null
+++ b/ext/sockets/tests/socket_setoption_tcpusertimeout_32bit.phpt
@@ -0,0 +1,34 @@
+--TEST--
+Test if socket_set_option() works, option:TCP_USER_TIMEOUT
+--EXTENSIONS--
+sockets
+--SKIPIF--
+<?php
+if (!defined('TCP_USER_TIMEOUT')) { die('skip TCP_USER_TIMEOUT is not defined'); }
+if (PHP_INT_SIZE != 4) { die("skip 32-bit only"); }
+?>
+--FILE--
+<?php
+$socket = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
+if (!$socket) {
+        die('Unable to create AF_INET socket [socket]');
+}
+socket_set_block($socket);
+
+try {
+	socket_setopt($socket, SOL_TCP, TCP_USER_TIMEOUT, -1);
+} catch (\ValueError $e) {
+	echo $e->getMessage(), PHP_EOL;
+}
+
+$timeout = 200;
+$retval_2 = socket_set_option($socket, SOL_TCP, TCP_USER_TIMEOUT, $timeout);
+$retval_3 = socket_get_option($socket, SOL_TCP, TCP_USER_TIMEOUT);
+var_dump($retval_2);
+var_dump($retval_3 === $timeout);
+socket_close($socket);
+?>
+--EXPECTF--
+socket_setopt(): Argument #4 ($value) must be of between 0 and %d
+bool(true)
+bool(true)
diff --git a/ext/sockets/tests/socket_setoption_tcpusertimeout_64bit.phpt b/ext/sockets/tests/socket_setoption_tcpusertimeout_64bit.phpt
new file mode 100644
index 00000000000..8dbb7f80e48
--- /dev/null
+++ b/ext/sockets/tests/socket_setoption_tcpusertimeout_64bit.phpt
@@ -0,0 +1,41 @@
+--TEST--
+Test if socket_set_option() works, option:TCP_USER_TIMEOUT
+--EXTENSIONS--
+sockets
+--SKIPIF--
+<?php
+if (!defined('TCP_USER_TIMEOUT')) { die('skip TCP_USER_TIMEOUT is not defined'); }
+if (PHP_INT_SIZE != 8) { die("skip 64-bit only"); }
+?>
+--FILE--
+<?php
+$socket = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
+if (!$socket) {
+        die('Unable to create AF_INET socket [socket]');
+}
+socket_set_block($socket);
+
+try {
+	socket_setopt($socket, SOL_TCP, TCP_USER_TIMEOUT, -1);
+} catch (\ValueError $e) {
+	echo $e->getMessage(), PHP_EOL;
+}
+
+try {
+	socket_setopt($socket, SOL_TCP, TCP_USER_TIMEOUT, PHP_INT_MAX);
+} catch (\ValueError $e) {
+	echo $e->getMessage(), PHP_EOL;
+}
+
+$timeout = 200;
+$retval_2 = socket_set_option($socket, SOL_TCP, TCP_USER_TIMEOUT, $timeout);
+$retval_3 = socket_get_option($socket, SOL_TCP, TCP_USER_TIMEOUT);
+var_dump($retval_2);
+var_dump($retval_3 === $timeout);
+socket_close($socket);
+?>
+--EXPECTF--
+socket_setopt(): Argument #4 ($value) must be of between 0 and %d
+socket_setopt(): Argument #4 ($value) must be of between 0 and %d
+bool(true)
+bool(true)