[php-src] PHP-8.4.24: Fix GHSA-vc5h-9ppw-p5f3: phar circular symlink crash

From: Date: Wed, 29 Jul 2026 05:23:32 +0000
Subject: [php-src] PHP-8.4.24: Fix GHSA-vc5h-9ppw-p5f3: phar circular symlink crash
Groups: php.cvs 
Request: Send a blank email to php-cvs+get-140304@lists.php.net to get a copy of this message
Author: Jakub Zelenka (bukka) Committer: Ilija Tovilo (iluuu1994) Pusher: NattyNarwhal Date: 2026-07-28T11:09:58+02:00 Commit: https://github.com/php/php-src/commit/16af1694dd4e0d0e4a24f90740651d3053edffa3 Raw diff: https://github.com/php/php-src/commit/16af1694dd4e0d0e4a24f90740651d3053edffa3.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 2d1db6a1b3e1..d3bdf3d52a78 100644 --- a/ext/phar/util.c +++ b/ext/phar/util.c @@ -60,30 +60,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; } } -/* }}} */ static php_stream *phar_get_entrypufp(const phar_entry_info *entry) {

« previous php.cvs (#140304) next »