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) {