From 09e770d666020fd5ecf4cf3a3680a6da03262457 Mon Sep 17 00:00:00 2001 From: Ilia Alshanetsky Date: Sun, 26 Apr 2026 22:00:34 -0400 Subject: [PATCH] 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 --- ext/phar/tests/gh14481.phpt | 61 +++++++++++++++++++++++++++++++++++++ ext/spl/spl_directory.c | 11 +++++++ ext/standard/file.c | 10 ++++++ 3 files changed, 82 insertions(+) create mode 100644 ext/phar/tests/gh14481.phpt diff --git a/ext/phar/tests/gh14481.phpt b/ext/phar/tests/gh14481.phpt new file mode 100644 index 000000000000..c87d6538bddc --- /dev/null +++ b/ext/phar/tests/gh14481.phpt @@ -0,0 +1,61 @@ +--TEST-- +GH-14481 (realpath() and SplFileInfo::getRealPath inside Phar) +--EXTENSIONS-- +phar +--INI-- +phar.readonly=0 +--FILE-- +addFromString('inner.php', "setStub("getRealPath()); + +echo "SplFileInfo missing entry:\n"; +var_dump((new SplFileInfo($missing))->getRealPath()); + +echo "PharFileInfo existing entry:\n"; +var_dump((new PharFileInfo($existing))->getRealPath()); + +echo "RecursiveIteratorIterator entry:\n"; +foreach (new RecursiveIteratorIterator(new Phar($file)) as $info) { + var_dump($info->getRealPath()); +} + +echo "plain-filesystem branch unaffected:\n"; +var_dump(realpath(__FILE__) === __FILE__); +?> +--CLEAN-- + +--EXPECTF-- +realpath existing entry: +string(%d) "phar://%sgh14481.phar/inner.php" +realpath missing entry: +bool(false) +SplFileInfo existing entry: +string(%d) "phar://%sgh14481.phar/inner.php" +SplFileInfo missing entry: +bool(false) +PharFileInfo existing entry: +string(%d) "phar://%sgh14481.phar/inner.php" +RecursiveIteratorIterator entry: +string(%d) "phar://%sgh14481.phar/inner.php" +plain-filesystem branch unaffected: +bool(true) diff --git a/ext/spl/spl_directory.c b/ext/spl/spl_directory.c index 89af25dd9d35..c9477ab47c20 100644 --- a/ext/spl/spl_directory.c +++ b/ext/spl/spl_directory.c @@ -1244,6 +1244,17 @@ PHP_METHOD(SplFileInfo, getRealPath) filename = intern->file_name ? ZSTR_VAL(intern->file_name) : NULL; } + if (filename) { + const char *path_to_open = filename; + php_stream_wrapper *wrapper = php_stream_locate_url_wrapper(filename, &path_to_open, 0); + if (wrapper && wrapper != &php_plain_files_wrapper && !wrapper->is_url && wrapper->wops && wrapper->wops->url_stat) { + php_stream_statbuf ssb; + if (wrapper->wops->url_stat(wrapper, path_to_open, PHP_STREAM_URL_STAT_QUIET, &ssb, NULL) == 0) { + RETURN_STRING(filename); + } + RETURN_FALSE; + } + } if (filename && VCWD_REALPATH(filename, buff)) { #ifdef ZTS diff --git a/ext/standard/file.c b/ext/standard/file.c index 1841c242b870..2fe8a8c39a84 100644 --- a/ext/standard/file.c +++ b/ext/standard/file.c @@ -2102,6 +2102,16 @@ PHP_FUNCTION(realpath) Z_PARAM_PATH(filename, filename_len) ZEND_PARSE_PARAMETERS_END(); + const char *path_to_open = filename; + php_stream_wrapper *wrapper = php_stream_locate_url_wrapper(filename, &path_to_open, 0); + if (wrapper && wrapper != &php_plain_files_wrapper && !wrapper->is_url && wrapper->wops && wrapper->wops->url_stat) { + php_stream_statbuf ssb; + if (wrapper->wops->url_stat(wrapper, path_to_open, PHP_STREAM_URL_STAT_QUIET, &ssb, NULL) == 0) { + RETURN_STRINGL(filename, filename_len); + } + RETURN_FALSE; + } + if (VCWD_REALPATH(filename, resolved_path_buff)) { if (php_check_open_basedir(resolved_path_buff)) { RETURN_FALSE;