[PHP-CVS] [php-src] php-8.3.33: Fix GHSA-vc5h-9ppw-p5f3: phar circular symlink crash
[email protected] (Jakub Zelenka via Ilija Tovilo) Tue, 28 Jul 2026 17:57:06 +0000
| Newsgroups | php.cvs |
|---|---|
| Message-ID | <[email protected]> |
Author: Jakub Zelenka (bukka)
Committer: Ilija Tovilo (iluuu1994)
Pusher: ericmann
Date: 2026-07-28T11:12:40+02:00
Commit: https://github.com/php/php-src/commit/9eb06f37fc3ec0146864152ae79a5b69dcd0b77f
Raw diff: https://github.com/php/php-src/commit/9eb06f37fc3ec0146864152ae79a5b69dcd0b77f.diff
Fix GHSA-vc5h-9ppw-p5f3: phar circular symlink crash
Prevents infinite recursion in phar_get_link_source.
Changed paths:
A ext/phar/tests/tar/files/circular_symlinks.tar
A ext/phar/tests/tar/files/circular_symlinks_long.tar
A ext/phar/tests/tar/files/circular_symlinks_rho.tar
A ext/phar/tests/tar/ghsa-vc5h-9ppw-p5f3-symlink-circular.phpt
M ext/phar/util.c
Diff:
diff --git a/ext/phar/tests/tar/files/circular_symlinks.tar b/ext/phar/tests/tar/files/circular_symlinks.tar
new file mode 100644
index 000000000000..2af7bb8c2384
Binary files /dev/null and b/ext/phar/tests/tar/files/circular_symlinks.tar differ
diff --git a/ext/phar/tests/tar/files/circular_symlinks_long.tar b/ext/phar/tests/tar/files/circular_symlinks_long.tar
new file mode 100644
index 000000000000..b2b4c1cb095c
Binary files /dev/null and b/ext/phar/tests/tar/files/circular_symlinks_long.tar differ
diff --git a/ext/phar/tests/tar/files/circular_symlinks_rho.tar b/ext/phar/tests/tar/files/circular_symlinks_rho.tar
new file mode 100644
index 000000000000..2ccbf17c8c63
Binary files /dev/null and b/ext/phar/tests/tar/files/circular_symlinks_rho.tar differ
diff --git a/ext/phar/tests/tar/ghsa-vc5h-9ppw-p5f3-symlink-circular.phpt b/ext/phar/tests/tar/ghsa-vc5h-9ppw-p5f3-symlink-circular.phpt
new file mode 100644
index 000000000000..cf0048c0fcd9
--- /dev/null
+++ b/ext/phar/tests/tar/ghsa-vc5h-9ppw-p5f3-symlink-circular.phpt
@@ -0,0 +1,27 @@
+--TEST--
+GHSA-vc5h-9ppw-p5f3 (circular symlinks in tar should not cause stack overflow)
+--CREDITS--
+Calvin Young - eWalker Consulting (HK) Limited
+Enoch Chow - Isomorph Cyber
+--EXTENSIONS--
+phar
+--FILE--
+<?php
+$base = dirname(__FILE__);
+
+// simple 2-cycle
+$phar = new PharData($base . '/files/circular_symlinks.tar');
+var_dump($phar['file_a']->getContent() === '');
+
+// rho-shaped cycle (tail leading into a loop)
+$phar = new PharData($base . '/files/circular_symlinks_rho.tar');
+var_dump($phar['file_a']->getContent() === '');
+
+// long cycle (400 entries)
+$phar = new PharData($base . '/files/circular_symlinks_long.tar');
+var_dump($phar['link_0']->getContent() === '');
+?>
+--EXPECT--
+bool(true)
+bool(true)
+bool(true)
diff --git a/ext/phar/util.c b/ext/phar/util.c
index 4b9ff1cb58ba..4df7060c1d3f 100644
--- a/ext/phar/util.c
+++ b/ext/phar/util.c
@@ -57,30 +57,59 @@ static char *phar_get_link_location(phar_entry_info *entry) /* {{{ */
}
/* }}} */
-phar_entry_info *phar_get_link_source(phar_entry_info *entry) /* {{{ */
+static phar_entry_info *phar_follow_one_link(phar_entry_info *entry)
{
phar_entry_info *link_entry;
char *link;
- if (!entry->link) {
- return entry;
- }
-
link = phar_get_link_location(entry);
if (NULL != (link_entry = zend_hash_str_find_ptr(&(entry->phar->manifest), entry->link, strlen(entry->link))) ||
NULL != (link_entry = zend_hash_str_find_ptr(&(entry->phar->manifest), link, strlen(link)))) {
if (link != entry->link) {
efree(link);
}
- return phar_get_link_source(link_entry);
- } else {
- if (link != entry->link) {
- efree(link);
+ return link_entry;
+ }
+
+ if (link != entry->link) {
+ efree(link);
+ }
+ return NULL;
+}
+
+phar_entry_info *phar_get_link_source(phar_entry_info *entry)
+{
+ phar_entry_info *slow, *fast;
+
+ if (!entry->link) {
+ return entry;
+ }
+
+ /*
+ * Use Floyd's cycle detection algorithm to follow the symlink chain without unbounded
+ * recursion. Each entry has at most one outgoing link, so if a cycle exists the fast pointer
+ * will eventually meet the slow one. Otherwise the fast pointer reaches the end first.
+ */
+ slow = fast = entry;
+ while (1) {
+ fast = phar_follow_one_link(fast);
+ if (!fast || !fast->link) {
+ return fast;
+ }
+ fast = phar_follow_one_link(fast);
+ if (!fast || !fast->link) {
+ return fast;
+ }
+
+ /* no need to check slow as it's always behind */
+ slow = phar_follow_one_link(slow);
+
+ if (slow == fast) {
+ /* circular symlink chain */
+ return NULL;
}
- return NULL;
}
}
-/* }}} */
/* retrieve a phar_entry_info's current file pointer for reading contents */
php_stream *phar_get_efp(phar_entry_info *entry, int follow_links) /* {{{ */