Perform preloading attempt on copied class

It is very hard to determine in advance whether class linking will
fail due to missing dependencies in variance checks (#7314 attempts
this). This patch takes an alternative approach where we try to
perform inheritance on a copy of the class (zend_lazy_class_load)
and then restore the original class if inheritance fails. The fatal
error in that case is recorded and thrown as a warning later.

Closes GH-7319.
This commit is contained in:
Nikita Popov 2021-07-29 16:50:00 +02:00
parent 6b1337b736
commit d836046ab8
5 changed files with 116 additions and 168 deletions

View file

@ -248,7 +248,7 @@ static zend_class_entry *lookup_class_ex(
ce = zend_lookup_class_ex( ce = zend_lookup_class_ex(
name, NULL, ZEND_FETCH_CLASS_ALLOW_UNLINKED | ZEND_FETCH_CLASS_NO_AUTOLOAD); name, NULL, ZEND_FETCH_CLASS_ALLOW_UNLINKED | ZEND_FETCH_CLASS_NO_AUTOLOAD);
if (!CG(in_compilation)) { if (!CG(in_compilation) || (CG(compiler_options) & ZEND_COMPILE_PRELOAD)) {
if (ce) { if (ce) {
return ce; return ce;
} }
@ -2593,7 +2593,6 @@ static zend_class_entry *zend_lazy_class_load(zend_class_entry *pce)
ce->ce_flags &= ~ZEND_ACC_IMMUTABLE; ce->ce_flags &= ~ZEND_ACC_IMMUTABLE;
ce->refcount = 1; ce->refcount = 1;
ce->inheritance_cache = NULL; ce->inheritance_cache = NULL;
ZEND_MAP_PTR_INIT(ce->mutable_data, NULL);
/* properties */ /* properties */
if (ce->default_properties_table) { if (ce->default_properties_table) {
@ -2817,6 +2816,7 @@ ZEND_API zend_class_entry *zend_do_link_class(zend_class_entry *ce, zend_string
} }
#endif #endif
bool orig_record_errors = EG(record_errors);
if (ce->ce_flags & ZEND_ACC_IMMUTABLE) { if (ce->ce_flags & ZEND_ACC_IMMUTABLE) {
if (is_cacheable) { if (is_cacheable) {
if (zend_inheritance_cache_get && zend_inheritance_cache_add) { if (zend_inheritance_cache_get && zend_inheritance_cache_add) {
@ -2902,7 +2902,7 @@ ZEND_API zend_class_entry *zend_do_link_class(zend_class_entry *ce, zend_string
} }
zend_build_properties_info_table(ce); zend_build_properties_info_table(ce);
EG(record_errors) = false; EG(record_errors) = orig_record_errors;
if (!(ce->ce_flags & ZEND_ACC_UNRESOLVED_VARIANCE)) { if (!(ce->ce_flags & ZEND_ACC_UNRESOLVED_VARIANCE)) {
ce->ce_flags |= ZEND_ACC_LINKED; ce->ce_flags |= ZEND_ACC_LINKED;
@ -2948,7 +2948,9 @@ ZEND_API zend_class_entry *zend_do_link_class(zend_class_entry *ce, zend_string
} }
} }
zend_free_recorded_errors(); if (!orig_record_errors) {
zend_free_recorded_errors();
}
if (traits_and_interfaces) { if (traits_and_interfaces) {
free_alloca(traits_and_interfaces, use_heap); free_alloca(traits_and_interfaces, use_heap);
} }

View file

@ -3665,96 +3665,52 @@ try_again:
} }
typedef struct { typedef struct {
zend_class_entry *parent; const char *kind;
zend_class_entry **interfaces; const char *name;
zend_class_entry **traits; } preload_error;
const char *error_kind;
const char *error_name;
void *checkpoint;
} preload_deps;
static bool preload_needed_types_known(const preload_deps *deps, zend_class_entry *ce); static zend_result preload_resolve_deps(preload_error *error, const zend_class_entry *ce)
static zend_result preload_resolve_deps(preload_deps *deps, zend_class_entry *ce)
{ {
memset(deps, 0, sizeof(preload_deps)); memset(error, 0, sizeof(preload_error));
deps->checkpoint = zend_arena_checkpoint(CG(arena));
if (ce->parent_name) { if (ce->parent_name) {
zend_string *key = zend_string_tolower(ce->parent_name); zend_string *key = zend_string_tolower(ce->parent_name);
deps->parent = zend_hash_find_ptr(EG(class_table), key); zend_class_entry *parent = zend_hash_find_ptr(EG(class_table), key);
zend_string_release(key); zend_string_release(key);
if (!deps->parent) { if (!parent) {
deps->error_kind = "Unknown parent "; error->kind = "Unknown parent ";
deps->error_name = ZSTR_VAL(ce->parent_name); error->name = ZSTR_VAL(ce->parent_name);
return FAILURE; return FAILURE;
} }
} }
if (ce->num_interfaces) { if (ce->num_interfaces) {
deps->interfaces =
zend_arena_alloc(&CG(arena), ce->num_interfaces * sizeof(zend_class_entry));
for (uint32_t i = 0; i < ce->num_interfaces; i++) { for (uint32_t i = 0; i < ce->num_interfaces; i++) {
deps->interfaces[i] = zend_class_entry *interface =
zend_hash_find_ptr(EG(class_table), ce->interface_names[i].lc_name); zend_hash_find_ptr(EG(class_table), ce->interface_names[i].lc_name);
if (!deps->interfaces[i]) { if (!interface) {
deps->error_kind = "Unknown interface "; error->kind = "Unknown interface ";
deps->error_name = ZSTR_VAL(ce->interface_names[i].name); error->name = ZSTR_VAL(ce->interface_names[i].name);
return FAILURE; return FAILURE;
} }
} }
} }
if (ce->num_traits) { if (ce->num_traits) {
deps->traits =
zend_arena_alloc(&CG(arena), ce->num_traits * sizeof(zend_class_entry));
for (uint32_t i = 0; i < ce->num_traits; i++) { for (uint32_t i = 0; i < ce->num_traits; i++) {
deps->traits[i] = zend_hash_find_ptr(EG(class_table), ce->trait_names[i].lc_name); zend_class_entry *trait =
if (!deps->traits[i]) { zend_hash_find_ptr(EG(class_table), ce->trait_names[i].lc_name);
deps->error_kind = "Unknown trait "; if (!trait) {
deps->error_name = ZSTR_VAL(ce->trait_names[i].name); error->kind = "Unknown trait ";
error->name = ZSTR_VAL(ce->trait_names[i].name);
return FAILURE; return FAILURE;
} }
} }
} }
/* TODO: This is much more restrictive than necessary. We only need to actually
* know the types for covariant checks, but don't need them if we can ensure
* compatibility through a simple string comparison. We could improve this using
* a more general version of zend_can_early_bind(). */
if (!preload_needed_types_known(deps, ce)) {
deps->error_kind = "Unknown type dependencies";
deps->error_name = "";
return FAILURE;
}
return SUCCESS; return SUCCESS;
} }
static void preload_release_deps(preload_deps *deps)
{
zend_arena_release(&CG(arena), deps->checkpoint);
}
static bool preload_can_resolve_deps(zend_class_entry *ce)
{
preload_deps deps;
zend_result result = preload_resolve_deps(&deps, ce);
preload_release_deps(&deps);
return result == SUCCESS;
}
static void get_unlinked_dependency(zend_class_entry *ce, const char **kind, const char **name) {
preload_deps deps;
if (preload_resolve_deps(&deps, ce) == FAILURE) {
*kind = deps.error_kind;
*name = deps.error_name;
} else {
*kind = "Unknown reason";
*name = "";
}
preload_release_deps(&deps);
}
static zend_result preload_update_constant(zval *val, zend_class_entry *scope) static zend_result preload_update_constant(zval *val, zend_class_entry *scope)
{ {
zval tmp; zval tmp;
@ -3863,81 +3819,14 @@ static void preload_try_resolve_property_types(zend_class_entry *ce)
} }
} }
static bool preload_is_class_type_known(zend_class_entry *ce, zend_string *name) { static void (*orig_error_cb)(int type, zend_string *error_filename, const uint32_t error_lineno, zend_string *message);
if (zend_string_equals_literal_ci(name, "self") ||
zend_string_equals_literal_ci(name, "parent") || static void preload_error_cb(int type, zend_string *error_filename, const uint32_t error_lineno, zend_string *message)
zend_string_equals_ci(name, ce->name)) { {
return 1; /* Suppress printing of the error, only bail out for fatal errors. */
if (type & E_FATAL_ERRORS) {
zend_bailout();
} }
zend_string *lcname = zend_string_tolower(name);
bool known = zend_hash_exists(EG(class_table), lcname);
zend_string_release(lcname);
return known;
}
static bool preload_is_type_known(zend_class_entry *ce, zend_type *type) {
zend_type *single_type;
ZEND_TYPE_FOREACH(*type, single_type) {
if (ZEND_TYPE_HAS_NAME(*single_type)) {
if (!preload_is_class_type_known(ce, ZEND_TYPE_NAME(*single_type))) {
return 0;
}
}
} ZEND_TYPE_FOREACH_END();
return 1;
}
static bool preload_is_method_maybe_override(
const preload_deps *deps, zend_class_entry *ce, zend_string *lcname) {
if (ce->trait_aliases || ce->trait_precedences) {
return true;
}
if (ce->parent_name) {
if (zend_hash_exists(&deps->parent->function_table, lcname)) {
return true;
}
}
if (ce->num_interfaces) {
for (uint32_t i = 0; i < ce->num_interfaces; i++) {
if (zend_hash_exists(&deps->interfaces[i]->function_table, lcname)) {
return true;
}
}
}
if (ce->num_traits) {
for (uint32_t i = 0; i < ce->num_traits; i++) {
if (zend_hash_exists(&deps->traits[i]->function_table, lcname)) {
return true;
}
}
}
return false;
}
static bool preload_needed_types_known(const preload_deps *deps, zend_class_entry *ce) {
zend_function *fptr;
zend_string *lcname;
ZEND_HASH_FOREACH_STR_KEY_PTR(&ce->function_table, lcname, fptr) {
uint32_t i;
if (fptr->common.fn_flags & ZEND_ACC_HAS_RETURN_TYPE) {
if (!preload_is_type_known(ce, &fptr->common.arg_info[-1].type) &&
preload_is_method_maybe_override(deps, ce, lcname)) {
return 0;
}
}
for (i = 0; i < fptr->common.num_args; i++) {
if (!preload_is_type_known(ce, &fptr->common.arg_info[i].type) &&
preload_is_method_maybe_override(deps, ce, lcname)) {
return 0;
}
}
} ZEND_HASH_FOREACH_END();
return 1;
} }
static void preload_link(void) static void preload_link(void)
@ -3948,50 +3837,99 @@ static void preload_link(void)
zend_string *key; zend_string *key;
bool changed; bool changed;
HashTable errors;
zend_hash_init(&errors, 0, NULL, NULL, 0);
/* Resolve class dependencies */ /* Resolve class dependencies */
do { do {
changed = 0; changed = 0;
ZEND_HASH_REVERSE_FOREACH_VAL(EG(class_table), zv) { ZEND_HASH_REVERSE_FOREACH_STR_KEY_VAL(EG(class_table), key, zv) {
ce = Z_PTR_P(zv); ce = Z_PTR_P(zv);
if (ce->type == ZEND_INTERNAL_CLASS) { if (ce->type == ZEND_INTERNAL_CLASS) {
break; break;
} }
if ((ce->ce_flags & (ZEND_ACC_TOP_LEVEL|ZEND_ACC_ANON_CLASS)) if ((ce->ce_flags & (ZEND_ACC_TOP_LEVEL|ZEND_ACC_ANON_CLASS))
&& !(ce->ce_flags & ZEND_ACC_LINKED)) { && !(ce->ce_flags & ZEND_ACC_LINKED)) {
zend_string *lcname = zend_string_tolower(ce->name);
if (!(ce->ce_flags & ZEND_ACC_ANON_CLASS)) { if (!(ce->ce_flags & ZEND_ACC_ANON_CLASS)) {
key = zend_string_tolower(ce->name); if (zend_hash_exists(EG(class_table), lcname)) {
if (zend_hash_exists(EG(class_table), key)) { zend_string_release(lcname);
zend_string_release(key);
continue; continue;
} }
zend_string_release(key);
} }
if (!preload_can_resolve_deps(ce)) { preload_error error_info;
if (preload_resolve_deps(&error_info, ce) == FAILURE) {
zend_string_release(lcname);
continue; continue;
} }
key = zend_string_tolower(ce->name); zv = zend_hash_set_bucket_key(EG(class_table), (Bucket*)zv, lcname);
zv = zend_hash_set_bucket_key(EG(class_table), (Bucket*)zv, key);
if (EXPECTED(zv)) { if (EXPECTED(zv)) {
/* Set the FILE_CACHED flag to force a lazy load, and the CACHED flag to
* prevent freeing of interface names. */
void *checkpoint = zend_arena_checkpoint(CG(arena));
zend_class_entry *orig_ce = ce;
uint32_t temporary_flags = ZEND_ACC_FILE_CACHED|ZEND_ACC_CACHED;
ce->ce_flags |= temporary_flags;
if (ce->parent_name) {
zend_string_addref(ce->parent_name);
}
/* Record and suppress errors during inheritance. */
orig_error_cb = zend_error_cb;
zend_error_cb = preload_error_cb;
zend_begin_record_errors();
/* Set filename & lineno information for inheritance errors */ /* Set filename & lineno information for inheritance errors */
CG(in_compilation) = 1; CG(in_compilation) = 1;
CG(compiled_filename) = ce->info.user.filename; CG(compiled_filename) = ce->info.user.filename;
CG(zend_lineno) = ce->info.user.line_start; CG(zend_lineno) = ce->info.user.line_start;
ce = zend_do_link_class(ce, NULL, key); zend_try {
if (!ce) { ce = zend_do_link_class(ce, NULL, lcname);
ZEND_ASSERT(0 && "Class linking failed?"); if (!ce) {
} ZEND_ASSERT(0 && "Class linking failed?");
}
ce->ce_flags &= ~temporary_flags;
changed = true;
/* Inheritance successful, print out any warnings. */
zend_error_cb = orig_error_cb;
EG(record_errors) = false;
for (uint32_t i = 0; i < EG(num_errors); i++) {
zend_error_info *error = EG(errors)[i];
zend_error_zstr_at(
error->type, error->filename, error->lineno, error->message);
}
} zend_catch {
/* Clear variance obligations that were left behind on bailout. */
if (CG(delayed_variance_obligations)) {
zend_hash_index_del(
CG(delayed_variance_obligations), (uintptr_t) Z_CE_P(zv));
}
/* Restore the original class. */
zv = zend_hash_set_bucket_key(EG(class_table), (Bucket*)zv, key);
Z_CE_P(zv) = orig_ce;
orig_ce->ce_flags &= ~temporary_flags;
zend_arena_release(&CG(arena), checkpoint);
/* Remember the last error. */
zend_error_cb = orig_error_cb;
EG(record_errors) = false;
ZEND_ASSERT(EG(num_errors) > 0);
zend_hash_update_ptr(&errors, key, EG(errors)[EG(num_errors)-1]);
EG(num_errors)--;
} zend_end_try();
CG(in_compilation) = 0; CG(in_compilation) = 0;
CG(compiled_filename) = NULL; CG(compiled_filename) = NULL;
zend_free_recorded_errors();
changed = 1;
} }
zend_string_release(key); zend_string_release(lcname);
} }
} ZEND_HASH_FOREACH_END(); } ZEND_HASH_FOREACH_END();
} while (changed); } while (changed);
@ -4036,24 +3974,31 @@ static void preload_link(void)
} }
if ((ce->ce_flags & (ZEND_ACC_TOP_LEVEL|ZEND_ACC_ANON_CLASS)) if ((ce->ce_flags & (ZEND_ACC_TOP_LEVEL|ZEND_ACC_ANON_CLASS))
&& !(ce->ce_flags & ZEND_ACC_LINKED)) { && !(ce->ce_flags & ZEND_ACC_LINKED)) {
zend_string *key = zend_string_tolower(ce->name); zend_string *lcname = zend_string_tolower(ce->name);
preload_error error;
if (!(ce->ce_flags & ZEND_ACC_ANON_CLASS) if (!(ce->ce_flags & ZEND_ACC_ANON_CLASS)
&& zend_hash_exists(EG(class_table), key)) { && zend_hash_exists(EG(class_table), lcname)) {
zend_error_at( zend_error_at(
E_WARNING, ce->info.user.filename, ce->info.user.line_start, E_WARNING, ce->info.user.filename, ce->info.user.line_start,
"Can't preload already declared class %s", ZSTR_VAL(ce->name)); "Can't preload already declared class %s", ZSTR_VAL(ce->name));
} else { } else if (preload_resolve_deps(&error, ce)) {
const char *kind, *name;
get_unlinked_dependency(ce, &kind, &name);
zend_error_at( zend_error_at(
E_WARNING, ce->info.user.filename, ce->info.user.line_start, E_WARNING, ce->info.user.filename, ce->info.user.line_start,
"Can't preload unlinked class %s: %s%s", "Can't preload unlinked class %s: %s%s",
ZSTR_VAL(ce->name), kind, name); ZSTR_VAL(ce->name), error.kind, error.name);
} else {
zend_error_info *error = zend_hash_find_ptr(&errors, key);
zend_error_at(
E_WARNING, error->filename, error->lineno,
"Can't preload unlinked class %s: %s",
ZSTR_VAL(ce->name), ZSTR_VAL(error->message));
} }
zend_string_release(key); zend_string_release(lcname);
} }
} ZEND_HASH_FOREACH_END(); } ZEND_HASH_FOREACH_END();
zend_hash_destroy(&errors);
/* Remove DECLARE opcodes */ /* Remove DECLARE opcodes */
ZEND_HASH_FOREACH_PTR(preload_scripts, script) { ZEND_HASH_FOREACH_PTR(preload_scripts, script) {
zend_op_array *op_array = &script->script.main_op_array; zend_op_array *op_array = &script->script.main_op_array;

View file

@ -17,4 +17,5 @@ if (getenv('SKIP_ASAN')) die('xfail Startup failure leak');
echo "Foobar\n"; echo "Foobar\n";
?> ?>
--EXPECTF-- --EXPECTF--
Fatal error: Declaration of B::foo($bar) must be compatible with A::foo() in %spreload_inheritance_error.inc on line 8 Warning: Can't preload unlinked class B: Declaration of B::foo($bar) must be compatible with A::foo() in %spreload_inheritance_error.inc on line 8
Foobar

View file

@ -25,8 +25,4 @@ $g = new G;
?> ?>
--EXPECTF-- --EXPECTF--
Warning: Can't preload unlinked class H: Unknown type dependencies in %s on line %d Warning: Can't preload unlinked class H: Could not check compatibility between H::method($a): L and G::method($a): K, because class L is not available in %spreload_variance.inc on line 43
Warning: Can't preload unlinked class B: Unknown type dependencies in %s on line %d
Warning: Can't preload unlinked class A: Unknown type dependencies in %s on line %d

View file

@ -1,2 +1,6 @@
<?php <?php
spl_autoload_register(function($class) {
// Autoloader should not get called.
echo "Trying to autoload $class\n";
});
opcache_compile_file(__DIR__ . '/preload_variance.inc'); opcache_compile_file(__DIR__ . '/preload_variance.inc');