Skip to content

Commit df77309

Browse files
committed
phpGH-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 php#22617 close phpGH-22704
1 parent 103c84f commit df77309

6 files changed

Lines changed: 82 additions & 3 deletions

File tree

NEWS

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,10 @@ PHP NEWS
9494
. Fixed bug GH-22678 (Use-after-free in array_multisort() when the comparator
9595
mutates the array being sorted). (azchin, iliaal)
9696

97+
- Streams:
98+
. Fixed bug GH-22617 (persistent stream keys truncated at null bytes, causing
99+
distinct abstract unix domain sockets to share a resource). (David Carlier)
100+
97101
- Zip:
98102
. Fixed bug GH-22649 (ZipArchive::setCommentName() and setCommentIndex()
99103
could crash after overwriting an entry and resetting its inherited

ext/standard/fsock.c

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
#include <stddef.h>
2323
#include "php_network.h"
2424
#include "file.h"
25+
#include "streams/php_streams_int.h"
2526

2627
static size_t php_fsockopen_format_host_port(char **message, const char *prefix, size_t prefix_len,
2728
const char *host, size_t host_len, zend_long port)
@@ -84,8 +85,9 @@ static void php_fsockopen_stream(INTERNAL_FUNCTION_PARAMETERS, int persistent)
8485
}
8586

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

9193
if (port > 0) {

ext/standard/streamsfuncs.c

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
#include "streamsfuncs.h"
2424
#include "php_network.h"
2525
#include "php_string.h"
26+
#include "streams/php_streams_int.h"
2627
#ifdef HAVE_UNISTD_H
2728
#include <unistd.h>
2829
#endif
@@ -130,7 +131,9 @@ PHP_FUNCTION(stream_socket_client)
130131
context = php_stream_context_from_zval(zcontext, flags & PHP_FILE_NO_DEFAULT_CONTEXT);
131132

132133
if (flags & PHP_STREAM_CLIENT_PERSISTENT) {
133-
spprintf(&hashkey, 0, "stream_socket_client__%s", ZSTR_VAL(host));
134+
zend_string *escaped = php_stream_escape_persistent_key(ZSTR_VAL(host), ZSTR_LEN(host));
135+
spprintf(&hashkey, 0, "stream_socket_client__%s", ZSTR_VAL(escaped));
136+
zend_string_release_ex(escaped, false);
134137
}
135138

136139
/* 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)

main/streams/php_streams_int.h

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,3 +62,7 @@
6262
* any other function that expects standard modes and you allow non-standard
6363
* ones. result should be a char[5]. */
6464
void php_stream_mode_sanitize_fdopen_fopencookie(php_stream *stream, char *result);
65+
66+
/* Escapes NUL and backslash bytes so a host containing them cannot truncate or
67+
* collide the persistent stream hash key. */
68+
zend_string *php_stream_escape_persistent_key(const char *host, size_t hostlen);

main/streams/streams.c

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,27 @@ PHPAPI int php_stream_from_persistent_id(const char *persistent_id, php_stream *
140140

141141
/* }}} */
142142

143+
zend_string *php_stream_escape_persistent_key(const char *host, size_t hostlen)
144+
{
145+
zend_string *escaped = zend_string_safe_alloc(hostlen, 2, 0, 0);
146+
char *ptr = ZSTR_VAL(escaped);
147+
for (size_t i = 0; i < hostlen; i++) {
148+
if (host[i] == '\0') {
149+
*ptr++ = '\\';
150+
*ptr++ = '0';
151+
} else if (host[i] == '\\') {
152+
*ptr++ = '\\';
153+
*ptr++ = '\\';
154+
} else {
155+
*ptr++ = host[i];
156+
}
157+
}
158+
*ptr = '\0';
159+
ZSTR_LEN(escaped) = ptr - ZSTR_VAL(escaped);
160+
161+
return escaped;
162+
}
163+
143164
static zend_llist *php_get_wrapper_errors_list(php_stream_wrapper *wrapper)
144165
{
145166
if (!FG(wrapper_errors)) {

0 commit comments

Comments
 (0)