From 6e1b6b867ad447192a246e209be081dc037081df Mon Sep 17 00:00:00 2001 From: David Carlier Date: Sat, 22 Aug 2026 12:11:41 +0100 Subject: [PATCH 1/3] 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(). Values outside 1 to INT_MAX throw a ValueError. --- .../tests/network/so_rcvbuf_sndbuf.phpt | 88 +++++++++++++++++++ .../tests/network/so_rcvbuf_sndbuf_error.phpt | 33 +++++++ .../tests/network/so_rcvbuf_sndbuf_udp.phpt | 47 ++++++++++ main/network.c | 16 ++++ main/php_network.h | 4 + main/streams/xp_socket.c | 49 +++++++++++ 6 files changed, 237 insertions(+) create mode 100644 ext/standard/tests/network/so_rcvbuf_sndbuf.phpt create mode 100644 ext/standard/tests/network/so_rcvbuf_sndbuf_error.phpt create mode 100644 ext/standard/tests/network/so_rcvbuf_sndbuf_udp.phpt 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 000000000000..1d7c32a79597 --- /dev/null +++ b/ext/standard/tests/network/so_rcvbuf_sndbuf.phpt @@ -0,0 +1,88 @@ +--TEST-- +stream_socket_server() and stream_socket_client() SO_RCVBUF and SO_SNDBUF context options test +--EXTENSIONS-- +sockets +--FILE-- + [ + 'so_rcvbuf' => intdiv($rcvbuf, 4), + 'so_sndbuf' => intdiv($sndbuf, 4), + ]]); +} + +$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'); +} + +$addr = stream_socket_get_name($server, false); +$port = (int)substr(strrchr($addr, ':'), 1); + +$client = stream_socket_client("tcp://127.0.0.1:$port", $errno, $errstr, 30, + STREAM_CLIENT_CONNECT, context($rcvbuf, $sndbuf)); + +if (!$client) { + die('Unable to create client'); +} + +$accepted = stream_socket_accept($server, 1); + +if (!$accepted) { + die('Unable to accept connection'); +} + +// Verify the listening socket +[$listen_rcvbuf, $listen_sndbuf] = buffers($server); +echo "Listen buffers\n"; +var_dump($listen_rcvbuf < $rcvbuf); +var_dump($listen_sndbuf < $sndbuf); + +// Verify server side (accepted connection, inherits from the listening socket) +[$server_rcvbuf, $server_sndbuf] = buffers($accepted); +echo "Server buffers\n"; +var_dump($server_rcvbuf < $rcvbuf); +var_dump($server_sndbuf < $sndbuf); + +// Verify client side +[$client_rcvbuf, $client_sndbuf] = buffers($client); +echo "Client buffers\n"; +var_dump($client_rcvbuf < $rcvbuf); +var_dump($client_sndbuf < $sndbuf); + +fclose($accepted); +fclose($client); +fclose($server); +fclose($control); + +?> +--EXPECT-- +Listen buffers +bool(true) +bool(true) +Server buffers +bool(true) +bool(true) +Client buffers +bool(true) +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 000000000000..5f94b15691de --- /dev/null +++ b/ext/standard/tests/network/so_rcvbuf_sndbuf_error.phpt @@ -0,0 +1,33 @@ +--TEST-- +SO_RCVBUF and SO_SNDBUF context options reject invalid values +--FILE-- + [$option => $value]]); + try { + @stream_socket_server("tcp://127.0.0.1:0", $errno, $errstr, + STREAM_SERVER_BIND | STREAM_SERVER_LISTEN, $context); + } catch (ValueError $e) { + echo $e::class, ': ', $e->getMessage(), PHP_EOL; + } + } +} + +$context = stream_context_create(['socket' => ['so_rcvbuf' => 0]]); +try { + @stream_socket_client("tcp://127.0.0.1:1", $errno, $errstr, 1, + STREAM_CLIENT_CONNECT, $context); +} catch (ValueError $e) { + echo $e::class, ': ', $e->getMessage(), PHP_EOL; +} + +?> +--EXPECT-- +ValueError: stream context option 'so_rcvbuf' must be between 1 and 2147483647 +ValueError: stream context option 'so_rcvbuf' must be between 1 and 2147483647 +ValueError: stream context option 'so_rcvbuf' must be between 1 and 2147483647 +ValueError: stream context option 'so_sndbuf' must be between 1 and 2147483647 +ValueError: stream context option 'so_sndbuf' must be between 1 and 2147483647 +ValueError: stream context option 'so_sndbuf' must be between 1 and 2147483647 +ValueError: stream context option 'so_rcvbuf' 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 000000000000..82dcb3c83ffe --- /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-- + [ + '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 836abaeac7dd..01e4f2e778d4 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 c966b7af0053..c93a519911f0 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 a9e2fa2b1d05..da5c73a02977 100644 --- a/main/streams/xp_socket.c +++ b/main/streams/xp_socket.c @@ -677,6 +677,45 @@ 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_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) { + zend_value_error("stream context option 'so_rcvbuf' 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) { + zend_value_error("stream context option 'so_sndbuf' 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 +759,11 @@ static inline int php_tcp_sockop_bind(php_stream *stream, php_netstream_data_t * return -1; } + if (php_sockop_parse_buffer_sizes(stream, &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 +912,11 @@ static inline int php_tcp_sockop_connect(php_stream *stream, php_netstream_data_ return -1; } + if (php_sockop_parse_buffer_sizes(stream, &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) { From 334237c0cd64383904993e3a0b1b4e3a6d95ed5f Mon Sep 17 00:00:00 2001 From: David Carlier Date: Sat, 22 Aug 2026 12:59:14 +0100 Subject: [PATCH 2/3] fix test and assumptions. --- .../tests/network/so_rcvbuf_sndbuf.phpt | 54 ++++++++++--------- 1 file changed, 30 insertions(+), 24 deletions(-) diff --git a/ext/standard/tests/network/so_rcvbuf_sndbuf.phpt b/ext/standard/tests/network/so_rcvbuf_sndbuf.phpt index 1d7c32a79597..fdb76713cb05 100644 --- a/ext/standard/tests/network/so_rcvbuf_sndbuf.phpt +++ b/ext/standard/tests/network/so_rcvbuf_sndbuf.phpt @@ -20,6 +20,10 @@ function context(int $rcvbuf, int $sndbuf) { ]]); } +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); @@ -36,11 +40,29 @@ if (!$server) { die('Unable to create server'); } -$addr = stream_socket_get_name($server, false); -$port = (int)substr(strrchr($addr, ':'), 1); +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", $errno, $errstr, 30, - STREAM_CLIENT_CONNECT, context($rcvbuf, $sndbuf)); +$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'); @@ -52,26 +74,14 @@ if (!$accepted) { die('Unable to accept connection'); } -// Verify the listening socket -[$listen_rcvbuf, $listen_sndbuf] = buffers($server); -echo "Listen buffers\n"; -var_dump($listen_rcvbuf < $rcvbuf); -var_dump($listen_sndbuf < $sndbuf); - -// Verify server side (accepted connection, inherits from the listening socket) -[$server_rcvbuf, $server_sndbuf] = buffers($accepted); -echo "Server buffers\n"; -var_dump($server_rcvbuf < $rcvbuf); -var_dump($server_sndbuf < $sndbuf); - -// Verify client side -[$client_rcvbuf, $client_sndbuf] = buffers($client); echo "Client buffers\n"; -var_dump($client_rcvbuf < $rcvbuf); -var_dump($client_sndbuf < $sndbuf); +[, $client_sndbuf2] = buffers($client); +var_dump($client_sndbuf2 < $client_sndbuf); fclose($accepted); +fclose($control_accepted); fclose($client); +fclose($control_client); fclose($server); fclose($control); @@ -80,9 +90,5 @@ fclose($control); Listen buffers bool(true) bool(true) -Server buffers -bool(true) -bool(true) Client buffers bool(true) -bool(true) From 83ad2434ba397e46169def1f4cd4b078cd218376 Mon Sep 17 00:00:00 2001 From: David Carlier Date: Fri, 4 Sep 2026 22:45:10 +0100 Subject: [PATCH 3/3] address feedback --- UPGRADING | 7 ++++ .../tests/network/so_rcvbuf_sndbuf_error.phpt | 39 ++++++++++--------- main/streams/xp_socket.c | 15 ++++--- 3 files changed, 37 insertions(+), 24 deletions(-) diff --git a/UPGRADING b/UPGRADING index feaec16c4826..111b8d10bf81 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_error.phpt b/ext/standard/tests/network/so_rcvbuf_sndbuf_error.phpt index 5f94b15691de..dbad2a3ff0e5 100644 --- a/ext/standard/tests/network/so_rcvbuf_sndbuf_error.phpt +++ b/ext/standard/tests/network/so_rcvbuf_sndbuf_error.phpt @@ -5,29 +5,30 @@ SO_RCVBUF and SO_SNDBUF context options reject invalid values foreach (['so_rcvbuf', 'so_sndbuf'] as $option) { foreach ([0, -1, 'abc'] as $value) { $context = stream_context_create(['socket' => [$option => $value]]); - try { - @stream_socket_server("tcp://127.0.0.1:0", $errno, $errstr, - STREAM_SERVER_BIND | STREAM_SERVER_LISTEN, $context); - } catch (ValueError $e) { - echo $e::class, ': ', $e->getMessage(), PHP_EOL; - } + 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]]); -try { - @stream_socket_client("tcp://127.0.0.1:1", $errno, $errstr, 1, - STREAM_CLIENT_CONNECT, $context); -} catch (ValueError $e) { - echo $e::class, ': ', $e->getMessage(), PHP_EOL; -} +var_dump(@stream_socket_client("tcp://127.0.0.1:1", $errno, $errstr, 1, + STREAM_CLIENT_CONNECT, $context)); +echo $errstr, PHP_EOL; ?> --EXPECT-- -ValueError: stream context option 'so_rcvbuf' must be between 1 and 2147483647 -ValueError: stream context option 'so_rcvbuf' must be between 1 and 2147483647 -ValueError: stream context option 'so_rcvbuf' must be between 1 and 2147483647 -ValueError: stream context option 'so_sndbuf' must be between 1 and 2147483647 -ValueError: stream context option 'so_sndbuf' must be between 1 and 2147483647 -ValueError: stream context option 'so_sndbuf' must be between 1 and 2147483647 -ValueError: stream context option 'so_rcvbuf' 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_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/main/streams/xp_socket.c b/main/streams/xp_socket.c index da5c73a02977..3844414d8048 100644 --- a/main/streams/xp_socket.c +++ b/main/streams/xp_socket.c @@ -677,7 +677,8 @@ 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_sockvals *sockvals) +static int php_sockop_parse_buffer_sizes(php_stream *stream, php_stream_xport_param *xparam, + php_sockvals *sockvals) { zval *tmpzval; @@ -690,7 +691,9 @@ static int php_sockop_parse_buffer_sizes(php_stream *stream, php_sockvals *sockv zend_long bufsize = zval_get_long(tmpzval); if (bufsize < 1 || bufsize > INT_MAX) { - zend_value_error("stream context option 'so_rcvbuf' must be between 1 and %d", 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; } @@ -704,7 +707,9 @@ static int php_sockop_parse_buffer_sizes(php_stream *stream, php_sockvals *sockv zend_long bufsize = zval_get_long(tmpzval); if (bufsize < 1 || bufsize > INT_MAX) { - zend_value_error("stream context option 'so_sndbuf' must be between 1 and %d", 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; } @@ -759,7 +764,7 @@ static inline int php_tcp_sockop_bind(php_stream *stream, php_netstream_data_t * return -1; } - if (php_sockop_parse_buffer_sizes(stream, &sockvals) == -1) { + if (php_sockop_parse_buffer_sizes(stream, xparam, &sockvals) == -1) { efree(host); return -1; } @@ -912,7 +917,7 @@ static inline int php_tcp_sockop_connect(php_stream *stream, php_netstream_data_ return -1; } - if (php_sockop_parse_buffer_sizes(stream, &sockvals) == -1) { + if (php_sockop_parse_buffer_sizes(stream, xparam, &sockvals) == -1) { efree(host); return -1; }