Commit 0fd8aae6e85 for php.net

commit 0fd8aae6e851b94123288ea67726ea68622c0c17
Author: Jakub Zelenka <bukka@php.net>
Date:   Tue Dec 30 16:53:22 2025 +0100

    Fix TCP_KEEPALIVE no inheriting for accepted sockets on MacOS

diff --git a/ext/openssl/xp_ssl.c b/ext/openssl/xp_ssl.c
index 62929246a07..482715c7455 100644
--- a/ext/openssl/xp_ssl.c
+++ b/ext/openssl/xp_ssl.c
@@ -2219,25 +2219,32 @@ static int php_openssl_sockop_stat(php_stream *stream, php_stream_statbuf *ssb)
 static inline int php_openssl_tcp_sockop_accept(php_stream *stream, php_openssl_netstream_data_t *sock,
 		php_stream_xport_param *xparam STREAMS_DC)  /* {{{ */
 {
-	bool nodelay = false;
+	php_sockvals sockvals = {0};
 	zval *tmpzval = NULL;

 	xparam->outputs.client = NULL;

-	if (PHP_STREAM_CONTEXT(stream) &&
-		(tmpzval = php_stream_context_get_option(PHP_STREAM_CONTEXT(stream), "socket", "tcp_nodelay")) != NULL &&
-		zend_is_true(tmpzval)) {
-		nodelay = true;
+	if (PHP_STREAM_CONTEXT(stream)) {
+		tmpzval = php_stream_context_get_option(PHP_STREAM_CONTEXT(stream), "socket", "tcp_nodelay");
+		if (tmpzval != NULL && zend_is_true(tmpzval)) {
+			sockvals.mask |= PHP_SOCKVAL_TCP_NODELAY;
+			sockvals.tcp_nodelay = 1;
+		}
+		tmpzval = php_stream_context_get_option(PHP_STREAM_CONTEXT(stream), "socket", "tcp_keepidle");
+		if (tmpzval != NULL && Z_TYPE_P(tmpzval) == IS_LONG) {
+			sockvals.mask |= PHP_SOCKVAL_TCP_KEEPIDLE;
+			sockvals.keepalive.keepidle = Z_LVAL_P(tmpzval);
+		}
 	}

-	php_socket_t clisock = php_network_accept_incoming(sock->s.socket,
+	php_socket_t clisock = php_network_accept_incoming_ex(sock->s.socket,
 		xparam->want_textaddr ? &xparam->outputs.textaddr : NULL,
 		xparam->want_addr ? &xparam->outputs.addr : NULL,
 		xparam->want_addr ? &xparam->outputs.addrlen : NULL,
 		xparam->inputs.timeout,
 		xparam->want_errortext ? &xparam->outputs.error_text : NULL,
 		&xparam->outputs.error_code,
-		nodelay);
+		&sockvals);

 	if (clisock != SOCK_ERR) {
 		php_openssl_netstream_data_t *clisockdata = (php_openssl_netstream_data_t*) emalloc(sizeof(*clisockdata));
diff --git a/main/network.c b/main/network.c
index 58f688a4fea..c4e2ab2e67d 100644
--- a/main/network.c
+++ b/main/network.c
@@ -801,15 +801,22 @@ PHPAPI int php_network_get_sock_name(php_socket_t sock,
  * version of the address will be emalloc'd and returned.
  * */

-/* {{{ php_network_accept_incoming */
-PHPAPI php_socket_t php_network_accept_incoming(php_socket_t srvsock,
+ /* Accept a client connection from a server socket,
+ * using an optional timeout.
+ * Returns the peer address in addr/addrlen (it will emalloc
+ * these, so be sure to efree the result).
+ * If you specify textaddr, a text-printable
+ * version of the address will be emalloc'd and returned.
+ * */
+
+PHPAPI php_socket_t php_network_accept_incoming_ex(php_socket_t srvsock,
 		zend_string **textaddr,
 		struct sockaddr **addr,
 		socklen_t *addrlen,
 		struct timeval *timeout,
 		zend_string **error_string,
 		int *error_code,
-		int tcp_nodelay
+		php_sockvals *sockvals
 		)
 {
 	php_socket_t clisock = -1;
@@ -833,11 +840,19 @@ PHPAPI php_socket_t php_network_accept_incoming(php_socket_t srvsock,
 					textaddr,
 					addr, addrlen
 					);
-			if (tcp_nodelay) {
 #ifdef TCP_NODELAY
+			if (PHP_SOCKVAL_IS_SET(sockvals, PHP_SOCKVAL_TCP_NODELAY)) {
+				int tcp_nodelay = 1;
 				setsockopt(clisock, IPPROTO_TCP, TCP_NODELAY, (char*)&tcp_nodelay, sizeof(tcp_nodelay));
+			}
 #endif
+#ifdef TCP_KEEPALIVE
+			/* MacOS does not inherit TCP_KEEPALIVE so it needs to be set */
+			if (PHP_SOCKVAL_IS_SET(sockvals, PHP_SOCKVAL_TCP_KEEPIDLE)) {
+				setsockopt(clisock, IPPROTO_TCP, TCP_KEEPALIVE,
+						(char*)&sockvals->keepalive.keepidle, sizeof(sockvals->keepalive.keepidle));
 			}
+#endif
 		} else {
 			error = php_socket_errno();
 		}
@@ -852,7 +867,22 @@ PHPAPI php_socket_t php_network_accept_incoming(php_socket_t srvsock,

 	return clisock;
 }
-/* }}} */
+
+PHPAPI php_socket_t php_network_accept_incoming(php_socket_t srvsock,
+		zend_string **textaddr,
+		struct sockaddr **addr,
+		socklen_t *addrlen,
+		struct timeval *timeout,
+		zend_string **error_string,
+		int *error_code,
+		int tcp_nodelay
+		)
+{
+	php_sockvals sockvals = { .mask = tcp_nodelay ? PHP_SOCKVAL_TCP_NODELAY : 0 };
+
+	return php_network_accept_incoming_ex(srvsock, textaddr, addr, addrlen, timeout, error_string,
+			error_code, &sockvals);
+}

 /* Connect to a remote host using an interruptible connect with optional timeout.
  * Optionally, the connect can be made asynchronously, which will implicitly
diff --git a/main/php_network.h b/main/php_network.h
index 08d6bbc140c..db449ae35ff 100644
--- a/main/php_network.h
+++ b/main/php_network.h
@@ -267,12 +267,16 @@ typedef struct {
 } php_sockaddr_storage;
 #endif

-#define PHP_SOCKVAL_TCP_KEEPIDLE  (1 << 0)
-#define PHP_SOCKVAL_TCP_KEEPCNT   (1 << 1)
-#define PHP_SOCKVAL_TCP_KEEPINTVL (1 << 2)
+#define PHP_SOCKVAL_TCP_NODELAY   (1 << 0)
+#define PHP_SOCKVAL_TCP_KEEPIDLE  (1 << 1)
+#define PHP_SOCKVAL_TCP_KEEPCNT   (1 << 2)
+#define PHP_SOCKVAL_TCP_KEEPINTVL (1 << 3)
+
+#define PHP_SOCKVAL_IS_SET(sockvals, opt) (sockvals->mask & opt)

 typedef struct {
 	unsigned int mask;
+	int tcp_nodelay;
 	struct {
 		int keepidle;
 		int keepcnt;
@@ -313,6 +317,16 @@ PHPAPI php_socket_t php_network_bind_socket_to_local_addr(const char *host, unsi
 		int socktype, long sockopts, zend_string **error_string, int *error_code
 		);

+PHPAPI php_socket_t php_network_accept_incoming_ex(php_socket_t srvsock,
+		zend_string **textaddr,
+		struct sockaddr **addr,
+		socklen_t *addrlen,
+		struct timeval *timeout,
+		zend_string **error_string,
+		int *error_code,
+		php_sockvals *sockvals
+		);
+
 PHPAPI php_socket_t php_network_accept_incoming(php_socket_t srvsock,
 		zend_string **textaddr,
 		struct sockaddr **addr,
diff --git a/main/streams/xp_socket.c b/main/streams/xp_socket.c
index 969d364ffe1..7a7f007f918 100644
--- a/main/streams/xp_socket.c
+++ b/main/streams/xp_socket.c
@@ -964,25 +964,32 @@ static inline int php_tcp_sockop_connect(php_stream *stream, php_netstream_data_
 static inline int php_tcp_sockop_accept(php_stream *stream, php_netstream_data_t *sock,
 		php_stream_xport_param *xparam STREAMS_DC)
 {
-	bool nodelay = 0;
+	php_sockvals sockvals = {0};
 	zval *tmpzval = NULL;

 	xparam->outputs.client = NULL;

-	if ((NULL != PHP_STREAM_CONTEXT(stream)) &&
-		(tmpzval = php_stream_context_get_option(PHP_STREAM_CONTEXT(stream), "socket", "tcp_nodelay")) != NULL &&
-		zend_is_true(tmpzval)) {
-		nodelay = 1;
+	if (PHP_STREAM_CONTEXT(stream)) {
+		tmpzval = php_stream_context_get_option(PHP_STREAM_CONTEXT(stream), "socket", "tcp_nodelay");
+		if (tmpzval != NULL && zend_is_true(tmpzval)) {
+			sockvals.mask |= PHP_SOCKVAL_TCP_NODELAY;
+			sockvals.tcp_nodelay = 1;
+		}
+		tmpzval = php_stream_context_get_option(PHP_STREAM_CONTEXT(stream), "socket", "tcp_keepidle");
+		if (tmpzval != NULL && Z_TYPE_P(tmpzval) == IS_LONG) {
+			sockvals.mask |= PHP_SOCKVAL_TCP_KEEPIDLE;
+			sockvals.keepalive.keepidle = Z_LVAL_P(tmpzval);
+		}
 	}

-	php_socket_t clisock = php_network_accept_incoming(sock->socket,
+	php_socket_t clisock = php_network_accept_incoming_ex(sock->socket,
 		xparam->want_textaddr ? &xparam->outputs.textaddr : NULL,
 		xparam->want_addr ? &xparam->outputs.addr : NULL,
 		xparam->want_addr ? &xparam->outputs.addrlen : NULL,
 		xparam->inputs.timeout,
 		xparam->want_errortext ? &xparam->outputs.error_text : NULL,
 		&xparam->outputs.error_code,
-		nodelay);
+		&sockvals);

 	if (clisock != SOCK_ERR) {
 		php_netstream_data_t *clisockdata = (php_netstream_data_t*) emalloc(sizeof(*clisockdata));