From 6c89842b8540d5f5f22bfd828db5e0396672f3c9 Mon Sep 17 00:00:00 2001 From: Jakub Zelenka Date: Wed, 31 Dec 2025 00:04:04 +0100 Subject: [PATCH 1/3] Add stream socket so_linger context option 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. --- ext/standard/tests/network/so_linger.phpt | 81 +++++++++++++++++++++++ main/network.c | 18 +++++ main/php_network.h | 2 + main/streams/xp_socket.c | 20 ++++++ 4 files changed, 121 insertions(+) create mode 100644 ext/standard/tests/network/so_linger.phpt diff --git a/ext/standard/tests/network/so_linger.phpt b/ext/standard/tests/network/so_linger.phpt new file mode 100644 index 0000000000000..6598350f424ad --- /dev/null +++ b/ext/standard/tests/network/so_linger.phpt @@ -0,0 +1,81 @@ +--TEST-- +stream_socket_server() and stream_socket_client() SO_LINGER context option test +--EXTENSIONS-- +sockets +--SKIPIF-- + +--FILE-- + [ + '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'); +} + +// Verify server side (accepted connection) +$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); + +// 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); + +fclose($accepted); +fclose($client); +fclose($server); + +?> +--EXPECT-- +Server SO_LINGER +array(2) { + ["l_onoff"]=> + int(1) + ["l_linger"]=> + int(10) +} +Client SO_LINGER +array(2) { + ["l_onoff"]=> + int(1) + ["l_linger"]=> + int(8) +} diff --git a/main/network.c b/main/network.c index 54dbf2b8e580f..48a6cd13bd128 100644 --- a/main/network.c +++ b/main/network.c @@ -541,6 +541,15 @@ 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) { + struct linger linger_val = { + .l_onoff = (sockvals->linger > 0), + .l_linger = (unsigned short)sockvals->linger + }; + setsockopt(sock, SOL_SOCKET, SO_LINGER, (char*)&linger_val, sizeof(linger_val)); + } +#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)); @@ -1027,6 +1036,15 @@ 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) { + struct linger linger_val = { + .l_onoff = (sockvals->linger > 0), + .l_linger = (unsigned short)sockvals->linger + }; + setsockopt(sock, SOL_SOCKET, SO_LINGER, (char*)&linger_val, sizeof(linger_val)); + } +#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 6700ab42dd3fa..276c155a01f7b 100644 --- a/main/php_network.h +++ b/main/php_network.h @@ -271,12 +271,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 9eae9e460e475..74ebade61f222 100644 --- a/main/streams/xp_socket.c +++ b/main/streams/xp_socket.c @@ -749,6 +749,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) @@ -877,6 +887,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) From 6c67b5fd197d4ae0c513ab3c4cf249d7c92331c9 Mon Sep 17 00:00:00 2001 From: Jakub Zelenka Date: Fri, 6 Mar 2026 20:36:15 +0100 Subject: [PATCH 2/3] Use SO_LINGER_SEC if available (MacOS) --- ext/standard/tests/network/so_linger.phpt | 6 ++++-- main/network.c | 8 ++++++++ 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/ext/standard/tests/network/so_linger.phpt b/ext/standard/tests/network/so_linger.phpt index 6598350f424ad..126cf4919d8e1 100644 --- a/ext/standard/tests/network/so_linger.phpt +++ b/ext/standard/tests/network/so_linger.phpt @@ -47,15 +47,17 @@ if (!$accepted) { die('Unable to accept connection'); } +$so_linger = PHP_OS_FAMILY === 'Darwin' ? SO_LINGER_SEC : SO_LINGER; + // Verify server side (accepted connection) $server_sock = socket_import_stream($accepted); -$server_linger = socket_get_option($server_sock, SOL_SOCKET, SO_LINGER); +$server_linger = socket_get_option($server_sock, SOL_SOCKET, $so_linger); echo "Server SO_LINGER\n"; var_dump($server_linger); // Verify client side $client_sock = socket_import_stream($client); -$client_linger = socket_get_option($client_sock, SOL_SOCKET, SO_LINGER); +$client_linger = socket_get_option($client_sock, SOL_SOCKET, $so_linger); echo "Client SO_LINGER\n"; var_dump($client_linger); diff --git a/main/network.c b/main/network.c index 48a6cd13bd128..794d2e4a33f42 100644 --- a/main/network.c +++ b/main/network.c @@ -547,7 +547,11 @@ php_socket_t php_network_bind_socket_to_local_addr_ex(const char *host, unsigned .l_onoff = (sockvals->linger > 0), .l_linger = (unsigned short)sockvals->linger }; +#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) @@ -1042,7 +1046,11 @@ php_socket_t php_network_connect_socket_to_host_ex(const char *host, unsigned sh .l_onoff = (sockvals->linger > 0), .l_linger = (unsigned short)sockvals->linger }; +#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) From eefbf0e5ef28fdf3231342bf81541f8e55e2d7a9 Mon Sep 17 00:00:00 2001 From: Jakub Zelenka Date: Fri, 6 Mar 2026 21:59:39 +0100 Subject: [PATCH 3/3] Do not relay on exact l_onoff value (FreeBSD uses 128 instead of 1) --- ext/standard/tests/network/so_linger.phpt | 22 ++++++++-------------- 1 file changed, 8 insertions(+), 14 deletions(-) diff --git a/ext/standard/tests/network/so_linger.phpt b/ext/standard/tests/network/so_linger.phpt index 126cf4919d8e1..1193f147054b2 100644 --- a/ext/standard/tests/network/so_linger.phpt +++ b/ext/standard/tests/network/so_linger.phpt @@ -53,13 +53,15 @@ $so_linger = PHP_OS_FAMILY === 'Darwin' ? SO_LINGER_SEC : SO_LINGER; $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); +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); +var_dump($client_linger['l_onoff'] > 0); +var_dump($client_linger['l_linger']); fclose($accepted); fclose($client); @@ -68,16 +70,8 @@ fclose($server); ?> --EXPECT-- Server SO_LINGER -array(2) { - ["l_onoff"]=> - int(1) - ["l_linger"]=> - int(10) -} +bool(true) +int(10) Client SO_LINGER -array(2) { - ["l_onoff"]=> - int(1) - ["l_linger"]=> - int(8) -} +bool(true) +int(8)