Commit 87b2d5dd102 for php

commit 87b2d5dd10215a3751d68a35b8fca9f66fd987c1
Author: David Carlier <devnexen@gmail.com>
Date:   Wed Sep 30 18:13:34 2026 +0100

    streams: apply so_linger, so_rcvbuf and so_sndbuf to unix sockets

    The unix and udg transports returned from the AF_UNIX branch before any
    socket context option was read. The parsing happens before the transport
    split now and the values are applied through the new
    php_network_apply_sockvals().

    Close GH-23584

diff --git a/NEWS b/NEWS
index e1bf6e4a3dd..124d6c51638 100644
--- a/NEWS
+++ b/NEWS
@@ -80,6 +80,10 @@ PHP                                                                        NEWS
   . Fixed bug GH-22051 (Inconsistent error reporting in SQLite3Result::reset()
     and SQLite3Result::finalize()). (Ilia Alshanetsky)

+- Streams:
+  . Applied the so_linger, so_rcvbuf and so_sndbuf context options to unix
+    sockets. (David Carlier)
+
 - Standard:
   . Fixed bug #60110 (fclose(), file_put_contents(), copy() do not return false
     properly). (Jakub Zelenka, Ilija Tovilo)
diff --git a/UPGRADING b/UPGRADING
index 10994928470..c46af00cfe2 100644
--- a/UPGRADING
+++ b/UPGRADING
@@ -33,6 +33,10 @@ PHP 8.7 UPGRADE NOTES
 2. New Features
 ========================================

+- Streams:
+  . The so_linger, so_rcvbuf and so_sndbuf stream socket context options are
+    now also applied to unix and unix datagram sockets.
+
 ========================================
 3. Changes in SAPI modules
 ========================================
