Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions NEWS
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@ PHP NEWS
IntlCalendar::equals(), ::before(), ::after(), and ::isEquivalentTo().
(Weilin Du)

- Opcache:
. Fixed bug GH-20469 (Crash during inheritance cache lookup with nested
autoloading). (Levi Morrison)

- Phar:
. Fixed a bypass of the magic ".phar" directory protection in
Phar::addEmptyDir() for paths starting with "/.phar", while allowing
Expand Down
17 changes: 13 additions & 4 deletions Zend/zend_inheritance.c
Original file line number Diff line number Diff line change
Expand Up @@ -310,14 +310,23 @@ static zend_class_entry *lookup_class(zend_class_entry *scope, zend_string *name

/* Instanceof that's safe to use on unlinked classes. */
static bool unlinked_instanceof(zend_class_entry *ce1, const zend_class_entry *ce2) {
/* Do not short-circuit to instanceof_function() here:
*
* if (ce1->ce_flags & ZEND_ACC_LINKED) {
* return instanceof_function(ce1, ce2);
* }
*
* See ext/opcache/tests/gh20469.phpt. During inheritance cache lookups,
* autoloading may re-enter class linking in a way where ce1 itself is
* linked, but a class in its parent chain is still unlinked. The normal
* instanceof_function() assumes the whole parent chain is linked, and may
* interpret an unresolved parent_name as a zend_class_entry (and crash).
*/

if (ce1 == ce2) {
return 1;
}

if (ce1->ce_flags & ZEND_ACC_LINKED) {
return instanceof_function(ce1, ce2);
}

if (ce1->parent) {
zend_class_entry *parent_ce;
if (ce1->ce_flags & ZEND_ACC_RESOLVED_PARENT) {
Expand Down
134 changes: 134 additions & 0 deletions ext/opcache/tests/gh20469.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
--TEST--
GH-20469: Inheritance cache with reentrant autoloading must not crash
--EXTENSIONS--
opcache
--CONFLICTS--
server
--FILE--
<?php
$dir = __DIR__ . '/gh20469';
@mkdir($dir . '/classes', 0777, true);

file_put_contents($dir . '/autoload.php', <<<'PHP'
<?php
spl_autoload_register(function ($class) {
$prefix = 'APP\\';
if (strncmp($class, $prefix, strlen($prefix)) === 0) {
require __DIR__ . '/classes/' . substr($class, strlen($prefix)) . '.php';
}
});
PHP);

/* The dependency cycle is:
* ChildOfParentBeingLinked -> ParentBeingLinked -> CovariantReturnWithTrait
* -> RequiresRootReturnTrait -> ChildOfParentBeingLinked.
*/
file_put_contents($dir . '/test1.php', <<<'PHP'
<?php
require __DIR__ . '/autoload.php';
echo \APP\ChildOfParentBeingLinked::SOME_CONSTANT;
PHP);

file_put_contents($dir . '/test2.php', <<<'PHP'
<?php
require __DIR__ . '/autoload.php';
echo \APP\ParentBeingLinked::SOME_CONSTANT;
PHP);

file_put_contents($dir . '/classes/RootForTraitReturn.php', <<<'PHP'
<?php
namespace APP;

class RootForTraitReturn
{
function createResult(): BaseCovariantReturn
{
}
}
PHP);

file_put_contents($dir . '/classes/ParentBeingLinked.php', <<<'PHP'
<?php
namespace APP;

class ParentBeingLinked extends RootForTraitReturn
{
public const SOME_CONSTANT = 3;

function createResult(): CovariantReturnWithTrait
{
}
}
PHP);

file_put_contents($dir . '/classes/ChildOfParentBeingLinked.php', <<<'PHP'
<?php
namespace APP;

class ChildOfParentBeingLinked extends ParentBeingLinked
{
}
PHP);

file_put_contents($dir . '/classes/BaseCovariantReturn.php', <<<'PHP'
<?php
namespace APP;

abstract class BaseCovariantReturn
{
}
PHP);

file_put_contents($dir . '/classes/RequiresRootReturnTrait.php', <<<'PHP'
<?php
namespace APP;

trait RequiresRootReturnTrait
{
abstract function build(): RootForTraitReturn;
}
PHP);

file_put_contents($dir . '/classes/CovariantReturnWithTrait.php', <<<'PHP'
<?php
namespace APP;

class CovariantReturnWithTrait extends BaseCovariantReturn
{
use RequiresRootReturnTrait;

function build(): ChildOfParentBeingLinked
{
}
}
PHP);

include 'php_cli_server.inc';
$ini = trim((string) getenv('TEST_PHP_EXTRA_ARGS'));
$ini .= ($ini !== '' ? ' ' : '') . '-d opcache.enable=1 -d opcache.enable_cli=1 -d opcache.file_update_protection=0';
php_cli_server_start($ini);

echo file_get_contents('http://' . PHP_CLI_SERVER_ADDRESS . '/gh20469/test1.php'), "\n";
echo file_get_contents('http://' . PHP_CLI_SERVER_ADDRESS . '/gh20469/test2.php'), "\n";
?>
--CLEAN--
<?php
$dir = __DIR__ . '/gh20469';
if (is_dir($dir)) {
$iterator = new RecursiveIteratorIterator(
new RecursiveDirectoryIterator($dir, RecursiveDirectoryIterator::SKIP_DOTS),
RecursiveIteratorIterator::CHILD_FIRST
);
foreach ($iterator as $file) {
if ($file->isDir()) {
rmdir($file->getPathname());
} else {
unlink($file->getPathname());
}
}
rmdir($dir);
}
?>
--EXPECT--
3
3