Allow throwing exception while loading parent class

This is a fix for symfony/symfony#32995.

The behavior is:

* Throwing exception when loading parent/interface is allowed
  (and we will also throw one if the class is simply not found).
* If this happens, the bucket key for the class is reset, so
  it's possibly to try registering the same class again.
* However, if the class has already been used due to a variance
  obligation, the exception is upgraded to a fatal error, as we
  cannot safely unregister the class stub anymore.
This commit is contained in:
Nikita Popov 2019-09-11 15:31:04 +02:00
parent 679cbee870
commit 4b9ebd837b
20 changed files with 253 additions and 51 deletions

View File

@ -6,4 +6,7 @@ class test implements a {
}
?>
--EXPECTF--
Fatal error: Interface 'a' not found in %sbug30519.php on line 2
Fatal error: Uncaught Error: Interface 'a' not found in %s:%d
Stack trace:
#0 {main}
thrown in %s on line %d

View File

@ -10,4 +10,7 @@ var_dump($a instanceOf A);
echo "ok\n";
?>
--EXPECTF--
Fatal error: Interface 'RecurisiveFooFar' not found in %s on line %d
Fatal error: Uncaught Error: Interface 'RecurisiveFooFar' not found in %s:%d
Stack trace:
#0 {main}
thrown in %s on line %d

View File

@ -24,11 +24,14 @@ var_dump(new Foo());
--EXPECTF--
string(3) "Foo"
string(3) "Bar"
string(3) "Foo"
string(3) "Bar"
Fatal error: During class fetch: Uncaught Exception: Bar in %s:%d
Fatal error: Uncaught Exception: Bar in %s:%d
Stack trace:
#0 [internal function]: {closure}('Bar')
#1 %s(%d): spl_autoload_call('Bar')
#2 [internal function]: {closure}('Foo')
#3 %s(%d): spl_autoload_call('Foo')
#4 {main} in %s on line %d
#4 {main}
thrown in %s on line %d

View File

@ -0,0 +1,49 @@
--TEST--
Exception while loading class -- parent case
--FILE--
<?php
spl_autoload_register(function($class) {
throw new Exception("Class $class does not exist");
});
// Graceful failure allowed
for ($i = 0; $i < 2; $i++) {
try {
class B extends A {
}
} catch (Exception $e) {
echo $e->getMessage(), "\n";
}
}
interface I {}
spl_autoload_register(function($class) {
// Tie up B in a variance obligation.
class X {
public function test(): I {}
}
class Y extends X {
public function test(): B {}
}
}, true, true);
// Fallback to fatal error, as we can't unlink class B anymore.
try {
class B extends A implements I {
}
} catch (Exception $e) {
echo $e->getMessage(), "\n";
}
?>
--EXPECTF--
Class A does not exist
Class A does not exist
Fatal error: During inheritance of B with variance dependencies: Uncaught Exception: Class A does not exist in %s:%d
Stack trace:
#0 [internal function]: {closure}('A')
#1 %s(%d): spl_autoload_call('A')
#2 {main} in %s on line %d

View File

@ -0,0 +1,51 @@
--TEST--
Exception while loading class -- interface case
--FILE--
<?php
spl_autoload_register(function($class) {
throw new Exception("Class $class does not exist");
});
class A {}
// Graceful failure allowed
for ($i = 0; $i < 2; $i++) {
try {
class B extends A implements I {
}
} catch (Exception $e) {
echo $e->getMessage(), "\n";
}
}
interface J {}
spl_autoload_register(function($class) {
// Tie up B in a variance obligation.
class X {
public function test(): J {}
}
class Y extends X {
public function test(): B {}
}
}, true, true);
// Fallback to fatal error, as we can't unlink class B anymore.
try {
class B extends A implements I, J {
}
} catch (Exception $e) {
echo $e->getMessage(), "\n";
}
?>
--EXPECTF--
Class I does not exist
Class I does not exist
Fatal error: During inheritance of B with variance dependencies: Uncaught Exception: Class I does not exist in %s:%d
Stack trace:
#0 [internal function]: {closure}('I')
#1 %s(%d): spl_autoload_call('I')
#2 {main} in %s on line %d

View File

