Skip to content

Commit fce41e6

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.
1 parent 834bdce commit fce41e6

5 files changed

Lines changed: 78 additions & 3 deletions

File tree

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: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -130,7 +130,9 @@ PHP_FUNCTION(stream_socket_client)
130130
context = php_stream_context_from_zval(zcontext, flags & PHP_FILE_NO_DEFAULT_CONTEXT);
131131

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

136138
/* 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/php_streams.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -298,6 +298,8 @@ PHPAPI int php_stream_from_persistent_id(const char *persistent_id, php_stream *
298298
#define PHP_STREAM_PERSISTENT_FAILURE 1 /* id exists but is not a stream! */
299299
#define PHP_STREAM_PERSISTENT_NOT_EXIST 2 /* id does not exist */
300300

301+
zend_string *php_stream_escape_persistent_key(const char *host, size_t hostlen);
302+
301303
#define PHP_STREAM_FREE_CALL_DTOR 1 /* call ops->close */
302304
#define PHP_STREAM_FREE_RELEASE_STREAM 2 /* pefree(stream) */
303305
#define PHP_STREAM_FREE_PRESERVE_HANDLE 4 /* tell ops->close to not close its underlying handle */

main/streams/streams.c

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,31 @@ 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+
size_t escaped_len = 0, i;
146+
for (i = 0; i < hostlen; i++) {
147+
escaped_len += (host[i] == '\0' || host[i] == '\\') ? 2 : 1;
148+
}
149+
150+
zend_string *escaped = zend_string_alloc(escaped_len, 0);
151+
char *ptr = ZSTR_VAL(escaped);
152+
for (i = 0; i < hostlen; i++) {
153+
if (host[i] == '\0') {
154+
*ptr++ = '\\';
155+
*ptr++ = '0';
156+
} else if (host[i] == '\\') {
157+
*ptr++ = '\\';
158+
*ptr++ = '\\';
159+
} else {
160+
*ptr++ = host[i];
161+
}
162+
}
163+
*ptr = '\0';
164+
165+
return escaped;
166+
}
167+
143168
static zend_llist *php_get_wrapper_errors_list(php_stream_wrapper *wrapper)
144169
{
145170
if (!FG(wrapper_errors)) {

0 commit comments

Comments
 (0)