Commit 3081f7f5ae0 for php.net
commit 3081f7f5ae0db7e2ce970d08739f6c816c0fd639
Author: Jakub Zelenka <bukka@php.net>
Date: Wed Aug 5 20:15:42 2026 +0200
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 <devnexen@gmail.com>
diff --git a/NEWS b/NEWS
index cdfca0fbf0f..0f3ba67434d 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 9389c7b04b2..ced36548070 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 00000000000..ee0561b7738
--- /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 b4d3ded1519..836abaeac7d 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 e6d3009a6c8..c966b7af005 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 3eef731544d..a9e2fa2b1d0 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)