Commit 12b1f654db6 for php.net
commit 12b1f654db632e1420142dd87f190a93a44fb88e
Author: Weilin Du <weilindu@php.net>
Date: Thu Aug 27 00:09:11 2026 +0800
Fix GH-23418: UAF when accessing mounted Phar subdirectories (#23442)
Here we passes a properly null-terminated copy of the shortened path
to `phar_mount_entry()` instead and keep `test` alive until error
formatting and manifest lookup have completed.
diff --git a/NEWS b/NEWS
index e4c3889e292..935b3123489 100644
--- a/NEWS
+++ b/NEWS
@@ -52,6 +52,10 @@ PHP NEWS
. Fixed a leak when a persistent connection failed a liveness check
with no other live PDO handle. (iliaal)
+- Phar:
+ . Fixed bug GH-23418 (Use-after-free when looking up mounted directories).
+ (Weilin Du)
+
- Standard:
. Fixed a memory leak in array_merge_recursive() when the recursive merge of
an object converted to an array fails. (David Carlier)
diff --git a/ext/phar/tests/gh23418.phpt b/ext/phar/tests/gh23418.phpt
new file mode 100644
index 00000000000..d7ebebcb9ec
--- /dev/null
+++ b/ext/phar/tests/gh23418.phpt
@@ -0,0 +1,32 @@
+--TEST--
+GH-23418: Access a subdirectory of a mounted directory with a trailing slash
+--EXTENSIONS--
+phar
+--INI--
+phar.readonly=0
+--FILE--
+<?php
+$phar = __DIR__ . '/gh23418.phar';
+$mount = __DIR__ . '/gh23418';
+
+@mkdir($mount . '/s2', 0777, true);
+
+$p = new Phar($phar);
+$p->addFromString('x.txt', 'x');
+$p->setStub('<?php __HALT_COMPILER(); ?>');
+unset($p);
+
+$p = new Phar($phar);
+Phar::mount('phar://' . $phar . '/m', $mount);
+$info = $p['m/s2/'];
+
+echo get_class($info), ', isDir=', $info->isDir() ? 'true' : 'false', PHP_EOL;
+?>
+--CLEAN--
+<?php
+@unlink(__DIR__ . '/gh23418.phar');
+@rmdir(__DIR__ . '/gh23418/s2');
+@rmdir(__DIR__ . '/gh23418');
+?>
+--EXPECT--
+PharFileInfo, isDir=true
diff --git a/ext/phar/util.c b/ext/phar/util.c
index d3bdf3d52a7..f4de9922f89 100644
--- a/ext/phar/util.c
+++ b/ext/phar/util.c
@@ -1382,7 +1382,7 @@ phar_entry_info *phar_get_entry_info_dir(phar_archive_data *phar, char *path, si
if (ZSTR_LEN(str_key) >= path_len || strncmp(ZSTR_VAL(str_key), path, ZSTR_LEN(str_key))) {
continue;
} else {
- char *test;
+ char *test, *mount_path;
size_t test_len;
php_stream_statbuf ssb;
@@ -1425,22 +1425,25 @@ phar_entry_info *phar_get_entry_info_dir(phar_archive_data *phar, char *path, si
}
/* mount the file just in time */
- if (SUCCESS != phar_mount_entry(phar, test, test_len, path, path_len)) {
- efree(test);
+ mount_path = estrndup(path, path_len);
+ if (SUCCESS != phar_mount_entry(phar, test, test_len, mount_path, path_len)) {
if (error) {
spprintf(error, 4096, "phar error: path \"%s\" exists as file \"%s\" and could not be mounted", path, test);
}
+ efree(mount_path);
+ efree(test);
return NULL;
}
-
- efree(test);
+ efree(mount_path);
if (NULL == (entry = zend_hash_str_find_ptr(&phar->manifest, path, path_len))) {
if (error) {
spprintf(error, 4096, "phar error: path \"%s\" exists as file \"%s\" and could not be retrieved after being mounted", path, test);
}
+ efree(test);
return NULL;
}
+ efree(test);
return entry;
}
} ZEND_HASH_FOREACH_END();