Skip to content

Commit 08ffd70

Browse files
committed
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
1 parent 834bdce commit 08ffd70

4 files changed

Lines changed: 76 additions & 3 deletions

File tree

ext/standard/file.h

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,10 @@ PHPAPI void php_fstat(php_stream *stream, zval *return_value);
4444
PHPAPI void php_flock_common(php_stream *stream, zend_long operation, uint32_t operation_arg_num,
4545
zval *wouldblock, zval *return_value);
4646

47+
/* Escapes NUL and backslash bytes so a host containing them cannot truncate or
48+
* collide the persistent stream hash key. */
49+
zend_string *php_stream_escape_persistent_key(const char *host, size_t hostlen);
50+
4751
#define PHP_CSV_NO_ESCAPE EOF
4852
#define PHP_CSV_ESCAPE_ERROR -500
4953

ext/standard/fsock.c

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -84,8 +84,9 @@ static void php_fsockopen_stream(INTERNAL_FUNCTION_PARAMETERS, int persistent)
8484
}
8585

8686
if (persistent) {
87-
php_fsockopen_format_host_port(&hashkey, "pfsockopen__", strlen("pfsockopen__"), host,
88-
host_len, port);
87+
zend_string *escaped = php_stream_escape_persistent_key(host, host_len);
88+
spprintf(&hashkey, 0, "pfsockopen__%s:" ZEND_LONG_FMT, ZSTR_VAL(escaped), port);
89+
zend_string_release_ex(escaped, false);
8990
}
9091

9192
if (port > 0) {

ext/standard/streamsfuncs.c

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,27 @@ typedef unsigned __int64 php_timeout_ull;
4141

4242
static php_stream_context *decode_context_param(zval *contextresource);
4343

44+
zend_string *php_stream_escape_persistent_key(const char *host, size_t hostlen)
45+
{
46+
zend_string *escaped = zend_string_safe_alloc(hostlen, 2, 0, 0);
47+
char *ptr = ZSTR_VAL(escaped);
48+
for (size_t i = 0; i < hostlen; i++) {
49+
if (host[i] == '\0') {
50+
*ptr++ = '\\';
51+
*ptr++ = '0';
52+
} else if (host[i] == '\\') {
53+
*ptr++ = '\\';
54+
*ptr++ = '\\';
55+
} else {
56+
*ptr++ = host[i];
57+
}
58+
}
59+
*ptr = '\0';
60+
ZSTR_LEN(escaped) = ptr - ZSTR_VAL(escaped);
61+
62+
return escaped;
63+
}
64+
4465
/* Streams based network functions */
4566

4667
#ifdef HAVE_SOCKETPAIR
@@ -130,7 +151,9 @@ PHP_FUNCTION(stream_socket_client)
130151
context = php_stream_context_from_zval(zcontext, flags & PHP_FILE_NO_DEFAULT_CONTEXT);
131152

132153
if (flags & PHP_STREAM_CLIENT_PERSISTENT) {
133-
spprintf(&hashkey, 0, "stream_socket_client__%s", ZSTR_VAL(host));
154+
zend_string *escaped = php_stream_escape_persistent_key(ZSTR_VAL(host), ZSTR_LEN(host));
155+
spprintf(&hashkey, 0, "stream_socket_client__%s", ZSTR_VAL(escaped));
156+
zend_string_release_ex(escaped, false);
134157
}
135158

136159
/* prepare the timeout value for use */
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
--TEST--
2+
GH-22617: Persistent abstract unix domain sockets resolved to wrong resource
3+
--CREDITS--
4+
Roysten
5+
--SKIPIF--
6+
<?php
7+
if (PHP_OS_FAMILY !== "Linux") die("skip abstract unix domain sockets are Linux-only");
8+
if (!function_exists("pfsockopen")) die("skip pfsockopen() not available");
9+
?>
10+
--FILE--
11+
<?php
12+
// Abstract sockets: the address is prefixed with a null byte.
13+
$name1 = "gh22617_" . getmypid() . "_1";
14+
$name2 = "gh22617_" . getmypid() . "_2";
15+
16+
$server1 = stream_socket_server("unix://\0$name1", $errno, $errstr);
17+
$server2 = stream_socket_server("unix://\0$name2", $errno, $errstr);
18+
var_dump($server1 !== false, $server2 !== false);
19+
20+
// Connect to the first abstract socket.
21+
$socket1 = pfsockopen("unix://\0$name1", 0, $errno1, $errstr1);
22+
var_dump($socket1 !== false);
23+
var_dump(get_resource_type($socket1));
24+
25+
// Connect to the second abstract socket.
26+
$socket2 = pfsockopen("unix://\0$name2", 0, $errno2, $errstr2);
27+
var_dump($socket2 !== false);
28+
var_dump(get_resource_type($socket2));
29+
30+
// The two distinct abstract sockets must resolve to distinct resources.
31+
var_dump((int) $socket1 !== (int) $socket2);
32+
33+
fclose($socket1);
34+
fclose($socket2);
35+
fclose($server1);
36+
fclose($server2);
37+
?>
38+
--EXPECT--
39+
bool(true)
40+
bool(true)
41+
bool(true)
42+
string(17) "persistent stream"
43+
bool(true)
44+
string(17) "persistent stream"
45+
bool(true)

0 commit comments

Comments
 (0)