From 7a7642f08a2e3c8934ec3de1f10fb71bc6f48e56 Mon Sep 17 00:00:00 2001 From: Nikita Popov Date: Wed, 31 Jul 2019 13:54:29 +0200 Subject: [PATCH] WIP package implementation --- .../packages/package_declare_errors.phpt | 111 ++++++++++++++++++ .../packages/package_first_statement.phpt | 11 ++ Zend/tests/packages/package_strict_types.phpt | 50 ++++++++ Zend/tests/packages/package_ticks.phpt | 50 ++++++++ Zend/tests/packages/unknown_package.phpt | 10 ++ Zend/zend_ast.h | 1 + Zend/zend_builtin_functions.c | 97 +++++++++++++++ Zend/zend_compile.c | 48 +++++++- Zend/zend_compile.h | 8 ++ Zend/zend_globals.h | 4 + Zend/zend_language_parser.y | 5 + Zend/zend_language_scanner.l | 5 + ext/opcache/ZendAccelerator.c | 22 +++- ext/opcache/ZendAccelerator.h | 1 + ext/opcache/tests/packages_invalidation.phpt | 33 ++++++ .../packages_invalidation_file_cache.phpt | 37 ++++++ ...packages_invalidation_file_cache_only.phpt | 36 ++++++ .../tests/packages_invalidation_use_pkg.php | 9 ++ ...ackages_invalidation_with_strict_types.php | 9 ++ ...ages_invalidation_without_strict_types.php | 9 ++ ext/opcache/zend_accelerator_util_funcs.c | 13 ++ ext/opcache/zend_accelerator_util_funcs.h | 1 + ext/opcache/zend_file_cache.c | 43 +++++++ ext/opcache/zend_persist.c | 8 ++ ext/opcache/zend_persist_calc.c | 8 ++ 25 files changed, 624 insertions(+), 5 deletions(-) create mode 100644 Zend/tests/packages/package_declare_errors.phpt create mode 100644 Zend/tests/packages/package_first_statement.phpt create mode 100644 Zend/tests/packages/package_strict_types.phpt create mode 100644 Zend/tests/packages/package_ticks.phpt create mode 100644 Zend/tests/packages/unknown_package.phpt create mode 100644 ext/opcache/tests/packages_invalidation.phpt create mode 100644 ext/opcache/tests/packages_invalidation_file_cache.phpt create mode 100644 ext/opcache/tests/packages_invalidation_file_cache_only.phpt create mode 100644 ext/opcache/tests/packages_invalidation_use_pkg.php create mode 100644 ext/opcache/tests/packages_invalidation_with_strict_types.php create mode 100644 ext/opcache/tests/packages_invalidation_without_strict_types.php diff --git a/Zend/tests/packages/package_declare_errors.phpt b/Zend/tests/packages/package_declare_errors.phpt new file mode 100644 index 0000000000000..fc9ffd9774185 --- /dev/null +++ b/Zend/tests/packages/package_declare_errors.phpt @@ -0,0 +1,111 @@ +--TEST-- +Error conditions of package_declare() +--FILE-- +getMessage(), "\n"; +} + +try { + package_declare([ + "name" => 42, + ]); +} catch (Error $e) { + echo $e->getMessage(), "\n"; +} + +try { + package_declare([ + "name" => "foo/bar", + "declares" => 42, + ]); +} catch (Error $e) { + echo $e->getMessage(), "\n"; +} + +try { + package_declare([ + "name" => "foo/bar", + "declares" => [ + "foo" + ], + ]); +} catch (Error $e) { + echo $e->getMessage(), "\n"; +} + +try { + package_declare([ + "name" => "foo/bar", + "declares" => [ + "ticks" => "foo", + ], + ]); +} catch (Error $e) { + echo $e->getMessage(), "\n"; +} + +try { + package_declare([ + "name" => "foo/bar", + "declares" => [ + "ticks" => -1, + ], + ]); +} catch (Error $e) { + echo $e->getMessage(), "\n"; +} + +try { + package_declare([ + "name" => "foo/bar", + "declares" => [ + "strict_types" => "foo", + ], + ]); +} catch (Error $e) { + echo $e->getMessage(), "\n"; +} + +try { + package_declare([ + "name" => "foo/bar", + "declares" => [ + "strict_types" => 42, + ], + ]); +} catch (Error $e) { + echo $e->getMessage(), "\n"; +} + +// Allowed for forward-compatibility +package_declare([ + "name" => "foo/bar", + "declares" => [ + "unknown" => "foo", + ], +]); + + +try { + package_declare([ + "name" => "foo/bar", + ]); +} catch (Error $e) { + echo $e->getMessage(), "\n"; +} + +?> +--EXPECT-- +Package specification must contain "name" key of type string +Package specification must contain "name" key of type string +Package specification key "declares" must be of type array +Declares array in package specification must have string keys +Value of "ticks" must be a non-negative integer +Value of "ticks" must be a non-negative integer +Value of "strict_types" must be 0 or 1 +Value of "strict_types" must be 0 or 1 +Package "foo/bar" already registered diff --git a/Zend/tests/packages/package_first_statement.phpt b/Zend/tests/packages/package_first_statement.phpt new file mode 100644 index 0000000000000..52440fb19a3c9 --- /dev/null +++ b/Zend/tests/packages/package_first_statement.phpt @@ -0,0 +1,11 @@ +--TEST-- +Package declaration must be the first statement (even before declares) +--FILE-- + +--EXPECTF-- +Fatal error: Package declaration must be the first statement in the script in %s on line %d diff --git a/Zend/tests/packages/package_strict_types.phpt b/Zend/tests/packages/package_strict_types.phpt new file mode 100644 index 0000000000000..ed5d4c861828f --- /dev/null +++ b/Zend/tests/packages/package_strict_types.phpt @@ -0,0 +1,50 @@ +--TEST-- +Specifying strict_types through package declaration +--FILE-- + "with_strict_types", + "declares" => [ + "strict_types" => 1, + ], +]); + +eval(<<<'PHP' +// Package defaults to strict types +package "with_strict_types"; + +try { + var_dump(strlen(42)); +} catch (TypeError $e) { + echo $e->getMessage(), "\n"; +} +PHP); + +eval(<<<'PHP' +// Manual override to strict_types=0 +package "with_strict_types"; +declare(strict_types=0); + +try { + var_dump(strlen(42)); +} catch (TypeError $e) { + echo $e->getMessage(), "\n"; +} +PHP); + +eval(<<<'PHP' +// No package, no strict_types + +try { + var_dump(strlen(42)); +} catch (TypeError $e) { + echo $e->getMessage(), "\n"; +} +PHP); + +?> +--EXPECT-- +strlen() expects parameter 1 to be string, int given +int(2) +int(2) diff --git a/Zend/tests/packages/package_ticks.phpt b/Zend/tests/packages/package_ticks.phpt new file mode 100644 index 0000000000000..91fdea73c1e86 --- /dev/null +++ b/Zend/tests/packages/package_ticks.phpt @@ -0,0 +1,50 @@ +--TEST-- +Specifying ticks through package declaration +--FILE-- + "with_ticks", + "declares" => [ + "ticks" => 1, + ], +]); + +eval(<<<'PHP' +// Package defaults to ticks=1 +package "with_ticks"; + +echo "Foo\n"; +echo "Foo\n"; +PHP); + +eval(<<<'PHP' +// Manual override to ticks=0 +package "with_ticks"; +declare(ticks=0); + +echo "Bar\n"; +echo "Bar\n"; +PHP); + +eval(<<<'PHP' +// No package, no ticks + +echo "Baz\n"; +echo "Baz\n"; +PHP); + +?> +--EXPECT-- +Foo +Tick +Foo +Tick +Bar +Bar +Baz +Baz diff --git a/Zend/tests/packages/unknown_package.phpt b/Zend/tests/packages/unknown_package.phpt new file mode 100644 index 0000000000000..036a74aa14c8b --- /dev/null +++ b/Zend/tests/packages/unknown_package.phpt @@ -0,0 +1,10 @@ +--TEST-- +Package declaration references an unknown package +--FILE-- + +--EXPECTF-- +Fatal error: Unknown package "foo/bar" in %s on line %d diff --git a/Zend/zend_ast.h b/Zend/zend_ast.h index fd6dd1677a450..ccb0a21565ea5 100644 --- a/Zend/zend_ast.h +++ b/Zend/zend_ast.h @@ -101,6 +101,7 @@ enum _zend_ast_kind { ZEND_AST_GOTO, ZEND_AST_BREAK, ZEND_AST_CONTINUE, + ZEND_AST_PACKAGE, /* 2 child nodes */ ZEND_AST_DIM = 2 << ZEND_AST_NUM_CHILDREN_SHIFT, diff --git a/Zend/zend_builtin_functions.c b/Zend/zend_builtin_functions.c index ce939f6489b82..1b0ed71fa9c6e 100644 --- a/Zend/zend_builtin_functions.c +++ b/Zend/zend_builtin_functions.c @@ -84,6 +84,7 @@ static ZEND_FUNCTION(gc_enabled); static ZEND_FUNCTION(gc_enable); static ZEND_FUNCTION(gc_disable); static ZEND_FUNCTION(gc_status); +static ZEND_FUNCTION(package_declare); /* {{{ arginfo */ ZEND_BEGIN_ARG_INFO(arginfo_zend__void, 0) @@ -226,6 +227,9 @@ ZEND_BEGIN_ARG_INFO_EX(arginfo_extension_loaded, 0, 0, 1) ZEND_ARG_INFO(0, extension_name) ZEND_END_ARG_INFO() +ZEND_BEGIN_ARG_INFO_EX(arginfo_package_declare, 0, 0, 1) + ZEND_ARG_INFO(0, info) +ZEND_END_ARG_INFO() /* }}} */ static const zend_function_entry builtin_functions[] = { /* {{{ */ @@ -287,6 +291,7 @@ static const zend_function_entry builtin_functions[] = { /* {{{ */ ZEND_FE(gc_enable, arginfo_zend__void) ZEND_FE(gc_disable, arginfo_zend__void) ZEND_FE(gc_status, arginfo_zend__void) + ZEND_FE(package_declare, arginfo_package_declare) ZEND_FE_END }; /* }}} */ @@ -2532,3 +2537,95 @@ ZEND_FUNCTION(get_extension_funcs) } } /* }}} */ + +static int init_package_declares(zend_package_info *info, HashTable *declares) { + zend_string *name; + zval *val; + ZEND_HASH_FOREACH_STR_KEY_VAL(declares, name, val) { + ZVAL_DEREF(val); + if (!name) { + zend_throw_error(NULL, "Declares array in package specification must have string keys"); + return FAILURE; + } + + if (zend_string_equals_literal(name, "encoding")) { + zend_throw_error(NULL, "Cannot use \"encoding\" in package-level declares"); + return FAILURE; + } + + if (zend_string_equals_literal_ci(name, "strict_types")) { + if (Z_TYPE_P(val) != IS_LONG || (Z_LVAL_P(val) != 0 && Z_LVAL_P(val) != 1)) { + zend_throw_error(NULL, "Value of \"strict_types\" must be 0 or 1"); + return FAILURE; + } + + info->declares.strict_types = Z_LVAL_P(val); + continue; + } + + if (zend_string_equals_literal_ci(name, "ticks")) { + if (Z_TYPE_P(val) != IS_LONG || Z_LVAL_P(val) < 0) { + zend_throw_error(NULL, "Value of \"ticks\" must be a non-negative integer"); + return FAILURE; + } + + info->declares.ticks = Z_LVAL_P(val); + continue; + } + + /* Unknown declares intentionally ignored for forward-compatibility reasons. */ + } ZEND_HASH_FOREACH_END(); + return SUCCESS; +} + +static void package_info_dtor(zval *zv) { + zend_package_info *info = Z_PTR_P(zv); + zend_string_release(info->name); + efree(info); +} + +/* {{{ proto void package_declare(array spec) */ +ZEND_FUNCTION(package_declare) +{ + HashTable *spec; + zval *name_zv; + zval *declares_zv; + zend_package_info info; + + if (zend_parse_parameters(ZEND_NUM_ARGS(), "h", &spec) == FAILURE) { + return; + } + + name_zv = zend_hash_str_find_deref(spec, "name", sizeof("name") - 1); + declares_zv = zend_hash_str_find_deref(spec, "declares", sizeof("declares") - 1); + + if (!name_zv || Z_TYPE_P(name_zv) != IS_STRING) { + zend_throw_error(NULL, "Package specification must contain \"name\" key of type string"); + return; + } + + memset(&info, 0, sizeof(zend_package_info)); + if (declares_zv) { + if (Z_TYPE_P(declares_zv) != IS_ARRAY) { + zend_throw_error(NULL, "Package specification key \"declares\" must be of type array"); + return; + } + + if (init_package_declares(&info, Z_ARRVAL_P(declares_zv)) == FAILURE) { + return; + } + } + + if (!CG(packages)) { + ALLOC_HASHTABLE(CG(packages)); + zend_hash_init(CG(packages), 0, NULL, package_info_dtor, 0); + } + + info.name = zend_string_copy(Z_STR_P(name_zv)); + if (!zend_hash_add_mem(CG(packages), Z_STR_P(name_zv), &info, sizeof(zend_package_info))) { + zend_string_release(info.name); + zend_throw_error(NULL, "Package \"%s\" already registered", Z_STRVAL_P(name_zv)); + return; + } +} +/* }}} */ diff --git a/Zend/zend_compile.c b/Zend/zend_compile.c index cd04a01b0dea1..7ea3c885751a8 100644 --- a/Zend/zend_compile.c +++ b/Zend/zend_compile.c @@ -312,6 +312,11 @@ void zend_file_context_begin(zend_file_context *prev_context) /* {{{ */ FC(has_bracketed_namespaces) = 0; FC(declarables).ticks = 0; zend_hash_init(&FC(seen_symbols), 8, NULL, NULL, 0); + + /* This is not part of FC, because we want it to remain available after + * compilation, so it may be used by opcache. Nonetheless it needs to + * be reset when compiling a new file. */ + CG(current_package) = NULL; } /* }}} */ @@ -373,6 +378,7 @@ void init_compiler(void) /* {{{ */ CG(delayed_variance_obligations) = NULL; CG(delayed_autoloads) = NULL; + CG(packages) = NULL; } /* }}} */ @@ -393,6 +399,11 @@ void shutdown_compiler(void) /* {{{ */ FREE_HASHTABLE(CG(delayed_autoloads)); CG(delayed_autoloads) = NULL; } + if (CG(packages)) { + zend_hash_destroy(CG(packages)); + FREE_HASHTABLE(CG(packages)); + CG(packages) = NULL; + } } /* }}} */ @@ -2154,7 +2165,8 @@ static inline zend_bool zend_is_unticked_stmt(zend_ast *ast) /* {{{ */ { return ast->kind == ZEND_AST_STMT_LIST || ast->kind == ZEND_AST_LABEL || ast->kind == ZEND_AST_PROP_DECL || ast->kind == ZEND_AST_CLASS_CONST_DECL - || ast->kind == ZEND_AST_USE_TRAIT || ast->kind == ZEND_AST_METHOD; + || ast->kind == ZEND_AST_USE_TRAIT || ast->kind == ZEND_AST_METHOD + || ast->kind == ZEND_AST_PACKAGE; } /* }}} */ @@ -5124,8 +5136,9 @@ static int zend_declare_is_first_statement(zend_ast *ast) /* {{{ */ } else if (file_ast->child[i] == NULL) { /* Empty statements are not allowed prior to a declare */ return FAILURE; - } else if (file_ast->child[i]->kind != ZEND_AST_DECLARE) { - /* declares can only be preceded by other declares */ + } else if (file_ast->child[i]->kind != ZEND_AST_DECLARE + && file_ast->child[i]->kind != ZEND_AST_PACKAGE) { + /* declares can only be preceded by other declares or package declarations */ return FAILURE; } i++; @@ -5183,8 +5196,9 @@ void zend_compile_declare(zend_ast *ast) /* {{{ */ if (Z_LVAL(value_zv) == 1) { CG(active_op_array)->fn_flags |= ZEND_ACC_STRICT_TYPES; + } else { + CG(active_op_array)->fn_flags &= ~ZEND_ACC_STRICT_TYPES; } - } else { zend_error(E_COMPILE_WARNING, "Unsupported declare '%s'", ZSTR_VAL(name)); } @@ -6706,6 +6720,29 @@ void zend_compile_namespace(zend_ast *ast) /* {{{ */ } /* }}} */ +static void zend_compile_package(zend_ast *ast) /* {{{ */ +{ + zend_string *name = zend_ast_get_str(ast->child[0]); + zend_package_info *info = CG(packages) ? zend_hash_find_ptr(CG(packages), name) : NULL; + + if (zend_ast_get_list(CG(ast))->child[0] != ast) { + zend_error(E_COMPILE_ERROR, + "Package declaration must be the first statement in the script"); + } + + if (!info) { + zend_error(E_COMPILE_ERROR, "Unknown package \"%s\"", ZSTR_VAL(name)); + } + + CG(current_package) = info; + + FC(declarables).ticks = info->declares.ticks; + if (info->declares.strict_types) { + CG(active_op_array)->fn_flags |= ZEND_ACC_STRICT_TYPES; + } +} +/* }}} */ + void zend_compile_halt_compiler(zend_ast *ast) /* {{{ */ { zend_ast *offset_ast = ast->child[0]; @@ -8365,6 +8402,9 @@ void zend_compile_stmt(zend_ast *ast) /* {{{ */ case ZEND_AST_NAMESPACE: zend_compile_namespace(ast); break; + case ZEND_AST_PACKAGE: + zend_compile_package(ast); + break; case ZEND_AST_HALT_COMPILER: zend_compile_halt_compiler(ast); break; diff --git a/Zend/zend_compile.h b/Zend/zend_compile.h index 7b8a0cc2b8c6c..02e0fcdf8997f 100644 --- a/Zend/zend_compile.h +++ b/Zend/zend_compile.h @@ -497,6 +497,14 @@ struct _zend_execute_data { void **run_time_cache; /* cache op_array->run_time_cache */ }; +typedef struct { + zend_string *name; + struct { + zend_long ticks; + int strict_types : 1; + } declares; +} zend_package_info; + #define ZEND_CALL_HAS_THIS IS_OBJECT_EX /* Top 16 bits of Z_TYPE_INFO(EX(This)) are used as call_info flags */ diff --git a/Zend/zend_globals.h b/Zend/zend_globals.h index 62ff68b87799f..01bba79a6436c 100644 --- a/Zend/zend_globals.h +++ b/Zend/zend_globals.h @@ -127,6 +127,10 @@ struct _zend_compiler_globals { HashTable *delayed_variance_obligations; HashTable *delayed_autoloads; + + HashTable *packages; + /* Available after compilation for use by opcache */ + zend_package_info *current_package; }; diff --git a/Zend/zend_language_parser.y b/Zend/zend_language_parser.y index 99f20941d6143..b714be30a5b1a 100644 --- a/Zend/zend_language_parser.y +++ b/Zend/zend_language_parser.y @@ -197,6 +197,7 @@ static YYSIZE_T zend_yytnamerr(char*, const char*); %token T_LIST "list (T_LIST)" %token T_ARRAY "array (T_ARRAY)" %token T_CALLABLE "callable (T_CALLABLE)" +%token T_PACKAGE "package (T_PACKAGE)" %token T_LINE "__LINE__ (T_LINE)" %token T_FILE "__FILE__ (T_FILE)" %token T_DIR "__DIR__ (T_DIR)" @@ -334,6 +335,10 @@ top_statement: | T_USE use_declarations ';' { $$ = $2; $$->attr = ZEND_SYMBOL_CLASS; } | T_USE use_type use_declarations ';' { $$ = $3; $$->attr = $2; } | T_CONST const_list ';' { $$ = $2; } + | T_PACKAGE T_CONSTANT_ENCAPSED_STRING ';' + { $$ = zend_ast_create(ZEND_AST_PACKAGE, $2); } + | T_PACKAGE '(' T_CONSTANT_ENCAPSED_STRING ')' ';' + { $$ = zend_ast_create(ZEND_AST_PACKAGE, $3); } ; use_type: diff --git a/Zend/zend_language_scanner.l b/Zend/zend_language_scanner.l index bbb6602372720..6a6779d769ac2 100644 --- a/Zend/zend_language_scanner.l +++ b/Zend/zend_language_scanner.l @@ -1607,6 +1607,11 @@ NEWLINE ("\r"|"\n"|"\r\n") RETURN_TOKEN(T_CALLABLE); } +"package" { + /* TODO: Implement this as a contextually-reserved keyword instead */ + RETURN_TOKEN(T_PACKAGE); +} + "++" { RETURN_TOKEN(T_INC); } diff --git a/ext/opcache/ZendAccelerator.c b/ext/opcache/ZendAccelerator.c index 09d9dc78719dd..ac2deb002f03a 100644 --- a/ext/opcache/ZendAccelerator.c +++ b/ext/opcache/ZendAccelerator.c @@ -1770,6 +1770,9 @@ static zend_persistent_script *opcache_compile_file(zend_file_handle *file_handl zend_build_delayed_early_binding_list(op_array) : (uint32_t)-1; + /* Remember the package the script is part of. */ + new_persistent_script->package = CG(current_package); + efree(op_array); /* we have valid persistent_script, so it's safe to free op_array */ /* Fill in the ping_auto_globals_mask for the new script. If jit for auto globals is enabled we @@ -1893,7 +1896,6 @@ int check_persistent_script_access(zend_persistent_script *persistent_script) } } - /* zend_compile() replacement */ zend_op_array *persistent_compile_file(zend_file_handle *file_handle, int type) { @@ -2079,6 +2081,24 @@ zend_op_array *persistent_compile_file(zend_file_handle *file_handle, int type) } } + /* If this script is part of a package, make sure the package declaration did not change. */ + if (persistent_script && persistent_script->package && + !zend_accel_check_package_consistency(persistent_script->package)) { + zend_shared_alloc_lock(); + if (!persistent_script->corrupted) { + persistent_script->corrupted = 1; + persistent_script->timestamp = 0; + ZSMMG(wasted_shared_memory) += persistent_script->dynamic_members.memory_consumption; + if (ZSMMG(memory_exhausted)) { + zend_accel_restart_reason reason = + zend_accel_hash_is_full(&ZCSG(hash)) ? ACCEL_RESTART_HASH : ACCEL_RESTART_OOM; + zend_accel_schedule_restart_if_necessary(reason); + } + } + zend_shared_alloc_unlock(); + persistent_script = NULL; + } + /* Check the second level cache */ if (!persistent_script && ZCG(accel_directives).file_cache) { persistent_script = zend_file_cache_script_load(file_handle); diff --git a/ext/opcache/ZendAccelerator.h b/ext/opcache/ZendAccelerator.h index 0f31c65182852..d1c8978a1a3b1 100644 --- a/ext/opcache/ZendAccelerator.h +++ b/ext/opcache/ZendAccelerator.h @@ -111,6 +111,7 @@ typedef enum _zend_accel_restart_reason { typedef struct _zend_persistent_script { zend_script script; + zend_package_info *package; zend_long compiler_halt_offset; /* position of __HALT_COMPILER or -1 */ int ping_auto_globals_mask; /* which autoglobals are used by the script */ accel_time_t timestamp; /* the script modification time */ diff --git a/ext/opcache/tests/packages_invalidation.phpt b/ext/opcache/tests/packages_invalidation.phpt new file mode 100644 index 0000000000000..2fd99768e4d1f --- /dev/null +++ b/ext/opcache/tests/packages_invalidation.phpt @@ -0,0 +1,33 @@ +--TEST-- +Files are invalidated if the package definition changes +--SKIPIF-- + +--CONFLICTS-- +server +--FILE-- + +--EXPECT-- +strlen() expects parameter 1 to be string, int given +Misses: 2 +strlen() expects parameter 1 to be string, int given +Misses: 2 +int(1) +Misses: 4 +int(1) +Misses: 4 +strlen() expects parameter 1 to be string, int given +Misses: 5 +strlen() expects parameter 1 to be string, int given +Misses: 5 diff --git a/ext/opcache/tests/packages_invalidation_file_cache.phpt b/ext/opcache/tests/packages_invalidation_file_cache.phpt new file mode 100644 index 0000000000000..3250fa14e7741 --- /dev/null +++ b/ext/opcache/tests/packages_invalidation_file_cache.phpt @@ -0,0 +1,37 @@ +--TEST-- +Files are invalidated if the package definition changes (with file cache) +--SKIPIF-- + +--CONFLICTS-- +server +--INI-- +opcache.file_cache={TMP} +opcache.file_cache_only=0 +opcache.jit_buffer_size=0 +--FILE-- + +--EXPECTF-- +strlen() expects parameter 1 to be string, int given +Misses: %d +strlen() expects parameter 1 to be string, int given +Misses: %d +int(1) +Misses: %d +int(1) +Misses: %d +strlen() expects parameter 1 to be string, int given +Misses: %d +strlen() expects parameter 1 to be string, int given +Misses: %d diff --git a/ext/opcache/tests/packages_invalidation_file_cache_only.phpt b/ext/opcache/tests/packages_invalidation_file_cache_only.phpt new file mode 100644 index 0000000000000..2790ca5d58f1f --- /dev/null +++ b/ext/opcache/tests/packages_invalidation_file_cache_only.phpt @@ -0,0 +1,36 @@ +--TEST-- +Files are invalidated if the package definition changes (with file cache only) +--SKIPIF-- + +--CONFLICTS-- +server +--INI-- +opcache.file_cache={TMP} +opcache.file_cache_only=1 +--FILE-- + +--EXPECT-- +strlen() expects parameter 1 to be string, int given +Misses: - +strlen() expects parameter 1 to be string, int given +Misses: - +int(1) +Misses: - +int(1) +Misses: - +strlen() expects parameter 1 to be string, int given +Misses: - +strlen() expects parameter 1 to be string, int given +Misses: - diff --git a/ext/opcache/tests/packages_invalidation_use_pkg.php b/ext/opcache/tests/packages_invalidation_use_pkg.php new file mode 100644 index 0000000000000..909610ad91d53 --- /dev/null +++ b/ext/opcache/tests/packages_invalidation_use_pkg.php @@ -0,0 +1,9 @@ +getMessage(), "\n"; +} diff --git a/ext/opcache/tests/packages_invalidation_with_strict_types.php b/ext/opcache/tests/packages_invalidation_with_strict_types.php new file mode 100644 index 0000000000000..821a6eec55672 --- /dev/null +++ b/ext/opcache/tests/packages_invalidation_with_strict_types.php @@ -0,0 +1,9 @@ + "pkg", + "declares" => ["strict_types" => 1], +]); + +require __DIR__ . '/packages_invalidation_use_pkg.php'; +echo "Misses: ", opcache_get_status()["opcache_statistics"]["misses"] ?? "-", "\n"; diff --git a/ext/opcache/tests/packages_invalidation_without_strict_types.php b/ext/opcache/tests/packages_invalidation_without_strict_types.php new file mode 100644 index 0000000000000..6c68bc13a0b6a --- /dev/null +++ b/ext/opcache/tests/packages_invalidation_without_strict_types.php @@ -0,0 +1,9 @@ + "pkg", + "declares" => ["strict_types" => 0], +]); + +require __DIR__ . '/packages_invalidation_use_pkg.php'; +echo "Misses: ", opcache_get_status()["opcache_statistics"]["misses"] ?? "-", "\n"; diff --git a/ext/opcache/zend_accelerator_util_funcs.c b/ext/opcache/zend_accelerator_util_funcs.c index be927f6447986..8c0e455dc8760 100644 --- a/ext/opcache/zend_accelerator_util_funcs.c +++ b/ext/opcache/zend_accelerator_util_funcs.c @@ -896,3 +896,16 @@ unsigned int zend_accel_script_checksum(zend_persistent_script *persistent_scrip } return checksum; } + +zend_bool zend_accel_check_package_consistency(zend_package_info *cached_package) +{ + zend_package_info *info = + CG(packages) ? zend_hash_find_ptr(CG(packages), cached_package->name) : NULL; + if (!info) { + /* Package not registered, force a recompilation that will throw an error. */ + return 0; + } + + /* Make sure that the package declares are the same. */ + return memcmp(&info->declares, &cached_package->declares, sizeof(info->declares)) == 0; +} diff --git a/ext/opcache/zend_accelerator_util_funcs.h b/ext/opcache/zend_accelerator_util_funcs.h index 30531fc2e7f54..51da40bd4e489 100644 --- a/ext/opcache/zend_accelerator_util_funcs.h +++ b/ext/opcache/zend_accelerator_util_funcs.h @@ -38,5 +38,6 @@ zend_op_array* zend_accel_load_script(zend_persistent_script *persistent_script, unsigned int zend_adler32(unsigned int checksum, signed char *buf, uint32_t len); unsigned int zend_accel_script_checksum(zend_persistent_script *persistent_script); +zend_bool zend_accel_check_package_consistency(zend_package_info *cached_package); #endif /* ZEND_ACCELERATOR_UTIL_FUNCS_H */ diff --git a/ext/opcache/zend_file_cache.c b/ext/opcache/zend_file_cache.c index 663f511bc17c8..0ee154388e144 100644 --- a/ext/opcache/zend_file_cache.c +++ b/ext/opcache/zend_file_cache.c @@ -794,6 +794,18 @@ static void zend_file_cache_serialize_class(zval *zv, ZEND_MAP_PTR_INIT(ce->static_members_table, &ce->default_static_members_table); } +static void zend_file_cache_serialize_package_info( + zend_persistent_script *script, zend_file_cache_metainfo *info, void *buf) +{ + if (script->package) { + zend_package_info *package; + SERIALIZE_PTR(script->package); + package = script->package; + UNSERIALIZE_PTR(package); + SERIALIZE_STR(package->name); + } +} + static void zend_file_cache_serialize(zend_persistent_script *script, zend_file_cache_metainfo *info, void *buf) @@ -815,6 +827,7 @@ static void zend_file_cache_serialize(zend_persistent_script *script, zend_file_cache_serialize_hash(&new_script->script.class_table, script, info, buf, zend_file_cache_serialize_class); zend_file_cache_serialize_hash(&new_script->script.function_table, script, info, buf, zend_file_cache_serialize_func); zend_file_cache_serialize_op_array(&new_script->script.main_op_array, script, info, buf); + zend_file_cache_serialize_package_info(new_script, info, buf); SERIALIZE_PTR(new_script->arena_mem); new_script->mem = NULL; @@ -1498,9 +1511,24 @@ static void zend_file_cache_unserialize(zend_persistent_script *script, script, buf, zend_file_cache_unserialize_func, ZEND_FUNCTION_DTOR); zend_file_cache_unserialize_op_array(&script->script.main_op_array, script, buf); + if (script->package) { + UNSERIALIZE_PTR(script->package); + UNSERIALIZE_STR(script->package->name); + } + UNSERIALIZE_PTR(script->arena_mem); } +static zend_bool zend_file_cache_check_package_consistency( + zend_package_info *serialized_info, char *mem, char *str_mem) { + zend_package_info unserialized_info; + serialized_info = (zend_package_info *) (mem + (size_t) serialized_info); + memcpy(&unserialized_info, serialized_info, sizeof(zend_package_info)); + /* We only need the string temporarily for a HT lookup, use it in the raw form. */ + unserialized_info.name = (zend_string *) (mem + (size_t)(serialized_info->name)); + return zend_accel_check_package_consistency(&unserialized_info); +} + zend_persistent_script *zend_file_cache_script_load(zend_file_handle *file_handle) { zend_string *full_path = file_handle->opened_path; @@ -1602,6 +1630,21 @@ zend_persistent_script *zend_file_cache_script_load(zend_file_handle *file_handl return NULL; } + { + /* Peek at the package info of the script and make sure the package declaration + * didn't change. If it has, discard the current cache. */ + zend_persistent_script *tmp_script = + (zend_persistent_script *) ((char *) mem + info.script_offset); + char *str_mem = (char *) mem + info.mem_size; + if (tmp_script->package && + !zend_file_cache_check_package_consistency(tmp_script->package, mem, str_mem)) { + zend_file_cache_unlink(filename); + zend_arena_release(&CG(arena), checkpoint); + efree(filename); + return NULL; + } + } + if (!file_cache_only && !ZCSG(restart_in_progress) && !ZSMMG(memory_exhausted) && diff --git a/ext/opcache/zend_persist.c b/ext/opcache/zend_persist.c index 0b9d127fe5c04..8a4810d05095f 100644 --- a/ext/opcache/zend_persist.c +++ b/ext/opcache/zend_persist.c @@ -1047,6 +1047,13 @@ static void zend_accel_persist_class_table(HashTable *class_table) } ZEND_HASH_FOREACH_END(); } +static void zend_persist_package(zend_package_info **info) { + if (*info) { + *info = zend_shared_memdup(*info, sizeof(zend_package_info)); + zend_accel_store_interned_string((*info)->name); + } +} + zend_persistent_script *zend_accel_script_persist(zend_persistent_script *script, const char **key, unsigned int key_length, int for_shm) { Bucket *p; @@ -1096,6 +1103,7 @@ zend_persistent_script *zend_accel_script_persist(zend_persistent_script *script zend_persist_op_array(&p->val); } ZEND_HASH_FOREACH_END(); zend_persist_op_array_ex(&script->script.main_op_array, script); + zend_persist_package(&script->package); ZCSG(map_ptr_last) = CG(map_ptr_last); diff --git a/ext/opcache/zend_persist_calc.c b/ext/opcache/zend_persist_calc.c index cc798b27de050..a3904ab96eab6 100644 --- a/ext/opcache/zend_persist_calc.c +++ b/ext/opcache/zend_persist_calc.c @@ -492,6 +492,13 @@ static void zend_accel_persist_class_table_calc(HashTable *class_table) } ZEND_HASH_FOREACH_END(); } +static void zend_persist_package_calc(zend_package_info *info) { + if (info) { + ADD_SIZE(sizeof(zend_package_info)); + ADD_INTERNED_STRING(info->name); + } +} + uint32_t zend_accel_script_persist_calc(zend_persistent_script *new_persistent_script, const char *key, unsigned int key_length, int for_shm) { Bucket *p; @@ -534,6 +541,7 @@ uint32_t zend_accel_script_persist_calc(zend_persistent_script *new_persistent_s zend_persist_op_array_calc(&p->val); } ZEND_HASH_FOREACH_END(); zend_persist_op_array_calc_ex(&new_persistent_script->script.main_op_array); + zend_persist_package_calc(new_persistent_script->package); #if defined(__AVX__) || defined(__SSE2__) /* Align size to 64-byte boundary */