From e5b12ee8e27a6aaeea43bcfe76438df78b24a79e Mon Sep 17 00:00:00 2001 From: David Carlier Date: Sat, 22 Aug 2026 12:11:41 +0100 Subject: [PATCH 1/2] 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. --- UPGRADING | 8 ++ .../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 +++++++++++ 7 files changed, 245 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/UPGRADING b/UPGRADING index b55cb53a2bad..3952b9ec2012 100644 --- a/UPGRADING +++ b/UPGRADING @@ -435,6 +435,14 @@ 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 throws a ValueError. 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. + Sockets returned by stream_socket_accept() inherit both sizes from the + listening socket. . Allowed casting filtered streams as file descriptor for select. . Added the "write_seek_mode stream" 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 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 eb7864108ea549604ea82a43e1df2c53b95f7e21 Mon Sep 17 00:00:00 2001 From: David Carlier Date: Sat, 22 Aug 2026 12:59:14 +0100 Subject: [PATCH 2/2] fix test and assumptions. --- UPGRADING | 2 - .../tests/network/so_rcvbuf_sndbuf.phpt | 54 ++++++++++--------- 2 files changed, 30 insertions(+), 26 deletions(-) diff --git a/UPGRADING b/UPGRADING index 3952b9ec2012..3d8aa09a040e 100644 --- a/UPGRADING +++ b/UPGRADING @@ -441,8 +441,6 @@ PHP 8.6 UPGRADE NOTES any other value throws a ValueError. 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. - Sockets returned by stream_socket_accept() inherit both sizes from the - listening socket. . Allowed casting filtered streams as file descriptor for select. . Added the "write_seek_mode stream" 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 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)