Commit aead67d0bb2 for php.net

commit aead67d0bb2f6a3903325b68f274e6b1bceb7bce
Author: Jakub Zelenka <bukka@php.net>
Date:   Thu Sep 25 20:33:10 2025 +0200

    Add so_reuseaddr stream socket context option

    This is to allow disabling of SO_REUSEADDR that is enabled by default.

    To achieve better compatibility on Windows SO_EXCLUSIVEADDRUSE is set
    if so_reuseaddr is false.

    Closes GH-19967

diff --git a/NEWS b/NEWS
index 92b404f3a68..938aa1f72ed 100644
--- a/NEWS
+++ b/NEWS
@@ -33,6 +33,10 @@ PHP                                                                        NEWS
   . Fixed bug GH-19926 (reset internal pointer earlier while splicing array
     while COW violation flag is still set). (alexandre-daubois)

+- Streams:
+  . Added so_reuseaddr streams context socket option that allows disabling
+    address resuse.
+
 - Zip:
   . Fixed ZipArchive callback being called after executor has shut down.
     (ilutov)
diff --git a/UPGRADING b/UPGRADING
index eb158460cef..f4a455caefa 100644
--- a/UPGRADING
+++ b/UPGRADING
@@ -36,6 +36,12 @@ PHP 8.6 UPGRADE NOTES
     IntlNumberRangeFormatter::IDENTITY_FALLBACK_SINGLE_VALUE, IntlNumberRangeFormatter::IDENTITY_FALLBACK_APPROXIMATELY_OR_SINGLE_VALUE, IntlNumberRangeFormatter::IDENTITY_FALLBACK_APPROXIMATELY and
     IntlNumberRangeFormatter::IDENTITY_FALLBACK_RANGE identity fallbacks.
     It is supported from icu 63.
+
+- Streams:
+  . Added stream socket context option so_reuseaddr that allows disabling
+    address reuse (SO_REUSEADDR) and explicitly uses SO_EXCLUSIVEADDRUSE on
+    Windows.
+
 ========================================
 3. Changes in SAPI modules
 ========================================
