[php-src] PHP-8.5: Merge branch 'PHP-8.4' into PHP-8.5

David Carlier <[email protected]>
Newsgroups gmane.comp.php.cvs.general
Message-ID <[email protected]>
Author: David Carlier (devnexen)
Date: 2026-07-14T20:11:30+01:00

Commit: https://github.com/php/php-src/commit/3583a7b8bdb7e8f6f2f87c8ee6718857efbc2711
Raw diff: https://github.com/php/php-src/commit/3583a7b8bdb7e8f6f2f87c8ee6718857efbc2711.diff

Merge branch 'PHP-8.4' into PHP-8.5

* PHP-8.4:
  GH-22617: avoid null byte truncation of persistent stream keys

Changed paths:
  A  ext/standard/tests/streams/gh22617.phpt
  M  ext/standard/fsock.c
  M  ext/standard/streamsfuncs.c
  M  main/streams/php_streams_int.h
  M  main/streams/streams.c


Diff:

diff --git a/ext/standard/fsock.c b/ext/standard/fsock.c
index 2b9e00a57554..c8e6ed08e4c0 100644
--- a/ext/standard/fsock.c
+++ b/ext/standard/fsock.c
@@ -22,6 +22,7 @@
 #include <stddef.h>
 #include "php_network.h"
 #include "file.h"
+#include "streams/php_streams_int.h"
 
 static size_t php_fsockopen_format_host_port(char **message, const char *prefix, size_t prefix_len,
 	const char *host, size_t host_len, zend_long port)
@@ -84,8 +85,9 @@ static void php_fsockopen_stream(INTERNAL_FUNCTION_PARAMETERS, int persistent)
 	}
 
 	if (persistent) {
-		php_fsockopen_format_host_port(&hashkey, "pfsockopen__", strlen("pfsockopen__"), host,
-				host_len, port);
+		zend_string *escaped = php_stream_escape_persistent_key(host, host_len);
+		spprintf(&hashkey, 0, "pfsockopen__%s:"  ZEND_LONG_FMT, ZSTR_VAL(escaped), port);
+		zend_string_release_ex(escaped, false);
 	}
 
 	if (port > 0) {
diff --git a/ext/standard/streamsfuncs.c b/ext/standard/streamsfuncs.c
index 506ce0dafed8..9b23600b976a 100644
--- a/ext/standard/streamsfuncs.c
+++ b/ext/standard/streamsfuncs.c
@@ -23,6 +23,7 @@
 #include "streamsfuncs.h"
 #include "php_network.h"
 #include "php_string.h"
+#include "streams/php_streams_int.h"
 #ifdef HAVE_UNISTD_H
 #include <unistd.h>
 #endif
@@ -129,7 +130,9 @@ PHP_FUNCTION(stream_socket_client)
 	context = php_stream_context_from_zval(zcontext, flags & PHP_FILE_NO_DEFAULT_CONTEXT);
 
 	if (flags & PHP_STREAM_CLIENT_PERSISTENT) {
-		spprintf(&hashkey, 0, "stream_socket_client__%s", ZSTR_VAL(host));
+		zend_string *escaped = php_stream_escape_persistent_key(ZSTR_VAL(host), ZSTR_LEN(host));
+		spprintf(&hashkey, 0, "stream_socket_client__%s", ZSTR_VAL(escaped));
+		zend_string_release_ex(escaped, false);
 	}
 
 	/* prepare the timeout value for use */
diff --git a/ext/standard/tests/streams/gh22617.phpt b/ext/standard/tests/streams/gh22617.phpt
new file mode 100644
index 000000000000..83dbd832e652
--- /dev/null
+++ b/ext/standard/tests/streams/gh22617.phpt
@@ -0,0 +1,45 @@
+--TEST--
+GH-22617: Persistent abstract unix domain sockets resolved to wrong resource
+--CREDITS--
+Roysten
+--SKIPIF--
+<?php
+if (PHP_OS_FAMILY !== "Linux") die("skip abstract unix domain sockets are Linux-only");
+if (!function_exists("pfsockopen")) die("skip pfsockopen() not available");
+?>
+--FILE--
+<?php
+// Abstract sockets: the address is prefixed with a null byte.
+$name1 = "gh22617_" . getmypid() . "_1";
+$name2 = "gh22617_" . getmypid() . "_2";
+
+$server1 = stream_socket_server("unix://\0$name1", $errno, $errstr);
+$server2 = stream_socket_server("unix://\0$name2", $errno, $errstr);
+var_dump($server1 !== false, $server2 !== false);
+
+// Connect to the first abstract socket.
+$socket1 = pfsockopen("unix://\0$name1", 0, $errno1, $errstr1);
+var_dump($socket1 !== false);
+var_dump(get_resource_type($socket1));
+
+// Connect to the second abstract socket.
+$socket2 = pfsockopen("unix://\0$name2", 0, $errno2, $errstr2);
+var_dump($socket2 !== false);
+var_dump(get_resource_type($socket2));
+
+// The two distinct abstract sockets must resolve to distinct resources.
+var_dump((int) $socket1 !== (int) $socket2);
+
+fclose($socket1);
+fclose($socket2);
+fclose($server1);
+fclose($server2);
+?>
+--EXPECT--
+bool(true)
+bool(true)
+bool(true)
+string(17) "persistent stream"
+bool(true)
+string(17) "persistent stream"
+bool(true)
diff --git a/main/streams/php_streams_int.h b/main/streams/php_streams_int.h
index 7580088fba31..f9dda9a52ff7 100644
--- a/main/streams/php_streams_int.h
+++ b/main/streams/php_streams_int.h
@@ -62,3 +62,7 @@
  * any other function that expects standard modes and you allow non-standard
  * ones. result should be a char[5]. */
 void php_stream_mode_sanitize_fdopen_fopencookie(php_stream *stream, char *result);
+
+/* Escapes NUL and backslash bytes so a host containing them cannot truncate or
+ * collide the persistent stream hash key. */
+zend_string *php_stream_escape_persistent_key(const char *host, size_t hostlen);
diff --git a/main/streams/streams.c b/main/streams/streams.c
index 73fdc785721b..634d84df2670 100644
--- a/main/streams/streams.c
+++ b/main/streams/streams.c
@@ -141,6 +141,27 @@ PHPAPI int php_stream_from_persistent_id(const char *persistent_id, php_stream *
 
 /* }}} */
 
+zend_string *php_stream_escape_persistent_key(const char *host, size_t hostlen)
+{
+	zend_string *escaped = zend_string_safe_alloc(hostlen, 2, 0, 0);
+	char *ptr = ZSTR_VAL(escaped);
+	for (size_t i = 0; i < hostlen; i++) {
+		if (host[i] == '\0') {
+			*ptr++ = '\\';
+			*ptr++ = '0';
+		} else if (host[i] == '\\') {
+			*ptr++ = '\\';
+			*ptr++ = '\\';
+		} else {
+			*ptr++ = host[i];
+		}
+	}
+	*ptr = '\0';
+	ZSTR_LEN(escaped) = ptr - ZSTR_VAL(escaped);
+
+	return escaped;
+}
+
 static zend_llist *php_get_wrapper_errors_list(php_stream_wrapper *wrapper)
 {
 	if (!FG(wrapper_errors)) {
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.