]> granicus.if.org Git - php/commitdiff
Fixed bug #78935: Check that all linked classes can be preloaded
authorNikita Popov <nikita.ppv@gmail.com>
Mon, 9 Dec 2019 14:14:39 +0000 (15:14 +0100)
committerNikita Popov <nikita.ppv@gmail.com>
Tue, 10 Dec 2019 12:05:48 +0000 (13:05 +0100)
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.

16 files changed:
NEWS
Zend/zend_compile.c
ext/opcache/ZendAccelerator.c
ext/opcache/tests/preload_004.phpt
ext/opcache/tests/preload_009.phpt
ext/opcache/tests/preload_loadable_classes_1.inc [new file with mode: 0644]
ext/opcache/tests/preload_loadable_classes_1.phpt [new file with mode: 0644]
ext/opcache/tests/preload_loadable_classes_2.inc [new file with mode: 0644]
ext/opcache/tests/preload_loadable_classes_2.phpt [new file with mode: 0644]
ext/opcache/tests/preload_loadable_classes_3.inc [new file with mode: 0644]
ext/opcache/tests/preload_loadable_classes_3.phpt [new file with mode: 0644]
ext/opcache/tests/preload_loadable_classes_4.inc [new file with mode: 0644]
ext/opcache/tests/preload_loadable_classes_4.phpt [new file with mode: 0644]
ext/opcache/tests/preload_unresolved_prop_type.inc [new file with mode: 0644]
ext/opcache/tests/preload_unresolved_prop_type.phpt [new file with mode: 0644]
ext/opcache/tests/preload_unresolved_prop_type_2.inc [new file with mode: 0644]

diff --git a/NEWS b/NEWS
index afe928926fcbf225a6f781be66a15793c4d2455f..8b51908bf60cb96d467abcc6a2f10420b5770ea5 100644 (file)
--- a/NEWS
+++ b/NEWS
@@ -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)
index 116fc0b7c539fb52b3f9aa6a08b76fe6f158052f..7d35a92ade679d132c9435ff0b04b271968953a6 100644 (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))) {
 
index 895bf82b452181e7a72659c8856fb374aab6ee8c..ccd300390fc69b471a466eb62cdb787bdfeda052 100644 (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();
index 0441c78883a844299a9b7f1d30863a653708ec4e..df92abdc28f5e91e6bfcd0fbdab3b741385dc7ec 100644 (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
index 84f444768c022a064a7980ba3b148e6cef0890e2..1adbf5b00649f53b4640575a4c1e07081fd3ae74 100644 (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)
diff --git a/ext/opcache/tests/preload_loadable_classes_1.inc b/ext/opcache/tests/preload_loadable_classes_1.inc
new file mode 100644 (file)
index 0000000..b2bdaba
--- /dev/null
@@ -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;
+}
diff --git a/ext/opcache/tests/preload_loadable_classes_1.phpt b/ext/opcache/tests/preload_loadable_classes_1.phpt
new file mode 100644 (file)
index 0000000..8673814
--- /dev/null
@@ -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)
diff --git a/ext/opcache/tests/preload_loadable_classes_2.inc b/ext/opcache/tests/preload_loadable_classes_2.inc
new file mode 100644 (file)
index 0000000..d21384d
--- /dev/null
@@ -0,0 +1,6 @@
+<?php
+
+class Test {
+    const X = UNDEF;
+    const Y = Foo::UNDEF;
+}
diff --git a/ext/opcache/tests/preload_loadable_classes_2.phpt b/ext/opcache/tests/preload_loadable_classes_2.phpt
new file mode 100644 (file)
index 0000000..4a5d2b8
--- /dev/null
@@ -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
diff --git a/ext/opcache/tests/preload_loadable_classes_3.inc b/ext/opcache/tests/preload_loadable_classes_3.inc
new file mode 100644 (file)
index 0000000..d5c550f
--- /dev/null
@@ -0,0 +1,5 @@
+<?php
+
+class Test {
+    protected Foo $prop;
+}
diff --git a/ext/opcache/tests/preload_loadable_classes_3.phpt b/ext/opcache/tests/preload_loadable_classes_3.phpt
new file mode 100644 (file)
index 0000000..a486929
--- /dev/null
@@ -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
diff --git a/ext/opcache/tests/preload_loadable_classes_4.inc b/ext/opcache/tests/preload_loadable_classes_4.inc
new file mode 100644 (file)
index 0000000..162de6e
--- /dev/null
@@ -0,0 +1,3 @@
+<?php
+
+class Test extends Exception {}
diff --git a/ext/opcache/tests/preload_loadable_classes_4.phpt b/ext/opcache/tests/preload_loadable_classes_4.phpt
new file mode 100644 (file)
index 0000000..07f0d09
--- /dev/null
@@ -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
diff --git a/ext/opcache/tests/preload_unresolved_prop_type.inc b/ext/opcache/tests/preload_unresolved_prop_type.inc
new file mode 100644 (file)
index 0000000..05f4ee0
--- /dev/null
@@ -0,0 +1,2 @@
+<?php
+opcache_compile_file(__DIR__ . "/preload_unresolved_prop_type_2.inc");
diff --git a/ext/opcache/tests/preload_unresolved_prop_type.phpt b/ext/opcache/tests/preload_unresolved_prop_type.phpt
new file mode 100644 (file)
index 0000000..117f366
--- /dev/null
@@ -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===
diff --git a/ext/opcache/tests/preload_unresolved_prop_type_2.inc b/ext/opcache/tests/preload_unresolved_prop_type_2.inc
new file mode 100644 (file)
index 0000000..c5557d1
--- /dev/null
@@ -0,0 +1,5 @@
+<?php
+
+class Test {
+    public Unknown $prop;
+}