diff --git a/ext/standard/tests/network/so_rcvbuf_sndbuf_udg.phpt b/ext/standard/tests/network/so_rcvbuf_sndbuf_udg.phpt
new file mode 100644
index 00000000000..8b7528d79a2
--- /dev/null
+++ b/ext/standard/tests/network/so_rcvbuf_sndbuf_udg.phpt
@@ -0,0 +1,58 @@
+--TEST--
+stream_socket_server() SO_RCVBUF and SO_SNDBUF context options test with unix datagram sockets
+--EXTENSIONS--
+sockets
+--SKIPIF--
+<?php
+if (array_search("udg", stream_get_transports()) === false) {
+    die('skip No support for UNIX domain sockets.');
+}
+?>
+--FILE--
+<?php
+function buffers($stream): array {
+    $sock = socket_import_stream($stream);
+    return [
+        socket_get_option($sock, SOL_SOCKET, SO_RCVBUF),
+        socket_get_option($sock, SOL_SOCKET, SO_SNDBUF),
+    ];
+}
+
+$control_path = sys_get_temp_dir() . '/' . uniqid('so_buf_control_') . '.sock';
+$server_path = sys_get_temp_dir() . '/' . uniqid('so_buf_server_') . '.sock';
+
+$control = stream_socket_server("udg://$control_path", $errno, $errstr, STREAM_SERVER_BIND);
+
+if (!$control) {
+    die('Unable to create server');
+}
+
+[$rcvbuf, $sndbuf] = buffers($control);
+
+// Shrinking is always honoured, while growing may be capped by the system maximum.
+$context = stream_context_create(['socket' => [
+    'so_rcvbuf' => intdiv($rcvbuf, 4),
+    'so_sndbuf' => intdiv($sndbuf, 4),
+]]);
+
+$server = stream_socket_server("udg://$server_path", $errno, $errstr, STREAM_SERVER_BIND, $context);
+
+if (!$server) {
+    die('Unable to create server');
+}
+
+[$server_rcvbuf, $server_sndbuf] = buffers($server);
+echo "Server buffers", PHP_EOL;
+var_dump($server_rcvbuf < $rcvbuf);
+var_dump($server_sndbuf < $sndbuf);
+
+fclose($server);
+fclose($control);
+unlink($server_path);
+unlink($control_path);
+
+?>
+--EXPECT--
+Server buffers
+bool(true)
+bool(true)
diff --git a/ext/standard/tests/network/so_rcvbuf_sndbuf_unix.phpt b/ext/standard/tests/network/so_rcvbuf_sndbuf_unix.phpt
new file mode 100644
index 00000000000..962fb4a1f6a
--- /dev/null
+++ b/ext/standard/tests/network/so_rcvbuf_sndbuf_unix.phpt
@@ -0,0 +1,108 @@
+--TEST--
+stream_socket_server() and stream_socket_client() SO_RCVBUF and SO_SNDBUF context options test with unix sockets
+--EXTENSIONS--
+sockets
+--SKIPIF--
+<?php
+if (array_search("unix", stream_get_transports()) === false) {
+    die('skip No support for UNIX domain sockets.');
+}
+?>
+--FILE--
+<?php
+function buffers($stream): array {
+    $sock = socket_import_stream($stream);
+    return [
+        socket_get_option($sock, SOL_SOCKET, SO_RCVBUF),
+        socket_get_option($sock, SOL_SOCKET, SO_SNDBUF),
+    ];
+}
+
+$control_path = sys_get_temp_dir() . '/' . uniqid('so_buf_control_') . '.sock';
+$server_path = sys_get_temp_dir() . '/' . uniqid('so_buf_server_') . '.sock';
+
+$control = stream_socket_server("unix://$control_path", $errno, $errstr,
+    STREAM_SERVER_BIND | STREAM_SERVER_LISTEN);
+
+if (!$control) {
+    die('Unable to create server');
+}
+
+$control_client = stream_socket_client("unix://$control_path", $errno, $errstr, 30);
+
+if (!$control_client) {
+    die('Unable to create client');
+}
+
+[$rcvbuf, $sndbuf] = buffers($control);
+[$client_rcvbuf, $client_sndbuf] = buffers($control_client);
+
+// Shrinking is always honoured, while growing may be capped by the system maximum.
+function context(int $rcvbuf, int $sndbuf) {
+    return stream_context_create(['socket' => [
+        'so_rcvbuf' => intdiv($rcvbuf, 4),
+        'so_sndbuf' => intdiv($sndbuf, 4),
+    ]]);
+}
+
+$server = stream_socket_server("unix://$server_path", $errno, $errstr,
+    STREAM_SERVER_BIND | STREAM_SERVER_LISTEN, context($rcvbuf, $sndbuf));
+
+if (!$server) {
+    die('Unable to create server');
+}
+
+echo "Listen buffers", PHP_EOL;
+[$listen_rcvbuf, $listen_sndbuf] = buffers($server);
+var_dump($listen_rcvbuf < $rcvbuf);
+var_dump($listen_sndbuf < $sndbuf);
+
+$client = stream_socket_client("unix://$server_path", $errno, $errstr, 30,
+    STREAM_CLIENT_CONNECT, context($client_rcvbuf, $client_sndbuf));
+
+if (!$client) {
+    die('Unable to create client');
+}
+
+$accepted = stream_socket_accept($server, 1);
+
+if (!$accepted) {
+    die('Unable to accept connection');
+}
+
+echo "Client buffers", PHP_EOL;
+[$rcvbuf2, $sndbuf2] = buffers($client);
+var_dump($rcvbuf2 < $client_rcvbuf);
+var_dump($sndbuf2 < $client_sndbuf);
+
+$invalid = stream_context_create(['socket' => ['so_rcvbuf' => 0]]);
+
+echo "Invalid buffer size", PHP_EOL;
+var_dump(@stream_socket_server("unix://$server_path.invalid", $errno, $errstr,
+    STREAM_SERVER_BIND | STREAM_SERVER_LISTEN, $invalid));
+echo $errstr, PHP_EOL;
+var_dump(@stream_socket_client("unix://$server_path", $errno, $errstr, 30,
+    STREAM_CLIENT_CONNECT, $invalid));
+echo $errstr, PHP_EOL;
+
+fclose($accepted);
+fclose($client);
+fclose($server);
+fclose($control_client);
+fclose($control);
+unlink($server_path);
+unlink($control_path);
+
+?>
+--EXPECT--
+Listen buffers
+bool(true)
+bool(true)
+Client buffers
+bool(true)
+bool(true)
+Invalid buffer size
+bool(false)
+so_rcvbuf context option must be between 1 and 2147483647
+bool(false)
+so_rcvbuf context option must be between 1 and 2147483647
diff --git a/main/network.c b/main/network.c
index 01e4f2e778d..1eee7390a23 100644
--- a/main/network.c
+++ b/main/network.c
@@ -443,8 +443,42 @@ PHPAPI int php_network_connect_socket(php_socket_t sockfd,
 }
 /* }}} */