@ -7,9 +7,13 @@ spl_autoload_register(function($class) {
class X extends B {}
});
class B extends A {
try {
class B extends A {
}
} catch (Error $e) {
echo $e->getMessage(), "\n";
}
?>
--EXPECTF--
Fatal error: Class 'B' not found in %s on line %d
--EXPECT--
Class 'B' not found

View File

@ -7,9 +7,13 @@ spl_autoload_register(function($class) {
class X implements B {}
});
interface B extends A {
try {
interface B extends A {
}
} catch (Error $e) {
echo $e->getMessage(), "\n";
}
?>
--EXPECTF--
Fatal error: Interface 'B' not found in %s on line %d
--EXPECT--
Interface 'B' not found

View File

@ -12,9 +12,10 @@ class A implements I {
?>
--EXPECTF--
Fatal error: During class fetch: Uncaught ReflectionException: Class A does not exist in %s:%d
Fatal error: Uncaught ReflectionException: Class A does not exist in %s:%d
Stack trace:
#0 %s(%d): ReflectionClass->__construct('A')
#1 [internal function]: {closure}('I')
#2 %s(%d): spl_autoload_call('I')
#3 {main} in %s on line %d
#3 {main}
thrown in %s on line %d

View File

@ -1081,7 +1081,11 @@ ZEND_API int do_bind_class(zval *lcname, zend_string *lc_parent_name) /* {{{ */
return FAILURE;
}
zend_do_link_class(ce, lc_parent_name);
if (zend_do_link_class(ce, lc_parent_name) == FAILURE) {
zend_hash_set_bucket_key(EG(class_table), (Bucket *) zv, Z_STR_P(rtd_key));
return FAILURE;
}
return SUCCESS;
}
/* }}} */

View File

@ -229,7 +229,7 @@ typedef struct _zend_oparray_context {
/* op_array or class is preloaded | | | */
#define ZEND_ACC_PRELOADED (1 << 10) /* X | X | | */
/* | | | */
/* Class Flags (unused: 23...) | | | */
/* Class Flags (unused: 24...) | | | */
/* =========== | | | */
/* | | | */
/* Special class types | | | */
@ -281,6 +281,9 @@ typedef struct _zend_oparray_context {
/* Class is linked apart from variance obligations. | | | */
#define ZEND_ACC_NEARLY_LINKED (1 << 22) /* X | | | */
/* | | | */
/* Whether this class was used in its unlinked state. | | | */
#define ZEND_ACC_HAS_UNLINKED_USES (1 << 23) /* X | | | */
/* | | | */
/* Function Flags (unused: 23, 26) | | | */
/* ============== | | | */
/* | | | */

View File

@ -921,6 +921,7 @@ ZEND_API zend_class_entry *zend_lookup_class_ex(zend_string *name, zend_string *
if ((flags & ZEND_FETCH_CLASS_ALLOW_UNLINKED) ||
((flags & ZEND_FETCH_CLASS_ALLOW_NEARLY_LINKED) &&
(ce->ce_flags & ZEND_ACC_NEARLY_LINKED))) {
ce->ce_flags |= ZEND_ACC_HAS_UNLINKED_USES;
return ce;
}
return NULL;

View File

@ -25,6 +25,7 @@
#include "zend_interfaces.h"
#include "zend_smart_str.h"
#include "zend_operators.h"
#include "zend_exceptions.h"
static void add_dependency_obligation(zend_class_entry *ce, zend_class_entry *dependency_ce);
static void add_compatibility_obligation(
@ -1437,26 +1438,17 @@ ZEND_API void zend_do_implement_interface(zend_class_entry *ce, zend_class_entry
}
/* }}} */
static void zend_do_implement_interfaces(zend_class_entry *ce) /* {{{ */
static void zend_do_implement_interfaces(zend_class_entry *ce, zend_class_entry **interfaces) /* {{{ */
{
zend_class_entry **interfaces, *iface;
uint32_t num_interfaces = 0;
zend_class_entry *iface;
uint32_t num_parent_interfaces = ce->parent ? ce->parent->num_interfaces : 0;
uint32_t num_interfaces = num_parent_interfaces;
zend_string *key;
zend_class_constant *c;
uint32_t i, j;
if (ce->parent && ce->parent->num_interfaces) {
interfaces = emalloc(sizeof(zend_class_entry*) * (ce->parent->num_interfaces + ce->num_interfaces));
memcpy(interfaces, ce->parent->interfaces, sizeof(zend_class_entry*) * ce->parent->num_interfaces);
num_interfaces = ce->parent->num_interfaces;
} else {
interfaces = emalloc(sizeof(zend_class_entry*) * ce->num_interfaces);
}
for (i = 0; i < ce->num_interfaces; i++) {
iface = zend_fetch_class_by_name(
ce->interface_names[i].name, ce->interface_names[i].lc_name,
ZEND_FETCH_CLASS_INTERFACE|ZEND_FETCH_CLASS_ALLOW_NEARLY_LINKED);
iface = interfaces[num_parent_interfaces + i];
if (!(iface->ce_flags & ZEND_ACC_LINKED)) {
add_dependency_obligation(ce, iface);
}
@ -1467,7 +1459,7 @@ static void zend_do_implement_interfaces(zend_class_entry *ce) /* {{{ */
}
for (j = 0; j < num_interfaces; j++) {
if (interfaces[j] == iface) {
if (!ce->parent || j >= ce->parent->num_interfaces) {
if (j >= num_parent_interfaces) {
efree(interfaces);
zend_error_noreturn(E_COMPILE_ERROR, "Class %s cannot implement previously implemented interface %s", ZSTR_VAL(ce->name), ZSTR_VAL(iface->name));
return;
@ -1497,7 +1489,7 @@ static void zend_do_implement_interfaces(zend_class_entry *ce) /* {{{ */
ce->interfaces = interfaces;
ce->ce_flags |= ZEND_ACC_RESOLVED_INTERFACES;
i = ce->parent ? ce->parent->num_interfaces : 0;
i = num_parent_interfaces;
for (; i < ce->num_interfaces; i++) {
do_interface_implementation(ce, ce->interfaces[i]);
}
@ -2399,11 +2391,66 @@ static void report_variance_errors(zend_class_entry *ce) {
zend_hash_index_del(all_obligations, num_key);
}
ZEND_API void zend_do_link_class(zend_class_entry *ce, zend_string *lc_parent_name) /* {{{ */
static void check_unrecoverable_load_failure(zend_class_entry *ce) {
/* If this class has been used while unlinked through a variance obligation, it is not legal
* to remove the class from the class table and throw an exception, because there is already
* a dependence on the inheritance hierarchy of this specific class. Instead we fall back to
* a fatal error, as would happen if we did not allow exceptions in the first place. */
if (ce->ce_flags & ZEND_ACC_HAS_UNLINKED_USES) {
zend_string *exception_str;
zval exception_zv;
ZEND_ASSERT(EG(exception) && "Exception must have been thrown");
ZVAL_OBJ(&exception_zv, EG(exception));
Z_ADDREF(exception_zv);
zend_clear_exception();
exception_str = zval_get_string(&exception_zv);
zend_error_noreturn(E_ERROR,
"During inheritance of %s with variance dependencies: Uncaught %s", ZSTR_VAL(ce->name), ZSTR_VAL(exception_str));
}
}
ZEND_API int zend_do_link_class(zend_class_entry *ce, zend_string *lc_parent_name) /* {{{ */
{
/* Load parent/interface dependencies first, so we can still gracefully abort linking
* with an exception and remove the class from the class table. This is only possible
* if no variance obligations on the current class have been added during autoloading. */
zend_class_entry *parent = NULL;
zend_class_entry **interfaces = NULL;
if (ce->parent_name) {
zend_class_entry *parent = zend_fetch_class_by_name(
ce->parent_name, lc_parent_name, ZEND_FETCH_CLASS_ALLOW_NEARLY_LINKED);
parent = zend_fetch_class_by_name(
ce->parent_name, lc_parent_name,
ZEND_FETCH_CLASS_ALLOW_NEARLY_LINKED | ZEND_FETCH_CLASS_EXCEPTION);
if (!parent) {
check_unrecoverable_load_failure(ce);
return FAILURE;
}
}
if (ce->num_interfaces) {
/* Also copy the parent interfaces here, so we don't need to reallocate later. */
uint32_t i, num_parent_interfaces = parent ? parent->num_interfaces : 0;
interfaces = emalloc(
sizeof(zend_class_entry *) * (ce->num_interfaces + num_parent_interfaces));
if (num_parent_interfaces) {
memcpy(interfaces, parent->interfaces,
sizeof(zend_class_entry *) * num_parent_interfaces);
}
for (i = 0; i < ce->num_interfaces; i++) {
zend_class_entry *iface = zend_fetch_class_by_name(
ce->interface_names[i].name, ce->interface_names[i].lc_name,
ZEND_FETCH_CLASS_INTERFACE |
ZEND_FETCH_CLASS_ALLOW_NEARLY_LINKED | ZEND_FETCH_CLASS_EXCEPTION);
if (!iface) {
check_unrecoverable_load_failure(ce);
efree(interfaces);
return FAILURE;
}
interfaces[num_parent_interfaces + i] = iface;
}
}
if (parent) {
if (!(parent->ce_flags & ZEND_ACC_LINKED)) {
add_dependency_obligation(ce, parent);
}
@ -2413,7 +2460,7 @@ ZEND_API void zend_do_link_class(zend_class_entry *ce, zend_string *lc_parent_na
zend_do_bind_traits(ce);
}
if (ce->ce_flags & ZEND_ACC_IMPLEMENT_INTERFACES) {
zend_do_implement_interfaces(ce);
zend_do_implement_interfaces(ce, interfaces);
}
if ((ce->ce_flags & (ZEND_ACC_IMPLICIT_ABSTRACT_CLASS|ZEND_ACC_INTERFACE|ZEND_ACC_TRAIT|ZEND_ACC_EXPLICIT_ABSTRACT_CLASS)) == ZEND_ACC_IMPLICIT_ABSTRACT_CLASS) {
zend_verify_abstract_class(ce);
@ -2423,7 +2470,7 @@ ZEND_API void zend_do_link_class(zend_class_entry *ce, zend_string *lc_parent_na
if (!(ce->ce_flags & ZEND_ACC_UNRESOLVED_VARIANCE)) {
ce->ce_flags |= ZEND_ACC_LINKED;
return;
return SUCCESS;
}
ce->ce_flags |= ZEND_ACC_NEARLY_LINKED;
@ -2434,6 +2481,8 @@ ZEND_API void zend_do_link_class(zend_class_entry *ce, zend_string *lc_parent_na
report_variance_errors(ce);
}
}
return SUCCESS;
}
/* }}} */

View File

@ -30,7 +30,7 @@ ZEND_API void zend_do_inheritance_ex(zend_class_entry *ce, zend_class_entry *par
#define zend_do_inheritance(ce, parent_ce) \
zend_do_inheritance_ex(ce, parent_ce, 0)
ZEND_API void zend_do_link_class(zend_class_entry *ce, zend_string *lc_parent_name);
ZEND_API int zend_do_link_class(zend_class_entry *ce, zend_string *lc_parent_name);
void zend_verify_abstract_class(zend_class_entry *ce);
void zend_check_deprecated_constructor(const zend_class_entry *ce);

View File

@ -7273,8 +7273,8 @@ ZEND_VM_HANDLER(145, ZEND_DECLARE_CLASS_DELAYED, CONST, CONST)
if (UNEXPECTED(!zv)) {
zend_error_noreturn(E_COMPILE_ERROR, "Cannot declare %s %s, because the name is already in use", zend_get_object_type(ce), ZSTR_VAL(ce->name));
} else {
zend_do_link_class(ce, Z_STR_P(RT_CONSTANT(opline, opline->op2)));
if (UNEXPECTED(EG(exception))) {
if (zend_do_link_class(ce, Z_STR_P(RT_CONSTANT(opline, opline->op2))) == FAILURE) {
zend_hash_set_bucket_key(EG(class_table), (Bucket *) zv, Z_STR_P(lcname + 1));
HANDLE_EXCEPTION();
}
}
@ -7292,13 +7292,14 @@ ZEND_VM_HANDLER(146, ZEND_DECLARE_ANON_CLASS, ANY, ANY, CACHE_SLOT)
ce = CACHED_PTR(opline->extended_value);
if (UNEXPECTED(ce == NULL)) {
zv = zend_hash_find_ex(EG(class_table), Z_STR_P(RT_CONSTANT(opline, opline->op1)), 1);
zend_string *rtd_key = Z_STR_P(RT_CONSTANT(opline, opline->op1));
zv = zend_hash_find_ex(EG(class_table), rtd_key, 1);
ZEND_ASSERT(zv != NULL);
ce = Z_CE_P(zv);
if (!(ce->ce_flags & ZEND_ACC_LINKED)) {
SAVE_OPLINE();
zend_do_link_class(ce, (OP2_TYPE == IS_CONST) ? Z_STR_P(RT_CONSTANT(opline, opline->op2)) : NULL);
if (UNEXPECTED(EG(exception))) {
if (zend_do_link_class(ce, (OP2_TYPE == IS_CONST) ? Z_STR_P(RT_CONSTANT(opline, opline->op2)) : NULL) == FAILURE) {
zend_hash_set_bucket_key(EG(class_table), (Bucket *) zv, rtd_key);
HANDLE_EXCEPTION();
}
}

View File

@ -2444,13 +2444,14 @@ static ZEND_OPCODE_HANDLER_RET ZEND_FASTCALL ZEND_DECLARE_ANON_CLASS_SPEC_HANDLE
ce = CACHED_PTR(opline->extended_value);
if (UNEXPECTED(ce == NULL)) {
zv = zend_hash_find_ex(EG(class_table), Z_STR_P(RT_CONSTANT(opline, opline->op1)), 1);
zend_string *rtd_key = Z_STR_P(RT_CONSTANT(opline, opline->op1));
zv = zend_hash_find_ex(EG(class_table), rtd_key, 1);
ZEND_ASSERT(zv != NULL);
ce = Z_CE_P(zv);
if (!(ce->ce_flags & ZEND_ACC_LINKED)) {
SAVE_OPLINE();
zend_do_link_class(ce, (opline->op2_type == IS_CONST) ? Z_STR_P(RT_CONSTANT(opline, opline->op2)) : NULL);
if (UNEXPECTED(EG(exception))) {
if (zend_do_link_class(ce, (opline->op2_type == IS_CONST) ? Z_STR_P(RT_CONSTANT(opline, opline->op2)) : NULL) == FAILURE) {
zend_hash_set_bucket_key(EG(class_table), (Bucket *) zv, rtd_key);
HANDLE_EXCEPTION();
}
}
@ -6382,8 +6383,8 @@ static ZEND_OPCODE_HANDLER_RET ZEND_FASTCALL ZEND_DECLARE_CLASS_DELAYED_SPEC_CON
if (UNEXPECTED(!zv)) {
zend_error_noreturn(E_COMPILE_ERROR, "Cannot declare %s %s, because the name is already in use", zend_get_object_type(ce), ZSTR_VAL(ce->name));
} else {
zend_do_link_class(ce, Z_STR_P(RT_CONSTANT(opline, opline->op2)));
if (UNEXPECTED(EG(exception))) {
if (zend_do_link_class(ce, Z_STR_P(RT_CONSTANT(opline, opline->op2))) == FAILURE) {
zend_hash_set_bucket_key(EG(class_table), (Bucket *) zv, Z_STR_P(lcname + 1));
HANDLE_EXCEPTION();
}
}

View File

@ -3731,7 +3731,9 @@ static void preload_link(void)
} else {
CG(zend_lineno) = ce->info.user.line_start;
}
zend_do_link_class(ce, NULL);
if (zend_do_link_class(ce, NULL) == FAILURE) {
ZEND_ASSERT(0 && "Class linking failed?");
}
CG(in_compilation) = 0;
CG(compiled_filename) = NULL;

View File

@ -68,4 +68,15 @@ foreach (new \RecursiveIteratorIterator (new fooIterator ($foo)) as $bar) ;
?>
--EXPECTF--
Fatal error: Class 'NotExists' not found in %s(%d) : eval()'d code on line 1
Fatal error: Uncaught Error: Class 'NotExists' not found in %s:%d
Stack trace:
#0 %s(%d): eval()
#1 %s(%d): fooIterator->__destruct()
#2 {main}
Next Error: Class 'NotExists' not found in %s:%d
Stack trace:
#0 %s(%d): eval()
#1 %s(%d): fooIterator->__destruct()
#2 {main}
thrown in %s on line %d

View File

@ -14,4 +14,7 @@ class C implements UndefI
--EXPECTF--
In autoload: string(6) "UndefI"
Fatal error: Interface 'UndefI' not found in %s on line %d
Fatal error: Uncaught Error: Interface 'UndefI' not found in %s:%d
Stack trace:
#0 {main}
thrown in %s on line %d

View File

@ -14,4 +14,7 @@ class C extends UndefBase
--EXPECTF--
In autoload: string(9) "UndefBase"
Fatal error: Class 'UndefBase' not found in %s on line %d
Fatal error: Uncaught Error: Class 'UndefBase' not found in %s:%d
Stack trace:
#0 {main}
thrown in %s on line %d

View File

@ -13,4 +13,10 @@ try {
?>
--EXPECTF--
Fatal error: Class 'B' not found in %s on line %d
bool(false)
bool(false)
Fatal error: Uncaught Error: Class 'B' not found in %s:%d
Stack trace:
#0 {main}
thrown in %s on line %d