From 08ffd70e0212cb788aca7a1e36e16cbb7c4c1867 Mon Sep 17 00:00:00 2001 From: David Carlier Date: Sun, 12 Jul 2026 23:47:06 +0100 Subject: [PATCH] GH-22617: avoid null byte truncation of persistent stream keys Abstract unix domain socket addresses begin with a null byte, but the persistent stream list is keyed by a NUL-terminated string. The key was truncated at that null byte, so distinct abstract sockets collapsed onto the same persistent resource in p(f)sockopen() and stream_socket_client(). Escape null bytes (and backslashes, to stay unambiguous) when building the persistent hash key so the full address is preserved. The escaping helper lives in ext/standard rather than the public streams header, since only p(f)sockopen() and stream_socket_client() need it. Fix #22617 --- ext/standard/file.h | 4 +++ ext/standard/fsock.c | 5 +-- ext/standard/streamsfuncs.c | 25 +++++++++++++- ext/standard/tests/streams/gh22617.phpt | 45 +++++++++++++++++++++++++ 4 files changed, 76 insertions(+), 3 deletions(-) create mode 100644 ext/standard/tests/streams/gh22617.phpt diff --git a/ext/standard/file.h b/ext/standard/file.h index 3a9cf1435b14..f1ededf083ac 100644 --- a/ext/standard/file.h +++ b/ext/standard/file.h @@ -44,6 +44,10 @@ PHPAPI void php_fstat(php_stream *stream, zval *return_value); PHPAPI void php_flock_common(php_stream *stream, zend_long operation, uint32_t operation_arg_num, zval *wouldblock, zval *return_value); +/* 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); + #define PHP_CSV_NO_ESCAPE EOF #define PHP_CSV_ESCAPE_ERROR -500 diff --git a/ext/standard/fsock.c b/ext/standard/fsock.c index 2b9e00a57554..6277456c98ea 100644 --- a/ext/standard/fsock.c +++ b/ext/standard/fsock.c @@ -84,8 +84,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 1929075b60b7..974d857d63cf 100644 --- a/ext/standard/streamsfuncs.c +++ b/ext/standard/streamsfuncs.c @@ -41,6 +41,27 @@ typedef unsigned __int64 php_timeout_ull; static php_stream_context *decode_context_param(zval *contextresource); +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; +} + /* Streams based network functions */ #ifdef HAVE_SOCKETPAIR @@ -130,7 +151,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-- + +--FILE-- + +--EXPECT-- +bool(true) +bool(true) +bool(true) +string(17) "persistent stream" +bool(true) +string(17) "persistent stream" +bool(true)