Commit 203e59e5495 for php.net

commit 203e59e549568a6cf1ba4cb1a1352ea5e621d5f1
Author: David Carlier <devnexen@gmail.com>
Date:   Sat Sep 5 10:30:55 2026 +0100

    streams: add so_rcvbuf and so_sndbuf socket context options

    They set SO_RCVBUF and SO_SNDBUF in bytes on TCP and UDP sockets, applied
    before connect() and listen(). A value outside the 1 to INT_MAX range makes
    the stream creation fail.

    Close GH-23406

diff --git a/NEWS b/NEWS
index e507963f205..3416660ced1 100644
--- a/NEWS
+++ b/NEWS
@@ -80,6 +80,10 @@ PHP                                                                        NEWS
   . Fixed writing to a dimension of the object returned by attributes() not
     creating the attribute. (Ilia Alshanetsky)

+- Streams:
+  . Added so_rcvbuf and so_sndbuf stream socket context options, setting the
+    socket receive and send buffer sizes in bytes. (David Carlier)
+

 27 Aug 2026, PHP 8.6.0beta2

diff --git a/UPGRADING b/UPGRADING
index feaec16c482..111b8d10bf8 100644
--- a/UPGRADING
+++ b/UPGRADING
@@ -486,6 +486,13 @@ PHP 8.6 UPGRADE NOTES
     sockets. A positive value enables lingering for that many seconds, zero
     or a negative value disables it. Values above 65535 are clamped as the
     linger time is limited to an unsigned short on some platforms.
+  . Added stream socket context options so_rcvbuf and so_sndbuf that set the
+    socket receive and send buffer sizes in bytes (SO_RCVBUF and SO_SNDBUF) on
+    TCP and UDP sockets. The value must be an integer between 1 and 2147483647,
+    any other value makes the stream creation fail. The operating system may
+    round, cap or otherwise adjust the requested size, and may stop sizing that
+    buffer automatically, so the size read back can differ from the one
+    requested.
   . Allowed casting filtered streams as file descriptors for select.
   . Added the "write_seek_mode" filter parameter for the bz2, iconv,
     zlib, and string stream filters. This parameter must be set via an
