diff --git a/NEWS b/NEWS index e507963f2059..f3c9b6ee73b0 100644 --- a/NEWS +++ b/NEWS @@ -2,6 +2,10 @@ PHP NEWS ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||| ?? ??? ????, PHP 8.6.0beta3 +- Core: + . Calling is_a() or is_subclass_of() with a string as the first argument + when $allow_string is false is now deprecated. (Daniel Scherzer) + - CLI: . Fixed bug GH-23242 (PHP development server does not support Expect 100-continue flow control). (Sjoerd Langkemper) @@ -80,6 +84,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 feaec16c4826..fe6395dad746 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 @@ -529,6 +536,10 @@ PHP 8.6 UPGRADE NOTES RFC: https://wiki.php.net/rfc/deprecations_php_8_6#deprecate_the_possibility_to_name_a_function_readonly . Passing a 3rd argument to define() is now deprecated. RFC: https://wiki.php.net/rfc/deprecations_php_8_6#deprecate_define_with_case_insensitive_being_specified + . Calling is_a() or is_subclass_of() with a string as the first argument + when $allow_string is false is now deprecated. + RFC: https://wiki.php.net/rfc/deprecations_php_8_6#deprecate_is_a_with_string_when_allow_string_is_false + RFC: https://wiki.php.net/rfc/deprecations_php_8_6#deprecate_is_subclass_of_with_string_when_allow_string_is_false - BZ2: . Passing an object for the Bzip2 {de}compression stream filter is now diff --git a/ext/standard/basic_functions.stub.php b/ext/standard/basic_functions.stub.php index 8ae94d8c6d99..6b3b11a5fa3e 100644 --- a/ext/standard/basic_functions.stub.php +++ b/ext/standard/basic_functions.stub.php @@ -2331,13 +2331,21 @@ function strcoll(string $string1, string $string2): int {} */ function trim(string $string, string $characters = " \f\n\r\t\v\0"): string {} -/** @compile-time-eval */ +/** + * @compile-time-eval + * @frameless-function {"arity": 1} + * @frameless-function {"arity": 2} + */ function rtrim(string $string, string $characters = " \f\n\r\t\v\0"): string {} /** @alias rtrim */ function chop(string $string, string $characters = " \f\n\r\t\v\0"): string {} -/** @compile-time-eval */ +/** + * @compile-time-eval + * @frameless-function {"arity": 1} + * @frameless-function {"arity": 2} + */ function ltrim(string $string, string $characters = " \f\n\r\t\v\0"): string {} /** diff --git a/ext/standard/basic_functions_arginfo.h b/ext/standard/basic_functions_arginfo.h index 4bcf008f5fdd..7d9a42460534 100644 --- a/ext/standard/basic_functions_arginfo.h +++ b/ext/standard/basic_functions_arginfo.h @@ -1,5 +1,5 @@ /* This is a generated file, edit basic_functions.stub.php instead. - * Stub hash: 31018a787ba261316941b0d88f090b9cf271aa0e + * Stub hash: 4feeab72edf6b7440af99a60e1401492828f141e * Has decl header: yes */ ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_set_time_limit, 0, 1, _IS_BOOL, 0) @@ -2238,6 +2238,22 @@ static const zend_frameless_function_info frameless_function_infos_trim[] = { { 0 }, }; +ZEND_FRAMELESS_FUNCTION(rtrim, 1); +ZEND_FRAMELESS_FUNCTION(rtrim, 2); +static const zend_frameless_function_info frameless_function_infos_rtrim[] = { + { ZEND_FRAMELESS_FUNCTION_NAME(rtrim, 1), 1 }, + { ZEND_FRAMELESS_FUNCTION_NAME(rtrim, 2), 2 }, + { 0 }, +}; + +ZEND_FRAMELESS_FUNCTION(ltrim, 1); +ZEND_FRAMELESS_FUNCTION(ltrim, 2); +static const zend_frameless_function_info frameless_function_infos_ltrim[] = { + { ZEND_FRAMELESS_FUNCTION_NAME(ltrim, 1), 1 }, + { ZEND_FRAMELESS_FUNCTION_NAME(ltrim, 2), 2 }, + { 0 }, +}; + ZEND_FRAMELESS_FUNCTION(implode, 1); ZEND_FRAMELESS_FUNCTION(implode, 2); static const zend_frameless_function_info frameless_function_infos_implode[] = { @@ -3178,9 +3194,9 @@ static const zend_function_entry ext_functions[] = { #endif ZEND_RAW_FENTRY("strcoll", zif_strcoll, arginfo_strcoll, ZEND_ACC_DEPRECATED, NULL, NULL) ZEND_RAW_FENTRY("trim", zif_trim, arginfo_trim, ZEND_ACC_COMPILE_TIME_EVAL, frameless_function_infos_trim, NULL) - ZEND_RAW_FENTRY("rtrim", zif_rtrim, arginfo_rtrim, ZEND_ACC_COMPILE_TIME_EVAL, NULL, NULL) + ZEND_RAW_FENTRY("rtrim", zif_rtrim, arginfo_rtrim, ZEND_ACC_COMPILE_TIME_EVAL, frameless_function_infos_rtrim, NULL) ZEND_RAW_FENTRY("chop", zif_rtrim, arginfo_chop, 0, NULL, NULL) - ZEND_RAW_FENTRY("ltrim", zif_ltrim, arginfo_ltrim, ZEND_ACC_COMPILE_TIME_EVAL, NULL, NULL) + ZEND_RAW_FENTRY("ltrim", zif_ltrim, arginfo_ltrim, ZEND_ACC_COMPILE_TIME_EVAL, frameless_function_infos_ltrim, NULL) ZEND_RAW_FENTRY("wordwrap", zif_wordwrap, arginfo_wordwrap, ZEND_ACC_COMPILE_TIME_EVAL, NULL, NULL) ZEND_RAW_FENTRY("explode", zif_explode, arginfo_explode, ZEND_ACC_COMPILE_TIME_EVAL, NULL, NULL) ZEND_RAW_FENTRY("implode", zif_implode, arginfo_implode, ZEND_ACC_COMPILE_TIME_EVAL, frameless_function_infos_implode, NULL) diff --git a/ext/standard/basic_functions_decl.h b/ext/standard/basic_functions_decl.h index db81e0bfc077..d60317766993 100644 --- a/ext/standard/basic_functions_decl.h +++ b/ext/standard/basic_functions_decl.h @@ -1,8 +1,8 @@ /* This is a generated file, edit basic_functions.stub.php instead. - * Stub hash: 31018a787ba261316941b0d88f090b9cf271aa0e */ + * Stub hash: 4feeab72edf6b7440af99a60e1401492828f141e */ -#ifndef ZEND_BASIC_FUNCTIONS_DECL_31018a787ba261316941b0d88f090b9cf271aa0e_H -#define ZEND_BASIC_FUNCTIONS_DECL_31018a787ba261316941b0d88f090b9cf271aa0e_H +#ifndef ZEND_BASIC_FUNCTIONS_DECL_4feeab72edf6b7440af99a60e1401492828f141e_H +#define ZEND_BASIC_FUNCTIONS_DECL_4feeab72edf6b7440af99a60e1401492828f141e_H typedef enum zend_enum_SortDirection { ZEND_ENUM_SortDirection_Ascending = 1, @@ -20,4 +20,4 @@ typedef enum zend_enum_RoundingMode { ZEND_ENUM_RoundingMode_PositiveInfinity = 8, } zend_enum_RoundingMode; -#endif /* ZEND_BASIC_FUNCTIONS_DECL_31018a787ba261316941b0d88f090b9cf271aa0e_H */ +#endif /* ZEND_BASIC_FUNCTIONS_DECL_4feeab72edf6b7440af99a60e1401492828f141e_H */ diff --git a/ext/standard/string.c b/ext/standard/string.c index af3f6a461dcf..2d868513e4ad 100644 --- a/ext/standard/string.c +++ b/ext/standard/string.c @@ -651,6 +651,34 @@ PHP_FUNCTION(rtrim) } /* }}} */ +ZEND_FRAMELESS_FUNCTION(rtrim, 1) +{ + zval str_tmp; + zend_string *str; + + Z_FLF_PARAM_STR(1, str, str_tmp); + + ZVAL_STR(return_value, php_trim_int(str, /* what */ NULL, /* what_len */ 0, /* mode */ 2)); + +flf_clean: + Z_FLF_PARAM_FREE_STR(1, str_tmp); +} + +ZEND_FRAMELESS_FUNCTION(rtrim, 2) +{ + zval str_tmp, what_tmp; + zend_string *str, *what; + + Z_FLF_PARAM_STR(1, str, str_tmp); + Z_FLF_PARAM_STR(2, what, what_tmp); + + ZVAL_STR(return_value, php_trim_int(str, ZSTR_VAL(what), ZSTR_LEN(what), /* mode */ 2)); + +flf_clean: + Z_FLF_PARAM_FREE_STR(1, str_tmp); + Z_FLF_PARAM_FREE_STR(2, what_tmp); +} + /* {{{ Strips whitespace from the beginning of a string */ PHP_FUNCTION(ltrim) { @@ -658,6 +686,34 @@ PHP_FUNCTION(ltrim) } /* }}} */ +ZEND_FRAMELESS_FUNCTION(ltrim, 1) +{ + zval str_tmp; + zend_string *str; + + Z_FLF_PARAM_STR(1, str, str_tmp); + + ZVAL_STR(return_value, php_trim_int(str, /* what */ NULL, /* what_len */ 0, /* mode */ 1)); + +flf_clean: + Z_FLF_PARAM_FREE_STR(1, str_tmp); +} + +ZEND_FRAMELESS_FUNCTION(ltrim, 2) +{ + zval str_tmp, what_tmp; + zend_string *str, *what; + + Z_FLF_PARAM_STR(1, str, str_tmp); + Z_FLF_PARAM_STR(2, what, what_tmp); + + ZVAL_STR(return_value, php_trim_int(str, ZSTR_VAL(what), ZSTR_LEN(what), /* mode */ 1)); + +flf_clean: + Z_FLF_PARAM_FREE_STR(1, str_tmp); + Z_FLF_PARAM_FREE_STR(2, what_tmp); +} + /* {{{ Wraps buffer to selected number of characters using string break char */ PHP_FUNCTION(wordwrap) { 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..fdb76713cb05 --- /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-- + [ + '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 000000000000..dbad2a3ff0e5 --- /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-- + [$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 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..3844414d8048 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) {