[php-src] master: Merge branch '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:16:09+01:00

Commit: https://github.com/php/php-src/commit/84f90c5b1fdc037ac0382ccd89ed936c03f0c02c
Raw diff: https://github.com/php/php-src/commit/84f90c5b1fdc037ac0382ccd89ed936c03f0c02c.diff

Merge branch 'PHP-8.5'

* PHP-8.5:
  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 0b27a13e624c..e1a8c1868b93 100644
--- a/ext/standard/fsock.c
+++ b/ext/standard/fsock.c
@@ -20,6 +20,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)
@@ -82,8 +83,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 7962f0f85de3..d544e8bf5f0f 100644
--- a/ext/standard/streamsfuncs.c
+++ b/ext/standard/streamsfuncs.c
@@ -21,6 +21,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
@@ -139,7 +140,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 abf68aeacb80..ceb0dc508a39 100644
--- a/main/streams/php_streams_int.h
+++ b/main/streams/php_streams_int.h
@@ -60,3 +60,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 171748e6a08a..2525e4126e88 100644
--- a/main/streams/streams.c
+++ b/main/streams/streams.c
@@ -140,6 +140,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;
+}
+
 /* allocate a new stream for a particular ops */
 PHPAPI php_stream *_php_stream_alloc(const php_stream_ops *ops, void *abstract, const char *persistent_id, const char *mode STREAMS_DC) /* {{{ */
 {
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.