From 696c3a57ba8ecf2fcef82f1162eb38e4009d1292 Mon Sep 17 00:00:00 2001 From: David Carlier Date: Sat, 5 Sep 2026 12:55:27 +0100 Subject: [PATCH] streams: apply so_linger, so_rcvbuf and so_sndbuf to unix sockets The unix and udg transports returned from the AF_UNIX branch before any socket context option was read. The parsing happens before the transport split now and the values are applied through the new php_network_apply_sockvals(). --- UPGRADING | 14 +-- .../tests/network/so_linger_unix.phpt | 85 +++++++++++++ .../tests/network/so_rcvbuf_sndbuf_udg.phpt | 58 +++++++++ .../tests/network/so_rcvbuf_sndbuf_unix.phpt | 108 ++++++++++++++++ main/network.c | 118 ++++++------------ main/php_network.h | 2 + main/streams/xp_socket.c | 55 ++++---- 7 files changed, 320 insertions(+), 120 deletions(-) create mode 100644 ext/standard/tests/network/so_linger_unix.phpt create mode 100644 ext/standard/tests/network/so_rcvbuf_sndbuf_udg.phpt create mode 100644 ext/standard/tests/network/so_rcvbuf_sndbuf_unix.phpt diff --git a/UPGRADING b/UPGRADING index fe6395dad746..815964b2de4d 100644 --- a/UPGRADING +++ b/UPGRADING @@ -483,16 +483,16 @@ PHP 8.6 UPGRADE NOTES tcp_keepintvl and tcp_keepcnt that allow setting socket keepalive options. . Added stream socket context option so_linger that sets SO_LINGER on TCP - sockets. A positive value enables lingering for that many seconds, zero - or a negative value disables it. Values above 65535 are clamped as the + and unix 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. + TCP, UDP, unix and unix datagram 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_linger_unix.phpt b/ext/standard/tests/network/so_linger_unix.phpt new file mode 100644 index 000000000000..fe77578f31ac --- /dev/null +++ b/ext/standard/tests/network/so_linger_unix.phpt @@ -0,0 +1,85 @@ +--TEST-- +stream_socket_server() and stream_socket_client() SO_LINGER context option test with unix sockets +--EXTENSIONS-- +sockets +--SKIPIF-- + +--FILE-- + ['so_linger' => 10]])); + +if (!$server) { + die('Unable to create server'); +} + +$client = stream_socket_client("unix://$server_path", $errno, $errstr, 30, + STREAM_CLIENT_CONNECT, + stream_context_create(['socket' => ['so_linger' => 8]])); + +if (!$client) { + die('Unable to create client'); +} + +$accepted = stream_socket_accept($server, 1); + +if (!$accepted) { + die('Unable to accept connection'); +} + +$listen_linger = linger($server); +echo "Listen SO_LINGER", PHP_EOL; +var_dump($listen_linger['l_onoff'] > 0); +var_dump($listen_linger['l_linger']); + +$client_linger = linger($client); +echo "Client SO_LINGER", PHP_EOL; +var_dump($client_linger['l_onoff'] > 0); +var_dump($client_linger['l_linger']); + +// The option is meaningless on datagram sockets and is not applied there. +$dgram = stream_socket_server("udg://$dgram_path", $errno, $errstr, STREAM_SERVER_BIND, + stream_context_create(['socket' => ['so_linger' => 10]])); + +if (!$dgram) { + die('Unable to create server'); +} + +$dgram_linger = linger($dgram); +echo "Datagram SO_LINGER", PHP_EOL; +var_dump($dgram_linger['l_onoff'] > 0); + +fclose($accepted); +fclose($client); +fclose($server); +fclose($dgram); +unlink($server_path); +unlink($dgram_path); + +?> +--EXPECT-- +Listen SO_LINGER +bool(true) +int(10) +Client SO_LINGER +bool(true) +int(8) +Datagram SO_LINGER +bool(false) diff --git a/ext/standard/tests/network/so_rcvbuf_sndbuf_udg.phpt b/ext/standard/tests/network/so_rcvbuf_sndbuf_udg.phpt new file mode 100644 index 000000000000..8b7528d79a2b --- /dev/null +++ b/ext/standard/tests/network/so_rcvbuf_sndbuf_udg.phpt @@ -0,0 +1,58 @@ +--TEST-- +stream_socket_server() SO_RCVBUF and SO_SNDBUF context options test with unix datagram sockets +--EXTENSIONS-- +sockets +--SKIPIF-- + +--FILE-- + [ + 'so_rcvbuf' => intdiv($rcvbuf, 4), + 'so_sndbuf' => intdiv($sndbuf, 4), +]]); + +$server = stream_socket_server("udg://$server_path", $errno, $errstr, STREAM_SERVER_BIND, $context); + +if (!$server) { + die('Unable to create server'); +} + +[$server_rcvbuf, $server_sndbuf] = buffers($server); +echo "Server buffers", PHP_EOL; +var_dump($server_rcvbuf < $rcvbuf); +var_dump($server_sndbuf < $sndbuf); + +fclose($server); +fclose($control); +unlink($server_path); +unlink($control_path); + +?> +--EXPECT-- +Server buffers +bool(true) +bool(true) diff --git a/ext/standard/tests/network/so_rcvbuf_sndbuf_unix.phpt b/ext/standard/tests/network/so_rcvbuf_sndbuf_unix.phpt new file mode 100644 index 000000000000..962fb4a1f6a2 --- /dev/null +++ b/ext/standard/tests/network/so_rcvbuf_sndbuf_unix.phpt @@ -0,0 +1,108 @@ +--TEST-- +stream_socket_server() and stream_socket_client() SO_RCVBUF and SO_SNDBUF context options test with unix sockets +--EXTENSIONS-- +sockets +--SKIPIF-- + +--FILE-- + [ + 'so_rcvbuf' => intdiv($rcvbuf, 4), + 'so_sndbuf' => intdiv($sndbuf, 4), + ]]); +} + +$server = stream_socket_server("unix://$server_path", $errno, $errstr, + STREAM_SERVER_BIND | STREAM_SERVER_LISTEN, context($rcvbuf, $sndbuf)); + +if (!$server) { + die('Unable to create server'); +} + +echo "Listen buffers", PHP_EOL; +[$listen_rcvbuf, $listen_sndbuf] = buffers($server); +var_dump($listen_rcvbuf < $rcvbuf); +var_dump($listen_sndbuf < $sndbuf); + +$client = stream_socket_client("unix://$server_path", $errno, $errstr, 30, + STREAM_CLIENT_CONNECT, context($client_rcvbuf, $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", PHP_EOL; +[$rcvbuf2, $sndbuf2] = buffers($client); +var_dump($rcvbuf2 < $client_rcvbuf); +var_dump($sndbuf2 < $client_sndbuf); + +$invalid = stream_context_create(['socket' => ['so_rcvbuf' => 0]]); + +echo "Invalid buffer size", PHP_EOL; +var_dump(@stream_socket_server("unix://$server_path.invalid", $errno, $errstr, + STREAM_SERVER_BIND | STREAM_SERVER_LISTEN, $invalid)); +echo $errstr, PHP_EOL; +var_dump(@stream_socket_client("unix://$server_path", $errno, $errstr, 30, + STREAM_CLIENT_CONNECT, $invalid)); +echo $errstr, PHP_EOL; + +fclose($accepted); +fclose($client); +fclose($server); +fclose($control_client); +fclose($control); +unlink($server_path); +unlink($control_path); + +?> +--EXPECT-- +Listen buffers +bool(true) +bool(true) +Client buffers +bool(true) +bool(true) +Invalid buffer size +bool(false) +so_rcvbuf context option must be between 1 and 2147483647 +bool(false) +so_rcvbuf context option must be between 1 and 2147483647 diff --git a/main/network.c b/main/network.c index 01e4f2e778d4..1eee7390a23a 100644 --- a/main/network.c +++ b/main/network.c @@ -443,8 +443,42 @@ 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) +PHPAPI void php_network_apply_sockvals(php_socket_t sock, const php_sockvals *sockvals) { +#ifdef SO_LINGER + if (sockvals->mask & PHP_SOCKVAL_SO_LINGER) { + unsigned short secs = sockvals->linger > USHRT_MAX + ? USHRT_MAX : (unsigned short)sockvals->linger; + struct linger linger_val = { + .l_onoff = (sockvals->linger > 0), + .l_linger = sockvals->linger > 0 ? secs : 0 + }; +#ifdef SO_LINGER_SEC + setsockopt(sock, SOL_SOCKET, SO_LINGER_SEC, (char*)&linger_val, sizeof(linger_val)); +#else + setsockopt(sock, SOL_SOCKET, SO_LINGER, (char*)&linger_val, sizeof(linger_val)); +#endif + } +#endif +#if defined(TCP_KEEPIDLE) + if (sockvals->mask & PHP_SOCKVAL_TCP_KEEPIDLE) { + setsockopt(sock, IPPROTO_TCP, TCP_KEEPIDLE, (char*)&sockvals->keepalive.keepidle, sizeof(sockvals->keepalive.keepidle)); + } +#elif defined(TCP_KEEPALIVE) + if (sockvals->mask & PHP_SOCKVAL_TCP_KEEPIDLE) { + setsockopt(sock, IPPROTO_TCP, TCP_KEEPALIVE, (char*)&sockvals->keepalive.keepidle, sizeof(sockvals->keepalive.keepidle)); + } +#endif +#ifdef TCP_KEEPINTVL + if (sockvals->mask & PHP_SOCKVAL_TCP_KEEPINTVL) { + setsockopt(sock, IPPROTO_TCP, TCP_KEEPINTVL, (char*)&sockvals->keepalive.keepintvl, sizeof(sockvals->keepalive.keepintvl)); + } +#endif +#ifdef TCP_KEEPCNT + if (sockvals->mask & PHP_SOCKVAL_TCP_KEEPCNT) { + setsockopt(sock, IPPROTO_TCP, TCP_KEEPCNT, (char*)&sockvals->keepalive.keepcnt, sizeof(sockvals->keepalive.keepcnt)); + } +#endif #ifdef SO_RCVBUF if (sockvals->mask & PHP_SOCKVAL_SO_RCVBUF) { setsockopt(sock, SOL_SOCKET, SO_RCVBUF, (char*)&sockvals->rcvbuf, sizeof(sockvals->rcvbuf)); @@ -547,47 +581,8 @@ php_socket_t php_network_bind_socket_to_local_addr_ex(const char *host, unsigned } #endif - /* Set socket values if provided */ if (sockvals != NULL) { -#ifdef SO_LINGER - if (sockvals->mask & PHP_SOCKVAL_SO_LINGER) { - /* l_linger is an unsigned short on Windows, so clamp rather than - * truncate: a truncated value may still be in range and would then - * be applied silently (e.g. 65536 becoming 0, an abortive close). */ - unsigned short secs = sockvals->linger > USHRT_MAX - ? USHRT_MAX : (unsigned short)sockvals->linger; - struct linger linger_val = { - .l_onoff = (sockvals->linger > 0), - .l_linger = sockvals->linger > 0 ? secs : 0 - }; -#ifdef SO_LINGER_SEC - setsockopt(sock, SOL_SOCKET, SO_LINGER_SEC, (char*)&linger_val, sizeof(linger_val)); -#else - setsockopt(sock, SOL_SOCKET, SO_LINGER, (char*)&linger_val, sizeof(linger_val)); -#endif - } -#endif -#if defined(TCP_KEEPIDLE) - if (sockvals->mask & PHP_SOCKVAL_TCP_KEEPIDLE) { - setsockopt(sock, IPPROTO_TCP, TCP_KEEPIDLE, (char*)&sockvals->keepalive.keepidle, sizeof(sockvals->keepalive.keepidle)); - } -#elif defined(TCP_KEEPALIVE) - /* macOS uses TCP_KEEPALIVE instead of TCP_KEEPIDLE */ - if (sockvals->mask & PHP_SOCKVAL_TCP_KEEPIDLE) { - setsockopt(sock, IPPROTO_TCP, TCP_KEEPALIVE, (char*)&sockvals->keepalive.keepidle, sizeof(sockvals->keepalive.keepidle)); - } -#endif -#ifdef TCP_KEEPINTVL - if (sockvals->mask & PHP_SOCKVAL_TCP_KEEPINTVL) { - setsockopt(sock, IPPROTO_TCP, TCP_KEEPINTVL, (char*)&sockvals->keepalive.keepintvl, sizeof(sockvals->keepalive.keepintvl)); - } -#endif -#ifdef TCP_KEEPCNT - if (sockvals->mask & PHP_SOCKVAL_TCP_KEEPCNT) { - setsockopt(sock, IPPROTO_TCP, TCP_KEEPCNT, (char*)&sockvals->keepalive.keepcnt, sizeof(sockvals->keepalive.keepcnt)); - } -#endif - php_network_set_socket_buffers(sock, sockvals); + php_network_apply_sockvals(sock, sockvals); } n = bind(sock, sa, socklen); @@ -1052,47 +1047,8 @@ php_socket_t php_network_connect_socket_to_host_ex(const char *host, unsigned sh } #endif - /* Set socket values if provided */ if (sockvals != NULL) { -#ifdef SO_LINGER - if (sockvals->mask & PHP_SOCKVAL_SO_LINGER) { - /* l_linger is an unsigned short on Windows, so clamp rather than - * truncate: a truncated value may still be in range and would then - * be applied silently (e.g. 65536 becoming 0, an abortive close). */ - unsigned short secs = sockvals->linger > USHRT_MAX - ? USHRT_MAX : (unsigned short)sockvals->linger; - struct linger linger_val = { - .l_onoff = (sockvals->linger > 0), - .l_linger = sockvals->linger > 0 ? secs : 0 - }; -#ifdef SO_LINGER_SEC - setsockopt(sock, SOL_SOCKET, SO_LINGER_SEC, (char*)&linger_val, sizeof(linger_val)); -#else - setsockopt(sock, SOL_SOCKET, SO_LINGER, (char*)&linger_val, sizeof(linger_val)); -#endif - } -#endif -#if defined(TCP_KEEPIDLE) - if (sockvals->mask & PHP_SOCKVAL_TCP_KEEPIDLE) { - setsockopt(sock, IPPROTO_TCP, TCP_KEEPIDLE, (char*)&sockvals->keepalive.keepidle, sizeof(sockvals->keepalive.keepidle)); - } -#elif defined(TCP_KEEPALIVE) - /* macOS uses TCP_KEEPALIVE instead of TCP_KEEPIDLE */ - if (sockvals->mask & PHP_SOCKVAL_TCP_KEEPIDLE) { - setsockopt(sock, IPPROTO_TCP, TCP_KEEPALIVE, (char*)&sockvals->keepalive.keepidle, sizeof(sockvals->keepalive.keepidle)); - } -#endif -#ifdef TCP_KEEPINTVL - if (sockvals->mask & PHP_SOCKVAL_TCP_KEEPINTVL) { - setsockopt(sock, IPPROTO_TCP, TCP_KEEPINTVL, (char*)&sockvals->keepalive.keepintvl, sizeof(sockvals->keepalive.keepintvl)); - } -#endif -#ifdef TCP_KEEPCNT - if (sockvals->mask & PHP_SOCKVAL_TCP_KEEPCNT) { - setsockopt(sock, IPPROTO_TCP, TCP_KEEPCNT, (char*)&sockvals->keepalive.keepcnt, sizeof(sockvals->keepalive.keepcnt)); - } -#endif - php_network_set_socket_buffers(sock, sockvals); + php_network_apply_sockvals(sock, sockvals); } n = php_network_connect_socket(sock, sa, socklen, asynchronous, diff --git a/main/php_network.h b/main/php_network.h index c93a519911f0..5c2cb0acede2 100644 --- a/main/php_network.h +++ b/main/php_network.h @@ -292,6 +292,8 @@ BEGIN_EXTERN_C() PHPAPI int php_network_getaddresses(const char *host, int socktype, struct sockaddr ***sal, zend_string **error_string); PHPAPI void php_network_freeaddresses(struct sockaddr **sal); +PHPAPI void php_network_apply_sockvals(php_socket_t sock, const php_sockvals *sockvals); + PHPAPI php_socket_t php_network_connect_socket_to_host_ex(const char *host, unsigned short port, int socktype, int asynchronous, struct timeval *timeout, zend_string **error_string, int *error_code, const char *bindto, unsigned short bindport, long sockopts, php_sockvals *sockvals diff --git a/main/streams/xp_socket.c b/main/streams/xp_socket.c index 3844414d8048..eef1f30c4681 100644 --- a/main/streams/xp_socket.c +++ b/main/streams/xp_socket.c @@ -53,7 +53,7 @@ static const php_stream_ops php_stream_unixdg_socket_ops; (PHP_STREAM_XPORT_IS_UNIX_DG(stream) || PHP_STREAM_XPORT_IS_UNIX_ST(stream)) #else #define PHP_STREAM_XPORT_IS_UNIX_DG(stream) false -#define PHP_STREAM_XPORT_IS_UNIX_STD(stream) false +#define PHP_STREAM_XPORT_IS_UNIX_ST(stream) false #define PHP_STREAM_XPORT_IS_UNIX(stream) false #endif #define PHP_STREAM_XPORT_IS_UDP(stream) (php_stream_is(stream, &php_stream_udp_socket_ops)) @@ -677,7 +677,7 @@ 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, +static int php_sockop_parse_sockvals(php_stream *stream, php_stream_xport_param *xparam, php_sockvals *sockvals) { zval *tmpzval; @@ -686,6 +686,15 @@ static int php_sockop_parse_buffer_sizes(php_stream *stream, php_stream_xport_pa return 0; } +#ifdef SO_LINGER + if ((PHP_STREAM_XPORT_IS_TCP(stream) || PHP_STREAM_XPORT_IS_UNIX_ST(stream)) + && (tmpzval = php_stream_context_get_option(PHP_STREAM_CONTEXT(stream), "socket", "so_linger")) != NULL + ) { + sockvals->mask |= PHP_SOCKVAL_SO_LINGER; + sockvals->linger = (int)zval_get_long(tmpzval); + } +#endif + #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); @@ -730,6 +739,10 @@ static inline int php_tcp_sockop_bind(php_stream *stream, php_netstream_data_t * zval *tmpzval = NULL; php_sockvals sockvals = {0}; + if (php_sockop_parse_sockvals(stream, xparam, &sockvals) == -1) { + return -1; + } + #ifdef AF_UNIX if (PHP_STREAM_XPORT_IS_UNIX(stream)) { struct sockaddr_un unix_addr; @@ -746,6 +759,8 @@ static inline int php_tcp_sockop_bind(php_stream *stream, php_netstream_data_t * return -1; } + php_network_apply_sockvals(sock->socket, &sockvals); + parse_unix_address(stream, xparam, &unix_addr); int result = bind(sock->socket, (const struct sockaddr *)&unix_addr, @@ -764,11 +779,6 @@ 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 @@ -808,16 +818,6 @@ static inline int php_tcp_sockop_bind(php_stream *stream, php_netstream_data_t * } #endif -#ifdef SO_LINGER - if (PHP_STREAM_XPORT_IS_TCP(stream) - && PHP_STREAM_CONTEXT(stream) - && (tmpzval = php_stream_context_get_option(PHP_STREAM_CONTEXT(stream), "socket", "so_linger")) != NULL - ) { - sockvals.mask |= PHP_SOCKVAL_SO_LINGER; - sockvals.linger = (int)zval_get_long(tmpzval); - } -#endif - #ifdef SO_KEEPALIVE if (PHP_STREAM_XPORT_IS_TCP(stream) /* SO_KEEPALIVE is only applicable for TCP */ && PHP_STREAM_CONTEXT(stream) @@ -884,6 +884,10 @@ static inline int php_tcp_sockop_connect(php_stream *stream, php_netstream_data_ long sockopts = STREAM_SOCKOP_NONE; php_sockvals sockvals = {0}; + if (php_sockop_parse_sockvals(stream, xparam, &sockvals) == -1) { + return -1; + } + #ifdef AF_UNIX if (PHP_STREAM_XPORT_IS_UNIX(stream)) { struct sockaddr_un unix_addr; @@ -897,6 +901,8 @@ static inline int php_tcp_sockop_connect(php_stream *stream, php_netstream_data_ return -1; } + php_network_apply_sockvals(sock->socket, &sockvals); + parse_unix_address(stream, xparam, &unix_addr); ret = php_network_connect_socket(sock->socket, @@ -917,11 +923,6 @@ 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) { @@ -951,16 +952,6 @@ static inline int php_tcp_sockop_connect(php_stream *stream, php_netstream_data_ sockopts |= STREAM_SOCKOP_TCP_NODELAY; } -#ifdef SO_LINGER - if (PHP_STREAM_XPORT_IS_TCP(stream) - && PHP_STREAM_CONTEXT(stream) - && (tmpzval = php_stream_context_get_option(PHP_STREAM_CONTEXT(stream), "socket", "so_linger")) != NULL - ) { - sockvals.mask |= PHP_SOCKVAL_SO_LINGER; - sockvals.linger = (int)zval_get_long(tmpzval); - } -#endif - #ifdef SO_KEEPALIVE if (PHP_STREAM_XPORT_IS_TCP(stream) /* SO_KEEPALIVE is only applicable for TCP */ && PHP_STREAM_CONTEXT(stream)