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();