diff --git a/ext/standard/tests/network/so_reuseaddr.phpt b/ext/standard/tests/network/so_reuseaddr.phpt
new file mode 100644
index 00000000000..d400a81ac33
--- /dev/null
+++ b/ext/standard/tests/network/so_reuseaddr.phpt
@@ -0,0 +1,73 @@
+--TEST--
+stream_socket_server() SO_REUSEADDR context option test
+--FILE--
+<?php
+$is_win = substr(PHP_OS, 0, 3) == "WIN";
+// Test default behavior (SO_REUSEADDR enabled)
+$server1 = stream_socket_server("tcp://127.0.0.1:0", $errno, $errstr, STREAM_SERVER_BIND | STREAM_SERVER_LISTEN);
+if (!$server1) {
+    die('Unable to create server3');
+}
+
+$addr = stream_socket_get_name($server1, false);
+$port = (int)substr(strrchr($addr, ':'), 1);
+
+$client1 = stream_socket_client("tcp://127.0.0.1:$port");
+$accepted = stream_socket_accept($server1, 1);
+
+// Force real TCP connection with data
+fwrite($client1, "test");
+fread($accepted, 4);
+fwrite($accepted, "response");
+fread($client1, 8);
+
+fclose($client1);
+if (!$is_win) { // Windows would always succeed if server is closed
+    fclose($server1);
+}
+
+$server2 = @stream_socket_server("tcp://127.0.0.1:$port", $errno, $errstr, STREAM_SERVER_BIND | STREAM_SERVER_LISTEN);
+if ($server2) {
+    echo "Default: Server restart succeeded\n";
+    fclose($server2);
+} else {
+    echo "Default: Server restart failed\n";
+}
+
+// Test with SO_REUSEADDR explicitly disabled
+$context = stream_context_create(['socket' => ['so_reuseaddr' => false]]);
+$server3 = stream_socket_server("tcp://127.0.0.1:0", $errno, $errstr, STREAM_SERVER_BIND | STREAM_SERVER_LISTEN, $context);
+if (!$server3) {
+    die('Unable to create server3');
+}
+
+$addr = stream_socket_get_name($server3, false);
+$port = (int)substr(strrchr($addr, ':'), 1);
+
+$client3 = stream_socket_client("tcp://127.0.0.1:$port");
+$accepted = stream_socket_accept($server3, 1);
+
+// Force real TCP connection with data
+fwrite($client3, "test");
+fread($accepted, 4);
+fwrite($accepted, "response");
+fread($client3, 8);
+
+// Client closes first (becomes active closer)
+fclose($client3);  // This enters TIME_WAIT
+if (!$is_win) { // Windows would always succeed if server is closed
+    fclose($server3);
+}
+
+// Try immediate bind with SO_REUSEADDR disabled (should fail)
+$server4 = @stream_socket_server("tcp://127.0.0.1:$port", $errno, $errstr, STREAM_SERVER_BIND | STREAM_SERVER_LISTEN, $context);
+if ($server4) {
+    echo "Disabled: Server restart succeeded\n";
+    fclose($server4);
+} else {
+    echo "Disabled: Server restart failed\n";
+}
+?>
+--EXPECT--
+Default: Server restart succeeded
+Disabled: Server restart failed
diff --git a/main/network.c b/main/network.c
index 7be64c2c4e1..0dadf0bb4dc 100644
--- a/main/network.c
+++ b/main/network.c
@@ -496,8 +496,13 @@ php_socket_t php_network_bind_socket_to_local_addr(const char *host, unsigned po

 		/* attempt to bind */

-#ifdef SO_REUSEADDR
-		setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, (char*)&sockoptval, sizeof(sockoptval));
+		if (sockopts & STREAM_SOCKOP_SO_REUSEADDR) {
+			setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, (char*)&sockoptval, sizeof(sockoptval));
+		}
+#ifdef PHP_WIN32
+		else {
+			setsockopt(sock, SOL_SOCKET, SO_EXCLUSIVEADDRUSE, (char*)&sockoptval, sizeof(sockoptval));
+		}
 #endif
 #ifdef IPV6_V6ONLY
 		if (sockopts & STREAM_SOCKOP_IPV6_V6ONLY) {
diff --git a/main/php_network.h b/main/php_network.h
index 6df73cf7af0..45e1e190263 100644
--- a/main/php_network.h
+++ b/main/php_network.h
@@ -123,7 +123,7 @@ typedef int php_socket_t;
 #define STREAM_SOCKOP_IPV6_V6ONLY         (1 << 3)
 #define STREAM_SOCKOP_IPV6_V6ONLY_ENABLED (1 << 4)
 #define STREAM_SOCKOP_TCP_NODELAY         (1 << 5)
-
+#define STREAM_SOCKOP_SO_REUSEADDR        (1 << 6)

 /* uncomment this to debug poll(2) emulation on systems that have poll(2) */
 /* #define PHP_USE_POLL_2_EMULATION 1 */
diff --git a/main/streams/xp_socket.c b/main/streams/xp_socket.c
index e3e81323c3f..db35a9b7952 100644
--- a/main/streams/xp_socket.c
+++ b/main/streams/xp_socket.c
@@ -718,6 +718,16 @@ static inline int php_tcp_sockop_bind(php_stream *stream, php_netstream_data_t *
 	}
 #endif

+#ifdef SO_REUSEADDR
+	/* SO_REUSEADDR is enabled by default so this option is just to disable it if set to false. */
+	if (!PHP_STREAM_CONTEXT(stream)
+		|| (tmpzval = php_stream_context_get_option(PHP_STREAM_CONTEXT(stream), "socket", "so_reuseaddr")) == NULL
+		|| zend_is_true(tmpzval)
+	) {
+		sockopts |= STREAM_SOCKOP_SO_REUSEADDR;
+	}
+#endif
+
 #ifdef SO_BROADCAST
 	if (stream->ops == &php_stream_udp_socket_ops /* SO_BROADCAST is only applicable for UDP */
 		&& PHP_STREAM_CONTEXT(stream)