[php-src] master: Add stream socket so_linger context option (#20808)

Jakub Zelenka via GitHub <[email protected]>
Newsgroups gmane.comp.php.cvs.general
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)
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.