-static void php_network_set_socket_buffers(php_socket_t sock, const php_sockvals *sockvals)
+PHPAPI void php_network_apply_sockvals(php_socket_t sock, const php_sockvals *sockvals)
 {
+#ifdef SO_LINGER
+	if (sockvals->mask & PHP_SOCKVAL_SO_LINGER) {
+		unsigned short secs = sockvals->linger > USHRT_MAX
+			? USHRT_MAX : (unsigned short)sockvals->linger;
+		struct linger linger_val = {
+			.l_onoff = (sockvals->linger > 0),
+			.l_linger = sockvals->linger > 0 ? secs : 0
+		};
+#ifdef SO_LINGER_SEC
+		setsockopt(sock, SOL_SOCKET, SO_LINGER_SEC, (char*)&linger_val, sizeof(linger_val));
+#else
+		setsockopt(sock, SOL_SOCKET, SO_LINGER, (char*)&linger_val, sizeof(linger_val));
+#endif
+	}
+#endif
+#if defined(TCP_KEEPIDLE)
+	if (sockvals->mask & PHP_SOCKVAL_TCP_KEEPIDLE) {
+		setsockopt(sock, IPPROTO_TCP, TCP_KEEPIDLE, (char*)&sockvals->keepalive.keepidle, sizeof(sockvals->keepalive.keepidle));
+	}
+#elif defined(TCP_KEEPALIVE)
+	if (sockvals->mask & PHP_SOCKVAL_TCP_KEEPIDLE) {
+		setsockopt(sock, IPPROTO_TCP, TCP_KEEPALIVE, (char*)&sockvals->keepalive.keepidle, sizeof(sockvals->keepalive.keepidle));
+	}
+#endif
+#ifdef TCP_KEEPINTVL
+	if (sockvals->mask & PHP_SOCKVAL_TCP_KEEPINTVL) {
+		setsockopt(sock, IPPROTO_TCP, TCP_KEEPINTVL, (char*)&sockvals->keepalive.keepintvl, sizeof(sockvals->keepalive.keepintvl));
+	}
+#endif
+#ifdef TCP_KEEPCNT
+	if (sockvals->mask & PHP_SOCKVAL_TCP_KEEPCNT) {
+		setsockopt(sock, IPPROTO_TCP, TCP_KEEPCNT, (char*)&sockvals->keepalive.keepcnt, sizeof(sockvals->keepalive.keepcnt));
+	}
+#endif
 #ifdef SO_RCVBUF
 	if (sockvals->mask & PHP_SOCKVAL_SO_RCVBUF) {
 		setsockopt(sock, SOL_SOCKET, SO_RCVBUF, (char*)&sockvals->rcvbuf, sizeof(sockvals->rcvbuf));
@@ -547,47 +581,8 @@ php_socket_t php_network_bind_socket_to_local_addr_ex(const char *host, unsigned
 		}
 #endif

-		/* Set socket values if provided */
 		if (sockvals != NULL) {
-#ifdef SO_LINGER
-			if (sockvals->mask & PHP_SOCKVAL_SO_LINGER) {
-				/* l_linger is an unsigned short on Windows, so clamp rather than
-				 * truncate: a truncated value may still be in range and would then
-				 * be applied silently (e.g. 65536 becoming 0, an abortive close). */
-				unsigned short secs = sockvals->linger > USHRT_MAX
-					? USHRT_MAX : (unsigned short)sockvals->linger;
-				struct linger linger_val = {
-					.l_onoff = (sockvals->linger > 0),
-					.l_linger = sockvals->linger > 0 ? secs : 0
-				};
-#ifdef SO_LINGER_SEC
-				setsockopt(sock, SOL_SOCKET, SO_LINGER_SEC, (char*)&linger_val, sizeof(linger_val));
-#else
-				setsockopt(sock, SOL_SOCKET, SO_LINGER, (char*)&linger_val, sizeof(linger_val));
-#endif
-			}
-#endif
-#if defined(TCP_KEEPIDLE)
-			if (sockvals->mask & PHP_SOCKVAL_TCP_KEEPIDLE) {
-				setsockopt(sock, IPPROTO_TCP, TCP_KEEPIDLE, (char*)&sockvals->keepalive.keepidle, sizeof(sockvals->keepalive.keepidle));
-			}
-#elif defined(TCP_KEEPALIVE)
-			/* macOS uses TCP_KEEPALIVE instead of TCP_KEEPIDLE */
-			if (sockvals->mask & PHP_SOCKVAL_TCP_KEEPIDLE) {
-				setsockopt(sock, IPPROTO_TCP, TCP_KEEPALIVE, (char*)&sockvals->keepalive.keepidle, sizeof(sockvals->keepalive.keepidle));
-			}
-#endif
-#ifdef TCP_KEEPINTVL
-			if (sockvals->mask & PHP_SOCKVAL_TCP_KEEPINTVL) {
-				setsockopt(sock, IPPROTO_TCP, TCP_KEEPINTVL, (char*)&sockvals->keepalive.keepintvl, sizeof(sockvals->keepalive.keepintvl));
-			}
-#endif
-#ifdef TCP_KEEPCNT
-			if (sockvals->mask & PHP_SOCKVAL_TCP_KEEPCNT) {
-				setsockopt(sock, IPPROTO_TCP, TCP_KEEPCNT, (char*)&sockvals->keepalive.keepcnt, sizeof(sockvals->keepalive.keepcnt));
-			}
-#endif
-			php_network_set_socket_buffers(sock, sockvals);
+			php_network_apply_sockvals(sock, sockvals);
 		}

 		n = bind(sock, sa, socklen);
@@ -1052,47 +1047,8 @@ php_socket_t php_network_connect_socket_to_host_ex(const char *host, unsigned sh
 		}
 #endif

-		/* Set socket values if provided */
 		if (sockvals != NULL) {
-#ifdef SO_LINGER
-			if (sockvals->mask & PHP_SOCKVAL_SO_LINGER) {
-				/* l_linger is an unsigned short on Windows, so clamp rather than
-				 * truncate: a truncated value may still be in range and would then
-				 * be applied silently (e.g. 65536 becoming 0, an abortive close). */
-				unsigned short secs = sockvals->linger > USHRT_MAX
-					? USHRT_MAX : (unsigned short)sockvals->linger;
-				struct linger linger_val = {
-					.l_onoff = (sockvals->linger > 0),
-					.l_linger = sockvals->linger > 0 ? secs : 0
-				};
-#ifdef SO_LINGER_SEC
-				setsockopt(sock, SOL_SOCKET, SO_LINGER_SEC, (char*)&linger_val, sizeof(linger_val));
-#else
-				setsockopt(sock, SOL_SOCKET, SO_LINGER, (char*)&linger_val, sizeof(linger_val));
-#endif
-			}
-#endif
-#if defined(TCP_KEEPIDLE)
-			if (sockvals->mask & PHP_SOCKVAL_TCP_KEEPIDLE) {
-				setsockopt(sock, IPPROTO_TCP, TCP_KEEPIDLE, (char*)&sockvals->keepalive.keepidle, sizeof(sockvals->keepalive.keepidle));
-			}
-#elif defined(TCP_KEEPALIVE)
-			/* macOS uses TCP_KEEPALIVE instead of TCP_KEEPIDLE */
-			if (sockvals->mask & PHP_SOCKVAL_TCP_KEEPIDLE) {
-				setsockopt(sock, IPPROTO_TCP, TCP_KEEPALIVE, (char*)&sockvals->keepalive.keepidle, sizeof(sockvals->keepalive.keepidle));
-			}
-#endif
-#ifdef TCP_KEEPINTVL
-			if (sockvals->mask & PHP_SOCKVAL_TCP_KEEPINTVL) {
-				setsockopt(sock, IPPROTO_TCP, TCP_KEEPINTVL, (char*)&sockvals->keepalive.keepintvl, sizeof(sockvals->keepalive.keepintvl));
-			}
-#endif
-#ifdef TCP_KEEPCNT
-			if (sockvals->mask & PHP_SOCKVAL_TCP_KEEPCNT) {
-				setsockopt(sock, IPPROTO_TCP, TCP_KEEPCNT, (char*)&sockvals->keepalive.keepcnt, sizeof(sockvals->keepalive.keepcnt));
-			}
-#endif
-			php_network_set_socket_buffers(sock, sockvals);
+			php_network_apply_sockvals(sock, sockvals);
 		}

 		n = php_network_connect_socket(sock, sa, socklen, asynchronous,
diff --git a/main/php_network.h b/main/php_network.h
index c93a519911f..5c2cb0acede 100644
--- a/main/php_network.h
+++ b/main/php_network.h
@@ -292,6 +292,8 @@ BEGIN_EXTERN_C()
 PHPAPI int php_network_getaddresses(const char *host, int socktype, struct sockaddr ***sal, zend_string **error_string);
 PHPAPI void php_network_freeaddresses(struct sockaddr **sal);

+PHPAPI void php_network_apply_sockvals(php_socket_t sock, const php_sockvals *sockvals);
+
 PHPAPI php_socket_t php_network_connect_socket_to_host_ex(const char *host, unsigned short port,
 		int socktype, int asynchronous, struct timeval *timeout, zend_string **error_string,
 		int *error_code, const char *bindto, unsigned short bindport, long sockopts, php_sockvals *sockvals
diff --git a/main/streams/xp_socket.c b/main/streams/xp_socket.c
index 3844414d804..4d022c48571 100644
--- a/main/streams/xp_socket.c
+++ b/main/streams/xp_socket.c
@@ -53,7 +53,7 @@ static const php_stream_ops php_stream_unixdg_socket_ops;
 	(PHP_STREAM_XPORT_IS_UNIX_DG(stream) || PHP_STREAM_XPORT_IS_UNIX_ST(stream))
 #else
 #define PHP_STREAM_XPORT_IS_UNIX_DG(stream) false
-#define PHP_STREAM_XPORT_IS_UNIX_STD(stream) false
+#define PHP_STREAM_XPORT_IS_UNIX_ST(stream) false
 #define PHP_STREAM_XPORT_IS_UNIX(stream) false
 #endif
 #define PHP_STREAM_XPORT_IS_UDP(stream) (php_stream_is(stream, &php_stream_udp_socket_ops))
@@ -677,7 +677,7 @@ static inline char *parse_ip_address(php_stream_xport_param *xparam, int *portno
 	return parse_ip_address_ex(xparam->inputs.name, xparam->inputs.namelen, portno, xparam->want_errortext, &xparam->outputs.error_text);
 }

-static int php_sockop_parse_buffer_sizes(php_stream *stream, php_stream_xport_param *xparam,
+static int php_sockop_parse_sockvals(php_stream *stream, php_stream_xport_param *xparam,
 		php_sockvals *sockvals)
 {
 	zval *tmpzval;
@@ -686,6 +686,15 @@ static int php_sockop_parse_buffer_sizes(php_stream *stream, php_stream_xport_pa
 		return 0;
 	}

+#ifdef SO_LINGER
+	if (PHP_STREAM_XPORT_IS_TCP(stream)
+		&& (tmpzval = php_stream_context_get_option(PHP_STREAM_CONTEXT(stream), "socket", "so_linger")) != NULL
+	) {
+		sockvals->mask |= PHP_SOCKVAL_SO_LINGER;
+		sockvals->linger = (int)zval_get_long(tmpzval);
+	}
+#endif
+
 #ifdef SO_RCVBUF
 	if ((tmpzval = php_stream_context_get_option(PHP_STREAM_CONTEXT(stream), "socket", "so_rcvbuf")) != NULL) {
 		zend_long bufsize = zval_get_long(tmpzval);
@@ -730,6 +739,10 @@ static inline int php_tcp_sockop_bind(php_stream *stream, php_netstream_data_t *
 	zval *tmpzval = NULL;
 	php_sockvals sockvals = {0};

+	if (php_sockop_parse_sockvals(stream, xparam, &sockvals) == -1) {
+		return -1;
+	}
+
 #ifdef AF_UNIX
 	if (PHP_STREAM_XPORT_IS_UNIX(stream)) {
 		struct sockaddr_un unix_addr;
@@ -746,6 +759,8 @@ static inline int php_tcp_sockop_bind(php_stream *stream, php_netstream_data_t *
 			return -1;
 		}

+		php_network_apply_sockvals(sock->socket, &sockvals);
+
 		parse_unix_address(stream, xparam, &unix_addr);

 		int result = bind(sock->socket, (const struct sockaddr *)&unix_addr,
@@ -764,11 +779,6 @@ static inline int php_tcp_sockop_bind(php_stream *stream, php_netstream_data_t *
 		return -1;
 	}

-	if (php_sockop_parse_buffer_sizes(stream, xparam, &sockvals) == -1) {
-		efree(host);
-		return -1;
-	}
-
 #ifdef IPV6_V6ONLY
 	if (PHP_STREAM_CONTEXT(stream)
 		&& (tmpzval = php_stream_context_get_option(PHP_STREAM_CONTEXT(stream), "socket", "ipv6_v6only")) != NULL
@@ -808,16 +818,6 @@ static inline int php_tcp_sockop_bind(php_stream *stream, php_netstream_data_t *
 	}
 #endif

-#ifdef SO_LINGER
-	if (PHP_STREAM_XPORT_IS_TCP(stream)
-		&& PHP_STREAM_CONTEXT(stream)
-		&& (tmpzval = php_stream_context_get_option(PHP_STREAM_CONTEXT(stream), "socket", "so_linger")) != NULL
-	) {
-		sockvals.mask |= PHP_SOCKVAL_SO_LINGER;
-		sockvals.linger = (int)zval_get_long(tmpzval);
-	}
-#endif
-
 #ifdef SO_KEEPALIVE
 	if (PHP_STREAM_XPORT_IS_TCP(stream) /* SO_KEEPALIVE is only applicable for TCP */
 		&& PHP_STREAM_CONTEXT(stream)
@@ -884,6 +884,10 @@ static inline int php_tcp_sockop_connect(php_stream *stream, php_netstream_data_
 	long sockopts = STREAM_SOCKOP_NONE;
 	php_sockvals sockvals = {0};

+	if (php_sockop_parse_sockvals(stream, xparam, &sockvals) == -1) {
+		return -1;
+	}
+
 #ifdef AF_UNIX
 	if (PHP_STREAM_XPORT_IS_UNIX(stream)) {
 		struct sockaddr_un unix_addr;
@@ -897,6 +901,8 @@ static inline int php_tcp_sockop_connect(php_stream *stream, php_netstream_data_
 			return -1;
 		}

+		php_network_apply_sockvals(sock->socket, &sockvals);
+
 		parse_unix_address(stream, xparam, &unix_addr);

 		ret = php_network_connect_socket(sock->socket,
@@ -917,11 +923,6 @@ static inline int php_tcp_sockop_connect(php_stream *stream, php_netstream_data_
 		return -1;
 	}

-	if (php_sockop_parse_buffer_sizes(stream, xparam, &sockvals) == -1) {
-		efree(host);
-		return -1;
-	}
-
 	if (PHP_STREAM_CONTEXT(stream) && (tmpzval = php_stream_context_get_option(PHP_STREAM_CONTEXT(stream), "socket", "bindto")) != NULL) {
 		if (Z_TYPE_P(tmpzval) != IS_STRING) {
 			if (xparam->want_errortext) {
@@ -951,16 +952,6 @@ static inline int php_tcp_sockop_connect(php_stream *stream, php_netstream_data_
 		sockopts |= STREAM_SOCKOP_TCP_NODELAY;
 	}

-#ifdef SO_LINGER
-	if (PHP_STREAM_XPORT_IS_TCP(stream)
-		&& PHP_STREAM_CONTEXT(stream)
-		&& (tmpzval = php_stream_context_get_option(PHP_STREAM_CONTEXT(stream), "socket", "so_linger")) != NULL
-	) {
-		sockvals.mask |= PHP_SOCKVAL_SO_LINGER;
-		sockvals.linger = (int)zval_get_long(tmpzval);
-	}
-#endif
-
 #ifdef SO_KEEPALIVE
 	if (PHP_STREAM_XPORT_IS_TCP(stream) /* SO_KEEPALIVE is only applicable for TCP */
 		&& PHP_STREAM_CONTEXT(stream)