Fixed bug #78935: Check that all linked classes can be preloaded

During preloading, check that all classes that have been included
as part of the preload script itself (rather than through opcache_compile_file)
can actually be preloaded, i.e. satisfy Windows restrictions, have
resolved initializers and resolved property types. When resolving
initializers and property types, also autoload additional classes.
Because of this, the resolution runs in a loop.
This commit is contained in:
Nikita Popov 2019-12-09 15:14:39 +01:00
parent 4313659bb9
commit 3f86adb0ef
16 changed files with 233 additions and 6 deletions

2
NEWS
View File

@ -36,6 +36,8 @@ PHP NEWS
- OPcache:
. Fixed $x = (bool)$x; with opcache (should emit undeclared variable notice).
(Tyson Andre)
. Fixed bug #78935 (Preloading removes classes that have dependencies).
(Nikita, Dmitry)
- PCRE:
. Fixed bug #78853 (preg_match() may return integer > 1). (cmb)

View File

@ -6497,13 +6497,13 @@ zend_op *zend_compile_class_decl(zend_ast *ast, zend_bool toplevel) /* {{{ */
if (toplevel
/* We currently don't early-bind classes that implement interfaces or use traits */
&& !(ce->ce_flags & (ZEND_ACC_IMPLEMENT_INTERFACES|ZEND_ACC_IMPLEMENT_TRAITS))) {
&& !(ce->ce_flags & (ZEND_ACC_IMPLEMENT_INTERFACES|ZEND_ACC_IMPLEMENT_TRAITS))
&& !(CG(compiler_options) & ZEND_COMPILE_PRELOAD)) {
if (extends_ast) {
zend_class_entry *parent_ce = zend_lookup_class_ex(
ce->parent_name, NULL, ZEND_FETCH_CLASS_NO_AUTOLOAD);
if (parent_ce
&& !(CG(compiler_options) & ZEND_COMPILE_PRELOAD) /* delay inheritance till preloading */
&& ((parent_ce->type != ZEND_INTERNAL_CLASS) || !(CG(compiler_options) & ZEND_COMPILE_IGNORE_INTERNAL_CLASSES))
&& ((parent_ce->type != ZEND_USER_CLASS) || !(CG(compiler_options) & ZEND_COMPILE_IGNORE_OTHER_FILES) || (parent_ce->info.user.filename == ce->info.user.filename))) {

View File

@ -3850,6 +3850,108 @@ static void preload_link(void)
} ZEND_HASH_FOREACH_END();
}
#ifdef ZEND_WIN32
static void preload_check_windows_restriction(zend_class_entry *scope, zend_class_entry *ce) {
if (ce && ce->type == ZEND_INTERNAL_CLASS) {
zend_error_noreturn(E_ERROR,
"Class %s uses internal class %s during preloading, which is not supported on Windows",
ZSTR_VAL(scope->name), ZSTR_VAL(ce->name));
}
}
static void preload_check_windows_restrictions(zend_class_entry *scope) {
uint32_t i;
preload_check_windows_restriction(scope, scope->parent);
for (i = 0; i < scope->num_interfaces; i++) {
preload_check_windows_restriction(scope, scope->interfaces[i]);
}
}
#endif
static zend_class_entry *preload_load_prop_type(zend_property_info *prop, zend_string *name) {
zend_class_entry *ce;
if (zend_string_equals_literal_ci(name, "self")) {
ce = prop->ce;
} else if (zend_string_equals_literal_ci(name, "parent")) {
ce = prop->ce->parent;
} else {
ce = zend_lookup_class(name);
}
if (ce) {
return ce;
}
zend_error_noreturn(E_ERROR,
"Failed to load class %s used by typed property %s::$%s during preloading",
ZSTR_VAL(name), ZSTR_VAL(prop->ce->name), zend_get_unmangled_property_name(prop->name));
return ce;
}
static void preload_ensure_classes_loadable() {
/* Run this in a loop, because additional classes may be loaded while updating constants etc. */
uint32_t checked_classes_idx = 0;
while (1) {
zend_class_entry *ce;
uint32_t num_classes = zend_hash_num_elements(EG(class_table));
if (num_classes == checked_classes_idx) {
return;
}
ZEND_HASH_REVERSE_FOREACH_PTR(EG(class_table), ce) {
if (ce->type == ZEND_INTERNAL_CLASS || _idx == checked_classes_idx) {
break;
}
if (!(ce->ce_flags & ZEND_ACC_LINKED)) {
/* Only require that already linked classes are loadable, we'll properly check
* things when linking additional classes. */
continue;
}
#ifdef ZEND_WIN32
preload_check_windows_restrictions(ce);
#endif
if (!(ce->ce_flags & ZEND_ACC_CONSTANTS_UPDATED)) {
int result = SUCCESS;
zend_try {
result = zend_update_class_constants(ce);
} zend_catch {
/* Provide some context for the generated error. */
zend_error_noreturn(E_ERROR,
"Error generated while resolving initializers of class %s during preloading",
ZSTR_VAL(ce->name));
} zend_end_try();
if (result == FAILURE) {
/* Just present to be safe: We generally always throw some
* other fatal error as part of update_class_constants(). */
zend_error_noreturn(E_ERROR,
"Failed to resolve initializers of class %s during preloading",
ZSTR_VAL(ce->name));
}
ZEND_ASSERT(ce->ce_flags & ZEND_ACC_CONSTANTS_UPDATED);
}
if (!(ce->ce_flags & ZEND_ACC_PROPERTY_TYPES_RESOLVED)) {
if (ce->ce_flags & ZEND_ACC_HAS_TYPE_HINTS) {
zend_property_info *prop;
ZEND_HASH_FOREACH_PTR(&ce->properties_info, prop) {
if (ZEND_TYPE_IS_NAME(prop->type)) {
zend_class_entry *ce =
preload_load_prop_type(prop, ZEND_TYPE_NAME(prop->type));
prop->type = ZEND_TYPE_ENCODE_CE(ce, ZEND_TYPE_ALLOW_NULL(prop->type));
}
} ZEND_HASH_FOREACH_END();
}
ce->ce_flags |= ZEND_ACC_PROPERTY_TYPES_RESOLVED;
}
} ZEND_HASH_FOREACH_END();
checked_classes_idx = num_classes;
}
}
static zend_string *preload_resolve_path(zend_string *filename)
{
if (is_stream_path(ZSTR_VAL(filename))) {
@ -4205,6 +4307,10 @@ static int accel_preload(const char *config)
CG(unclean_shutdown) = 1;
ret = FAILURE;
}
if (ret == SUCCESS) {
preload_ensure_classes_loadable();
}
} zend_catch {
ret = FAILURE;
} zend_end_try();

View File

@ -12,5 +12,6 @@ opcache.preload={PWD}/preload_undef_const.inc
var_dump(class_exists('Foo'));
?>
--EXPECTF--
Warning: Can't preload class Foo with unresolved initializer for constant A in %spreload_undef_const.inc on line 2
bool(false)
Fatal error: Undefined class constant 'self::DOES_NOT_EXIST' in Unknown on line 0
Fatal error: Error generated while resolving initializers of class Foo during preloading in Unknown on line 0

View File

@ -13,6 +13,6 @@ var_dump(trait_exists('T'));
var_dump(class_exists('Foo'));
?>
--EXPECTF--
Warning: Can't preload class Foo with unresolved initializer for constant C in %spreload_undef_const_2.inc on line 8
Warning: Use of undefined constant UNDEF - assumed 'UNDEF' (this will throw an Error in a future version of PHP) in Unknown on line 0
bool(true)
bool(true)
bool(false)

View File

@ -0,0 +1,18 @@
<?php
spl_autoload_register(function($class) {
if ($class == 'Bar') {
class Bar {
const BAZ = 42;
public self $x;
public Foo $y;
}
} else if ($class == 'Foo') {
class Foo {}
}
});
class Test {
const FOO = Bar::BAZ;
}

View File

@ -0,0 +1,19 @@
--TEST--
Preloading: Loadable class checking (1)
--INI--
opcache.enable=1
opcache.enable_cli=1
opcache.optimization_level=-1
opcache.preload={PWD}/preload_loadable_classes_1.inc
--SKIPIF--
<?php require_once('skipif.inc'); ?>
--FILE--
<?php
var_dump(class_exists('Test'));
var_dump(class_exists('Bar'));
var_dump(class_exists('Foo'));
?>
--EXPECT--
bool(true)
bool(true)
bool(true)

View File

@ -0,0 +1,6 @@
<?php
class Test {
const X = UNDEF;
const Y = Foo::UNDEF;
}

View File

@ -0,0 +1,17 @@
--TEST--
Preloading: Loadable class checking (2)
--INI--
opcache.enable=1
opcache.enable_cli=1
opcache.optimization_level=-1
opcache.preload={PWD}/preload_loadable_classes_2.inc
--SKIPIF--
<?php require_once('skipif.inc'); ?>
--FILE--
Unreachable
--EXPECTF--
Warning: Use of undefined constant UNDEF - assumed 'UNDEF' (this will throw an Error in a future version of PHP) in Unknown on line 0
Fatal error: Class 'Foo' not found in Unknown on line 0
Fatal error: Error generated while resolving initializers of class Test during preloading in Unknown on line 0

View File

@ -0,0 +1,5 @@
<?php
class Test {
protected Foo $prop;
}

View File

@ -0,0 +1,13 @@
--TEST--
Preloading: Loadable class checking (3)
--INI--
opcache.enable=1
opcache.enable_cli=1
opcache.optimization_level=-1
opcache.preload={PWD}/preload_loadable_classes_3.inc
--SKIPIF--
<?php require_once('skipif.inc'); ?>
--FILE--
Unreachable
--EXPECTF--
Fatal error: Failed to load class Foo used by typed property Test::$prop during preloading in Unknown on line 0

View File

@ -0,0 +1,3 @@
<?php
class Test extends Exception {}

View File

@ -0,0 +1,16 @@
--TEST--
Preloading: Loadable class checking (4)
--INI--
opcache.enable=1
opcache.enable_cli=1
opcache.optimization_level=-1
opcache.preload={PWD}/preload_loadable_classes_4.inc
--SKIPIF--
<?php
require_once('skipif.inc');
if (PHP_OS_FAMILY != 'Windows') die('skip Windows only');
?>
--FILE--
Unreachable
--EXPECTF--
Fatal error: Class Test uses internal class Exception during preloading, which is not supported on Windows in Unknown on line 0

View File

@ -0,0 +1,2 @@
<?php
opcache_compile_file(__DIR__ . "/preload_unresolved_prop_type_2.inc");

View File

@ -0,0 +1,14 @@
--TEST--
Preload: Unresolved property type
--INI--
opcache.enable=1
opcache.enable_cli=1
opcache.optimization_level=-1
opcache.preload={PWD}/preload_unresolved_prop_type.inc
--SKIPIF--
<?php require_once('skipif.inc'); ?>
--FILE--
===DONE===
--EXPECTF--
Warning: Can't preload class Test with unresolved property types in %s on line %d
===DONE===

View File

@ -0,0 +1,5 @@
<?php
class Test {
public Unknown $prop;
}