diff --git a/ext/standard/tests/network/so_rcvbuf_sndbuf.phpt b/ext/standard/tests/network/so_rcvbuf_sndbuf.phpt
new file mode 100644
index 00000000000..fdb76713cb0
--- /dev/null
+++ b/ext/standard/tests/network/so_rcvbuf_sndbuf.phpt
@@ -0,0 +1,94 @@
+--TEST--
+stream_socket_server() and stream_socket_client() SO_RCVBUF and SO_SNDBUF context options test
+--EXTENSIONS--
+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),
+    ];
+}
+
+// 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),
+    ]]);
+}
+
+function port($server): int {
+    return (int)substr(strrchr(stream_socket_get_name($server, false), ':'), 1);
+}
+
+$control = stream_socket_server("tcp://127.0.0.1:0", $errno, $errstr,
+    STREAM_SERVER_BIND | STREAM_SERVER_LISTEN);
+
+if (!$control) {
+    die('Unable to create server');
+}
+
+[$rcvbuf, $sndbuf] = buffers($control);
+
+$server = stream_socket_server("tcp://127.0.0.1:0", $errno, $errstr,
+    STREAM_SERVER_BIND | STREAM_SERVER_LISTEN, context($rcvbuf, $sndbuf));
+
+if (!$server) {
+    die('Unable to create server');
+}
+
+echo "Listen buffers\n";
+[$listen_rcvbuf, $listen_sndbuf] = buffers($server);
+var_dump($listen_rcvbuf < $rcvbuf);
+var_dump($listen_sndbuf < $sndbuf);
+
+// A connection is compared against another connection: some systems size the
+// receive buffer of a connected socket on their own.
+$control_client = stream_socket_client("tcp://127.0.0.1:" . port($server), $errno, $errstr, 30);
+
+if (!$control_client) {
+    die('Unable to create client');
+}
+
+$control_accepted = stream_socket_accept($server, 1);
+
+if (!$control_accepted) {
+    die('Unable to accept connection');
+}
+
+[, $client_sndbuf] = buffers($control_client);
+
+$client = stream_socket_client("tcp://127.0.0.1:" . port($server), $errno, $errstr, 30,
+    STREAM_CLIENT_CONNECT, context($client_sndbuf, $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\n";
+[, $client_sndbuf2] = buffers($client);
+var_dump($client_sndbuf2 < $client_sndbuf);
+
+fclose($accepted);
+fclose($control_accepted);
+fclose($client);
+fclose($control_client);
+fclose($server);
+fclose($control);
+
+?>
+--EXPECT--
+Listen buffers
+bool(true)
+bool(true)
+Client buffers
+bool(true)
diff --git a/ext/standard/tests/network/so_rcvbuf_sndbuf_error.phpt b/ext/standard/tests/network/so_rcvbuf_sndbuf_error.phpt
new file mode 100644
index 00000000000..dbad2a3ff0e
--- /dev/null
+++ b/ext/standard/tests/network/so_rcvbuf_sndbuf_error.phpt
@@ -0,0 +1,34 @@
+--TEST--
+SO_RCVBUF and SO_SNDBUF context options reject invalid values
+--FILE--
+<?php
+foreach (['so_rcvbuf', 'so_sndbuf'] as $option) {
+    foreach ([0, -1, 'abc'] as $value) {
+        $context = stream_context_create(['socket' => [$option => $value]]);
+        var_dump(@stream_socket_server("tcp://127.0.0.1:0", $errno, $errstr,
+            STREAM_SERVER_BIND | STREAM_SERVER_LISTEN, $context));
+        echo $errstr, PHP_EOL;
+    }
+}
+
+$context = stream_context_create(['socket' => ['so_rcvbuf' => 0]]);
+var_dump(@stream_socket_client("tcp://127.0.0.1:1", $errno, $errstr, 1,
+    STREAM_CLIENT_CONNECT, $context));
+echo $errstr, PHP_EOL;
+
+?>
+--EXPECT--
+bool(false)
+so_rcvbuf context option must be between 1 and 2147483647
+bool(false)
+so_rcvbuf context option must be between 1 and 2147483647
+bool(false)
+so_rcvbuf context option must be between 1 and 2147483647
+bool(false)
+so_sndbuf context option must be between 1 and 2147483647
+bool(false)
+so_sndbuf context option must be between 1 and 2147483647
+bool(false)
+so_sndbuf context option must be between 1 and 2147483647
+bool(false)
+so_rcvbuf context option must be between 1 and 2147483647
diff --git a/ext/standard/tests/network/so_rcvbuf_sndbuf_udp.phpt b/ext/standard/tests/network/so_rcvbuf_sndbuf_udp.phpt
new file mode 100644
index 00000000000..82dcb3c83ff
--- /dev/null
+++ b/ext/standard/tests/network/so_rcvbuf_sndbuf_udp.phpt
@@ -0,0 +1,47 @@
+--TEST--
+stream_socket_server() SO_RCVBUF and SO_SNDBUF context options test with UDP
+--EXTENSIONS--
+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 = stream_socket_server("udp://127.0.0.1:0", $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("udp://127.0.0.1:0", $errno, $errstr, STREAM_SERVER_BIND, $context);
+
+if (!$server) {
+    die('Unable to create server');
+}
+
+[$server_rcvbuf, $server_sndbuf] = buffers($server);
+echo "Server buffers\n";
+var_dump($server_rcvbuf < $rcvbuf);
+var_dump($server_sndbuf < $sndbuf);
+
+fclose($server);
+fclose($control);
+
+?>
+--EXPECT--
+Server buffers
+bool(true)
+bool(true)
diff --git a/main/network.c b/main/network.c
index 836abaeac7d..01e4f2e778d 100644
--- a/main/network.c
+++ b/main/network.c
@@ -443,6 +443,20 @@ 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)
+{
+#ifdef SO_RCVBUF
+	if (sockvals->mask & PHP_SOCKVAL_SO_RCVBUF) {
+		setsockopt(sock, SOL_SOCKET, SO_RCVBUF, (char*)&sockvals->rcvbuf, sizeof(sockvals->rcvbuf));
+	}
+#endif
+#ifdef SO_SNDBUF
+	if (sockvals->mask & PHP_SOCKVAL_SO_SNDBUF) {
+		setsockopt(sock, SOL_SOCKET, SO_SNDBUF, (char*)&sockvals->sndbuf, sizeof(sockvals->sndbuf));
+	}
+#endif
+}
+
 /* Bind to a local IP address.
  * Returns the bound socket, or -1 on failure.
  * */
@@ -573,6 +587,7 @@ php_socket_t php_network_bind_socket_to_local_addr_ex(const char *host, unsigned
 				setsockopt(sock, IPPROTO_TCP, TCP_KEEPCNT, (char*)&sockvals->keepalive.keepcnt, sizeof(sockvals->keepalive.keepcnt));
 			}
 #endif
+			php_network_set_socket_buffers(sock, sockvals);
 		}

 		n = bind(sock, sa, socklen);
@@ -1077,6 +1092,7 @@ php_socket_t php_network_connect_socket_to_host_ex(const char *host, unsigned sh
 				setsockopt(sock, IPPROTO_TCP, TCP_KEEPCNT, (char*)&sockvals->keepalive.keepcnt, sizeof(sockvals->keepalive.keepcnt));
 			}
 #endif
+			php_network_set_socket_buffers(sock, sockvals);
 		}

 		n = php_network_connect_socket(sock, sa, socklen, asynchronous,
diff --git a/main/php_network.h b/main/php_network.h
index c966b7af005..c93a519911f 100644
--- a/main/php_network.h
+++ b/main/php_network.h
@@ -270,6 +270,8 @@ typedef struct {
 #define PHP_SOCKVAL_TCP_KEEPCNT   (1 << 2)
 #define PHP_SOCKVAL_TCP_KEEPINTVL (1 << 3)
 #define PHP_SOCKVAL_SO_LINGER     (1 << 4)
+#define PHP_SOCKVAL_SO_RCVBUF     (1 << 5)
+#define PHP_SOCKVAL_SO_SNDBUF     (1 << 6)

 #define PHP_SOCKVAL_IS_SET(sockvals, opt) ((sockvals)->mask & (opt))

@@ -277,6 +279,8 @@ typedef struct {
 	unsigned int mask;
 	int tcp_nodelay;
 	int linger;
+	int rcvbuf;
+	int sndbuf;
 	struct {
 		int keepidle;
 		int keepcnt;
diff --git a/main/streams/xp_socket.c b/main/streams/xp_socket.c
index a9e2fa2b1d0..3844414d804 100644
--- a/main/streams/xp_socket.c
+++ b/main/streams/xp_socket.c
@@ -677,6 +677,50 @@ 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,
+		php_sockvals *sockvals)
+{
+	zval *tmpzval;
+
+	if (!PHP_STREAM_CONTEXT(stream)) {
+		return 0;
+	}
+
+#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);
+
+		if (bufsize < 1 || bufsize > INT_MAX) {
+			if (xparam->want_errortext) {
+				xparam->outputs.error_text = strpprintf(0, "so_rcvbuf context option must be between 1 and %d", INT_MAX);
+			}
+			return -1;
+		}
+
+		sockvals->mask |= PHP_SOCKVAL_SO_RCVBUF;
+		sockvals->rcvbuf = (int) bufsize;
+	}
+#endif
+
+#ifdef SO_SNDBUF
+	if ((tmpzval = php_stream_context_get_option(PHP_STREAM_CONTEXT(stream), "socket", "so_sndbuf")) != NULL) {
+		zend_long bufsize = zval_get_long(tmpzval);
+
+		if (bufsize < 1 || bufsize > INT_MAX) {
+			if (xparam->want_errortext) {
+				xparam->outputs.error_text = strpprintf(0, "so_sndbuf context option must be between 1 and %d", INT_MAX);
+			}
+			return -1;
+		}
+
+		sockvals->mask |= PHP_SOCKVAL_SO_SNDBUF;
+		sockvals->sndbuf = (int) bufsize;
+	}
+#endif
+
+	return 0;
+}
+
 static inline int php_tcp_sockop_bind(php_stream *stream, php_netstream_data_t *sock,
 		php_stream_xport_param *xparam)
 {
@@ -720,6 +764,11 @@ 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
@@ -868,6 +917,11 @@ 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) {