[PHP-CVS] [php-src] master: Add stream socket so_linger context option (#20808)
[email protected] (Jakub Zelenka via GitHub) Wed, 5 Aug 2026 18:15:45 +0000
| Newsgroups | php.cvs |
|---|---|
| Message-ID | <[email protected]> |
Author: Jakub Zelenka (bukka) Committer: GitHub (web-flow) Pusher: bukka Date: 2026-08-05T20:15:42+02:00 Commit: https://github.com/php/php-src/commit/3081f7f5ae0db7e2ce970d08739f6c816c0fd639 Raw diff: https://github.com/php/php-src/commit/3081f7f5ae0db7e2ce970d08739f6c816c0fd639.diff Add stream socket so_linger context option (#20808) This adds so_linger stream socket context options that is used to set the lingering time in seconds. If the value is lower or equal to 0, then the lingering is disabled. Co-authored-by: David Carlier <[email protected]> Changed paths: A ext/standard/tests/network/so_linger.phpt M NEWS M UPGRADING M main/network.c M main/php_network.h M main/streams/xp_socket.c Diff: diff --git a/NEWS b/NEWS index cdfca0fbf0f5..0f3ba67434dc 100644 --- a/NEWS +++ b/NEWS @@ -607,7 +607,8 @@ PHP NEWS . Added so_keepalive, tcp_keepidle, tcp_keepintvl and tcp_keepcnt stream socket context options. (Jakub Zelenka) . Added so_reuseaddr streams context socket option that allows disabling - address reuse. (Jakub Zelenka) + address resuse. (Jakub Zelenka) + . Added so_linger stream socket context option. (Jakub Zelenka) . Fixed bug GH-20370 (User stream filters could violate typed property constraints). (alexandre-daubois) . Allowed filtered streams to be casted as fd for select. (Jakub Zelenka) diff --git a/UPGRADING b/UPGRADING index 9389c7b04b20..ced365480707 100644 --- a/UPGRADING +++ b/UPGRADING @@ -382,6 +382,10 @@ PHP 8.6 UPGRADE NOTES . Added stream socket context options so_keepalive, tcp_keepidle, 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 + linger time is limited to an unsigned short on some platforms. . Allowed casting 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_linger.phpt b/ext/standard/tests/network/so_linger.phpt new file mode 100644 index 000000000000..ee0561b77381 --- /dev/null +++ b/ext/standard/tests/network/so_linger.phpt @@ -0,0 +1,82 @@ +--TEST-- +stream_socket_server() and stream_socket_client() SO_LINGER context option test +--EXTENSIONS-- +sockets +--FILE-- +<?php +// Test server with SO_LINGER enabled +$server_context = stream_context_create([ + 'socket' => [ + 'so_linger' => 10, + ] +]); + +$server = stream_socket_server("tcp://127.0.0.1:0", $errno, $errstr, + STREAM_SERVER_BIND | STREAM_SERVER_LISTEN, $server_context); + +if (!$server) { + die('Unable to create server'); +} + +$addr = stream_socket_get_name($server, false); +$port = (int)substr(strrchr($addr, ':'), 1); + +// Test client with SO_LINGER enabled +$client_context = stream_context_create([ + 'socket' => [ + 'so_linger' => 8, + ] +]); + +$client = stream_socket_client("tcp://127.0.0.1:$port", $errno, $errstr, 30, + STREAM_CLIENT_CONNECT, $client_context); + +if (!$client) { + die('Unable to create client'); +} + +$accepted = stream_socket_accept($server, 1); + +if (!$accepted) { + die('Unable to accept connection'); +} + +// macOS expresses SO_LINGER in ticks, SO_LINGER_SEC in seconds. +$so_linger = defined('SO_LINGER_SEC') ? SO_LINGER_SEC : SO_LINGER; + +// Verify the listening socket +$listen_sock = socket_import_stream($server); +$listen_linger = socket_get_option($listen_sock, SOL_SOCKET, $so_linger); +echo "Listen SO_LINGER\n"; +var_dump($listen_linger['l_onoff'] > 0); +var_dump($listen_linger['l_linger']); + +// Verify server side (accepted connection, inherits from the listening socket) +$server_sock = socket_import_stream($accepted); +$server_linger = socket_get_option($server_sock, SOL_SOCKET, $so_linger); +echo "Server SO_LINGER\n"; +var_dump($server_linger['l_onoff'] > 0); +var_dump($server_linger['l_linger']); + +// Verify client side +$client_sock = socket_import_stream($client); +$client_linger = socket_get_option($client_sock, SOL_SOCKET, $so_linger); +echo "Client SO_LINGER\n"; +var_dump($client_linger['l_onoff'] > 0); +var_dump($client_linger['l_linger']); + +fclose($accepted); +fclose($client); +fclose($server); + +?> +--EXPECT-- +Listen SO_LINGER +bool(true) +int(10) +Server SO_LINGER +bool(true) +int(10) +Client SO_LINGER +bool(true) +int(8) diff --git a/main/network.c b/main/network.c index b4d3ded15196..836abaeac7dd 100644 --- a/main/network.c +++ b/main/network.c @@ -535,6 +535,24 @@ php_socket_t php_network_bind_socket_to_local_addr_ex(const char *host, unsigned /* 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)); @@ -1021,6 +1039,24 @@ php_socket_t php_network_connect_socket_to_host_ex(const char *host, unsigned sh /* 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)); diff --git a/main/php_network.h b/main/php_network.h index e6d3009a6c82..c966b7af0053 100644 --- a/main/php_network.h +++ b/main/php_network.h @@ -269,12 +269,14 @@ typedef struct { #define PHP_SOCKVAL_TCP_KEEPIDLE (1 << 1) #define PHP_SOCKVAL_TCP_KEEPCNT (1 << 2) #define PHP_SOCKVAL_TCP_KEEPINTVL (1 << 3) +#define PHP_SOCKVAL_SO_LINGER (1 << 4) #define PHP_SOCKVAL_IS_SET(sockvals, opt) ((sockvals)->mask & (opt)) typedef struct { unsigned int mask; int tcp_nodelay; + int linger; struct { int keepidle; int keepcnt; diff --git a/main/streams/xp_socket.c b/main/streams/xp_socket.c index 3eef731544de..a9e2fa2b1d05 100644 --- a/main/streams/xp_socket.c +++ b/main/streams/xp_socket.c @@ -759,6 +759,16 @@ 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) @@ -887,6 +897,16 @@ 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)