Skip to content

Commit 09e770d

Browse files
committed
Fix GH-14481: realpath() and SplFileInfo::getRealPath inside Phar
realpath() and SplFileInfo::getRealPath() route directly through VCWD_REALPATH, which has no notion of stream wrappers. For a phar:// URL the call chain treats the URI as relative, prepends CWD, and stat()s a nonsense path, so existing entries inside a Phar return false even though file_exists()/is_file()/include all see them. Before VCWD_REALPATH, look up the stream wrapper for the path. If a non-plain, non-URL wrapper supplies url_stat and reports SUCCESS, return the input string; on FAILURE, return false. Plain paths (and file:// URLs that resolve back to the plain-files wrapper) keep the existing VCWD_REALPATH semantics: realpath cache, ZTS access guard, and open_basedir check. For phar URLs, open_basedir is enforced by the wrapper's own url_stat, matching how is_file() and file_exists() behave today. The !wrapper->is_url guard keeps URL-style wrappers (http://, ftp://, data://, user wrappers registered with STREAM_IS_URL) on the existing false return so realpath() does not gain network or third-party url_stat side effects. In-tree, only phar matches the new branch. User wrappers registered without STREAM_IS_URL that implement url_stat will now have url_stat called from realpath(). PharFileInfo, SplFileObject, DirectoryIterator, RecursiveDirectoryIterator, and FilesystemIterator inherit SplFileInfo::getRealPath and pick up the fix. Closes GH-14481
1 parent bd78496 commit 09e770d

3 files changed

Lines changed: 82 additions & 0 deletions

File tree

ext/phar/tests/gh14481.phpt

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
--TEST--
2+
GH-14481 (realpath() and SplFileInfo::getRealPath inside Phar)
3+
--EXTENSIONS--
4+
phar
5+
--INI--
6+
phar.readonly=0
7+
--FILE--
8+
<?php
9+
$file = __DIR__ . DIRECTORY_SEPARATOR . 'gh14481.phar';
10+
@unlink($file);
11+
12+
$phar = new Phar($file);
13+
$phar->addFromString('inner.php', "<?php echo 'hi';");
14+
$phar->setStub("<?php __HALT_COMPILER();");
15+
unset($phar);
16+
17+
$existing = "phar://" . $file . "/inner.php";
18+
$missing = "phar://" . $file . "/nope.php";
19+
20+
echo "realpath existing entry:\n";
21+
var_dump(realpath($existing));
22+
23+
echo "realpath missing entry:\n";
24+
var_dump(realpath($missing));
25+
26+
echo "SplFileInfo existing entry:\n";
27+
var_dump((new SplFileInfo($existing))->getRealPath());
28+
29+
echo "SplFileInfo missing entry:\n";
30+
var_dump((new SplFileInfo($missing))->getRealPath());
31+
32+
echo "PharFileInfo existing entry:\n";
33+
var_dump((new PharFileInfo($existing))->getRealPath());
34+
35+
echo "RecursiveIteratorIterator entry:\n";
36+
foreach (new RecursiveIteratorIterator(new Phar($file)) as $info) {
37+
var_dump($info->getRealPath());
38+
}
39+
40+
echo "plain-filesystem branch unaffected:\n";
41+
var_dump(realpath(__FILE__) === __FILE__);
42+
?>
43+
--CLEAN--
44+
<?php
45+
@unlink(__DIR__ . DIRECTORY_SEPARATOR . 'gh14481.phar');
46+
?>
47+
--EXPECTF--
48+
realpath existing entry:
49+
string(%d) "phar://%sgh14481.phar/inner.php"
50+
realpath missing entry:
51+
bool(false)
52+
SplFileInfo existing entry:
53+
string(%d) "phar://%sgh14481.phar/inner.php"
54+
SplFileInfo missing entry:
55+
bool(false)
56+
PharFileInfo existing entry:
57+
string(%d) "phar://%sgh14481.phar/inner.php"
58+
RecursiveIteratorIterator entry:
59+
string(%d) "phar://%sgh14481.phar/inner.php"
60+
plain-filesystem branch unaffected:
61+
bool(true)

ext/spl/spl_directory.c

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1244,6 +1244,17 @@ PHP_METHOD(SplFileInfo, getRealPath)
12441244
filename = intern->file_name ? ZSTR_VAL(intern->file_name) : NULL;
12451245
}
12461246

1247+
if (filename) {
1248+
const char *path_to_open = filename;
1249+
php_stream_wrapper *wrapper = php_stream_locate_url_wrapper(filename, &path_to_open, 0);
1250+
if (wrapper && wrapper != &php_plain_files_wrapper && !wrapper->is_url && wrapper->wops && wrapper->wops->url_stat) {
1251+
php_stream_statbuf ssb;
1252+
if (wrapper->wops->url_stat(wrapper, path_to_open, PHP_STREAM_URL_STAT_QUIET, &ssb, NULL) == 0) {
1253+
RETURN_STRING(filename);
1254+
}
1255+
RETURN_FALSE;
1256+
}
1257+
}
12471258

12481259
if (filename && VCWD_REALPATH(filename, buff)) {
12491260
#ifdef ZTS

ext/standard/file.c

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2102,6 +2102,16 @@ PHP_FUNCTION(realpath)
21022102
Z_PARAM_PATH(filename, filename_len)
21032103
ZEND_PARSE_PARAMETERS_END();
21042104

2105+
const char *path_to_open = filename;
2106+
php_stream_wrapper *wrapper = php_stream_locate_url_wrapper(filename, &path_to_open, 0);
2107+
if (wrapper && wrapper != &php_plain_files_wrapper && !wrapper->is_url && wrapper->wops && wrapper->wops->url_stat) {
2108+
php_stream_statbuf ssb;
2109+
if (wrapper->wops->url_stat(wrapper, path_to_open, PHP_STREAM_URL_STAT_QUIET, &ssb, NULL) == 0) {
2110+
RETURN_STRINGL(filename, filename_len);
2111+
}
2112+
RETURN_FALSE;
2113+
}
2114+
21052115
if (VCWD_REALPATH(filename, resolved_path_buff)) {
21062116
if (php_check_open_basedir(resolved_path_buff)) {
21072117
RETURN_FALSE;

0 commit comments

Comments
 (0)