diff --git a/Zend/Optimizer/zend_func_infos.h b/Zend/Optimizer/zend_func_infos.h index b7b118c710c5..11a115fcdb5c 100644 --- a/Zend/Optimizer/zend_func_infos.h +++ b/Zend/Optimizer/zend_func_infos.h @@ -288,6 +288,10 @@ static const func_info_t func_infos[] = { F1("mysqli_use_result", MAY_BE_OBJECT|MAY_BE_FALSE), F1("opcache_get_status", MAY_BE_ARRAY|MAY_BE_ARRAY_KEY_STRING|MAY_BE_ARRAY_OF_ANY|MAY_BE_FALSE), F1("opcache_get_configuration", MAY_BE_ARRAY|MAY_BE_ARRAY_KEY_STRING|MAY_BE_ARRAY_OF_ANY|MAY_BE_FALSE), + FN("OPcache\\volatile_fetch_array", MAY_BE_ARRAY|MAY_BE_ARRAY_KEY_STRING|MAY_BE_ARRAY_OF_NULL|MAY_BE_ARRAY_OF_FALSE|MAY_BE_ARRAY_OF_TRUE|MAY_BE_ARRAY_OF_LONG|MAY_BE_ARRAY_OF_DOUBLE|MAY_BE_ARRAY_OF_STRING|MAY_BE_ARRAY_OF_ARRAY|MAY_BE_ARRAY_OF_OBJECT|MAY_BE_NULL), + FN("OPcache\\volatile_cache_info", MAY_BE_ARRAY|MAY_BE_ARRAY_KEY_STRING|MAY_BE_ARRAY_OF_ANY), + FN("OPcache\\persistent_fetch_array", MAY_BE_ARRAY|MAY_BE_ARRAY_KEY_STRING|MAY_BE_ARRAY_OF_NULL|MAY_BE_ARRAY_OF_FALSE|MAY_BE_ARRAY_OF_TRUE|MAY_BE_ARRAY_OF_LONG|MAY_BE_ARRAY_OF_DOUBLE|MAY_BE_ARRAY_OF_STRING|MAY_BE_ARRAY_OF_ARRAY|MAY_BE_ARRAY_OF_OBJECT|MAY_BE_NULL), + FN("OPcache\\persistent_cache_info", MAY_BE_ARRAY|MAY_BE_ARRAY_KEY_STRING|MAY_BE_ARRAY_OF_ANY), F1("openssl_x509_parse", MAY_BE_ARRAY|MAY_BE_ARRAY_KEY_STRING|MAY_BE_ARRAY_OF_LONG|MAY_BE_ARRAY_OF_STRING|MAY_BE_ARRAY_OF_ARRAY|MAY_BE_FALSE), F1("openssl_csr_get_subject", MAY_BE_ARRAY|MAY_BE_ARRAY_KEY_STRING|MAY_BE_ARRAY_OF_STRING|MAY_BE_ARRAY_OF_ARRAY|MAY_BE_FALSE), F1("openssl_pkey_get_details", MAY_BE_ARRAY|MAY_BE_ARRAY_KEY_STRING|MAY_BE_ARRAY_OF_LONG|MAY_BE_ARRAY_OF_STRING|MAY_BE_ARRAY_OF_ARRAY|MAY_BE_FALSE), diff --git a/Zend/tests/assign_obj_ref_track_mutation_order.phpt b/Zend/tests/assign_obj_ref_track_mutation_order.phpt new file mode 100644 index 000000000000..a36db4c09dca --- /dev/null +++ b/Zend/tests/assign_obj_ref_track_mutation_order.phpt @@ -0,0 +1,85 @@ +--TEST-- +ASSIGN_OBJ_REF tracks object mutations before freeing operands +--SKIPIF-- + +--FILE-- +op1.var));', + 'zval_ptr_dtor_nogc(EX_VAR(opline->op2.var));', + 'zval_ptr_dtor_nogc(EX_VAR((opline+1)->op1.var));', + ]; + $freePositions = []; + + foreach ($freeNeedles as $needle) { + $pos = strpos($section, $needle); + if ($pos !== false) { + $freePositions[] = $pos; + } + } + + if ($trackPos === false) { + throw new RuntimeException($label . ' is missing the expected ASSIGN_OBJ_REF sequence'); + } + + if ($freePositions === []) { + return false; + } + + if ($trackPos > min($freePositions)) { + throw new RuntimeException($label . ' tracks object mutation after freeing operands'); + } + + return true; +} + +$zendDir = dirname(__DIR__); + +$vmDef = file_get_contents($zendDir . '/zend_vm_def.h'); +$defStart = strpos($vmDef, 'ZEND_VM_HANDLER(32, ZEND_ASSIGN_OBJ_REF'); +$defEnd = strpos($vmDef, 'ZEND_VM_HANDLER(33, ZEND_ASSIGN_STATIC_PROP_REF'); +if ($defStart === false || $defEnd === false) { + throw new RuntimeException('Unable to locate ZEND_ASSIGN_OBJ_REF in zend_vm_def.h'); +} + +if (!assertAssignObjRefTrackingOrder(substr($vmDef, $defStart, $defEnd - $defStart), 'zend_vm_def.h')) { + throw new RuntimeException('zend_vm_def.h ASSIGN_OBJ_REF unexpectedly has no operand frees'); +} + +$vmExecute = file_get_contents($zendDir . '/zend_vm_execute.h'); +$sections = preg_split('/(?=static ZEND_OPCODE_HANDLER_RET )/', $vmExecute); +$checked = 0; + +foreach ($sections as $section) { + $headerEnd = strpos($section, "\n"); + $label = $headerEnd === false ? 'zend_vm_execute.h ASSIGN_OBJ_REF handler' : trim(substr($section, 0, $headerEnd)); + if (!preg_match('/\bZEND_ASSIGN_OBJ_REF_[A-Z0-9_]+_HANDLER\b/', $label)) { + continue; + } + + if (assertAssignObjRefTrackingOrder($section, $label)) { + $checked++; + } +} + +if ($checked === 0) { + throw new RuntimeException('Unable to locate generated ZEND_ASSIGN_OBJ_REF handlers in zend_vm_execute.h'); +} + +echo "OK\n"; + +?> +--EXPECT-- +OK diff --git a/Zend/tests/tracked_mutation_hook_safety.phpt b/Zend/tests/tracked_mutation_hook_safety.phpt new file mode 100644 index 000000000000..079cb7d538dc --- /dev/null +++ b/Zend/tests/tracked_mutation_hook_safety.phpt @@ -0,0 +1,78 @@ +--TEST-- +Tracked mutation hooks are called through defensive helpers +--SKIPIF-- + +--FILE-- + +--EXPECT-- +OK diff --git a/Zend/zend.c b/Zend/zend.c index f83389a96a6e..96ded250e0b8 100644 --- a/Zend/zend.c +++ b/Zend/zend.c @@ -96,6 +96,13 @@ ZEND_API zend_string *(*zend_resolve_path)(zend_string *filename); ZEND_API zend_result (*zend_post_startup_cb)(void) = NULL; ZEND_API void (*zend_post_shutdown_cb)(void) = NULL; ZEND_API void (*zend_accel_schedule_restart_hook)(int reason) = NULL; +ZEND_API void (*zend_class_init_statics_hook)(zend_class_entry *ce) = NULL; +ZEND_API void (*zend_function_init_statics_hook)(zend_execute_data *execute_data) = NULL; +ZEND_API void (*zend_class_static_access_hook)(zend_class_entry *ce) = NULL; +ZEND_API void (*zend_class_static_update_hook)(zend_class_entry *ce) = NULL; +ZEND_API void (*zend_tracked_reference_update_hook)(zend_reference *ref) = NULL; +ZEND_API bool (*zend_tracked_hash_mutation_hook)(HashTable *ht, bool publish) = NULL; +ZEND_API void (*zend_tracked_object_mutation_hook)(zend_object *obj) = NULL; ZEND_ATTRIBUTE_NONNULL ZEND_API zend_result (*zend_random_bytes)(void *bytes, size_t size, char *errstr, size_t errstr_size) = NULL; ZEND_ATTRIBUTE_NONNULL ZEND_API void (*zend_random_bytes_insecure)(zend_random_bytes_insecure_state *state, void *bytes, size_t size) = NULL; @@ -819,6 +826,8 @@ static void executor_globals_ctor(zend_executor_globals *executor_globals) /* {{ #endif executor_globals->saved_fpu_cw_ptr = NULL; executor_globals->active = false; + executor_globals->static_cache_class_access_active = false; + executor_globals->tracked_mutation_hooks_active = false; executor_globals->bailout = NULL; executor_globals->error_handling = EH_NORMAL; executor_globals->exception_class = NULL; diff --git a/Zend/zend.h b/Zend/zend.h index 0d5303192b57..e2cd40c72fd9 100644 --- a/Zend/zend.h +++ b/Zend/zend.h @@ -386,6 +386,17 @@ extern ZEND_API void (*zend_post_shutdown_cb)(void); extern ZEND_API void (*zend_accel_schedule_restart_hook)(int reason); +/* These hooks are used by OPcache Static Cache to restore, publish, and track + * selected VolatileStatic and PersistentStatic state across requests. They remain + * NULL when the static-cache subsystem is not active. */ +extern ZEND_API void (*zend_class_init_statics_hook)(zend_class_entry *ce); +extern ZEND_API void (*zend_function_init_statics_hook)(zend_execute_data *execute_data); +extern ZEND_API void (*zend_class_static_access_hook)(zend_class_entry *ce); +extern ZEND_API void (*zend_class_static_update_hook)(zend_class_entry *ce); +extern ZEND_API void (*zend_tracked_reference_update_hook)(zend_reference *ref); +extern ZEND_API bool (*zend_tracked_hash_mutation_hook)(HashTable *ht, bool publish); +extern ZEND_API void (*zend_tracked_object_mutation_hook)(zend_object *obj); + ZEND_API ZEND_COLD void zend_error(int type, const char *format, ...) ZEND_ATTRIBUTE_FORMAT(printf, 2, 3); ZEND_API ZEND_COLD ZEND_NORETURN void zend_error_noreturn(int type, const char *format, ...) ZEND_ATTRIBUTE_FORMAT(printf, 2, 3); ZEND_API ZEND_COLD ZEND_NORETURN void zend_error_noreturn_unchecked(int type, const char *format, ...); diff --git a/Zend/zend_execute.c b/Zend/zend_execute.c index 4253037fda52..bd06a048214e 100644 --- a/Zend/zend_execute.c +++ b/Zend/zend_execute.c @@ -2908,6 +2908,11 @@ static zend_always_inline void zend_fetch_dimension_address(zval *result, zval * if (EXPECTED(Z_TYPE_P(container) == IS_ARRAY)) { try_array: + if (UNEXPECTED((type == BP_VAR_W || type == BP_VAR_RW || type == BP_VAR_UNSET) && + EG(tracked_mutation_hooks_active)) + ) { + zend_maybe_track_hash_mutation(Z_ARRVAL_P(container), false); + } SEPARATE_ARRAY(container); fetch_from_array: if (dim == NULL) { @@ -3507,6 +3512,20 @@ static zend_never_inline bool zend_handle_fetch_obj_flags( return 1; } +static zend_always_inline void zend_track_property_array_indirect_mutation(zval *ptr, int type) +{ + zval *tracked = ptr; + + if (UNEXPECTED((type == BP_VAR_W || type == BP_VAR_RW || type == BP_VAR_UNSET) && + EG(tracked_mutation_hooks_active)) + ) { + ZVAL_DEREF(tracked); + if (Z_TYPE_P(tracked) == IS_ARRAY) { + zend_maybe_track_hash_mutation(Z_ARRVAL_P(tracked), false); + } + } +} + static zend_always_inline void zend_fetch_property_address( zval *result, const zval *container, @@ -3591,6 +3610,7 @@ static zend_always_inline void zend_fetch_property_address( zend_handle_fetch_obj_flags(result, ptr, NULL, prop_info, flags); } } + zend_track_property_array_indirect_mutation(ptr, type); return; } } else if (UNEXPECTED(IS_HOOKED_PROPERTY_OFFSET(prop_offset))) { @@ -3606,6 +3626,7 @@ static zend_always_inline void zend_fetch_property_address( ptr = zend_hash_find_known_hash(zobj->properties, Z_STR_P(prop_ptr)); if (EXPECTED(ptr)) { ZVAL_INDIRECT(result, ptr); + zend_track_property_array_indirect_mutation(ptr, type); return; } } @@ -3653,6 +3674,7 @@ static zend_always_inline void zend_fetch_property_address( } } } + zend_track_property_array_indirect_mutation(ptr, type); end: if (prop_info_p) { @@ -3828,6 +3850,12 @@ static zend_always_inline zval* zend_fetch_static_property_address(zend_property result = CACHED_PTR(cache_slot + sizeof(void *)); property_info = CACHED_PTR(cache_slot + sizeof(void *) * 2); + if (UNEXPECTED(EG(static_cache_class_access_active) && + zend_class_static_access_hook != NULL) + ) { + zend_class_static_access_hook(property_info->ce); + } + if ((fetch_type == BP_VAR_R || fetch_type == BP_VAR_RW) && UNEXPECTED(Z_TYPE_P(result) == IS_UNDEF) && ZEND_TYPE_IS_SET(property_info->type)) { @@ -4102,6 +4130,20 @@ ZEND_API zval* zend_assign_to_typed_ref(zval *variable_ptr, zval *orig_value, ui return result; } +zend_never_inline ZEND_COLD void ZEND_FASTCALL zend_track_object_mutation_slow(zend_object *zobj) +{ + if (zobj != NULL && zend_tracked_object_mutation_hook != NULL && EG(exception) == NULL) { + zend_tracked_object_mutation_hook(zobj); + } +} + +zend_never_inline ZEND_COLD void ZEND_FASTCALL zend_track_object_mutation_with_value_slow(zend_object *zobj, zval *value) +{ + if (zobj != NULL && zend_tracked_object_mutation_hook != NULL && EG(exception) == NULL && value != &EG(error_zval)) { + zend_tracked_object_mutation_hook(zobj); + } +} + ZEND_API bool ZEND_FASTCALL zend_verify_prop_assignable_by_ref_ex(const zend_property_info *prop_info, zval *orig_val, bool strict, zend_verify_prop_assignable_by_ref_context context) { zval *val = orig_val; if (Z_ISREF_P(val) && ZEND_REF_HAS_TYPE_SOURCES(Z_REF_P(val))) { diff --git a/Zend/zend_execute.h b/Zend/zend_execute.h index ba48b19bcfe1..cbbb9ba60a18 100644 --- a/Zend/zend_execute.h +++ b/Zend/zend_execute.h @@ -127,13 +127,36 @@ ZEND_API bool zend_verify_internal_return_type(const zend_function *zf, zval *re ? ZEND_PROPERTY_INFO_SOURCE_TO_LIST((ref)->sources.list)->ptr[0] \ : (ref)->sources.ptr) - ZEND_API void ZEND_FASTCALL zend_ref_add_type_source(zend_property_info_source_list *source_list, zend_property_info *prop); ZEND_API void ZEND_FASTCALL zend_ref_del_type_source(zend_property_info_source_list *source_list, const zend_property_info *prop); ZEND_API zval* zend_assign_to_typed_ref(zval *variable_ptr, zval *value, uint8_t value_type, bool strict); ZEND_API zval* zend_assign_to_typed_ref_ex(zval *variable_ptr, zval *value, uint8_t value_type, bool strict, zend_refcounted **garbage_ptr); +zend_never_inline ZEND_COLD void ZEND_FASTCALL zend_track_object_mutation_slow(zend_object *zobj); +zend_never_inline ZEND_COLD void ZEND_FASTCALL zend_track_object_mutation_with_value_slow(zend_object *zobj, zval *value); + +#define ZEND_MAYBE_TRACK_OBJECT_MUTATION(zobj) do { \ + if (UNEXPECTED(EG(tracked_mutation_hooks_active))) { \ + zend_track_object_mutation_slow((zobj)); \ + } \ +} while (0) + +#define ZEND_MAYBE_TRACK_OBJECT_MUTATION_WITH_VALUE(zobj, value) do { \ + if (UNEXPECTED(EG(tracked_mutation_hooks_active))) { \ + zend_track_object_mutation_with_value_slow((zobj), (value)); \ + } \ +} while (0) + +static zend_always_inline bool zend_maybe_track_hash_mutation(HashTable *ht, bool publish) +{ + if (UNEXPECTED(zend_tracked_hash_mutation_hook != NULL)) { + return zend_tracked_hash_mutation_hook(ht, publish); + } + + return false; +} + static zend_always_inline void zend_copy_to_variable(zval *variable_ptr, const zval *value, uint8_t value_type) { zend_refcounted *ref = NULL; @@ -161,15 +184,27 @@ static zend_always_inline void zend_copy_to_variable(zval *variable_ptr, const z } } +static zend_always_inline void zend_maybe_track_reference_update(zend_reference *updated_ref) +{ + if (UNEXPECTED(updated_ref != NULL && zend_tracked_reference_update_hook != NULL && EG(exception) == NULL)) { + zend_tracked_reference_update_hook(updated_ref); + } +} + static zend_always_inline zval* zend_assign_to_variable(zval *variable_ptr, zval *value, uint8_t value_type, bool strict) { + zend_reference *updated_ref = NULL; + do { if (UNEXPECTED(Z_REFCOUNTED_P(variable_ptr))) { zend_refcounted *garbage; if (Z_ISREF_P(variable_ptr)) { + updated_ref = Z_REF_P(variable_ptr); if (UNEXPECTED(ZEND_REF_HAS_TYPE_SOURCES(Z_REF_P(variable_ptr)))) { - return zend_assign_to_typed_ref(variable_ptr, value, value_type, strict); + variable_ptr = zend_assign_to_typed_ref(variable_ptr, value, value_type, strict); + zend_maybe_track_reference_update(updated_ref); + return variable_ptr; } variable_ptr = Z_REFVAL_P(variable_ptr); @@ -179,22 +214,29 @@ static zend_always_inline zval* zend_assign_to_variable(zval *variable_ptr, zval } garbage = Z_COUNTED_P(variable_ptr); zend_copy_to_variable(variable_ptr, value, value_type); + zend_maybe_track_reference_update(updated_ref); GC_DTOR_NO_REF(garbage); return variable_ptr; } } while (0); zend_copy_to_variable(variable_ptr, value, value_type); + zend_maybe_track_reference_update(updated_ref); return variable_ptr; } static zend_always_inline zval* zend_assign_to_variable_ex(zval *variable_ptr, zval *value, zend_uchar value_type, bool strict, zend_refcounted **garbage_ptr) { + zend_reference *updated_ref = NULL; + do { if (UNEXPECTED(Z_REFCOUNTED_P(variable_ptr))) { if (Z_ISREF_P(variable_ptr)) { + updated_ref = Z_REF_P(variable_ptr); if (UNEXPECTED(ZEND_REF_HAS_TYPE_SOURCES(Z_REF_P(variable_ptr)))) { - return zend_assign_to_typed_ref_ex(variable_ptr, value, value_type, strict, garbage_ptr); + variable_ptr = zend_assign_to_typed_ref_ex(variable_ptr, value, value_type, strict, garbage_ptr); + zend_maybe_track_reference_update(updated_ref); + return variable_ptr; } variable_ptr = Z_REFVAL_P(variable_ptr); @@ -207,9 +249,20 @@ static zend_always_inline zval* zend_assign_to_variable_ex(zval *variable_ptr, z } while (0); zend_copy_to_variable(variable_ptr, value, value_type); + zend_maybe_track_reference_update(updated_ref); return variable_ptr; } +static zend_always_inline void zend_class_static_update(zend_class_entry *ce) +{ + if (UNEXPECTED(EG(static_cache_class_access_active) && + zend_class_static_update_hook != NULL && + EG(exception) == NULL) + ) { + zend_class_static_update_hook(ce); + } +} + static zend_always_inline void zend_safe_assign_to_variable_noref(zval *variable_ptr, const zval *value) { if (Z_REFCOUNTED_P(variable_ptr)) { ZEND_ASSERT(Z_TYPE_P(variable_ptr) != IS_REFERENCE); diff --git a/Zend/zend_globals.h b/Zend/zend_globals.h index 8257df32e831..bae276f93ca2 100644 --- a/Zend/zend_globals.h +++ b/Zend/zend_globals.h @@ -268,6 +268,10 @@ struct _zend_executor_globals { struct _zend_module_entry *current_module; bool active; + + bool static_cache_class_access_active; /* fast guard for zend_class_static_access_hook */ + bool tracked_mutation_hooks_active; /* fast guard for tracked array/object mutation hooks */ + uint8_t flags; zend_long assertions; diff --git a/Zend/zend_object_handlers.c b/Zend/zend_object_handlers.c index 571aa9e92abc..bfc37c672e14 100644 --- a/Zend/zend_object_handlers.c +++ b/Zend/zend_object_handlers.c @@ -2094,6 +2094,10 @@ ZEND_API void zend_class_init_statics(zend_class_entry *class_type) /* {{{ */ ZVAL_COPY_OR_DUP(&CE_STATIC_MEMBERS(class_type)[i], p); } } + + if (UNEXPECTED(zend_class_init_statics_hook != NULL)) { + zend_class_init_statics_hook(class_type); + } } } /* }}} */ @@ -2139,6 +2143,12 @@ ZEND_API zval *zend_std_get_static_property_with_info(zend_class_entry *ce, zend zend_class_init_statics(ce); } + if (UNEXPECTED(EG(static_cache_class_access_active) && + zend_class_static_access_hook != NULL) + ) { + zend_class_static_access_hook(ce); + } + ret = CE_STATIC_MEMBERS(ce) + property_info->offset; ZVAL_DEINDIRECT(ret); diff --git a/Zend/zend_portability.h b/Zend/zend_portability.h index c5b5fe02929f..b09432fe7831 100644 --- a/Zend/zend_portability.h +++ b/Zend/zend_portability.h @@ -424,6 +424,156 @@ char *alloca(); # endif #endif /* ZEND_DEBUG */ +#ifdef ZTS + +typedef void *(*zend_thread_routine_t)(void *arg); + +#ifdef ZEND_WIN32 + +#include +#include + +typedef struct _zend_thread { + HANDLE handle; + unsigned id; +} zend_thread_t; + +typedef SRWLOCK zend_thread_rwlock_t; + +typedef struct _zend_thread_start_ctx { + zend_thread_routine_t routine; + void *arg; +} zend_thread_start_ctx; + +static unsigned __stdcall zend_thread_trampoline(void *arg) +{ + zend_thread_start_ctx *ctx = (zend_thread_start_ctx *) arg; + zend_thread_routine_t routine = ctx->routine; + void *routine_arg = ctx->arg; + + routine(routine_arg); + free(ctx); + return 0; +} + +static zend_always_inline bool zend_thread_start(zend_thread_t *thread, zend_thread_routine_t routine, void *arg) +{ + zend_thread_start_ctx *ctx; + + ctx = malloc(sizeof(*ctx)); + if (ctx == NULL) { + return false; + } + + ctx->routine = routine; + ctx->arg = arg; + thread->handle = (HANDLE) _beginthreadex(NULL, 0, zend_thread_trampoline, ctx, 0, &thread->id); + if (thread->handle == NULL) { + free(ctx); + return false; + } + + return true; +} + +static zend_always_inline bool zend_thread_join(zend_thread_t *thread) +{ + DWORD wait_status; + + wait_status = WaitForSingleObject(thread->handle, INFINITE); + if (thread->handle != NULL) { + CloseHandle(thread->handle); + thread->handle = NULL; + } + + return wait_status == WAIT_OBJECT_0; +} + +static zend_always_inline bool zend_thread_rwlock_init(zend_thread_rwlock_t *lock) +{ + InitializeSRWLock(lock); + return true; +} + +static zend_always_inline bool zend_thread_rwlock_rdlock(zend_thread_rwlock_t *lock) +{ + AcquireSRWLockShared(lock); + return true; +} + +static zend_always_inline bool zend_thread_rwlock_wrlock(zend_thread_rwlock_t *lock) +{ + AcquireSRWLockExclusive(lock); + return true; +} + +static zend_always_inline bool zend_thread_rwlock_unlock_rd(zend_thread_rwlock_t *lock) +{ + ReleaseSRWLockShared(lock); + return true; +} + +static zend_always_inline bool zend_thread_rwlock_unlock_wr(zend_thread_rwlock_t *lock) +{ + ReleaseSRWLockExclusive(lock); + return true; +} + +static zend_always_inline void zend_thread_rwlock_destroy(zend_thread_rwlock_t *lock) +{ + ZEND_IGNORE_VALUE(lock); + return; +} + +#else + +typedef pthread_t zend_thread_t; +typedef pthread_rwlock_t zend_thread_rwlock_t; + +static zend_always_inline bool zend_thread_start(zend_thread_t *thread, zend_thread_routine_t routine, void *arg) +{ + return pthread_create(thread, NULL, routine, arg) == 0; +} + +static zend_always_inline bool zend_thread_join(zend_thread_t *thread) +{ + return pthread_join(*thread, NULL) == 0; +} + +static zend_always_inline bool zend_thread_rwlock_init(zend_thread_rwlock_t *lock) +{ + return pthread_rwlock_init(lock, NULL) == 0; +} + +static zend_always_inline bool zend_thread_rwlock_rdlock(zend_thread_rwlock_t *lock) +{ + return pthread_rwlock_rdlock(lock) == 0; +} + +static zend_always_inline bool zend_thread_rwlock_wrlock(zend_thread_rwlock_t *lock) +{ + return pthread_rwlock_wrlock(lock) == 0; +} + +static zend_always_inline bool zend_thread_rwlock_unlock_rd(zend_thread_rwlock_t *lock) +{ + return pthread_rwlock_unlock(lock) == 0; +} + +static zend_always_inline bool zend_thread_rwlock_unlock_wr(zend_thread_rwlock_t *lock) +{ + return pthread_rwlock_unlock(lock) == 0; +} + +static zend_always_inline void zend_thread_rwlock_destroy(zend_thread_rwlock_t *lock) +{ + pthread_rwlock_destroy(lock); +} + +#endif + +#endif + #ifdef PHP_HAVE_BUILTIN_EXPECT # define EXPECTED(condition) __builtin_expect(!!(condition), 1) # define UNEXPECTED(condition) __builtin_expect(!!(condition), 0) diff --git a/Zend/zend_vm_def.h b/Zend/zend_vm_def.h index 05923bbc2b61..83d9c1abbebf 100644 --- a/Zend/zend_vm_def.h +++ b/Zend/zend_vm_def.h @@ -1014,7 +1014,7 @@ ZEND_VM_HANDLER(28, ZEND_ASSIGN_OBJ_OP, VAR|UNUSED|THIS|CV, CONST|TMP|CV, OP) void *_cache_slot[3] = {0}; void **cache_slot; zend_property_info *prop_info; - zend_object *zobj; + zend_object *zobj = NULL; zend_string *name, *tmp_name; SAVE_OPLINE(); @@ -1089,6 +1089,8 @@ ZEND_VM_C_LABEL(assign_op_object): } } while (0); + ZEND_MAYBE_TRACK_OBJECT_MUTATION(zobj); + FREE_OP_DATA(); FREE_OP2(); FREE_OP1(); @@ -1148,6 +1150,7 @@ ZEND_VM_HANDLER(29, ZEND_ASSIGN_STATIC_PROP_OP, ANY, ANY, OP) } FREE_OP_DATA(); + zend_class_static_update(prop_info->ce); /* assign_static_prop has two opcodes! */ ZEND_VM_NEXT_OPCODE_EX(1, 2); } @@ -1158,12 +1161,16 @@ ZEND_VM_HANDLER(27, ZEND_ASSIGN_DIM_OP, VAR|CV, CONST|TMP|UNUSED|NEXT|CV, OP) zval *var_ptr; zval *value, *container, *dim; HashTable *ht; + bool should_publish_tracked_array = false; SAVE_OPLINE(); container = GET_OP1_OBJ_ZVAL_PTR_PTR_UNDEF(BP_VAR_RW); if (EXPECTED(Z_TYPE_P(container) == IS_ARRAY)) { ZEND_VM_C_LABEL(assign_dim_op_array): + if (UNEXPECTED(EG(tracked_mutation_hooks_active))) { + should_publish_tracked_array = zend_maybe_track_hash_mutation(Z_ARRVAL_P(container), false); + } SEPARATE_ARRAY(container); ht = Z_ARRVAL_P(container); ZEND_VM_C_LABEL(assign_dim_op_new_array): @@ -1203,6 +1210,9 @@ ZEND_VM_C_LABEL(assign_dim_op_new_array): ZVAL_COPY(EX_VAR(opline->result.var), var_ptr); } FREE_OP((opline+1)->op1_type, (opline+1)->op1.var); + if (UNEXPECTED(should_publish_tracked_array)) { + zend_maybe_track_hash_mutation(Z_ARRVAL_P(container), true); + } } else { if (EXPECTED(Z_ISREF_P(container))) { container = Z_REFVAL_P(container); @@ -1219,6 +1229,7 @@ ZEND_VM_C_LABEL(assign_dim_op_new_array): dim++; } zend_binary_assign_op_obj_dim(obj, dim OPLINE_CC EXECUTE_DATA_CC); + ZEND_MAYBE_TRACK_OBJECT_MUTATION(obj); } else if (EXPECTED(Z_TYPE_P(container) <= IS_FALSE)) { uint8_t old_type; @@ -1293,7 +1304,7 @@ ZEND_VM_HANDLER(132, ZEND_PRE_INC_OBJ, VAR|UNUSED|THIS|CV, CONST|TMP|CV, CACHE_S void *_cache_slot[3] = {0}; void **cache_slot; zend_property_info *prop_info; - zend_object *zobj; + zend_object *zobj = NULL; zend_string *name, *tmp_name; SAVE_OPLINE(); @@ -1345,6 +1356,8 @@ ZEND_VM_C_LABEL(pre_incdec_object): } } while (0); + ZEND_MAYBE_TRACK_OBJECT_MUTATION(zobj); + FREE_OP2(); FREE_OP1(); ZEND_VM_NEXT_OPCODE_CHECK_EXCEPTION(); @@ -1364,7 +1377,7 @@ ZEND_VM_HANDLER(134, ZEND_POST_INC_OBJ, VAR|UNUSED|THIS|CV, CONST|TMP|CV, CACHE_ void *_cache_slot[3] = {0}; void **cache_slot; zend_property_info *prop_info; - zend_object *zobj; + zend_object *zobj = NULL; zend_string *name, *tmp_name; SAVE_OPLINE(); @@ -1414,6 +1427,8 @@ ZEND_VM_C_LABEL(post_incdec_object): } } while (0); + ZEND_MAYBE_TRACK_OBJECT_MUTATION(zobj); + FREE_OP2(); FREE_OP1(); ZEND_VM_NEXT_OPCODE_CHECK_EXCEPTION(); @@ -2490,7 +2505,7 @@ ZEND_VM_HANDLER(24, ZEND_ASSIGN_OBJ, VAR|UNUSED|THIS|CV, CONST|TMP|CV, CACHE_SLO { USE_OPLINE zval *object, *value, tmp; - zend_object *zobj; + zend_object *zobj = NULL; zend_string *name, *tmp_name; zend_refcounted *garbage = NULL; @@ -2630,6 +2645,7 @@ ZEND_VM_C_LABEL(free_and_exit_assign_obj): } FREE_OP_DATA(); ZEND_VM_C_LABEL(exit_assign_obj): + ZEND_MAYBE_TRACK_OBJECT_MUTATION_WITH_VALUE(zobj, value); if (garbage) { GC_DTOR_NO_REF(garbage); } @@ -2673,6 +2689,7 @@ ZEND_VM_HANDLER(25, ZEND_ASSIGN_STATIC_PROP, ANY, ANY, CACHE_SLOT, SPEC(OP_DATA= GC_DTOR_NO_REF(garbage); } + zend_class_static_update(prop_info->ce); /* assign_static_prop has two opcodes! */ ZEND_VM_NEXT_OPCODE_EX(1, 2); } @@ -2685,12 +2702,16 @@ ZEND_VM_HANDLER(23, ZEND_ASSIGN_DIM, VAR|CV, CONST|TMP|UNUSED|NEXT|CV, SPEC(OP_D zval *variable_ptr; zval *dim; zend_refcounted *garbage = NULL; + bool should_publish_tracked_array = false; SAVE_OPLINE(); orig_object_ptr = object_ptr = GET_OP1_ZVAL_PTR_PTR_UNDEF(BP_VAR_W); if (EXPECTED(Z_TYPE_P(object_ptr) == IS_ARRAY)) { ZEND_VM_C_LABEL(try_assign_dim_array): + if (UNEXPECTED(EG(tracked_mutation_hooks_active))) { + should_publish_tracked_array = zend_maybe_track_hash_mutation(Z_ARRVAL_P(object_ptr), false); + } SEPARATE_ARRAY(object_ptr); if (OP2_TYPE == IS_UNUSED) { value = GET_OP_DATA_ZVAL_PTR_UNDEF(BP_VAR_R); @@ -2748,6 +2769,9 @@ ZEND_VM_C_LABEL(try_assign_dim_array): if (garbage) { GC_DTOR_NO_REF(garbage); } + if (UNEXPECTED(should_publish_tracked_array)) { + zend_maybe_track_hash_mutation(Z_ARRVAL_P(object_ptr), true); + } } else { if (EXPECTED(Z_ISREF_P(object_ptr))) { object_ptr = Z_REFVAL_P(object_ptr); @@ -2774,6 +2798,7 @@ ZEND_VM_C_LABEL(try_assign_dim_array): } zend_assign_to_object_dim(obj, dim, value OPLINE_CC EXECUTE_DATA_CC); + ZEND_MAYBE_TRACK_OBJECT_MUTATION(obj); FREE_OP_DATA(); if (UNEXPECTED(GC_DELREF(obj) == 0)) { @@ -2902,6 +2927,7 @@ ZEND_VM_HANDLER(32, ZEND_ASSIGN_OBJ_REF, VAR|UNUSED|THIS|CV, CONST|TMP|CV, CACHE { USE_OPLINE zval *property, *container, *value_ptr; + zend_object *zobj = NULL; SAVE_OPLINE(); @@ -2909,6 +2935,11 @@ ZEND_VM_HANDLER(32, ZEND_ASSIGN_OBJ_REF, VAR|UNUSED|THIS|CV, CONST|TMP|CV, CACHE property = GET_OP2_ZVAL_PTR(BP_VAR_R); value_ptr = GET_OP_DATA_ZVAL_PTR_PTR(BP_VAR_W); + if (EXPECTED(Z_TYPE_P(container) == IS_OBJECT)) { + zobj = Z_OBJ_P(container); + } else if (Z_ISREF_P(container) && EXPECTED(Z_TYPE_P(Z_REFVAL_P(container)) == IS_OBJECT)) { + zobj = Z_OBJ_P(Z_REFVAL_P(container)); + } if (ZEND_VM_SPEC) { if (OP1_TYPE == IS_UNUSED) { @@ -2928,6 +2959,7 @@ ZEND_VM_HANDLER(32, ZEND_ASSIGN_OBJ_REF, VAR|UNUSED|THIS|CV, CONST|TMP|CV, CACHE zend_assign_to_property_reference(container, OP1_TYPE, property, OP2_TYPE, value_ptr OPLINE_CC EXECUTE_DATA_CC); } + ZEND_MAYBE_TRACK_OBJECT_MUTATION(zobj); FREE_OP1(); FREE_OP2(); FREE_OP_DATA(); @@ -2980,6 +3012,7 @@ ZEND_VM_HANDLER(33, ZEND_ASSIGN_STATIC_PROP_REF, ANY, ANY, CACHE_SLOT|SRC) } FREE_OP_DATA(); + zend_class_static_update(prop_info->ce); ZEND_VM_NEXT_OPCODE_EX(1, 2); } @@ -6773,6 +6806,8 @@ ZEND_VM_HANDLER(75, ZEND_UNSET_DIM, VAR|CV, CONST|TMP|CV) zval *offset; zend_ulong hval; zend_string *key; + bool should_flush_array = false; + bool should_publish_tracked_array = false; SAVE_OPLINE(); container = GET_OP1_OBJ_ZVAL_PTR_PTR_UNDEF(BP_VAR_UNSET); @@ -6783,6 +6818,9 @@ ZEND_VM_HANDLER(75, ZEND_UNSET_DIM, VAR|CV, CONST|TMP|CV) HashTable *ht; ZEND_VM_C_LABEL(unset_dim_array): + if (UNEXPECTED(EG(tracked_mutation_hooks_active))) { + should_publish_tracked_array = zend_maybe_track_hash_mutation(Z_ARRVAL_P(container), false); + } SEPARATE_ARRAY(container); ht = Z_ARRVAL_P(container); ZEND_VM_C_LABEL(offset_again): @@ -6796,10 +6834,12 @@ ZEND_VM_C_LABEL(offset_again): ZEND_VM_C_LABEL(str_index_dim): ZEND_ASSERT(ht != &EG(symbol_table)); zend_hash_del(ht, key); + should_flush_array = true; } else if (EXPECTED(Z_TYPE_P(offset) == IS_LONG)) { hval = Z_LVAL_P(offset); ZEND_VM_C_LABEL(num_index_dim): zend_hash_index_del(ht, hval); + should_flush_array = true; } else if ((OP2_TYPE & (IS_VAR|IS_CV)) && EXPECTED(Z_TYPE_P(offset) == IS_REFERENCE)) { offset = Z_REFVAL_P(offset); ZEND_VM_C_GOTO(offset_again); @@ -6836,6 +6876,9 @@ ZEND_VM_C_LABEL(num_index_dim): } else { zend_illegal_array_offset_unset(offset); } + if (UNEXPECTED(should_flush_array && should_publish_tracked_array)) { + zend_maybe_track_hash_mutation(ht, true); + } break; } else if (Z_ISREF_P(container)) { container = Z_REFVAL_P(container); @@ -6854,6 +6897,7 @@ ZEND_VM_C_LABEL(num_index_dim): offset++; } Z_OBJ_HT_P(container)->unset_dimension(Z_OBJ_P(container), offset); + ZEND_MAYBE_TRACK_OBJECT_MUTATION(Z_OBJ_P(container)); } else if (UNEXPECTED(Z_TYPE_P(container) == IS_STRING)) { zend_throw_error(NULL, "Cannot unset string offsets"); } else if (UNEXPECTED(Z_TYPE_P(container) > IS_FALSE)) { @@ -6874,6 +6918,7 @@ ZEND_VM_HANDLER(76, ZEND_UNSET_OBJ, VAR|UNUSED|THIS|CV, CONST|TMP|CV, CACHE_SLOT zval *container; zval *offset; zend_string *name, *tmp_name; + zend_object *zobj = NULL; SAVE_OPLINE(); container = GET_OP1_OBJ_ZVAL_PTR_PTR_UNDEF(BP_VAR_UNSET); @@ -6894,6 +6939,7 @@ ZEND_VM_HANDLER(76, ZEND_UNSET_OBJ, VAR|UNUSED|THIS|CV, CONST|TMP|CV, CACHE_SLOT break; } } + zobj = Z_OBJ_P(container); if (OP2_TYPE == IS_CONST) { name = Z_STR_P(offset); } else { @@ -6908,6 +6954,8 @@ ZEND_VM_HANDLER(76, ZEND_UNSET_OBJ, VAR|UNUSED|THIS|CV, CONST|TMP|CV, CACHE_SLOT } } while (0); + ZEND_MAYBE_TRACK_OBJECT_MUTATION(zobj); + FREE_OP2(); FREE_OP1(); ZEND_VM_NEXT_OPCODE_CHECK_EXCEPTION(); @@ -9219,6 +9267,10 @@ ZEND_VM_HANDLER(183, ZEND_BIND_STATIC, CV, ANY, REF) SAVE_OPLINE(); + if (UNEXPECTED(zend_function_init_statics_hook != NULL)) { + zend_function_init_statics_hook(execute_data); + } + ht = ZEND_MAP_PTR_GET(EX(func)->op_array.static_variables_ptr); if (!ht) { ht = zend_array_dup(EX(func)->op_array.static_variables); @@ -9270,6 +9322,10 @@ ZEND_VM_HANDLER(203, ZEND_BIND_INIT_STATIC_OR_JMP, CV, JMP_ADDR) variable_ptr = GET_OP1_ZVAL_PTR_PTR_UNDEF(BP_VAR_W); + if (UNEXPECTED(zend_function_init_statics_hook != NULL)) { + zend_function_init_statics_hook(execute_data); + } + ht = ZEND_MAP_PTR_GET(EX(func)->op_array.static_variables_ptr); if (!ht) { ZEND_VM_NEXT_OPCODE(); diff --git a/Zend/zend_vm_execute.h b/Zend/zend_vm_execute.h index 2b5b9e5fcd47..034550a7e750 100644 --- a/Zend/zend_vm_execute.h +++ b/Zend/zend_vm_execute.h @@ -822,6 +822,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_ASSIGN_STATIC } FREE_OP((opline+1)->op1_type, (opline+1)->op1.var); + zend_class_static_update(prop_info->ce); /* assign_static_prop has two opcodes! */ ZEND_VM_NEXT_OPCODE_EX(1, 2); } @@ -1015,6 +1016,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_ASSIGN_STATIC GC_DTOR_NO_REF(garbage); } + zend_class_static_update(prop_info->ce); /* assign_static_prop has two opcodes! */ ZEND_VM_NEXT_OPCODE_EX(1, 2); } @@ -1052,6 +1054,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_ASSIGN_STATIC GC_DTOR_NO_REF(garbage); } + zend_class_static_update(prop_info->ce); /* assign_static_prop has two opcodes! */ ZEND_VM_NEXT_OPCODE_EX(1, 2); } @@ -1091,6 +1094,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_ASSIGN_STATIC GC_DTOR_NO_REF(garbage); } + zend_class_static_update(prop_info->ce); /* assign_static_prop has two opcodes! */ ZEND_VM_NEXT_OPCODE_EX(1, 2); } @@ -1140,6 +1144,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_ASSIGN_STATIC } FREE_OP((opline+1)->op1_type, (opline+1)->op1.var); + zend_class_static_update(prop_info->ce); ZEND_VM_NEXT_OPCODE_EX(1, 2); } @@ -23902,7 +23907,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_ASSIGN_OBJ_OP void *_cache_slot[3] = {0}; void **cache_slot; zend_property_info *prop_info; - zend_object *zobj; + zend_object *zobj = NULL; zend_string *name, *tmp_name; SAVE_OPLINE(); @@ -23977,6 +23982,8 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_ASSIGN_OBJ_OP } } while (0); + ZEND_MAYBE_TRACK_OBJECT_MUTATION(zobj); + FREE_OP((opline+1)->op1_type, (opline+1)->op1.var); @@ -23992,12 +23999,16 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_ASSIGN_DIM_OP zval *var_ptr; zval *value, *container, *dim; HashTable *ht; + bool should_publish_tracked_array = false; SAVE_OPLINE(); container = _get_zval_ptr_ptr_var(opline->op1.var EXECUTE_DATA_CC); if (EXPECTED(Z_TYPE_P(container) == IS_ARRAY)) { assign_dim_op_array: + if (UNEXPECTED(EG(tracked_mutation_hooks_active))) { + should_publish_tracked_array = zend_maybe_track_hash_mutation(Z_ARRVAL_P(container), false); + } SEPARATE_ARRAY(container); ht = Z_ARRVAL_P(container); assign_dim_op_new_array: @@ -24037,6 +24048,9 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_ASSIGN_DIM_OP ZVAL_COPY(EX_VAR(opline->result.var), var_ptr); } FREE_OP((opline+1)->op1_type, (opline+1)->op1.var); + if (UNEXPECTED(should_publish_tracked_array)) { + zend_maybe_track_hash_mutation(Z_ARRVAL_P(container), true); + } } else { if (EXPECTED(Z_ISREF_P(container))) { container = Z_REFVAL_P(container); @@ -24053,6 +24067,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_ASSIGN_DIM_OP dim++; } zend_binary_assign_op_obj_dim(obj, dim OPLINE_CC EXECUTE_DATA_CC); + ZEND_MAYBE_TRACK_OBJECT_MUTATION(obj); } else if (EXPECTED(Z_TYPE_P(container) <= IS_FALSE)) { uint8_t old_type; @@ -24127,7 +24142,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_PRE_INC_OBJ_S void *_cache_slot[3] = {0}; void **cache_slot; zend_property_info *prop_info; - zend_object *zobj; + zend_object *zobj = NULL; zend_string *name, *tmp_name; SAVE_OPLINE(); @@ -24179,6 +24194,8 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_PRE_INC_OBJ_S } } while (0); + ZEND_MAYBE_TRACK_OBJECT_MUTATION(zobj); + zval_ptr_dtor_nogc(EX_VAR(opline->op1.var)); ZEND_VM_NEXT_OPCODE_CHECK_EXCEPTION(); @@ -24193,7 +24210,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_POST_INC_OBJ_ void *_cache_slot[3] = {0}; void **cache_slot; zend_property_info *prop_info; - zend_object *zobj; + zend_object *zobj = NULL; zend_string *name, *tmp_name; SAVE_OPLINE(); @@ -24243,6 +24260,8 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_POST_INC_OBJ_ } } while (0); + ZEND_MAYBE_TRACK_OBJECT_MUTATION(zobj); + zval_ptr_dtor_nogc(EX_VAR(opline->op1.var)); ZEND_VM_NEXT_OPCODE_CHECK_EXCEPTION(); @@ -24429,7 +24448,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_ASSIGN_OBJ_SP { USE_OPLINE zval *object, *value, tmp; - zend_object *zobj; + zend_object *zobj = NULL; zend_string *name, *tmp_name; zend_refcounted *garbage = NULL; @@ -24571,6 +24590,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_ASSIGN_OBJ_SP exit_assign_obj: + ZEND_MAYBE_TRACK_OBJECT_MUTATION_WITH_VALUE(zobj, value); if (garbage) { GC_DTOR_NO_REF(garbage); } @@ -24586,7 +24606,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_ASSIGN_OBJ_SP { USE_OPLINE zval *object, *value, tmp; - zend_object *zobj; + zend_object *zobj = NULL; zend_string *name, *tmp_name; zend_refcounted *garbage = NULL; @@ -24726,6 +24746,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_ASSIGN_OBJ_SP } zval_ptr_dtor_nogc(EX_VAR((opline+1)->op1.var)); exit_assign_obj: + ZEND_MAYBE_TRACK_OBJECT_MUTATION_WITH_VALUE(zobj, value); if (garbage) { GC_DTOR_NO_REF(garbage); } @@ -24741,7 +24762,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_ASSIGN_OBJ_SP { USE_OPLINE zval *object, *value, tmp; - zend_object *zobj; + zend_object *zobj = NULL; zend_string *name, *tmp_name; zend_refcounted *garbage = NULL; @@ -24883,6 +24904,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_ASSIGN_OBJ_SP exit_assign_obj: + ZEND_MAYBE_TRACK_OBJECT_MUTATION_WITH_VALUE(zobj, value); if (garbage) { GC_DTOR_NO_REF(garbage); } @@ -24902,12 +24924,16 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_ASSIGN_DIM_SP zval *variable_ptr; zval *dim; zend_refcounted *garbage = NULL; + bool should_publish_tracked_array = false; SAVE_OPLINE(); orig_object_ptr = object_ptr = _get_zval_ptr_ptr_var(opline->op1.var EXECUTE_DATA_CC); if (EXPECTED(Z_TYPE_P(object_ptr) == IS_ARRAY)) { try_assign_dim_array: + if (UNEXPECTED(EG(tracked_mutation_hooks_active))) { + should_publish_tracked_array = zend_maybe_track_hash_mutation(Z_ARRVAL_P(object_ptr), false); + } SEPARATE_ARRAY(object_ptr); if (IS_CONST == IS_UNUSED) { value = RT_CONSTANT((opline+1), (opline+1)->op1); @@ -24965,6 +24991,9 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_ASSIGN_DIM_SP if (garbage) { GC_DTOR_NO_REF(garbage); } + if (UNEXPECTED(should_publish_tracked_array)) { + zend_maybe_track_hash_mutation(Z_ARRVAL_P(object_ptr), true); + } } else { if (EXPECTED(Z_ISREF_P(object_ptr))) { object_ptr = Z_REFVAL_P(object_ptr); @@ -24991,6 +25020,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_ASSIGN_DIM_SP } zend_assign_to_object_dim(obj, dim, value OPLINE_CC EXECUTE_DATA_CC); + ZEND_MAYBE_TRACK_OBJECT_MUTATION(obj); if (UNEXPECTED(GC_DELREF(obj) == 0)) { @@ -25060,12 +25090,16 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_ASSIGN_DIM_SP zval *variable_ptr; zval *dim; zend_refcounted *garbage = NULL; + bool should_publish_tracked_array = false; SAVE_OPLINE(); orig_object_ptr = object_ptr = _get_zval_ptr_ptr_var(opline->op1.var EXECUTE_DATA_CC); if (EXPECTED(Z_TYPE_P(object_ptr) == IS_ARRAY)) { try_assign_dim_array: + if (UNEXPECTED(EG(tracked_mutation_hooks_active))) { + should_publish_tracked_array = zend_maybe_track_hash_mutation(Z_ARRVAL_P(object_ptr), false); + } SEPARATE_ARRAY(object_ptr); if (IS_CONST == IS_UNUSED) { value = _get_zval_ptr_tmp((opline+1)->op1.var EXECUTE_DATA_CC); @@ -25123,6 +25157,9 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_ASSIGN_DIM_SP if (garbage) { GC_DTOR_NO_REF(garbage); } + if (UNEXPECTED(should_publish_tracked_array)) { + zend_maybe_track_hash_mutation(Z_ARRVAL_P(object_ptr), true); + } } else { if (EXPECTED(Z_ISREF_P(object_ptr))) { object_ptr = Z_REFVAL_P(object_ptr); @@ -25149,6 +25186,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_ASSIGN_DIM_SP } zend_assign_to_object_dim(obj, dim, value OPLINE_CC EXECUTE_DATA_CC); + ZEND_MAYBE_TRACK_OBJECT_MUTATION(obj); zval_ptr_dtor_nogc(EX_VAR((opline+1)->op1.var)); if (UNEXPECTED(GC_DELREF(obj) == 0)) { @@ -25214,12 +25252,16 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_ASSIGN_DIM_SP zval *variable_ptr; zval *dim; zend_refcounted *garbage = NULL; + bool should_publish_tracked_array = false; SAVE_OPLINE(); orig_object_ptr = object_ptr = _get_zval_ptr_ptr_var(opline->op1.var EXECUTE_DATA_CC); if (EXPECTED(Z_TYPE_P(object_ptr) == IS_ARRAY)) { try_assign_dim_array: + if (UNEXPECTED(EG(tracked_mutation_hooks_active))) { + should_publish_tracked_array = zend_maybe_track_hash_mutation(Z_ARRVAL_P(object_ptr), false); + } SEPARATE_ARRAY(object_ptr); if (IS_CONST == IS_UNUSED) { value = EX_VAR((opline+1)->op1.var); @@ -25277,6 +25319,9 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_ASSIGN_DIM_SP if (garbage) { GC_DTOR_NO_REF(garbage); } + if (UNEXPECTED(should_publish_tracked_array)) { + zend_maybe_track_hash_mutation(Z_ARRVAL_P(object_ptr), true); + } } else { if (EXPECTED(Z_ISREF_P(object_ptr))) { object_ptr = Z_REFVAL_P(object_ptr); @@ -25303,6 +25348,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_ASSIGN_DIM_SP } zend_assign_to_object_dim(obj, dim, value OPLINE_CC EXECUTE_DATA_CC); + ZEND_MAYBE_TRACK_OBJECT_MUTATION(obj); if (UNEXPECTED(GC_DELREF(obj) == 0)) { @@ -25426,6 +25472,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_ASSIGN_OBJ_RE { USE_OPLINE zval *property, *container, *value_ptr; + zend_object *zobj = NULL; SAVE_OPLINE(); @@ -25433,6 +25480,11 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_ASSIGN_OBJ_RE property = RT_CONSTANT(opline, opline->op2); value_ptr = _get_zval_ptr_ptr_var((opline+1)->op1.var EXECUTE_DATA_CC); + if (EXPECTED(Z_TYPE_P(container) == IS_OBJECT)) { + zobj = Z_OBJ_P(container); + } else if (Z_ISREF_P(container) && EXPECTED(Z_TYPE_P(Z_REFVAL_P(container)) == IS_OBJECT)) { + zobj = Z_OBJ_P(Z_REFVAL_P(container)); + } if (1) { if (IS_VAR == IS_UNUSED) { @@ -25452,6 +25504,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_ASSIGN_OBJ_RE zend_assign_to_property_reference(container, IS_VAR, property, IS_CONST, value_ptr OPLINE_CC EXECUTE_DATA_CC); } + ZEND_MAYBE_TRACK_OBJECT_MUTATION(zobj); zval_ptr_dtor_nogc(EX_VAR(opline->op1.var)); @@ -25464,6 +25517,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_ASSIGN_OBJ_RE { USE_OPLINE zval *property, *container, *value_ptr; + zend_object *zobj = NULL; SAVE_OPLINE(); @@ -25471,6 +25525,11 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_ASSIGN_OBJ_RE property = RT_CONSTANT(opline, opline->op2); value_ptr = _get_zval_ptr_cv_BP_VAR_W((opline+1)->op1.var EXECUTE_DATA_CC); + if (EXPECTED(Z_TYPE_P(container) == IS_OBJECT)) { + zobj = Z_OBJ_P(container); + } else if (Z_ISREF_P(container) && EXPECTED(Z_TYPE_P(Z_REFVAL_P(container)) == IS_OBJECT)) { + zobj = Z_OBJ_P(Z_REFVAL_P(container)); + } if (1) { if (IS_VAR == IS_UNUSED) { @@ -25490,6 +25549,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_ASSIGN_OBJ_RE zend_assign_to_property_reference(container, IS_VAR, property, IS_CONST, value_ptr OPLINE_CC EXECUTE_DATA_CC); } + ZEND_MAYBE_TRACK_OBJECT_MUTATION(zobj); zval_ptr_dtor_nogc(EX_VAR(opline->op1.var)); @@ -26213,6 +26273,8 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_UNSET_DIM_SPE zval *offset; zend_ulong hval; zend_string *key; + bool should_flush_array = false; + bool should_publish_tracked_array = false; SAVE_OPLINE(); container = _get_zval_ptr_ptr_var(opline->op1.var EXECUTE_DATA_CC); @@ -26223,6 +26285,9 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_UNSET_DIM_SPE HashTable *ht; unset_dim_array: + if (UNEXPECTED(EG(tracked_mutation_hooks_active))) { + should_publish_tracked_array = zend_maybe_track_hash_mutation(Z_ARRVAL_P(container), false); + } SEPARATE_ARRAY(container); ht = Z_ARRVAL_P(container); offset_again: @@ -26236,10 +26301,12 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_UNSET_DIM_SPE str_index_dim: ZEND_ASSERT(ht != &EG(symbol_table)); zend_hash_del(ht, key); + should_flush_array = true; } else if (EXPECTED(Z_TYPE_P(offset) == IS_LONG)) { hval = Z_LVAL_P(offset); num_index_dim: zend_hash_index_del(ht, hval); + should_flush_array = true; } else if ((IS_CONST & (IS_VAR|IS_CV)) && EXPECTED(Z_TYPE_P(offset) == IS_REFERENCE)) { offset = Z_REFVAL_P(offset); goto offset_again; @@ -26276,6 +26343,9 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_UNSET_DIM_SPE } else { zend_illegal_array_offset_unset(offset); } + if (UNEXPECTED(should_flush_array && should_publish_tracked_array)) { + zend_maybe_track_hash_mutation(ht, true); + } break; } else if (Z_ISREF_P(container)) { container = Z_REFVAL_P(container); @@ -26294,6 +26364,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_UNSET_DIM_SPE offset++; } Z_OBJ_HT_P(container)->unset_dimension(Z_OBJ_P(container), offset); + ZEND_MAYBE_TRACK_OBJECT_MUTATION(Z_OBJ_P(container)); } else if (UNEXPECTED(Z_TYPE_P(container) == IS_STRING)) { zend_throw_error(NULL, "Cannot unset string offsets"); } else if (UNEXPECTED(Z_TYPE_P(container) > IS_FALSE)) { @@ -26314,6 +26385,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_UNSET_OBJ_SPE zval *container; zval *offset; zend_string *name, *tmp_name; + zend_object *zobj = NULL; SAVE_OPLINE(); container = _get_zval_ptr_ptr_var(opline->op1.var EXECUTE_DATA_CC); @@ -26334,6 +26406,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_UNSET_OBJ_SPE break; } } + zobj = Z_OBJ_P(container); if (IS_CONST == IS_CONST) { name = Z_STR_P(offset); } else { @@ -26348,6 +26421,8 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_UNSET_OBJ_SPE } } while (0); + ZEND_MAYBE_TRACK_OBJECT_MUTATION(zobj); + zval_ptr_dtor_nogc(EX_VAR(opline->op1.var)); ZEND_VM_NEXT_OPCODE_CHECK_EXCEPTION(); @@ -26617,7 +26692,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_ASSIGN_OBJ_OP void *_cache_slot[3] = {0}; void **cache_slot; zend_property_info *prop_info; - zend_object *zobj; + zend_object *zobj = NULL; zend_string *name, *tmp_name; SAVE_OPLINE(); @@ -26692,6 +26767,8 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_ASSIGN_OBJ_OP } } while (0); + ZEND_MAYBE_TRACK_OBJECT_MUTATION(zobj); + FREE_OP((opline+1)->op1_type, (opline+1)->op1.var); zval_ptr_dtor_nogc(EX_VAR(opline->op2.var)); zval_ptr_dtor_nogc(EX_VAR(opline->op1.var)); @@ -26706,12 +26783,16 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_ASSIGN_DIM_OP zval *var_ptr; zval *value, *container, *dim; HashTable *ht; + bool should_publish_tracked_array = false; SAVE_OPLINE(); container = _get_zval_ptr_ptr_var(opline->op1.var EXECUTE_DATA_CC); if (EXPECTED(Z_TYPE_P(container) == IS_ARRAY)) { assign_dim_op_array: + if (UNEXPECTED(EG(tracked_mutation_hooks_active))) { + should_publish_tracked_array = zend_maybe_track_hash_mutation(Z_ARRVAL_P(container), false); + } SEPARATE_ARRAY(container); ht = Z_ARRVAL_P(container); assign_dim_op_new_array: @@ -26751,6 +26832,9 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_ASSIGN_DIM_OP ZVAL_COPY(EX_VAR(opline->result.var), var_ptr); } FREE_OP((opline+1)->op1_type, (opline+1)->op1.var); + if (UNEXPECTED(should_publish_tracked_array)) { + zend_maybe_track_hash_mutation(Z_ARRVAL_P(container), true); + } } else { if (EXPECTED(Z_ISREF_P(container))) { container = Z_REFVAL_P(container); @@ -26767,6 +26851,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_ASSIGN_DIM_OP dim++; } zend_binary_assign_op_obj_dim(obj, dim OPLINE_CC EXECUTE_DATA_CC); + ZEND_MAYBE_TRACK_OBJECT_MUTATION(obj); } else if (EXPECTED(Z_TYPE_P(container) <= IS_FALSE)) { uint8_t old_type; @@ -26841,7 +26926,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_PRE_INC_OBJ_S void *_cache_slot[3] = {0}; void **cache_slot; zend_property_info *prop_info; - zend_object *zobj; + zend_object *zobj = NULL; zend_string *name, *tmp_name; SAVE_OPLINE(); @@ -26893,6 +26978,8 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_PRE_INC_OBJ_S } } while (0); + ZEND_MAYBE_TRACK_OBJECT_MUTATION(zobj); + zval_ptr_dtor_nogc(EX_VAR(opline->op2.var)); zval_ptr_dtor_nogc(EX_VAR(opline->op1.var)); ZEND_VM_NEXT_OPCODE_CHECK_EXCEPTION(); @@ -26907,7 +26994,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_POST_INC_OBJ_ void *_cache_slot[3] = {0}; void **cache_slot; zend_property_info *prop_info; - zend_object *zobj; + zend_object *zobj = NULL; zend_string *name, *tmp_name; SAVE_OPLINE(); @@ -26957,6 +27044,8 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_POST_INC_OBJ_ } } while (0); + ZEND_MAYBE_TRACK_OBJECT_MUTATION(zobj); + zval_ptr_dtor_nogc(EX_VAR(opline->op2.var)); zval_ptr_dtor_nogc(EX_VAR(opline->op1.var)); ZEND_VM_NEXT_OPCODE_CHECK_EXCEPTION(); @@ -27137,7 +27226,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_ASSIGN_OBJ_SP { USE_OPLINE zval *object, *value, tmp; - zend_object *zobj; + zend_object *zobj = NULL; zend_string *name, *tmp_name; zend_refcounted *garbage = NULL; @@ -27279,6 +27368,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_ASSIGN_OBJ_SP exit_assign_obj: + ZEND_MAYBE_TRACK_OBJECT_MUTATION_WITH_VALUE(zobj, value); if (garbage) { GC_DTOR_NO_REF(garbage); } @@ -27293,7 +27383,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_ASSIGN_OBJ_SP { USE_OPLINE zval *object, *value, tmp; - zend_object *zobj; + zend_object *zobj = NULL; zend_string *name, *tmp_name; zend_refcounted *garbage = NULL; @@ -27433,6 +27523,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_ASSIGN_OBJ_SP } zval_ptr_dtor_nogc(EX_VAR((opline+1)->op1.var)); exit_assign_obj: + ZEND_MAYBE_TRACK_OBJECT_MUTATION_WITH_VALUE(zobj, value); if (garbage) { GC_DTOR_NO_REF(garbage); } @@ -27447,7 +27538,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_ASSIGN_OBJ_SP { USE_OPLINE zval *object, *value, tmp; - zend_object *zobj; + zend_object *zobj = NULL; zend_string *name, *tmp_name; zend_refcounted *garbage = NULL; @@ -27589,6 +27680,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_ASSIGN_OBJ_SP exit_assign_obj: + ZEND_MAYBE_TRACK_OBJECT_MUTATION_WITH_VALUE(zobj, value); if (garbage) { GC_DTOR_NO_REF(garbage); } @@ -27607,12 +27699,16 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_ASSIGN_DIM_SP zval *variable_ptr; zval *dim; zend_refcounted *garbage = NULL; + bool should_publish_tracked_array = false; SAVE_OPLINE(); orig_object_ptr = object_ptr = _get_zval_ptr_ptr_var(opline->op1.var EXECUTE_DATA_CC); if (EXPECTED(Z_TYPE_P(object_ptr) == IS_ARRAY)) { try_assign_dim_array: + if (UNEXPECTED(EG(tracked_mutation_hooks_active))) { + should_publish_tracked_array = zend_maybe_track_hash_mutation(Z_ARRVAL_P(object_ptr), false); + } SEPARATE_ARRAY(object_ptr); if (IS_TMP_VAR == IS_UNUSED) { value = RT_CONSTANT((opline+1), (opline+1)->op1); @@ -27670,6 +27766,9 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_ASSIGN_DIM_SP if (garbage) { GC_DTOR_NO_REF(garbage); } + if (UNEXPECTED(should_publish_tracked_array)) { + zend_maybe_track_hash_mutation(Z_ARRVAL_P(object_ptr), true); + } } else { if (EXPECTED(Z_ISREF_P(object_ptr))) { object_ptr = Z_REFVAL_P(object_ptr); @@ -27696,6 +27795,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_ASSIGN_DIM_SP } zend_assign_to_object_dim(obj, dim, value OPLINE_CC EXECUTE_DATA_CC); + ZEND_MAYBE_TRACK_OBJECT_MUTATION(obj); if (UNEXPECTED(GC_DELREF(obj) == 0)) { @@ -27764,12 +27864,16 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_ASSIGN_DIM_SP zval *variable_ptr; zval *dim; zend_refcounted *garbage = NULL; + bool should_publish_tracked_array = false; SAVE_OPLINE(); orig_object_ptr = object_ptr = _get_zval_ptr_ptr_var(opline->op1.var EXECUTE_DATA_CC); if (EXPECTED(Z_TYPE_P(object_ptr) == IS_ARRAY)) { try_assign_dim_array: + if (UNEXPECTED(EG(tracked_mutation_hooks_active))) { + should_publish_tracked_array = zend_maybe_track_hash_mutation(Z_ARRVAL_P(object_ptr), false); + } SEPARATE_ARRAY(object_ptr); if (IS_TMP_VAR == IS_UNUSED) { value = _get_zval_ptr_tmp((opline+1)->op1.var EXECUTE_DATA_CC); @@ -27827,6 +27931,9 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_ASSIGN_DIM_SP if (garbage) { GC_DTOR_NO_REF(garbage); } + if (UNEXPECTED(should_publish_tracked_array)) { + zend_maybe_track_hash_mutation(Z_ARRVAL_P(object_ptr), true); + } } else { if (EXPECTED(Z_ISREF_P(object_ptr))) { object_ptr = Z_REFVAL_P(object_ptr); @@ -27853,6 +27960,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_ASSIGN_DIM_SP } zend_assign_to_object_dim(obj, dim, value OPLINE_CC EXECUTE_DATA_CC); + ZEND_MAYBE_TRACK_OBJECT_MUTATION(obj); zval_ptr_dtor_nogc(EX_VAR((opline+1)->op1.var)); if (UNEXPECTED(GC_DELREF(obj) == 0)) { @@ -27917,12 +28025,16 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_ASSIGN_DIM_SP zval *variable_ptr; zval *dim; zend_refcounted *garbage = NULL; + bool should_publish_tracked_array = false; SAVE_OPLINE(); orig_object_ptr = object_ptr = _get_zval_ptr_ptr_var(opline->op1.var EXECUTE_DATA_CC); if (EXPECTED(Z_TYPE_P(object_ptr) == IS_ARRAY)) { try_assign_dim_array: + if (UNEXPECTED(EG(tracked_mutation_hooks_active))) { + should_publish_tracked_array = zend_maybe_track_hash_mutation(Z_ARRVAL_P(object_ptr), false); + } SEPARATE_ARRAY(object_ptr); if (IS_TMP_VAR == IS_UNUSED) { value = EX_VAR((opline+1)->op1.var); @@ -27980,6 +28092,9 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_ASSIGN_DIM_SP if (garbage) { GC_DTOR_NO_REF(garbage); } + if (UNEXPECTED(should_publish_tracked_array)) { + zend_maybe_track_hash_mutation(Z_ARRVAL_P(object_ptr), true); + } } else { if (EXPECTED(Z_ISREF_P(object_ptr))) { object_ptr = Z_REFVAL_P(object_ptr); @@ -28006,6 +28121,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_ASSIGN_DIM_SP } zend_assign_to_object_dim(obj, dim, value OPLINE_CC EXECUTE_DATA_CC); + ZEND_MAYBE_TRACK_OBJECT_MUTATION(obj); if (UNEXPECTED(GC_DELREF(obj) == 0)) { @@ -28128,6 +28244,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_ASSIGN_OBJ_RE { USE_OPLINE zval *property, *container, *value_ptr; + zend_object *zobj = NULL; SAVE_OPLINE(); @@ -28135,6 +28252,11 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_ASSIGN_OBJ_RE property = _get_zval_ptr_tmp(opline->op2.var EXECUTE_DATA_CC); value_ptr = _get_zval_ptr_ptr_var((opline+1)->op1.var EXECUTE_DATA_CC); + if (EXPECTED(Z_TYPE_P(container) == IS_OBJECT)) { + zobj = Z_OBJ_P(container); + } else if (Z_ISREF_P(container) && EXPECTED(Z_TYPE_P(Z_REFVAL_P(container)) == IS_OBJECT)) { + zobj = Z_OBJ_P(Z_REFVAL_P(container)); + } if (1) { if (IS_VAR == IS_UNUSED) { @@ -28154,6 +28276,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_ASSIGN_OBJ_RE zend_assign_to_property_reference(container, IS_VAR, property, IS_TMP_VAR, value_ptr OPLINE_CC EXECUTE_DATA_CC); } + ZEND_MAYBE_TRACK_OBJECT_MUTATION(zobj); zval_ptr_dtor_nogc(EX_VAR(opline->op1.var)); zval_ptr_dtor_nogc(EX_VAR(opline->op2.var)); zval_ptr_dtor_nogc(EX_VAR((opline+1)->op1.var)); @@ -28165,6 +28288,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_ASSIGN_OBJ_RE { USE_OPLINE zval *property, *container, *value_ptr; + zend_object *zobj = NULL; SAVE_OPLINE(); @@ -28172,6 +28296,11 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_ASSIGN_OBJ_RE property = _get_zval_ptr_tmp(opline->op2.var EXECUTE_DATA_CC); value_ptr = _get_zval_ptr_cv_BP_VAR_W((opline+1)->op1.var EXECUTE_DATA_CC); + if (EXPECTED(Z_TYPE_P(container) == IS_OBJECT)) { + zobj = Z_OBJ_P(container); + } else if (Z_ISREF_P(container) && EXPECTED(Z_TYPE_P(Z_REFVAL_P(container)) == IS_OBJECT)) { + zobj = Z_OBJ_P(Z_REFVAL_P(container)); + } if (1) { if (IS_VAR == IS_UNUSED) { @@ -28191,6 +28320,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_ASSIGN_OBJ_RE zend_assign_to_property_reference(container, IS_VAR, property, IS_TMP_VAR, value_ptr OPLINE_CC EXECUTE_DATA_CC); } + ZEND_MAYBE_TRACK_OBJECT_MUTATION(zobj); zval_ptr_dtor_nogc(EX_VAR(opline->op1.var)); zval_ptr_dtor_nogc(EX_VAR(opline->op2.var)); @@ -28471,6 +28601,8 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_UNSET_DIM_SPE zval *offset; zend_ulong hval; zend_string *key; + bool should_flush_array = false; + bool should_publish_tracked_array = false; SAVE_OPLINE(); container = _get_zval_ptr_ptr_var(opline->op1.var EXECUTE_DATA_CC); @@ -28481,6 +28613,9 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_UNSET_DIM_SPE HashTable *ht; unset_dim_array: + if (UNEXPECTED(EG(tracked_mutation_hooks_active))) { + should_publish_tracked_array = zend_maybe_track_hash_mutation(Z_ARRVAL_P(container), false); + } SEPARATE_ARRAY(container); ht = Z_ARRVAL_P(container); offset_again: @@ -28494,10 +28629,12 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_UNSET_DIM_SPE str_index_dim: ZEND_ASSERT(ht != &EG(symbol_table)); zend_hash_del(ht, key); + should_flush_array = true; } else if (EXPECTED(Z_TYPE_P(offset) == IS_LONG)) { hval = Z_LVAL_P(offset); num_index_dim: zend_hash_index_del(ht, hval); + should_flush_array = true; } else if ((IS_TMP_VAR & (IS_VAR|IS_CV)) && EXPECTED(Z_TYPE_P(offset) == IS_REFERENCE)) { offset = Z_REFVAL_P(offset); goto offset_again; @@ -28534,6 +28671,9 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_UNSET_DIM_SPE } else { zend_illegal_array_offset_unset(offset); } + if (UNEXPECTED(should_flush_array && should_publish_tracked_array)) { + zend_maybe_track_hash_mutation(ht, true); + } break; } else if (Z_ISREF_P(container)) { container = Z_REFVAL_P(container); @@ -28552,6 +28692,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_UNSET_DIM_SPE offset++; } Z_OBJ_HT_P(container)->unset_dimension(Z_OBJ_P(container), offset); + ZEND_MAYBE_TRACK_OBJECT_MUTATION(Z_OBJ_P(container)); } else if (UNEXPECTED(Z_TYPE_P(container) == IS_STRING)) { zend_throw_error(NULL, "Cannot unset string offsets"); } else if (UNEXPECTED(Z_TYPE_P(container) > IS_FALSE)) { @@ -28572,6 +28713,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_UNSET_OBJ_SPE zval *container; zval *offset; zend_string *name, *tmp_name; + zend_object *zobj = NULL; SAVE_OPLINE(); container = _get_zval_ptr_ptr_var(opline->op1.var EXECUTE_DATA_CC); @@ -28592,6 +28734,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_UNSET_OBJ_SPE break; } } + zobj = Z_OBJ_P(container); if (IS_TMP_VAR == IS_CONST) { name = Z_STR_P(offset); } else { @@ -28606,6 +28749,8 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_UNSET_OBJ_SPE } } while (0); + ZEND_MAYBE_TRACK_OBJECT_MUTATION(zobj); + zval_ptr_dtor_nogc(EX_VAR(opline->op2.var)); zval_ptr_dtor_nogc(EX_VAR(opline->op1.var)); ZEND_VM_NEXT_OPCODE_CHECK_EXCEPTION(); @@ -28777,12 +28922,16 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_ASSIGN_DIM_OP zval *var_ptr; zval *value, *container, *dim; HashTable *ht; + bool should_publish_tracked_array = false; SAVE_OPLINE(); container = _get_zval_ptr_ptr_var(opline->op1.var EXECUTE_DATA_CC); if (EXPECTED(Z_TYPE_P(container) == IS_ARRAY)) { assign_dim_op_array: + if (UNEXPECTED(EG(tracked_mutation_hooks_active))) { + should_publish_tracked_array = zend_maybe_track_hash_mutation(Z_ARRVAL_P(container), false); + } SEPARATE_ARRAY(container); ht = Z_ARRVAL_P(container); assign_dim_op_new_array: @@ -28822,6 +28971,9 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_ASSIGN_DIM_OP ZVAL_COPY(EX_VAR(opline->result.var), var_ptr); } FREE_OP((opline+1)->op1_type, (opline+1)->op1.var); + if (UNEXPECTED(should_publish_tracked_array)) { + zend_maybe_track_hash_mutation(Z_ARRVAL_P(container), true); + } } else { if (EXPECTED(Z_ISREF_P(container))) { container = Z_REFVAL_P(container); @@ -28838,6 +28990,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_ASSIGN_DIM_OP dim++; } zend_binary_assign_op_obj_dim(obj, dim OPLINE_CC EXECUTE_DATA_CC); + ZEND_MAYBE_TRACK_OBJECT_MUTATION(obj); } else if (EXPECTED(Z_TYPE_P(container) <= IS_FALSE)) { uint8_t old_type; @@ -28937,12 +29090,16 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_ASSIGN_DIM_SP zval *variable_ptr; zval *dim; zend_refcounted *garbage = NULL; + bool should_publish_tracked_array = false; SAVE_OPLINE(); orig_object_ptr = object_ptr = _get_zval_ptr_ptr_var(opline->op1.var EXECUTE_DATA_CC); if (EXPECTED(Z_TYPE_P(object_ptr) == IS_ARRAY)) { try_assign_dim_array: + if (UNEXPECTED(EG(tracked_mutation_hooks_active))) { + should_publish_tracked_array = zend_maybe_track_hash_mutation(Z_ARRVAL_P(object_ptr), false); + } SEPARATE_ARRAY(object_ptr); if (IS_UNUSED == IS_UNUSED) { value = RT_CONSTANT((opline+1), (opline+1)->op1); @@ -29000,6 +29157,9 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_ASSIGN_DIM_SP if (garbage) { GC_DTOR_NO_REF(garbage); } + if (UNEXPECTED(should_publish_tracked_array)) { + zend_maybe_track_hash_mutation(Z_ARRVAL_P(object_ptr), true); + } } else { if (EXPECTED(Z_ISREF_P(object_ptr))) { object_ptr = Z_REFVAL_P(object_ptr); @@ -29026,6 +29186,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_ASSIGN_DIM_SP } zend_assign_to_object_dim(obj, dim, value OPLINE_CC EXECUTE_DATA_CC); + ZEND_MAYBE_TRACK_OBJECT_MUTATION(obj); if (UNEXPECTED(GC_DELREF(obj) == 0)) { @@ -29095,12 +29256,16 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_ASSIGN_DIM_SP zval *variable_ptr; zval *dim; zend_refcounted *garbage = NULL; + bool should_publish_tracked_array = false; SAVE_OPLINE(); orig_object_ptr = object_ptr = _get_zval_ptr_ptr_var(opline->op1.var EXECUTE_DATA_CC); if (EXPECTED(Z_TYPE_P(object_ptr) == IS_ARRAY)) { try_assign_dim_array: + if (UNEXPECTED(EG(tracked_mutation_hooks_active))) { + should_publish_tracked_array = zend_maybe_track_hash_mutation(Z_ARRVAL_P(object_ptr), false); + } SEPARATE_ARRAY(object_ptr); if (IS_UNUSED == IS_UNUSED) { value = _get_zval_ptr_tmp((opline+1)->op1.var EXECUTE_DATA_CC); @@ -29158,6 +29323,9 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_ASSIGN_DIM_SP if (garbage) { GC_DTOR_NO_REF(garbage); } + if (UNEXPECTED(should_publish_tracked_array)) { + zend_maybe_track_hash_mutation(Z_ARRVAL_P(object_ptr), true); + } } else { if (EXPECTED(Z_ISREF_P(object_ptr))) { object_ptr = Z_REFVAL_P(object_ptr); @@ -29184,6 +29352,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_ASSIGN_DIM_SP } zend_assign_to_object_dim(obj, dim, value OPLINE_CC EXECUTE_DATA_CC); + ZEND_MAYBE_TRACK_OBJECT_MUTATION(obj); zval_ptr_dtor_nogc(EX_VAR((opline+1)->op1.var)); if (UNEXPECTED(GC_DELREF(obj) == 0)) { @@ -29249,12 +29418,16 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_ASSIGN_DIM_SP zval *variable_ptr; zval *dim; zend_refcounted *garbage = NULL; + bool should_publish_tracked_array = false; SAVE_OPLINE(); orig_object_ptr = object_ptr = _get_zval_ptr_ptr_var(opline->op1.var EXECUTE_DATA_CC); if (EXPECTED(Z_TYPE_P(object_ptr) == IS_ARRAY)) { try_assign_dim_array: + if (UNEXPECTED(EG(tracked_mutation_hooks_active))) { + should_publish_tracked_array = zend_maybe_track_hash_mutation(Z_ARRVAL_P(object_ptr), false); + } SEPARATE_ARRAY(object_ptr); if (IS_UNUSED == IS_UNUSED) { value = EX_VAR((opline+1)->op1.var); @@ -29312,6 +29485,9 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_ASSIGN_DIM_SP if (garbage) { GC_DTOR_NO_REF(garbage); } + if (UNEXPECTED(should_publish_tracked_array)) { + zend_maybe_track_hash_mutation(Z_ARRVAL_P(object_ptr), true); + } } else { if (EXPECTED(Z_ISREF_P(object_ptr))) { object_ptr = Z_REFVAL_P(object_ptr); @@ -29338,6 +29514,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_ASSIGN_DIM_SP } zend_assign_to_object_dim(obj, dim, value OPLINE_CC EXECUTE_DATA_CC); + ZEND_MAYBE_TRACK_OBJECT_MUTATION(obj); if (UNEXPECTED(GC_DELREF(obj) == 0)) { @@ -30434,7 +30611,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_ASSIGN_OBJ_OP void *_cache_slot[3] = {0}; void **cache_slot; zend_property_info *prop_info; - zend_object *zobj; + zend_object *zobj = NULL; zend_string *name, *tmp_name; SAVE_OPLINE(); @@ -30509,6 +30686,8 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_ASSIGN_OBJ_OP } } while (0); + ZEND_MAYBE_TRACK_OBJECT_MUTATION(zobj); + FREE_OP((opline+1)->op1_type, (opline+1)->op1.var); @@ -30524,12 +30703,16 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_ASSIGN_DIM_OP zval *var_ptr; zval *value, *container, *dim; HashTable *ht; + bool should_publish_tracked_array = false; SAVE_OPLINE(); container = _get_zval_ptr_ptr_var(opline->op1.var EXECUTE_DATA_CC); if (EXPECTED(Z_TYPE_P(container) == IS_ARRAY)) { assign_dim_op_array: + if (UNEXPECTED(EG(tracked_mutation_hooks_active))) { + should_publish_tracked_array = zend_maybe_track_hash_mutation(Z_ARRVAL_P(container), false); + } SEPARATE_ARRAY(container); ht = Z_ARRVAL_P(container); assign_dim_op_new_array: @@ -30569,6 +30752,9 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_ASSIGN_DIM_OP ZVAL_COPY(EX_VAR(opline->result.var), var_ptr); } FREE_OP((opline+1)->op1_type, (opline+1)->op1.var); + if (UNEXPECTED(should_publish_tracked_array)) { + zend_maybe_track_hash_mutation(Z_ARRVAL_P(container), true); + } } else { if (EXPECTED(Z_ISREF_P(container))) { container = Z_REFVAL_P(container); @@ -30585,6 +30771,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_ASSIGN_DIM_OP dim++; } zend_binary_assign_op_obj_dim(obj, dim OPLINE_CC EXECUTE_DATA_CC); + ZEND_MAYBE_TRACK_OBJECT_MUTATION(obj); } else if (EXPECTED(Z_TYPE_P(container) <= IS_FALSE)) { uint8_t old_type; @@ -30659,7 +30846,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_PRE_INC_OBJ_S void *_cache_slot[3] = {0}; void **cache_slot; zend_property_info *prop_info; - zend_object *zobj; + zend_object *zobj = NULL; zend_string *name, *tmp_name; SAVE_OPLINE(); @@ -30711,6 +30898,8 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_PRE_INC_OBJ_S } } while (0); + ZEND_MAYBE_TRACK_OBJECT_MUTATION(zobj); + zval_ptr_dtor_nogc(EX_VAR(opline->op1.var)); ZEND_VM_NEXT_OPCODE_CHECK_EXCEPTION(); @@ -30725,7 +30914,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_POST_INC_OBJ_ void *_cache_slot[3] = {0}; void **cache_slot; zend_property_info *prop_info; - zend_object *zobj; + zend_object *zobj = NULL; zend_string *name, *tmp_name; SAVE_OPLINE(); @@ -30775,6 +30964,8 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_POST_INC_OBJ_ } } while (0); + ZEND_MAYBE_TRACK_OBJECT_MUTATION(zobj); + zval_ptr_dtor_nogc(EX_VAR(opline->op1.var)); ZEND_VM_NEXT_OPCODE_CHECK_EXCEPTION(); @@ -30961,7 +31152,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_ASSIGN_OBJ_SP { USE_OPLINE zval *object, *value, tmp; - zend_object *zobj; + zend_object *zobj = NULL; zend_string *name, *tmp_name; zend_refcounted *garbage = NULL; @@ -31103,6 +31294,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_ASSIGN_OBJ_SP exit_assign_obj: + ZEND_MAYBE_TRACK_OBJECT_MUTATION_WITH_VALUE(zobj, value); if (garbage) { GC_DTOR_NO_REF(garbage); } @@ -31118,7 +31310,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_ASSIGN_OBJ_SP { USE_OPLINE zval *object, *value, tmp; - zend_object *zobj; + zend_object *zobj = NULL; zend_string *name, *tmp_name; zend_refcounted *garbage = NULL; @@ -31258,6 +31450,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_ASSIGN_OBJ_SP } zval_ptr_dtor_nogc(EX_VAR((opline+1)->op1.var)); exit_assign_obj: + ZEND_MAYBE_TRACK_OBJECT_MUTATION_WITH_VALUE(zobj, value); if (garbage) { GC_DTOR_NO_REF(garbage); } @@ -31273,7 +31466,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_ASSIGN_OBJ_SP { USE_OPLINE zval *object, *value, tmp; - zend_object *zobj; + zend_object *zobj = NULL; zend_string *name, *tmp_name; zend_refcounted *garbage = NULL; @@ -31415,6 +31608,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_ASSIGN_OBJ_SP exit_assign_obj: + ZEND_MAYBE_TRACK_OBJECT_MUTATION_WITH_VALUE(zobj, value); if (garbage) { GC_DTOR_NO_REF(garbage); } @@ -31434,12 +31628,16 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_ASSIGN_DIM_SP zval *variable_ptr; zval *dim; zend_refcounted *garbage = NULL; + bool should_publish_tracked_array = false; SAVE_OPLINE(); orig_object_ptr = object_ptr = _get_zval_ptr_ptr_var(opline->op1.var EXECUTE_DATA_CC); if (EXPECTED(Z_TYPE_P(object_ptr) == IS_ARRAY)) { try_assign_dim_array: + if (UNEXPECTED(EG(tracked_mutation_hooks_active))) { + should_publish_tracked_array = zend_maybe_track_hash_mutation(Z_ARRVAL_P(object_ptr), false); + } SEPARATE_ARRAY(object_ptr); if (IS_CV == IS_UNUSED) { value = RT_CONSTANT((opline+1), (opline+1)->op1); @@ -31497,6 +31695,9 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_ASSIGN_DIM_SP if (garbage) { GC_DTOR_NO_REF(garbage); } + if (UNEXPECTED(should_publish_tracked_array)) { + zend_maybe_track_hash_mutation(Z_ARRVAL_P(object_ptr), true); + } } else { if (EXPECTED(Z_ISREF_P(object_ptr))) { object_ptr = Z_REFVAL_P(object_ptr); @@ -31523,6 +31724,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_ASSIGN_DIM_SP } zend_assign_to_object_dim(obj, dim, value OPLINE_CC EXECUTE_DATA_CC); + ZEND_MAYBE_TRACK_OBJECT_MUTATION(obj); if (UNEXPECTED(GC_DELREF(obj) == 0)) { @@ -31592,12 +31794,16 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_ASSIGN_DIM_SP zval *variable_ptr; zval *dim; zend_refcounted *garbage = NULL; + bool should_publish_tracked_array = false; SAVE_OPLINE(); orig_object_ptr = object_ptr = _get_zval_ptr_ptr_var(opline->op1.var EXECUTE_DATA_CC); if (EXPECTED(Z_TYPE_P(object_ptr) == IS_ARRAY)) { try_assign_dim_array: + if (UNEXPECTED(EG(tracked_mutation_hooks_active))) { + should_publish_tracked_array = zend_maybe_track_hash_mutation(Z_ARRVAL_P(object_ptr), false); + } SEPARATE_ARRAY(object_ptr); if (IS_CV == IS_UNUSED) { value = _get_zval_ptr_tmp((opline+1)->op1.var EXECUTE_DATA_CC); @@ -31655,6 +31861,9 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_ASSIGN_DIM_SP if (garbage) { GC_DTOR_NO_REF(garbage); } + if (UNEXPECTED(should_publish_tracked_array)) { + zend_maybe_track_hash_mutation(Z_ARRVAL_P(object_ptr), true); + } } else { if (EXPECTED(Z_ISREF_P(object_ptr))) { object_ptr = Z_REFVAL_P(object_ptr); @@ -31681,6 +31890,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_ASSIGN_DIM_SP } zend_assign_to_object_dim(obj, dim, value OPLINE_CC EXECUTE_DATA_CC); + ZEND_MAYBE_TRACK_OBJECT_MUTATION(obj); zval_ptr_dtor_nogc(EX_VAR((opline+1)->op1.var)); if (UNEXPECTED(GC_DELREF(obj) == 0)) { @@ -31746,12 +31956,16 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_ASSIGN_DIM_SP zval *variable_ptr; zval *dim; zend_refcounted *garbage = NULL; + bool should_publish_tracked_array = false; SAVE_OPLINE(); orig_object_ptr = object_ptr = _get_zval_ptr_ptr_var(opline->op1.var EXECUTE_DATA_CC); if (EXPECTED(Z_TYPE_P(object_ptr) == IS_ARRAY)) { try_assign_dim_array: + if (UNEXPECTED(EG(tracked_mutation_hooks_active))) { + should_publish_tracked_array = zend_maybe_track_hash_mutation(Z_ARRVAL_P(object_ptr), false); + } SEPARATE_ARRAY(object_ptr); if (IS_CV == IS_UNUSED) { value = EX_VAR((opline+1)->op1.var); @@ -31809,6 +32023,9 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_ASSIGN_DIM_SP if (garbage) { GC_DTOR_NO_REF(garbage); } + if (UNEXPECTED(should_publish_tracked_array)) { + zend_maybe_track_hash_mutation(Z_ARRVAL_P(object_ptr), true); + } } else { if (EXPECTED(Z_ISREF_P(object_ptr))) { object_ptr = Z_REFVAL_P(object_ptr); @@ -31835,6 +32052,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_ASSIGN_DIM_SP } zend_assign_to_object_dim(obj, dim, value OPLINE_CC EXECUTE_DATA_CC); + ZEND_MAYBE_TRACK_OBJECT_MUTATION(obj); if (UNEXPECTED(GC_DELREF(obj) == 0)) { @@ -31997,6 +32215,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_ASSIGN_OBJ_RE { USE_OPLINE zval *property, *container, *value_ptr; + zend_object *zobj = NULL; SAVE_OPLINE(); @@ -32004,6 +32223,11 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_ASSIGN_OBJ_RE property = _get_zval_ptr_cv_BP_VAR_R(opline->op2.var EXECUTE_DATA_CC); value_ptr = _get_zval_ptr_ptr_var((opline+1)->op1.var EXECUTE_DATA_CC); + if (EXPECTED(Z_TYPE_P(container) == IS_OBJECT)) { + zobj = Z_OBJ_P(container); + } else if (Z_ISREF_P(container) && EXPECTED(Z_TYPE_P(Z_REFVAL_P(container)) == IS_OBJECT)) { + zobj = Z_OBJ_P(Z_REFVAL_P(container)); + } if (1) { if (IS_VAR == IS_UNUSED) { @@ -32023,6 +32247,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_ASSIGN_OBJ_RE zend_assign_to_property_reference(container, IS_VAR, property, IS_CV, value_ptr OPLINE_CC EXECUTE_DATA_CC); } + ZEND_MAYBE_TRACK_OBJECT_MUTATION(zobj); zval_ptr_dtor_nogc(EX_VAR(opline->op1.var)); @@ -32035,6 +32260,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_ASSIGN_OBJ_RE { USE_OPLINE zval *property, *container, *value_ptr; + zend_object *zobj = NULL; SAVE_OPLINE(); @@ -32042,6 +32268,11 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_ASSIGN_OBJ_RE property = _get_zval_ptr_cv_BP_VAR_R(opline->op2.var EXECUTE_DATA_CC); value_ptr = _get_zval_ptr_cv_BP_VAR_W((opline+1)->op1.var EXECUTE_DATA_CC); + if (EXPECTED(Z_TYPE_P(container) == IS_OBJECT)) { + zobj = Z_OBJ_P(container); + } else if (Z_ISREF_P(container) && EXPECTED(Z_TYPE_P(Z_REFVAL_P(container)) == IS_OBJECT)) { + zobj = Z_OBJ_P(Z_REFVAL_P(container)); + } if (1) { if (IS_VAR == IS_UNUSED) { @@ -32061,6 +32292,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_ASSIGN_OBJ_RE zend_assign_to_property_reference(container, IS_VAR, property, IS_CV, value_ptr OPLINE_CC EXECUTE_DATA_CC); } + ZEND_MAYBE_TRACK_OBJECT_MUTATION(zobj); zval_ptr_dtor_nogc(EX_VAR(opline->op1.var)); @@ -32348,6 +32580,8 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_UNSET_DIM_SPE zval *offset; zend_ulong hval; zend_string *key; + bool should_flush_array = false; + bool should_publish_tracked_array = false; SAVE_OPLINE(); container = _get_zval_ptr_ptr_var(opline->op1.var EXECUTE_DATA_CC); @@ -32358,6 +32592,9 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_UNSET_DIM_SPE HashTable *ht; unset_dim_array: + if (UNEXPECTED(EG(tracked_mutation_hooks_active))) { + should_publish_tracked_array = zend_maybe_track_hash_mutation(Z_ARRVAL_P(container), false); + } SEPARATE_ARRAY(container); ht = Z_ARRVAL_P(container); offset_again: @@ -32371,10 +32608,12 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_UNSET_DIM_SPE str_index_dim: ZEND_ASSERT(ht != &EG(symbol_table)); zend_hash_del(ht, key); + should_flush_array = true; } else if (EXPECTED(Z_TYPE_P(offset) == IS_LONG)) { hval = Z_LVAL_P(offset); num_index_dim: zend_hash_index_del(ht, hval); + should_flush_array = true; } else if ((IS_CV & (IS_VAR|IS_CV)) && EXPECTED(Z_TYPE_P(offset) == IS_REFERENCE)) { offset = Z_REFVAL_P(offset); goto offset_again; @@ -32411,6 +32650,9 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_UNSET_DIM_SPE } else { zend_illegal_array_offset_unset(offset); } + if (UNEXPECTED(should_flush_array && should_publish_tracked_array)) { + zend_maybe_track_hash_mutation(ht, true); + } break; } else if (Z_ISREF_P(container)) { container = Z_REFVAL_P(container); @@ -32429,6 +32671,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_UNSET_DIM_SPE offset++; } Z_OBJ_HT_P(container)->unset_dimension(Z_OBJ_P(container), offset); + ZEND_MAYBE_TRACK_OBJECT_MUTATION(Z_OBJ_P(container)); } else if (UNEXPECTED(Z_TYPE_P(container) == IS_STRING)) { zend_throw_error(NULL, "Cannot unset string offsets"); } else if (UNEXPECTED(Z_TYPE_P(container) > IS_FALSE)) { @@ -32449,6 +32692,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_UNSET_OBJ_SPE zval *container; zval *offset; zend_string *name, *tmp_name; + zend_object *zobj = NULL; SAVE_OPLINE(); container = _get_zval_ptr_ptr_var(opline->op1.var EXECUTE_DATA_CC); @@ -32469,6 +32713,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_UNSET_OBJ_SPE break; } } + zobj = Z_OBJ_P(container); if (IS_CV == IS_CONST) { name = Z_STR_P(offset); } else { @@ -32483,6 +32728,8 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_UNSET_OBJ_SPE } } while (0); + ZEND_MAYBE_TRACK_OBJECT_MUTATION(zobj); + zval_ptr_dtor_nogc(EX_VAR(opline->op1.var)); ZEND_VM_NEXT_OPCODE_CHECK_EXCEPTION(); @@ -32887,7 +33134,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_ASSIGN_OBJ_OP void *_cache_slot[3] = {0}; void **cache_slot; zend_property_info *prop_info; - zend_object *zobj; + zend_object *zobj = NULL; zend_string *name, *tmp_name; SAVE_OPLINE(); @@ -32962,6 +33209,8 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_ASSIGN_OBJ_OP } } while (0); + ZEND_MAYBE_TRACK_OBJECT_MUTATION(zobj); + FREE_OP((opline+1)->op1_type, (opline+1)->op1.var); @@ -32981,7 +33230,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_PRE_INC_OBJ_S void *_cache_slot[3] = {0}; void **cache_slot; zend_property_info *prop_info; - zend_object *zobj; + zend_object *zobj = NULL; zend_string *name, *tmp_name; SAVE_OPLINE(); @@ -33033,6 +33282,8 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_PRE_INC_OBJ_S } } while (0); + ZEND_MAYBE_TRACK_OBJECT_MUTATION(zobj); + @@ -33048,7 +33299,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_POST_INC_OBJ_ void *_cache_slot[3] = {0}; void **cache_slot; zend_property_info *prop_info; - zend_object *zobj; + zend_object *zobj = NULL; zend_string *name, *tmp_name; SAVE_OPLINE(); @@ -33098,6 +33349,8 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_POST_INC_OBJ_ } } while (0); + ZEND_MAYBE_TRACK_OBJECT_MUTATION(zobj); + @@ -33495,7 +33748,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_ASSIGN_OBJ_SP { USE_OPLINE zval *object, *value, tmp; - zend_object *zobj; + zend_object *zobj = NULL; zend_string *name, *tmp_name; zend_refcounted *garbage = NULL; @@ -33637,6 +33890,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_ASSIGN_OBJ_SP exit_assign_obj: + ZEND_MAYBE_TRACK_OBJECT_MUTATION_WITH_VALUE(zobj, value); if (garbage) { GC_DTOR_NO_REF(garbage); } @@ -33653,7 +33907,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_ASSIGN_OBJ_SP { USE_OPLINE zval *object, *value, tmp; - zend_object *zobj; + zend_object *zobj = NULL; zend_string *name, *tmp_name; zend_refcounted *garbage = NULL; @@ -33793,6 +34047,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_ASSIGN_OBJ_SP } zval_ptr_dtor_nogc(EX_VAR((opline+1)->op1.var)); exit_assign_obj: + ZEND_MAYBE_TRACK_OBJECT_MUTATION_WITH_VALUE(zobj, value); if (garbage) { GC_DTOR_NO_REF(garbage); } @@ -33809,7 +34064,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_ASSIGN_OBJ_SP { USE_OPLINE zval *object, *value, tmp; - zend_object *zobj; + zend_object *zobj = NULL; zend_string *name, *tmp_name; zend_refcounted *garbage = NULL; @@ -33951,6 +34206,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_ASSIGN_OBJ_SP exit_assign_obj: + ZEND_MAYBE_TRACK_OBJECT_MUTATION_WITH_VALUE(zobj, value); if (garbage) { GC_DTOR_NO_REF(garbage); } @@ -33967,6 +34223,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_ASSIGN_OBJ_RE { USE_OPLINE zval *property, *container, *value_ptr; + zend_object *zobj = NULL; SAVE_OPLINE(); @@ -33974,6 +34231,11 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_ASSIGN_OBJ_RE property = RT_CONSTANT(opline, opline->op2); value_ptr = _get_zval_ptr_ptr_var((opline+1)->op1.var EXECUTE_DATA_CC); + if (EXPECTED(Z_TYPE_P(container) == IS_OBJECT)) { + zobj = Z_OBJ_P(container); + } else if (Z_ISREF_P(container) && EXPECTED(Z_TYPE_P(Z_REFVAL_P(container)) == IS_OBJECT)) { + zobj = Z_OBJ_P(Z_REFVAL_P(container)); + } if (1) { if (IS_UNUSED == IS_UNUSED) { @@ -33993,6 +34255,8 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_ASSIGN_OBJ_RE zend_assign_to_property_reference(container, IS_UNUSED, property, IS_CONST, value_ptr OPLINE_CC EXECUTE_DATA_CC); } + ZEND_MAYBE_TRACK_OBJECT_MUTATION(zobj); + @@ -34005,6 +34269,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_ASSIGN_OBJ_RE { USE_OPLINE zval *property, *container, *value_ptr; + zend_object *zobj = NULL; SAVE_OPLINE(); @@ -34012,6 +34277,11 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_ASSIGN_OBJ_RE property = RT_CONSTANT(opline, opline->op2); value_ptr = _get_zval_ptr_cv_BP_VAR_W((opline+1)->op1.var EXECUTE_DATA_CC); + if (EXPECTED(Z_TYPE_P(container) == IS_OBJECT)) { + zobj = Z_OBJ_P(container); + } else if (Z_ISREF_P(container) && EXPECTED(Z_TYPE_P(Z_REFVAL_P(container)) == IS_OBJECT)) { + zobj = Z_OBJ_P(Z_REFVAL_P(container)); + } if (1) { if (IS_UNUSED == IS_UNUSED) { @@ -34031,6 +34301,8 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_ASSIGN_OBJ_RE zend_assign_to_property_reference(container, IS_UNUSED, property, IS_CONST, value_ptr OPLINE_CC EXECUTE_DATA_CC); } + ZEND_MAYBE_TRACK_OBJECT_MUTATION(zobj); + @@ -34639,6 +34911,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_UNSET_OBJ_SPE zval *container; zval *offset; zend_string *name, *tmp_name; + zend_object *zobj = NULL; SAVE_OPLINE(); container = &EX(This); @@ -34659,6 +34932,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_UNSET_OBJ_SPE break; } } + zobj = Z_OBJ_P(container); if (IS_CONST == IS_CONST) { name = Z_STR_P(offset); } else { @@ -34673,6 +34947,8 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_UNSET_OBJ_SPE } } while (0); + ZEND_MAYBE_TRACK_OBJECT_MUTATION(zobj); + @@ -34996,7 +35272,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_ASSIGN_OBJ_OP void *_cache_slot[3] = {0}; void **cache_slot; zend_property_info *prop_info; - zend_object *zobj; + zend_object *zobj = NULL; zend_string *name, *tmp_name; SAVE_OPLINE(); @@ -35071,6 +35347,8 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_ASSIGN_OBJ_OP } } while (0); + ZEND_MAYBE_TRACK_OBJECT_MUTATION(zobj); + FREE_OP((opline+1)->op1_type, (opline+1)->op1.var); zval_ptr_dtor_nogc(EX_VAR(opline->op2.var)); @@ -35089,7 +35367,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_PRE_INC_OBJ_S void *_cache_slot[3] = {0}; void **cache_slot; zend_property_info *prop_info; - zend_object *zobj; + zend_object *zobj = NULL; zend_string *name, *tmp_name; SAVE_OPLINE(); @@ -35141,6 +35419,8 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_PRE_INC_OBJ_S } } while (0); + ZEND_MAYBE_TRACK_OBJECT_MUTATION(zobj); + zval_ptr_dtor_nogc(EX_VAR(opline->op2.var)); @@ -35156,7 +35436,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_POST_INC_OBJ_ void *_cache_slot[3] = {0}; void **cache_slot; zend_property_info *prop_info; - zend_object *zobj; + zend_object *zobj = NULL; zend_string *name, *tmp_name; SAVE_OPLINE(); @@ -35206,6 +35486,8 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_POST_INC_OBJ_ } } while (0); + ZEND_MAYBE_TRACK_OBJECT_MUTATION(zobj); + zval_ptr_dtor_nogc(EX_VAR(opline->op2.var)); @@ -35593,7 +35875,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_ASSIGN_OBJ_SP { USE_OPLINE zval *object, *value, tmp; - zend_object *zobj; + zend_object *zobj = NULL; zend_string *name, *tmp_name; zend_refcounted *garbage = NULL; @@ -35735,6 +36017,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_ASSIGN_OBJ_SP exit_assign_obj: + ZEND_MAYBE_TRACK_OBJECT_MUTATION_WITH_VALUE(zobj, value); if (garbage) { GC_DTOR_NO_REF(garbage); } @@ -35750,7 +36033,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_ASSIGN_OBJ_SP { USE_OPLINE zval *object, *value, tmp; - zend_object *zobj; + zend_object *zobj = NULL; zend_string *name, *tmp_name; zend_refcounted *garbage = NULL; @@ -35890,6 +36173,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_ASSIGN_OBJ_SP } zval_ptr_dtor_nogc(EX_VAR((opline+1)->op1.var)); exit_assign_obj: + ZEND_MAYBE_TRACK_OBJECT_MUTATION_WITH_VALUE(zobj, value); if (garbage) { GC_DTOR_NO_REF(garbage); } @@ -35905,7 +36189,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_ASSIGN_OBJ_SP { USE_OPLINE zval *object, *value, tmp; - zend_object *zobj; + zend_object *zobj = NULL; zend_string *name, *tmp_name; zend_refcounted *garbage = NULL; @@ -36047,6 +36331,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_ASSIGN_OBJ_SP exit_assign_obj: + ZEND_MAYBE_TRACK_OBJECT_MUTATION_WITH_VALUE(zobj, value); if (garbage) { GC_DTOR_NO_REF(garbage); } @@ -36062,6 +36347,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_ASSIGN_OBJ_RE { USE_OPLINE zval *property, *container, *value_ptr; + zend_object *zobj = NULL; SAVE_OPLINE(); @@ -36069,6 +36355,11 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_ASSIGN_OBJ_RE property = _get_zval_ptr_tmp(opline->op2.var EXECUTE_DATA_CC); value_ptr = _get_zval_ptr_ptr_var((opline+1)->op1.var EXECUTE_DATA_CC); + if (EXPECTED(Z_TYPE_P(container) == IS_OBJECT)) { + zobj = Z_OBJ_P(container); + } else if (Z_ISREF_P(container) && EXPECTED(Z_TYPE_P(Z_REFVAL_P(container)) == IS_OBJECT)) { + zobj = Z_OBJ_P(Z_REFVAL_P(container)); + } if (1) { if (IS_UNUSED == IS_UNUSED) { @@ -36088,6 +36379,8 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_ASSIGN_OBJ_RE zend_assign_to_property_reference(container, IS_UNUSED, property, IS_TMP_VAR, value_ptr OPLINE_CC EXECUTE_DATA_CC); } + ZEND_MAYBE_TRACK_OBJECT_MUTATION(zobj); + zval_ptr_dtor_nogc(EX_VAR(opline->op2.var)); zval_ptr_dtor_nogc(EX_VAR((opline+1)->op1.var)); @@ -36099,6 +36392,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_ASSIGN_OBJ_RE { USE_OPLINE zval *property, *container, *value_ptr; + zend_object *zobj = NULL; SAVE_OPLINE(); @@ -36106,6 +36400,11 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_ASSIGN_OBJ_RE property = _get_zval_ptr_tmp(opline->op2.var EXECUTE_DATA_CC); value_ptr = _get_zval_ptr_cv_BP_VAR_W((opline+1)->op1.var EXECUTE_DATA_CC); + if (EXPECTED(Z_TYPE_P(container) == IS_OBJECT)) { + zobj = Z_OBJ_P(container); + } else if (Z_ISREF_P(container) && EXPECTED(Z_TYPE_P(Z_REFVAL_P(container)) == IS_OBJECT)) { + zobj = Z_OBJ_P(Z_REFVAL_P(container)); + } if (1) { if (IS_UNUSED == IS_UNUSED) { @@ -36125,6 +36424,8 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_ASSIGN_OBJ_RE zend_assign_to_property_reference(container, IS_UNUSED, property, IS_TMP_VAR, value_ptr OPLINE_CC EXECUTE_DATA_CC); } + ZEND_MAYBE_TRACK_OBJECT_MUTATION(zobj); + zval_ptr_dtor_nogc(EX_VAR(opline->op2.var)); @@ -36530,6 +36831,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_UNSET_OBJ_SPE zval *container; zval *offset; zend_string *name, *tmp_name; + zend_object *zobj = NULL; SAVE_OPLINE(); container = &EX(This); @@ -36550,6 +36852,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_UNSET_OBJ_SPE break; } } + zobj = Z_OBJ_P(container); if (IS_TMP_VAR == IS_CONST) { name = Z_STR_P(offset); } else { @@ -36564,6 +36867,8 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_UNSET_OBJ_SPE } } while (0); + ZEND_MAYBE_TRACK_OBJECT_MUTATION(zobj); + zval_ptr_dtor_nogc(EX_VAR(opline->op2.var)); @@ -37559,7 +37864,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_ASSIGN_OBJ_OP void *_cache_slot[3] = {0}; void **cache_slot; zend_property_info *prop_info; - zend_object *zobj; + zend_object *zobj = NULL; zend_string *name, *tmp_name; SAVE_OPLINE(); @@ -37634,6 +37939,8 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_ASSIGN_OBJ_OP } } while (0); + ZEND_MAYBE_TRACK_OBJECT_MUTATION(zobj); + FREE_OP((opline+1)->op1_type, (opline+1)->op1.var); @@ -37653,7 +37960,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_PRE_INC_OBJ_S void *_cache_slot[3] = {0}; void **cache_slot; zend_property_info *prop_info; - zend_object *zobj; + zend_object *zobj = NULL; zend_string *name, *tmp_name; SAVE_OPLINE(); @@ -37705,6 +38012,8 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_PRE_INC_OBJ_S } } while (0); + ZEND_MAYBE_TRACK_OBJECT_MUTATION(zobj); + @@ -37720,7 +38029,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_POST_INC_OBJ_ void *_cache_slot[3] = {0}; void **cache_slot; zend_property_info *prop_info; - zend_object *zobj; + zend_object *zobj = NULL; zend_string *name, *tmp_name; SAVE_OPLINE(); @@ -37770,6 +38079,8 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_POST_INC_OBJ_ } } while (0); + ZEND_MAYBE_TRACK_OBJECT_MUTATION(zobj); + @@ -38162,7 +38473,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_ASSIGN_OBJ_SP { USE_OPLINE zval *object, *value, tmp; - zend_object *zobj; + zend_object *zobj = NULL; zend_string *name, *tmp_name; zend_refcounted *garbage = NULL; @@ -38304,6 +38615,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_ASSIGN_OBJ_SP exit_assign_obj: + ZEND_MAYBE_TRACK_OBJECT_MUTATION_WITH_VALUE(zobj, value); if (garbage) { GC_DTOR_NO_REF(garbage); } @@ -38320,7 +38632,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_ASSIGN_OBJ_SP { USE_OPLINE zval *object, *value, tmp; - zend_object *zobj; + zend_object *zobj = NULL; zend_string *name, *tmp_name; zend_refcounted *garbage = NULL; @@ -38460,6 +38772,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_ASSIGN_OBJ_SP } zval_ptr_dtor_nogc(EX_VAR((opline+1)->op1.var)); exit_assign_obj: + ZEND_MAYBE_TRACK_OBJECT_MUTATION_WITH_VALUE(zobj, value); if (garbage) { GC_DTOR_NO_REF(garbage); } @@ -38476,7 +38789,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_ASSIGN_OBJ_SP { USE_OPLINE zval *object, *value, tmp; - zend_object *zobj; + zend_object *zobj = NULL; zend_string *name, *tmp_name; zend_refcounted *garbage = NULL; @@ -38618,6 +38931,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_ASSIGN_OBJ_SP exit_assign_obj: + ZEND_MAYBE_TRACK_OBJECT_MUTATION_WITH_VALUE(zobj, value); if (garbage) { GC_DTOR_NO_REF(garbage); } @@ -38634,6 +38948,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_ASSIGN_OBJ_RE { USE_OPLINE zval *property, *container, *value_ptr; + zend_object *zobj = NULL; SAVE_OPLINE(); @@ -38641,6 +38956,11 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_ASSIGN_OBJ_RE property = _get_zval_ptr_cv_BP_VAR_R(opline->op2.var EXECUTE_DATA_CC); value_ptr = _get_zval_ptr_ptr_var((opline+1)->op1.var EXECUTE_DATA_CC); + if (EXPECTED(Z_TYPE_P(container) == IS_OBJECT)) { + zobj = Z_OBJ_P(container); + } else if (Z_ISREF_P(container) && EXPECTED(Z_TYPE_P(Z_REFVAL_P(container)) == IS_OBJECT)) { + zobj = Z_OBJ_P(Z_REFVAL_P(container)); + } if (1) { if (IS_UNUSED == IS_UNUSED) { @@ -38660,6 +38980,8 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_ASSIGN_OBJ_RE zend_assign_to_property_reference(container, IS_UNUSED, property, IS_CV, value_ptr OPLINE_CC EXECUTE_DATA_CC); } + ZEND_MAYBE_TRACK_OBJECT_MUTATION(zobj); + @@ -38672,6 +38994,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_ASSIGN_OBJ_RE { USE_OPLINE zval *property, *container, *value_ptr; + zend_object *zobj = NULL; SAVE_OPLINE(); @@ -38679,6 +39002,11 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_ASSIGN_OBJ_RE property = _get_zval_ptr_cv_BP_VAR_R(opline->op2.var EXECUTE_DATA_CC); value_ptr = _get_zval_ptr_cv_BP_VAR_W((opline+1)->op1.var EXECUTE_DATA_CC); + if (EXPECTED(Z_TYPE_P(container) == IS_OBJECT)) { + zobj = Z_OBJ_P(container); + } else if (Z_ISREF_P(container) && EXPECTED(Z_TYPE_P(Z_REFVAL_P(container)) == IS_OBJECT)) { + zobj = Z_OBJ_P(Z_REFVAL_P(container)); + } if (1) { if (IS_UNUSED == IS_UNUSED) { @@ -38698,6 +39026,8 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_ASSIGN_OBJ_RE zend_assign_to_property_reference(container, IS_UNUSED, property, IS_CV, value_ptr OPLINE_CC EXECUTE_DATA_CC); } + ZEND_MAYBE_TRACK_OBJECT_MUTATION(zobj); + @@ -39115,6 +39445,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_UNSET_OBJ_SPE zval *container; zval *offset; zend_string *name, *tmp_name; + zend_object *zobj = NULL; SAVE_OPLINE(); container = &EX(This); @@ -39135,6 +39466,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_UNSET_OBJ_SPE break; } } + zobj = Z_OBJ_P(container); if (IS_CV == IS_CONST) { name = Z_STR_P(offset); } else { @@ -39149,6 +39481,8 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_UNSET_OBJ_SPE } } while (0); + ZEND_MAYBE_TRACK_OBJECT_MUTATION(zobj); + @@ -40888,6 +41222,10 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_BIND_STATIC_S SAVE_OPLINE(); + if (UNEXPECTED(zend_function_init_statics_hook != NULL)) { + zend_function_init_statics_hook(execute_data); + } + ht = ZEND_MAP_PTR_GET(EX(func)->op_array.static_variables_ptr); if (!ht) { ht = zend_array_dup(EX(func)->op_array.static_variables); @@ -40939,6 +41277,10 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_BIND_INIT_STA variable_ptr = EX_VAR(opline->op1.var); + if (UNEXPECTED(zend_function_init_statics_hook != NULL)) { + zend_function_init_statics_hook(execute_data); + } + ht = ZEND_MAP_PTR_GET(EX(func)->op_array.static_variables_ptr); if (!ht) { ZEND_VM_NEXT_OPCODE(); @@ -41658,7 +42000,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_ASSIGN_OBJ_OP void *_cache_slot[3] = {0}; void **cache_slot; zend_property_info *prop_info; - zend_object *zobj; + zend_object *zobj = NULL; zend_string *name, *tmp_name; SAVE_OPLINE(); @@ -41733,6 +42075,8 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_ASSIGN_OBJ_OP } } while (0); + ZEND_MAYBE_TRACK_OBJECT_MUTATION(zobj); + FREE_OP((opline+1)->op1_type, (opline+1)->op1.var); @@ -41749,12 +42093,16 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_ASSIGN_DIM_OP zval *var_ptr; zval *value, *container, *dim; HashTable *ht; + bool should_publish_tracked_array = false; SAVE_OPLINE(); container = EX_VAR(opline->op1.var); if (EXPECTED(Z_TYPE_P(container) == IS_ARRAY)) { assign_dim_op_array: + if (UNEXPECTED(EG(tracked_mutation_hooks_active))) { + should_publish_tracked_array = zend_maybe_track_hash_mutation(Z_ARRVAL_P(container), false); + } SEPARATE_ARRAY(container); ht = Z_ARRVAL_P(container); assign_dim_op_new_array: @@ -41794,6 +42142,9 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_ASSIGN_DIM_OP ZVAL_COPY(EX_VAR(opline->result.var), var_ptr); } FREE_OP((opline+1)->op1_type, (opline+1)->op1.var); + if (UNEXPECTED(should_publish_tracked_array)) { + zend_maybe_track_hash_mutation(Z_ARRVAL_P(container), true); + } } else { if (EXPECTED(Z_ISREF_P(container))) { container = Z_REFVAL_P(container); @@ -41810,6 +42161,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_ASSIGN_DIM_OP dim++; } zend_binary_assign_op_obj_dim(obj, dim OPLINE_CC EXECUTE_DATA_CC); + ZEND_MAYBE_TRACK_OBJECT_MUTATION(obj); } else if (EXPECTED(Z_TYPE_P(container) <= IS_FALSE)) { uint8_t old_type; @@ -41886,7 +42238,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_PRE_INC_OBJ_S void *_cache_slot[3] = {0}; void **cache_slot; zend_property_info *prop_info; - zend_object *zobj; + zend_object *zobj = NULL; zend_string *name, *tmp_name; SAVE_OPLINE(); @@ -41938,6 +42290,8 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_PRE_INC_OBJ_S } } while (0); + ZEND_MAYBE_TRACK_OBJECT_MUTATION(zobj); + @@ -41953,7 +42307,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_POST_INC_OBJ_ void *_cache_slot[3] = {0}; void **cache_slot; zend_property_info *prop_info; - zend_object *zobj; + zend_object *zobj = NULL; zend_string *name, *tmp_name; SAVE_OPLINE(); @@ -42003,6 +42357,8 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_POST_INC_OBJ_ } } while (0); + ZEND_MAYBE_TRACK_OBJECT_MUTATION(zobj); + @@ -42531,7 +42887,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_ASSIGN_OBJ_SP { USE_OPLINE zval *object, *value, tmp; - zend_object *zobj; + zend_object *zobj = NULL; zend_string *name, *tmp_name; zend_refcounted *garbage = NULL; @@ -42673,6 +43029,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_ASSIGN_OBJ_SP exit_assign_obj: + ZEND_MAYBE_TRACK_OBJECT_MUTATION_WITH_VALUE(zobj, value); if (garbage) { GC_DTOR_NO_REF(garbage); } @@ -42689,7 +43046,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_ASSIGN_OBJ_SP { USE_OPLINE zval *object, *value, tmp; - zend_object *zobj; + zend_object *zobj = NULL; zend_string *name, *tmp_name; zend_refcounted *garbage = NULL; @@ -42829,6 +43186,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_ASSIGN_OBJ_SP } zval_ptr_dtor_nogc(EX_VAR((opline+1)->op1.var)); exit_assign_obj: + ZEND_MAYBE_TRACK_OBJECT_MUTATION_WITH_VALUE(zobj, value); if (garbage) { GC_DTOR_NO_REF(garbage); } @@ -42845,7 +43203,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_ASSIGN_OBJ_SP { USE_OPLINE zval *object, *value, tmp; - zend_object *zobj; + zend_object *zobj = NULL; zend_string *name, *tmp_name; zend_refcounted *garbage = NULL; @@ -42987,6 +43345,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_ASSIGN_OBJ_SP exit_assign_obj: + ZEND_MAYBE_TRACK_OBJECT_MUTATION_WITH_VALUE(zobj, value); if (garbage) { GC_DTOR_NO_REF(garbage); } @@ -43007,12 +43366,16 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_ASSIGN_DIM_SP zval *variable_ptr; zval *dim; zend_refcounted *garbage = NULL; + bool should_publish_tracked_array = false; SAVE_OPLINE(); orig_object_ptr = object_ptr = EX_VAR(opline->op1.var); if (EXPECTED(Z_TYPE_P(object_ptr) == IS_ARRAY)) { try_assign_dim_array: + if (UNEXPECTED(EG(tracked_mutation_hooks_active))) { + should_publish_tracked_array = zend_maybe_track_hash_mutation(Z_ARRVAL_P(object_ptr), false); + } SEPARATE_ARRAY(object_ptr); if (IS_CONST == IS_UNUSED) { value = RT_CONSTANT((opline+1), (opline+1)->op1); @@ -43070,6 +43433,9 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_ASSIGN_DIM_SP if (garbage) { GC_DTOR_NO_REF(garbage); } + if (UNEXPECTED(should_publish_tracked_array)) { + zend_maybe_track_hash_mutation(Z_ARRVAL_P(object_ptr), true); + } } else { if (EXPECTED(Z_ISREF_P(object_ptr))) { object_ptr = Z_REFVAL_P(object_ptr); @@ -43096,6 +43462,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_ASSIGN_DIM_SP } zend_assign_to_object_dim(obj, dim, value OPLINE_CC EXECUTE_DATA_CC); + ZEND_MAYBE_TRACK_OBJECT_MUTATION(obj); if (UNEXPECTED(GC_DELREF(obj) == 0)) { @@ -43166,12 +43533,16 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_ASSIGN_DIM_SP zval *variable_ptr; zval *dim; zend_refcounted *garbage = NULL; + bool should_publish_tracked_array = false; SAVE_OPLINE(); orig_object_ptr = object_ptr = EX_VAR(opline->op1.var); if (EXPECTED(Z_TYPE_P(object_ptr) == IS_ARRAY)) { try_assign_dim_array: + if (UNEXPECTED(EG(tracked_mutation_hooks_active))) { + should_publish_tracked_array = zend_maybe_track_hash_mutation(Z_ARRVAL_P(object_ptr), false); + } SEPARATE_ARRAY(object_ptr); if (IS_CONST == IS_UNUSED) { value = _get_zval_ptr_tmp((opline+1)->op1.var EXECUTE_DATA_CC); @@ -43229,6 +43600,9 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_ASSIGN_DIM_SP if (garbage) { GC_DTOR_NO_REF(garbage); } + if (UNEXPECTED(should_publish_tracked_array)) { + zend_maybe_track_hash_mutation(Z_ARRVAL_P(object_ptr), true); + } } else { if (EXPECTED(Z_ISREF_P(object_ptr))) { object_ptr = Z_REFVAL_P(object_ptr); @@ -43255,6 +43629,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_ASSIGN_DIM_SP } zend_assign_to_object_dim(obj, dim, value OPLINE_CC EXECUTE_DATA_CC); + ZEND_MAYBE_TRACK_OBJECT_MUTATION(obj); zval_ptr_dtor_nogc(EX_VAR((opline+1)->op1.var)); if (UNEXPECTED(GC_DELREF(obj) == 0)) { @@ -43321,12 +43696,16 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_ASSIGN_DIM_SP zval *variable_ptr; zval *dim; zend_refcounted *garbage = NULL; + bool should_publish_tracked_array = false; SAVE_OPLINE(); orig_object_ptr = object_ptr = EX_VAR(opline->op1.var); if (EXPECTED(Z_TYPE_P(object_ptr) == IS_ARRAY)) { try_assign_dim_array: + if (UNEXPECTED(EG(tracked_mutation_hooks_active))) { + should_publish_tracked_array = zend_maybe_track_hash_mutation(Z_ARRVAL_P(object_ptr), false); + } SEPARATE_ARRAY(object_ptr); if (IS_CONST == IS_UNUSED) { value = EX_VAR((opline+1)->op1.var); @@ -43384,6 +43763,9 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_ASSIGN_DIM_SP if (garbage) { GC_DTOR_NO_REF(garbage); } + if (UNEXPECTED(should_publish_tracked_array)) { + zend_maybe_track_hash_mutation(Z_ARRVAL_P(object_ptr), true); + } } else { if (EXPECTED(Z_ISREF_P(object_ptr))) { object_ptr = Z_REFVAL_P(object_ptr); @@ -43410,6 +43792,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_ASSIGN_DIM_SP } zend_assign_to_object_dim(obj, dim, value OPLINE_CC EXECUTE_DATA_CC); + ZEND_MAYBE_TRACK_OBJECT_MUTATION(obj); if (UNEXPECTED(GC_DELREF(obj) == 0)) { @@ -43536,6 +43919,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_ASSIGN_OBJ_RE { USE_OPLINE zval *property, *container, *value_ptr; + zend_object *zobj = NULL; SAVE_OPLINE(); @@ -43543,6 +43927,11 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_ASSIGN_OBJ_RE property = RT_CONSTANT(opline, opline->op2); value_ptr = _get_zval_ptr_ptr_var((opline+1)->op1.var EXECUTE_DATA_CC); + if (EXPECTED(Z_TYPE_P(container) == IS_OBJECT)) { + zobj = Z_OBJ_P(container); + } else if (Z_ISREF_P(container) && EXPECTED(Z_TYPE_P(Z_REFVAL_P(container)) == IS_OBJECT)) { + zobj = Z_OBJ_P(Z_REFVAL_P(container)); + } if (1) { if (IS_CV == IS_UNUSED) { @@ -43562,6 +43951,8 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_ASSIGN_OBJ_RE zend_assign_to_property_reference(container, IS_CV, property, IS_CONST, value_ptr OPLINE_CC EXECUTE_DATA_CC); } + ZEND_MAYBE_TRACK_OBJECT_MUTATION(zobj); + @@ -43574,6 +43965,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_ASSIGN_OBJ_RE { USE_OPLINE zval *property, *container, *value_ptr; + zend_object *zobj = NULL; SAVE_OPLINE(); @@ -43581,6 +43973,11 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_ASSIGN_OBJ_RE property = RT_CONSTANT(opline, opline->op2); value_ptr = _get_zval_ptr_cv_BP_VAR_W((opline+1)->op1.var EXECUTE_DATA_CC); + if (EXPECTED(Z_TYPE_P(container) == IS_OBJECT)) { + zobj = Z_OBJ_P(container); + } else if (Z_ISREF_P(container) && EXPECTED(Z_TYPE_P(Z_REFVAL_P(container)) == IS_OBJECT)) { + zobj = Z_OBJ_P(Z_REFVAL_P(container)); + } if (1) { if (IS_CV == IS_UNUSED) { @@ -43600,6 +43997,8 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_ASSIGN_OBJ_RE zend_assign_to_property_reference(container, IS_CV, property, IS_CONST, value_ptr OPLINE_CC EXECUTE_DATA_CC); } + ZEND_MAYBE_TRACK_OBJECT_MUTATION(zobj); + @@ -44186,6 +44585,8 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_UNSET_DIM_SPE zval *offset; zend_ulong hval; zend_string *key; + bool should_flush_array = false; + bool should_publish_tracked_array = false; SAVE_OPLINE(); container = EX_VAR(opline->op1.var); @@ -44196,6 +44597,9 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_UNSET_DIM_SPE HashTable *ht; unset_dim_array: + if (UNEXPECTED(EG(tracked_mutation_hooks_active))) { + should_publish_tracked_array = zend_maybe_track_hash_mutation(Z_ARRVAL_P(container), false); + } SEPARATE_ARRAY(container); ht = Z_ARRVAL_P(container); offset_again: @@ -44209,10 +44613,12 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_UNSET_DIM_SPE str_index_dim: ZEND_ASSERT(ht != &EG(symbol_table)); zend_hash_del(ht, key); + should_flush_array = true; } else if (EXPECTED(Z_TYPE_P(offset) == IS_LONG)) { hval = Z_LVAL_P(offset); num_index_dim: zend_hash_index_del(ht, hval); + should_flush_array = true; } else if ((IS_CONST & (IS_VAR|IS_CV)) && EXPECTED(Z_TYPE_P(offset) == IS_REFERENCE)) { offset = Z_REFVAL_P(offset); goto offset_again; @@ -44249,6 +44655,9 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_UNSET_DIM_SPE } else { zend_illegal_array_offset_unset(offset); } + if (UNEXPECTED(should_flush_array && should_publish_tracked_array)) { + zend_maybe_track_hash_mutation(ht, true); + } break; } else if (Z_ISREF_P(container)) { container = Z_REFVAL_P(container); @@ -44267,6 +44676,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_UNSET_DIM_SPE offset++; } Z_OBJ_HT_P(container)->unset_dimension(Z_OBJ_P(container), offset); + ZEND_MAYBE_TRACK_OBJECT_MUTATION(Z_OBJ_P(container)); } else if (UNEXPECTED(Z_TYPE_P(container) == IS_STRING)) { zend_throw_error(NULL, "Cannot unset string offsets"); } else if (UNEXPECTED(Z_TYPE_P(container) > IS_FALSE)) { @@ -44288,6 +44698,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_UNSET_OBJ_SPE zval *container; zval *offset; zend_string *name, *tmp_name; + zend_object *zobj = NULL; SAVE_OPLINE(); container = EX_VAR(opline->op1.var); @@ -44308,6 +44719,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_UNSET_OBJ_SPE break; } } + zobj = Z_OBJ_P(container); if (IS_CONST == IS_CONST) { name = Z_STR_P(offset); } else { @@ -44322,6 +44734,8 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_UNSET_OBJ_SPE } } while (0); + ZEND_MAYBE_TRACK_OBJECT_MUTATION(zobj); + @@ -45497,7 +45911,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_ASSIGN_OBJ_OP void *_cache_slot[3] = {0}; void **cache_slot; zend_property_info *prop_info; - zend_object *zobj; + zend_object *zobj = NULL; zend_string *name, *tmp_name; SAVE_OPLINE(); @@ -45572,6 +45986,8 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_ASSIGN_OBJ_OP } } while (0); + ZEND_MAYBE_TRACK_OBJECT_MUTATION(zobj); + FREE_OP((opline+1)->op1_type, (opline+1)->op1.var); zval_ptr_dtor_nogc(EX_VAR(opline->op2.var)); @@ -45587,12 +46003,16 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_ASSIGN_DIM_OP zval *var_ptr; zval *value, *container, *dim; HashTable *ht; + bool should_publish_tracked_array = false; SAVE_OPLINE(); container = EX_VAR(opline->op1.var); if (EXPECTED(Z_TYPE_P(container) == IS_ARRAY)) { assign_dim_op_array: + if (UNEXPECTED(EG(tracked_mutation_hooks_active))) { + should_publish_tracked_array = zend_maybe_track_hash_mutation(Z_ARRVAL_P(container), false); + } SEPARATE_ARRAY(container); ht = Z_ARRVAL_P(container); assign_dim_op_new_array: @@ -45632,6 +46052,9 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_ASSIGN_DIM_OP ZVAL_COPY(EX_VAR(opline->result.var), var_ptr); } FREE_OP((opline+1)->op1_type, (opline+1)->op1.var); + if (UNEXPECTED(should_publish_tracked_array)) { + zend_maybe_track_hash_mutation(Z_ARRVAL_P(container), true); + } } else { if (EXPECTED(Z_ISREF_P(container))) { container = Z_REFVAL_P(container); @@ -45648,6 +46071,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_ASSIGN_DIM_OP dim++; } zend_binary_assign_op_obj_dim(obj, dim OPLINE_CC EXECUTE_DATA_CC); + ZEND_MAYBE_TRACK_OBJECT_MUTATION(obj); } else if (EXPECTED(Z_TYPE_P(container) <= IS_FALSE)) { uint8_t old_type; @@ -45724,7 +46148,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_PRE_INC_OBJ_S void *_cache_slot[3] = {0}; void **cache_slot; zend_property_info *prop_info; - zend_object *zobj; + zend_object *zobj = NULL; zend_string *name, *tmp_name; SAVE_OPLINE(); @@ -45776,6 +46200,8 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_PRE_INC_OBJ_S } } while (0); + ZEND_MAYBE_TRACK_OBJECT_MUTATION(zobj); + zval_ptr_dtor_nogc(EX_VAR(opline->op2.var)); @@ -45791,7 +46217,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_POST_INC_OBJ_ void *_cache_slot[3] = {0}; void **cache_slot; zend_property_info *prop_info; - zend_object *zobj; + zend_object *zobj = NULL; zend_string *name, *tmp_name; SAVE_OPLINE(); @@ -45841,6 +46267,8 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_POST_INC_OBJ_ } } while (0); + ZEND_MAYBE_TRACK_OBJECT_MUTATION(zobj); + zval_ptr_dtor_nogc(EX_VAR(opline->op2.var)); @@ -46354,7 +46782,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_ASSIGN_OBJ_SP { USE_OPLINE zval *object, *value, tmp; - zend_object *zobj; + zend_object *zobj = NULL; zend_string *name, *tmp_name; zend_refcounted *garbage = NULL; @@ -46496,6 +46924,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_ASSIGN_OBJ_SP exit_assign_obj: + ZEND_MAYBE_TRACK_OBJECT_MUTATION_WITH_VALUE(zobj, value); if (garbage) { GC_DTOR_NO_REF(garbage); } @@ -46511,7 +46940,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_ASSIGN_OBJ_SP { USE_OPLINE zval *object, *value, tmp; - zend_object *zobj; + zend_object *zobj = NULL; zend_string *name, *tmp_name; zend_refcounted *garbage = NULL; @@ -46651,6 +47080,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_ASSIGN_OBJ_SP } zval_ptr_dtor_nogc(EX_VAR((opline+1)->op1.var)); exit_assign_obj: + ZEND_MAYBE_TRACK_OBJECT_MUTATION_WITH_VALUE(zobj, value); if (garbage) { GC_DTOR_NO_REF(garbage); } @@ -46666,7 +47096,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_ASSIGN_OBJ_SP { USE_OPLINE zval *object, *value, tmp; - zend_object *zobj; + zend_object *zobj = NULL; zend_string *name, *tmp_name; zend_refcounted *garbage = NULL; @@ -46808,6 +47238,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_ASSIGN_OBJ_SP exit_assign_obj: + ZEND_MAYBE_TRACK_OBJECT_MUTATION_WITH_VALUE(zobj, value); if (garbage) { GC_DTOR_NO_REF(garbage); } @@ -46827,12 +47258,16 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_ASSIGN_DIM_SP zval *variable_ptr; zval *dim; zend_refcounted *garbage = NULL; + bool should_publish_tracked_array = false; SAVE_OPLINE(); orig_object_ptr = object_ptr = EX_VAR(opline->op1.var); if (EXPECTED(Z_TYPE_P(object_ptr) == IS_ARRAY)) { try_assign_dim_array: + if (UNEXPECTED(EG(tracked_mutation_hooks_active))) { + should_publish_tracked_array = zend_maybe_track_hash_mutation(Z_ARRVAL_P(object_ptr), false); + } SEPARATE_ARRAY(object_ptr); if (IS_TMP_VAR == IS_UNUSED) { value = RT_CONSTANT((opline+1), (opline+1)->op1); @@ -46890,6 +47325,9 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_ASSIGN_DIM_SP if (garbage) { GC_DTOR_NO_REF(garbage); } + if (UNEXPECTED(should_publish_tracked_array)) { + zend_maybe_track_hash_mutation(Z_ARRVAL_P(object_ptr), true); + } } else { if (EXPECTED(Z_ISREF_P(object_ptr))) { object_ptr = Z_REFVAL_P(object_ptr); @@ -46916,6 +47354,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_ASSIGN_DIM_SP } zend_assign_to_object_dim(obj, dim, value OPLINE_CC EXECUTE_DATA_CC); + ZEND_MAYBE_TRACK_OBJECT_MUTATION(obj); if (UNEXPECTED(GC_DELREF(obj) == 0)) { @@ -46985,12 +47424,16 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_ASSIGN_DIM_SP zval *variable_ptr; zval *dim; zend_refcounted *garbage = NULL; + bool should_publish_tracked_array = false; SAVE_OPLINE(); orig_object_ptr = object_ptr = EX_VAR(opline->op1.var); if (EXPECTED(Z_TYPE_P(object_ptr) == IS_ARRAY)) { try_assign_dim_array: + if (UNEXPECTED(EG(tracked_mutation_hooks_active))) { + should_publish_tracked_array = zend_maybe_track_hash_mutation(Z_ARRVAL_P(object_ptr), false); + } SEPARATE_ARRAY(object_ptr); if (IS_TMP_VAR == IS_UNUSED) { value = _get_zval_ptr_tmp((opline+1)->op1.var EXECUTE_DATA_CC); @@ -47048,6 +47491,9 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_ASSIGN_DIM_SP if (garbage) { GC_DTOR_NO_REF(garbage); } + if (UNEXPECTED(should_publish_tracked_array)) { + zend_maybe_track_hash_mutation(Z_ARRVAL_P(object_ptr), true); + } } else { if (EXPECTED(Z_ISREF_P(object_ptr))) { object_ptr = Z_REFVAL_P(object_ptr); @@ -47074,6 +47520,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_ASSIGN_DIM_SP } zend_assign_to_object_dim(obj, dim, value OPLINE_CC EXECUTE_DATA_CC); + ZEND_MAYBE_TRACK_OBJECT_MUTATION(obj); zval_ptr_dtor_nogc(EX_VAR((opline+1)->op1.var)); if (UNEXPECTED(GC_DELREF(obj) == 0)) { @@ -47139,12 +47586,16 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_ASSIGN_DIM_SP zval *variable_ptr; zval *dim; zend_refcounted *garbage = NULL; + bool should_publish_tracked_array = false; SAVE_OPLINE(); orig_object_ptr = object_ptr = EX_VAR(opline->op1.var); if (EXPECTED(Z_TYPE_P(object_ptr) == IS_ARRAY)) { try_assign_dim_array: + if (UNEXPECTED(EG(tracked_mutation_hooks_active))) { + should_publish_tracked_array = zend_maybe_track_hash_mutation(Z_ARRVAL_P(object_ptr), false); + } SEPARATE_ARRAY(object_ptr); if (IS_TMP_VAR == IS_UNUSED) { value = EX_VAR((opline+1)->op1.var); @@ -47202,6 +47653,9 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_ASSIGN_DIM_SP if (garbage) { GC_DTOR_NO_REF(garbage); } + if (UNEXPECTED(should_publish_tracked_array)) { + zend_maybe_track_hash_mutation(Z_ARRVAL_P(object_ptr), true); + } } else { if (EXPECTED(Z_ISREF_P(object_ptr))) { object_ptr = Z_REFVAL_P(object_ptr); @@ -47228,6 +47682,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_ASSIGN_DIM_SP } zend_assign_to_object_dim(obj, dim, value OPLINE_CC EXECUTE_DATA_CC); + ZEND_MAYBE_TRACK_OBJECT_MUTATION(obj); if (UNEXPECTED(GC_DELREF(obj) == 0)) { @@ -47353,6 +47808,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_ASSIGN_OBJ_RE { USE_OPLINE zval *property, *container, *value_ptr; + zend_object *zobj = NULL; SAVE_OPLINE(); @@ -47360,6 +47816,11 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_ASSIGN_OBJ_RE property = _get_zval_ptr_tmp(opline->op2.var EXECUTE_DATA_CC); value_ptr = _get_zval_ptr_ptr_var((opline+1)->op1.var EXECUTE_DATA_CC); + if (EXPECTED(Z_TYPE_P(container) == IS_OBJECT)) { + zobj = Z_OBJ_P(container); + } else if (Z_ISREF_P(container) && EXPECTED(Z_TYPE_P(Z_REFVAL_P(container)) == IS_OBJECT)) { + zobj = Z_OBJ_P(Z_REFVAL_P(container)); + } if (1) { if (IS_CV == IS_UNUSED) { @@ -47379,6 +47840,8 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_ASSIGN_OBJ_RE zend_assign_to_property_reference(container, IS_CV, property, IS_TMP_VAR, value_ptr OPLINE_CC EXECUTE_DATA_CC); } + ZEND_MAYBE_TRACK_OBJECT_MUTATION(zobj); + zval_ptr_dtor_nogc(EX_VAR(opline->op2.var)); zval_ptr_dtor_nogc(EX_VAR((opline+1)->op1.var)); @@ -47390,6 +47853,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_ASSIGN_OBJ_RE { USE_OPLINE zval *property, *container, *value_ptr; + zend_object *zobj = NULL; SAVE_OPLINE(); @@ -47397,6 +47861,11 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_ASSIGN_OBJ_RE property = _get_zval_ptr_tmp(opline->op2.var EXECUTE_DATA_CC); value_ptr = _get_zval_ptr_cv_BP_VAR_W((opline+1)->op1.var EXECUTE_DATA_CC); + if (EXPECTED(Z_TYPE_P(container) == IS_OBJECT)) { + zobj = Z_OBJ_P(container); + } else if (Z_ISREF_P(container) && EXPECTED(Z_TYPE_P(Z_REFVAL_P(container)) == IS_OBJECT)) { + zobj = Z_OBJ_P(Z_REFVAL_P(container)); + } if (1) { if (IS_CV == IS_UNUSED) { @@ -47416,6 +47885,8 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_ASSIGN_OBJ_RE zend_assign_to_property_reference(container, IS_CV, property, IS_TMP_VAR, value_ptr OPLINE_CC EXECUTE_DATA_CC); } + ZEND_MAYBE_TRACK_OBJECT_MUTATION(zobj); + zval_ptr_dtor_nogc(EX_VAR(opline->op2.var)); @@ -47848,6 +48319,8 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_UNSET_DIM_SPE zval *offset; zend_ulong hval; zend_string *key; + bool should_flush_array = false; + bool should_publish_tracked_array = false; SAVE_OPLINE(); container = EX_VAR(opline->op1.var); @@ -47858,6 +48331,9 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_UNSET_DIM_SPE HashTable *ht; unset_dim_array: + if (UNEXPECTED(EG(tracked_mutation_hooks_active))) { + should_publish_tracked_array = zend_maybe_track_hash_mutation(Z_ARRVAL_P(container), false); + } SEPARATE_ARRAY(container); ht = Z_ARRVAL_P(container); offset_again: @@ -47871,10 +48347,12 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_UNSET_DIM_SPE str_index_dim: ZEND_ASSERT(ht != &EG(symbol_table)); zend_hash_del(ht, key); + should_flush_array = true; } else if (EXPECTED(Z_TYPE_P(offset) == IS_LONG)) { hval = Z_LVAL_P(offset); num_index_dim: zend_hash_index_del(ht, hval); + should_flush_array = true; } else if ((IS_TMP_VAR & (IS_VAR|IS_CV)) && EXPECTED(Z_TYPE_P(offset) == IS_REFERENCE)) { offset = Z_REFVAL_P(offset); goto offset_again; @@ -47911,6 +48389,9 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_UNSET_DIM_SPE } else { zend_illegal_array_offset_unset(offset); } + if (UNEXPECTED(should_flush_array && should_publish_tracked_array)) { + zend_maybe_track_hash_mutation(ht, true); + } break; } else if (Z_ISREF_P(container)) { container = Z_REFVAL_P(container); @@ -47929,6 +48410,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_UNSET_DIM_SPE offset++; } Z_OBJ_HT_P(container)->unset_dimension(Z_OBJ_P(container), offset); + ZEND_MAYBE_TRACK_OBJECT_MUTATION(Z_OBJ_P(container)); } else if (UNEXPECTED(Z_TYPE_P(container) == IS_STRING)) { zend_throw_error(NULL, "Cannot unset string offsets"); } else if (UNEXPECTED(Z_TYPE_P(container) > IS_FALSE)) { @@ -47950,6 +48432,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_UNSET_OBJ_SPE zval *container; zval *offset; zend_string *name, *tmp_name; + zend_object *zobj = NULL; SAVE_OPLINE(); container = EX_VAR(opline->op1.var); @@ -47970,6 +48453,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_UNSET_OBJ_SPE break; } } + zobj = Z_OBJ_P(container); if (IS_TMP_VAR == IS_CONST) { name = Z_STR_P(offset); } else { @@ -47984,6 +48468,8 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_UNSET_OBJ_SPE } } while (0); + ZEND_MAYBE_TRACK_OBJECT_MUTATION(zobj); + zval_ptr_dtor_nogc(EX_VAR(opline->op2.var)); @@ -48370,12 +48856,16 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_ASSIGN_DIM_OP zval *var_ptr; zval *value, *container, *dim; HashTable *ht; + bool should_publish_tracked_array = false; SAVE_OPLINE(); container = EX_VAR(opline->op1.var); if (EXPECTED(Z_TYPE_P(container) == IS_ARRAY)) { assign_dim_op_array: + if (UNEXPECTED(EG(tracked_mutation_hooks_active))) { + should_publish_tracked_array = zend_maybe_track_hash_mutation(Z_ARRVAL_P(container), false); + } SEPARATE_ARRAY(container); ht = Z_ARRVAL_P(container); assign_dim_op_new_array: @@ -48415,6 +48905,9 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_ASSIGN_DIM_OP ZVAL_COPY(EX_VAR(opline->result.var), var_ptr); } FREE_OP((opline+1)->op1_type, (opline+1)->op1.var); + if (UNEXPECTED(should_publish_tracked_array)) { + zend_maybe_track_hash_mutation(Z_ARRVAL_P(container), true); + } } else { if (EXPECTED(Z_ISREF_P(container))) { container = Z_REFVAL_P(container); @@ -48431,6 +48924,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_ASSIGN_DIM_OP dim++; } zend_binary_assign_op_obj_dim(obj, dim OPLINE_CC EXECUTE_DATA_CC); + ZEND_MAYBE_TRACK_OBJECT_MUTATION(obj); } else if (EXPECTED(Z_TYPE_P(container) <= IS_FALSE)) { uint8_t old_type; @@ -48668,12 +49162,16 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_ASSIGN_DIM_SP zval *variable_ptr; zval *dim; zend_refcounted *garbage = NULL; + bool should_publish_tracked_array = false; SAVE_OPLINE(); orig_object_ptr = object_ptr = EX_VAR(opline->op1.var); if (EXPECTED(Z_TYPE_P(object_ptr) == IS_ARRAY)) { try_assign_dim_array: + if (UNEXPECTED(EG(tracked_mutation_hooks_active))) { + should_publish_tracked_array = zend_maybe_track_hash_mutation(Z_ARRVAL_P(object_ptr), false); + } SEPARATE_ARRAY(object_ptr); if (IS_UNUSED == IS_UNUSED) { value = RT_CONSTANT((opline+1), (opline+1)->op1); @@ -48731,6 +49229,9 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_ASSIGN_DIM_SP if (garbage) { GC_DTOR_NO_REF(garbage); } + if (UNEXPECTED(should_publish_tracked_array)) { + zend_maybe_track_hash_mutation(Z_ARRVAL_P(object_ptr), true); + } } else { if (EXPECTED(Z_ISREF_P(object_ptr))) { object_ptr = Z_REFVAL_P(object_ptr); @@ -48757,6 +49258,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_ASSIGN_DIM_SP } zend_assign_to_object_dim(obj, dim, value OPLINE_CC EXECUTE_DATA_CC); + ZEND_MAYBE_TRACK_OBJECT_MUTATION(obj); if (UNEXPECTED(GC_DELREF(obj) == 0)) { @@ -48827,12 +49329,16 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_ASSIGN_DIM_SP zval *variable_ptr; zval *dim; zend_refcounted *garbage = NULL; + bool should_publish_tracked_array = false; SAVE_OPLINE(); orig_object_ptr = object_ptr = EX_VAR(opline->op1.var); if (EXPECTED(Z_TYPE_P(object_ptr) == IS_ARRAY)) { try_assign_dim_array: + if (UNEXPECTED(EG(tracked_mutation_hooks_active))) { + should_publish_tracked_array = zend_maybe_track_hash_mutation(Z_ARRVAL_P(object_ptr), false); + } SEPARATE_ARRAY(object_ptr); if (IS_UNUSED == IS_UNUSED) { value = _get_zval_ptr_tmp((opline+1)->op1.var EXECUTE_DATA_CC); @@ -48890,6 +49396,9 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_ASSIGN_DIM_SP if (garbage) { GC_DTOR_NO_REF(garbage); } + if (UNEXPECTED(should_publish_tracked_array)) { + zend_maybe_track_hash_mutation(Z_ARRVAL_P(object_ptr), true); + } } else { if (EXPECTED(Z_ISREF_P(object_ptr))) { object_ptr = Z_REFVAL_P(object_ptr); @@ -48916,6 +49425,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_ASSIGN_DIM_SP } zend_assign_to_object_dim(obj, dim, value OPLINE_CC EXECUTE_DATA_CC); + ZEND_MAYBE_TRACK_OBJECT_MUTATION(obj); zval_ptr_dtor_nogc(EX_VAR((opline+1)->op1.var)); if (UNEXPECTED(GC_DELREF(obj) == 0)) { @@ -48982,12 +49492,16 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_ASSIGN_DIM_SP zval *variable_ptr; zval *dim; zend_refcounted *garbage = NULL; + bool should_publish_tracked_array = false; SAVE_OPLINE(); orig_object_ptr = object_ptr = EX_VAR(opline->op1.var); if (EXPECTED(Z_TYPE_P(object_ptr) == IS_ARRAY)) { try_assign_dim_array: + if (UNEXPECTED(EG(tracked_mutation_hooks_active))) { + should_publish_tracked_array = zend_maybe_track_hash_mutation(Z_ARRVAL_P(object_ptr), false); + } SEPARATE_ARRAY(object_ptr); if (IS_UNUSED == IS_UNUSED) { value = EX_VAR((opline+1)->op1.var); @@ -49045,6 +49559,9 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_ASSIGN_DIM_SP if (garbage) { GC_DTOR_NO_REF(garbage); } + if (UNEXPECTED(should_publish_tracked_array)) { + zend_maybe_track_hash_mutation(Z_ARRVAL_P(object_ptr), true); + } } else { if (EXPECTED(Z_ISREF_P(object_ptr))) { object_ptr = Z_REFVAL_P(object_ptr); @@ -49071,6 +49588,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_ASSIGN_DIM_SP } zend_assign_to_object_dim(obj, dim, value OPLINE_CC EXECUTE_DATA_CC); + ZEND_MAYBE_TRACK_OBJECT_MUTATION(obj); if (UNEXPECTED(GC_DELREF(obj) == 0)) { @@ -50602,7 +51120,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_ASSIGN_OBJ_OP void *_cache_slot[3] = {0}; void **cache_slot; zend_property_info *prop_info; - zend_object *zobj; + zend_object *zobj = NULL; zend_string *name, *tmp_name; SAVE_OPLINE(); @@ -50677,6 +51195,8 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_ASSIGN_OBJ_OP } } while (0); + ZEND_MAYBE_TRACK_OBJECT_MUTATION(zobj); + FREE_OP((opline+1)->op1_type, (opline+1)->op1.var); @@ -50693,12 +51213,16 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_ASSIGN_DIM_OP zval *var_ptr; zval *value, *container, *dim; HashTable *ht; + bool should_publish_tracked_array = false; SAVE_OPLINE(); container = EX_VAR(opline->op1.var); if (EXPECTED(Z_TYPE_P(container) == IS_ARRAY)) { assign_dim_op_array: + if (UNEXPECTED(EG(tracked_mutation_hooks_active))) { + should_publish_tracked_array = zend_maybe_track_hash_mutation(Z_ARRVAL_P(container), false); + } SEPARATE_ARRAY(container); ht = Z_ARRVAL_P(container); assign_dim_op_new_array: @@ -50738,6 +51262,9 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_ASSIGN_DIM_OP ZVAL_COPY(EX_VAR(opline->result.var), var_ptr); } FREE_OP((opline+1)->op1_type, (opline+1)->op1.var); + if (UNEXPECTED(should_publish_tracked_array)) { + zend_maybe_track_hash_mutation(Z_ARRVAL_P(container), true); + } } else { if (EXPECTED(Z_ISREF_P(container))) { container = Z_REFVAL_P(container); @@ -50754,6 +51281,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_ASSIGN_DIM_OP dim++; } zend_binary_assign_op_obj_dim(obj, dim OPLINE_CC EXECUTE_DATA_CC); + ZEND_MAYBE_TRACK_OBJECT_MUTATION(obj); } else if (EXPECTED(Z_TYPE_P(container) <= IS_FALSE)) { uint8_t old_type; @@ -50830,7 +51358,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_PRE_INC_OBJ_S void *_cache_slot[3] = {0}; void **cache_slot; zend_property_info *prop_info; - zend_object *zobj; + zend_object *zobj = NULL; zend_string *name, *tmp_name; SAVE_OPLINE(); @@ -50882,6 +51410,8 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_PRE_INC_OBJ_S } } while (0); + ZEND_MAYBE_TRACK_OBJECT_MUTATION(zobj); + @@ -50897,7 +51427,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_POST_INC_OBJ_ void *_cache_slot[3] = {0}; void **cache_slot; zend_property_info *prop_info; - zend_object *zobj; + zend_object *zobj = NULL; zend_string *name, *tmp_name; SAVE_OPLINE(); @@ -50947,6 +51477,8 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_POST_INC_OBJ_ } } while (0); + ZEND_MAYBE_TRACK_OBJECT_MUTATION(zobj); + @@ -51470,7 +52002,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_ASSIGN_OBJ_SP { USE_OPLINE zval *object, *value, tmp; - zend_object *zobj; + zend_object *zobj = NULL; zend_string *name, *tmp_name; zend_refcounted *garbage = NULL; @@ -51612,6 +52144,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_ASSIGN_OBJ_SP exit_assign_obj: + ZEND_MAYBE_TRACK_OBJECT_MUTATION_WITH_VALUE(zobj, value); if (garbage) { GC_DTOR_NO_REF(garbage); } @@ -51628,7 +52161,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_ASSIGN_OBJ_SP { USE_OPLINE zval *object, *value, tmp; - zend_object *zobj; + zend_object *zobj = NULL; zend_string *name, *tmp_name; zend_refcounted *garbage = NULL; @@ -51768,6 +52301,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_ASSIGN_OBJ_SP } zval_ptr_dtor_nogc(EX_VAR((opline+1)->op1.var)); exit_assign_obj: + ZEND_MAYBE_TRACK_OBJECT_MUTATION_WITH_VALUE(zobj, value); if (garbage) { GC_DTOR_NO_REF(garbage); } @@ -51784,7 +52318,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_ASSIGN_OBJ_SP { USE_OPLINE zval *object, *value, tmp; - zend_object *zobj; + zend_object *zobj = NULL; zend_string *name, *tmp_name; zend_refcounted *garbage = NULL; @@ -51926,6 +52460,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_ASSIGN_OBJ_SP exit_assign_obj: + ZEND_MAYBE_TRACK_OBJECT_MUTATION_WITH_VALUE(zobj, value); if (garbage) { GC_DTOR_NO_REF(garbage); } @@ -51946,12 +52481,16 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_ASSIGN_DIM_SP zval *variable_ptr; zval *dim; zend_refcounted *garbage = NULL; + bool should_publish_tracked_array = false; SAVE_OPLINE(); orig_object_ptr = object_ptr = EX_VAR(opline->op1.var); if (EXPECTED(Z_TYPE_P(object_ptr) == IS_ARRAY)) { try_assign_dim_array: + if (UNEXPECTED(EG(tracked_mutation_hooks_active))) { + should_publish_tracked_array = zend_maybe_track_hash_mutation(Z_ARRVAL_P(object_ptr), false); + } SEPARATE_ARRAY(object_ptr); if (IS_CV == IS_UNUSED) { value = RT_CONSTANT((opline+1), (opline+1)->op1); @@ -52009,6 +52548,9 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_ASSIGN_DIM_SP if (garbage) { GC_DTOR_NO_REF(garbage); } + if (UNEXPECTED(should_publish_tracked_array)) { + zend_maybe_track_hash_mutation(Z_ARRVAL_P(object_ptr), true); + } } else { if (EXPECTED(Z_ISREF_P(object_ptr))) { object_ptr = Z_REFVAL_P(object_ptr); @@ -52035,6 +52577,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_ASSIGN_DIM_SP } zend_assign_to_object_dim(obj, dim, value OPLINE_CC EXECUTE_DATA_CC); + ZEND_MAYBE_TRACK_OBJECT_MUTATION(obj); if (UNEXPECTED(GC_DELREF(obj) == 0)) { @@ -52105,12 +52648,16 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_ASSIGN_DIM_SP zval *variable_ptr; zval *dim; zend_refcounted *garbage = NULL; + bool should_publish_tracked_array = false; SAVE_OPLINE(); orig_object_ptr = object_ptr = EX_VAR(opline->op1.var); if (EXPECTED(Z_TYPE_P(object_ptr) == IS_ARRAY)) { try_assign_dim_array: + if (UNEXPECTED(EG(tracked_mutation_hooks_active))) { + should_publish_tracked_array = zend_maybe_track_hash_mutation(Z_ARRVAL_P(object_ptr), false); + } SEPARATE_ARRAY(object_ptr); if (IS_CV == IS_UNUSED) { value = _get_zval_ptr_tmp((opline+1)->op1.var EXECUTE_DATA_CC); @@ -52168,6 +52715,9 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_ASSIGN_DIM_SP if (garbage) { GC_DTOR_NO_REF(garbage); } + if (UNEXPECTED(should_publish_tracked_array)) { + zend_maybe_track_hash_mutation(Z_ARRVAL_P(object_ptr), true); + } } else { if (EXPECTED(Z_ISREF_P(object_ptr))) { object_ptr = Z_REFVAL_P(object_ptr); @@ -52194,6 +52744,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_ASSIGN_DIM_SP } zend_assign_to_object_dim(obj, dim, value OPLINE_CC EXECUTE_DATA_CC); + ZEND_MAYBE_TRACK_OBJECT_MUTATION(obj); zval_ptr_dtor_nogc(EX_VAR((opline+1)->op1.var)); if (UNEXPECTED(GC_DELREF(obj) == 0)) { @@ -52260,12 +52811,16 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_ASSIGN_DIM_SP zval *variable_ptr; zval *dim; zend_refcounted *garbage = NULL; + bool should_publish_tracked_array = false; SAVE_OPLINE(); orig_object_ptr = object_ptr = EX_VAR(opline->op1.var); if (EXPECTED(Z_TYPE_P(object_ptr) == IS_ARRAY)) { try_assign_dim_array: + if (UNEXPECTED(EG(tracked_mutation_hooks_active))) { + should_publish_tracked_array = zend_maybe_track_hash_mutation(Z_ARRVAL_P(object_ptr), false); + } SEPARATE_ARRAY(object_ptr); if (IS_CV == IS_UNUSED) { value = EX_VAR((opline+1)->op1.var); @@ -52323,6 +52878,9 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_ASSIGN_DIM_SP if (garbage) { GC_DTOR_NO_REF(garbage); } + if (UNEXPECTED(should_publish_tracked_array)) { + zend_maybe_track_hash_mutation(Z_ARRVAL_P(object_ptr), true); + } } else { if (EXPECTED(Z_ISREF_P(object_ptr))) { object_ptr = Z_REFVAL_P(object_ptr); @@ -52349,6 +52907,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_ASSIGN_DIM_SP } zend_assign_to_object_dim(obj, dim, value OPLINE_CC EXECUTE_DATA_CC); + ZEND_MAYBE_TRACK_OBJECT_MUTATION(obj); if (UNEXPECTED(GC_DELREF(obj) == 0)) { @@ -52515,6 +53074,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_ASSIGN_OBJ_RE { USE_OPLINE zval *property, *container, *value_ptr; + zend_object *zobj = NULL; SAVE_OPLINE(); @@ -52522,6 +53082,11 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_ASSIGN_OBJ_RE property = _get_zval_ptr_cv_BP_VAR_R(opline->op2.var EXECUTE_DATA_CC); value_ptr = _get_zval_ptr_ptr_var((opline+1)->op1.var EXECUTE_DATA_CC); + if (EXPECTED(Z_TYPE_P(container) == IS_OBJECT)) { + zobj = Z_OBJ_P(container); + } else if (Z_ISREF_P(container) && EXPECTED(Z_TYPE_P(Z_REFVAL_P(container)) == IS_OBJECT)) { + zobj = Z_OBJ_P(Z_REFVAL_P(container)); + } if (1) { if (IS_CV == IS_UNUSED) { @@ -52541,6 +53106,8 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_ASSIGN_OBJ_RE zend_assign_to_property_reference(container, IS_CV, property, IS_CV, value_ptr OPLINE_CC EXECUTE_DATA_CC); } + ZEND_MAYBE_TRACK_OBJECT_MUTATION(zobj); + @@ -52553,6 +53120,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_ASSIGN_OBJ_RE { USE_OPLINE zval *property, *container, *value_ptr; + zend_object *zobj = NULL; SAVE_OPLINE(); @@ -52560,6 +53128,11 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_ASSIGN_OBJ_RE property = _get_zval_ptr_cv_BP_VAR_R(opline->op2.var EXECUTE_DATA_CC); value_ptr = _get_zval_ptr_cv_BP_VAR_W((opline+1)->op1.var EXECUTE_DATA_CC); + if (EXPECTED(Z_TYPE_P(container) == IS_OBJECT)) { + zobj = Z_OBJ_P(container); + } else if (Z_ISREF_P(container) && EXPECTED(Z_TYPE_P(Z_REFVAL_P(container)) == IS_OBJECT)) { + zobj = Z_OBJ_P(Z_REFVAL_P(container)); + } if (1) { if (IS_CV == IS_UNUSED) { @@ -52579,6 +53152,8 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_ASSIGN_OBJ_RE zend_assign_to_property_reference(container, IS_CV, property, IS_CV, value_ptr OPLINE_CC EXECUTE_DATA_CC); } + ZEND_MAYBE_TRACK_OBJECT_MUTATION(zobj); + @@ -53019,6 +53594,8 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_UNSET_DIM_SPE zval *offset; zend_ulong hval; zend_string *key; + bool should_flush_array = false; + bool should_publish_tracked_array = false; SAVE_OPLINE(); container = EX_VAR(opline->op1.var); @@ -53029,6 +53606,9 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_UNSET_DIM_SPE HashTable *ht; unset_dim_array: + if (UNEXPECTED(EG(tracked_mutation_hooks_active))) { + should_publish_tracked_array = zend_maybe_track_hash_mutation(Z_ARRVAL_P(container), false); + } SEPARATE_ARRAY(container); ht = Z_ARRVAL_P(container); offset_again: @@ -53042,10 +53622,12 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_UNSET_DIM_SPE str_index_dim: ZEND_ASSERT(ht != &EG(symbol_table)); zend_hash_del(ht, key); + should_flush_array = true; } else if (EXPECTED(Z_TYPE_P(offset) == IS_LONG)) { hval = Z_LVAL_P(offset); num_index_dim: zend_hash_index_del(ht, hval); + should_flush_array = true; } else if ((IS_CV & (IS_VAR|IS_CV)) && EXPECTED(Z_TYPE_P(offset) == IS_REFERENCE)) { offset = Z_REFVAL_P(offset); goto offset_again; @@ -53082,6 +53664,9 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_UNSET_DIM_SPE } else { zend_illegal_array_offset_unset(offset); } + if (UNEXPECTED(should_flush_array && should_publish_tracked_array)) { + zend_maybe_track_hash_mutation(ht, true); + } break; } else if (Z_ISREF_P(container)) { container = Z_REFVAL_P(container); @@ -53100,6 +53685,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_UNSET_DIM_SPE offset++; } Z_OBJ_HT_P(container)->unset_dimension(Z_OBJ_P(container), offset); + ZEND_MAYBE_TRACK_OBJECT_MUTATION(Z_OBJ_P(container)); } else if (UNEXPECTED(Z_TYPE_P(container) == IS_STRING)) { zend_throw_error(NULL, "Cannot unset string offsets"); } else if (UNEXPECTED(Z_TYPE_P(container) > IS_FALSE)) { @@ -53121,6 +53707,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_UNSET_OBJ_SPE zval *container; zval *offset; zend_string *name, *tmp_name; + zend_object *zobj = NULL; SAVE_OPLINE(); container = EX_VAR(opline->op1.var); @@ -53141,6 +53728,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_UNSET_OBJ_SPE break; } } + zobj = Z_OBJ_P(container); if (IS_CV == IS_CONST) { name = Z_STR_P(offset); } else { @@ -53155,6 +53743,8 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_UNSET_OBJ_SPE } } while (0); + ZEND_MAYBE_TRACK_OBJECT_MUTATION(zobj); + @@ -53630,6 +54220,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_ASSIGN_STATIC_PROP } FREE_OP((opline+1)->op1_type, (opline+1)->op1.var); + zend_class_static_update(prop_info->ce); /* assign_static_prop has two opcodes! */ ZEND_VM_NEXT_OPCODE_EX(1, 2); } @@ -53789,6 +54380,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_ASSIGN_STATIC_PROP GC_DTOR_NO_REF(garbage); } + zend_class_static_update(prop_info->ce); /* assign_static_prop has two opcodes! */ ZEND_VM_NEXT_OPCODE_EX(1, 2); } @@ -53826,6 +54418,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_ASSIGN_STATIC_PROP GC_DTOR_NO_REF(garbage); } + zend_class_static_update(prop_info->ce); /* assign_static_prop has two opcodes! */ ZEND_VM_NEXT_OPCODE_EX(1, 2); } @@ -53865,6 +54458,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_ASSIGN_STATIC_PROP GC_DTOR_NO_REF(garbage); } + zend_class_static_update(prop_info->ce); /* assign_static_prop has two opcodes! */ ZEND_VM_NEXT_OPCODE_EX(1, 2); } @@ -53914,6 +54508,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_ASSIGN_STATIC_PROP } FREE_OP((opline+1)->op1_type, (opline+1)->op1.var); + zend_class_static_update(prop_info->ce); ZEND_VM_NEXT_OPCODE_EX(1, 2); } @@ -76358,7 +76953,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_ASSIGN_OBJ_OP_SPEC void *_cache_slot[3] = {0}; void **cache_slot; zend_property_info *prop_info; - zend_object *zobj; + zend_object *zobj = NULL; zend_string *name, *tmp_name; SAVE_OPLINE(); @@ -76433,6 +77028,8 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_ASSIGN_OBJ_OP_SPEC } } while (0); + ZEND_MAYBE_TRACK_OBJECT_MUTATION(zobj); + FREE_OP((opline+1)->op1_type, (opline+1)->op1.var); @@ -76448,12 +77045,16 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_ASSIGN_DIM_OP_SPEC zval *var_ptr; zval *value, *container, *dim; HashTable *ht; + bool should_publish_tracked_array = false; SAVE_OPLINE(); container = _get_zval_ptr_ptr_var(opline->op1.var EXECUTE_DATA_CC); if (EXPECTED(Z_TYPE_P(container) == IS_ARRAY)) { assign_dim_op_array: + if (UNEXPECTED(EG(tracked_mutation_hooks_active))) { + should_publish_tracked_array = zend_maybe_track_hash_mutation(Z_ARRVAL_P(container), false); + } SEPARATE_ARRAY(container); ht = Z_ARRVAL_P(container); assign_dim_op_new_array: @@ -76493,6 +77094,9 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_ASSIGN_DIM_OP_SPEC ZVAL_COPY(EX_VAR(opline->result.var), var_ptr); } FREE_OP((opline+1)->op1_type, (opline+1)->op1.var); + if (UNEXPECTED(should_publish_tracked_array)) { + zend_maybe_track_hash_mutation(Z_ARRVAL_P(container), true); + } } else { if (EXPECTED(Z_ISREF_P(container))) { container = Z_REFVAL_P(container); @@ -76509,6 +77113,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_ASSIGN_DIM_OP_SPEC dim++; } zend_binary_assign_op_obj_dim(obj, dim OPLINE_CC EXECUTE_DATA_CC); + ZEND_MAYBE_TRACK_OBJECT_MUTATION(obj); } else if (EXPECTED(Z_TYPE_P(container) <= IS_FALSE)) { uint8_t old_type; @@ -76583,7 +77188,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_PRE_INC_OBJ_SPEC_V void *_cache_slot[3] = {0}; void **cache_slot; zend_property_info *prop_info; - zend_object *zobj; + zend_object *zobj = NULL; zend_string *name, *tmp_name; SAVE_OPLINE(); @@ -76635,6 +77240,8 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_PRE_INC_OBJ_SPEC_V } } while (0); + ZEND_MAYBE_TRACK_OBJECT_MUTATION(zobj); + zval_ptr_dtor_nogc(EX_VAR(opline->op1.var)); ZEND_VM_NEXT_OPCODE_CHECK_EXCEPTION(); @@ -76649,7 +77256,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_POST_INC_OBJ_SPEC_ void *_cache_slot[3] = {0}; void **cache_slot; zend_property_info *prop_info; - zend_object *zobj; + zend_object *zobj = NULL; zend_string *name, *tmp_name; SAVE_OPLINE(); @@ -76699,6 +77306,8 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_POST_INC_OBJ_SPEC_ } } while (0); + ZEND_MAYBE_TRACK_OBJECT_MUTATION(zobj); + zval_ptr_dtor_nogc(EX_VAR(opline->op1.var)); ZEND_VM_NEXT_OPCODE_CHECK_EXCEPTION(); @@ -76885,7 +77494,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_ASSIGN_OBJ_SPEC_VA { USE_OPLINE zval *object, *value, tmp; - zend_object *zobj; + zend_object *zobj = NULL; zend_string *name, *tmp_name; zend_refcounted *garbage = NULL; @@ -77027,6 +77636,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_ASSIGN_OBJ_SPEC_VA exit_assign_obj: + ZEND_MAYBE_TRACK_OBJECT_MUTATION_WITH_VALUE(zobj, value); if (garbage) { GC_DTOR_NO_REF(garbage); } @@ -77042,7 +77652,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_ASSIGN_OBJ_SPEC_VA { USE_OPLINE zval *object, *value, tmp; - zend_object *zobj; + zend_object *zobj = NULL; zend_string *name, *tmp_name; zend_refcounted *garbage = NULL; @@ -77182,6 +77792,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_ASSIGN_OBJ_SPEC_VA } zval_ptr_dtor_nogc(EX_VAR((opline+1)->op1.var)); exit_assign_obj: + ZEND_MAYBE_TRACK_OBJECT_MUTATION_WITH_VALUE(zobj, value); if (garbage) { GC_DTOR_NO_REF(garbage); } @@ -77197,7 +77808,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_ASSIGN_OBJ_SPEC_VA { USE_OPLINE zval *object, *value, tmp; - zend_object *zobj; + zend_object *zobj = NULL; zend_string *name, *tmp_name; zend_refcounted *garbage = NULL; @@ -77339,6 +77950,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_ASSIGN_OBJ_SPEC_VA exit_assign_obj: + ZEND_MAYBE_TRACK_OBJECT_MUTATION_WITH_VALUE(zobj, value); if (garbage) { GC_DTOR_NO_REF(garbage); } @@ -77358,12 +77970,16 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_ASSIGN_DIM_SPEC_VA zval *variable_ptr; zval *dim; zend_refcounted *garbage = NULL; + bool should_publish_tracked_array = false; SAVE_OPLINE(); orig_object_ptr = object_ptr = _get_zval_ptr_ptr_var(opline->op1.var EXECUTE_DATA_CC); if (EXPECTED(Z_TYPE_P(object_ptr) == IS_ARRAY)) { try_assign_dim_array: + if (UNEXPECTED(EG(tracked_mutation_hooks_active))) { + should_publish_tracked_array = zend_maybe_track_hash_mutation(Z_ARRVAL_P(object_ptr), false); + } SEPARATE_ARRAY(object_ptr); if (IS_CONST == IS_UNUSED) { value = RT_CONSTANT((opline+1), (opline+1)->op1); @@ -77421,6 +78037,9 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_ASSIGN_DIM_SPEC_VA if (garbage) { GC_DTOR_NO_REF(garbage); } + if (UNEXPECTED(should_publish_tracked_array)) { + zend_maybe_track_hash_mutation(Z_ARRVAL_P(object_ptr), true); + } } else { if (EXPECTED(Z_ISREF_P(object_ptr))) { object_ptr = Z_REFVAL_P(object_ptr); @@ -77447,6 +78066,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_ASSIGN_DIM_SPEC_VA } zend_assign_to_object_dim(obj, dim, value OPLINE_CC EXECUTE_DATA_CC); + ZEND_MAYBE_TRACK_OBJECT_MUTATION(obj); if (UNEXPECTED(GC_DELREF(obj) == 0)) { @@ -77516,12 +78136,16 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_ASSIGN_DIM_SPEC_VA zval *variable_ptr; zval *dim; zend_refcounted *garbage = NULL; + bool should_publish_tracked_array = false; SAVE_OPLINE(); orig_object_ptr = object_ptr = _get_zval_ptr_ptr_var(opline->op1.var EXECUTE_DATA_CC); if (EXPECTED(Z_TYPE_P(object_ptr) == IS_ARRAY)) { try_assign_dim_array: + if (UNEXPECTED(EG(tracked_mutation_hooks_active))) { + should_publish_tracked_array = zend_maybe_track_hash_mutation(Z_ARRVAL_P(object_ptr), false); + } SEPARATE_ARRAY(object_ptr); if (IS_CONST == IS_UNUSED) { value = _get_zval_ptr_tmp((opline+1)->op1.var EXECUTE_DATA_CC); @@ -77579,6 +78203,9 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_ASSIGN_DIM_SPEC_VA if (garbage) { GC_DTOR_NO_REF(garbage); } + if (UNEXPECTED(should_publish_tracked_array)) { + zend_maybe_track_hash_mutation(Z_ARRVAL_P(object_ptr), true); + } } else { if (EXPECTED(Z_ISREF_P(object_ptr))) { object_ptr = Z_REFVAL_P(object_ptr); @@ -77605,6 +78232,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_ASSIGN_DIM_SPEC_VA } zend_assign_to_object_dim(obj, dim, value OPLINE_CC EXECUTE_DATA_CC); + ZEND_MAYBE_TRACK_OBJECT_MUTATION(obj); zval_ptr_dtor_nogc(EX_VAR((opline+1)->op1.var)); if (UNEXPECTED(GC_DELREF(obj) == 0)) { @@ -77670,12 +78298,16 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_ASSIGN_DIM_SPEC_VA zval *variable_ptr; zval *dim; zend_refcounted *garbage = NULL; + bool should_publish_tracked_array = false; SAVE_OPLINE(); orig_object_ptr = object_ptr = _get_zval_ptr_ptr_var(opline->op1.var EXECUTE_DATA_CC); if (EXPECTED(Z_TYPE_P(object_ptr) == IS_ARRAY)) { try_assign_dim_array: + if (UNEXPECTED(EG(tracked_mutation_hooks_active))) { + should_publish_tracked_array = zend_maybe_track_hash_mutation(Z_ARRVAL_P(object_ptr), false); + } SEPARATE_ARRAY(object_ptr); if (IS_CONST == IS_UNUSED) { value = EX_VAR((opline+1)->op1.var); @@ -77733,6 +78365,9 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_ASSIGN_DIM_SPEC_VA if (garbage) { GC_DTOR_NO_REF(garbage); } + if (UNEXPECTED(should_publish_tracked_array)) { + zend_maybe_track_hash_mutation(Z_ARRVAL_P(object_ptr), true); + } } else { if (EXPECTED(Z_ISREF_P(object_ptr))) { object_ptr = Z_REFVAL_P(object_ptr); @@ -77759,6 +78394,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_ASSIGN_DIM_SPEC_VA } zend_assign_to_object_dim(obj, dim, value OPLINE_CC EXECUTE_DATA_CC); + ZEND_MAYBE_TRACK_OBJECT_MUTATION(obj); if (UNEXPECTED(GC_DELREF(obj) == 0)) { @@ -77882,6 +78518,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_ASSIGN_OBJ_REF_SPE { USE_OPLINE zval *property, *container, *value_ptr; + zend_object *zobj = NULL; SAVE_OPLINE(); @@ -77889,6 +78526,11 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_ASSIGN_OBJ_REF_SPE property = RT_CONSTANT(opline, opline->op2); value_ptr = _get_zval_ptr_ptr_var((opline+1)->op1.var EXECUTE_DATA_CC); + if (EXPECTED(Z_TYPE_P(container) == IS_OBJECT)) { + zobj = Z_OBJ_P(container); + } else if (Z_ISREF_P(container) && EXPECTED(Z_TYPE_P(Z_REFVAL_P(container)) == IS_OBJECT)) { + zobj = Z_OBJ_P(Z_REFVAL_P(container)); + } if (1) { if (IS_VAR == IS_UNUSED) { @@ -77908,6 +78550,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_ASSIGN_OBJ_REF_SPE zend_assign_to_property_reference(container, IS_VAR, property, IS_CONST, value_ptr OPLINE_CC EXECUTE_DATA_CC); } + ZEND_MAYBE_TRACK_OBJECT_MUTATION(zobj); zval_ptr_dtor_nogc(EX_VAR(opline->op1.var)); @@ -77920,6 +78563,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_ASSIGN_OBJ_REF_SPE { USE_OPLINE zval *property, *container, *value_ptr; + zend_object *zobj = NULL; SAVE_OPLINE(); @@ -77927,6 +78571,11 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_ASSIGN_OBJ_REF_SPE property = RT_CONSTANT(opline, opline->op2); value_ptr = _get_zval_ptr_cv_BP_VAR_W((opline+1)->op1.var EXECUTE_DATA_CC); + if (EXPECTED(Z_TYPE_P(container) == IS_OBJECT)) { + zobj = Z_OBJ_P(container); + } else if (Z_ISREF_P(container) && EXPECTED(Z_TYPE_P(Z_REFVAL_P(container)) == IS_OBJECT)) { + zobj = Z_OBJ_P(Z_REFVAL_P(container)); + } if (1) { if (IS_VAR == IS_UNUSED) { @@ -77946,6 +78595,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_ASSIGN_OBJ_REF_SPE zend_assign_to_property_reference(container, IS_VAR, property, IS_CONST, value_ptr OPLINE_CC EXECUTE_DATA_CC); } + ZEND_MAYBE_TRACK_OBJECT_MUTATION(zobj); zval_ptr_dtor_nogc(EX_VAR(opline->op1.var)); @@ -78669,6 +79319,8 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_UNSET_DIM_SPEC_VAR zval *offset; zend_ulong hval; zend_string *key; + bool should_flush_array = false; + bool should_publish_tracked_array = false; SAVE_OPLINE(); container = _get_zval_ptr_ptr_var(opline->op1.var EXECUTE_DATA_CC); @@ -78679,6 +79331,9 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_UNSET_DIM_SPEC_VAR HashTable *ht; unset_dim_array: + if (UNEXPECTED(EG(tracked_mutation_hooks_active))) { + should_publish_tracked_array = zend_maybe_track_hash_mutation(Z_ARRVAL_P(container), false); + } SEPARATE_ARRAY(container); ht = Z_ARRVAL_P(container); offset_again: @@ -78692,10 +79347,12 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_UNSET_DIM_SPEC_VAR str_index_dim: ZEND_ASSERT(ht != &EG(symbol_table)); zend_hash_del(ht, key); + should_flush_array = true; } else if (EXPECTED(Z_TYPE_P(offset) == IS_LONG)) { hval = Z_LVAL_P(offset); num_index_dim: zend_hash_index_del(ht, hval); + should_flush_array = true; } else if ((IS_CONST & (IS_VAR|IS_CV)) && EXPECTED(Z_TYPE_P(offset) == IS_REFERENCE)) { offset = Z_REFVAL_P(offset); goto offset_again; @@ -78732,6 +79389,9 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_UNSET_DIM_SPEC_VAR } else { zend_illegal_array_offset_unset(offset); } + if (UNEXPECTED(should_flush_array && should_publish_tracked_array)) { + zend_maybe_track_hash_mutation(ht, true); + } break; } else if (Z_ISREF_P(container)) { container = Z_REFVAL_P(container); @@ -78750,6 +79410,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_UNSET_DIM_SPEC_VAR offset++; } Z_OBJ_HT_P(container)->unset_dimension(Z_OBJ_P(container), offset); + ZEND_MAYBE_TRACK_OBJECT_MUTATION(Z_OBJ_P(container)); } else if (UNEXPECTED(Z_TYPE_P(container) == IS_STRING)) { zend_throw_error(NULL, "Cannot unset string offsets"); } else if (UNEXPECTED(Z_TYPE_P(container) > IS_FALSE)) { @@ -78770,6 +79431,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_UNSET_OBJ_SPEC_VAR zval *container; zval *offset; zend_string *name, *tmp_name; + zend_object *zobj = NULL; SAVE_OPLINE(); container = _get_zval_ptr_ptr_var(opline->op1.var EXECUTE_DATA_CC); @@ -78790,6 +79452,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_UNSET_OBJ_SPEC_VAR break; } } + zobj = Z_OBJ_P(container); if (IS_CONST == IS_CONST) { name = Z_STR_P(offset); } else { @@ -78804,6 +79467,8 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_UNSET_OBJ_SPEC_VAR } } while (0); + ZEND_MAYBE_TRACK_OBJECT_MUTATION(zobj); + zval_ptr_dtor_nogc(EX_VAR(opline->op1.var)); ZEND_VM_NEXT_OPCODE_CHECK_EXCEPTION(); @@ -79073,7 +79738,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_ASSIGN_OBJ_OP_SPEC void *_cache_slot[3] = {0}; void **cache_slot; zend_property_info *prop_info; - zend_object *zobj; + zend_object *zobj = NULL; zend_string *name, *tmp_name; SAVE_OPLINE(); @@ -79148,6 +79813,8 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_ASSIGN_OBJ_OP_SPEC } } while (0); + ZEND_MAYBE_TRACK_OBJECT_MUTATION(zobj); + FREE_OP((opline+1)->op1_type, (opline+1)->op1.var); zval_ptr_dtor_nogc(EX_VAR(opline->op2.var)); zval_ptr_dtor_nogc(EX_VAR(opline->op1.var)); @@ -79162,12 +79829,16 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_ASSIGN_DIM_OP_SPEC zval *var_ptr; zval *value, *container, *dim; HashTable *ht; + bool should_publish_tracked_array = false; SAVE_OPLINE(); container = _get_zval_ptr_ptr_var(opline->op1.var EXECUTE_DATA_CC); if (EXPECTED(Z_TYPE_P(container) == IS_ARRAY)) { assign_dim_op_array: + if (UNEXPECTED(EG(tracked_mutation_hooks_active))) { + should_publish_tracked_array = zend_maybe_track_hash_mutation(Z_ARRVAL_P(container), false); + } SEPARATE_ARRAY(container); ht = Z_ARRVAL_P(container); assign_dim_op_new_array: @@ -79207,6 +79878,9 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_ASSIGN_DIM_OP_SPEC ZVAL_COPY(EX_VAR(opline->result.var), var_ptr); } FREE_OP((opline+1)->op1_type, (opline+1)->op1.var); + if (UNEXPECTED(should_publish_tracked_array)) { + zend_maybe_track_hash_mutation(Z_ARRVAL_P(container), true); + } } else { if (EXPECTED(Z_ISREF_P(container))) { container = Z_REFVAL_P(container); @@ -79223,6 +79897,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_ASSIGN_DIM_OP_SPEC dim++; } zend_binary_assign_op_obj_dim(obj, dim OPLINE_CC EXECUTE_DATA_CC); + ZEND_MAYBE_TRACK_OBJECT_MUTATION(obj); } else if (EXPECTED(Z_TYPE_P(container) <= IS_FALSE)) { uint8_t old_type; @@ -79297,7 +79972,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_PRE_INC_OBJ_SPEC_V void *_cache_slot[3] = {0}; void **cache_slot; zend_property_info *prop_info; - zend_object *zobj; + zend_object *zobj = NULL; zend_string *name, *tmp_name; SAVE_OPLINE(); @@ -79349,6 +80024,8 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_PRE_INC_OBJ_SPEC_V } } while (0); + ZEND_MAYBE_TRACK_OBJECT_MUTATION(zobj); + zval_ptr_dtor_nogc(EX_VAR(opline->op2.var)); zval_ptr_dtor_nogc(EX_VAR(opline->op1.var)); ZEND_VM_NEXT_OPCODE_CHECK_EXCEPTION(); @@ -79363,7 +80040,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_POST_INC_OBJ_SPEC_ void *_cache_slot[3] = {0}; void **cache_slot; zend_property_info *prop_info; - zend_object *zobj; + zend_object *zobj = NULL; zend_string *name, *tmp_name; SAVE_OPLINE(); @@ -79413,6 +80090,8 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_POST_INC_OBJ_SPEC_ } } while (0); + ZEND_MAYBE_TRACK_OBJECT_MUTATION(zobj); + zval_ptr_dtor_nogc(EX_VAR(opline->op2.var)); zval_ptr_dtor_nogc(EX_VAR(opline->op1.var)); ZEND_VM_NEXT_OPCODE_CHECK_EXCEPTION(); @@ -79593,7 +80272,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_ASSIGN_OBJ_SPEC_VA { USE_OPLINE zval *object, *value, tmp; - zend_object *zobj; + zend_object *zobj = NULL; zend_string *name, *tmp_name; zend_refcounted *garbage = NULL; @@ -79735,6 +80414,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_ASSIGN_OBJ_SPEC_VA exit_assign_obj: + ZEND_MAYBE_TRACK_OBJECT_MUTATION_WITH_VALUE(zobj, value); if (garbage) { GC_DTOR_NO_REF(garbage); } @@ -79749,7 +80429,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_ASSIGN_OBJ_SPEC_VA { USE_OPLINE zval *object, *value, tmp; - zend_object *zobj; + zend_object *zobj = NULL; zend_string *name, *tmp_name; zend_refcounted *garbage = NULL; @@ -79889,6 +80569,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_ASSIGN_OBJ_SPEC_VA } zval_ptr_dtor_nogc(EX_VAR((opline+1)->op1.var)); exit_assign_obj: + ZEND_MAYBE_TRACK_OBJECT_MUTATION_WITH_VALUE(zobj, value); if (garbage) { GC_DTOR_NO_REF(garbage); } @@ -79903,7 +80584,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_ASSIGN_OBJ_SPEC_VA { USE_OPLINE zval *object, *value, tmp; - zend_object *zobj; + zend_object *zobj = NULL; zend_string *name, *tmp_name; zend_refcounted *garbage = NULL; @@ -80045,6 +80726,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_ASSIGN_OBJ_SPEC_VA exit_assign_obj: + ZEND_MAYBE_TRACK_OBJECT_MUTATION_WITH_VALUE(zobj, value); if (garbage) { GC_DTOR_NO_REF(garbage); } @@ -80063,12 +80745,16 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_ASSIGN_DIM_SPEC_VA zval *variable_ptr; zval *dim; zend_refcounted *garbage = NULL; + bool should_publish_tracked_array = false; SAVE_OPLINE(); orig_object_ptr = object_ptr = _get_zval_ptr_ptr_var(opline->op1.var EXECUTE_DATA_CC); if (EXPECTED(Z_TYPE_P(object_ptr) == IS_ARRAY)) { try_assign_dim_array: + if (UNEXPECTED(EG(tracked_mutation_hooks_active))) { + should_publish_tracked_array = zend_maybe_track_hash_mutation(Z_ARRVAL_P(object_ptr), false); + } SEPARATE_ARRAY(object_ptr); if (IS_TMP_VAR == IS_UNUSED) { value = RT_CONSTANT((opline+1), (opline+1)->op1); @@ -80126,6 +80812,9 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_ASSIGN_DIM_SPEC_VA if (garbage) { GC_DTOR_NO_REF(garbage); } + if (UNEXPECTED(should_publish_tracked_array)) { + zend_maybe_track_hash_mutation(Z_ARRVAL_P(object_ptr), true); + } } else { if (EXPECTED(Z_ISREF_P(object_ptr))) { object_ptr = Z_REFVAL_P(object_ptr); @@ -80152,6 +80841,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_ASSIGN_DIM_SPEC_VA } zend_assign_to_object_dim(obj, dim, value OPLINE_CC EXECUTE_DATA_CC); + ZEND_MAYBE_TRACK_OBJECT_MUTATION(obj); if (UNEXPECTED(GC_DELREF(obj) == 0)) { @@ -80220,12 +80910,16 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_ASSIGN_DIM_SPEC_VA zval *variable_ptr; zval *dim; zend_refcounted *garbage = NULL; + bool should_publish_tracked_array = false; SAVE_OPLINE(); orig_object_ptr = object_ptr = _get_zval_ptr_ptr_var(opline->op1.var EXECUTE_DATA_CC); if (EXPECTED(Z_TYPE_P(object_ptr) == IS_ARRAY)) { try_assign_dim_array: + if (UNEXPECTED(EG(tracked_mutation_hooks_active))) { + should_publish_tracked_array = zend_maybe_track_hash_mutation(Z_ARRVAL_P(object_ptr), false); + } SEPARATE_ARRAY(object_ptr); if (IS_TMP_VAR == IS_UNUSED) { value = _get_zval_ptr_tmp((opline+1)->op1.var EXECUTE_DATA_CC); @@ -80283,6 +80977,9 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_ASSIGN_DIM_SPEC_VA if (garbage) { GC_DTOR_NO_REF(garbage); } + if (UNEXPECTED(should_publish_tracked_array)) { + zend_maybe_track_hash_mutation(Z_ARRVAL_P(object_ptr), true); + } } else { if (EXPECTED(Z_ISREF_P(object_ptr))) { object_ptr = Z_REFVAL_P(object_ptr); @@ -80309,6 +81006,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_ASSIGN_DIM_SPEC_VA } zend_assign_to_object_dim(obj, dim, value OPLINE_CC EXECUTE_DATA_CC); + ZEND_MAYBE_TRACK_OBJECT_MUTATION(obj); zval_ptr_dtor_nogc(EX_VAR((opline+1)->op1.var)); if (UNEXPECTED(GC_DELREF(obj) == 0)) { @@ -80373,12 +81071,16 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_ASSIGN_DIM_SPEC_VA zval *variable_ptr; zval *dim; zend_refcounted *garbage = NULL; + bool should_publish_tracked_array = false; SAVE_OPLINE(); orig_object_ptr = object_ptr = _get_zval_ptr_ptr_var(opline->op1.var EXECUTE_DATA_CC); if (EXPECTED(Z_TYPE_P(object_ptr) == IS_ARRAY)) { try_assign_dim_array: + if (UNEXPECTED(EG(tracked_mutation_hooks_active))) { + should_publish_tracked_array = zend_maybe_track_hash_mutation(Z_ARRVAL_P(object_ptr), false); + } SEPARATE_ARRAY(object_ptr); if (IS_TMP_VAR == IS_UNUSED) { value = EX_VAR((opline+1)->op1.var); @@ -80436,6 +81138,9 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_ASSIGN_DIM_SPEC_VA if (garbage) { GC_DTOR_NO_REF(garbage); } + if (UNEXPECTED(should_publish_tracked_array)) { + zend_maybe_track_hash_mutation(Z_ARRVAL_P(object_ptr), true); + } } else { if (EXPECTED(Z_ISREF_P(object_ptr))) { object_ptr = Z_REFVAL_P(object_ptr); @@ -80462,6 +81167,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_ASSIGN_DIM_SPEC_VA } zend_assign_to_object_dim(obj, dim, value OPLINE_CC EXECUTE_DATA_CC); + ZEND_MAYBE_TRACK_OBJECT_MUTATION(obj); if (UNEXPECTED(GC_DELREF(obj) == 0)) { @@ -80584,6 +81290,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_ASSIGN_OBJ_REF_SPE { USE_OPLINE zval *property, *container, *value_ptr; + zend_object *zobj = NULL; SAVE_OPLINE(); @@ -80591,6 +81298,11 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_ASSIGN_OBJ_REF_SPE property = _get_zval_ptr_tmp(opline->op2.var EXECUTE_DATA_CC); value_ptr = _get_zval_ptr_ptr_var((opline+1)->op1.var EXECUTE_DATA_CC); + if (EXPECTED(Z_TYPE_P(container) == IS_OBJECT)) { + zobj = Z_OBJ_P(container); + } else if (Z_ISREF_P(container) && EXPECTED(Z_TYPE_P(Z_REFVAL_P(container)) == IS_OBJECT)) { + zobj = Z_OBJ_P(Z_REFVAL_P(container)); + } if (1) { if (IS_VAR == IS_UNUSED) { @@ -80610,6 +81322,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_ASSIGN_OBJ_REF_SPE zend_assign_to_property_reference(container, IS_VAR, property, IS_TMP_VAR, value_ptr OPLINE_CC EXECUTE_DATA_CC); } + ZEND_MAYBE_TRACK_OBJECT_MUTATION(zobj); zval_ptr_dtor_nogc(EX_VAR(opline->op1.var)); zval_ptr_dtor_nogc(EX_VAR(opline->op2.var)); zval_ptr_dtor_nogc(EX_VAR((opline+1)->op1.var)); @@ -80621,6 +81334,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_ASSIGN_OBJ_REF_SPE { USE_OPLINE zval *property, *container, *value_ptr; + zend_object *zobj = NULL; SAVE_OPLINE(); @@ -80628,6 +81342,11 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_ASSIGN_OBJ_REF_SPE property = _get_zval_ptr_tmp(opline->op2.var EXECUTE_DATA_CC); value_ptr = _get_zval_ptr_cv_BP_VAR_W((opline+1)->op1.var EXECUTE_DATA_CC); + if (EXPECTED(Z_TYPE_P(container) == IS_OBJECT)) { + zobj = Z_OBJ_P(container); + } else if (Z_ISREF_P(container) && EXPECTED(Z_TYPE_P(Z_REFVAL_P(container)) == IS_OBJECT)) { + zobj = Z_OBJ_P(Z_REFVAL_P(container)); + } if (1) { if (IS_VAR == IS_UNUSED) { @@ -80647,6 +81366,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_ASSIGN_OBJ_REF_SPE zend_assign_to_property_reference(container, IS_VAR, property, IS_TMP_VAR, value_ptr OPLINE_CC EXECUTE_DATA_CC); } + ZEND_MAYBE_TRACK_OBJECT_MUTATION(zobj); zval_ptr_dtor_nogc(EX_VAR(opline->op1.var)); zval_ptr_dtor_nogc(EX_VAR(opline->op2.var)); @@ -80927,6 +81647,8 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_UNSET_DIM_SPEC_VAR zval *offset; zend_ulong hval; zend_string *key; + bool should_flush_array = false; + bool should_publish_tracked_array = false; SAVE_OPLINE(); container = _get_zval_ptr_ptr_var(opline->op1.var EXECUTE_DATA_CC); @@ -80937,6 +81659,9 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_UNSET_DIM_SPEC_VAR HashTable *ht; unset_dim_array: + if (UNEXPECTED(EG(tracked_mutation_hooks_active))) { + should_publish_tracked_array = zend_maybe_track_hash_mutation(Z_ARRVAL_P(container), false); + } SEPARATE_ARRAY(container); ht = Z_ARRVAL_P(container); offset_again: @@ -80950,10 +81675,12 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_UNSET_DIM_SPEC_VAR str_index_dim: ZEND_ASSERT(ht != &EG(symbol_table)); zend_hash_del(ht, key); + should_flush_array = true; } else if (EXPECTED(Z_TYPE_P(offset) == IS_LONG)) { hval = Z_LVAL_P(offset); num_index_dim: zend_hash_index_del(ht, hval); + should_flush_array = true; } else if ((IS_TMP_VAR & (IS_VAR|IS_CV)) && EXPECTED(Z_TYPE_P(offset) == IS_REFERENCE)) { offset = Z_REFVAL_P(offset); goto offset_again; @@ -80990,6 +81717,9 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_UNSET_DIM_SPEC_VAR } else { zend_illegal_array_offset_unset(offset); } + if (UNEXPECTED(should_flush_array && should_publish_tracked_array)) { + zend_maybe_track_hash_mutation(ht, true); + } break; } else if (Z_ISREF_P(container)) { container = Z_REFVAL_P(container); @@ -81008,6 +81738,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_UNSET_DIM_SPEC_VAR offset++; } Z_OBJ_HT_P(container)->unset_dimension(Z_OBJ_P(container), offset); + ZEND_MAYBE_TRACK_OBJECT_MUTATION(Z_OBJ_P(container)); } else if (UNEXPECTED(Z_TYPE_P(container) == IS_STRING)) { zend_throw_error(NULL, "Cannot unset string offsets"); } else if (UNEXPECTED(Z_TYPE_P(container) > IS_FALSE)) { @@ -81028,6 +81759,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_UNSET_OBJ_SPEC_VAR zval *container; zval *offset; zend_string *name, *tmp_name; + zend_object *zobj = NULL; SAVE_OPLINE(); container = _get_zval_ptr_ptr_var(opline->op1.var EXECUTE_DATA_CC); @@ -81048,6 +81780,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_UNSET_OBJ_SPEC_VAR break; } } + zobj = Z_OBJ_P(container); if (IS_TMP_VAR == IS_CONST) { name = Z_STR_P(offset); } else { @@ -81062,6 +81795,8 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_UNSET_OBJ_SPEC_VAR } } while (0); + ZEND_MAYBE_TRACK_OBJECT_MUTATION(zobj); + zval_ptr_dtor_nogc(EX_VAR(opline->op2.var)); zval_ptr_dtor_nogc(EX_VAR(opline->op1.var)); ZEND_VM_NEXT_OPCODE_CHECK_EXCEPTION(); @@ -81233,12 +81968,16 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_ASSIGN_DIM_OP_SPEC zval *var_ptr; zval *value, *container, *dim; HashTable *ht; + bool should_publish_tracked_array = false; SAVE_OPLINE(); container = _get_zval_ptr_ptr_var(opline->op1.var EXECUTE_DATA_CC); if (EXPECTED(Z_TYPE_P(container) == IS_ARRAY)) { assign_dim_op_array: + if (UNEXPECTED(EG(tracked_mutation_hooks_active))) { + should_publish_tracked_array = zend_maybe_track_hash_mutation(Z_ARRVAL_P(container), false); + } SEPARATE_ARRAY(container); ht = Z_ARRVAL_P(container); assign_dim_op_new_array: @@ -81278,6 +82017,9 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_ASSIGN_DIM_OP_SPEC ZVAL_COPY(EX_VAR(opline->result.var), var_ptr); } FREE_OP((opline+1)->op1_type, (opline+1)->op1.var); + if (UNEXPECTED(should_publish_tracked_array)) { + zend_maybe_track_hash_mutation(Z_ARRVAL_P(container), true); + } } else { if (EXPECTED(Z_ISREF_P(container))) { container = Z_REFVAL_P(container); @@ -81294,6 +82036,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_ASSIGN_DIM_OP_SPEC dim++; } zend_binary_assign_op_obj_dim(obj, dim OPLINE_CC EXECUTE_DATA_CC); + ZEND_MAYBE_TRACK_OBJECT_MUTATION(obj); } else if (EXPECTED(Z_TYPE_P(container) <= IS_FALSE)) { uint8_t old_type; @@ -81393,12 +82136,16 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_ASSIGN_DIM_SPEC_VA zval *variable_ptr; zval *dim; zend_refcounted *garbage = NULL; + bool should_publish_tracked_array = false; SAVE_OPLINE(); orig_object_ptr = object_ptr = _get_zval_ptr_ptr_var(opline->op1.var EXECUTE_DATA_CC); if (EXPECTED(Z_TYPE_P(object_ptr) == IS_ARRAY)) { try_assign_dim_array: + if (UNEXPECTED(EG(tracked_mutation_hooks_active))) { + should_publish_tracked_array = zend_maybe_track_hash_mutation(Z_ARRVAL_P(object_ptr), false); + } SEPARATE_ARRAY(object_ptr); if (IS_UNUSED == IS_UNUSED) { value = RT_CONSTANT((opline+1), (opline+1)->op1); @@ -81456,6 +82203,9 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_ASSIGN_DIM_SPEC_VA if (garbage) { GC_DTOR_NO_REF(garbage); } + if (UNEXPECTED(should_publish_tracked_array)) { + zend_maybe_track_hash_mutation(Z_ARRVAL_P(object_ptr), true); + } } else { if (EXPECTED(Z_ISREF_P(object_ptr))) { object_ptr = Z_REFVAL_P(object_ptr); @@ -81482,6 +82232,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_ASSIGN_DIM_SPEC_VA } zend_assign_to_object_dim(obj, dim, value OPLINE_CC EXECUTE_DATA_CC); + ZEND_MAYBE_TRACK_OBJECT_MUTATION(obj); if (UNEXPECTED(GC_DELREF(obj) == 0)) { @@ -81551,12 +82302,16 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_ASSIGN_DIM_SPEC_VA zval *variable_ptr; zval *dim; zend_refcounted *garbage = NULL; + bool should_publish_tracked_array = false; SAVE_OPLINE(); orig_object_ptr = object_ptr = _get_zval_ptr_ptr_var(opline->op1.var EXECUTE_DATA_CC); if (EXPECTED(Z_TYPE_P(object_ptr) == IS_ARRAY)) { try_assign_dim_array: + if (UNEXPECTED(EG(tracked_mutation_hooks_active))) { + should_publish_tracked_array = zend_maybe_track_hash_mutation(Z_ARRVAL_P(object_ptr), false); + } SEPARATE_ARRAY(object_ptr); if (IS_UNUSED == IS_UNUSED) { value = _get_zval_ptr_tmp((opline+1)->op1.var EXECUTE_DATA_CC); @@ -81614,6 +82369,9 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_ASSIGN_DIM_SPEC_VA if (garbage) { GC_DTOR_NO_REF(garbage); } + if (UNEXPECTED(should_publish_tracked_array)) { + zend_maybe_track_hash_mutation(Z_ARRVAL_P(object_ptr), true); + } } else { if (EXPECTED(Z_ISREF_P(object_ptr))) { object_ptr = Z_REFVAL_P(object_ptr); @@ -81640,6 +82398,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_ASSIGN_DIM_SPEC_VA } zend_assign_to_object_dim(obj, dim, value OPLINE_CC EXECUTE_DATA_CC); + ZEND_MAYBE_TRACK_OBJECT_MUTATION(obj); zval_ptr_dtor_nogc(EX_VAR((opline+1)->op1.var)); if (UNEXPECTED(GC_DELREF(obj) == 0)) { @@ -81705,12 +82464,16 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_ASSIGN_DIM_SPEC_VA zval *variable_ptr; zval *dim; zend_refcounted *garbage = NULL; + bool should_publish_tracked_array = false; SAVE_OPLINE(); orig_object_ptr = object_ptr = _get_zval_ptr_ptr_var(opline->op1.var EXECUTE_DATA_CC); if (EXPECTED(Z_TYPE_P(object_ptr) == IS_ARRAY)) { try_assign_dim_array: + if (UNEXPECTED(EG(tracked_mutation_hooks_active))) { + should_publish_tracked_array = zend_maybe_track_hash_mutation(Z_ARRVAL_P(object_ptr), false); + } SEPARATE_ARRAY(object_ptr); if (IS_UNUSED == IS_UNUSED) { value = EX_VAR((opline+1)->op1.var); @@ -81768,6 +82531,9 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_ASSIGN_DIM_SPEC_VA if (garbage) { GC_DTOR_NO_REF(garbage); } + if (UNEXPECTED(should_publish_tracked_array)) { + zend_maybe_track_hash_mutation(Z_ARRVAL_P(object_ptr), true); + } } else { if (EXPECTED(Z_ISREF_P(object_ptr))) { object_ptr = Z_REFVAL_P(object_ptr); @@ -81794,6 +82560,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_ASSIGN_DIM_SPEC_VA } zend_assign_to_object_dim(obj, dim, value OPLINE_CC EXECUTE_DATA_CC); + ZEND_MAYBE_TRACK_OBJECT_MUTATION(obj); if (UNEXPECTED(GC_DELREF(obj) == 0)) { @@ -82890,7 +83657,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_ASSIGN_OBJ_OP_SPEC void *_cache_slot[3] = {0}; void **cache_slot; zend_property_info *prop_info; - zend_object *zobj; + zend_object *zobj = NULL; zend_string *name, *tmp_name; SAVE_OPLINE(); @@ -82965,6 +83732,8 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_ASSIGN_OBJ_OP_SPEC } } while (0); + ZEND_MAYBE_TRACK_OBJECT_MUTATION(zobj); + FREE_OP((opline+1)->op1_type, (opline+1)->op1.var); @@ -82980,12 +83749,16 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_ASSIGN_DIM_OP_SPEC zval *var_ptr; zval *value, *container, *dim; HashTable *ht; + bool should_publish_tracked_array = false; SAVE_OPLINE(); container = _get_zval_ptr_ptr_var(opline->op1.var EXECUTE_DATA_CC); if (EXPECTED(Z_TYPE_P(container) == IS_ARRAY)) { assign_dim_op_array: + if (UNEXPECTED(EG(tracked_mutation_hooks_active))) { + should_publish_tracked_array = zend_maybe_track_hash_mutation(Z_ARRVAL_P(container), false); + } SEPARATE_ARRAY(container); ht = Z_ARRVAL_P(container); assign_dim_op_new_array: @@ -83025,6 +83798,9 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_ASSIGN_DIM_OP_SPEC ZVAL_COPY(EX_VAR(opline->result.var), var_ptr); } FREE_OP((opline+1)->op1_type, (opline+1)->op1.var); + if (UNEXPECTED(should_publish_tracked_array)) { + zend_maybe_track_hash_mutation(Z_ARRVAL_P(container), true); + } } else { if (EXPECTED(Z_ISREF_P(container))) { container = Z_REFVAL_P(container); @@ -83041,6 +83817,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_ASSIGN_DIM_OP_SPEC dim++; } zend_binary_assign_op_obj_dim(obj, dim OPLINE_CC EXECUTE_DATA_CC); + ZEND_MAYBE_TRACK_OBJECT_MUTATION(obj); } else if (EXPECTED(Z_TYPE_P(container) <= IS_FALSE)) { uint8_t old_type; @@ -83115,7 +83892,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_PRE_INC_OBJ_SPEC_V void *_cache_slot[3] = {0}; void **cache_slot; zend_property_info *prop_info; - zend_object *zobj; + zend_object *zobj = NULL; zend_string *name, *tmp_name; SAVE_OPLINE(); @@ -83167,6 +83944,8 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_PRE_INC_OBJ_SPEC_V } } while (0); + ZEND_MAYBE_TRACK_OBJECT_MUTATION(zobj); + zval_ptr_dtor_nogc(EX_VAR(opline->op1.var)); ZEND_VM_NEXT_OPCODE_CHECK_EXCEPTION(); @@ -83181,7 +83960,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_POST_INC_OBJ_SPEC_ void *_cache_slot[3] = {0}; void **cache_slot; zend_property_info *prop_info; - zend_object *zobj; + zend_object *zobj = NULL; zend_string *name, *tmp_name; SAVE_OPLINE(); @@ -83231,6 +84010,8 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_POST_INC_OBJ_SPEC_ } } while (0); + ZEND_MAYBE_TRACK_OBJECT_MUTATION(zobj); + zval_ptr_dtor_nogc(EX_VAR(opline->op1.var)); ZEND_VM_NEXT_OPCODE_CHECK_EXCEPTION(); @@ -83417,7 +84198,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_ASSIGN_OBJ_SPEC_VA { USE_OPLINE zval *object, *value, tmp; - zend_object *zobj; + zend_object *zobj = NULL; zend_string *name, *tmp_name; zend_refcounted *garbage = NULL; @@ -83559,6 +84340,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_ASSIGN_OBJ_SPEC_VA exit_assign_obj: + ZEND_MAYBE_TRACK_OBJECT_MUTATION_WITH_VALUE(zobj, value); if (garbage) { GC_DTOR_NO_REF(garbage); } @@ -83574,7 +84356,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_ASSIGN_OBJ_SPEC_VA { USE_OPLINE zval *object, *value, tmp; - zend_object *zobj; + zend_object *zobj = NULL; zend_string *name, *tmp_name; zend_refcounted *garbage = NULL; @@ -83714,6 +84496,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_ASSIGN_OBJ_SPEC_VA } zval_ptr_dtor_nogc(EX_VAR((opline+1)->op1.var)); exit_assign_obj: + ZEND_MAYBE_TRACK_OBJECT_MUTATION_WITH_VALUE(zobj, value); if (garbage) { GC_DTOR_NO_REF(garbage); } @@ -83729,7 +84512,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_ASSIGN_OBJ_SPEC_VA { USE_OPLINE zval *object, *value, tmp; - zend_object *zobj; + zend_object *zobj = NULL; zend_string *name, *tmp_name; zend_refcounted *garbage = NULL; @@ -83871,6 +84654,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_ASSIGN_OBJ_SPEC_VA exit_assign_obj: + ZEND_MAYBE_TRACK_OBJECT_MUTATION_WITH_VALUE(zobj, value); if (garbage) { GC_DTOR_NO_REF(garbage); } @@ -83890,12 +84674,16 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_ASSIGN_DIM_SPEC_VA zval *variable_ptr; zval *dim; zend_refcounted *garbage = NULL; + bool should_publish_tracked_array = false; SAVE_OPLINE(); orig_object_ptr = object_ptr = _get_zval_ptr_ptr_var(opline->op1.var EXECUTE_DATA_CC); if (EXPECTED(Z_TYPE_P(object_ptr) == IS_ARRAY)) { try_assign_dim_array: + if (UNEXPECTED(EG(tracked_mutation_hooks_active))) { + should_publish_tracked_array = zend_maybe_track_hash_mutation(Z_ARRVAL_P(object_ptr), false); + } SEPARATE_ARRAY(object_ptr); if (IS_CV == IS_UNUSED) { value = RT_CONSTANT((opline+1), (opline+1)->op1); @@ -83953,6 +84741,9 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_ASSIGN_DIM_SPEC_VA if (garbage) { GC_DTOR_NO_REF(garbage); } + if (UNEXPECTED(should_publish_tracked_array)) { + zend_maybe_track_hash_mutation(Z_ARRVAL_P(object_ptr), true); + } } else { if (EXPECTED(Z_ISREF_P(object_ptr))) { object_ptr = Z_REFVAL_P(object_ptr); @@ -83979,6 +84770,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_ASSIGN_DIM_SPEC_VA } zend_assign_to_object_dim(obj, dim, value OPLINE_CC EXECUTE_DATA_CC); + ZEND_MAYBE_TRACK_OBJECT_MUTATION(obj); if (UNEXPECTED(GC_DELREF(obj) == 0)) { @@ -84048,12 +84840,16 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_ASSIGN_DIM_SPEC_VA zval *variable_ptr; zval *dim; zend_refcounted *garbage = NULL; + bool should_publish_tracked_array = false; SAVE_OPLINE(); orig_object_ptr = object_ptr = _get_zval_ptr_ptr_var(opline->op1.var EXECUTE_DATA_CC); if (EXPECTED(Z_TYPE_P(object_ptr) == IS_ARRAY)) { try_assign_dim_array: + if (UNEXPECTED(EG(tracked_mutation_hooks_active))) { + should_publish_tracked_array = zend_maybe_track_hash_mutation(Z_ARRVAL_P(object_ptr), false); + } SEPARATE_ARRAY(object_ptr); if (IS_CV == IS_UNUSED) { value = _get_zval_ptr_tmp((opline+1)->op1.var EXECUTE_DATA_CC); @@ -84111,6 +84907,9 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_ASSIGN_DIM_SPEC_VA if (garbage) { GC_DTOR_NO_REF(garbage); } + if (UNEXPECTED(should_publish_tracked_array)) { + zend_maybe_track_hash_mutation(Z_ARRVAL_P(object_ptr), true); + } } else { if (EXPECTED(Z_ISREF_P(object_ptr))) { object_ptr = Z_REFVAL_P(object_ptr); @@ -84137,6 +84936,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_ASSIGN_DIM_SPEC_VA } zend_assign_to_object_dim(obj, dim, value OPLINE_CC EXECUTE_DATA_CC); + ZEND_MAYBE_TRACK_OBJECT_MUTATION(obj); zval_ptr_dtor_nogc(EX_VAR((opline+1)->op1.var)); if (UNEXPECTED(GC_DELREF(obj) == 0)) { @@ -84202,12 +85002,16 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_ASSIGN_DIM_SPEC_VA zval *variable_ptr; zval *dim; zend_refcounted *garbage = NULL; + bool should_publish_tracked_array = false; SAVE_OPLINE(); orig_object_ptr = object_ptr = _get_zval_ptr_ptr_var(opline->op1.var EXECUTE_DATA_CC); if (EXPECTED(Z_TYPE_P(object_ptr) == IS_ARRAY)) { try_assign_dim_array: + if (UNEXPECTED(EG(tracked_mutation_hooks_active))) { + should_publish_tracked_array = zend_maybe_track_hash_mutation(Z_ARRVAL_P(object_ptr), false); + } SEPARATE_ARRAY(object_ptr); if (IS_CV == IS_UNUSED) { value = EX_VAR((opline+1)->op1.var); @@ -84265,6 +85069,9 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_ASSIGN_DIM_SPEC_VA if (garbage) { GC_DTOR_NO_REF(garbage); } + if (UNEXPECTED(should_publish_tracked_array)) { + zend_maybe_track_hash_mutation(Z_ARRVAL_P(object_ptr), true); + } } else { if (EXPECTED(Z_ISREF_P(object_ptr))) { object_ptr = Z_REFVAL_P(object_ptr); @@ -84291,6 +85098,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_ASSIGN_DIM_SPEC_VA } zend_assign_to_object_dim(obj, dim, value OPLINE_CC EXECUTE_DATA_CC); + ZEND_MAYBE_TRACK_OBJECT_MUTATION(obj); if (UNEXPECTED(GC_DELREF(obj) == 0)) { @@ -84453,6 +85261,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_ASSIGN_OBJ_REF_SPE { USE_OPLINE zval *property, *container, *value_ptr; + zend_object *zobj = NULL; SAVE_OPLINE(); @@ -84460,6 +85269,11 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_ASSIGN_OBJ_REF_SPE property = _get_zval_ptr_cv_BP_VAR_R(opline->op2.var EXECUTE_DATA_CC); value_ptr = _get_zval_ptr_ptr_var((opline+1)->op1.var EXECUTE_DATA_CC); + if (EXPECTED(Z_TYPE_P(container) == IS_OBJECT)) { + zobj = Z_OBJ_P(container); + } else if (Z_ISREF_P(container) && EXPECTED(Z_TYPE_P(Z_REFVAL_P(container)) == IS_OBJECT)) { + zobj = Z_OBJ_P(Z_REFVAL_P(container)); + } if (1) { if (IS_VAR == IS_UNUSED) { @@ -84479,6 +85293,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_ASSIGN_OBJ_REF_SPE zend_assign_to_property_reference(container, IS_VAR, property, IS_CV, value_ptr OPLINE_CC EXECUTE_DATA_CC); } + ZEND_MAYBE_TRACK_OBJECT_MUTATION(zobj); zval_ptr_dtor_nogc(EX_VAR(opline->op1.var)); @@ -84491,6 +85306,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_ASSIGN_OBJ_REF_SPE { USE_OPLINE zval *property, *container, *value_ptr; + zend_object *zobj = NULL; SAVE_OPLINE(); @@ -84498,6 +85314,11 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_ASSIGN_OBJ_REF_SPE property = _get_zval_ptr_cv_BP_VAR_R(opline->op2.var EXECUTE_DATA_CC); value_ptr = _get_zval_ptr_cv_BP_VAR_W((opline+1)->op1.var EXECUTE_DATA_CC); + if (EXPECTED(Z_TYPE_P(container) == IS_OBJECT)) { + zobj = Z_OBJ_P(container); + } else if (Z_ISREF_P(container) && EXPECTED(Z_TYPE_P(Z_REFVAL_P(container)) == IS_OBJECT)) { + zobj = Z_OBJ_P(Z_REFVAL_P(container)); + } if (1) { if (IS_VAR == IS_UNUSED) { @@ -84517,6 +85338,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_ASSIGN_OBJ_REF_SPE zend_assign_to_property_reference(container, IS_VAR, property, IS_CV, value_ptr OPLINE_CC EXECUTE_DATA_CC); } + ZEND_MAYBE_TRACK_OBJECT_MUTATION(zobj); zval_ptr_dtor_nogc(EX_VAR(opline->op1.var)); @@ -84804,6 +85626,8 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_UNSET_DIM_SPEC_VAR zval *offset; zend_ulong hval; zend_string *key; + bool should_flush_array = false; + bool should_publish_tracked_array = false; SAVE_OPLINE(); container = _get_zval_ptr_ptr_var(opline->op1.var EXECUTE_DATA_CC); @@ -84814,6 +85638,9 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_UNSET_DIM_SPEC_VAR HashTable *ht; unset_dim_array: + if (UNEXPECTED(EG(tracked_mutation_hooks_active))) { + should_publish_tracked_array = zend_maybe_track_hash_mutation(Z_ARRVAL_P(container), false); + } SEPARATE_ARRAY(container); ht = Z_ARRVAL_P(container); offset_again: @@ -84827,10 +85654,12 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_UNSET_DIM_SPEC_VAR str_index_dim: ZEND_ASSERT(ht != &EG(symbol_table)); zend_hash_del(ht, key); + should_flush_array = true; } else if (EXPECTED(Z_TYPE_P(offset) == IS_LONG)) { hval = Z_LVAL_P(offset); num_index_dim: zend_hash_index_del(ht, hval); + should_flush_array = true; } else if ((IS_CV & (IS_VAR|IS_CV)) && EXPECTED(Z_TYPE_P(offset) == IS_REFERENCE)) { offset = Z_REFVAL_P(offset); goto offset_again; @@ -84867,6 +85696,9 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_UNSET_DIM_SPEC_VAR } else { zend_illegal_array_offset_unset(offset); } + if (UNEXPECTED(should_flush_array && should_publish_tracked_array)) { + zend_maybe_track_hash_mutation(ht, true); + } break; } else if (Z_ISREF_P(container)) { container = Z_REFVAL_P(container); @@ -84885,6 +85717,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_UNSET_DIM_SPEC_VAR offset++; } Z_OBJ_HT_P(container)->unset_dimension(Z_OBJ_P(container), offset); + ZEND_MAYBE_TRACK_OBJECT_MUTATION(Z_OBJ_P(container)); } else if (UNEXPECTED(Z_TYPE_P(container) == IS_STRING)) { zend_throw_error(NULL, "Cannot unset string offsets"); } else if (UNEXPECTED(Z_TYPE_P(container) > IS_FALSE)) { @@ -84905,6 +85738,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_UNSET_OBJ_SPEC_VAR zval *container; zval *offset; zend_string *name, *tmp_name; + zend_object *zobj = NULL; SAVE_OPLINE(); container = _get_zval_ptr_ptr_var(opline->op1.var EXECUTE_DATA_CC); @@ -84925,6 +85759,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_UNSET_OBJ_SPEC_VAR break; } } + zobj = Z_OBJ_P(container); if (IS_CV == IS_CONST) { name = Z_STR_P(offset); } else { @@ -84939,6 +85774,8 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_UNSET_OBJ_SPEC_VAR } } while (0); + ZEND_MAYBE_TRACK_OBJECT_MUTATION(zobj); + zval_ptr_dtor_nogc(EX_VAR(opline->op1.var)); ZEND_VM_NEXT_OPCODE_CHECK_EXCEPTION(); @@ -85343,7 +86180,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_ASSIGN_OBJ_OP_SPEC void *_cache_slot[3] = {0}; void **cache_slot; zend_property_info *prop_info; - zend_object *zobj; + zend_object *zobj = NULL; zend_string *name, *tmp_name; SAVE_OPLINE(); @@ -85418,6 +86255,8 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_ASSIGN_OBJ_OP_SPEC } } while (0); + ZEND_MAYBE_TRACK_OBJECT_MUTATION(zobj); + FREE_OP((opline+1)->op1_type, (opline+1)->op1.var); @@ -85437,7 +86276,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_PRE_INC_OBJ_SPEC_U void *_cache_slot[3] = {0}; void **cache_slot; zend_property_info *prop_info; - zend_object *zobj; + zend_object *zobj = NULL; zend_string *name, *tmp_name; SAVE_OPLINE(); @@ -85489,6 +86328,8 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_PRE_INC_OBJ_SPEC_U } } while (0); + ZEND_MAYBE_TRACK_OBJECT_MUTATION(zobj); + @@ -85504,7 +86345,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_POST_INC_OBJ_SPEC_ void *_cache_slot[3] = {0}; void **cache_slot; zend_property_info *prop_info; - zend_object *zobj; + zend_object *zobj = NULL; zend_string *name, *tmp_name; SAVE_OPLINE(); @@ -85554,6 +86395,8 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_POST_INC_OBJ_SPEC_ } } while (0); + ZEND_MAYBE_TRACK_OBJECT_MUTATION(zobj); + @@ -85951,7 +86794,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_ASSIGN_OBJ_SPEC_UN { USE_OPLINE zval *object, *value, tmp; - zend_object *zobj; + zend_object *zobj = NULL; zend_string *name, *tmp_name; zend_refcounted *garbage = NULL; @@ -86093,6 +86936,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_ASSIGN_OBJ_SPEC_UN exit_assign_obj: + ZEND_MAYBE_TRACK_OBJECT_MUTATION_WITH_VALUE(zobj, value); if (garbage) { GC_DTOR_NO_REF(garbage); } @@ -86109,7 +86953,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_ASSIGN_OBJ_SPEC_UN { USE_OPLINE zval *object, *value, tmp; - zend_object *zobj; + zend_object *zobj = NULL; zend_string *name, *tmp_name; zend_refcounted *garbage = NULL; @@ -86249,6 +87093,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_ASSIGN_OBJ_SPEC_UN } zval_ptr_dtor_nogc(EX_VAR((opline+1)->op1.var)); exit_assign_obj: + ZEND_MAYBE_TRACK_OBJECT_MUTATION_WITH_VALUE(zobj, value); if (garbage) { GC_DTOR_NO_REF(garbage); } @@ -86265,7 +87110,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_ASSIGN_OBJ_SPEC_UN { USE_OPLINE zval *object, *value, tmp; - zend_object *zobj; + zend_object *zobj = NULL; zend_string *name, *tmp_name; zend_refcounted *garbage = NULL; @@ -86407,6 +87252,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_ASSIGN_OBJ_SPEC_UN exit_assign_obj: + ZEND_MAYBE_TRACK_OBJECT_MUTATION_WITH_VALUE(zobj, value); if (garbage) { GC_DTOR_NO_REF(garbage); } @@ -86423,6 +87269,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_ASSIGN_OBJ_REF_SPE { USE_OPLINE zval *property, *container, *value_ptr; + zend_object *zobj = NULL; SAVE_OPLINE(); @@ -86430,6 +87277,11 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_ASSIGN_OBJ_REF_SPE property = RT_CONSTANT(opline, opline->op2); value_ptr = _get_zval_ptr_ptr_var((opline+1)->op1.var EXECUTE_DATA_CC); + if (EXPECTED(Z_TYPE_P(container) == IS_OBJECT)) { + zobj = Z_OBJ_P(container); + } else if (Z_ISREF_P(container) && EXPECTED(Z_TYPE_P(Z_REFVAL_P(container)) == IS_OBJECT)) { + zobj = Z_OBJ_P(Z_REFVAL_P(container)); + } if (1) { if (IS_UNUSED == IS_UNUSED) { @@ -86449,6 +87301,8 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_ASSIGN_OBJ_REF_SPE zend_assign_to_property_reference(container, IS_UNUSED, property, IS_CONST, value_ptr OPLINE_CC EXECUTE_DATA_CC); } + ZEND_MAYBE_TRACK_OBJECT_MUTATION(zobj); + @@ -86461,6 +87315,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_ASSIGN_OBJ_REF_SPE { USE_OPLINE zval *property, *container, *value_ptr; + zend_object *zobj = NULL; SAVE_OPLINE(); @@ -86468,6 +87323,11 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_ASSIGN_OBJ_REF_SPE property = RT_CONSTANT(opline, opline->op2); value_ptr = _get_zval_ptr_cv_BP_VAR_W((opline+1)->op1.var EXECUTE_DATA_CC); + if (EXPECTED(Z_TYPE_P(container) == IS_OBJECT)) { + zobj = Z_OBJ_P(container); + } else if (Z_ISREF_P(container) && EXPECTED(Z_TYPE_P(Z_REFVAL_P(container)) == IS_OBJECT)) { + zobj = Z_OBJ_P(Z_REFVAL_P(container)); + } if (1) { if (IS_UNUSED == IS_UNUSED) { @@ -86487,6 +87347,8 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_ASSIGN_OBJ_REF_SPE zend_assign_to_property_reference(container, IS_UNUSED, property, IS_CONST, value_ptr OPLINE_CC EXECUTE_DATA_CC); } + ZEND_MAYBE_TRACK_OBJECT_MUTATION(zobj); + @@ -87095,6 +87957,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_UNSET_OBJ_SPEC_UNU zval *container; zval *offset; zend_string *name, *tmp_name; + zend_object *zobj = NULL; SAVE_OPLINE(); container = &EX(This); @@ -87115,6 +87978,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_UNSET_OBJ_SPEC_UNU break; } } + zobj = Z_OBJ_P(container); if (IS_CONST == IS_CONST) { name = Z_STR_P(offset); } else { @@ -87129,6 +87993,8 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_UNSET_OBJ_SPEC_UNU } } while (0); + ZEND_MAYBE_TRACK_OBJECT_MUTATION(zobj); + @@ -87452,7 +88318,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_ASSIGN_OBJ_OP_SPEC void *_cache_slot[3] = {0}; void **cache_slot; zend_property_info *prop_info; - zend_object *zobj; + zend_object *zobj = NULL; zend_string *name, *tmp_name; SAVE_OPLINE(); @@ -87527,6 +88393,8 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_ASSIGN_OBJ_OP_SPEC } } while (0); + ZEND_MAYBE_TRACK_OBJECT_MUTATION(zobj); + FREE_OP((opline+1)->op1_type, (opline+1)->op1.var); zval_ptr_dtor_nogc(EX_VAR(opline->op2.var)); @@ -87545,7 +88413,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_PRE_INC_OBJ_SPEC_U void *_cache_slot[3] = {0}; void **cache_slot; zend_property_info *prop_info; - zend_object *zobj; + zend_object *zobj = NULL; zend_string *name, *tmp_name; SAVE_OPLINE(); @@ -87597,6 +88465,8 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_PRE_INC_OBJ_SPEC_U } } while (0); + ZEND_MAYBE_TRACK_OBJECT_MUTATION(zobj); + zval_ptr_dtor_nogc(EX_VAR(opline->op2.var)); @@ -87612,7 +88482,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_POST_INC_OBJ_SPEC_ void *_cache_slot[3] = {0}; void **cache_slot; zend_property_info *prop_info; - zend_object *zobj; + zend_object *zobj = NULL; zend_string *name, *tmp_name; SAVE_OPLINE(); @@ -87662,6 +88532,8 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_POST_INC_OBJ_SPEC_ } } while (0); + ZEND_MAYBE_TRACK_OBJECT_MUTATION(zobj); + zval_ptr_dtor_nogc(EX_VAR(opline->op2.var)); @@ -88049,7 +88921,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_ASSIGN_OBJ_SPEC_UN { USE_OPLINE zval *object, *value, tmp; - zend_object *zobj; + zend_object *zobj = NULL; zend_string *name, *tmp_name; zend_refcounted *garbage = NULL; @@ -88191,6 +89063,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_ASSIGN_OBJ_SPEC_UN exit_assign_obj: + ZEND_MAYBE_TRACK_OBJECT_MUTATION_WITH_VALUE(zobj, value); if (garbage) { GC_DTOR_NO_REF(garbage); } @@ -88206,7 +89079,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_ASSIGN_OBJ_SPEC_UN { USE_OPLINE zval *object, *value, tmp; - zend_object *zobj; + zend_object *zobj = NULL; zend_string *name, *tmp_name; zend_refcounted *garbage = NULL; @@ -88346,6 +89219,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_ASSIGN_OBJ_SPEC_UN } zval_ptr_dtor_nogc(EX_VAR((opline+1)->op1.var)); exit_assign_obj: + ZEND_MAYBE_TRACK_OBJECT_MUTATION_WITH_VALUE(zobj, value); if (garbage) { GC_DTOR_NO_REF(garbage); } @@ -88361,7 +89235,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_ASSIGN_OBJ_SPEC_UN { USE_OPLINE zval *object, *value, tmp; - zend_object *zobj; + zend_object *zobj = NULL; zend_string *name, *tmp_name; zend_refcounted *garbage = NULL; @@ -88503,6 +89377,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_ASSIGN_OBJ_SPEC_UN exit_assign_obj: + ZEND_MAYBE_TRACK_OBJECT_MUTATION_WITH_VALUE(zobj, value); if (garbage) { GC_DTOR_NO_REF(garbage); } @@ -88518,6 +89393,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_ASSIGN_OBJ_REF_SPE { USE_OPLINE zval *property, *container, *value_ptr; + zend_object *zobj = NULL; SAVE_OPLINE(); @@ -88525,6 +89401,11 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_ASSIGN_OBJ_REF_SPE property = _get_zval_ptr_tmp(opline->op2.var EXECUTE_DATA_CC); value_ptr = _get_zval_ptr_ptr_var((opline+1)->op1.var EXECUTE_DATA_CC); + if (EXPECTED(Z_TYPE_P(container) == IS_OBJECT)) { + zobj = Z_OBJ_P(container); + } else if (Z_ISREF_P(container) && EXPECTED(Z_TYPE_P(Z_REFVAL_P(container)) == IS_OBJECT)) { + zobj = Z_OBJ_P(Z_REFVAL_P(container)); + } if (1) { if (IS_UNUSED == IS_UNUSED) { @@ -88544,6 +89425,8 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_ASSIGN_OBJ_REF_SPE zend_assign_to_property_reference(container, IS_UNUSED, property, IS_TMP_VAR, value_ptr OPLINE_CC EXECUTE_DATA_CC); } + ZEND_MAYBE_TRACK_OBJECT_MUTATION(zobj); + zval_ptr_dtor_nogc(EX_VAR(opline->op2.var)); zval_ptr_dtor_nogc(EX_VAR((opline+1)->op1.var)); @@ -88555,6 +89438,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_ASSIGN_OBJ_REF_SPE { USE_OPLINE zval *property, *container, *value_ptr; + zend_object *zobj = NULL; SAVE_OPLINE(); @@ -88562,6 +89446,11 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_ASSIGN_OBJ_REF_SPE property = _get_zval_ptr_tmp(opline->op2.var EXECUTE_DATA_CC); value_ptr = _get_zval_ptr_cv_BP_VAR_W((opline+1)->op1.var EXECUTE_DATA_CC); + if (EXPECTED(Z_TYPE_P(container) == IS_OBJECT)) { + zobj = Z_OBJ_P(container); + } else if (Z_ISREF_P(container) && EXPECTED(Z_TYPE_P(Z_REFVAL_P(container)) == IS_OBJECT)) { + zobj = Z_OBJ_P(Z_REFVAL_P(container)); + } if (1) { if (IS_UNUSED == IS_UNUSED) { @@ -88581,6 +89470,8 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_ASSIGN_OBJ_REF_SPE zend_assign_to_property_reference(container, IS_UNUSED, property, IS_TMP_VAR, value_ptr OPLINE_CC EXECUTE_DATA_CC); } + ZEND_MAYBE_TRACK_OBJECT_MUTATION(zobj); + zval_ptr_dtor_nogc(EX_VAR(opline->op2.var)); @@ -88986,6 +89877,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_UNSET_OBJ_SPEC_UNU zval *container; zval *offset; zend_string *name, *tmp_name; + zend_object *zobj = NULL; SAVE_OPLINE(); container = &EX(This); @@ -89006,6 +89898,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_UNSET_OBJ_SPEC_UNU break; } } + zobj = Z_OBJ_P(container); if (IS_TMP_VAR == IS_CONST) { name = Z_STR_P(offset); } else { @@ -89020,6 +89913,8 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_UNSET_OBJ_SPEC_UNU } } while (0); + ZEND_MAYBE_TRACK_OBJECT_MUTATION(zobj); + zval_ptr_dtor_nogc(EX_VAR(opline->op2.var)); @@ -90015,7 +90910,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_ASSIGN_OBJ_OP_SPEC void *_cache_slot[3] = {0}; void **cache_slot; zend_property_info *prop_info; - zend_object *zobj; + zend_object *zobj = NULL; zend_string *name, *tmp_name; SAVE_OPLINE(); @@ -90090,6 +90985,8 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_ASSIGN_OBJ_OP_SPEC } } while (0); + ZEND_MAYBE_TRACK_OBJECT_MUTATION(zobj); + FREE_OP((opline+1)->op1_type, (opline+1)->op1.var); @@ -90109,7 +91006,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_PRE_INC_OBJ_SPEC_U void *_cache_slot[3] = {0}; void **cache_slot; zend_property_info *prop_info; - zend_object *zobj; + zend_object *zobj = NULL; zend_string *name, *tmp_name; SAVE_OPLINE(); @@ -90161,6 +91058,8 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_PRE_INC_OBJ_SPEC_U } } while (0); + ZEND_MAYBE_TRACK_OBJECT_MUTATION(zobj); + @@ -90176,7 +91075,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_POST_INC_OBJ_SPEC_ void *_cache_slot[3] = {0}; void **cache_slot; zend_property_info *prop_info; - zend_object *zobj; + zend_object *zobj = NULL; zend_string *name, *tmp_name; SAVE_OPLINE(); @@ -90226,6 +91125,8 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_POST_INC_OBJ_SPEC_ } } while (0); + ZEND_MAYBE_TRACK_OBJECT_MUTATION(zobj); + @@ -90618,7 +91519,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_ASSIGN_OBJ_SPEC_UN { USE_OPLINE zval *object, *value, tmp; - zend_object *zobj; + zend_object *zobj = NULL; zend_string *name, *tmp_name; zend_refcounted *garbage = NULL; @@ -90760,6 +91661,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_ASSIGN_OBJ_SPEC_UN exit_assign_obj: + ZEND_MAYBE_TRACK_OBJECT_MUTATION_WITH_VALUE(zobj, value); if (garbage) { GC_DTOR_NO_REF(garbage); } @@ -90776,7 +91678,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_ASSIGN_OBJ_SPEC_UN { USE_OPLINE zval *object, *value, tmp; - zend_object *zobj; + zend_object *zobj = NULL; zend_string *name, *tmp_name; zend_refcounted *garbage = NULL; @@ -90916,6 +91818,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_ASSIGN_OBJ_SPEC_UN } zval_ptr_dtor_nogc(EX_VAR((opline+1)->op1.var)); exit_assign_obj: + ZEND_MAYBE_TRACK_OBJECT_MUTATION_WITH_VALUE(zobj, value); if (garbage) { GC_DTOR_NO_REF(garbage); } @@ -90932,7 +91835,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_ASSIGN_OBJ_SPEC_UN { USE_OPLINE zval *object, *value, tmp; - zend_object *zobj; + zend_object *zobj = NULL; zend_string *name, *tmp_name; zend_refcounted *garbage = NULL; @@ -91074,6 +91977,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_ASSIGN_OBJ_SPEC_UN exit_assign_obj: + ZEND_MAYBE_TRACK_OBJECT_MUTATION_WITH_VALUE(zobj, value); if (garbage) { GC_DTOR_NO_REF(garbage); } @@ -91090,6 +91994,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_ASSIGN_OBJ_REF_SPE { USE_OPLINE zval *property, *container, *value_ptr; + zend_object *zobj = NULL; SAVE_OPLINE(); @@ -91097,6 +92002,11 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_ASSIGN_OBJ_REF_SPE property = _get_zval_ptr_cv_BP_VAR_R(opline->op2.var EXECUTE_DATA_CC); value_ptr = _get_zval_ptr_ptr_var((opline+1)->op1.var EXECUTE_DATA_CC); + if (EXPECTED(Z_TYPE_P(container) == IS_OBJECT)) { + zobj = Z_OBJ_P(container); + } else if (Z_ISREF_P(container) && EXPECTED(Z_TYPE_P(Z_REFVAL_P(container)) == IS_OBJECT)) { + zobj = Z_OBJ_P(Z_REFVAL_P(container)); + } if (1) { if (IS_UNUSED == IS_UNUSED) { @@ -91116,6 +92026,8 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_ASSIGN_OBJ_REF_SPE zend_assign_to_property_reference(container, IS_UNUSED, property, IS_CV, value_ptr OPLINE_CC EXECUTE_DATA_CC); } + ZEND_MAYBE_TRACK_OBJECT_MUTATION(zobj); + @@ -91128,6 +92040,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_ASSIGN_OBJ_REF_SPE { USE_OPLINE zval *property, *container, *value_ptr; + zend_object *zobj = NULL; SAVE_OPLINE(); @@ -91135,6 +92048,11 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_ASSIGN_OBJ_REF_SPE property = _get_zval_ptr_cv_BP_VAR_R(opline->op2.var EXECUTE_DATA_CC); value_ptr = _get_zval_ptr_cv_BP_VAR_W((opline+1)->op1.var EXECUTE_DATA_CC); + if (EXPECTED(Z_TYPE_P(container) == IS_OBJECT)) { + zobj = Z_OBJ_P(container); + } else if (Z_ISREF_P(container) && EXPECTED(Z_TYPE_P(Z_REFVAL_P(container)) == IS_OBJECT)) { + zobj = Z_OBJ_P(Z_REFVAL_P(container)); + } if (1) { if (IS_UNUSED == IS_UNUSED) { @@ -91154,6 +92072,8 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_ASSIGN_OBJ_REF_SPE zend_assign_to_property_reference(container, IS_UNUSED, property, IS_CV, value_ptr OPLINE_CC EXECUTE_DATA_CC); } + ZEND_MAYBE_TRACK_OBJECT_MUTATION(zobj); + @@ -91571,6 +92491,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_UNSET_OBJ_SPEC_UNU zval *container; zval *offset; zend_string *name, *tmp_name; + zend_object *zobj = NULL; SAVE_OPLINE(); container = &EX(This); @@ -91591,6 +92512,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_UNSET_OBJ_SPEC_UNU break; } } + zobj = Z_OBJ_P(container); if (IS_CV == IS_CONST) { name = Z_STR_P(offset); } else { @@ -91605,6 +92527,8 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_UNSET_OBJ_SPEC_UNU } } while (0); + ZEND_MAYBE_TRACK_OBJECT_MUTATION(zobj); + @@ -93344,6 +94268,10 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_BIND_STATIC_SPEC_C SAVE_OPLINE(); + if (UNEXPECTED(zend_function_init_statics_hook != NULL)) { + zend_function_init_statics_hook(execute_data); + } + ht = ZEND_MAP_PTR_GET(EX(func)->op_array.static_variables_ptr); if (!ht) { ht = zend_array_dup(EX(func)->op_array.static_variables); @@ -93395,6 +94323,10 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_BIND_INIT_STATIC_O variable_ptr = EX_VAR(opline->op1.var); + if (UNEXPECTED(zend_function_init_statics_hook != NULL)) { + zend_function_init_statics_hook(execute_data); + } + ht = ZEND_MAP_PTR_GET(EX(func)->op_array.static_variables_ptr); if (!ht) { ZEND_VM_NEXT_OPCODE(); @@ -94114,7 +95046,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_ASSIGN_OBJ_OP_SPEC void *_cache_slot[3] = {0}; void **cache_slot; zend_property_info *prop_info; - zend_object *zobj; + zend_object *zobj = NULL; zend_string *name, *tmp_name; SAVE_OPLINE(); @@ -94189,6 +95121,8 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_ASSIGN_OBJ_OP_SPEC } } while (0); + ZEND_MAYBE_TRACK_OBJECT_MUTATION(zobj); + FREE_OP((opline+1)->op1_type, (opline+1)->op1.var); @@ -94205,12 +95139,16 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_ASSIGN_DIM_OP_SPEC zval *var_ptr; zval *value, *container, *dim; HashTable *ht; + bool should_publish_tracked_array = false; SAVE_OPLINE(); container = EX_VAR(opline->op1.var); if (EXPECTED(Z_TYPE_P(container) == IS_ARRAY)) { assign_dim_op_array: + if (UNEXPECTED(EG(tracked_mutation_hooks_active))) { + should_publish_tracked_array = zend_maybe_track_hash_mutation(Z_ARRVAL_P(container), false); + } SEPARATE_ARRAY(container); ht = Z_ARRVAL_P(container); assign_dim_op_new_array: @@ -94250,6 +95188,9 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_ASSIGN_DIM_OP_SPEC ZVAL_COPY(EX_VAR(opline->result.var), var_ptr); } FREE_OP((opline+1)->op1_type, (opline+1)->op1.var); + if (UNEXPECTED(should_publish_tracked_array)) { + zend_maybe_track_hash_mutation(Z_ARRVAL_P(container), true); + } } else { if (EXPECTED(Z_ISREF_P(container))) { container = Z_REFVAL_P(container); @@ -94266,6 +95207,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_ASSIGN_DIM_OP_SPEC dim++; } zend_binary_assign_op_obj_dim(obj, dim OPLINE_CC EXECUTE_DATA_CC); + ZEND_MAYBE_TRACK_OBJECT_MUTATION(obj); } else if (EXPECTED(Z_TYPE_P(container) <= IS_FALSE)) { uint8_t old_type; @@ -94342,7 +95284,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_PRE_INC_OBJ_SPEC_C void *_cache_slot[3] = {0}; void **cache_slot; zend_property_info *prop_info; - zend_object *zobj; + zend_object *zobj = NULL; zend_string *name, *tmp_name; SAVE_OPLINE(); @@ -94394,6 +95336,8 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_PRE_INC_OBJ_SPEC_C } } while (0); + ZEND_MAYBE_TRACK_OBJECT_MUTATION(zobj); + @@ -94409,7 +95353,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_POST_INC_OBJ_SPEC_ void *_cache_slot[3] = {0}; void **cache_slot; zend_property_info *prop_info; - zend_object *zobj; + zend_object *zobj = NULL; zend_string *name, *tmp_name; SAVE_OPLINE(); @@ -94459,6 +95403,8 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_POST_INC_OBJ_SPEC_ } } while (0); + ZEND_MAYBE_TRACK_OBJECT_MUTATION(zobj); + @@ -94987,7 +95933,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_ASSIGN_OBJ_SPEC_CV { USE_OPLINE zval *object, *value, tmp; - zend_object *zobj; + zend_object *zobj = NULL; zend_string *name, *tmp_name; zend_refcounted *garbage = NULL; @@ -95129,6 +96075,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_ASSIGN_OBJ_SPEC_CV exit_assign_obj: + ZEND_MAYBE_TRACK_OBJECT_MUTATION_WITH_VALUE(zobj, value); if (garbage) { GC_DTOR_NO_REF(garbage); } @@ -95145,7 +96092,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_ASSIGN_OBJ_SPEC_CV { USE_OPLINE zval *object, *value, tmp; - zend_object *zobj; + zend_object *zobj = NULL; zend_string *name, *tmp_name; zend_refcounted *garbage = NULL; @@ -95285,6 +96232,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_ASSIGN_OBJ_SPEC_CV } zval_ptr_dtor_nogc(EX_VAR((opline+1)->op1.var)); exit_assign_obj: + ZEND_MAYBE_TRACK_OBJECT_MUTATION_WITH_VALUE(zobj, value); if (garbage) { GC_DTOR_NO_REF(garbage); } @@ -95301,7 +96249,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_ASSIGN_OBJ_SPEC_CV { USE_OPLINE zval *object, *value, tmp; - zend_object *zobj; + zend_object *zobj = NULL; zend_string *name, *tmp_name; zend_refcounted *garbage = NULL; @@ -95443,6 +96391,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_ASSIGN_OBJ_SPEC_CV exit_assign_obj: + ZEND_MAYBE_TRACK_OBJECT_MUTATION_WITH_VALUE(zobj, value); if (garbage) { GC_DTOR_NO_REF(garbage); } @@ -95463,12 +96412,16 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_ASSIGN_DIM_SPEC_CV zval *variable_ptr; zval *dim; zend_refcounted *garbage = NULL; + bool should_publish_tracked_array = false; SAVE_OPLINE(); orig_object_ptr = object_ptr = EX_VAR(opline->op1.var); if (EXPECTED(Z_TYPE_P(object_ptr) == IS_ARRAY)) { try_assign_dim_array: + if (UNEXPECTED(EG(tracked_mutation_hooks_active))) { + should_publish_tracked_array = zend_maybe_track_hash_mutation(Z_ARRVAL_P(object_ptr), false); + } SEPARATE_ARRAY(object_ptr); if (IS_CONST == IS_UNUSED) { value = RT_CONSTANT((opline+1), (opline+1)->op1); @@ -95526,6 +96479,9 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_ASSIGN_DIM_SPEC_CV if (garbage) { GC_DTOR_NO_REF(garbage); } + if (UNEXPECTED(should_publish_tracked_array)) { + zend_maybe_track_hash_mutation(Z_ARRVAL_P(object_ptr), true); + } } else { if (EXPECTED(Z_ISREF_P(object_ptr))) { object_ptr = Z_REFVAL_P(object_ptr); @@ -95552,6 +96508,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_ASSIGN_DIM_SPEC_CV } zend_assign_to_object_dim(obj, dim, value OPLINE_CC EXECUTE_DATA_CC); + ZEND_MAYBE_TRACK_OBJECT_MUTATION(obj); if (UNEXPECTED(GC_DELREF(obj) == 0)) { @@ -95622,12 +96579,16 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_ASSIGN_DIM_SPEC_CV zval *variable_ptr; zval *dim; zend_refcounted *garbage = NULL; + bool should_publish_tracked_array = false; SAVE_OPLINE(); orig_object_ptr = object_ptr = EX_VAR(opline->op1.var); if (EXPECTED(Z_TYPE_P(object_ptr) == IS_ARRAY)) { try_assign_dim_array: + if (UNEXPECTED(EG(tracked_mutation_hooks_active))) { + should_publish_tracked_array = zend_maybe_track_hash_mutation(Z_ARRVAL_P(object_ptr), false); + } SEPARATE_ARRAY(object_ptr); if (IS_CONST == IS_UNUSED) { value = _get_zval_ptr_tmp((opline+1)->op1.var EXECUTE_DATA_CC); @@ -95685,6 +96646,9 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_ASSIGN_DIM_SPEC_CV if (garbage) { GC_DTOR_NO_REF(garbage); } + if (UNEXPECTED(should_publish_tracked_array)) { + zend_maybe_track_hash_mutation(Z_ARRVAL_P(object_ptr), true); + } } else { if (EXPECTED(Z_ISREF_P(object_ptr))) { object_ptr = Z_REFVAL_P(object_ptr); @@ -95711,6 +96675,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_ASSIGN_DIM_SPEC_CV } zend_assign_to_object_dim(obj, dim, value OPLINE_CC EXECUTE_DATA_CC); + ZEND_MAYBE_TRACK_OBJECT_MUTATION(obj); zval_ptr_dtor_nogc(EX_VAR((opline+1)->op1.var)); if (UNEXPECTED(GC_DELREF(obj) == 0)) { @@ -95777,12 +96742,16 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_ASSIGN_DIM_SPEC_CV zval *variable_ptr; zval *dim; zend_refcounted *garbage = NULL; + bool should_publish_tracked_array = false; SAVE_OPLINE(); orig_object_ptr = object_ptr = EX_VAR(opline->op1.var); if (EXPECTED(Z_TYPE_P(object_ptr) == IS_ARRAY)) { try_assign_dim_array: + if (UNEXPECTED(EG(tracked_mutation_hooks_active))) { + should_publish_tracked_array = zend_maybe_track_hash_mutation(Z_ARRVAL_P(object_ptr), false); + } SEPARATE_ARRAY(object_ptr); if (IS_CONST == IS_UNUSED) { value = EX_VAR((opline+1)->op1.var); @@ -95840,6 +96809,9 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_ASSIGN_DIM_SPEC_CV if (garbage) { GC_DTOR_NO_REF(garbage); } + if (UNEXPECTED(should_publish_tracked_array)) { + zend_maybe_track_hash_mutation(Z_ARRVAL_P(object_ptr), true); + } } else { if (EXPECTED(Z_ISREF_P(object_ptr))) { object_ptr = Z_REFVAL_P(object_ptr); @@ -95866,6 +96838,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_ASSIGN_DIM_SPEC_CV } zend_assign_to_object_dim(obj, dim, value OPLINE_CC EXECUTE_DATA_CC); + ZEND_MAYBE_TRACK_OBJECT_MUTATION(obj); if (UNEXPECTED(GC_DELREF(obj) == 0)) { @@ -95992,6 +96965,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_ASSIGN_OBJ_REF_SPE { USE_OPLINE zval *property, *container, *value_ptr; + zend_object *zobj = NULL; SAVE_OPLINE(); @@ -95999,6 +96973,11 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_ASSIGN_OBJ_REF_SPE property = RT_CONSTANT(opline, opline->op2); value_ptr = _get_zval_ptr_ptr_var((opline+1)->op1.var EXECUTE_DATA_CC); + if (EXPECTED(Z_TYPE_P(container) == IS_OBJECT)) { + zobj = Z_OBJ_P(container); + } else if (Z_ISREF_P(container) && EXPECTED(Z_TYPE_P(Z_REFVAL_P(container)) == IS_OBJECT)) { + zobj = Z_OBJ_P(Z_REFVAL_P(container)); + } if (1) { if (IS_CV == IS_UNUSED) { @@ -96018,6 +96997,8 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_ASSIGN_OBJ_REF_SPE zend_assign_to_property_reference(container, IS_CV, property, IS_CONST, value_ptr OPLINE_CC EXECUTE_DATA_CC); } + ZEND_MAYBE_TRACK_OBJECT_MUTATION(zobj); + @@ -96030,6 +97011,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_ASSIGN_OBJ_REF_SPE { USE_OPLINE zval *property, *container, *value_ptr; + zend_object *zobj = NULL; SAVE_OPLINE(); @@ -96037,6 +97019,11 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_ASSIGN_OBJ_REF_SPE property = RT_CONSTANT(opline, opline->op2); value_ptr = _get_zval_ptr_cv_BP_VAR_W((opline+1)->op1.var EXECUTE_DATA_CC); + if (EXPECTED(Z_TYPE_P(container) == IS_OBJECT)) { + zobj = Z_OBJ_P(container); + } else if (Z_ISREF_P(container) && EXPECTED(Z_TYPE_P(Z_REFVAL_P(container)) == IS_OBJECT)) { + zobj = Z_OBJ_P(Z_REFVAL_P(container)); + } if (1) { if (IS_CV == IS_UNUSED) { @@ -96056,6 +97043,8 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_ASSIGN_OBJ_REF_SPE zend_assign_to_property_reference(container, IS_CV, property, IS_CONST, value_ptr OPLINE_CC EXECUTE_DATA_CC); } + ZEND_MAYBE_TRACK_OBJECT_MUTATION(zobj); + @@ -96642,6 +97631,8 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_UNSET_DIM_SPEC_CV_ zval *offset; zend_ulong hval; zend_string *key; + bool should_flush_array = false; + bool should_publish_tracked_array = false; SAVE_OPLINE(); container = EX_VAR(opline->op1.var); @@ -96652,6 +97643,9 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_UNSET_DIM_SPEC_CV_ HashTable *ht; unset_dim_array: + if (UNEXPECTED(EG(tracked_mutation_hooks_active))) { + should_publish_tracked_array = zend_maybe_track_hash_mutation(Z_ARRVAL_P(container), false); + } SEPARATE_ARRAY(container); ht = Z_ARRVAL_P(container); offset_again: @@ -96665,10 +97659,12 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_UNSET_DIM_SPEC_CV_ str_index_dim: ZEND_ASSERT(ht != &EG(symbol_table)); zend_hash_del(ht, key); + should_flush_array = true; } else if (EXPECTED(Z_TYPE_P(offset) == IS_LONG)) { hval = Z_LVAL_P(offset); num_index_dim: zend_hash_index_del(ht, hval); + should_flush_array = true; } else if ((IS_CONST & (IS_VAR|IS_CV)) && EXPECTED(Z_TYPE_P(offset) == IS_REFERENCE)) { offset = Z_REFVAL_P(offset); goto offset_again; @@ -96705,6 +97701,9 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_UNSET_DIM_SPEC_CV_ } else { zend_illegal_array_offset_unset(offset); } + if (UNEXPECTED(should_flush_array && should_publish_tracked_array)) { + zend_maybe_track_hash_mutation(ht, true); + } break; } else if (Z_ISREF_P(container)) { container = Z_REFVAL_P(container); @@ -96723,6 +97722,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_UNSET_DIM_SPEC_CV_ offset++; } Z_OBJ_HT_P(container)->unset_dimension(Z_OBJ_P(container), offset); + ZEND_MAYBE_TRACK_OBJECT_MUTATION(Z_OBJ_P(container)); } else if (UNEXPECTED(Z_TYPE_P(container) == IS_STRING)) { zend_throw_error(NULL, "Cannot unset string offsets"); } else if (UNEXPECTED(Z_TYPE_P(container) > IS_FALSE)) { @@ -96744,6 +97744,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_UNSET_OBJ_SPEC_CV_ zval *container; zval *offset; zend_string *name, *tmp_name; + zend_object *zobj = NULL; SAVE_OPLINE(); container = EX_VAR(opline->op1.var); @@ -96764,6 +97765,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_UNSET_OBJ_SPEC_CV_ break; } } + zobj = Z_OBJ_P(container); if (IS_CONST == IS_CONST) { name = Z_STR_P(offset); } else { @@ -96778,6 +97780,8 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_UNSET_OBJ_SPEC_CV_ } } while (0); + ZEND_MAYBE_TRACK_OBJECT_MUTATION(zobj); + @@ -97953,7 +98957,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_ASSIGN_OBJ_OP_SPEC void *_cache_slot[3] = {0}; void **cache_slot; zend_property_info *prop_info; - zend_object *zobj; + zend_object *zobj = NULL; zend_string *name, *tmp_name; SAVE_OPLINE(); @@ -98028,6 +99032,8 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_ASSIGN_OBJ_OP_SPEC } } while (0); + ZEND_MAYBE_TRACK_OBJECT_MUTATION(zobj); + FREE_OP((opline+1)->op1_type, (opline+1)->op1.var); zval_ptr_dtor_nogc(EX_VAR(opline->op2.var)); @@ -98043,12 +99049,16 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_ASSIGN_DIM_OP_SPEC zval *var_ptr; zval *value, *container, *dim; HashTable *ht; + bool should_publish_tracked_array = false; SAVE_OPLINE(); container = EX_VAR(opline->op1.var); if (EXPECTED(Z_TYPE_P(container) == IS_ARRAY)) { assign_dim_op_array: + if (UNEXPECTED(EG(tracked_mutation_hooks_active))) { + should_publish_tracked_array = zend_maybe_track_hash_mutation(Z_ARRVAL_P(container), false); + } SEPARATE_ARRAY(container); ht = Z_ARRVAL_P(container); assign_dim_op_new_array: @@ -98088,6 +99098,9 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_ASSIGN_DIM_OP_SPEC ZVAL_COPY(EX_VAR(opline->result.var), var_ptr); } FREE_OP((opline+1)->op1_type, (opline+1)->op1.var); + if (UNEXPECTED(should_publish_tracked_array)) { + zend_maybe_track_hash_mutation(Z_ARRVAL_P(container), true); + } } else { if (EXPECTED(Z_ISREF_P(container))) { container = Z_REFVAL_P(container); @@ -98104,6 +99117,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_ASSIGN_DIM_OP_SPEC dim++; } zend_binary_assign_op_obj_dim(obj, dim OPLINE_CC EXECUTE_DATA_CC); + ZEND_MAYBE_TRACK_OBJECT_MUTATION(obj); } else if (EXPECTED(Z_TYPE_P(container) <= IS_FALSE)) { uint8_t old_type; @@ -98180,7 +99194,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_PRE_INC_OBJ_SPEC_C void *_cache_slot[3] = {0}; void **cache_slot; zend_property_info *prop_info; - zend_object *zobj; + zend_object *zobj = NULL; zend_string *name, *tmp_name; SAVE_OPLINE(); @@ -98232,6 +99246,8 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_PRE_INC_OBJ_SPEC_C } } while (0); + ZEND_MAYBE_TRACK_OBJECT_MUTATION(zobj); + zval_ptr_dtor_nogc(EX_VAR(opline->op2.var)); @@ -98247,7 +99263,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_POST_INC_OBJ_SPEC_ void *_cache_slot[3] = {0}; void **cache_slot; zend_property_info *prop_info; - zend_object *zobj; + zend_object *zobj = NULL; zend_string *name, *tmp_name; SAVE_OPLINE(); @@ -98297,6 +99313,8 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_POST_INC_OBJ_SPEC_ } } while (0); + ZEND_MAYBE_TRACK_OBJECT_MUTATION(zobj); + zval_ptr_dtor_nogc(EX_VAR(opline->op2.var)); @@ -98810,7 +99828,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_ASSIGN_OBJ_SPEC_CV { USE_OPLINE zval *object, *value, tmp; - zend_object *zobj; + zend_object *zobj = NULL; zend_string *name, *tmp_name; zend_refcounted *garbage = NULL; @@ -98952,6 +99970,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_ASSIGN_OBJ_SPEC_CV exit_assign_obj: + ZEND_MAYBE_TRACK_OBJECT_MUTATION_WITH_VALUE(zobj, value); if (garbage) { GC_DTOR_NO_REF(garbage); } @@ -98967,7 +99986,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_ASSIGN_OBJ_SPEC_CV { USE_OPLINE zval *object, *value, tmp; - zend_object *zobj; + zend_object *zobj = NULL; zend_string *name, *tmp_name; zend_refcounted *garbage = NULL; @@ -99107,6 +100126,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_ASSIGN_OBJ_SPEC_CV } zval_ptr_dtor_nogc(EX_VAR((opline+1)->op1.var)); exit_assign_obj: + ZEND_MAYBE_TRACK_OBJECT_MUTATION_WITH_VALUE(zobj, value); if (garbage) { GC_DTOR_NO_REF(garbage); } @@ -99122,7 +100142,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_ASSIGN_OBJ_SPEC_CV { USE_OPLINE zval *object, *value, tmp; - zend_object *zobj; + zend_object *zobj = NULL; zend_string *name, *tmp_name; zend_refcounted *garbage = NULL; @@ -99264,6 +100284,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_ASSIGN_OBJ_SPEC_CV exit_assign_obj: + ZEND_MAYBE_TRACK_OBJECT_MUTATION_WITH_VALUE(zobj, value); if (garbage) { GC_DTOR_NO_REF(garbage); } @@ -99283,12 +100304,16 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_ASSIGN_DIM_SPEC_CV zval *variable_ptr; zval *dim; zend_refcounted *garbage = NULL; + bool should_publish_tracked_array = false; SAVE_OPLINE(); orig_object_ptr = object_ptr = EX_VAR(opline->op1.var); if (EXPECTED(Z_TYPE_P(object_ptr) == IS_ARRAY)) { try_assign_dim_array: + if (UNEXPECTED(EG(tracked_mutation_hooks_active))) { + should_publish_tracked_array = zend_maybe_track_hash_mutation(Z_ARRVAL_P(object_ptr), false); + } SEPARATE_ARRAY(object_ptr); if (IS_TMP_VAR == IS_UNUSED) { value = RT_CONSTANT((opline+1), (opline+1)->op1); @@ -99346,6 +100371,9 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_ASSIGN_DIM_SPEC_CV if (garbage) { GC_DTOR_NO_REF(garbage); } + if (UNEXPECTED(should_publish_tracked_array)) { + zend_maybe_track_hash_mutation(Z_ARRVAL_P(object_ptr), true); + } } else { if (EXPECTED(Z_ISREF_P(object_ptr))) { object_ptr = Z_REFVAL_P(object_ptr); @@ -99372,6 +100400,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_ASSIGN_DIM_SPEC_CV } zend_assign_to_object_dim(obj, dim, value OPLINE_CC EXECUTE_DATA_CC); + ZEND_MAYBE_TRACK_OBJECT_MUTATION(obj); if (UNEXPECTED(GC_DELREF(obj) == 0)) { @@ -99441,12 +100470,16 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_ASSIGN_DIM_SPEC_CV zval *variable_ptr; zval *dim; zend_refcounted *garbage = NULL; + bool should_publish_tracked_array = false; SAVE_OPLINE(); orig_object_ptr = object_ptr = EX_VAR(opline->op1.var); if (EXPECTED(Z_TYPE_P(object_ptr) == IS_ARRAY)) { try_assign_dim_array: + if (UNEXPECTED(EG(tracked_mutation_hooks_active))) { + should_publish_tracked_array = zend_maybe_track_hash_mutation(Z_ARRVAL_P(object_ptr), false); + } SEPARATE_ARRAY(object_ptr); if (IS_TMP_VAR == IS_UNUSED) { value = _get_zval_ptr_tmp((opline+1)->op1.var EXECUTE_DATA_CC); @@ -99504,6 +100537,9 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_ASSIGN_DIM_SPEC_CV if (garbage) { GC_DTOR_NO_REF(garbage); } + if (UNEXPECTED(should_publish_tracked_array)) { + zend_maybe_track_hash_mutation(Z_ARRVAL_P(object_ptr), true); + } } else { if (EXPECTED(Z_ISREF_P(object_ptr))) { object_ptr = Z_REFVAL_P(object_ptr); @@ -99530,6 +100566,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_ASSIGN_DIM_SPEC_CV } zend_assign_to_object_dim(obj, dim, value OPLINE_CC EXECUTE_DATA_CC); + ZEND_MAYBE_TRACK_OBJECT_MUTATION(obj); zval_ptr_dtor_nogc(EX_VAR((opline+1)->op1.var)); if (UNEXPECTED(GC_DELREF(obj) == 0)) { @@ -99595,12 +100632,16 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_ASSIGN_DIM_SPEC_CV zval *variable_ptr; zval *dim; zend_refcounted *garbage = NULL; + bool should_publish_tracked_array = false; SAVE_OPLINE(); orig_object_ptr = object_ptr = EX_VAR(opline->op1.var); if (EXPECTED(Z_TYPE_P(object_ptr) == IS_ARRAY)) { try_assign_dim_array: + if (UNEXPECTED(EG(tracked_mutation_hooks_active))) { + should_publish_tracked_array = zend_maybe_track_hash_mutation(Z_ARRVAL_P(object_ptr), false); + } SEPARATE_ARRAY(object_ptr); if (IS_TMP_VAR == IS_UNUSED) { value = EX_VAR((opline+1)->op1.var); @@ -99658,6 +100699,9 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_ASSIGN_DIM_SPEC_CV if (garbage) { GC_DTOR_NO_REF(garbage); } + if (UNEXPECTED(should_publish_tracked_array)) { + zend_maybe_track_hash_mutation(Z_ARRVAL_P(object_ptr), true); + } } else { if (EXPECTED(Z_ISREF_P(object_ptr))) { object_ptr = Z_REFVAL_P(object_ptr); @@ -99684,6 +100728,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_ASSIGN_DIM_SPEC_CV } zend_assign_to_object_dim(obj, dim, value OPLINE_CC EXECUTE_DATA_CC); + ZEND_MAYBE_TRACK_OBJECT_MUTATION(obj); if (UNEXPECTED(GC_DELREF(obj) == 0)) { @@ -99809,6 +100854,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_ASSIGN_OBJ_REF_SPE { USE_OPLINE zval *property, *container, *value_ptr; + zend_object *zobj = NULL; SAVE_OPLINE(); @@ -99816,6 +100862,11 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_ASSIGN_OBJ_REF_SPE property = _get_zval_ptr_tmp(opline->op2.var EXECUTE_DATA_CC); value_ptr = _get_zval_ptr_ptr_var((opline+1)->op1.var EXECUTE_DATA_CC); + if (EXPECTED(Z_TYPE_P(container) == IS_OBJECT)) { + zobj = Z_OBJ_P(container); + } else if (Z_ISREF_P(container) && EXPECTED(Z_TYPE_P(Z_REFVAL_P(container)) == IS_OBJECT)) { + zobj = Z_OBJ_P(Z_REFVAL_P(container)); + } if (1) { if (IS_CV == IS_UNUSED) { @@ -99835,6 +100886,8 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_ASSIGN_OBJ_REF_SPE zend_assign_to_property_reference(container, IS_CV, property, IS_TMP_VAR, value_ptr OPLINE_CC EXECUTE_DATA_CC); } + ZEND_MAYBE_TRACK_OBJECT_MUTATION(zobj); + zval_ptr_dtor_nogc(EX_VAR(opline->op2.var)); zval_ptr_dtor_nogc(EX_VAR((opline+1)->op1.var)); @@ -99846,6 +100899,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_ASSIGN_OBJ_REF_SPE { USE_OPLINE zval *property, *container, *value_ptr; + zend_object *zobj = NULL; SAVE_OPLINE(); @@ -99853,6 +100907,11 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_ASSIGN_OBJ_REF_SPE property = _get_zval_ptr_tmp(opline->op2.var EXECUTE_DATA_CC); value_ptr = _get_zval_ptr_cv_BP_VAR_W((opline+1)->op1.var EXECUTE_DATA_CC); + if (EXPECTED(Z_TYPE_P(container) == IS_OBJECT)) { + zobj = Z_OBJ_P(container); + } else if (Z_ISREF_P(container) && EXPECTED(Z_TYPE_P(Z_REFVAL_P(container)) == IS_OBJECT)) { + zobj = Z_OBJ_P(Z_REFVAL_P(container)); + } if (1) { if (IS_CV == IS_UNUSED) { @@ -99872,6 +100931,8 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_ASSIGN_OBJ_REF_SPE zend_assign_to_property_reference(container, IS_CV, property, IS_TMP_VAR, value_ptr OPLINE_CC EXECUTE_DATA_CC); } + ZEND_MAYBE_TRACK_OBJECT_MUTATION(zobj); + zval_ptr_dtor_nogc(EX_VAR(opline->op2.var)); @@ -100304,6 +101365,8 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_UNSET_DIM_SPEC_CV_ zval *offset; zend_ulong hval; zend_string *key; + bool should_flush_array = false; + bool should_publish_tracked_array = false; SAVE_OPLINE(); container = EX_VAR(opline->op1.var); @@ -100314,6 +101377,9 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_UNSET_DIM_SPEC_CV_ HashTable *ht; unset_dim_array: + if (UNEXPECTED(EG(tracked_mutation_hooks_active))) { + should_publish_tracked_array = zend_maybe_track_hash_mutation(Z_ARRVAL_P(container), false); + } SEPARATE_ARRAY(container); ht = Z_ARRVAL_P(container); offset_again: @@ -100327,10 +101393,12 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_UNSET_DIM_SPEC_CV_ str_index_dim: ZEND_ASSERT(ht != &EG(symbol_table)); zend_hash_del(ht, key); + should_flush_array = true; } else if (EXPECTED(Z_TYPE_P(offset) == IS_LONG)) { hval = Z_LVAL_P(offset); num_index_dim: zend_hash_index_del(ht, hval); + should_flush_array = true; } else if ((IS_TMP_VAR & (IS_VAR|IS_CV)) && EXPECTED(Z_TYPE_P(offset) == IS_REFERENCE)) { offset = Z_REFVAL_P(offset); goto offset_again; @@ -100367,6 +101435,9 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_UNSET_DIM_SPEC_CV_ } else { zend_illegal_array_offset_unset(offset); } + if (UNEXPECTED(should_flush_array && should_publish_tracked_array)) { + zend_maybe_track_hash_mutation(ht, true); + } break; } else if (Z_ISREF_P(container)) { container = Z_REFVAL_P(container); @@ -100385,6 +101456,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_UNSET_DIM_SPEC_CV_ offset++; } Z_OBJ_HT_P(container)->unset_dimension(Z_OBJ_P(container), offset); + ZEND_MAYBE_TRACK_OBJECT_MUTATION(Z_OBJ_P(container)); } else if (UNEXPECTED(Z_TYPE_P(container) == IS_STRING)) { zend_throw_error(NULL, "Cannot unset string offsets"); } else if (UNEXPECTED(Z_TYPE_P(container) > IS_FALSE)) { @@ -100406,6 +101478,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_UNSET_OBJ_SPEC_CV_ zval *container; zval *offset; zend_string *name, *tmp_name; + zend_object *zobj = NULL; SAVE_OPLINE(); container = EX_VAR(opline->op1.var); @@ -100426,6 +101499,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_UNSET_OBJ_SPEC_CV_ break; } } + zobj = Z_OBJ_P(container); if (IS_TMP_VAR == IS_CONST) { name = Z_STR_P(offset); } else { @@ -100440,6 +101514,8 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_UNSET_OBJ_SPEC_CV_ } } while (0); + ZEND_MAYBE_TRACK_OBJECT_MUTATION(zobj); + zval_ptr_dtor_nogc(EX_VAR(opline->op2.var)); @@ -100826,12 +101902,16 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_ASSIGN_DIM_OP_SPEC zval *var_ptr; zval *value, *container, *dim; HashTable *ht; + bool should_publish_tracked_array = false; SAVE_OPLINE(); container = EX_VAR(opline->op1.var); if (EXPECTED(Z_TYPE_P(container) == IS_ARRAY)) { assign_dim_op_array: + if (UNEXPECTED(EG(tracked_mutation_hooks_active))) { + should_publish_tracked_array = zend_maybe_track_hash_mutation(Z_ARRVAL_P(container), false); + } SEPARATE_ARRAY(container); ht = Z_ARRVAL_P(container); assign_dim_op_new_array: @@ -100871,6 +101951,9 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_ASSIGN_DIM_OP_SPEC ZVAL_COPY(EX_VAR(opline->result.var), var_ptr); } FREE_OP((opline+1)->op1_type, (opline+1)->op1.var); + if (UNEXPECTED(should_publish_tracked_array)) { + zend_maybe_track_hash_mutation(Z_ARRVAL_P(container), true); + } } else { if (EXPECTED(Z_ISREF_P(container))) { container = Z_REFVAL_P(container); @@ -100887,6 +101970,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_ASSIGN_DIM_OP_SPEC dim++; } zend_binary_assign_op_obj_dim(obj, dim OPLINE_CC EXECUTE_DATA_CC); + ZEND_MAYBE_TRACK_OBJECT_MUTATION(obj); } else if (EXPECTED(Z_TYPE_P(container) <= IS_FALSE)) { uint8_t old_type; @@ -101022,12 +102106,16 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_ASSIGN_DIM_SPEC_CV zval *variable_ptr; zval *dim; zend_refcounted *garbage = NULL; + bool should_publish_tracked_array = false; SAVE_OPLINE(); orig_object_ptr = object_ptr = EX_VAR(opline->op1.var); if (EXPECTED(Z_TYPE_P(object_ptr) == IS_ARRAY)) { try_assign_dim_array: + if (UNEXPECTED(EG(tracked_mutation_hooks_active))) { + should_publish_tracked_array = zend_maybe_track_hash_mutation(Z_ARRVAL_P(object_ptr), false); + } SEPARATE_ARRAY(object_ptr); if (IS_UNUSED == IS_UNUSED) { value = RT_CONSTANT((opline+1), (opline+1)->op1); @@ -101085,6 +102173,9 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_ASSIGN_DIM_SPEC_CV if (garbage) { GC_DTOR_NO_REF(garbage); } + if (UNEXPECTED(should_publish_tracked_array)) { + zend_maybe_track_hash_mutation(Z_ARRVAL_P(object_ptr), true); + } } else { if (EXPECTED(Z_ISREF_P(object_ptr))) { object_ptr = Z_REFVAL_P(object_ptr); @@ -101111,6 +102202,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_ASSIGN_DIM_SPEC_CV } zend_assign_to_object_dim(obj, dim, value OPLINE_CC EXECUTE_DATA_CC); + ZEND_MAYBE_TRACK_OBJECT_MUTATION(obj); if (UNEXPECTED(GC_DELREF(obj) == 0)) { @@ -101181,12 +102273,16 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_ASSIGN_DIM_SPEC_CV zval *variable_ptr; zval *dim; zend_refcounted *garbage = NULL; + bool should_publish_tracked_array = false; SAVE_OPLINE(); orig_object_ptr = object_ptr = EX_VAR(opline->op1.var); if (EXPECTED(Z_TYPE_P(object_ptr) == IS_ARRAY)) { try_assign_dim_array: + if (UNEXPECTED(EG(tracked_mutation_hooks_active))) { + should_publish_tracked_array = zend_maybe_track_hash_mutation(Z_ARRVAL_P(object_ptr), false); + } SEPARATE_ARRAY(object_ptr); if (IS_UNUSED == IS_UNUSED) { value = _get_zval_ptr_tmp((opline+1)->op1.var EXECUTE_DATA_CC); @@ -101244,6 +102340,9 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_ASSIGN_DIM_SPEC_CV if (garbage) { GC_DTOR_NO_REF(garbage); } + if (UNEXPECTED(should_publish_tracked_array)) { + zend_maybe_track_hash_mutation(Z_ARRVAL_P(object_ptr), true); + } } else { if (EXPECTED(Z_ISREF_P(object_ptr))) { object_ptr = Z_REFVAL_P(object_ptr); @@ -101270,6 +102369,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_ASSIGN_DIM_SPEC_CV } zend_assign_to_object_dim(obj, dim, value OPLINE_CC EXECUTE_DATA_CC); + ZEND_MAYBE_TRACK_OBJECT_MUTATION(obj); zval_ptr_dtor_nogc(EX_VAR((opline+1)->op1.var)); if (UNEXPECTED(GC_DELREF(obj) == 0)) { @@ -101336,12 +102436,16 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_ASSIGN_DIM_SPEC_CV zval *variable_ptr; zval *dim; zend_refcounted *garbage = NULL; + bool should_publish_tracked_array = false; SAVE_OPLINE(); orig_object_ptr = object_ptr = EX_VAR(opline->op1.var); if (EXPECTED(Z_TYPE_P(object_ptr) == IS_ARRAY)) { try_assign_dim_array: + if (UNEXPECTED(EG(tracked_mutation_hooks_active))) { + should_publish_tracked_array = zend_maybe_track_hash_mutation(Z_ARRVAL_P(object_ptr), false); + } SEPARATE_ARRAY(object_ptr); if (IS_UNUSED == IS_UNUSED) { value = EX_VAR((opline+1)->op1.var); @@ -101399,6 +102503,9 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_ASSIGN_DIM_SPEC_CV if (garbage) { GC_DTOR_NO_REF(garbage); } + if (UNEXPECTED(should_publish_tracked_array)) { + zend_maybe_track_hash_mutation(Z_ARRVAL_P(object_ptr), true); + } } else { if (EXPECTED(Z_ISREF_P(object_ptr))) { object_ptr = Z_REFVAL_P(object_ptr); @@ -101425,6 +102532,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_ASSIGN_DIM_SPEC_CV } zend_assign_to_object_dim(obj, dim, value OPLINE_CC EXECUTE_DATA_CC); + ZEND_MAYBE_TRACK_OBJECT_MUTATION(obj); if (UNEXPECTED(GC_DELREF(obj) == 0)) { @@ -102956,7 +104064,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_ASSIGN_OBJ_OP_SPEC void *_cache_slot[3] = {0}; void **cache_slot; zend_property_info *prop_info; - zend_object *zobj; + zend_object *zobj = NULL; zend_string *name, *tmp_name; SAVE_OPLINE(); @@ -103031,6 +104139,8 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_ASSIGN_OBJ_OP_SPEC } } while (0); + ZEND_MAYBE_TRACK_OBJECT_MUTATION(zobj); + FREE_OP((opline+1)->op1_type, (opline+1)->op1.var); @@ -103047,12 +104157,16 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_ASSIGN_DIM_OP_SPEC zval *var_ptr; zval *value, *container, *dim; HashTable *ht; + bool should_publish_tracked_array = false; SAVE_OPLINE(); container = EX_VAR(opline->op1.var); if (EXPECTED(Z_TYPE_P(container) == IS_ARRAY)) { assign_dim_op_array: + if (UNEXPECTED(EG(tracked_mutation_hooks_active))) { + should_publish_tracked_array = zend_maybe_track_hash_mutation(Z_ARRVAL_P(container), false); + } SEPARATE_ARRAY(container); ht = Z_ARRVAL_P(container); assign_dim_op_new_array: @@ -103092,6 +104206,9 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_ASSIGN_DIM_OP_SPEC ZVAL_COPY(EX_VAR(opline->result.var), var_ptr); } FREE_OP((opline+1)->op1_type, (opline+1)->op1.var); + if (UNEXPECTED(should_publish_tracked_array)) { + zend_maybe_track_hash_mutation(Z_ARRVAL_P(container), true); + } } else { if (EXPECTED(Z_ISREF_P(container))) { container = Z_REFVAL_P(container); @@ -103108,6 +104225,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_ASSIGN_DIM_OP_SPEC dim++; } zend_binary_assign_op_obj_dim(obj, dim OPLINE_CC EXECUTE_DATA_CC); + ZEND_MAYBE_TRACK_OBJECT_MUTATION(obj); } else if (EXPECTED(Z_TYPE_P(container) <= IS_FALSE)) { uint8_t old_type; @@ -103184,7 +104302,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_PRE_INC_OBJ_SPEC_C void *_cache_slot[3] = {0}; void **cache_slot; zend_property_info *prop_info; - zend_object *zobj; + zend_object *zobj = NULL; zend_string *name, *tmp_name; SAVE_OPLINE(); @@ -103236,6 +104354,8 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_PRE_INC_OBJ_SPEC_C } } while (0); + ZEND_MAYBE_TRACK_OBJECT_MUTATION(zobj); + @@ -103251,7 +104371,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_POST_INC_OBJ_SPEC_ void *_cache_slot[3] = {0}; void **cache_slot; zend_property_info *prop_info; - zend_object *zobj; + zend_object *zobj = NULL; zend_string *name, *tmp_name; SAVE_OPLINE(); @@ -103301,6 +104421,8 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_POST_INC_OBJ_SPEC_ } } while (0); + ZEND_MAYBE_TRACK_OBJECT_MUTATION(zobj); + @@ -103824,7 +104946,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_ASSIGN_OBJ_SPEC_CV { USE_OPLINE zval *object, *value, tmp; - zend_object *zobj; + zend_object *zobj = NULL; zend_string *name, *tmp_name; zend_refcounted *garbage = NULL; @@ -103966,6 +105088,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_ASSIGN_OBJ_SPEC_CV exit_assign_obj: + ZEND_MAYBE_TRACK_OBJECT_MUTATION_WITH_VALUE(zobj, value); if (garbage) { GC_DTOR_NO_REF(garbage); } @@ -103982,7 +105105,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_ASSIGN_OBJ_SPEC_CV { USE_OPLINE zval *object, *value, tmp; - zend_object *zobj; + zend_object *zobj = NULL; zend_string *name, *tmp_name; zend_refcounted *garbage = NULL; @@ -104122,6 +105245,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_ASSIGN_OBJ_SPEC_CV } zval_ptr_dtor_nogc(EX_VAR((opline+1)->op1.var)); exit_assign_obj: + ZEND_MAYBE_TRACK_OBJECT_MUTATION_WITH_VALUE(zobj, value); if (garbage) { GC_DTOR_NO_REF(garbage); } @@ -104138,7 +105262,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_ASSIGN_OBJ_SPEC_CV { USE_OPLINE zval *object, *value, tmp; - zend_object *zobj; + zend_object *zobj = NULL; zend_string *name, *tmp_name; zend_refcounted *garbage = NULL; @@ -104280,6 +105404,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_ASSIGN_OBJ_SPEC_CV exit_assign_obj: + ZEND_MAYBE_TRACK_OBJECT_MUTATION_WITH_VALUE(zobj, value); if (garbage) { GC_DTOR_NO_REF(garbage); } @@ -104300,12 +105425,16 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_ASSIGN_DIM_SPEC_CV zval *variable_ptr; zval *dim; zend_refcounted *garbage = NULL; + bool should_publish_tracked_array = false; SAVE_OPLINE(); orig_object_ptr = object_ptr = EX_VAR(opline->op1.var); if (EXPECTED(Z_TYPE_P(object_ptr) == IS_ARRAY)) { try_assign_dim_array: + if (UNEXPECTED(EG(tracked_mutation_hooks_active))) { + should_publish_tracked_array = zend_maybe_track_hash_mutation(Z_ARRVAL_P(object_ptr), false); + } SEPARATE_ARRAY(object_ptr); if (IS_CV == IS_UNUSED) { value = RT_CONSTANT((opline+1), (opline+1)->op1); @@ -104363,6 +105492,9 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_ASSIGN_DIM_SPEC_CV if (garbage) { GC_DTOR_NO_REF(garbage); } + if (UNEXPECTED(should_publish_tracked_array)) { + zend_maybe_track_hash_mutation(Z_ARRVAL_P(object_ptr), true); + } } else { if (EXPECTED(Z_ISREF_P(object_ptr))) { object_ptr = Z_REFVAL_P(object_ptr); @@ -104389,6 +105521,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_ASSIGN_DIM_SPEC_CV } zend_assign_to_object_dim(obj, dim, value OPLINE_CC EXECUTE_DATA_CC); + ZEND_MAYBE_TRACK_OBJECT_MUTATION(obj); if (UNEXPECTED(GC_DELREF(obj) == 0)) { @@ -104459,12 +105592,16 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_ASSIGN_DIM_SPEC_CV zval *variable_ptr; zval *dim; zend_refcounted *garbage = NULL; + bool should_publish_tracked_array = false; SAVE_OPLINE(); orig_object_ptr = object_ptr = EX_VAR(opline->op1.var); if (EXPECTED(Z_TYPE_P(object_ptr) == IS_ARRAY)) { try_assign_dim_array: + if (UNEXPECTED(EG(tracked_mutation_hooks_active))) { + should_publish_tracked_array = zend_maybe_track_hash_mutation(Z_ARRVAL_P(object_ptr), false); + } SEPARATE_ARRAY(object_ptr); if (IS_CV == IS_UNUSED) { value = _get_zval_ptr_tmp((opline+1)->op1.var EXECUTE_DATA_CC); @@ -104522,6 +105659,9 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_ASSIGN_DIM_SPEC_CV if (garbage) { GC_DTOR_NO_REF(garbage); } + if (UNEXPECTED(should_publish_tracked_array)) { + zend_maybe_track_hash_mutation(Z_ARRVAL_P(object_ptr), true); + } } else { if (EXPECTED(Z_ISREF_P(object_ptr))) { object_ptr = Z_REFVAL_P(object_ptr); @@ -104548,6 +105688,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_ASSIGN_DIM_SPEC_CV } zend_assign_to_object_dim(obj, dim, value OPLINE_CC EXECUTE_DATA_CC); + ZEND_MAYBE_TRACK_OBJECT_MUTATION(obj); zval_ptr_dtor_nogc(EX_VAR((opline+1)->op1.var)); if (UNEXPECTED(GC_DELREF(obj) == 0)) { @@ -104614,12 +105755,16 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_ASSIGN_DIM_SPEC_CV zval *variable_ptr; zval *dim; zend_refcounted *garbage = NULL; + bool should_publish_tracked_array = false; SAVE_OPLINE(); orig_object_ptr = object_ptr = EX_VAR(opline->op1.var); if (EXPECTED(Z_TYPE_P(object_ptr) == IS_ARRAY)) { try_assign_dim_array: + if (UNEXPECTED(EG(tracked_mutation_hooks_active))) { + should_publish_tracked_array = zend_maybe_track_hash_mutation(Z_ARRVAL_P(object_ptr), false); + } SEPARATE_ARRAY(object_ptr); if (IS_CV == IS_UNUSED) { value = EX_VAR((opline+1)->op1.var); @@ -104677,6 +105822,9 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_ASSIGN_DIM_SPEC_CV if (garbage) { GC_DTOR_NO_REF(garbage); } + if (UNEXPECTED(should_publish_tracked_array)) { + zend_maybe_track_hash_mutation(Z_ARRVAL_P(object_ptr), true); + } } else { if (EXPECTED(Z_ISREF_P(object_ptr))) { object_ptr = Z_REFVAL_P(object_ptr); @@ -104703,6 +105851,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_ASSIGN_DIM_SPEC_CV } zend_assign_to_object_dim(obj, dim, value OPLINE_CC EXECUTE_DATA_CC); + ZEND_MAYBE_TRACK_OBJECT_MUTATION(obj); if (UNEXPECTED(GC_DELREF(obj) == 0)) { @@ -104869,6 +106018,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_ASSIGN_OBJ_REF_SPE { USE_OPLINE zval *property, *container, *value_ptr; + zend_object *zobj = NULL; SAVE_OPLINE(); @@ -104876,6 +106026,11 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_ASSIGN_OBJ_REF_SPE property = _get_zval_ptr_cv_BP_VAR_R(opline->op2.var EXECUTE_DATA_CC); value_ptr = _get_zval_ptr_ptr_var((opline+1)->op1.var EXECUTE_DATA_CC); + if (EXPECTED(Z_TYPE_P(container) == IS_OBJECT)) { + zobj = Z_OBJ_P(container); + } else if (Z_ISREF_P(container) && EXPECTED(Z_TYPE_P(Z_REFVAL_P(container)) == IS_OBJECT)) { + zobj = Z_OBJ_P(Z_REFVAL_P(container)); + } if (1) { if (IS_CV == IS_UNUSED) { @@ -104895,6 +106050,8 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_ASSIGN_OBJ_REF_SPE zend_assign_to_property_reference(container, IS_CV, property, IS_CV, value_ptr OPLINE_CC EXECUTE_DATA_CC); } + ZEND_MAYBE_TRACK_OBJECT_MUTATION(zobj); + @@ -104907,6 +106064,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_ASSIGN_OBJ_REF_SPE { USE_OPLINE zval *property, *container, *value_ptr; + zend_object *zobj = NULL; SAVE_OPLINE(); @@ -104914,6 +106072,11 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_ASSIGN_OBJ_REF_SPE property = _get_zval_ptr_cv_BP_VAR_R(opline->op2.var EXECUTE_DATA_CC); value_ptr = _get_zval_ptr_cv_BP_VAR_W((opline+1)->op1.var EXECUTE_DATA_CC); + if (EXPECTED(Z_TYPE_P(container) == IS_OBJECT)) { + zobj = Z_OBJ_P(container); + } else if (Z_ISREF_P(container) && EXPECTED(Z_TYPE_P(Z_REFVAL_P(container)) == IS_OBJECT)) { + zobj = Z_OBJ_P(Z_REFVAL_P(container)); + } if (1) { if (IS_CV == IS_UNUSED) { @@ -104933,6 +106096,8 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_ASSIGN_OBJ_REF_SPE zend_assign_to_property_reference(container, IS_CV, property, IS_CV, value_ptr OPLINE_CC EXECUTE_DATA_CC); } + ZEND_MAYBE_TRACK_OBJECT_MUTATION(zobj); + @@ -105373,6 +106538,8 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_UNSET_DIM_SPEC_CV_ zval *offset; zend_ulong hval; zend_string *key; + bool should_flush_array = false; + bool should_publish_tracked_array = false; SAVE_OPLINE(); container = EX_VAR(opline->op1.var); @@ -105383,6 +106550,9 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_UNSET_DIM_SPEC_CV_ HashTable *ht; unset_dim_array: + if (UNEXPECTED(EG(tracked_mutation_hooks_active))) { + should_publish_tracked_array = zend_maybe_track_hash_mutation(Z_ARRVAL_P(container), false); + } SEPARATE_ARRAY(container); ht = Z_ARRVAL_P(container); offset_again: @@ -105396,10 +106566,12 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_UNSET_DIM_SPEC_CV_ str_index_dim: ZEND_ASSERT(ht != &EG(symbol_table)); zend_hash_del(ht, key); + should_flush_array = true; } else if (EXPECTED(Z_TYPE_P(offset) == IS_LONG)) { hval = Z_LVAL_P(offset); num_index_dim: zend_hash_index_del(ht, hval); + should_flush_array = true; } else if ((IS_CV & (IS_VAR|IS_CV)) && EXPECTED(Z_TYPE_P(offset) == IS_REFERENCE)) { offset = Z_REFVAL_P(offset); goto offset_again; @@ -105436,6 +106608,9 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_UNSET_DIM_SPEC_CV_ } else { zend_illegal_array_offset_unset(offset); } + if (UNEXPECTED(should_flush_array && should_publish_tracked_array)) { + zend_maybe_track_hash_mutation(ht, true); + } break; } else if (Z_ISREF_P(container)) { container = Z_REFVAL_P(container); @@ -105454,6 +106629,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_UNSET_DIM_SPEC_CV_ offset++; } Z_OBJ_HT_P(container)->unset_dimension(Z_OBJ_P(container), offset); + ZEND_MAYBE_TRACK_OBJECT_MUTATION(Z_OBJ_P(container)); } else if (UNEXPECTED(Z_TYPE_P(container) == IS_STRING)) { zend_throw_error(NULL, "Cannot unset string offsets"); } else if (UNEXPECTED(Z_TYPE_P(container) > IS_FALSE)) { @@ -105475,6 +106651,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_UNSET_OBJ_SPEC_CV_ zval *container; zval *offset; zend_string *name, *tmp_name; + zend_object *zobj = NULL; SAVE_OPLINE(); container = EX_VAR(opline->op1.var); @@ -105495,6 +106672,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_UNSET_OBJ_SPEC_CV_ break; } } + zobj = Z_OBJ_P(container); if (IS_CV == IS_CONST) { name = Z_STR_P(offset); } else { @@ -105509,6 +106687,8 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_UNSET_OBJ_SPEC_CV_ } } while (0); + ZEND_MAYBE_TRACK_OBJECT_MUTATION(zobj); + diff --git a/ext/date/php_date.c b/ext/date/php_date.c index 18ac305e8430..8430e82fda53 100644 --- a/ext/date/php_date.c +++ b/ext/date/php_date.c @@ -17,6 +17,7 @@ #include "php_ini.h" #include "ext/standard/info.h" #include "ext/standard/php_versioning.h" +#include "ext/opcache/zend_static_cache.h" #include "php_date.h" #include "zend_attributes.h" #include "zend_interfaces.h" @@ -2200,6 +2201,217 @@ static void date_interval_object_to_hash(php_interval_obj *intervalobj, HashTabl #undef PHP_DATE_INTERVAL_ADD_PROPERTY } +static bool php_date_initialize_from_hash(php_date_obj **dateobj, const HashTable *myht); +static bool php_date_timezone_initialize_from_hash(php_timezone_obj **tzobj, const HashTable *myht); +static void php_date_interval_initialize_from_hash(php_interval_obj *intobj, const HashTable *myht); + +static bool php_date_copy_direct_cache_state( + void *context, + zend_object *old_object, + zend_object *new_object, + zend_opcache_static_cache_safe_direct_clone_value_func_t clone_value) +{ + (void) context; + (void) clone_value; + + if (instanceof_function(old_object->ce, date_ce_date) || + instanceof_function(old_object->ce, date_ce_immutable)) { + php_date_obj *old_obj = php_date_obj_from_obj(old_object); + php_date_obj *new_obj = php_date_obj_from_obj(new_object); + + if (old_obj->time == NULL) { + return true; + } + + new_obj->time = timelib_time_clone(old_obj->time); + + return new_obj->time != NULL; + } + + if (instanceof_function(old_object->ce, date_ce_timezone)) { + php_timezone_obj *old_obj = php_timezone_obj_from_obj(old_object); + php_timezone_obj *new_obj = php_timezone_obj_from_obj(new_object); + + if (!old_obj->initialized) { + return true; + } + + new_obj->type = old_obj->type; + new_obj->initialized = true; + switch (new_obj->type) { + case TIMELIB_ZONETYPE_ID: + new_obj->tzi.tz = old_obj->tzi.tz; + return true; + case TIMELIB_ZONETYPE_OFFSET: + new_obj->tzi.utc_offset = old_obj->tzi.utc_offset; + return true; + case TIMELIB_ZONETYPE_ABBR: + new_obj->tzi.z.utc_offset = old_obj->tzi.z.utc_offset; + new_obj->tzi.z.dst = old_obj->tzi.z.dst; + new_obj->tzi.z.abbr = old_obj->tzi.z.abbr != NULL + ? timelib_strdup(old_obj->tzi.z.abbr) + : NULL; + return old_obj->tzi.z.abbr == NULL || new_obj->tzi.z.abbr != NULL; + default: + return false; + } + } + + if (instanceof_function(old_object->ce, date_ce_interval)) { + php_interval_obj *old_obj = php_interval_obj_from_obj(old_object); + php_interval_obj *new_obj = php_interval_obj_from_obj(new_object); + + new_obj->civil_or_wall = old_obj->civil_or_wall; + new_obj->from_string = old_obj->from_string; + if (old_obj->date_string != NULL) { + new_obj->date_string = zend_string_copy(old_obj->date_string); + } + new_obj->initialized = old_obj->initialized; + if (old_obj->diff != NULL) { + new_obj->diff = timelib_rel_time_clone(old_obj->diff); + + return new_obj->diff != NULL; + } + + return true; + } + + return false; +} + +static void php_date_direct_cache_add_assoc_int64(zval *array_zv, const char *name, int64_t value) +{ +#if PHP_DATE_SIZEOF_LONG == 8 + add_assoc_long(array_zv, name, (zend_long) value); +#else + add_assoc_str(array_zv, name, zend_strpprintf(0, "%lld", (long long) value)); +#endif +} + +static bool php_date_serialize_direct_cache_state(const zval *object, zval *state) +{ + ZVAL_UNDEF(state); + + if (instanceof_function(Z_OBJCE_P(object), date_ce_date) || + instanceof_function(Z_OBJCE_P(object), date_ce_immutable)) { + php_date_obj *dateobj = Z_PHPDATE_P((zval *) object); + + if (dateobj->time == NULL || !dateobj->time->is_localtime) { + return false; + } + + array_init_size(state, 3); + date_object_to_hash(dateobj, Z_ARRVAL_P(state)); + + return true; + } + + if (instanceof_function(Z_OBJCE_P(object), date_ce_timezone)) { + php_timezone_obj *tzobj = Z_PHPTIMEZONE_P((zval *) object); + + if (!tzobj->initialized) { + return false; + } + + array_init_size(state, 2); + date_timezone_object_to_hash(tzobj, Z_ARRVAL_P(state)); + + return true; + } + + if (instanceof_function(Z_OBJCE_P(object), date_ce_interval)) { + php_interval_obj *intervalobj = Z_PHPINTERVAL_P((zval *) object); + + if (!intervalobj->initialized || intervalobj->diff == NULL) { + return false; + } + + if (intervalobj->from_string) { + if (intervalobj->date_string == NULL) { + return false; + } + + array_init_size(state, 2); + add_assoc_bool(state, "from_string", true); + add_assoc_str(state, "date_string", zend_string_copy(intervalobj->date_string)); + + return true; + } + + array_init_size(state, 18); + add_assoc_long(state, "y", intervalobj->diff->y); + add_assoc_long(state, "m", intervalobj->diff->m); + add_assoc_long(state, "d", intervalobj->diff->d); + add_assoc_long(state, "h", intervalobj->diff->h); + add_assoc_long(state, "i", intervalobj->diff->i); + add_assoc_long(state, "s", intervalobj->diff->s); + add_assoc_double(state, "f", (double) intervalobj->diff->us / 1000000.0); + add_assoc_long(state, "invert", intervalobj->diff->invert); + + if (intervalobj->diff->days != TIMELIB_UNSET) { + php_date_direct_cache_add_assoc_int64(state, "days", intervalobj->diff->days); + } else { + add_assoc_bool(state, "days", false); + } + + add_assoc_bool(state, "from_string", false); + add_assoc_long(state, "weekday", intervalobj->diff->weekday); + add_assoc_long(state, "weekday_behavior", intervalobj->diff->weekday_behavior); + add_assoc_long(state, "first_last_day_of", intervalobj->diff->first_last_day_of); + add_assoc_long(state, "special_type", intervalobj->diff->special.type); + php_date_direct_cache_add_assoc_int64(state, "special_amount", intervalobj->diff->special.amount); + add_assoc_long(state, "have_weekday_relative", intervalobj->diff->have_weekday_relative); + add_assoc_long(state, "have_special_relative", intervalobj->diff->have_special_relative); + add_assoc_long(state, "civil_or_wall", intervalobj->civil_or_wall); + + return true; + } + + return false; +} + +static bool php_date_unserialize_direct_cache_state(zval *object, zval *state) +{ + if (Z_TYPE_P(state) != IS_ARRAY) { + return false; + } + + if (instanceof_function(Z_OBJCE_P(object), date_ce_date) || + instanceof_function(Z_OBJCE_P(object), date_ce_immutable)) { + php_date_obj *dateobj = Z_PHPDATE_P(object); + + return php_date_initialize_from_hash(&dateobj, Z_ARRVAL_P(state)); + } + + if (instanceof_function(Z_OBJCE_P(object), date_ce_timezone)) { + php_timezone_obj *tzobj = Z_PHPTIMEZONE_P(object); + + return php_date_timezone_initialize_from_hash(&tzobj, Z_ARRVAL_P(state)); + } + + if (instanceof_function(Z_OBJCE_P(object), date_ce_interval)) { + php_date_interval_initialize_from_hash(Z_PHPINTERVAL_P(object), Z_ARRVAL_P(state)); + + return !EG(exception); + } + + return false; +} + +const zend_opcache_static_cache_safe_direct_handlers *php_date_get_direct_cache_handlers(void) +{ + static const zend_opcache_static_cache_safe_direct_handlers handlers = { + true, + { false, NULL, NULL }, + php_date_copy_direct_cache_state, + NULL, + php_date_serialize_direct_cache_state, + php_date_unserialize_direct_cache_state + }; + + return &handlers; +} + static HashTable *date_object_get_properties_interval(zend_object *object) /* {{{ */ { HashTable *props; diff --git a/ext/date/php_date.h b/ext/date/php_date.h index d480283643db..06804183bbd1 100644 --- a/ext/date/php_date.h +++ b/ext/date/php_date.h @@ -161,4 +161,10 @@ PHPAPI bool php_date_initialize(php_date_obj *dateobj, const char *time_str, siz PHPAPI void php_date_initialize_from_ts_long(php_date_obj *dateobj, zend_long sec, int usec); PHPAPI bool php_date_initialize_from_ts_double(php_date_obj *dateobj, double ts); +#ifndef ZEND_OPCACHE_STATIC_CACHE_SAFE_DIRECT_HANDLERS_FWD +# define ZEND_OPCACHE_STATIC_CACHE_SAFE_DIRECT_HANDLERS_FWD +typedef struct _zend_opcache_static_cache_safe_direct_handlers zend_opcache_static_cache_safe_direct_handlers; +#endif +const zend_opcache_static_cache_safe_direct_handlers *php_date_get_direct_cache_handlers(void); + #endif /* PHP_DATE_H */ diff --git a/ext/opcache/ZendAccelerator.c b/ext/opcache/ZendAccelerator.c index 3d005b3835a7..ed1496582e90 100644 --- a/ext/opcache/ZendAccelerator.c +++ b/ext/opcache/ZendAccelerator.c @@ -135,6 +135,67 @@ static zend_result (*orig_post_startup_cb)(void); static zend_result accel_post_startup(void); static zend_result accel_finish_startup(void); +static const zend_opcache_static_cache_accelerator_handlers *static_cache_handlers = NULL; + +void zend_accel_register_static_cache_handlers(const zend_opcache_static_cache_accelerator_handlers *handlers) +{ + static_cache_handlers = handlers; +} + +static zend_always_inline void zend_accel_static_cache_startup(void) +{ + if (static_cache_handlers != NULL && static_cache_handlers->startup != NULL) { + static_cache_handlers->startup(); + } +} + +static zend_always_inline void zend_accel_static_cache_post_startup(void) +{ + if (static_cache_handlers != NULL && static_cache_handlers->post_startup != NULL) { + static_cache_handlers->post_startup(); + } +} + +static zend_always_inline zend_result zend_accel_static_cache_rinit(void) +{ + if (static_cache_handlers != NULL && static_cache_handlers->rinit != NULL) { + return static_cache_handlers->rinit(); + } + + return SUCCESS; +} + +static zend_always_inline void zend_accel_static_cache_invalidate_script(zend_persistent_script *persistent_script) +{ + if (static_cache_handlers != NULL && static_cache_handlers->invalidate_script != NULL) { + static_cache_handlers->invalidate_script(persistent_script); + } +} + +#ifdef ZTS +typedef struct _zend_accel_thread_startup_config { + bool enabled; + zend_atomic_bool valid; + zend_accel_directives accel_directives; +} zend_accel_thread_startup_config; + +static zend_accel_thread_startup_config accel_thread_startup_config; + +static zend_always_inline void accel_apply_thread_startup_config(zend_accel_globals *accel_globals) +{ + if (!zend_atomic_bool_load_ex(&accel_thread_startup_config.valid)) { + return; + } + + accel_globals->enabled = accel_thread_startup_config.enabled; + accel_globals->accel_directives = accel_thread_startup_config.accel_directives; +} + +static zend_always_inline void accel_sync_thread_startup_config(void) +{ + accel_apply_thread_startup_config(TSRMG_FAST_BULK(accel_globals_offset, zend_accel_globals *)); +} +#endif #ifndef ZEND_WIN32 # define PRELOAD_SUPPORT @@ -1453,6 +1514,7 @@ zend_result zend_accel_invalidate(zend_string *filename, bool force) if (force || !ZCG(accel_directives).validate_timestamps || do_validate_timestamps(persistent_script, &file_handle) == FAILURE) { + zend_accel_static_cache_invalidate_script(persistent_script); HANDLE_BLOCK_INTERRUPTIONS(); SHM_UNPROTECT(); zend_accel_lock_discard_script(persistent_script); @@ -2149,6 +2211,7 @@ zend_op_array *persistent_compile_file(zend_file_handle *file_handle, int type) /* If script is found then validate_timestamps if option is enabled */ if (persistent_script && ZCG(accel_directives).validate_timestamps) { if (validate_timestamp_and_record(persistent_script, file_handle) == FAILURE) { + zend_accel_static_cache_invalidate_script(persistent_script); zend_accel_lock_discard_script(persistent_script); persistent_script = NULL; } @@ -2665,6 +2728,10 @@ static void accel_reset_pcre_cache(void) ZEND_RINIT_FUNCTION(zend_accelerator) { +#ifdef ZTS + accel_sync_thread_startup_config(); +#endif + if (!ZCG(enabled) || !accel_startup_ok) { ZCG(accelerator_enabled) = false; return SUCCESS; @@ -2803,6 +2870,8 @@ ZEND_RINIT_FUNCTION(zend_accelerator) } #endif + zend_accel_static_cache_rinit(); + return SUCCESS; } @@ -2967,6 +3036,9 @@ static void accel_globals_ctor(zend_accel_globals *accel_globals) memset(accel_globals, 0, sizeof(zend_accel_globals)); accel_globals->key = zend_string_alloc(ZCG_KEY_LEN, true); GC_MAKE_PERSISTENT_LOCAL(accel_globals->key); +#ifdef ZTS + accel_apply_thread_startup_config(accel_globals); +#endif } static void accel_globals_dtor(zend_accel_globals *accel_globals) @@ -3188,6 +3260,7 @@ static int accel_startup(zend_extension *extension) { #ifdef ZTS accel_globals_id = ts_allocate_fast_id(&accel_globals_id, &accel_globals_offset, sizeof(zend_accel_globals), (ts_allocate_ctor) accel_globals_ctor, (ts_allocate_dtor) accel_globals_dtor); + zend_atomic_bool_store_ex(&accel_thread_startup_config.valid, false); #else accel_globals_ctor(&accel_globals); #endif @@ -3204,6 +3277,12 @@ static int accel_startup(zend_extension *extension) zend_accel_register_ini_entries(); +#ifdef ZTS + accel_thread_startup_config.enabled = ZCG(enabled); + accel_thread_startup_config.accel_directives = ZCG(accel_directives); + zend_atomic_bool_store_ex(&accel_thread_startup_config.valid, true); +#endif + #ifdef ZEND_WIN32 if (UNEXPECTED(accel_gen_uname_id() == FAILURE)) { zps_startup_failure("Unable to get user name", NULL, accelerator_remove_cb); @@ -3462,6 +3541,21 @@ static zend_result accel_post_startup(void) return FAILURE; } + /* Initialize static cache if configured */ + bool static_cache_configured = + ZCG(accel_directives).static_cache_volatile_size_mb != 0 || + ZCG(accel_directives).static_cache_persistent_size_mb != 0 + ; + bool static_cache_preload_configured = static_cache_configured && + ZCG(accel_directives).preload && + *ZCG(accel_directives).preload + ; + + if (static_cache_preload_configured) { + zend_accel_static_cache_startup(); + } + zend_accel_static_cache_post_startup(); + if (ZCG(enabled) && accel_startup_ok) { /* Override inheritance cache callbaks */ accelerator_orig_inheritance_cache_get = zend_inheritance_cache_get; diff --git a/ext/opcache/ZendAccelerator.h b/ext/opcache/ZendAccelerator.h index 91642e288d31..e08fa53c4f29 100644 --- a/ext/opcache/ZendAccelerator.h +++ b/ext/opcache/ZendAccelerator.h @@ -139,8 +139,19 @@ typedef struct _zend_persistent_script { } dynamic_members; } zend_persistent_script; +typedef struct _zend_opcache_static_cache_accelerator_handlers { + void (*startup)(void); + void (*post_startup)(void); + zend_result (*rinit)(void); + void (*invalidate_script)(zend_persistent_script *persistent_script); +} zend_opcache_static_cache_accelerator_handlers; + +void zend_accel_register_static_cache_handlers(const zend_opcache_static_cache_accelerator_handlers *handlers); + typedef struct _zend_accel_directives { zend_long memory_consumption; + zend_long static_cache_volatile_size_mb; + zend_long static_cache_persistent_size_mb; zend_long max_accelerated_files; double max_wasted_percentage; char *user_blacklist_filename; diff --git a/ext/opcache/config.m4 b/ext/opcache/config.m4 index 70138726c56e..a3f8de655cea 100644 --- a/ext/opcache/config.m4 +++ b/ext/opcache/config.m4 @@ -331,6 +331,11 @@ PHP_NEW_EXTENSION([opcache], m4_normalize([ zend_accelerator_hash.c zend_accelerator_module.c zend_accelerator_util_funcs.c + zend_static_cache.c + zend_static_cache_storage.c + zend_static_cache_shared_graph.c + zend_static_cache_entries.c + zend_static_cache_statics.c zend_file_cache.c zend_persist_calc.c zend_persist.c diff --git a/ext/opcache/config.w32 b/ext/opcache/config.w32 index 397fa1bdd87d..55e3bc9f3e5e 100644 --- a/ext/opcache/config.w32 +++ b/ext/opcache/config.w32 @@ -10,6 +10,11 @@ ZEND_EXTENSION('opcache', "\ zend_accelerator_hash.c \ zend_accelerator_module.c \ zend_accelerator_util_funcs.c \ + zend_static_cache.c \ + zend_static_cache_storage.c \ + zend_static_cache_shared_graph.c \ + zend_static_cache_entries.c \ + zend_static_cache_statics.c \ zend_persist.c \ zend_persist_calc.c \ zend_file_cache.c \ diff --git a/ext/opcache/jit/zend_jit_helpers.c b/ext/opcache/jit/zend_jit_helpers.c index 64a48068f378..dbd03c4dfa39 100644 --- a/ext/opcache/jit/zend_jit_helpers.c +++ b/ext/opcache/jit/zend_jit_helpers.c @@ -3413,6 +3413,15 @@ static void ZEND_FASTCALL zend_jit_uninit_static_prop(void) zend_get_unmangled_property_name(property_info->name)); } +static void ZEND_FASTCALL zend_jit_static_prop_access_helper(const zend_property_info *property_info) +{ + if (UNEXPECTED(EG(static_cache_class_access_active) && + zend_class_static_access_hook != NULL) + ) { + zend_class_static_access_hook(property_info->ce); + } +} + static void ZEND_FASTCALL zend_jit_free_trampoline_helper(zend_function *func) { ZEND_ASSERT(func->common.fn_flags & ZEND_ACC_CALL_VIA_TRAMPOLINE); diff --git a/ext/opcache/jit/zend_jit_ir.c b/ext/opcache/jit/zend_jit_ir.c index cf43d3ad840f..0d4b35bc5646 100644 --- a/ext/opcache/jit/zend_jit_ir.c +++ b/ext/opcache/jit/zend_jit_ir.c @@ -16103,6 +16103,17 @@ static int zend_jit_fetch_static_prop(zend_jit_ctx *jit, const zend_op *opline, if_cached = ir_IF(ref); ir_IF_TRUE(if_cached); + /* Keep the JIT static-property fast path in sync with + * zend_fetch_static_property_address(): OPcache Static Cache may need to + * refresh class-blob state even when the run-time cache already resolved + * the static property slot. */ + prop_info_ref = known_prop_info != NULL + ? ir_CONST_ADDR(known_prop_info) + : ir_LOAD_L(ir_ADD_OFFSET(ir_LOAD_A(jit_EX(run_time_cache)), cache_slot + sizeof(void*) * 2)) + ; + ir_CALL_1(IR_VOID, ir_CONST_FC_FUNC(zend_jit_static_prop_access_helper), prop_info_ref); + zend_jit_check_exception_undef_result(jit, opline); + if (fetch_type == BP_VAR_R || fetch_type == BP_VAR_RW) { if (!known_prop_info || ZEND_TYPE_IS_SET(known_prop_info->type)) { ir_ref merge = IR_UNUSED; @@ -16113,8 +16124,6 @@ static int zend_jit_fetch_static_prop(zend_jit_ctx *jit, const zend_op *opline, ir_IF_FALSE_cold(if_def); if (!known_prop_info) { // JIT: if (ZEND_TYPE_IS_SET(property_info->type)) - prop_info_ref = ir_LOAD_L( - ir_ADD_OFFSET(ir_LOAD_A(jit_EX(run_time_cache)), cache_slot + sizeof(void*) * 2)); if_typed = ir_IF(ir_AND_U32( ir_LOAD_U32(ir_ADD_OFFSET(prop_info_ref, offsetof(zend_property_info, type.type_mask))), ir_CONST_U32(_ZEND_TYPE_MASK))); @@ -16138,20 +16147,16 @@ static int zend_jit_fetch_static_prop(zend_jit_ctx *jit, const zend_op *opline, } else if (fetch_type == BP_VAR_W) { flags = opline->extended_value & ZEND_FETCH_OBJ_FLAGS; if (flags && (!known_prop_info || ZEND_TYPE_IS_SET(known_prop_info->type))) { - ir_ref merge = IR_UNUSED; + ir_ref merge = IR_UNUSED; if (!known_prop_info) { // JIT: if (ZEND_TYPE_IS_SET(property_info->type)) - prop_info_ref = ir_LOAD_L( - ir_ADD_OFFSET(ir_LOAD_A(jit_EX(run_time_cache)), cache_slot + sizeof(void*) * 2)); if_typed = ir_IF(ir_AND_U32( ir_LOAD_U32(ir_ADD_OFFSET(prop_info_ref, offsetof(zend_property_info, type.type_mask))), ir_CONST_U32(_ZEND_TYPE_MASK))); ir_IF_FALSE(if_typed); ir_END_list(merge); ir_IF_TRUE(if_typed); - } else { - prop_info_ref = ir_CONST_ADDR(known_prop_info); } // JIT: zend_handle_fetch_obj_flags(NULL, *retval, NULL, property_info, flags); diff --git a/ext/opcache/opcache.stub.php b/ext/opcache/opcache.stub.php index 32673bb1dcee..4b793b491b8f 100644 --- a/ext/opcache/opcache.stub.php +++ b/ext/opcache/opcache.stub.php @@ -2,6 +2,8 @@ /** @generate-class-entries */ +namespace { + function opcache_reset(): bool {} /** @@ -25,3 +27,95 @@ function opcache_get_configuration(): array|false {} function opcache_is_script_cached(string $filename): bool {} function opcache_is_script_cached_in_file_cache(string $filename): bool {} + +} + +namespace OPcache { + +class StaticCacheException extends \Exception +{ +} + +#[\Attribute(13)] /* TARGET_CLASS | TARGET_METHOD | TARGET_PROPERTY */ +final class PersistentStatic +{ +} + +enum CacheStrategy: int +{ + case Immediate = 0; + case Tracking = 1; +} + +#[\Attribute(13)] /* TARGET_CLASS | TARGET_METHOD | TARGET_PROPERTY */ +final class VolatileStatic +{ + public readonly int $ttl; + + public readonly CacheStrategy $strategy; + + public function __construct(int $ttl = 0, CacheStrategy $strategy = CacheStrategy::Immediate) {} +} + +#[\Attribute(1)] /* TARGET_CLASS */ +final class __DirectCacheSafe +{ +} + +function volatile_store(string $key, null|bool|int|float|string|array|object $value, int $ttl = 0): bool {} + +function volatile_store_array(array $values, int $ttl = 0): bool {} + +function volatile_fetch(string $key, null|bool|int|float|string|array|object $default = null): null|bool|int|float|string|array|object {} + +/** + * @return array|null + */ +function volatile_fetch_array(array $keys, ?array $default = null): ?array {} + +function volatile_exists(string $key): bool {} + +function volatile_lock(string $key): bool {} + +function volatile_delete(string $key): void {} + +function volatile_delete_array(array $keys): void {} + +function volatile_clear(): void {} + +/** + * @return array + */ +function volatile_cache_info(): array {} + +function persistent_store(string $key, null|bool|int|float|string|array|object $value): void {} + +function persistent_store_array(array $values): void {} + +function persistent_fetch(string $key, null|bool|int|float|string|array|object $default = null): null|bool|int|float|string|array|object {} + +/** + * @return array|null + */ +function persistent_fetch_array(array $keys, ?array $default = null): ?array {} + +function persistent_exists(string $key): bool {} + +function persistent_lock(string $key): bool {} + +function persistent_delete(string $key): void {} + +function persistent_delete_array(array $keys): void {} + +function persistent_clear(): void {} + +function persistent_atomic_increment(string $key, int $step = 1): int {} + +function persistent_atomic_decrement(string $key, int $step = 1): int {} + +/** + * @return array + */ +function persistent_cache_info(): array {} + +} diff --git a/ext/opcache/opcache_arginfo.h b/ext/opcache/opcache_arginfo.h index 60a1633154c9..4aa7f5323e33 100644 --- a/ext/opcache/opcache_arginfo.h +++ b/ext/opcache/opcache_arginfo.h @@ -1,5 +1,5 @@ /* This is a generated file, edit opcache.stub.php instead. - * Stub hash: a8de025fa96a78db3a26d53a18bb2b365d094eca */ + * Stub hash: 7014b50c5940d238d5a5b3e4902a457f183ee298 */ ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_opcache_reset, 0, 0, _IS_BOOL, 0) ZEND_END_ARG_INFO() @@ -28,6 +28,84 @@ ZEND_END_ARG_INFO() #define arginfo_opcache_is_script_cached_in_file_cache arginfo_opcache_compile_file +ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_OPcache_volatile_store, 0, 2, _IS_BOOL, 0) + ZEND_ARG_TYPE_INFO(0, key, IS_STRING, 0) + ZEND_ARG_TYPE_MASK(0, value, MAY_BE_NULL|MAY_BE_BOOL|MAY_BE_LONG|MAY_BE_DOUBLE|MAY_BE_STRING|MAY_BE_ARRAY|MAY_BE_OBJECT, NULL) + ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, ttl, IS_LONG, 0, "0") +ZEND_END_ARG_INFO() + +ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_OPcache_volatile_store_array, 0, 1, _IS_BOOL, 0) + ZEND_ARG_TYPE_INFO(0, values, IS_ARRAY, 0) + ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, ttl, IS_LONG, 0, "0") +ZEND_END_ARG_INFO() + +ZEND_BEGIN_ARG_WITH_RETURN_TYPE_MASK_EX(arginfo_OPcache_volatile_fetch, 0, 1, MAY_BE_NULL|MAY_BE_BOOL|MAY_BE_LONG|MAY_BE_DOUBLE|MAY_BE_STRING|MAY_BE_ARRAY|MAY_BE_OBJECT) + ZEND_ARG_TYPE_INFO(0, key, IS_STRING, 0) + ZEND_ARG_TYPE_MASK(0, default, MAY_BE_NULL|MAY_BE_BOOL|MAY_BE_LONG|MAY_BE_DOUBLE|MAY_BE_STRING|MAY_BE_ARRAY|MAY_BE_OBJECT, "null") +ZEND_END_ARG_INFO() + +ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_OPcache_volatile_fetch_array, 0, 1, IS_ARRAY, 1) + ZEND_ARG_TYPE_INFO(0, keys, IS_ARRAY, 0) + ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, default, IS_ARRAY, 1, "null") +ZEND_END_ARG_INFO() + +ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_OPcache_volatile_exists, 0, 1, _IS_BOOL, 0) + ZEND_ARG_TYPE_INFO(0, key, IS_STRING, 0) +ZEND_END_ARG_INFO() + +#define arginfo_OPcache_volatile_lock arginfo_OPcache_volatile_exists + +ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_OPcache_volatile_delete, 0, 1, IS_VOID, 0) + ZEND_ARG_TYPE_INFO(0, key, IS_STRING, 0) +ZEND_END_ARG_INFO() + +ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_OPcache_volatile_delete_array, 0, 1, IS_VOID, 0) + ZEND_ARG_TYPE_INFO(0, keys, IS_ARRAY, 0) +ZEND_END_ARG_INFO() + +ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_OPcache_volatile_clear, 0, 0, IS_VOID, 0) +ZEND_END_ARG_INFO() + +ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_OPcache_volatile_cache_info, 0, 0, IS_ARRAY, 0) +ZEND_END_ARG_INFO() + +ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_OPcache_persistent_store, 0, 2, IS_VOID, 0) + ZEND_ARG_TYPE_INFO(0, key, IS_STRING, 0) + ZEND_ARG_TYPE_MASK(0, value, MAY_BE_NULL|MAY_BE_BOOL|MAY_BE_LONG|MAY_BE_DOUBLE|MAY_BE_STRING|MAY_BE_ARRAY|MAY_BE_OBJECT, NULL) +ZEND_END_ARG_INFO() + +ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_OPcache_persistent_store_array, 0, 1, IS_VOID, 0) + ZEND_ARG_TYPE_INFO(0, values, IS_ARRAY, 0) +ZEND_END_ARG_INFO() + +#define arginfo_OPcache_persistent_fetch arginfo_OPcache_volatile_fetch + +#define arginfo_OPcache_persistent_fetch_array arginfo_OPcache_volatile_fetch_array + +#define arginfo_OPcache_persistent_exists arginfo_OPcache_volatile_exists + +#define arginfo_OPcache_persistent_lock arginfo_OPcache_volatile_exists + +#define arginfo_OPcache_persistent_delete arginfo_OPcache_volatile_delete + +#define arginfo_OPcache_persistent_delete_array arginfo_OPcache_volatile_delete_array + +#define arginfo_OPcache_persistent_clear arginfo_OPcache_volatile_clear + +ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_OPcache_persistent_atomic_increment, 0, 1, IS_LONG, 0) + ZEND_ARG_TYPE_INFO(0, key, IS_STRING, 0) + ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, step, IS_LONG, 0, "1") +ZEND_END_ARG_INFO() + +#define arginfo_OPcache_persistent_atomic_decrement arginfo_OPcache_persistent_atomic_increment + +#define arginfo_OPcache_persistent_cache_info arginfo_OPcache_volatile_cache_info + +ZEND_BEGIN_ARG_INFO_EX(arginfo_class_OPcache_VolatileStatic___construct, 0, 0, 0) + ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, ttl, IS_LONG, 0, "0") + ZEND_ARG_OBJ_INFO_WITH_DEFAULT_VALUE(0, strategy, OPcache\\CacheStrategy, 0, "OPcache\\CacheStrategy::Immediate") +ZEND_END_ARG_INFO() + ZEND_FUNCTION(opcache_reset); ZEND_FUNCTION(opcache_get_status); ZEND_FUNCTION(opcache_compile_file); @@ -36,6 +114,29 @@ ZEND_FUNCTION(opcache_jit_blacklist); ZEND_FUNCTION(opcache_get_configuration); ZEND_FUNCTION(opcache_is_script_cached); ZEND_FUNCTION(opcache_is_script_cached_in_file_cache); +ZEND_FUNCTION(OPcache_volatile_store); +ZEND_FUNCTION(OPcache_volatile_store_array); +ZEND_FUNCTION(OPcache_volatile_fetch); +ZEND_FUNCTION(OPcache_volatile_fetch_array); +ZEND_FUNCTION(OPcache_volatile_exists); +ZEND_FUNCTION(OPcache_volatile_lock); +ZEND_FUNCTION(OPcache_volatile_delete); +ZEND_FUNCTION(OPcache_volatile_delete_array); +ZEND_FUNCTION(OPcache_volatile_clear); +ZEND_FUNCTION(OPcache_volatile_cache_info); +ZEND_FUNCTION(OPcache_persistent_store); +ZEND_FUNCTION(OPcache_persistent_store_array); +ZEND_FUNCTION(OPcache_persistent_fetch); +ZEND_FUNCTION(OPcache_persistent_fetch_array); +ZEND_FUNCTION(OPcache_persistent_exists); +ZEND_FUNCTION(OPcache_persistent_lock); +ZEND_FUNCTION(OPcache_persistent_delete); +ZEND_FUNCTION(OPcache_persistent_delete_array); +ZEND_FUNCTION(OPcache_persistent_clear); +ZEND_FUNCTION(OPcache_persistent_atomic_increment); +ZEND_FUNCTION(OPcache_persistent_atomic_decrement); +ZEND_FUNCTION(OPcache_persistent_cache_info); +ZEND_METHOD(OPcache_VolatileStatic, __construct); static const zend_function_entry ext_functions[] = { ZEND_FE(opcache_reset, arginfo_opcache_reset) @@ -46,5 +147,115 @@ static const zend_function_entry ext_functions[] = { ZEND_FE(opcache_get_configuration, arginfo_opcache_get_configuration) ZEND_FE(opcache_is_script_cached, arginfo_opcache_is_script_cached) ZEND_FE(opcache_is_script_cached_in_file_cache, arginfo_opcache_is_script_cached_in_file_cache) + ZEND_RAW_FENTRY(ZEND_NS_NAME("OPcache", "volatile_store"), zif_OPcache_volatile_store, arginfo_OPcache_volatile_store, 0, NULL, NULL) + ZEND_RAW_FENTRY(ZEND_NS_NAME("OPcache", "volatile_store_array"), zif_OPcache_volatile_store_array, arginfo_OPcache_volatile_store_array, 0, NULL, NULL) + ZEND_RAW_FENTRY(ZEND_NS_NAME("OPcache", "volatile_fetch"), zif_OPcache_volatile_fetch, arginfo_OPcache_volatile_fetch, 0, NULL, NULL) + ZEND_RAW_FENTRY(ZEND_NS_NAME("OPcache", "volatile_fetch_array"), zif_OPcache_volatile_fetch_array, arginfo_OPcache_volatile_fetch_array, 0, NULL, NULL) + ZEND_RAW_FENTRY(ZEND_NS_NAME("OPcache", "volatile_exists"), zif_OPcache_volatile_exists, arginfo_OPcache_volatile_exists, 0, NULL, NULL) + ZEND_RAW_FENTRY(ZEND_NS_NAME("OPcache", "volatile_lock"), zif_OPcache_volatile_lock, arginfo_OPcache_volatile_lock, 0, NULL, NULL) + ZEND_RAW_FENTRY(ZEND_NS_NAME("OPcache", "volatile_delete"), zif_OPcache_volatile_delete, arginfo_OPcache_volatile_delete, 0, NULL, NULL) + ZEND_RAW_FENTRY(ZEND_NS_NAME("OPcache", "volatile_delete_array"), zif_OPcache_volatile_delete_array, arginfo_OPcache_volatile_delete_array, 0, NULL, NULL) + ZEND_RAW_FENTRY(ZEND_NS_NAME("OPcache", "volatile_clear"), zif_OPcache_volatile_clear, arginfo_OPcache_volatile_clear, 0, NULL, NULL) + ZEND_RAW_FENTRY(ZEND_NS_NAME("OPcache", "volatile_cache_info"), zif_OPcache_volatile_cache_info, arginfo_OPcache_volatile_cache_info, 0, NULL, NULL) + ZEND_RAW_FENTRY(ZEND_NS_NAME("OPcache", "persistent_store"), zif_OPcache_persistent_store, arginfo_OPcache_persistent_store, 0, NULL, NULL) + ZEND_RAW_FENTRY(ZEND_NS_NAME("OPcache", "persistent_store_array"), zif_OPcache_persistent_store_array, arginfo_OPcache_persistent_store_array, 0, NULL, NULL) + ZEND_RAW_FENTRY(ZEND_NS_NAME("OPcache", "persistent_fetch"), zif_OPcache_persistent_fetch, arginfo_OPcache_persistent_fetch, 0, NULL, NULL) + ZEND_RAW_FENTRY(ZEND_NS_NAME("OPcache", "persistent_fetch_array"), zif_OPcache_persistent_fetch_array, arginfo_OPcache_persistent_fetch_array, 0, NULL, NULL) + ZEND_RAW_FENTRY(ZEND_NS_NAME("OPcache", "persistent_exists"), zif_OPcache_persistent_exists, arginfo_OPcache_persistent_exists, 0, NULL, NULL) + ZEND_RAW_FENTRY(ZEND_NS_NAME("OPcache", "persistent_lock"), zif_OPcache_persistent_lock, arginfo_OPcache_persistent_lock, 0, NULL, NULL) + ZEND_RAW_FENTRY(ZEND_NS_NAME("OPcache", "persistent_delete"), zif_OPcache_persistent_delete, arginfo_OPcache_persistent_delete, 0, NULL, NULL) + ZEND_RAW_FENTRY(ZEND_NS_NAME("OPcache", "persistent_delete_array"), zif_OPcache_persistent_delete_array, arginfo_OPcache_persistent_delete_array, 0, NULL, NULL) + ZEND_RAW_FENTRY(ZEND_NS_NAME("OPcache", "persistent_clear"), zif_OPcache_persistent_clear, arginfo_OPcache_persistent_clear, 0, NULL, NULL) + ZEND_RAW_FENTRY(ZEND_NS_NAME("OPcache", "persistent_atomic_increment"), zif_OPcache_persistent_atomic_increment, arginfo_OPcache_persistent_atomic_increment, 0, NULL, NULL) + ZEND_RAW_FENTRY(ZEND_NS_NAME("OPcache", "persistent_atomic_decrement"), zif_OPcache_persistent_atomic_decrement, arginfo_OPcache_persistent_atomic_decrement, 0, NULL, NULL) + ZEND_RAW_FENTRY(ZEND_NS_NAME("OPcache", "persistent_cache_info"), zif_OPcache_persistent_cache_info, arginfo_OPcache_persistent_cache_info, 0, NULL, NULL) ZEND_FE_END }; + +static const zend_function_entry class_OPcache_VolatileStatic_methods[] = { + ZEND_ME(OPcache_VolatileStatic, __construct, arginfo_class_OPcache_VolatileStatic___construct, ZEND_ACC_PUBLIC) + ZEND_FE_END +}; + +static zend_class_entry *register_class_OPcache_StaticCacheException(zend_class_entry *class_entry_Exception) +{ + zend_class_entry ce, *class_entry; + + INIT_NS_CLASS_ENTRY(ce, "OPcache", "StaticCacheException", NULL); + class_entry = zend_register_internal_class_with_flags(&ce, class_entry_Exception, 0); + + return class_entry; +} + +static zend_class_entry *register_class_OPcache_PersistentStatic(void) +{ + zend_class_entry ce, *class_entry; + + INIT_NS_CLASS_ENTRY(ce, "OPcache", "PersistentStatic", NULL); + class_entry = zend_register_internal_class_with_flags(&ce, NULL, ZEND_ACC_FINAL); + + zend_string *attribute_name_Attribute_class_OPcache_PersistentStatic_0 = zend_string_init_interned("Attribute", sizeof("Attribute") - 1, true); + zend_attribute *attribute_Attribute_class_OPcache_PersistentStatic_0 = zend_add_class_attribute(class_entry, attribute_name_Attribute_class_OPcache_PersistentStatic_0, 1); + zend_string_release_ex(attribute_name_Attribute_class_OPcache_PersistentStatic_0, true); + ZVAL_LONG(&attribute_Attribute_class_OPcache_PersistentStatic_0->args[0].value, 13); + + return class_entry; +} + +static zend_class_entry *register_class_OPcache_CacheStrategy(void) +{ + zend_class_entry *class_entry = zend_register_internal_enum("OPcache\\CacheStrategy", IS_LONG, NULL); + + zval enum_case_Immediate_value; + ZVAL_LONG(&enum_case_Immediate_value, 0); + zend_enum_add_case_cstr(class_entry, "Immediate", &enum_case_Immediate_value); + + zval enum_case_Tracking_value; + ZVAL_LONG(&enum_case_Tracking_value, 1); + zend_enum_add_case_cstr(class_entry, "Tracking", &enum_case_Tracking_value); + + return class_entry; +} + +static zend_class_entry *register_class_OPcache_VolatileStatic(void) +{ + zend_class_entry ce, *class_entry; + + INIT_NS_CLASS_ENTRY(ce, "OPcache", "VolatileStatic", class_OPcache_VolatileStatic_methods); + class_entry = zend_register_internal_class_with_flags(&ce, NULL, ZEND_ACC_FINAL); + + zval property_ttl_default_value; + ZVAL_UNDEF(&property_ttl_default_value); + zend_string *property_ttl_name = zend_string_init("ttl", sizeof("ttl") - 1, true); + zend_declare_typed_property(class_entry, property_ttl_name, &property_ttl_default_value, ZEND_ACC_PUBLIC|ZEND_ACC_READONLY, NULL, (zend_type) ZEND_TYPE_INIT_MASK(MAY_BE_LONG)); + zend_string_release_ex(property_ttl_name, true); + + zval property_strategy_default_value; + ZVAL_UNDEF(&property_strategy_default_value); + zend_string *property_strategy_name = zend_string_init("strategy", sizeof("strategy") - 1, true); + zend_string *property_strategy_class_OPcache_CacheStrategy = zend_string_init("OPcache\\CacheStrategy", sizeof("OPcache\\CacheStrategy")-1, 1); + zend_declare_typed_property(class_entry, property_strategy_name, &property_strategy_default_value, ZEND_ACC_PUBLIC|ZEND_ACC_READONLY, NULL, (zend_type) ZEND_TYPE_INIT_CLASS(property_strategy_class_OPcache_CacheStrategy, 0, 0)); + zend_string_release_ex(property_strategy_name, true); + + zend_string *attribute_name_Attribute_class_OPcache_VolatileStatic_0 = zend_string_init_interned("Attribute", sizeof("Attribute") - 1, true); + zend_attribute *attribute_Attribute_class_OPcache_VolatileStatic_0 = zend_add_class_attribute(class_entry, attribute_name_Attribute_class_OPcache_VolatileStatic_0, 1); + zend_string_release_ex(attribute_name_Attribute_class_OPcache_VolatileStatic_0, true); + ZVAL_LONG(&attribute_Attribute_class_OPcache_VolatileStatic_0->args[0].value, 13); + + return class_entry; +} + +static zend_class_entry *register_class_OPcache___DirectCacheSafe(void) +{ + zend_class_entry ce, *class_entry; + + INIT_NS_CLASS_ENTRY(ce, "OPcache", "__DirectCacheSafe", NULL); + class_entry = zend_register_internal_class_with_flags(&ce, NULL, ZEND_ACC_FINAL); + + zend_string *attribute_name_Attribute_class_OPcache___DirectCacheSafe_0 = zend_string_init_interned("Attribute", sizeof("Attribute") - 1, true); + zend_attribute *attribute_Attribute_class_OPcache___DirectCacheSafe_0 = zend_add_class_attribute(class_entry, attribute_name_Attribute_class_OPcache___DirectCacheSafe_0, 1); + zend_string_release_ex(attribute_name_Attribute_class_OPcache___DirectCacheSafe_0, true); + ZVAL_LONG(&attribute_Attribute_class_OPcache___DirectCacheSafe_0->args[0].value, 1); + + return class_entry; +} diff --git a/ext/opcache/tests/fpm/CONFLICTS b/ext/opcache/tests/fpm/CONFLICTS new file mode 100644 index 000000000000..14c2e0b65abd --- /dev/null +++ b/ext/opcache/tests/fpm/CONFLICTS @@ -0,0 +1 @@ +fpm diff --git a/ext/opcache/tests/fpm/skipif.inc b/ext/opcache/tests/fpm/skipif.inc new file mode 100644 index 000000000000..bc6813f63979 --- /dev/null +++ b/ext/opcache/tests/fpm/skipif.inc @@ -0,0 +1,13 @@ + +--FILE-- +start(iniEntries: [ + 'opcache.enable' => '1', + 'opcache.static_cache.persistent_size_mb' => '32', + 'opcache.file_update_protection' => '0', + 'opcache.jit' => 'tracing', + 'opcache.jit_buffer_size' => '64M', + 'opcache.jit_hot_loop' => '0', + 'opcache.jit_hot_func' => '2', + 'opcache.jit_hot_return' => '0', + 'opcache.jit_hot_side_exit' => '1', +]); +$tester->expectLogStartNotices(); + +$tester->request(query: 'request=1')->expectBody('1,7,6,21,1'); +$tester->request(query: 'request=2')->expectBody("1,6,21,1,1\n11,7,33,1"); +$tester->request(query: 'request=3')->expectBody('1,7,33,1,1'); + +$tester->terminate(); +$tester->expectLogTerminatingNotices(); +$tester->close(); + +echo "Done\n"; + +?> +--EXPECT-- +Done +--CLEAN-- + diff --git a/ext/opcache/tests/fpm/static_cache_fpm_persistent_static_class_blob_persistence_001.phpt b/ext/opcache/tests/fpm/static_cache_fpm_persistent_static_class_blob_persistence_001.phpt new file mode 100644 index 000000000000..563e39b2bbc5 --- /dev/null +++ b/ext/opcache/tests/fpm/static_cache_fpm_persistent_static_class_blob_persistence_001.phpt @@ -0,0 +1,75 @@ +--TEST-- +FPM: OPcache PersistentStatic class blob persists across requests +--SKIPIF-- + +--FILE-- +start(iniEntries: [ + 'opcache.enable' => '1', + 'opcache.static_cache.persistent_size_mb' => '32', +]); +$tester->expectLogStartNotices(); + +$tester->request(query: 'request=1')->expectBody('1,1'); +$tester->request(query: 'request=2')->expectBody("1,10,2,1\n2,21,3"); + +$tester->terminate(); +$tester->expectLogTerminatingNotices(); +$tester->close(); + +echo "Done\n"; + +?> +--EXPECT-- +Done +--CLEAN-- + diff --git a/ext/opcache/tests/fpm/static_cache_fpm_persistent_static_complex_value_persistence_001.phpt b/ext/opcache/tests/fpm/static_cache_fpm_persistent_static_complex_value_persistence_001.phpt new file mode 100644 index 000000000000..90c821c5e5a3 --- /dev/null +++ b/ext/opcache/tests/fpm/static_cache_fpm_persistent_static_complex_value_persistence_001.phpt @@ -0,0 +1,86 @@ +--TEST-- +FPM: OPcache PersistentStatic persists complex values across requests +--SKIPIF-- + +--FILE-- + new CounterBox(100), 'gap' => []]; + $state['gap'][4] = 'seed'; + unset($state['gap'][4]); + } + + $state['box']->value++; + $state['gap'][] = 'tail'; + + return $state['box']->value . ':' . array_key_last($state['gap']); + } +} + +if (ComplexState::$box === null) { + ComplexState::$box = new CounterBox(1); + ComplexState::$gap[3] = 'seed'; + unset(ComplexState::$gap[3]); +} + +ComplexState::$box->value++; +ComplexState::$gap[] = 'tail'; + +echo ComplexState::$box->value, ',', array_key_last(ComplexState::$gap), ',', ComplexState::methodState(), "\n"; +PHP; + +$tester = new FPM\Tester($cfg, $code); +$tester->start(iniEntries: [ + 'opcache.enable' => '1', + 'opcache.static_cache.persistent_size_mb' => '32', +]); +$tester->expectLogStartNotices(); + +$tester->request()->expectBody('2,4,101:5'); +$tester->request()->expectBody('3,5,102:6'); + +$tester->terminate(); +$tester->expectLogTerminatingNotices(); +$tester->close(); + +echo "Done\n"; + +?> +--EXPECT-- +Done +--CLEAN-- + diff --git a/ext/opcache/tests/fpm/static_cache_fpm_persistent_static_object_assignment_snapshot_001.phpt b/ext/opcache/tests/fpm/static_cache_fpm_persistent_static_object_assignment_snapshot_001.phpt new file mode 100644 index 000000000000..d7dda3838147 --- /dev/null +++ b/ext/opcache/tests/fpm/static_cache_fpm_persistent_static_object_assignment_snapshot_001.phpt @@ -0,0 +1,67 @@ +--TEST-- +FPM: OPcache PersistentStatic snapshots object assignments without following object property writes +--SKIPIF-- + +--FILE-- + 1]; +echo NestedObjectPropertyState::$propertyState->count, "\n"; +echo OPcache\persistent_cache_info()['entry_count'], "\n"; + +NestedObjectPropertyState::$propertyState->count++; +echo NestedObjectPropertyState::$propertyState->count, "\n"; +echo OPcache\persistent_cache_info()['entry_count'], "\n"; + +NestedObjectPropertyState::$propertyState->count = 10; +echo NestedObjectPropertyState::$propertyState->count, "\n"; +echo OPcache\persistent_cache_info()['entry_count']; +PHP; + +$tester = new FPM\Tester($cfg, $code); +$tester->start(iniEntries: [ + 'opcache.enable' => '1', + 'opcache.static_cache.persistent_size_mb' => '32', + 'opcache.optimization_level' => '0', + 'opcache.file_update_protection' => '0', + 'opcache.jit' => '0', +]); +$tester->expectLogStartNotices(); + +$tester->request()->expectBody("1\n1\n2\n1\n10\n1"); + +$tester->terminate(); +$tester->expectLogTerminatingNotices(); +$tester->close(); + +echo "Done\n"; + +?> +--EXPECT-- +Done +--CLEAN-- + diff --git a/ext/opcache/tests/fpm/static_cache_fpm_persistent_static_persistence_001.phpt b/ext/opcache/tests/fpm/static_cache_fpm_persistent_static_persistence_001.phpt new file mode 100644 index 000000000000..512999522da6 --- /dev/null +++ b/ext/opcache/tests/fpm/static_cache_fpm_persistent_static_persistence_001.phpt @@ -0,0 +1,80 @@ +--TEST-- +FPM: OPcache PersistentStatic persists across requests +--SKIPIF-- + +--FILE-- +start(iniEntries: [ + 'opcache.enable' => '1', + 'opcache.static_cache.persistent_size_mb' => '32', +]); +$tester->expectLogStartNotices(); + +$tester->request()->expectBody('1,1,1,1,1'); +$tester->request()->expectBody('2,2,2,1,1'); + +$tester->terminate(); +$tester->expectLogTerminatingNotices(); +$tester->close(); + +echo "Done\n"; + +?> +--EXPECT-- +Done +--CLEAN-- + diff --git a/ext/opcache/tests/fpm/static_cache_fpm_persistent_static_property_array_publish_001.phpt b/ext/opcache/tests/fpm/static_cache_fpm_persistent_static_property_array_publish_001.phpt new file mode 100644 index 000000000000..804dc30b2d28 --- /dev/null +++ b/ext/opcache/tests/fpm/static_cache_fpm_persistent_static_property_array_publish_001.phpt @@ -0,0 +1,63 @@ +--TEST-- +FPM: OPcache PersistentStatic publishes nested array writes for property-scoped state +--SKIPIF-- + +--FILE-- + [1]]; +echo json_encode(NestedArrayPropertyState::$propertyState['numbers']), "\n"; +echo OPcache\persistent_cache_info()['entry_count'], "\n"; + +NestedArrayPropertyState::$propertyState['numbers'][] = 2; +echo json_encode(NestedArrayPropertyState::$propertyState['numbers']), "\n"; +echo OPcache\persistent_cache_info()['entry_count']; +PHP; + +$tester = new FPM\Tester($cfg, $code); +$tester->start(iniEntries: [ + 'opcache.enable' => '1', + 'opcache.static_cache.persistent_size_mb' => '32', + 'opcache.optimization_level' => '0', + 'opcache.file_update_protection' => '0', + 'opcache.jit' => '0', +]); +$tester->expectLogStartNotices(); + +$tester->request()->expectBody("[1]\n1\n[1,2]\n1"); + +$tester->terminate(); +$tester->expectLogTerminatingNotices(); +$tester->close(); + +echo "Done\n"; + +?> +--EXPECT-- +Done +--CLEAN-- + diff --git a/ext/opcache/tests/fpm/static_cache_fpm_persistent_static_scope_writes_001.phpt b/ext/opcache/tests/fpm/static_cache_fpm_persistent_static_scope_writes_001.phpt new file mode 100644 index 000000000000..a1138952a85c --- /dev/null +++ b/ext/opcache/tests/fpm/static_cache_fpm_persistent_static_scope_writes_001.phpt @@ -0,0 +1,122 @@ +--TEST-- +FPM: OPcache PersistentStatic class, property, and method state handles sequential writes by scope +--SKIPIF-- + +--FILE-- + 0, 'numbers' => []]; + + public static function next(): int + { + static $value = 0; + + return ++$value; + } +} + +class FpmRacePropertyState +{ + #[OPcache\PersistentStatic] + public static array $data = ['value' => 0, 'numbers' => []]; +} + +class FpmRaceMethodState +{ + #[OPcache\PersistentStatic] + public static function next(): int + { + static $value = 0; + + return ++$value; + } +} + +$action = $_GET['action'] ?? 'reset'; + +if ($action === 'reset') { + opcache_reset(); + echo 'reset'; + return; +} + +if ($action === 'seed') { + FpmRaceClassState::$data = ['value' => 1, 'numbers' => [1]]; + FpmRaceClassState::next(); + FpmRacePropertyState::$data = ['value' => 1, 'numbers' => [1]]; + FpmRaceMethodState::next(); + echo 'seed'; + return; +} + +$tuple = static function (): string { + return FpmRaceClassState::$data['value'] . ',' + . array_sum(FpmRaceClassState::$data['numbers']) . ',' + . FpmRaceClassState::next() . ',' + . FpmRacePropertyState::$data['value'] . ',' + . array_sum(FpmRacePropertyState::$data['numbers']) . ',' + . FpmRaceMethodState::next(); +}; + +if ($action === 'writer') { + $classMethod = FpmRaceClassState::next(); + FpmRaceClassState::$data['value'] = 10; + FpmRaceClassState::$data['numbers'][] = 20; + FpmRacePropertyState::$data['value'] = 10; + FpmRacePropertyState::$data['numbers'][] = 20; + $method = FpmRaceMethodState::next(); + echo 'writer:', FpmRaceClassState::$data['value'], ',', array_sum(FpmRaceClassState::$data['numbers']), ',', $classMethod, ',', FpmRacePropertyState::$data['value'], ',', array_sum(FpmRacePropertyState::$data['numbers']), ',', $method; + return; +} + +if ($action === 'verify') { + echo 'verify:', $tuple(); + return; +} +PHP; + +$tester = new FPM\Tester($cfg, $code); +$tester->start(iniEntries: [ + 'opcache.enable' => '1', + 'opcache.static_cache.persistent_size_mb' => '32', + 'opcache.file_update_protection' => '0', +]); +$tester->expectLogStartNotices(); + +$tester->request(query: 'action=reset')->expectBody('reset'); +$tester->request(query: 'action=seed')->expectBody('seed'); +$tester->request(query: 'action=writer')->expectBody('writer:10,21,2,10,21,2'); +$tester->request(query: 'action=verify')->expectBody('verify:10,21,3,10,21,3'); + +$tester->terminate(); +$tester->expectLogTerminatingNotices(); +$tester->close(); + +echo "Done\n"; + +?> +--EXPECT-- +Done +--CLEAN-- + diff --git a/ext/opcache/tests/fpm/static_cache_fpm_volatile_cache_complex_value_persistence_001.phpt b/ext/opcache/tests/fpm/static_cache_fpm_volatile_cache_complex_value_persistence_001.phpt new file mode 100644 index 000000000000..e932f455aa25 --- /dev/null +++ b/ext/opcache/tests/fpm/static_cache_fpm_volatile_cache_complex_value_persistence_001.phpt @@ -0,0 +1,135 @@ +--TEST-- +FPM: OPcache volatile cache persists complex values across requests +--SKIPIF-- + +--FILE-- +id = $id; + $this->name = $name; + } + + public function __serialize(): array + { + return ['id' => $this->id, 'name' => $this->name]; + } + + public function __unserialize(array $data): void + { + $this->id = $data['id']; + $this->name = $data['name']; + } + + public function info(): string + { + return $this->id . ':' . $this->name; + } +} + +$action = $_GET['action'] ?? 'seed'; + +if ($action === 'seed') { + $gap = []; + $gap[4] = 'seed'; + unset($gap[4]); + + $payload = [ + 'props' => new SimpleUser('Alice', 30), + 'serialize' => new SerUser(7, 'Bob'), + 'internal' => new DateTimeImmutable('2026-06-15 09:30:00', new DateTimeZone('UTC')), + 'gap' => $gap, + ]; + + $shared = new stdClass(); + $shared->value = 42; + + $refs = ['value' => 1]; + $refs['alias'] =& $refs['value']; + + OPcache\volatile_clear(); + var_dump(OPcache\volatile_store('complex', $payload)); + var_dump(OPcache\volatile_store('shared_pair', [$shared, $shared])); + var_dump(OPcache\volatile_store('refs', $refs)); + echo "seed\n"; + return; +} + +$complex = OPcache\volatile_fetch('complex'); +$complex['gap'][] = 'tail'; +echo $complex['props']->name, ',', $complex['props']->age, ',', $complex['serialize']->info(), ',', $complex['internal']->format('Y-m-d H:i:s'), ',', array_key_last($complex['gap']), "\n"; + +$pair = OPcache\volatile_fetch('shared_pair'); +var_dump(spl_object_id($pair[0]) === spl_object_id($pair[1])); + +$refs = OPcache\volatile_fetch('refs'); +$refs['alias'] = 7; +var_dump($refs['value']); + +echo "fetch\n"; +PHP; + +$tester = new FPM\Tester($cfg, $code); +$tester->start(iniEntries: [ + 'opcache.enable' => '1', + 'opcache.static_cache.volatile_size_mb' => '32', +]); +$tester->expectLogStartNotices(); + +$tester->request(query: 'action=seed')->expectBody( + "bool(true)\n" . + "bool(true)\n" . + "bool(true)\n" . + "seed" +); + +$tester->request(query: 'action=fetch')->expectBody( + "Alice,30,7:Bob,2026-06-15 09:30:00,5\n" . + "bool(true)\n" . + "int(7)\n" . + "fetch" +); + +$tester->terminate(); +$tester->expectLogTerminatingNotices(); +$tester->close(); + +echo "Done\n"; + +?> +--EXPECT-- +Done +--CLEAN-- + diff --git a/ext/opcache/tests/fpm/static_cache_fpm_volatile_cache_direct_cache_safe_001.phpt b/ext/opcache/tests/fpm/static_cache_fpm_volatile_cache_direct_cache_safe_001.phpt new file mode 100644 index 000000000000..424e6cdfc7d1 --- /dev/null +++ b/ext/opcache/tests/fpm/static_cache_fpm_volatile_cache_direct_cache_safe_001.phpt @@ -0,0 +1,194 @@ +--TEST-- +FPM: OPcache __DirectCacheSafe subclasses survive requests, safe serializer overrides stay direct, and wakeup hooks fallback +--EXTENSIONS-- +opcache +spl +--SKIPIF-- + +--FILE-- +label = $label; + $this->revision = $revision; + } + + public function describe(): string + { + return $this->label . ':' . $this->revision; + } +} + +class LabelIterator extends ArrayIterator +{ +} + +class TaggedCollection extends ArrayObject +{ + private string $type; + + public function __construct(array $data, string $type, string $iteratorClass) + { + parent::__construct($data, 0, $iteratorClass); + $this->type = $type; + } + + public function type(): string + { + return $this->type; + } +} + +class CustomSerializedDateTime extends DateTime +{ + public static int $unserializeCount = 0; + + private string $label; + + public function __construct(string $time, DateTimeZone $timezone, string $label) + { + parent::__construct($time, $timezone); + $this->label = $label; + } + + public function __serialize(): array + { + return parent::__serialize() + ['label' => $this->label]; + } + + public function __unserialize(array $data): void + { + self::$unserializeCount++; + parent::__unserialize($data); + $this->label = $data['label']; + } + + public function label(): string + { + return $this->label; + } +} + +class WakefulSerializedDateTime extends DateTime +{ + public static int $unserializeCount = 0; + + private string $label; + + public function __construct(string $time, DateTimeZone $timezone, string $label) + { + parent::__construct($time, $timezone); + $this->label = $label; + } + + public function __serialize(): array + { + return parent::__serialize() + ['label' => $this->label]; + } + + public function __unserialize(array $data): void + { + self::$unserializeCount++; + parent::__unserialize($data); + $this->label = $data['label']; + } + + public function __sleep(): array + { + return ['label']; + } + + public function label(): string + { + return $this->label; + } +} + +$action = $_GET['action'] ?? 'seed'; + +if ($action === 'seed') { + OPcache\volatile_clear(); + + $event = new EventDateTime('2026-06-15 09:30:00.123456', new DateTimeZone('Europe/Paris'), 'launch', 7); + $collection = new TaggedCollection(['alpha' => 10, 'beta' => 20], 'metric', LabelIterator::class); + $fallback = new CustomSerializedDateTime('2026-06-15 10:45:00.654321', new DateTimeZone('UTC'), 'fallback'); + $wakeful = new WakefulSerializedDateTime('2026-06-15 12:15:00.987654', new DateTimeZone('UTC'), 'fallback'); + + var_dump(OPcache\volatile_store('safe_direct_event_datetime', $event)); + var_dump(OPcache\volatile_store('safe_direct_tagged_collection', $collection)); + var_dump(OPcache\volatile_store('safe_direct_custom_datetime', $fallback)); + var_dump(OPcache\volatile_store('safe_direct_wakeful_datetime', $wakeful)); + echo "seed\n"; + return; +} + +$event = OPcache\volatile_fetch('safe_direct_event_datetime'); +$collection = OPcache\volatile_fetch('safe_direct_tagged_collection'); +$fallback = OPcache\volatile_fetch('safe_direct_custom_datetime'); +$wakeful = OPcache\volatile_fetch('safe_direct_wakeful_datetime'); +$iterator = $collection->getIterator(); + +echo $event->format('Y-m-d H:i:s.u e'), ',', $event->describe(), "\n"; +echo $collection->type(), ',', ($iterator instanceof LabelIterator ? 'LabelIterator' : get_debug_type($iterator)), ',', $collection['alpha'], ',', $collection['beta'], "\n"; +echo $fallback->format('Y-m-d H:i:s.u e'), ',', $fallback->label(), ',', CustomSerializedDateTime::$unserializeCount, "\n"; +echo $wakeful->format('Y-m-d H:i:s.u e'), ',', $wakeful->label(), ',', WakefulSerializedDateTime::$unserializeCount, "\n"; +PHP; + +$tester = new FPM\Tester($cfg, $code); +$tester->start(iniEntries: [ + 'opcache.enable' => '1', + 'opcache.static_cache.volatile_size_mb' => '32', +]); +$tester->expectLogStartNotices(); + +$tester->request(query: 'action=seed')->expectBody( + "bool(true)\n" . + "bool(true)\n" . + "bool(true)\n" . + "bool(true)\n" . + "seed" +); + +$tester->request(query: 'action=fetch')->expectBody( + "2026-06-15 09:30:00.123456 Europe/Paris,launch:7\n" . + "metric,LabelIterator,10,20\n" . + "2026-06-15 10:45:00.654321 UTC,fallback,0\n" . + "2026-06-15 12:15:00.987654 UTC,fallback,1" +); + +$tester->terminate(); +$tester->expectLogTerminatingNotices(); +$tester->close(); + +echo "Done\n"; + +?> +--EXPECT-- +Done +--CLEAN-- + diff --git a/ext/opcache/tests/fpm/static_cache_fpm_volatile_cache_lookup_cache_001.phpt b/ext/opcache/tests/fpm/static_cache_fpm_volatile_cache_lookup_cache_001.phpt new file mode 100644 index 000000000000..81e6e45b1236 --- /dev/null +++ b/ext/opcache/tests/fpm/static_cache_fpm_volatile_cache_lookup_cache_001.phpt @@ -0,0 +1,91 @@ +--TEST-- +FPM: OPcache volatile cache request-local lookup cache sees same-request updates +--SKIPIF-- + +--FILE-- +start(iniEntries: [ + 'opcache.enable' => '1', + 'opcache.static_cache.volatile_size_mb' => '32', +]); +$tester->expectLogStartNotices(); + +$tester->request(query: 'scenario=hit_store')->expectBody("old\nnew"); +$tester->request(query: 'scenario=miss_store')->expectBody("MISS\ncreated"); +$tester->request(query: 'scenario=hit_delete')->expectBody("old\nMISS"); +$tester->request(query: 'scenario=hit_clear')->expectBody("old\nMISS"); + +$tester->terminate(); +$tester->expectLogTerminatingNotices(); +$tester->close(); + +echo "Done\n"; + +?> +--EXPECT-- +Done +--CLEAN-- + diff --git a/ext/opcache/tests/fpm/static_cache_fpm_volatile_cache_nested_shared_object_copy_on_write_001.phpt b/ext/opcache/tests/fpm/static_cache_fpm_volatile_cache_nested_shared_object_copy_on_write_001.phpt new file mode 100644 index 000000000000..38aaee74e677 --- /dev/null +++ b/ext/opcache/tests/fpm/static_cache_fpm_volatile_cache_nested_shared_object_copy_on_write_001.phpt @@ -0,0 +1,110 @@ +--TEST-- +FPM: OPcache volatile cache keeps nested userland objects shared until mutation with __DirectCacheSafe properties across requests +--SKIPIF-- + +--FILE-- + $i, + 'text' => str_repeat(chr(65 + ($i % 26)), 96), + 'flags' => [$i, $i + 1, $i + 2, $i + 3], + ]; + } + + return $rows; +} + +$action = $_GET['action'] ?? 'seed'; + +if ($action === 'seed') { + OPcache\volatile_clear(); + $payload = new WrappedPayload( + new LargePayload(build_rows(), 'nested'), + new DateTimeImmutable('2026-06-15 09:30:00', new DateTimeZone('UTC')), + ); + var_dump(OPcache\volatile_store('materialization_nested_payload', $payload)); + echo "seed\n"; + return; +} + +$before = memory_get_usage(); +$fetched = OPcache\volatile_fetch('materialization_nested_payload'); +$readOnly = $fetched->timestamp->format(DateTimeInterface::ATOM) . '|' . $fetched->payload->rows[100]['text']; +$afterFetch = memory_get_usage(); +$fetched->payload->rows[100]['text'] = 'changed'; +$afterMutate = memory_get_usage(); +$usesZendAlloc = getenv('USE_ZEND_ALLOC') !== '0'; + +var_dump(!$usesZendAlloc || ($afterFetch - $before) < 262144); +var_dump($fetched->payload->rows[100]['text'] === 'changed' && (!$usesZendAlloc || ($afterMutate - $afterFetch) > 131072)); +PHP; + +$tester = new FPM\Tester($cfg, $code); +$tester->start(iniEntries: [ + 'opcache.enable' => '1', + 'opcache.static_cache.volatile_size_mb' => '32', +]); +$tester->expectLogStartNotices(); + +$tester->request(query: 'action=seed')->expectBody( + "bool(true)\n" . + "seed" +); + +$tester->request(query: 'action=fetch')->expectBody( + "bool(true)\n" . + "bool(true)" +); + +$tester->terminate(); +$tester->expectLogTerminatingNotices(); +$tester->close(); + +echo "Done\n"; + +?> +--EXPECT-- +Done +--CLEAN-- + diff --git a/ext/opcache/tests/fpm/static_cache_fpm_volatile_cache_persistence_001.phpt b/ext/opcache/tests/fpm/static_cache_fpm_volatile_cache_persistence_001.phpt new file mode 100644 index 000000000000..f8d3e3f6d6fc --- /dev/null +++ b/ext/opcache/tests/fpm/static_cache_fpm_volatile_cache_persistence_001.phpt @@ -0,0 +1,103 @@ +--TEST-- +FPM: OPcache volatile cache persists across requests +--SKIPIF-- + +--FILE-- + 'hello from fpm', + 'payload' => ['a' => 1, 'b' => [2, 3]], + ])); + var_dump(OPcache\volatile_store('ttl_key', 'short-lived', 1)); + echo "seed\n"; + return; +} + +if ($action === 'fetch') { + var_dump(OPcache\volatile_fetch('counter')); + var_dump(OPcache\volatile_fetch('message')); + var_dump(OPcache\volatile_fetch('payload')); + OPcache\volatile_delete_array(['message']); + echo OPcache\volatile_fetch('message', 'MISS'), "\n"; + echo "fetch\n"; + return; +} + +echo OPcache\volatile_fetch('ttl_key', 'MISS'), "\n"; +echo "expire\n"; +PHP; + +$tester = new FPM\Tester($cfg, $code); +$tester->start(iniEntries: [ + 'opcache.enable' => '1', + 'opcache.static_cache.volatile_size_mb' => '32', +]); +$tester->expectLogStartNotices(); + +$tester->request(query: 'action=seed')->expectBody( + "bool(true)\n" . + "bool(true)\n" . + "bool(true)\n" . + "seed" +); + +$tester->request(query: 'action=fetch')->expectBody( + "int(41)\n" . + "string(14) \"hello from fpm\"\n" . + "array(2) {\n" . + " [\"a\"]=>\n" . + " int(1)\n" . + " [\"b\"]=>\n" . + " array(2) {\n" . + " [0]=>\n" . + " int(2)\n" . + " [1]=>\n" . + " int(3)\n" . + " }\n" . + "}\n" . + "MISS\n" . + "fetch" +); + +sleep(2); + +$tester->request(query: 'action=expire')->expectBody( + "MISS\n" . + "expire" +); + +$tester->terminate(); +$tester->expectLogTerminatingNotices(); +$tester->close(); + +echo "Done\n"; + +?> +--EXPECT-- +Done +--CLEAN-- + diff --git a/ext/opcache/tests/fpm/static_cache_fpm_volatile_cache_shared_object_copy_on_write_001.phpt b/ext/opcache/tests/fpm/static_cache_fpm_volatile_cache_shared_object_copy_on_write_001.phpt new file mode 100644 index 000000000000..dcb97fcf8e1f --- /dev/null +++ b/ext/opcache/tests/fpm/static_cache_fpm_volatile_cache_shared_object_copy_on_write_001.phpt @@ -0,0 +1,98 @@ +--TEST-- +FPM: OPcache volatile cache keeps plain userland objects shared until mutation across requests +--SKIPIF-- + +--FILE-- + $i, + 'text' => str_repeat(chr(65 + ($i % 26)), 96), + 'flags' => [$i, $i + 1, $i + 2, $i + 3], + ]; + } + + return $rows; +} + +$action = $_GET['action'] ?? 'seed'; + +if ($action === 'seed') { + OPcache\volatile_clear(); + var_dump(OPcache\volatile_store('materialization_plain_payload', new LargePayload(build_rows(), 'plain'))); + echo "seed\n"; + return; +} + +$before = memory_get_usage(); +$fetched = OPcache\volatile_fetch('materialization_plain_payload'); +$readOnly = $fetched->rows[100]['text']; +$afterFetch = memory_get_usage(); +$fetched->rows[100]['text'] = 'changed'; +$afterMutate = memory_get_usage(); +$usesZendAlloc = getenv('USE_ZEND_ALLOC') !== '0'; + +var_dump(!$usesZendAlloc || ($afterFetch - $before) < 262144); +var_dump($fetched->rows[100]['text'] === 'changed' && (!$usesZendAlloc || ($afterMutate - $afterFetch) > 131072)); +PHP; + +$tester = new FPM\Tester($cfg, $code); +$tester->start(iniEntries: [ + 'opcache.enable' => '1', + 'opcache.static_cache.volatile_size_mb' => '32', +]); +$tester->expectLogStartNotices(); + +$tester->request(query: 'action=seed')->expectBody( + "bool(true)\n" . + "seed" +); + +$tester->request(query: 'action=fetch')->expectBody( + "bool(true)\n" . + "bool(true)" +); + +$tester->terminate(); +$tester->expectLogTerminatingNotices(); +$tester->close(); + +echo "Done\n"; + +?> +--EXPECT-- +Done +--CLEAN-- + diff --git a/ext/opcache/tests/fpm/static_cache_fpm_volatile_persistent_cache_shared_workers_001.phpt b/ext/opcache/tests/fpm/static_cache_fpm_volatile_persistent_cache_shared_workers_001.phpt new file mode 100644 index 000000000000..8581e32fc4ea --- /dev/null +++ b/ext/opcache/tests/fpm/static_cache_fpm_volatile_persistent_cache_shared_workers_001.phpt @@ -0,0 +1,133 @@ +--TEST-- +FPM: OPcache volatile and persistent caches are shared across static workers +--SKIPIF-- + +--FILE-- +multiRequest(array_fill(0, 4, [ + 'query' => 'action=' . $action . '&seed_pid=' . $seedPid, + ]), readTimeout: 7000); + + $checked = 0; + foreach ($responses as $response) { + $body = trim((string) $response->getBody()); + if (preg_match('/^same:\d+$/', $body)) { + continue; + } + if (!preg_match('/^fetch:(\d+):(.*)$/', $body, $matches)) { + throw new RuntimeException('Unexpected response for ' . $action . ': ' . var_export($body, true)); + } + if ((int) $matches[1] === $seedPid) { + throw new RuntimeException('Seed worker unexpectedly returned a fetch response for ' . $action); + } + if ($matches[2] !== $expected) { + throw new RuntimeException(sprintf( + '%s was not shared across workers: expected %s, got %s', + $action, + $expected, + $matches[2] + )); + } + $checked++; + } + + if ($checked === 0) { + throw new RuntimeException('No non-seed worker handled ' . $action); + } +} + +$tester = new FPM\Tester($cfg, $code); +$tester->start(iniEntries: [ + 'opcache.enable' => '1', + 'opcache.static_cache.volatile_size_mb' => '32', + 'opcache.static_cache.persistent_size_mb' => '32', +]); +$tester->expectLogStartNotices(); + +$seedPid = parseSeedPid($tester->request(query: 'action=seed')->getBody()); + +expectCrossWorkerValue($tester, 'fetch_volatile', $seedPid, 'stored-value'); +expectCrossWorkerValue($tester, 'fetch_persistent', $seedPid, '42'); + +$tester->terminate(); +$tester->expectLogTerminatingNotices(); +$tester->close(); + +echo "Done\n"; + +?> +--EXPECT-- +Done +--CLEAN-- + diff --git a/ext/opcache/tests/fpm/static_cache_fpm_volatile_static_tracking_clear_invalidate_after_access_001.phpt b/ext/opcache/tests/fpm/static_cache_fpm_volatile_static_tracking_clear_invalidate_after_access_001.phpt new file mode 100644 index 000000000000..72c24177a388 --- /dev/null +++ b/ext/opcache/tests/fpm/static_cache_fpm_volatile_static_tracking_clear_invalidate_after_access_001.phpt @@ -0,0 +1,313 @@ +--TEST-- +OPcache VolatileStatic tracking reference graphs are not republished after volatile_clear() or opcache_invalidate() in the same request under FPM +--EXTENSIONS-- +opcache +--SKIPIF-- + +--FILE-- +holder = new stdClass(); + } +} + +#[OPcache\VolatileStatic(strategy: OPcache\CacheStrategy::Tracking)] +class TrackingClearInvalidateClassState +{ + public static ?TrackingClearInvalidatePayload $payload = null; +} + +class TrackingClearInvalidatePropertyState +{ + #[OPcache\VolatileStatic(strategy: OPcache\CacheStrategy::Tracking)] + public static ?TrackingClearInvalidatePayload $payload = null; +} + +class TrackingClearInvalidateMethodState +{ + #[OPcache\VolatileStatic(strategy: OPcache\CacheStrategy::Tracking)] + public static function payload(?TrackingClearInvalidatePayload $payload = null): ?TrackingClearInvalidatePayload + { + static $value = null; + + if ($payload !== null) { + $value = $payload; + } + + return $value; + } +} + +function tracking_clear_invalidate_set(string $kind, ?TrackingClearInvalidatePayload $payload): void +{ + switch ($kind) { + case 'class': + TrackingClearInvalidateClassState::$payload = $payload; + return; + + case 'property': + TrackingClearInvalidatePropertyState::$payload = $payload; + return; + + case 'method': + TrackingClearInvalidateMethodState::payload($payload); + return; + + default: + throw new RuntimeException('unknown kind'); + } +} + +function tracking_clear_invalidate_get(string $kind): ?TrackingClearInvalidatePayload +{ + return match ($kind) { + 'class' => TrackingClearInvalidateClassState::$payload, + 'property' => TrackingClearInvalidatePropertyState::$payload, + 'method' => TrackingClearInvalidateMethodState::payload(), + default => throw new RuntimeException('unknown kind'), + }; +} + +function tracking_clear_invalidate_apply(TrackingClearInvalidatePayload $payload, string $nodeName, string $childName, string $label): void +{ + $payload->objectRef->name = $nodeName; + $payload->objectRef->events[] = $label . '-object'; + $payload->arrayRef['child']->name = $childName; + $payload->arrayRef['child']->events[] = $label . '-child'; + $payload->holder->alias->events[] = $label . '-holder'; + $payload->log[] = $label; +} + +function tracking_clear_invalidate_seed(string $kind): void +{ + $leaf = new TrackingClearInvalidateNode('seed'); + $leaf->child = new TrackingClearInvalidateChild('child'); + + $payload = new TrackingClearInvalidatePayload(); + $payload->objectRef =& $leaf; + $payload->arrayRef = [ + 'alias' => &$leaf, + 'child' => &$leaf->child, + ]; + $payload->holder->alias =& $leaf; + $payload->holder->child =& $leaf->child; + $payload->log[] = 'seed'; + + tracking_clear_invalidate_set($kind, $payload); + tracking_clear_invalidate_apply($payload, 'seed-node', 'seed-child', 'seed'); +} + +function tracking_clear_invalidate_dump(string $kind, string $label): void +{ + $payload = tracking_clear_invalidate_get($kind); + if (!$payload instanceof TrackingClearInvalidatePayload) { + echo $kind, '@', $label, '=missing', "\n"; + return; + } + + echo $kind, '@', $label, '=', + $payload->objectRef->name, ',', + count($payload->objectRef->events), ',', + $payload->objectRef->child->name, ',', + count($payload->objectRef->child->events), ',', + count($payload->log), ',', + ($payload->objectRef === $payload->arrayRef['alias'] ? 'same' : 'copy'), ',', + ($payload->objectRef->child === $payload->arrayRef['child'] ? 'same' : 'copy'), ',', + ($payload->holder->alias === $payload->objectRef ? 'same' : 'copy'), ',', + ($payload->holder->child === $payload->objectRef->child ? 'same' : 'copy'), ',', + ($payload->holder->alias->child === $payload->objectRef->child ? 'same' : 'copy'), + "\n"; +} + +$action = $_GET['action'] ?? 'read'; +$kind = $_GET['kind'] ?? 'class'; + +if ($action === 'reset') { + OPcache\volatile_clear(); + opcache_reset(); + echo "reset\n"; + return; +} + +if ($action === 'seed') { + tracking_clear_invalidate_seed($kind); + tracking_clear_invalidate_dump($kind, 'seed'); + return; +} + +if ($action === 'clear_after_access') { + $payload = tracking_clear_invalidate_get($kind); + if (!$payload instanceof TrackingClearInvalidatePayload) { + throw new RuntimeException('missing payload'); + } + + tracking_clear_invalidate_apply($payload, 'clear-node', 'clear-child', 'clear'); + tracking_clear_invalidate_dump($kind, 'clear-before'); + OPcache\volatile_clear(); + echo "clear\n"; + return; +} + +if ($action === 'reset_after_access') { + $payload = tracking_clear_invalidate_get($kind); + if (!$payload instanceof TrackingClearInvalidatePayload) { + throw new RuntimeException('missing payload'); + } + + tracking_clear_invalidate_apply($payload, 'reset-node', 'reset-child', 'reset'); + tracking_clear_invalidate_dump($kind, 'reset-before'); + opcache_reset(); + echo "reset-after\n"; + return; +} + +if ($action === 'invalidate_after_access') { + $payload = tracking_clear_invalidate_get($kind); + if (!$payload instanceof TrackingClearInvalidatePayload) { + throw new RuntimeException('missing payload'); + } + + tracking_clear_invalidate_apply($payload, 'invalidate-node', 'invalidate-child', 'invalidate'); + tracking_clear_invalidate_dump($kind, 'invalidate-before'); + var_dump(opcache_invalidate(__FILE__, true)); + return; +} + +tracking_clear_invalidate_dump($kind, 'read'); +EOT; + +$args = []; +if (file_exists(ini_get('extension_dir') . '/opcache.so')) { + $args[] = '-dzend_extension=' . ini_get('extension_dir') . '/opcache.so'; +} +$args = [ + ...$args, + '-dopcache.enable=1', + '-dopcache.static_cache.volatile_size_mb=32', + '-dopcache.file_update_protection=0', + '-dopcache.jit=0', +]; + +$tester = new FPM\Tester($cfg, $code); +$tester->start($args); +$tester->expectLogStartNotices(); + +function tracking_clear_invalidate_fpm_request(FPM\Tester $tester, string $query): void +{ + echo $tester->request($query)->getBody(), "\n"; +} + +tracking_clear_invalidate_fpm_request($tester, 'action=reset'); +foreach (['class', 'property', 'method'] as $kind) { + tracking_clear_invalidate_fpm_request($tester, 'action=seed&kind=' . $kind); + tracking_clear_invalidate_fpm_request($tester, 'action=read&kind=' . $kind); + tracking_clear_invalidate_fpm_request($tester, 'action=clear_after_access&kind=' . $kind); + tracking_clear_invalidate_fpm_request($tester, 'action=read&kind=' . $kind); + tracking_clear_invalidate_fpm_request($tester, 'action=seed&kind=' . $kind); + tracking_clear_invalidate_fpm_request($tester, 'action=read&kind=' . $kind); + tracking_clear_invalidate_fpm_request($tester, 'action=reset_after_access&kind=' . $kind); + tracking_clear_invalidate_fpm_request($tester, 'action=read&kind=' . $kind); + tracking_clear_invalidate_fpm_request($tester, 'action=seed&kind=' . $kind); + tracking_clear_invalidate_fpm_request($tester, 'action=read&kind=' . $kind); + tracking_clear_invalidate_fpm_request($tester, 'action=invalidate_after_access&kind=' . $kind); + tracking_clear_invalidate_fpm_request($tester, 'action=read&kind=' . $kind); +} + +$tester->terminate(); +$tester->close(); +?> +--EXPECT-- +reset +class@seed=seed-node,2,seed-child,1,2,same,same,same,same,same +class@read=seed-node,2,seed-child,1,2,same,same,same,same,same +class@clear-before=clear-node,4,clear-child,2,3,same,same,same,same,same +clear +class@read=missing +class@seed=seed-node,2,seed-child,1,2,same,same,same,same,same +class@read=seed-node,2,seed-child,1,2,same,same,same,same,same +class@reset-before=reset-node,4,reset-child,2,3,same,same,same,same,same +reset-after +class@read=missing +class@seed=seed-node,2,seed-child,1,2,same,same,same,same,same +class@read=seed-node,2,seed-child,1,2,same,same,same,same,same +class@invalidate-before=invalidate-node,4,invalidate-child,2,3,same,same,same,same,same +bool(true) +class@read=missing +property@seed=seed-node,2,seed-child,1,2,same,same,same,same,same +property@read=seed-node,2,seed-child,1,2,same,same,same,same,same +property@clear-before=clear-node,4,clear-child,2,3,same,same,same,same,same +clear +property@read=missing +property@seed=seed-node,2,seed-child,1,2,same,same,same,same,same +property@read=seed-node,2,seed-child,1,2,same,same,same,same,same +property@reset-before=reset-node,4,reset-child,2,3,same,same,same,same,same +reset-after +property@read=missing +property@seed=seed-node,2,seed-child,1,2,same,same,same,same,same +property@read=seed-node,2,seed-child,1,2,same,same,same,same,same +property@invalidate-before=invalidate-node,4,invalidate-child,2,3,same,same,same,same,same +bool(true) +property@read=missing +method@seed=seed-node,2,seed-child,1,2,same,same,same,same,same +method@read=seed-node,2,seed-child,1,2,same,same,same,same,same +method@clear-before=clear-node,4,clear-child,2,3,same,same,same,same,same +clear +method@read=missing +method@seed=seed-node,2,seed-child,1,2,same,same,same,same,same +method@read=seed-node,2,seed-child,1,2,same,same,same,same,same +method@reset-before=reset-node,4,reset-child,2,3,same,same,same,same,same +reset-after +method@read=missing +method@seed=seed-node,2,seed-child,1,2,same,same,same,same,same +method@read=seed-node,2,seed-child,1,2,same,same,same,same,same +method@invalidate-before=invalidate-node,4,invalidate-child,2,3,same,same,same,same,same +bool(true) +method@read=missing +--CLEAN-- + diff --git a/ext/opcache/tests/fpm/static_cache_fpm_volatile_static_tracking_reference_object_graph_001.phpt b/ext/opcache/tests/fpm/static_cache_fpm_volatile_static_tracking_reference_object_graph_001.phpt new file mode 100644 index 000000000000..0e0ba8466300 --- /dev/null +++ b/ext/opcache/tests/fpm/static_cache_fpm_volatile_static_tracking_reference_object_graph_001.phpt @@ -0,0 +1,252 @@ +--TEST-- +OPcache VolatileStatic tracking persists deep mutations through referenced object graphs under FPM +--EXTENSIONS-- +opcache +--SKIPIF-- + +--FILE-- +holder = new stdClass(); + } +} + +#[OPcache\VolatileStatic(strategy: OPcache\CacheStrategy::Tracking)] +class TrackingReferenceClassState +{ + public static ?TrackingReferencePayload $payload = null; +} + +class TrackingReferencePropertyState +{ + #[OPcache\VolatileStatic(strategy: OPcache\CacheStrategy::Tracking)] + public static ?TrackingReferencePayload $payload = null; +} + +class TrackingReferenceMethodState +{ + #[OPcache\VolatileStatic(strategy: OPcache\CacheStrategy::Tracking)] + public static function payload(?TrackingReferencePayload $payload = null): ?TrackingReferencePayload + { + static $value = null; + + if ($payload !== null) { + $value = $payload; + } + + return $value; + } +} + +function tracking_reference_set(string $kind, ?TrackingReferencePayload $payload): void +{ + switch ($kind) { + case 'class': + TrackingReferenceClassState::$payload = $payload; + return; + + case 'property': + TrackingReferencePropertyState::$payload = $payload; + return; + + case 'method': + TrackingReferenceMethodState::payload($payload); + return; + + default: + throw new RuntimeException('unknown kind'); + } +} + +function tracking_reference_get(string $kind): ?TrackingReferencePayload +{ + return match ($kind) { + 'class' => TrackingReferenceClassState::$payload, + 'property' => TrackingReferencePropertyState::$payload, + 'method' => TrackingReferenceMethodState::payload(), + default => throw new RuntimeException('unknown kind'), + }; +} + +function tracking_reference_seed(string $kind): void +{ + $leaf = new TrackingReferenceNode('seed'); + $leaf->child = new TrackingReferenceChild('child'); + + $payload = new TrackingReferencePayload(); + $payload->objectRef =& $leaf; + $payload->arrayRef = [ + 'alias' => &$leaf, + 'child' => &$leaf->child, + ]; + $payload->holder->alias =& $leaf; + $payload->holder->child =& $leaf->child; + $payload->log[] = 'seed'; + + tracking_reference_set($kind, $payload); + + $leaf->name = 'seed-updated'; + $leaf->events[] = 'source-object'; + $leaf->child->name = 'child-updated'; + $leaf->child->events[] = 'source-child'; + $payload->arrayRef['alias']->events[] = 'array-alias'; + $payload->holder->alias->child->events[] = 'holder-alias-child'; + $payload->log[] = 'seed-mutated'; +} + +function tracking_reference_mutate(string $kind): void +{ + $payload = tracking_reference_get($kind); + if (!$payload instanceof TrackingReferencePayload) { + throw new RuntimeException('missing payload'); + } + + $payload->objectRef->name = 'mutated-again'; + $payload->objectRef->events[] = 'object-ref'; + $payload->arrayRef['child']->name = 'child-mutated-again'; + $payload->arrayRef['child']->events[] = 'array-child'; + $payload->holder->alias->events[] = 'holder-alias'; + $payload->holder->alias->child->events[] = 'holder-alias-child'; + $payload->log[] = 'mutated'; +} + +function tracking_reference_dump(string $kind, string $label): void +{ + $payload = tracking_reference_get($kind); + if (!$payload instanceof TrackingReferencePayload) { + echo $kind, '@', $label, '=missing', "\n"; + return; + } + + echo $kind, '@', $label, '=', + $payload->objectRef->name, ',', + count($payload->objectRef->events), ',', + $payload->objectRef->child->name, ',', + count($payload->objectRef->child->events), ',', + count($payload->log), ',', + ($payload->objectRef === $payload->arrayRef['alias'] ? 'same' : 'copy'), ',', + ($payload->objectRef->child === $payload->arrayRef['child'] ? 'same' : 'copy'), ',', + ($payload->holder->alias === $payload->objectRef ? 'same' : 'copy'), ',', + ($payload->holder->child === $payload->objectRef->child ? 'same' : 'copy'), ',', + ($payload->holder->alias->child === $payload->objectRef->child ? 'same' : 'copy'), + "\n"; +} + +$action = $_GET['action'] ?? 'read'; +$kind = $_GET['kind'] ?? 'class'; + +if ($action === 'reset') { + OPcache\volatile_clear(); + opcache_reset(); + echo "reset\n"; + return; +} + +if ($action === 'seed') { + tracking_reference_seed($kind); + tracking_reference_dump($kind, 'seed'); + return; +} + +if ($action === 'mutate') { + tracking_reference_mutate($kind); + tracking_reference_dump($kind, 'mutate'); + return; +} + +tracking_reference_dump($kind, 'read'); +EOT; + +$args = []; +if (file_exists(ini_get('extension_dir') . '/opcache.so')) { + $args[] = '-dzend_extension=' . ini_get('extension_dir') . '/opcache.so'; +} +$args = [ + ...$args, + '-dopcache.enable=1', + '-dopcache.static_cache.volatile_size_mb=32', + '-dopcache.file_update_protection=0', + '-dopcache.jit=0', +]; + +$tester = new FPM\Tester($cfg, $code); +$tester->start($args); +$tester->expectLogStartNotices(); + +function tracking_reference_fpm_request(FPM\Tester $tester, string $query): void +{ + echo $tester->request($query)->getBody(), "\n"; +} + +tracking_reference_fpm_request($tester, 'action=reset'); +foreach (['class', 'property', 'method'] as $kind) { + tracking_reference_fpm_request($tester, 'action=seed&kind=' . $kind); + tracking_reference_fpm_request($tester, 'action=read&kind=' . $kind); + tracking_reference_fpm_request($tester, 'action=mutate&kind=' . $kind); + tracking_reference_fpm_request($tester, 'action=read&kind=' . $kind); +} + +$tester->terminate(); +$tester->close(); +?> +--EXPECT-- +reset +class@seed=seed-updated,2,child-updated,2,2,same,same,same,same,same +class@read=seed-updated,2,child-updated,2,2,same,same,same,same,same +class@mutate=mutated-again,4,child-mutated-again,4,3,same,same,same,same,same +class@read=mutated-again,4,child-mutated-again,4,3,same,same,same,same,same +property@seed=seed-updated,2,child-updated,2,2,same,same,same,same,same +property@read=seed-updated,2,child-updated,2,2,same,same,same,same,same +property@mutate=mutated-again,4,child-mutated-again,4,3,same,same,same,same,same +property@read=mutated-again,4,child-mutated-again,4,3,same,same,same,same,same +method@seed=seed-updated,2,child-updated,2,2,same,same,same,same,same +method@read=seed-updated,2,child-updated,2,2,same,same,same,same,same +method@mutate=mutated-again,4,child-mutated-again,4,3,same,same,same,same,same +method@read=mutated-again,4,child-mutated-again,4,3,same,same,same,same,same +--CLEAN-- + diff --git a/ext/opcache/tests/fpm/static_cache_fpm_volatile_static_tracking_shared_reference_cell_001.phpt b/ext/opcache/tests/fpm/static_cache_fpm_volatile_static_tracking_shared_reference_cell_001.phpt new file mode 100644 index 000000000000..86c24f6c355f --- /dev/null +++ b/ext/opcache/tests/fpm/static_cache_fpm_volatile_static_tracking_shared_reference_cell_001.phpt @@ -0,0 +1,323 @@ +--TEST-- +OPcache VolatileStatic tracking publishes shared reference-cell mutations before restore and detaches roots after restore under FPM +--EXTENSIONS-- +opcache +--SKIPIF-- + +--FILE-- +holder = new stdClass(); + } +} + +#[OPcache\VolatileStatic(strategy: OPcache\CacheStrategy::Tracking)] +class TrackingSharedReferenceClassA +{ + public static ?TrackingSharedReferencePayload $payload = null; +} + +#[OPcache\VolatileStatic(strategy: OPcache\CacheStrategy::Tracking)] +class TrackingSharedReferenceClassB +{ + public static ?TrackingSharedReferencePayload $payload = null; +} + +class TrackingSharedReferencePropertyA +{ + #[OPcache\VolatileStatic(strategy: OPcache\CacheStrategy::Tracking)] + public static ?TrackingSharedReferencePayload $payload = null; +} + +class TrackingSharedReferencePropertyB +{ + #[OPcache\VolatileStatic(strategy: OPcache\CacheStrategy::Tracking)] + public static ?TrackingSharedReferencePayload $payload = null; +} + +class TrackingSharedReferenceMethodA +{ + #[OPcache\VolatileStatic(strategy: OPcache\CacheStrategy::Tracking)] + public static function payload(?TrackingSharedReferencePayload $payload = null): ?TrackingSharedReferencePayload + { + static $value = null; + + if ($payload !== null) { + $value = $payload; + } + + return $value; + } +} + +class TrackingSharedReferenceMethodB +{ + #[OPcache\VolatileStatic(strategy: OPcache\CacheStrategy::Tracking)] + public static function payload(?TrackingSharedReferencePayload $payload = null): ?TrackingSharedReferencePayload + { + static $value = null; + + if ($payload !== null) { + $value = $payload; + } + + return $value; + } +} + +#[OPcache\VolatileStatic(strategy: OPcache\CacheStrategy::Tracking)] +class TrackingSharedReferenceBlob +{ + public static ?TrackingSharedReferencePayload $first = null; + public static ?TrackingSharedReferencePayload $second = null; +} + +function tracking_shared_reference_make_payload(string $label, TrackingSharedReferenceNode &$shared): TrackingSharedReferencePayload +{ + $payload = new TrackingSharedReferencePayload($label); + $payload->nodeRef =& $shared; + $payload->refs['node'] =& $shared; + $payload->refs['child'] =& $shared->child; + $payload->holder->node =& $shared; + $payload->holder->child =& $shared->child; + $payload->labels[] = $label; + + return $payload; +} + +function tracking_shared_reference_pair(string $kind): array +{ + return match ($kind) { + 'class' => [TrackingSharedReferenceClassA::$payload, TrackingSharedReferenceClassB::$payload], + 'property' => [TrackingSharedReferencePropertyA::$payload, TrackingSharedReferencePropertyB::$payload], + 'method' => [TrackingSharedReferenceMethodA::payload(), TrackingSharedReferenceMethodB::payload()], + 'blob' => [TrackingSharedReferenceBlob::$first, TrackingSharedReferenceBlob::$second], + default => throw new RuntimeException('unknown kind'), + }; +} + +function tracking_shared_reference_seed(string $kind): void +{ + $shared = new TrackingSharedReferenceNode('seed'); + $shared->child = new TrackingSharedReferenceChild('child'); + + $first = tracking_shared_reference_make_payload($kind . '-A', $shared); + $second = tracking_shared_reference_make_payload($kind . '-B', $shared); + + match ($kind) { + 'class' => [TrackingSharedReferenceClassA::$payload = $first, TrackingSharedReferenceClassB::$payload = $second], + 'property' => [TrackingSharedReferencePropertyA::$payload = $first, TrackingSharedReferencePropertyB::$payload = $second], + 'method' => [TrackingSharedReferenceMethodA::payload($first), TrackingSharedReferenceMethodB::payload($second)], + 'blob' => [TrackingSharedReferenceBlob::$first = $first, TrackingSharedReferenceBlob::$second = $second], + default => throw new RuntimeException('unknown kind'), + }; + + $first->nodeRef->name = 'seed-updated'; + $first->nodeRef->events[] = 'seed-node'; + $first->refs['child']->name = 'child-updated'; + $first->holder->child->events[] = 'seed-child'; + $first->labels[] = 'seed-mutated'; + $second->labels[] = 'seed-observed'; +} + +function tracking_shared_reference_mutate(string $kind): void +{ + [$first, $second] = tracking_shared_reference_pair($kind); + if (!$first instanceof TrackingSharedReferencePayload || !$second instanceof TrackingSharedReferencePayload) { + throw new RuntimeException('missing payload'); + } + + $first->holder->node->name = 'mutated-again'; + $first->holder->node->events[] = 'mutate-node'; + $first->nodeRef->child->name = 'child-mutated-again'; + $first->nodeRef->child->events[] = 'mutate-child'; + $first->labels[] = 'mutated-first'; +} + +function tracking_shared_reference_dump_payload(string $kind, int $index, ?TrackingSharedReferencePayload $payload): void +{ + if (!$payload instanceof TrackingSharedReferencePayload) { + echo $kind, '#', $index, '=missing', "\n"; + return; + } + + echo $kind, '#', $index, '=', + $payload->label, ',', + $payload->nodeRef->name, ',', + count($payload->nodeRef->events), ',', + $payload->nodeRef->child->name, ',', + count($payload->nodeRef->child->events), ',', + count($payload->labels), ',', + ($payload->nodeRef === $payload->refs['node'] ? 'same' : 'copy'), ',', + ($payload->nodeRef === $payload->holder->node ? 'same' : 'copy'), ',', + ($payload->nodeRef->child === $payload->refs['child'] ? 'same' : 'copy'), ',', + ($payload->nodeRef->child === $payload->holder->child ? 'same' : 'copy'), + "\n"; +} + +function tracking_shared_reference_dump(string $kind): void +{ + [$first, $second] = tracking_shared_reference_pair($kind); + echo $kind, '@cross=', + ($first instanceof TrackingSharedReferencePayload && $second instanceof TrackingSharedReferencePayload && $first->nodeRef === $second->nodeRef ? 'same' : 'copy'), ',', + ($first instanceof TrackingSharedReferencePayload && $second instanceof TrackingSharedReferencePayload && $first->nodeRef->child === $second->nodeRef->child ? 'same' : 'copy'), + "\n"; + tracking_shared_reference_dump_payload($kind, 0, $first); + tracking_shared_reference_dump_payload($kind, 1, $second); +} + +$action = $_GET['action'] ?? 'read'; +$kind = $_GET['kind'] ?? 'class'; + +if ($action === 'reset') { + OPcache\volatile_clear(); + opcache_reset(); + echo "reset\n"; + return; +} + +if ($action === 'seed') { + tracking_shared_reference_seed($kind); + tracking_shared_reference_dump($kind); + return; +} + +if ($action === 'mutate') { + tracking_shared_reference_mutate($kind); + tracking_shared_reference_dump($kind); + return; +} + +tracking_shared_reference_dump($kind); +EOT; + +$args = []; +if (file_exists(ini_get('extension_dir') . '/opcache.so')) { + $args[] = '-dzend_extension=' . ini_get('extension_dir') . '/opcache.so'; +} +$args = [ + ...$args, + '-dopcache.enable=1', + '-dopcache.static_cache.volatile_size_mb=32', + '-dopcache.file_update_protection=0', + '-dopcache.jit=0', +]; + +$tester = new FPM\Tester($cfg, $code); +$tester->start($args); +$tester->expectLogStartNotices(); + +function tracking_shared_reference_fpm_request(FPM\Tester $tester, string $query): void +{ + echo $tester->request($query)->getBody(), "\n"; +} + +tracking_shared_reference_fpm_request($tester, 'action=reset'); +foreach (['class', 'property', 'method', 'blob'] as $kind) { + tracking_shared_reference_fpm_request($tester, 'action=seed&kind=' . $kind); + tracking_shared_reference_fpm_request($tester, 'action=read&kind=' . $kind); + tracking_shared_reference_fpm_request($tester, 'action=mutate&kind=' . $kind); + tracking_shared_reference_fpm_request($tester, 'action=read&kind=' . $kind); +} + +$tester->terminate(); +$tester->close(); +?> +--EXPECT-- +reset +class@cross=same,same +class#0=class-A,seed-updated,1,child-updated,1,2,same,same,same,same +class#1=class-B,seed-updated,1,child-updated,1,2,same,same,same,same +class@cross=copy,copy +class#0=class-A,seed-updated,1,child-updated,1,2,same,same,same,same +class#1=class-B,seed-updated,1,child-updated,1,2,same,same,same,same +class@cross=copy,copy +class#0=class-A,mutated-again,2,child-mutated-again,2,3,same,same,same,same +class#1=class-B,seed-updated,1,child-updated,1,2,same,same,same,same +class@cross=copy,copy +class#0=class-A,mutated-again,2,child-mutated-again,2,3,same,same,same,same +class#1=class-B,seed-updated,1,child-updated,1,2,same,same,same,same +property@cross=same,same +property#0=property-A,seed-updated,1,child-updated,1,2,same,same,same,same +property#1=property-B,seed-updated,1,child-updated,1,2,same,same,same,same +property@cross=copy,copy +property#0=property-A,seed-updated,1,child-updated,1,2,same,same,same,same +property#1=property-B,seed-updated,1,child-updated,1,2,same,same,same,same +property@cross=copy,copy +property#0=property-A,mutated-again,2,child-mutated-again,2,3,same,same,same,same +property#1=property-B,seed-updated,1,child-updated,1,2,same,same,same,same +property@cross=copy,copy +property#0=property-A,mutated-again,2,child-mutated-again,2,3,same,same,same,same +property#1=property-B,seed-updated,1,child-updated,1,2,same,same,same,same +method@cross=same,same +method#0=method-A,seed-updated,1,child-updated,1,2,same,same,same,same +method#1=method-B,seed-updated,1,child-updated,1,2,same,same,same,same +method@cross=copy,copy +method#0=method-A,seed-updated,1,child-updated,1,2,same,same,same,same +method#1=method-B,seed-updated,1,child-updated,1,2,same,same,same,same +method@cross=copy,copy +method#0=method-A,mutated-again,2,child-mutated-again,2,3,same,same,same,same +method#1=method-B,seed-updated,1,child-updated,1,2,same,same,same,same +method@cross=copy,copy +method#0=method-A,mutated-again,2,child-mutated-again,2,3,same,same,same,same +method#1=method-B,seed-updated,1,child-updated,1,2,same,same,same,same +blob@cross=same,same +blob#0=blob-A,seed-updated,1,child-updated,1,2,same,same,same,same +blob#1=blob-B,seed-updated,1,child-updated,1,2,same,same,same,same +blob@cross=copy,copy +blob#0=blob-A,seed-updated,1,child-updated,1,2,same,same,same,same +blob#1=blob-B,seed-updated,1,child-updated,1,2,same,same,same,same +blob@cross=copy,copy +blob#0=blob-A,mutated-again,2,child-mutated-again,2,3,same,same,same,same +blob#1=blob-B,seed-updated,1,child-updated,1,2,same,same,same,same +blob@cross=copy,copy +blob#0=blob-A,mutated-again,2,child-mutated-again,2,3,same,same,same,same +blob#1=blob-B,seed-updated,1,child-updated,1,2,same,same,same,same +--CLEAN-- + diff --git a/ext/opcache/tests/fpm/tester.inc b/ext/opcache/tests/fpm/tester.inc new file mode 100644 index 000000000000..32536a265d2b --- /dev/null +++ b/ext/opcache/tests/fpm/tester.inc @@ -0,0 +1,4 @@ +. | + | | + | SPDX-License-Identifier: BSD-3-Clause | + +----------------------------------------------------------------------+ +*/ + +#ifdef HAVE_CONFIG_H +# include "php_config.h" +#endif + +#include +#include +#include +#include + +#include "Zend/zend_exceptions.h" +#include "Zend/zend_execute.h" +#include "Zend/zend_portability.h" +#include "sapi/embed/php_embed.h" + +#ifndef ZTS +# error "This helper requires a ZTS build" +#endif + +typedef enum { + THREAD_MODE_SEED, + THREAD_MODE_DELETE +} zend_opcache_thread_mode; + +typedef struct _zend_opcache_thread_ctx { + zend_opcache_thread_mode mode; + int result; + char message[128]; +} zend_opcache_thread_ctx; + +static const char opcache_test_ini[] = + "html_errors=0\n" + "implicit_flush=1\n" + "output_buffering=0\n" + "max_execution_time=0\n" + "max_input_time=-1\n" + "opcache.enable=1\n" + "opcache.enable_cli=1\n" + "opcache.memory_consumption=64\n" + "opcache.max_accelerated_files=200\n" + "opcache.static_cache.volatile_size_mb=8\n" + "opcache.static_cache.persistent_size_mb=8\n\0"; + +static const char init_code[] = + "(static function (): bool {" + " OPcache\\volatile_clear();" + " OPcache\\persistent_clear();" + " return true;" + "})()"; + +static const char seed_code[] = + "(static function (): bool {" + " if (!OPcache\\volatile_store('zts_v_first', str_repeat('A', 1800000))) return false;" + " if (!OPcache\\volatile_store('zts_v_second', str_repeat('B', 1800000))) return false;" + " if (!OPcache\\volatile_store('zts_v_third', str_repeat('C', 1800000))) return false;" + " OPcache\\persistent_store('zts_p_first', str_repeat('A', 1800000));" + " OPcache\\persistent_store('zts_p_second', str_repeat('B', 1800000));" + " OPcache\\persistent_store('zts_p_third', str_repeat('C', 1800000));" + " return true;" + "})()"; + +static const char delete_code[] = + "(static function (): bool {" + " OPcache\\volatile_delete('zts_v_second');" + " OPcache\\persistent_delete('zts_p_second');" + " return OPcache\\volatile_fetch('zts_v_second', 'missing') === 'missing'" + " && OPcache\\persistent_fetch('zts_p_second', 'missing') === 'missing';" + "})()"; + +static const char refill_code[] = + "(static function (): bool {" + " if (!OPcache\\volatile_store('zts_v_replacement', str_repeat('R', 1500000))) return false;" + " OPcache\\persistent_store('zts_p_replacement', str_repeat('R', 1500000));" + " return strlen(OPcache\\volatile_fetch('zts_v_first')) === 1800000" + " && strlen(OPcache\\volatile_fetch('zts_v_third')) === 1800000" + " && strlen(OPcache\\volatile_fetch('zts_v_replacement')) === 1500000" + " && strlen(OPcache\\persistent_fetch('zts_p_first')) === 1800000" + " && strlen(OPcache\\persistent_fetch('zts_p_third')) === 1800000" + " && strlen(OPcache\\persistent_fetch('zts_p_replacement')) === 1500000;" + "})()"; + +static void zend_opcache_thread_set_failure(zend_opcache_thread_ctx *ctx, const char *message) +{ + ctx->result = 1; + snprintf(ctx->message, sizeof(ctx->message), "%s", message); +} + +static int zend_opcache_test_startup(int argc, char **argv) +{ + if (!php_tsrm_startup_ex(4)) { + return FAILURE; + } + ZEND_TSRMLS_CACHE_UPDATE(); + + zend_signal_startup(); + sapi_startup(&php_embed_module); + php_embed_module.ini_entries = opcache_test_ini; + if (argv != NULL) { + php_embed_module.executable_location = argv[0]; + } + + if (php_embed_module.startup(&php_embed_module) == FAILURE) { + sapi_shutdown(); + tsrm_shutdown(); + return FAILURE; + } + + SG(options) |= SAPI_OPTION_NO_CHDIR; + return SUCCESS; +} + +static void zend_opcache_test_shutdown(void) +{ + php_module_shutdown(); + sapi_shutdown(); + tsrm_shutdown(); +} + +static bool zend_opcache_thread_request_startup(void) +{ + (void) ts_resource(0); + ZEND_TSRMLS_CACHE_UPDATE(); + + SG(request_info).argc = 0; + SG(request_info).argv = NULL; + SG(request_info).query_string = NULL; + SG(request_info).request_uri = "-"; + SG(request_info).path_translated = NULL; + SG(request_info).request_method = "GET"; + SG(request_info).no_headers = 1; + + if (php_request_startup() == FAILURE) { + return false; + } + + SG(headers_sent) = 1; + SG(request_info).no_headers = 1; + php_register_variable("PHP_SELF", "-", NULL); + return true; +} + +static bool zend_opcache_thread_eval(const char *code, zval *retval, const char *label) +{ + ZVAL_UNDEF(retval); + if (zend_eval_stringl_ex(code, strlen(code), retval, label, true) == FAILURE) { + if (EG(exception)) { + zend_clear_exception(); + } + return false; + } + + if (EG(exception)) { + zend_clear_exception(); + if (!Z_ISUNDEF_P(retval)) { + zval_ptr_dtor(retval); + ZVAL_UNDEF(retval); + } + return false; + } + + return true; +} + +static bool zend_opcache_thread_run_request_code(const char *code, const char *label, char *message, size_t message_size) +{ + zval retval; + + if (!zend_opcache_thread_request_startup()) { + snprintf(message, message_size, "%s: request startup failed", label); + return false; + } + + if (!zend_opcache_thread_eval(code, &retval, label)) { + snprintf(message, message_size, "%s: eval failed", label); + php_request_shutdown(NULL); + return false; + } + + if (!zend_is_true(&retval)) { + snprintf(message, message_size, "%s: returned false", label); + if (!Z_ISUNDEF(retval)) { + zval_ptr_dtor(&retval); + } + php_request_shutdown(NULL); + return false; + } + + if (!Z_ISUNDEF(retval)) { + zval_ptr_dtor(&retval); + } + php_request_shutdown(NULL); + return true; +} + +static void *zend_opcache_thread_main(void *arg) +{ + zend_opcache_thread_ctx *ctx = (zend_opcache_thread_ctx *) arg; + const char *code = ctx->mode == THREAD_MODE_SEED ? seed_code : delete_code; + const char *label = ctx->mode == THREAD_MODE_SEED ? "zts explicit cache seed" : "zts explicit cache delete"; + + ctx->result = 0; + ctx->message[0] = '\0'; + if (!zend_opcache_thread_run_request_code(code, label, ctx->message, sizeof(ctx->message))) { + ctx->result = 1; + } + + ts_free_thread(); + return NULL; +} + +static bool zend_opcache_run_thread(zend_opcache_thread_mode mode, char *message, size_t message_size) +{ + zend_thread_t thread; + zend_opcache_thread_ctx ctx; + + ctx.mode = mode; + if (!zend_thread_start(&thread, zend_opcache_thread_main, &ctx)) { + snprintf(message, message_size, "thread creation failed"); + return false; + } + if (!zend_thread_join(&thread)) { + snprintf(message, message_size, "thread join failed"); + return false; + } + if (ctx.result != 0) { + snprintf(message, message_size, "%s", ctx.message); + return false; + } + + return true; +} + +int main(int argc, char **argv) +{ + char message[128]; + int exit_code; + + if (zend_opcache_test_startup(argc, argv) == FAILURE) { + fprintf(stderr, "startup failed\n"); + return 1; + } + + exit_code = 1; + message[0] = '\0'; + + if (!zend_opcache_thread_run_request_code(init_code, "zts explicit cache init", message, sizeof(message))) { + fprintf(stderr, "%s\n", message); + goto cleanup; + } + + if (!zend_opcache_run_thread(THREAD_MODE_SEED, message, sizeof(message))) { + fprintf(stderr, "%s\n", message); + goto cleanup; + } + + if (!zend_opcache_run_thread(THREAD_MODE_DELETE, message, sizeof(message))) { + fprintf(stderr, "%s\n", message); + goto cleanup; + } + + if (!zend_opcache_thread_run_request_code(refill_code, "zts explicit cache refill", message, sizeof(message))) { + fprintf(stderr, "%s\n", message); + goto cleanup; + } + + printf("ok\n"); + exit_code = 0; + +cleanup: + zend_opcache_test_shutdown(); + return exit_code; +} diff --git a/ext/opcache/tests/helpers/persistent_static_zts_threads_001.c b/ext/opcache/tests/helpers/persistent_static_zts_threads_001.c new file mode 100644 index 000000000000..0774b0ce6962 --- /dev/null +++ b/ext/opcache/tests/helpers/persistent_static_zts_threads_001.c @@ -0,0 +1,304 @@ +#ifdef HAVE_CONFIG_H +# include "php_config.h" +#endif + +#include +#include +#include +#include + +#include "Zend/zend_exceptions.h" +#include "Zend/zend_execute.h" +#include "Zend/zend_portability.h" +#include "sapi/embed/php_embed.h" + +#ifndef ZTS +# error "This helper requires a ZTS build" +#endif + +typedef struct _zend_opcache_thread_ctx { + const char *mode; + const char *scenario_path; + const char *label; + int result; + char message[256]; +} zend_opcache_thread_ctx; + +static const char opcache_test_ini[] = + "html_errors=0\n" + "implicit_flush=1\n" + "output_buffering=0\n" + "max_execution_time=0\n" + "max_input_time=-1\n" + "opcache.enable=1\n" + "opcache.enable_cli=1\n" + "opcache.memory_consumption=64\n" + "opcache.max_accelerated_files=200\n" + "opcache.static_cache.persistent_size_mb=32\n\0"; + +static void zend_opcache_thread_set_failure(zend_opcache_thread_ctx *ctx, const char *message) +{ + ctx->result = 1; + snprintf(ctx->message, sizeof(ctx->message), "%s", message); +} + +static int zend_opcache_test_startup(int argc, char **argv) +{ + if (!php_tsrm_startup_ex(8)) { + return FAILURE; + } + ZEND_TSRMLS_CACHE_UPDATE(); + + zend_signal_startup(); + sapi_startup(&php_embed_module); + php_embed_module.ini_entries = opcache_test_ini; + if (argv != NULL) { + php_embed_module.executable_location = argv[0]; + } + + if (php_embed_module.startup(&php_embed_module) == FAILURE) { + sapi_shutdown(); + tsrm_shutdown(); + return FAILURE; + } + + SG(options) |= SAPI_OPTION_NO_CHDIR; + return SUCCESS; +} + +static void zend_opcache_test_shutdown(void) +{ + php_module_shutdown(); + sapi_shutdown(); + tsrm_shutdown(); +} + +static bool zend_opcache_thread_request_startup(void) +{ + (void) ts_resource(0); + ZEND_TSRMLS_CACHE_UPDATE(); + + SG(request_info).argc = 0; + SG(request_info).argv = NULL; + SG(request_info).query_string = NULL; + SG(request_info).request_uri = "-"; + SG(request_info).path_translated = NULL; + SG(request_info).request_method = "GET"; + SG(request_info).no_headers = 1; + + if (php_request_startup() == FAILURE) { + return false; + } + + SG(headers_sent) = 1; + SG(request_info).no_headers = 1; + php_register_variable("PHP_SELF", "-", NULL); + return true; +} + +static bool zend_opcache_thread_eval(const char *code, zval *retval, const char *label) +{ + ZVAL_UNDEF(retval); + if (zend_eval_stringl_ex(code, strlen(code), retval, label, true) == FAILURE) { + if (EG(exception)) { + zend_clear_exception(); + } + return false; + } + + if (EG(exception)) { + zend_clear_exception(); + if (!Z_ISUNDEF_P(retval)) { + zval_ptr_dtor(retval); + ZVAL_UNDEF(retval); + } + return false; + } + + return true; +} + +static bool zend_opcache_thread_run_request_code(const char *code, const char *label, char *message, size_t message_size) +{ + zval retval; + + if (!zend_opcache_thread_request_startup()) { + snprintf(message, message_size, "%s: request startup failed", label); + return false; + } + + if (!zend_opcache_thread_eval(code, &retval, label)) { + snprintf(message, message_size, "%s: eval failed", label); + php_request_shutdown(NULL); + return false; + } + + if (!zend_is_true(&retval)) { + snprintf(message, message_size, "%s: returned false", label); + if (!Z_ISUNDEF(retval)) { + zval_ptr_dtor(&retval); + } + php_request_shutdown(NULL); + return false; + } + + if (!Z_ISUNDEF(retval)) { + zval_ptr_dtor(&retval); + } + php_request_shutdown(NULL); + return true; +} + +static char *zend_opcache_build_scenario_code(const char *mode, const char *scenario_path) +{ + char *code; + size_t code_len; + + code_len = strlen(mode) + strlen(scenario_path) + sizeof("$mode=''; return require '';\0"); + code = malloc(code_len); + if (code == NULL) { + return NULL; + } + + snprintf(code, code_len, "$mode='%s'; return require '%s';", mode, scenario_path); + return code; +} + +static bool zend_opcache_run_request_mode(const char *scenario_path, const char *mode, const char *label, char *message, size_t message_size) +{ + char *code; + bool result; + + code = zend_opcache_build_scenario_code(mode, scenario_path); + if (code == NULL) { + snprintf(message, message_size, "%s: failed to build scenario code", label); + return false; + } + + result = zend_opcache_thread_run_request_code(code, label, message, message_size); + free(code); + return result; +} + +static bool zend_opcache_write_done_file(const char *scenario_path) +{ + char *done_path; + size_t done_path_len; + FILE *fp; + + done_path_len = strlen(scenario_path) + sizeof(".done"); + done_path = malloc(done_path_len); + if (done_path == NULL) { + return false; + } + snprintf(done_path, done_path_len, "%s.done", scenario_path); + + fp = fopen(done_path, "wb"); + free(done_path); + if (fp == NULL) { + return false; + } + fputs("done", fp); + fclose(fp); + return true; +} + +static void *zend_opcache_thread_main(void *arg) +{ + zend_opcache_thread_ctx *ctx; + + ctx = (zend_opcache_thread_ctx *) arg; + ctx->result = 0; + ctx->message[0] = '\0'; + + if (!zend_opcache_run_request_mode(ctx->scenario_path, ctx->mode, ctx->label, ctx->message, sizeof(ctx->message))) { + ctx->result = 1; + ts_free_thread(); + return NULL; + } + + if (strcmp(ctx->mode, "writer") == 0 && !zend_opcache_write_done_file(ctx->scenario_path)) { + zend_opcache_thread_set_failure(ctx, "writer failed to signal completion"); + } + + ts_free_thread(); + return NULL; +} + +static bool zend_opcache_run_threaded_scenario(const char *scenario_path, char *message, size_t message_size) +{ + zend_thread_t reader_thread; + zend_thread_t writer_thread; + zend_opcache_thread_ctx reader_ctx; + zend_opcache_thread_ctx writer_ctx; + + if (!zend_opcache_run_request_mode(scenario_path, "init", "zts persistent_static init", message, message_size)) { + return false; + } + + reader_ctx.mode = "reader"; + reader_ctx.scenario_path = scenario_path; + reader_ctx.label = "zts persistent_static reader"; + if (!zend_thread_start(&reader_thread, zend_opcache_thread_main, &reader_ctx)) { + snprintf(message, message_size, "reader thread creation failed"); + return false; + } + + writer_ctx.mode = "writer"; + writer_ctx.scenario_path = scenario_path; + writer_ctx.label = "zts persistent_static writer"; + if (!zend_thread_start(&writer_thread, zend_opcache_thread_main, &writer_ctx)) { + snprintf(message, message_size, "writer thread creation failed"); + return false; + } + + if (!zend_thread_join(&writer_thread)) { + snprintf(message, message_size, "writer thread join failed"); + return false; + } + if (!zend_thread_join(&reader_thread)) { + snprintf(message, message_size, "reader thread join failed"); + return false; + } + if (writer_ctx.result != 0) { + snprintf(message, message_size, "%s", writer_ctx.message); + return false; + } + if (reader_ctx.result != 0) { + snprintf(message, message_size, "%s", reader_ctx.message); + return false; + } + + return zend_opcache_run_request_mode(scenario_path, "verify", "zts persistent_static verify", message, message_size); +} + +int main(int argc, char **argv) +{ + char message[256]; + int exit_code; + + if (argc < 2) { + fprintf(stderr, "scenario path required\n"); + return 1; + } + + if (zend_opcache_test_startup(argc, argv) == FAILURE) { + fprintf(stderr, "startup failed\n"); + return 1; + } + + exit_code = 1; + message[0] = '\0'; + + if (!zend_opcache_run_threaded_scenario(argv[1], message, sizeof(message))) { + fprintf(stderr, "%s\n", message); + goto cleanup; + } + + printf("ok\n"); + exit_code = 0; + +cleanup: + zend_opcache_test_shutdown(); + return exit_code; +} diff --git a/ext/opcache/tests/helpers/persistent_static_zts_threads_001.inc b/ext/opcache/tests/helpers/persistent_static_zts_threads_001.inc new file mode 100644 index 000000000000..ce2c0b52beff --- /dev/null +++ b/ext/opcache/tests/helpers/persistent_static_zts_threads_001.inc @@ -0,0 +1,100 @@ + 0, 'numbers' => []]; + + public static function next(): int + { + static $value = 0; + + return ++$value; + } +} + +class ZtsRacePropertyState +{ + #[OPcache\PersistentStatic] + public static array $data = ['value' => 0, 'numbers' => []]; +} + +class ZtsRaceMethodState +{ + #[OPcache\PersistentStatic] + public static function next(): int + { + static $value = 0; + + return ++$value; + } +} + +function zts_persistent_static_wait_for_file(string $path): bool +{ + $deadline = microtime(true) + 5.0; + while (!file_exists($path)) { + if (microtime(true) >= $deadline) { + return false; + } + usleep(1000); + } + return true; +} + +function zts_persistent_static_tuple(): string +{ + return ZtsRaceClassState::$data['value'] . ',' + . array_sum(ZtsRaceClassState::$data['numbers']) . ',' + . ZtsRaceClassState::next() . ',' + . ZtsRacePropertyState::$data['value'] . ',' + . array_sum(ZtsRacePropertyState::$data['numbers']) . ',' + . ZtsRaceMethodState::next(); +} + +$readyFile = __FILE__ . '.ready'; +$doneFile = __FILE__ . '.done'; + +if ($mode === 'init') { + @unlink($readyFile); + @unlink($doneFile); + opcache_reset(); + ZtsRaceClassState::$data = ['value' => 1, 'numbers' => [1]]; + ZtsRaceClassState::next(); + ZtsRacePropertyState::$data = ['value' => 1, 'numbers' => [1]]; + ZtsRaceMethodState::next(); + return true; +} + +if ($mode === 'writer') { + if (!zts_persistent_static_wait_for_file($readyFile)) { + return false; + } + $classMethod = ZtsRaceClassState::next(); + ZtsRaceClassState::$data['value'] = 10; + ZtsRaceClassState::$data['numbers'][] = 20; + ZtsRacePropertyState::$data['value'] = 10; + ZtsRacePropertyState::$data['numbers'][] = 20; + $method = ZtsRaceMethodState::next(); + return $classMethod === 2 + && $method === 2 + && ZtsRaceClassState::$data['value'] === 10 + && array_sum(ZtsRaceClassState::$data['numbers']) === 21 + && ZtsRacePropertyState::$data['value'] === 10 + && array_sum(ZtsRacePropertyState::$data['numbers']) === 21; +} + +if ($mode === 'reader') { + file_put_contents($readyFile, 'ready'); + if (!zts_persistent_static_wait_for_file($doneFile)) { + return false; + } + return zts_persistent_static_tuple() === '10,21,3,1,1,3'; +} + +if ($mode === 'verify') { + @unlink($readyFile); + @unlink($doneFile); + return zts_persistent_static_tuple() === '10,21,4,1,1,4'; +} + +return false; diff --git a/ext/opcache/tests/helpers/volatile_cache_process_lock_mode_001.c b/ext/opcache/tests/helpers/volatile_cache_process_lock_mode_001.c new file mode 100644 index 000000000000..ca0ad94e69a7 --- /dev/null +++ b/ext/opcache/tests/helpers/volatile_cache_process_lock_mode_001.c @@ -0,0 +1,300 @@ +#ifdef HAVE_CONFIG_H +# include "php_config.h" +# undef HAVE_CONFIG_H +#endif + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#ifdef HAVE_UNISTD_H +# include +#endif + +#include "sapi/embed/php_embed.h" +#include "zend_static_cache_internal.h" + +#ifdef ZEND_WIN32 +# error "This helper requires the POSIX static cache lock implementation" +#endif + +#define READER_HOLD_USEC 100000 +#define WRITER_EARLY_CHECK_USEC 20000 +#define WRITER_MIN_BLOCK_USEC 40000 + +static const char opcache_test_ini[] = + "html_errors=0\n" + "implicit_flush=1\n" + "output_buffering=0\n" + "max_execution_time=0\n" + "max_input_time=-1\n" + "opcache.enable=1\n" + "opcache.enable_cli=1\n" + "opcache.memory_consumption=64\n" + "opcache.max_accelerated_files=200\n" + "opcache.static_cache.volatile_size_mb=32\n\0"; + +static int zend_opcache_test_startup(int argc, char **argv) +{ +#ifdef ZTS + if (!php_tsrm_startup_ex(2)) { + return FAILURE; + } + ZEND_TSRMLS_CACHE_UPDATE(); +#endif + + zend_signal_startup(); + sapi_startup(&php_embed_module); + php_embed_module.ini_entries = opcache_test_ini; + if (argv != NULL) { + php_embed_module.executable_location = argv[0]; + } + + if (php_embed_module.startup(&php_embed_module) == FAILURE) { + sapi_shutdown(); +#ifdef ZTS + tsrm_shutdown(); +#endif + return FAILURE; + } + + SG(options) |= SAPI_OPTION_NO_CHDIR; + + return SUCCESS; +} + +static void zend_opcache_test_shutdown(void) +{ + php_module_shutdown(); + sapi_shutdown(); +#ifdef ZTS + tsrm_shutdown(); +#endif +} + +static void fail_with_message(const char *message) +{ + fprintf(stderr, "%s\n", message); + exit(1); +} + +static bool verify_lock_ready(void) +{ + if (!zend_opcache_static_cache_wlock()) { + return false; + } + + if (!zend_opcache_static_cache_header_init_locked()) { + zend_opcache_static_cache_unlock(); + return false; + } + + zend_opcache_static_cache_unlock(); + + return true; +} + +static bool read_exact(int fd, void *buffer, size_t length) +{ + unsigned char *cursor = (unsigned char *) buffer; + ssize_t bytes_read; + size_t total = 0; + + while (total < length) { + bytes_read = read(fd, cursor + total, length - total); + if (bytes_read <= 0) { + return false; + } + + total += (size_t) bytes_read; + } + + return true; +} + +static void reader_process(int write_fd) +{ + char signal = 'r'; + + if (!zend_opcache_static_cache_rlock()) { + _exit(10); + } + + if (write(write_fd, &signal, sizeof(signal)) != (ssize_t) sizeof(signal)) { + zend_opcache_static_cache_unlock(); + _exit(11); + } + + usleep(READER_HOLD_USEC); + zend_opcache_static_cache_unlock(); + _exit(0); +} + +static void writer_process(int write_fd) +{ + struct timeval start_time, end_time; + uint64_t elapsed_usec; + + if (gettimeofday(&start_time, NULL) != 0) { + _exit(20); + } + + if (!zend_opcache_static_cache_wlock()) { + _exit(21); + } + + if (gettimeofday(&end_time, NULL) != 0) { + zend_opcache_static_cache_unlock(); + _exit(22); + } + + elapsed_usec = (uint64_t) (end_time.tv_sec - start_time.tv_sec) * 1000000ULL; + elapsed_usec += (uint64_t) (end_time.tv_usec - start_time.tv_usec); + + if (write(write_fd, &elapsed_usec, sizeof(elapsed_usec)) != (ssize_t) sizeof(elapsed_usec)) { + zend_opcache_static_cache_unlock(); + _exit(23); + } + + zend_opcache_static_cache_unlock(); + _exit(0); +} + +static void require_child_success(pid_t pid, const char *label) +{ + int status; + + if (waitpid(pid, &status, 0) != pid) { + fail_with_message("waitpid failed"); + } + + if (!WIFEXITED(status) || WEXITSTATUS(status) != 0) { + fprintf(stderr, "%s exited with status %d\n", label, WIFEXITED(status) ? WEXITSTATUS(status) : -1); + exit(1); + } +} + +int main(int argc, char **argv) +{ + int reader_pipe[2], writer_pipe[2], flags; + char reader_signal[2]; + uint64_t writer_elapsed = 0; + ssize_t bytes_read; + pid_t reader_one, reader_two, writer; + + if (zend_opcache_test_startup(argc, argv) == FAILURE) { + fail_with_message("startup failed"); + } + + if (!verify_lock_ready()) { + zend_opcache_test_shutdown(); + fail_with_message("initial lock verification failed"); + } + + if (pipe(reader_pipe) != 0 || pipe(writer_pipe) != 0) { + zend_opcache_test_shutdown(); + fail_with_message("pipe creation failed"); + } + + reader_one = fork(); + if (reader_one == 0) { + close(reader_pipe[0]); + close(writer_pipe[0]); + close(writer_pipe[1]); + reader_process(reader_pipe[1]); + } + if (reader_one < 0) { + zend_opcache_test_shutdown(); + fail_with_message("first reader fork failed"); + } + + reader_two = fork(); + if (reader_two == 0) { + close(reader_pipe[0]); + close(writer_pipe[0]); + close(writer_pipe[1]); + reader_process(reader_pipe[1]); + } + if (reader_two < 0) { + kill(reader_one, SIGTERM); + require_child_success(reader_one, "reader_one"); + zend_opcache_test_shutdown(); + fail_with_message("second reader fork failed"); + } + + close(reader_pipe[1]); + if (!read_exact(reader_pipe[0], reader_signal, sizeof(reader_signal))) { + kill(reader_one, SIGTERM); + kill(reader_two, SIGTERM); + require_child_success(reader_one, "reader_one"); + require_child_success(reader_two, "reader_two"); + zend_opcache_test_shutdown(); + fail_with_message("readers did not acquire the lock"); + } + + writer = fork(); + if (writer == 0) { + close(reader_pipe[0]); + close(writer_pipe[0]); + writer_process(writer_pipe[1]); + } + if (writer < 0) { + kill(reader_one, SIGTERM); + kill(reader_two, SIGTERM); + require_child_success(reader_one, "reader_one"); + require_child_success(reader_two, "reader_two"); + zend_opcache_test_shutdown(); + fail_with_message("writer fork failed"); + } + + close(writer_pipe[1]); + usleep(WRITER_EARLY_CHECK_USEC); + flags = fcntl(writer_pipe[0], F_GETFL, 0); + if (flags >= 0) { + fcntl(writer_pipe[0], F_SETFL, flags | O_NONBLOCK); + } + bytes_read = read(writer_pipe[0], &writer_elapsed, sizeof(writer_elapsed)); + if (bytes_read > 0) { + kill(reader_one, SIGTERM); + kill(reader_two, SIGTERM); + kill(writer, SIGTERM); + require_child_success(reader_one, "reader_one"); + require_child_success(reader_two, "reader_two"); + require_child_success(writer, "writer"); + zend_opcache_test_shutdown(); + fail_with_message("writer acquired the lock before readers released it"); + } + if (flags >= 0) { + fcntl(writer_pipe[0], F_SETFL, flags); + } + + require_child_success(reader_one, "reader_one"); + require_child_success(reader_two, "reader_two"); + if (!read_exact(writer_pipe[0], &writer_elapsed, sizeof(writer_elapsed))) { + kill(writer, SIGTERM); + require_child_success(writer, "writer"); + zend_opcache_test_shutdown(); + fail_with_message("writer did not report timing"); + } + close(reader_pipe[0]); + close(writer_pipe[0]); + require_child_success(writer, "writer"); + + if (writer_elapsed < WRITER_MIN_BLOCK_USEC) { + zend_opcache_test_shutdown(); + fail_with_message("writer was not blocked by active readers"); + } + + printf("ok\n"); + zend_opcache_test_shutdown(); + + return 0; +} diff --git a/ext/opcache/tests/helpers/volatile_cache_retired_shared_graph_free_001.c b/ext/opcache/tests/helpers/volatile_cache_retired_shared_graph_free_001.c new file mode 100644 index 000000000000..960045d2b2da --- /dev/null +++ b/ext/opcache/tests/helpers/volatile_cache_retired_shared_graph_free_001.c @@ -0,0 +1,304 @@ +#ifdef HAVE_CONFIG_H +# include "php_config.h" +# undef HAVE_CONFIG_H +#endif + +#include +#include +#include +#include +#include + +#include "sapi/embed/php_embed.h" +#include "zend_static_cache_internal.h" + +#define TEST_KEY "retired_shared_graph_free_payload" + +static const char opcache_test_ini[] = + "html_errors=0\n" + "implicit_flush=1\n" + "output_buffering=0\n" + "max_execution_time=0\n" + "max_input_time=-1\n" + "opcache.enable=1\n" + "opcache.enable_cli=1\n" + "opcache.memory_consumption=64\n" + "opcache.max_accelerated_files=200\n" + "opcache.static_cache.volatile_size_mb=32\n\0"; + +static const char seed_code[] = + "(static function (): bool {" + " OPcache\\volatile_clear();" + " $payload = ['name' => 'retired-shared-graph-free', 'items' => []];" + " for ($i = 0; $i < 128; $i++) {" + " $payload['items'][] = ['id' => $i, 'path' => '/items/' . $i, 'flags' => [true, false, true]];" + " }" + " return OPcache\\volatile_store('" TEST_KEY "', $payload);" + "})()"; + +static int zend_opcache_test_startup(int argc, char **argv) +{ +#ifdef ZTS + if (!php_tsrm_startup_ex(2)) { + return FAILURE; + } + ZEND_TSRMLS_CACHE_UPDATE(); +#endif + + zend_signal_startup(); + sapi_startup(&php_embed_module); + php_embed_module.ini_entries = opcache_test_ini; + if (argv != NULL) { + php_embed_module.executable_location = argv[0]; + } + + if (php_embed_module.startup(&php_embed_module) == FAILURE) { + sapi_shutdown(); +#ifdef ZTS + tsrm_shutdown(); +#endif + return FAILURE; + } + + SG(options) |= SAPI_OPTION_NO_CHDIR; + + return SUCCESS; +} + +static void zend_opcache_test_shutdown(void) +{ + php_module_shutdown(); + sapi_shutdown(); +#ifdef ZTS + tsrm_shutdown(); +#endif +} + +static void fail_with_message(const char *message) +{ + fprintf(stderr, "%s\n", message); + exit(1); +} + +static bool zend_opcache_test_request_startup(void) +{ +#ifdef ZTS + (void) ts_resource(0); + ZEND_TSRMLS_CACHE_UPDATE(); +#endif + + SG(request_info).argc = 0; + SG(request_info).argv = NULL; + SG(request_info).query_string = NULL; + SG(request_info).request_uri = "-"; + SG(request_info).path_translated = NULL; + SG(request_info).request_method = "GET"; + SG(request_info).no_headers = 1; + + if (php_request_startup() == FAILURE) { + return false; + } + + SG(headers_sent) = 1; + SG(request_info).no_headers = 1; + php_register_variable("PHP_SELF", "-", NULL); + + return true; +} + +static bool zend_opcache_test_eval_bool(const char *code, const char *label) +{ + zval retval; + bool result = false; + + ZVAL_UNDEF(&retval); + if (zend_eval_stringl_ex(code, strlen(code), &retval, label, true) == SUCCESS && !EG(exception)) { + result = zend_is_true(&retval); + } + + if (EG(exception)) { + zend_clear_exception(); + } + if (!Z_ISUNDEF(retval)) { + zval_ptr_dtor(&retval); + } + + return result; +} + +static bool zend_opcache_test_run_request_bool(const char *code, const char *label) +{ + bool result; + + if (!zend_opcache_test_request_startup()) { + return false; + } + + result = zend_opcache_test_eval_bool(code, label); + php_request_shutdown(NULL); + + return result; +} + +static bool zend_opcache_test_locate_payload(uint32_t *payload_offset) +{ + zend_opcache_static_cache_context *previous_context; + zend_opcache_static_cache_header *header; + zend_opcache_static_cache_entry *entries; + zend_string *key; + zend_ulong hash; + uint32_t index; + bool found = false; + + key = zend_string_init(TEST_KEY, sizeof(TEST_KEY) - 1, false); + hash = zend_string_hash_val(key); + previous_context = zend_opcache_static_cache_activate_context(&zend_opcache_static_cache_volatile_context_state); + + if (zend_opcache_static_cache_rlock()) { + if (zend_opcache_static_cache_header_is_initialized_locked()) { + header = zend_opcache_static_cache_header_ptr(); + entries = zend_opcache_static_cache_entries(header); + for (index = 0; index < header->capacity; index++) { + if (zend_opcache_static_cache_key_equals(&entries[index], key, hash)) { + if (entries[index].value_type == ZEND_OPCACHE_STATIC_CACHE_VALUE_SHARED_GRAPH) { + *payload_offset = entries[index].value_offset; + found = true; + } + break; + } + } + } + zend_opcache_static_cache_unlock(); + } + + zend_opcache_static_cache_restore_context(previous_context); + zend_string_release(key); + + return found; +} + +static bool zend_opcache_test_payload_region_is_free_locked(uint32_t payload_offset, bool *is_free) +{ + zend_opcache_static_cache_header *header; + zend_opcache_static_cache_block *block; + uint32_t block_offset, free_offset; + + if (payload_offset < sizeof(zend_opcache_static_cache_block)) { + return false; + } + + header = zend_opcache_static_cache_header_ptr(); + if (header == NULL) { + return false; + } + + block_offset = payload_offset - (uint32_t) sizeof(zend_opcache_static_cache_block); + *is_free = false; + if (block_offset >= header->data_offset + header->next_free) { + *is_free = true; + + return true; + } + + free_offset = header->free_list; + while (free_offset != 0) { + block = zend_opcache_static_cache_block_ptr(free_offset); + if (zend_opcache_static_cache_block_is_free(block) && + block_offset >= free_offset && + block_offset < free_offset + block->size + ) { + *is_free = true; + break; + } + free_offset = block->next_free; + } + + return true; +} + +static bool zend_opcache_test_payload_region_is_free(uint32_t payload_offset, bool *is_free) +{ + zend_opcache_static_cache_context *previous_context; + bool result = false; + + previous_context = zend_opcache_static_cache_activate_context(&zend_opcache_static_cache_volatile_context_state); + if (zend_opcache_static_cache_rlock()) { + if (zend_opcache_static_cache_header_is_initialized_locked()) { + result = zend_opcache_test_payload_region_is_free_locked(payload_offset, is_free); + } + zend_opcache_static_cache_unlock(); + } + zend_opcache_static_cache_restore_context(previous_context); + + return result; +} + +static bool zend_opcache_test_queue_deferred_free(uint32_t payload_offset) +{ + zend_opcache_static_cache_context *previous_context; + bool is_free = false; + bool result = false; + + if (!zend_opcache_test_request_startup()) { + return false; + } + + previous_context = zend_opcache_static_cache_activate_context(&zend_opcache_static_cache_volatile_context_state); + if (zend_opcache_static_cache_rlock()) { + if (zend_opcache_static_cache_header_is_initialized_locked()) { + zend_opcache_static_cache_defer_retired_shared_graph_free(payload_offset); + result = zend_opcache_test_payload_region_is_free_locked(payload_offset, &is_free) && !is_free; + } + zend_opcache_static_cache_unlock(); + } + zend_opcache_static_cache_restore_context(previous_context); + + if (result) { + zend_opcache_static_cache_release_request_shared_graph_refs(); + result = zend_opcache_test_payload_region_is_free(payload_offset, &is_free) && is_free; + } + + php_request_shutdown(NULL); + + return result; +} + +int main(int argc, char **argv) +{ + uint32_t payload_offset = 0; + bool is_free = true; + + if (zend_opcache_test_startup(argc, argv) == FAILURE) { + fail_with_message("startup failed"); + } + + if (!zend_opcache_test_run_request_bool(seed_code, "seed shared graph")) { + zend_opcache_test_shutdown(); + fail_with_message("failed to seed shared graph payload"); + } + + if (!zend_opcache_test_locate_payload(&payload_offset)) { + zend_opcache_test_shutdown(); + fail_with_message("failed to locate shared graph payload"); + } + + if (!zend_opcache_test_payload_region_is_free(payload_offset, &is_free) || is_free) { + zend_opcache_test_shutdown(); + fail_with_message("shared graph payload was unexpectedly free before deferred cleanup"); + } + + if (!zend_opcache_test_queue_deferred_free(payload_offset)) { + zend_opcache_test_shutdown(); + fail_with_message("deferred cleanup did not free payload after the read lock was released"); + } + + if (!zend_opcache_test_payload_region_is_free(payload_offset, &is_free) || !is_free) { + zend_opcache_test_shutdown(); + fail_with_message("deferred cleanup did not leave the payload block free"); + } + + zend_opcache_test_shutdown(); + puts("ok"); + + return 0; +} diff --git a/ext/opcache/tests/helpers/volatile_cache_zts_request_shared_graph_refs_001.c b/ext/opcache/tests/helpers/volatile_cache_zts_request_shared_graph_refs_001.c new file mode 100644 index 000000000000..cacde6cfb164 --- /dev/null +++ b/ext/opcache/tests/helpers/volatile_cache_zts_request_shared_graph_refs_001.c @@ -0,0 +1,476 @@ +#ifdef HAVE_CONFIG_H +# include "php_config.h" +# undef HAVE_CONFIG_H +#endif + +#include +#include +#include + +#include "Zend/zend_atomic.h" +#include "Zend/zend_exceptions.h" +#include "Zend/zend_execute.h" +#include "Zend/zend_portability.h" + +#include "sapi/embed/php_embed.h" +#include "zend_static_cache_internal.h" + +#ifndef ZTS +# error "This helper requires a ZTS build" +#endif + +#define REQUEST_COUNT 4 + +typedef struct _zend_opcache_test_accel_directives { + zend_long memory_consumption; + zend_long static_cache_volatile_size_mb; + zend_long max_accelerated_files; + double max_wasted_percentage; + char *user_blacklist_filename; + zend_long force_restart_timeout; + bool use_cwd; + bool ignore_dups; + bool validate_timestamps; + bool revalidate_path; + bool save_comments; + bool record_warnings; + bool protect_memory; + bool file_override_enabled; + bool enable_cli; + bool validate_permission; +#ifndef ZEND_WIN32 + bool validate_root; +#endif + zend_ulong revalidate_freq; + zend_ulong file_update_protection; + char *error_log; +#ifdef ZEND_WIN32 + char *mmap_base; +#endif + char *memory_model; + zend_long log_verbosity_level; + zend_long optimization_level; + zend_long opt_debug_level; + zend_long max_file_size; + zend_long interned_strings_buffer; + char *restrict_api; +#ifndef ZEND_WIN32 + char *lockfile_path; +#endif + char *file_cache; + bool file_cache_read_only; + bool file_cache_only; + bool file_cache_consistency_checks; +#if ENABLE_FILE_CACHE_FALLBACK + bool file_cache_fallback; +#endif +#ifdef HAVE_HUGE_CODE_PAGES + bool huge_code_pages; +#endif + char *preload; +#ifndef ZEND_WIN32 + char *preload_user; +#endif +#ifdef ZEND_WIN32 + char *cache_id; +#endif +} zend_opcache_test_accel_directives; + +typedef struct _zend_opcache_test_globals { + bool counted; + bool enabled; + bool locked; + bool accelerator_enabled; + bool pcre_reseted; + zend_opcache_test_accel_directives accel_directives; +} zend_opcache_test_globals; + +typedef struct _zend_opcache_ref_thread_ctx { + int result; + char message[256]; +} zend_opcache_ref_thread_ctx; + +extern size_t accel_globals_offset; + +#define ZEND_OPCACHE_TEST_CG(v) ZEND_TSRMG_FAST(accel_globals_offset, zend_opcache_test_globals *, v) + +static const char opcache_test_ini[] = + "html_errors=0\n" + "implicit_flush=1\n" + "output_buffering=0\n" + "max_execution_time=0\n" + "max_input_time=-1\n" + "opcache.enable=1\n" + "opcache.enable_cli=1\n" + "opcache.memory_consumption=64\n" + "opcache.max_accelerated_files=200\n" + "opcache.static_cache.volatile_size_mb=32\n\0"; + +static const char clear_code[] = + "(static function (): bool {" + " OPcache\\volatile_clear();" + " return true;" + "})()"; + +static const char worker_code[] = + "(static function (): bool {" + " $payload = ['name' => 'compiled-route-table', 'routes' => [], 'generators' => []];" + " for ($i = 0; $i < 192; $i++) {" + " $path = '/catalog/books/' . $i;" + " $payload['routes'][$path] = [" + " 'controller' => 'CatalogController::show'," + " 'methods' => ['GET', 'POST']," + " 'flags' => [true, false, true]," + " 'score' => $i," + " ];" + " $payload['generators'][$path] = [" + " 'pattern' => $path," + " 'variables' => ['id' => $i]," + " ];" + " }" + " for ($i = 0; $i < 16; $i++) {" + " if (!OPcache\\volatile_store('zts_request_shared_graph_refs_payload', $payload)) {" + " return false;" + " }" + " }" + " for ($i = 0; $i < 64; $i++) {" + " $fetched = OPcache\\volatile_fetch('zts_request_shared_graph_refs_payload');" + " if (!is_array($fetched)" + " || !isset($fetched['routes'], $fetched['generators'])" + " || count($fetched['routes']) !== 192) {" + " return false;" + " }" + " }" + " return true;" + "})()"; + +static zend_opcache_test_accel_directives zend_opcache_thread_accel_directives; +static bool zend_opcache_thread_enabled; + +static const zend_opcache_static_cache_shared_graph_header *zend_opcache_locate_shared_graph_header(uint32_t payload_offset) +{ + const unsigned char *buffer; + size_t buffer_len; + size_t padding; + uintptr_t raw_address, aligned_address; + const zend_opcache_static_cache_shared_graph_header *header; + + buffer_len = zend_opcache_static_cache_block_payload_capacity(payload_offset); + if (buffer_len == 0) { + return NULL; + } + + buffer = (const unsigned char *) zend_opcache_static_cache_ptr(payload_offset); + raw_address = (uintptr_t) buffer; + aligned_address = (uintptr_t) ZEND_MM_ALIGNED_SIZE(raw_address); + padding = (size_t) (aligned_address - raw_address); + if (padding > buffer_len || buffer_len - padding < sizeof(*header)) { + return NULL; + } + + buffer += padding; + header = (const zend_opcache_static_cache_shared_graph_header *) buffer; + if (header->magic != ZEND_OPCACHE_STATIC_CACHE_SHARED_GRAPH_MAGIC || + header->version != ZEND_OPCACHE_STATIC_CACHE_SHARED_GRAPH_VERSION) { + return NULL; + } + + return header; +} + +static int zend_opcache_test_startup(int argc, char **argv) +{ + if (!php_tsrm_startup_ex(3)) { + return FAILURE; + } + ZEND_TSRMLS_CACHE_UPDATE(); + + zend_signal_startup(); + sapi_startup(&php_embed_module); + php_embed_module.ini_entries = opcache_test_ini; + if (argv != NULL) { + php_embed_module.executable_location = argv[0]; + } + + if (php_embed_module.startup(&php_embed_module) == FAILURE) { + sapi_shutdown(); + tsrm_shutdown(); + return FAILURE; + } + + zend_opcache_thread_accel_directives = ZEND_OPCACHE_TEST_CG(accel_directives); + zend_opcache_thread_enabled = ZEND_OPCACHE_TEST_CG(enabled); + + SG(options) |= SAPI_OPTION_NO_CHDIR; + return SUCCESS; +} + +static void zend_opcache_test_shutdown(void) +{ + php_module_shutdown(); + sapi_shutdown(); + tsrm_shutdown(); +} + +static bool zend_opcache_thread_request_startup(void) +{ + (void) ts_resource(0); + ZEND_TSRMLS_CACHE_UPDATE(); + ZEND_OPCACHE_TEST_CG(accel_directives) = zend_opcache_thread_accel_directives; + ZEND_OPCACHE_TEST_CG(enabled) = zend_opcache_thread_enabled; + + SG(request_info).argc = 0; + SG(request_info).argv = NULL; + SG(request_info).query_string = NULL; + SG(request_info).request_uri = "-"; + SG(request_info).path_translated = NULL; + SG(request_info).request_method = "GET"; + SG(request_info).no_headers = 1; + + if (php_request_startup() == FAILURE) { + return false; + } + + SG(headers_sent) = 1; + SG(request_info).no_headers = 1; + php_register_variable("PHP_SELF", "-", NULL); + return true; +} + +static bool zend_opcache_thread_eval(const char *code, zval *retval, const char *label) +{ + ZVAL_UNDEF(retval); + if (zend_eval_stringl_ex(code, strlen(code), retval, label, true) == FAILURE) { + if (EG(exception)) { + zend_clear_exception(); + } + return false; + } + + if (EG(exception)) { + zend_clear_exception(); + if (!Z_ISUNDEF_P(retval)) { + zval_ptr_dtor(retval); + ZVAL_UNDEF(retval); + } + return false; + } + + return true; +} + +static bool zend_opcache_run_request_code(const char *code, const char *label, bool expect_single_shared_graph_ref, char *message, size_t message_size) +{ + zval retval; + + if (!zend_opcache_thread_request_startup()) { + snprintf(message, message_size, "%s: request startup failed", label); + return false; + } + + if (!zend_opcache_thread_eval(code, &retval, label)) { + snprintf(message, message_size, "%s: eval failed", label); + php_request_shutdown(NULL); + return false; + } + + if (!zend_is_true(&retval)) { + snprintf(message, message_size, "%s: returned false", label); + if (!Z_ISUNDEF(retval)) { + zval_ptr_dtor(&retval); + } + php_request_shutdown(NULL); + return false; + } + + if (expect_single_shared_graph_ref && zend_opcache_static_cache_shared_graph_ref_count != 1) { + snprintf( + message, + message_size, + "%s: expected one request shared graph ref, got %u", + label, + zend_opcache_static_cache_shared_graph_ref_count + ); + if (!Z_ISUNDEF(retval)) { + zval_ptr_dtor(&retval); + } + php_request_shutdown(NULL); + return false; + } + + if (!Z_ISUNDEF(retval)) { + zval_ptr_dtor(&retval); + } + php_request_shutdown(NULL); + return true; +} + +static bool zend_opcache_inspect_current_payload_state( + uint32_t *value_offset_out, + uint32_t *next_free_out, + int *refcount_out, + char *message, + size_t message_size) +{ + static const char key_name[] = "zts_request_shared_graph_refs_payload"; + zend_opcache_static_cache_context *previous_context; + zend_opcache_static_cache_header *header; + zend_opcache_static_cache_entry *entries, *entry = NULL; + const zend_opcache_static_cache_shared_graph_header *graph_header; + zend_string *key; + zend_ulong hash; + uint32_t index; + int state; + bool ok = false; + + previous_context = zend_opcache_static_cache_activate_context(&zend_opcache_static_cache_volatile_context_state); + key = zend_string_init(key_name, sizeof(key_name) - 1, 0); + hash = zend_string_hash_val(key); + + if (!zend_opcache_static_cache_rlock()) { + snprintf(message, message_size, "unable to acquire volatile cache read lock"); + goto done; + } + + if (!zend_opcache_static_cache_header_is_initialized_locked()) { + snprintf(message, message_size, "volatile cache header is not initialized"); + zend_opcache_static_cache_unlock(); + goto done; + } + + header = zend_opcache_static_cache_header_ptr(); + entries = zend_opcache_static_cache_entries(header); + for (index = 0; index < header->capacity; index++) { + if (zend_opcache_static_cache_key_equals(&entries[index], key, hash)) { + entry = &entries[index]; + break; + } + } + + if (entry == NULL) { + snprintf(message, message_size, "volatile cache entry was not found after request shutdown"); + zend_opcache_static_cache_unlock(); + goto done; + } + + if (entry->value_type != ZEND_OPCACHE_STATIC_CACHE_VALUE_SHARED_GRAPH) { + snprintf(message, message_size, "unexpected cached value type %u", (unsigned int) entry->value_type); + zend_opcache_static_cache_unlock(); + goto done; + } + + graph_header = zend_opcache_locate_shared_graph_header(entry->value_offset); + if (graph_header == NULL) { + snprintf(message, message_size, "shared graph payload header is invalid"); + zend_opcache_static_cache_unlock(); + goto done; + } + + state = zend_atomic_int_load_ex(&graph_header->ref_state); + *value_offset_out = entry->value_offset; + *next_free_out = header->next_free; + *refcount_out = state & ZEND_OPCACHE_STATIC_CACHE_SHARED_GRAPH_REFCOUNT_MASK; + + zend_opcache_static_cache_unlock(); + ok = true; + +done: + zend_string_release(key); + zend_opcache_static_cache_restore_context(previous_context); + return ok; +} + +static void *zend_opcache_ref_thread_main(void *arg) +{ + zend_opcache_ref_thread_ctx *ctx = (zend_opcache_ref_thread_ctx *) arg; + uint32_t expected_offset = 0; + uint32_t value_offset, next_free; + int refcount; + int iteration; + + ctx->result = 0; + ctx->message[0] = '\0'; + + if (!zend_opcache_run_request_code(clear_code, "zts shared graph clear", false, ctx->message, sizeof(ctx->message))) { + ctx->result = 1; + ts_free_thread(); + return NULL; + } + + for (iteration = 0; iteration < REQUEST_COUNT; iteration++) { + if (!zend_opcache_run_request_code(worker_code, "zts shared graph worker", true, ctx->message, sizeof(ctx->message))) { + ctx->result = 1; + break; + } + + if (!zend_opcache_inspect_current_payload_state(&value_offset, &next_free, &refcount, ctx->message, sizeof(ctx->message))) { + ctx->result = 1; + break; + } + + if (refcount != 0) { + ctx->result = 1; + snprintf( + ctx->message, + sizeof(ctx->message), + "shared graph refcount leaked after request %d (offset=%u next_free=%u refcount=%d)", + iteration + 1, + value_offset, + next_free, + refcount + ); + break; + } + + if (iteration == 0) { + expected_offset = value_offset; + } else if (value_offset != expected_offset) { + ctx->result = 1; + snprintf( + ctx->message, + sizeof(ctx->message), + "shared graph payload offset changed between requests (%u -> %u, next_free=%u)", + expected_offset, + value_offset, + next_free + ); + break; + } + } + + ts_free_thread(); + return NULL; +} + +int main(int argc, char **argv) +{ + zend_thread_t thread; + zend_opcache_ref_thread_ctx ctx; + + if (zend_opcache_test_startup(argc, argv) == FAILURE) { + fprintf(stderr, "startup failed\n"); + return 1; + } + + if (!zend_thread_start(&thread, zend_opcache_ref_thread_main, &ctx)) { + fprintf(stderr, "thread creation failed\n"); + zend_opcache_test_shutdown(); + return 1; + } + + if (!zend_thread_join(&thread)) { + fprintf(stderr, "thread join failed\n"); + zend_opcache_test_shutdown(); + return 1; + } + + if (ctx.result != 0) { + fprintf(stderr, "%s\n", ctx.message); + zend_opcache_test_shutdown(); + return 1; + } + + printf("ok\n"); + zend_opcache_test_shutdown(); + return 0; +} diff --git a/ext/opcache/tests/helpers/volatile_cache_zts_threads_001.c b/ext/opcache/tests/helpers/volatile_cache_zts_threads_001.c new file mode 100644 index 000000000000..7f7b74edc390 --- /dev/null +++ b/ext/opcache/tests/helpers/volatile_cache_zts_threads_001.c @@ -0,0 +1,281 @@ +/* + +----------------------------------------------------------------------+ + | Zend OPcache | + +----------------------------------------------------------------------+ + | Copyright © The PHP Group and Contributors. | + +----------------------------------------------------------------------+ + | This source file is subject to the Modified BSD License that is | + | bundled with this package in the file LICENSE, and is available | + | through the World Wide Web at . | + | | + | SPDX-License-Identifier: BSD-3-Clause | + +----------------------------------------------------------------------+ +*/ + +#ifdef HAVE_CONFIG_H +# include "php_config.h" +#endif + +#include +#include +#include +#include + +#include "Zend/zend_exceptions.h" +#include "Zend/zend_execute.h" +#include "Zend/zend_portability.h" +#include "sapi/embed/php_embed.h" + +#ifndef ZTS +# error "This helper requires a ZTS build" +#endif + +#define TEST_KEY "zts-thread-shared-key" +#define TEST_VALUE "writer-thread-payload-0123456789" + +static const char opcache_test_ini[] = + "html_errors=0\n" + "implicit_flush=1\n" + "output_buffering=0\n" + "max_execution_time=0\n" + "max_input_time=-1\n" + "opcache.enable=1\n" + "opcache.enable_cli=1\n" + "opcache.memory_consumption=64\n" + "opcache.max_accelerated_files=200\n" + "opcache.static_cache.volatile_size_mb=32\n\0"; + +typedef enum { + THREAD_MODE_STORE, + THREAD_MODE_FETCH +} zend_opcache_thread_mode; + +typedef struct _zend_opcache_thread_ctx { + zend_opcache_thread_mode mode; + int result; + char message[128]; +} zend_opcache_thread_ctx; + +static void zend_opcache_thread_set_failure(zend_opcache_thread_ctx *ctx, const char *message) +{ + ctx->result = 1; + snprintf(ctx->message, sizeof(ctx->message), "%s", message); +} + +static int zend_opcache_test_startup(int argc, char **argv) +{ + if (!php_tsrm_startup_ex(4)) { + return FAILURE; + } + ZEND_TSRMLS_CACHE_UPDATE(); + + zend_signal_startup(); + sapi_startup(&php_embed_module); + php_embed_module.ini_entries = opcache_test_ini; + if (argv != NULL) { + php_embed_module.executable_location = argv[0]; + } + + if (php_embed_module.startup(&php_embed_module) == FAILURE) { + sapi_shutdown(); + tsrm_shutdown(); + return FAILURE; + } + + SG(options) |= SAPI_OPTION_NO_CHDIR; + return SUCCESS; +} + +static void zend_opcache_test_shutdown(void) +{ + php_module_shutdown(); + sapi_shutdown(); + tsrm_shutdown(); +} + +static bool zend_opcache_thread_request_startup(void) +{ + (void) ts_resource(0); + ZEND_TSRMLS_CACHE_UPDATE(); + + SG(request_info).argc = 0; + SG(request_info).argv = NULL; + SG(request_info).query_string = NULL; + SG(request_info).request_uri = "-"; + SG(request_info).path_translated = NULL; + SG(request_info).request_method = "GET"; + SG(request_info).no_headers = 1; + + if (php_request_startup() == FAILURE) { + return false; + } + + SG(headers_sent) = 1; + SG(request_info).no_headers = 1; + php_register_variable("PHP_SELF", "-", NULL); + return true; +} + +static bool zend_opcache_thread_eval(const char *code, zval *retval, const char *label) +{ + ZVAL_UNDEF(retval); + if (zend_eval_stringl_ex(code, strlen(code), retval, label, true) == FAILURE) { + if (EG(exception)) { + zend_clear_exception(); + } + return false; + } + + if (EG(exception)) { + zend_clear_exception(); + if (!Z_ISUNDEF_P(retval)) { + zval_ptr_dtor(retval); + ZVAL_UNDEF(retval); + } + return false; + } + + return true; +} + +static void *zend_opcache_thread_main(void *arg) +{ + static const char store_code[] = + "OPcache\\volatile_store('" TEST_KEY "', '" TEST_VALUE "');"; + static const char fetch_code[] = + "OPcache\\volatile_fetch('" TEST_KEY "');"; + zend_opcache_thread_ctx *ctx; + zval retval; + bool request_started = false; + + ctx = (zend_opcache_thread_ctx *) arg; + ctx->result = 0; + ctx->message[0] = '\0'; + ZVAL_UNDEF(&retval); + + if (!zend_opcache_thread_request_startup()) { + zend_opcache_thread_set_failure(ctx, "request startup failed"); + goto cleanup; + } + request_started = true; + + if (ctx->mode == THREAD_MODE_STORE) { + if (!zend_opcache_thread_eval(store_code, &retval, "zts volatile cache store")) { + zend_opcache_thread_set_failure(ctx, "volatile_store failed"); + goto cleanup; + } + + if (!zend_is_true(&retval)) { + zend_opcache_thread_set_failure(ctx, "volatile_store returned false"); + } + } else { + if (!zend_opcache_thread_eval(fetch_code, &retval, "zts volatile cache fetch")) { + zend_opcache_thread_set_failure(ctx, "volatile_fetch failed"); + goto cleanup; + } + + if (Z_TYPE(retval) != IS_STRING || strcmp(Z_STRVAL(retval), TEST_VALUE) != 0) { + zend_opcache_thread_set_failure(ctx, "volatile_fetch returned unexpected value"); + } + } + +cleanup: + if (!Z_ISUNDEF(retval)) { + zval_ptr_dtor(&retval); + } + if (request_started) { + php_request_shutdown(NULL); + } + ts_free_thread(); + return NULL; +} + +int main(int argc, char **argv) +{ + static const char clear_code[] = "OPcache\\volatile_clear();"; + static const char fetch_code[] = + "OPcache\\volatile_fetch('" TEST_KEY "');"; + zend_thread_t writer_thread; + zend_thread_t reader_thread; + zend_opcache_thread_ctx writer_ctx; + zend_opcache_thread_ctx reader_ctx; + zval retval; + int exit_code; + + if (zend_opcache_test_startup(argc, argv) == FAILURE) { + fprintf(stderr, "startup failed\n"); + return 1; + } + + exit_code = 1; + if (!zend_opcache_thread_request_startup()) { + fprintf(stderr, "volatile_clear request startup failed\n"); + goto cleanup; + } + if (!zend_opcache_thread_eval(clear_code, &retval, "zts volatile cache clear")) { + fprintf(stderr, "volatile_clear failed\n"); + php_request_shutdown(NULL); + goto cleanup; + } + if (!Z_ISUNDEF(retval)) { + zval_ptr_dtor(&retval); + } + php_request_shutdown(NULL); + + writer_ctx.mode = THREAD_MODE_STORE; + if (!zend_thread_start(&writer_thread, zend_opcache_thread_main, &writer_ctx)) { + fprintf(stderr, "writer thread creation failed\n"); + goto cleanup; + } + if (!zend_thread_join(&writer_thread)) { + fprintf(stderr, "writer thread join failed\n"); + goto cleanup; + } + if (writer_ctx.result != 0) { + fprintf(stderr, "%s\n", writer_ctx.message); + goto cleanup; + } + + reader_ctx.mode = THREAD_MODE_FETCH; + if (!zend_thread_start(&reader_thread, zend_opcache_thread_main, &reader_ctx)) { + fprintf(stderr, "reader thread creation failed\n"); + goto cleanup; + } + if (!zend_thread_join(&reader_thread)) { + fprintf(stderr, "reader thread join failed\n"); + goto cleanup; + } + if (reader_ctx.result != 0) { + fprintf(stderr, "%s\n", reader_ctx.message); + goto cleanup; + } + + if (!zend_opcache_thread_request_startup()) { + fprintf(stderr, "main thread request startup failed\n"); + goto cleanup; + } + if (!zend_opcache_thread_eval(fetch_code, &retval, "zts volatile cache main fetch")) { + fprintf(stderr, "main thread fetch failed\n"); + php_request_shutdown(NULL); + goto cleanup; + } + if (Z_TYPE(retval) != IS_STRING || strcmp(Z_STRVAL(retval), TEST_VALUE) != 0) { + fprintf(stderr, "main thread fetch returned unexpected value\n"); + if (!Z_ISUNDEF(retval)) { + zval_ptr_dtor(&retval); + } + php_request_shutdown(NULL); + goto cleanup; + } + if (!Z_ISUNDEF(retval)) { + zval_ptr_dtor(&retval); + } + php_request_shutdown(NULL); + + printf("ok\n"); + exit_code = 0; + +cleanup: + zend_opcache_test_shutdown(); + return exit_code; +} diff --git a/ext/opcache/tests/helpers/volatile_cache_zts_threads_002.c b/ext/opcache/tests/helpers/volatile_cache_zts_threads_002.c new file mode 100644 index 000000000000..09ca013893fc --- /dev/null +++ b/ext/opcache/tests/helpers/volatile_cache_zts_threads_002.c @@ -0,0 +1,312 @@ +/* + +----------------------------------------------------------------------+ + | Zend OPcache | + +----------------------------------------------------------------------+ + | Copyright © The PHP Group and Contributors. | + +----------------------------------------------------------------------+ + | This source file is subject to the Modified BSD License that is | + | bundled with this package in the file LICENSE, and is available | + | through the World Wide Web at . | + | | + | SPDX-License-Identifier: BSD-3-Clause | + +----------------------------------------------------------------------+ +*/ + +#ifdef HAVE_CONFIG_H +# include "php_config.h" +#endif + +#include +#include +#include +#include + +#include "Zend/zend_exceptions.h" +#include "Zend/zend_execute.h" +#include "Zend/zend_portability.h" +#include "sapi/embed/php_embed.h" + +#ifndef ZTS +# error "This helper requires a ZTS build" +#endif + +typedef struct _zend_opcache_thread_ctx { + const char *mode; + const char *scenario_path; + const char *label; + int result; + char message[256]; +} zend_opcache_thread_ctx; + +static const char opcache_test_ini[] = + "html_errors=0\n" + "implicit_flush=1\n" + "output_buffering=0\n" + "max_execution_time=0\n" + "max_input_time=-1\n" + "opcache.enable=1\n" + "opcache.enable_cli=1\n" + "opcache.memory_consumption=64\n" + "opcache.max_accelerated_files=200\n" + "opcache.static_cache.volatile_size_mb=32\n\0"; + +static char *zend_opcache_build_scenario_code(const char *mode, const char *scenario_path); + +static void zend_opcache_thread_set_failure(zend_opcache_thread_ctx *ctx, const char *message) +{ + ctx->result = 1; + snprintf(ctx->message, sizeof(ctx->message), "%s", message); +} + +static int zend_opcache_test_startup(int argc, char **argv) +{ + if (!php_tsrm_startup_ex(4)) { + return FAILURE; + } + ZEND_TSRMLS_CACHE_UPDATE(); + + zend_signal_startup(); + sapi_startup(&php_embed_module); + php_embed_module.ini_entries = opcache_test_ini; + if (argv != NULL) { + php_embed_module.executable_location = argv[0]; + } + + if (php_embed_module.startup(&php_embed_module) == FAILURE) { + sapi_shutdown(); + tsrm_shutdown(); + return FAILURE; + } + + SG(options) |= SAPI_OPTION_NO_CHDIR; + return SUCCESS; +} + +static void zend_opcache_test_shutdown(void) +{ + php_module_shutdown(); + sapi_shutdown(); + tsrm_shutdown(); +} + +static bool zend_opcache_thread_request_startup(void) +{ + (void) ts_resource(0); + ZEND_TSRMLS_CACHE_UPDATE(); + + SG(request_info).argc = 0; + SG(request_info).argv = NULL; + SG(request_info).query_string = NULL; + SG(request_info).request_uri = "-"; + SG(request_info).path_translated = NULL; + SG(request_info).request_method = "GET"; + SG(request_info).no_headers = 1; + + if (php_request_startup() == FAILURE) { + return false; + } + + SG(headers_sent) = 1; + SG(request_info).no_headers = 1; + php_register_variable("PHP_SELF", "-", NULL); + return true; +} + +static bool zend_opcache_thread_eval(const char *code, zval *retval, const char *label) +{ + ZVAL_UNDEF(retval); + if (zend_eval_stringl_ex(code, strlen(code), retval, label, true) == FAILURE) { + if (EG(exception)) { + zend_clear_exception(); + } + return false; + } + + if (EG(exception)) { + zend_clear_exception(); + if (!Z_ISUNDEF_P(retval)) { + zval_ptr_dtor(retval); + ZVAL_UNDEF(retval); + } + return false; + } + + return true; +} + +static bool zend_opcache_thread_run_request_code(const char *code, const char *label, char *message, size_t message_size) +{ + zval retval; + + if (!zend_opcache_thread_request_startup()) { + snprintf(message, message_size, "%s: request startup failed", label); + return false; + } + + if (!zend_opcache_thread_eval(code, &retval, label)) { + snprintf(message, message_size, "%s: eval failed", label); + php_request_shutdown(NULL); + return false; + } + + if (!zend_is_true(&retval)) { + snprintf(message, message_size, "%s: returned false", label); + if (!Z_ISUNDEF(retval)) { + zval_ptr_dtor(&retval); + } + php_request_shutdown(NULL); + return false; + } + + if (!Z_ISUNDEF(retval)) { + zval_ptr_dtor(&retval); + } + php_request_shutdown(NULL); + return true; +} + +static void *zend_opcache_thread_main(void *arg) +{ + char *code; + zend_opcache_thread_ctx *ctx; + + ctx = (zend_opcache_thread_ctx *) arg; + ctx->result = 0; + ctx->message[0] = '\0'; + + code = zend_opcache_build_scenario_code(ctx->mode, ctx->scenario_path); + if (code == NULL) { + zend_opcache_thread_set_failure(ctx, "failed to build scenario code"); + ts_free_thread(); + return NULL; + } + + if (!zend_opcache_thread_run_request_code(code, ctx->label, ctx->message, sizeof(ctx->message))) { + ctx->result = 1; + } + + free(code); + ts_free_thread(); + + return NULL; +} + +static bool zend_opcache_run_clear_request(char *message, size_t message_size) +{ + static const char clear_code[] = "OPcache\\volatile_clear(); return true;"; + zval retval; + + if (!zend_opcache_thread_request_startup()) { + snprintf(message, message_size, "zts volatile cache clear: request startup failed"); + return false; + } + + if (!zend_opcache_thread_eval(clear_code, &retval, "zts volatile cache clear")) { + snprintf(message, message_size, "zts volatile cache clear: eval failed"); + php_request_shutdown(NULL); + return false; + } + + if (!Z_ISUNDEF(retval)) { + zval_ptr_dtor(&retval); + } + php_request_shutdown(NULL); + return true; +} + +static char *zend_opcache_build_scenario_code(const char *mode, const char *scenario_path) +{ + char *code; + size_t code_len; + + code_len = strlen(mode) + strlen(scenario_path) + sizeof("$mode=''; require '';\0"); + code = malloc(code_len); + if (code == NULL) { + return NULL; + } + + snprintf(code, code_len, "$mode='%s'; require '%s';", mode, scenario_path); + return code; +} + +static bool zend_opcache_run_threaded_scenario(const char *scenario_path, char *message, size_t message_size) +{ + zend_thread_t store_thread; + zend_thread_t fetch_thread; + zend_opcache_thread_ctx store_ctx; + zend_opcache_thread_ctx fetch_ctx; + bool success; + + success = false; + + if (!zend_opcache_run_clear_request(message, message_size)) { + return false; + } + + store_ctx.mode = "store"; + store_ctx.scenario_path = scenario_path; + store_ctx.label = "zts threaded scenario store"; + if (!zend_thread_start(&store_thread, zend_opcache_thread_main, &store_ctx)) { + snprintf(message, message_size, "store thread creation failed"); + return false; + } + if (!zend_thread_join(&store_thread)) { + snprintf(message, message_size, "store thread join failed"); + return false; + } + if (store_ctx.result != 0) { + snprintf(message, message_size, "%s", store_ctx.message); + return false; + } + + fetch_ctx.mode = "fetch"; + fetch_ctx.scenario_path = scenario_path; + fetch_ctx.label = "zts threaded scenario fetch"; + if (!zend_thread_start(&fetch_thread, zend_opcache_thread_main, &fetch_ctx)) { + snprintf(message, message_size, "fetch thread creation failed"); + return false; + } + if (!zend_thread_join(&fetch_thread)) { + snprintf(message, message_size, "fetch thread join failed"); + return false; + } + if (fetch_ctx.result != 0) { + snprintf(message, message_size, "%s", fetch_ctx.message); + return false; + } + + success = true; + + return success; +} + +int main(int argc, char **argv) +{ + char message[256]; + int exit_code; + + if (argc < 2) { + fprintf(stderr, "scenario path required\n"); + return 1; + } + + if (zend_opcache_test_startup(argc, argv) == FAILURE) { + fprintf(stderr, "startup failed\n"); + return 1; + } + + exit_code = 1; + message[0] = '\0'; + + if (!zend_opcache_run_threaded_scenario(argv[1], message, sizeof(message))) { + fprintf(stderr, "%s\n", message); + goto cleanup; + } + + printf("ok\n"); + exit_code = 0; + +cleanup: + zend_opcache_test_shutdown(); + return exit_code; +} diff --git a/ext/opcache/tests/helpers/volatile_cache_zts_threads_002_materialization_nested.inc b/ext/opcache/tests/helpers/volatile_cache_zts_threads_002_materialization_nested.inc new file mode 100644 index 000000000000..c3998a748b8b --- /dev/null +++ b/ext/opcache/tests/helpers/volatile_cache_zts_threads_002_materialization_nested.inc @@ -0,0 +1,58 @@ + $i, + 'text' => str_repeat(chr(65 + ($i % 26)), 96), + 'flags' => [$i, $i + 1, $i + 2, $i + 3], + ]; + } + + return $rows; +} + +if ($mode === 'store') { + return OPcache\volatile_store( + 'zts_materialization_nested_payload', + new WrappedPayload( + new LargePayload(build_rows(), 'nested'), + new DateTimeImmutable('2026-06-15 09:30:00', new DateTimeZone('UTC')), + ), + ); +} + +$before = memory_get_usage(); +$fetched = OPcache\volatile_fetch('zts_materialization_nested_payload'); + +if (!$fetched instanceof WrappedPayload) { + return false; +} + +$readOnly = $fetched->timestamp->format(DateTimeInterface::ATOM) . '|' . $fetched->payload->rows[100]['text']; +$afterFetch = memory_get_usage(); +$fetched->payload->rows[100]['text'] = 'changed'; +$afterMutate = memory_get_usage(); + +return $readOnly === '2026-06-15T09:30:00+00:00|' . str_repeat(chr(65 + (100 % 26)), 96) + && ($afterFetch - $before) < 262144 + && ($afterMutate - $afterFetch) > 131072; diff --git a/ext/opcache/tests/helpers/volatile_cache_zts_threads_002_materialization_plain.inc b/ext/opcache/tests/helpers/volatile_cache_zts_threads_002_materialization_plain.inc new file mode 100644 index 000000000000..946b301a4d88 --- /dev/null +++ b/ext/opcache/tests/helpers/volatile_cache_zts_threads_002_materialization_plain.inc @@ -0,0 +1,44 @@ + $i, + 'text' => str_repeat(chr(65 + ($i % 26)), 96), + 'flags' => [$i, $i + 1, $i + 2, $i + 3], + ]; + } + + return $rows; +} + +if ($mode === 'store') { + return OPcache\volatile_store('zts_materialization_plain_payload', new LargePayload(build_rows(), 'plain')); +} + +$before = memory_get_usage(); +$fetched = OPcache\volatile_fetch('zts_materialization_plain_payload'); + +if (!$fetched instanceof LargePayload) { + return false; +} + +$readOnly = $fetched->rows[100]['text']; +$afterFetch = memory_get_usage(); +$fetched->rows[100]['text'] = 'changed'; +$afterMutate = memory_get_usage(); + +return $readOnly === str_repeat(chr(65 + (100 % 26)), 96) + && ($afterFetch - $before) < 262144 + && ($afterMutate - $afterFetch) > 131072; diff --git a/ext/opcache/tests/helpers/volatile_cache_zts_threads_002_safe_direct.inc b/ext/opcache/tests/helpers/volatile_cache_zts_threads_002_safe_direct.inc new file mode 100644 index 000000000000..4fc5311773ea --- /dev/null +++ b/ext/opcache/tests/helpers/volatile_cache_zts_threads_002_safe_direct.inc @@ -0,0 +1,110 @@ +label = $label; + $this->revision = $revision; + } + + public function describe(): string + { + return $this->label . ':' . $this->revision; + } +} + +class LabelIterator extends ArrayIterator +{ +} + +class TaggedCollection extends ArrayObject +{ + private string $type; + + public function __construct(array $data, string $type, string $iteratorClass) + { + parent::__construct($data, 0, $iteratorClass); + $this->type = $type; + } + + public function type(): string + { + return $this->type; + } +} + +class CustomSerializedDateTime extends DateTime +{ + public static int $unserializeCount = 0; + + private string $label; + + public function __construct(string $time, DateTimeZone $timezone, string $label) + { + parent::__construct($time, $timezone); + $this->label = $label; + } + + public function __serialize(): array + { + return parent::__serialize() + ['label' => $this->label]; + } + + public function __unserialize(array $data): void + { + self::$unserializeCount++; + parent::__unserialize($data); + $this->label = $data['label']; + } + + public function label(): string + { + return $this->label; + } +} + +if ($mode === 'store') { + return OPcache\volatile_store( + 'zts_safe_direct_event_datetime', + new EventDateTime('2026-06-15 09:30:00.123456', new DateTimeZone('Europe/Paris'), 'launch', 7), + ) && OPcache\volatile_store( + 'zts_safe_direct_tagged_collection', + new TaggedCollection(['alpha' => 10, 'beta' => 20], 'metric', LabelIterator::class), + ) && OPcache\volatile_store( + 'zts_safe_direct_custom_datetime', + new CustomSerializedDateTime('2026-06-15 10:45:00.654321', new DateTimeZone('UTC'), 'fallback'), + ); +} + +$event = OPcache\volatile_fetch('zts_safe_direct_event_datetime'); +$collection = OPcache\volatile_fetch('zts_safe_direct_tagged_collection'); +$fallback = OPcache\volatile_fetch('zts_safe_direct_custom_datetime'); + +if (!$event instanceof EventDateTime) { + return false; +} + +if (!$collection instanceof TaggedCollection) { + return false; +} + +if (!$fallback instanceof CustomSerializedDateTime) { + return false; +} + +$iterator = $collection->getIterator(); + +return $event->format('Y-m-d H:i:s.u e') === '2026-06-15 09:30:00.123456 Europe/Paris' + && $event->describe() === 'launch:7' + && $collection->type() === 'metric' + && $iterator instanceof LabelIterator + && $collection['alpha'] === 10 + && $collection['beta'] === 20 + && $fallback->format('Y-m-d H:i:s.u e') === '2026-06-15 10:45:00.654321 UTC' + && $fallback->label() === 'fallback' + && CustomSerializedDateTime::$unserializeCount === 1; diff --git a/ext/opcache/tests/helpers/volatile_cache_zts_threads_003.c b/ext/opcache/tests/helpers/volatile_cache_zts_threads_003.c new file mode 100644 index 000000000000..c5b1a800219c --- /dev/null +++ b/ext/opcache/tests/helpers/volatile_cache_zts_threads_003.c @@ -0,0 +1,373 @@ +/* + +----------------------------------------------------------------------+ + | Zend OPcache | + +----------------------------------------------------------------------+ + | Copyright © The PHP Group and Contributors. | + +----------------------------------------------------------------------+ + | This source file is subject to the Modified BSD License that is | + | bundled with this package in the file LICENSE, and is available | + | through the World Wide Web at . | + | | + | SPDX-License-Identifier: BSD-3-Clause | + +----------------------------------------------------------------------+ +*/ + +#ifdef HAVE_CONFIG_H +# include "php_config.h" +#endif + +#include +#include +#include +#include + +#include "Zend/zend_exceptions.h" +#include "Zend/zend_execute.h" +#include "Zend/zend_portability.h" +#include "sapi/embed/php_embed.h" + +#ifndef ZTS +# error "This helper requires a ZTS build" +#endif + +typedef struct _zend_opcache_test_accel_directives { + zend_long memory_consumption; + zend_long static_cache_volatile_size_mb; + zend_long max_accelerated_files; + double max_wasted_percentage; + char *user_blacklist_filename; + zend_long force_restart_timeout; + bool use_cwd; + bool ignore_dups; + bool validate_timestamps; + bool revalidate_path; + bool save_comments; + bool record_warnings; + bool protect_memory; + bool file_override_enabled; + bool enable_cli; + bool validate_permission; +#ifndef ZEND_WIN32 + bool validate_root; +#endif + zend_ulong revalidate_freq; + zend_ulong file_update_protection; + char *error_log; +#ifdef ZEND_WIN32 + char *mmap_base; +#endif + char *memory_model; + zend_long log_verbosity_level; + zend_long optimization_level; + zend_long opt_debug_level; + zend_long max_file_size; + zend_long interned_strings_buffer; + char *restrict_api; +#ifndef ZEND_WIN32 + char *lockfile_path; +#endif + char *file_cache; + bool file_cache_read_only; + bool file_cache_only; + bool file_cache_consistency_checks; +#if ENABLE_FILE_CACHE_FALLBACK + bool file_cache_fallback; +#endif +#ifdef HAVE_HUGE_CODE_PAGES + bool huge_code_pages; +#endif + char *preload; +#ifndef ZEND_WIN32 + char *preload_user; +#endif +#ifdef ZEND_WIN32 + char *cache_id; +#endif +} zend_opcache_test_accel_directives; + +typedef struct _zend_opcache_test_globals { + bool counted; + bool enabled; + bool locked; + bool accelerator_enabled; + bool pcre_reseted; + zend_opcache_test_accel_directives accel_directives; +} zend_opcache_test_globals; + +typedef struct _zend_opcache_thread_ctx { + const char *mode; + const char *scenario_path; + const char *label; + int result; + char message[256]; +} zend_opcache_thread_ctx; + +extern size_t accel_globals_offset; + +#define ZEND_OPCACHE_TEST_CG(v) ZEND_TSRMG_FAST(accel_globals_offset, zend_opcache_test_globals *, v) + +static const char opcache_test_ini[] = + "html_errors=0\n" + "implicit_flush=1\n" + "output_buffering=0\n" + "max_execution_time=0\n" + "max_input_time=-1\n" + "opcache.enable=1\n" + "opcache.enable_cli=1\n" + "opcache.memory_consumption=64\n" + "opcache.max_accelerated_files=200\n" + "opcache.static_cache.volatile_size_mb=32\n\0"; + +static zend_opcache_test_accel_directives zend_opcache_thread_accel_directives; +static bool zend_opcache_thread_enabled; + +static char *zend_opcache_build_scenario_code(const char *mode, const char *scenario_path); + +static int zend_opcache_test_startup(int argc, char **argv) +{ + if (!php_tsrm_startup_ex(4)) { + return FAILURE; + } + ZEND_TSRMLS_CACHE_UPDATE(); + + zend_signal_startup(); + sapi_startup(&php_embed_module); + php_embed_module.ini_entries = opcache_test_ini; + if (argv != NULL) { + php_embed_module.executable_location = argv[0]; + } + + if (php_embed_module.startup(&php_embed_module) == FAILURE) { + sapi_shutdown(); + tsrm_shutdown(); + return FAILURE; + } + + zend_opcache_thread_accel_directives = ZEND_OPCACHE_TEST_CG(accel_directives); + zend_opcache_thread_enabled = ZEND_OPCACHE_TEST_CG(enabled); + + SG(options) |= SAPI_OPTION_NO_CHDIR; + return SUCCESS; +} + +static void zend_opcache_test_shutdown(void) +{ + php_module_shutdown(); + sapi_shutdown(); + tsrm_shutdown(); +} + +static bool zend_opcache_thread_request_startup(void) +{ + (void) ts_resource(0); + ZEND_TSRMLS_CACHE_UPDATE(); + ZEND_OPCACHE_TEST_CG(accel_directives) = zend_opcache_thread_accel_directives; + ZEND_OPCACHE_TEST_CG(enabled) = zend_opcache_thread_enabled; + + SG(request_info).argc = 0; + SG(request_info).argv = NULL; + SG(request_info).query_string = NULL; + SG(request_info).request_uri = "-"; + SG(request_info).path_translated = NULL; + SG(request_info).request_method = "GET"; + SG(request_info).no_headers = 1; + + if (php_request_startup() == FAILURE) { + return false; + } + + SG(headers_sent) = 1; + SG(request_info).no_headers = 1; + php_register_variable("PHP_SELF", "-", NULL); + return true; +} + +static bool zend_opcache_thread_eval(const char *code, zval *retval, const char *label) +{ + ZVAL_UNDEF(retval); + if (zend_eval_stringl_ex(code, strlen(code), retval, label, true) == FAILURE) { + if (EG(exception)) { + zend_clear_exception(); + } + return false; + } + + if (EG(exception)) { + zend_clear_exception(); + if (!Z_ISUNDEF_P(retval)) { + zval_ptr_dtor(retval); + ZVAL_UNDEF(retval); + } + return false; + } + + return true; +} + +static bool zend_opcache_thread_run_request_code(const char *code, const char *label, char *message, size_t message_size) +{ + zval retval; + + if (!zend_opcache_thread_request_startup()) { + snprintf(message, message_size, "%s: request startup failed", label); + return false; + } + + if (!zend_opcache_thread_eval(code, &retval, label)) { + snprintf(message, message_size, "%s: eval failed", label); + php_request_shutdown(NULL); + return false; + } + + if (!zend_is_true(&retval)) { + snprintf(message, message_size, "%s: returned false", label); + if (!Z_ISUNDEF(retval)) { + zval_ptr_dtor(&retval); + } + php_request_shutdown(NULL); + return false; + } + + if (!Z_ISUNDEF(retval)) { + zval_ptr_dtor(&retval); + } + php_request_shutdown(NULL); + return true; +} + +static char *zend_opcache_build_scenario_code(const char *mode, const char *scenario_path) +{ + char *code; + size_t code_len; + + code_len = strlen(mode) + strlen(scenario_path) + sizeof("$mode=''; require '';\0"); + code = malloc(code_len); + if (code == NULL) { + return NULL; + } + + snprintf(code, code_len, "$mode='%s'; require '%s';", mode, scenario_path); + return code; +} + +static void *zend_opcache_thread_main(void *arg) +{ + char *code; + zend_opcache_thread_ctx *ctx; + + ctx = (zend_opcache_thread_ctx *) arg; + ctx->result = 0; + ctx->message[0] = '\0'; + + code = zend_opcache_build_scenario_code(ctx->mode, ctx->scenario_path); + if (code == NULL) { + ctx->result = 1; + snprintf(ctx->message, sizeof(ctx->message), "failed to build scenario code"); + ts_free_thread(); + return NULL; + } + + if (!zend_opcache_thread_run_request_code(code, ctx->label, ctx->message, sizeof(ctx->message))) { + ctx->result = 1; + } + + free(code); + ts_free_thread(); + return NULL; +} + +static bool zend_opcache_run_mode_request(const char *mode, const char *scenario_path, const char *label, char *message, size_t message_size) +{ + char *code; + bool success; + + code = zend_opcache_build_scenario_code(mode, scenario_path); + if (code == NULL) { + snprintf(message, message_size, "%s: failed to build scenario code", label); + return false; + } + + success = zend_opcache_thread_run_request_code(code, label, message, message_size); + free(code); + return success; +} + +static bool zend_opcache_run_threaded_scenario(const char *scenario_path, char *message, size_t message_size) +{ + zend_thread_t reader_thread; + zend_thread_t writer_thread; + zend_opcache_thread_ctx reader_ctx; + zend_opcache_thread_ctx writer_ctx; + + if (!zend_opcache_run_mode_request("init", scenario_path, "zts lookup init", message, message_size)) { + return false; + } + + reader_ctx.mode = "reader"; + reader_ctx.scenario_path = scenario_path; + reader_ctx.label = "zts lookup reader"; + if (!zend_thread_start(&reader_thread, zend_opcache_thread_main, &reader_ctx)) { + snprintf(message, message_size, "reader thread creation failed"); + return false; + } + + writer_ctx.mode = "writer"; + writer_ctx.scenario_path = scenario_path; + writer_ctx.label = "zts lookup writer"; + if (!zend_thread_start(&writer_thread, zend_opcache_thread_main, &writer_ctx)) { + snprintf(message, message_size, "writer thread creation failed"); + zend_thread_join(&reader_thread); + return false; + } + + if (!zend_thread_join(&reader_thread)) { + snprintf(message, message_size, "reader thread join failed"); + zend_thread_join(&writer_thread); + return false; + } + if (!zend_thread_join(&writer_thread)) { + snprintf(message, message_size, "writer thread join failed"); + return false; + } + if (reader_ctx.result != 0) { + snprintf(message, message_size, "%s", reader_ctx.message); + return false; + } + if (writer_ctx.result != 0) { + snprintf(message, message_size, "%s", writer_ctx.message); + return false; + } + + return true; +} + +int main(int argc, char **argv) +{ + char message[256]; + int exit_code; + + if (argc < 2) { + fprintf(stderr, "scenario path required\n"); + return 1; + } + + if (zend_opcache_test_startup(argc, argv) == FAILURE) { + fprintf(stderr, "startup failed\n"); + return 1; + } + + exit_code = 1; + message[0] = '\0'; + + if (!zend_opcache_run_threaded_scenario(argv[1], message, sizeof(message))) { + fprintf(stderr, "%s\n", message); + goto cleanup; + } + + printf("ok\n"); + exit_code = 0; + +cleanup: + zend_opcache_test_shutdown(); + return exit_code; +} diff --git a/ext/opcache/tests/helpers/volatile_cache_zts_threads_003_clear.inc b/ext/opcache/tests/helpers/volatile_cache_zts_threads_003_clear.inc new file mode 100644 index 000000000000..872c4d4832e1 --- /dev/null +++ b/ext/opcache/tests/helpers/volatile_cache_zts_threads_003_clear.inc @@ -0,0 +1,51 @@ += $deadline) { + throw new RuntimeException("timed out waiting for {$path}"); + } + + usleep(1000); + } +} + +function cleanup_files(string ...$paths): void +{ + foreach ($paths as $path) { + @unlink($path); + } +} + +$key = 'zts_lookup_user_clear_key'; +$readyFile = sys_get_temp_dir() . '/opcache_zts_lookup_user_clear.ready'; +$doneFile = sys_get_temp_dir() . '/opcache_zts_lookup_user_clear.done'; + +if ($mode === 'init') { + cleanup_files($readyFile, $doneFile); + OPcache\volatile_clear(); + return OPcache\volatile_store($key, 'old'); +} + +if ($mode === 'writer') { + wait_for_file($readyFile); + OPcache\volatile_clear(); + file_put_contents($doneFile, 'done'); + return true; +} + +$first = fetch_or_miss($key); +file_put_contents($readyFile, 'ready'); +wait_for_file($doneFile); +$second = fetch_or_miss($key); +cleanup_files($readyFile, $doneFile); + +return $first === 'old' && $second === 'MISS'; diff --git a/ext/opcache/tests/helpers/volatile_cache_zts_threads_003_delete.inc b/ext/opcache/tests/helpers/volatile_cache_zts_threads_003_delete.inc new file mode 100644 index 000000000000..5b078c4a9b9d --- /dev/null +++ b/ext/opcache/tests/helpers/volatile_cache_zts_threads_003_delete.inc @@ -0,0 +1,51 @@ += $deadline) { + throw new RuntimeException("timed out waiting for {$path}"); + } + + usleep(1000); + } +} + +function cleanup_files(string ...$paths): void +{ + foreach ($paths as $path) { + @unlink($path); + } +} + +$key = 'zts_lookup_user_delete_key'; +$readyFile = sys_get_temp_dir() . '/opcache_zts_lookup_user_delete.ready'; +$doneFile = sys_get_temp_dir() . '/opcache_zts_lookup_user_delete.done'; + +if ($mode === 'init') { + cleanup_files($readyFile, $doneFile); + OPcache\volatile_clear(); + return OPcache\volatile_store($key, 'old'); +} + +if ($mode === 'writer') { + wait_for_file($readyFile); + OPcache\volatile_delete($key); + file_put_contents($doneFile, 'done'); + return true; +} + +$first = fetch_or_miss($key); +file_put_contents($readyFile, 'ready'); +wait_for_file($doneFile); +$second = fetch_or_miss($key); +cleanup_files($readyFile, $doneFile); + +return $first === 'old' && $second === 'MISS'; diff --git a/ext/opcache/tests/helpers/volatile_cache_zts_threads_003_hit.inc b/ext/opcache/tests/helpers/volatile_cache_zts_threads_003_hit.inc new file mode 100644 index 000000000000..52f275d56123 --- /dev/null +++ b/ext/opcache/tests/helpers/volatile_cache_zts_threads_003_hit.inc @@ -0,0 +1,51 @@ += $deadline) { + throw new RuntimeException("timed out waiting for {$path}"); + } + + usleep(1000); + } +} + +function cleanup_files(string ...$paths): void +{ + foreach ($paths as $path) { + @unlink($path); + } +} + +$key = 'zts_lookup_cache_hit_key'; +$readyFile = sys_get_temp_dir() . '/opcache_zts_lookup_cache_hit.ready'; +$doneFile = sys_get_temp_dir() . '/opcache_zts_lookup_cache_hit.done'; + +if ($mode === 'init') { + cleanup_files($readyFile, $doneFile); + OPcache\volatile_clear(); + return OPcache\volatile_store($key, 'old'); +} + +if ($mode === 'writer') { + wait_for_file($readyFile); + $result = OPcache\volatile_store($key, 'new'); + file_put_contents($doneFile, 'done'); + return $result; +} + +$first = fetch_or_miss($key); +file_put_contents($readyFile, 'ready'); +wait_for_file($doneFile); +$second = fetch_or_miss($key); +cleanup_files($readyFile, $doneFile); + +return $first === 'old' && $second === 'new'; diff --git a/ext/opcache/tests/helpers/volatile_cache_zts_threads_003_materialized_clear.inc b/ext/opcache/tests/helpers/volatile_cache_zts_threads_003_materialized_clear.inc new file mode 100644 index 000000000000..35f93e67de14 --- /dev/null +++ b/ext/opcache/tests/helpers/volatile_cache_zts_threads_003_materialized_clear.inc @@ -0,0 +1,79 @@ + $i, + 'text' => str_repeat($prefix, 64), + 'nested' => ['value' => $i * $multiplier], + ]; + } + + return $rows; +} + +function wait_for_file(string $path): void +{ + $deadline = microtime(true) + 5.0; + + while (!file_exists($path)) { + if (microtime(true) >= $deadline) { + throw new RuntimeException("timed out waiting for {$path}"); + } + + usleep(1000); + } +} + +function cleanup_files(string ...$paths): void +{ + foreach ($paths as $path) { + @unlink($path); + } +} + +$key = 'zts_materialized_clear_payload'; +$overwriteKey = 'zts_materialized_clear_overwrite'; +$readyFile = sys_get_temp_dir() . '/opcache_zts_materialized_clear.ready'; +$doneFile = sys_get_temp_dir() . '/opcache_zts_materialized_clear.done'; + +if ($mode === 'init') { + cleanup_files($readyFile, $doneFile); + OPcache\volatile_clear(); + return OPcache\volatile_store($key, new ZtsClearPayload(build_zts_clear_rows('T', 5))); +} + +if ($mode === 'writer') { + wait_for_file($readyFile); + OPcache\volatile_clear(); + if (!OPcache\volatile_store($overwriteKey, new ZtsClearPayload(build_zts_clear_rows('X', 7)))) { + return false; + } + file_put_contents($doneFile, 'done'); + return true; +} + +$fetched = OPcache\volatile_fetch($key); +file_put_contents($readyFile, 'ready'); +wait_for_file($doneFile); +$before = $fetched->rows[123]['text']; +$fetched->rows[123]['text'] = 'changed after clear'; +$after = $fetched->rows[123]['text']; +$nested = $fetched->rows[123]['nested']['value']; + +$refetch = OPcache\volatile_fetch($key, 'MISS') === 'MISS' ? 'MISS' : 'HIT'; + +cleanup_files($readyFile, $doneFile); + +return $before === str_repeat('T', 64) + && $after === 'changed after clear' + && $nested === 615 + && $refetch === 'MISS'; diff --git a/ext/opcache/tests/helpers/volatile_cache_zts_threads_003_miss.inc b/ext/opcache/tests/helpers/volatile_cache_zts_threads_003_miss.inc new file mode 100644 index 000000000000..313f22f4b382 --- /dev/null +++ b/ext/opcache/tests/helpers/volatile_cache_zts_threads_003_miss.inc @@ -0,0 +1,52 @@ += $deadline) { + throw new RuntimeException("timed out waiting for {$path}"); + } + + usleep(1000); + } +} + +function cleanup_files(string ...$paths): void +{ + foreach ($paths as $path) { + @unlink($path); + } +} + +$key = 'zts_lookup_cache_miss_key'; +$readyFile = sys_get_temp_dir() . '/opcache_zts_lookup_cache_miss.ready'; +$doneFile = sys_get_temp_dir() . '/opcache_zts_lookup_cache_miss.done'; + +if ($mode === 'init') { + cleanup_files($readyFile, $doneFile); + OPcache\volatile_clear(); + OPcache\volatile_delete($key); + return true; +} + +if ($mode === 'writer') { + wait_for_file($readyFile); + $result = OPcache\volatile_store($key, 'created'); + file_put_contents($doneFile, 'done'); + return $result; +} + +$first = fetch_or_miss($key); +file_put_contents($readyFile, 'ready'); +wait_for_file($doneFile); +$second = fetch_or_miss($key); +cleanup_files($readyFile, $doneFile); + +return $first === 'MISS' && $second === 'created'; diff --git a/ext/opcache/tests/helpers/volatile_cache_zts_threads_003_volatile_static_class.inc b/ext/opcache/tests/helpers/volatile_cache_zts_threads_003_volatile_static_class.inc new file mode 100644 index 000000000000..a8703df93aba --- /dev/null +++ b/ext/opcache/tests/helpers/volatile_cache_zts_threads_003_volatile_static_class.inc @@ -0,0 +1,87 @@ + $i, + 'text' => str_repeat($kind, 64), + 'nested' => ['value' => $i * strlen($kind)], + ]; + } + + return [ + 'kind' => $kind, + 'rows' => $rows, + ]; +} + +function wait_for_file(string $path): void +{ + $deadline = microtime(true) + 5.0; + + while (!file_exists($path)) { + if (microtime(true) >= $deadline) { + throw new RuntimeException("timed out waiting for {$path}"); + } + + usleep(1000); + } +} + +function cleanup_files(string ...$paths): void +{ + foreach ($paths as $path) { + @unlink($path); + } +} + +$readyFile = sys_get_temp_dir() . '/opcache_zts_volatile_static_class.ready'; +$doneFile = sys_get_temp_dir() . '/opcache_zts_volatile_static_class.done'; + +if ($mode === 'init') { + cleanup_files($readyFile, $doneFile); + OPcache\volatile_clear(); + return true; +} + +if ($mode === 'writer') { + wait_for_file($readyFile); + $result = ZtsVolatileStaticClassPayload::resolve('writer'); + file_put_contents($doneFile, 'done'); + + return $result; +} + +$result = ZtsVolatileStaticClassPayload::resolve('reader'); +file_put_contents($readyFile, 'ready'); +wait_for_file($doneFile); +usleep(50000); + +for ($i = 0; $i < 100; $i++) { + $result = $result && ZtsVolatileStaticClassPayload::resolve('reader'); +} + +cleanup_files($readyFile, $doneFile); + +return $result; \ No newline at end of file diff --git a/ext/opcache/tests/static_cache_attribute_publish_userland_outside_lock_001.phpt b/ext/opcache/tests/static_cache_attribute_publish_userland_outside_lock_001.phpt new file mode 100644 index 000000000000..5871e3f946dd --- /dev/null +++ b/ext/opcache/tests/static_cache_attribute_publish_userland_outside_lock_001.phpt @@ -0,0 +1,63 @@ +--TEST-- +OPcache static attribute publish prepares userland-serialized values outside the cache write lock +--EXTENSIONS-- +opcache +--INI-- +opcache.enable=1 +opcache.enable_cli=1 +opcache.static_cache.volatile_size_mb=32 +opcache.static_cache.persistent_size_mb=32 +--FILE-- +backend, "\n"; + + if ($this->backend === 'volatile') { + OPcache\volatile_store('publish_inner_volatile', 'ok'); + } else { + OPcache\persistent_store('publish_inner_persistent', 'ok'); + } + + return ['backend' => $this->backend]; + } + + public function __unserialize(array $data): void + { + $this->backend = $data['backend']; + } +} + +class VolatilePublishTarget +{ + #[OPcache\VolatileStatic] + public static mixed $value; +} + +class PersistentPublishTarget +{ + #[OPcache\PersistentStatic] + public static mixed $value; +} + +OPcache\volatile_clear(); +VolatilePublishTarget::$value = new ReentrantPublishPayload('volatile'); +var_dump(OPcache\volatile_fetch('publish_inner_volatile')); + +OPcache\persistent_clear(); +PersistentPublishTarget::$value = new ReentrantPublishPayload('persistent'); +var_dump(OPcache\persistent_fetch('publish_inner_persistent')); + +?> +--EXPECT-- +serialize-volatile +string(2) "ok" +serialize-persistent +string(2) "ok" diff --git a/ext/opcache/tests/static_cache_attribute_signatures_001.phpt b/ext/opcache/tests/static_cache_attribute_signatures_001.phpt new file mode 100644 index 000000000000..7b3d7bff34b0 --- /dev/null +++ b/ext/opcache/tests/static_cache_attribute_signatures_001.phpt @@ -0,0 +1,50 @@ +--TEST-- +OPcache static cache attribute signatures expose TTL and strategy +--EXTENSIONS-- +opcache +--FILE-- +getName(); + } + + return 'complex'; +} + +$volatile = new ReflectionClass(OPcache\VolatileStatic::class); +$constructor = $volatile->getConstructor(); +foreach ($constructor->getParameters() as $parameter) { + $default = $parameter->isDefaultValueAvailable() ? $parameter->getDefaultValue() : null; + if ($default instanceof UnitEnum) { + $default = $default::class . '::' . $default->name; + } + echo $parameter->getName(), ':', describe_type($parameter->getType()), ':', var_export($default, true), "\n"; +} + +$attribute = new OPcache\VolatileStatic(5, OPcache\CacheStrategy::Tracking); +var_dump($attribute->ttl); +var_dump($attribute->strategy === OPcache\CacheStrategy::Tracking); + +try { + $attribute->ttl = 1; +} catch (Error $exception) { + echo "readonly-ttl\n"; +} + +var_dump((new ReflectionClass(OPcache\PersistentStatic::class))->getConstructor()); + +?> +--EXPECT-- +ttl:int:0 +strategy:OPcache\CacheStrategy:'OPcache\\CacheStrategy::Immediate' +int(5) +bool(true) +readonly-ttl +NULL diff --git a/ext/opcache/tests/static_cache_direct_cache_safe_disabled_without_static_cache_memory_001.phpt b/ext/opcache/tests/static_cache_direct_cache_safe_disabled_without_static_cache_memory_001.phpt new file mode 100644 index 000000000000..665089635d89 --- /dev/null +++ b/ext/opcache/tests/static_cache_direct_cache_safe_disabled_without_static_cache_memory_001.phpt @@ -0,0 +1,24 @@ +--TEST-- +OPcache __DirectCacheSafe does not touch internal classes when static cache memory is disabled +--EXTENSIONS-- +opcache +spl +--INI-- +opcache.enable=1 +opcache.enable_cli=1 +opcache.static_cache.volatile_size_mb=0 +opcache.static_cache.persistent_size_mb=0 +--FILE-- +getAttributes(OPcache\__DirectCacheSafe::class))); +var_dump(count((new ReflectionClass(DateTimeImmutable::class))->getAttributes(OPcache\__DirectCacheSafe::class))); +var_dump(count((new ReflectionClass(SplFixedArray::class))->getAttributes(OPcache\__DirectCacheSafe::class))); +var_dump(count((new ReflectionClass(ArrayObject::class))->getAttributes(OPcache\__DirectCacheSafe::class))); + +?> +--EXPECT-- +int(0) +int(0) +int(0) +int(0) diff --git a/ext/opcache/tests/static_cache_explicit_cache_complex_store_001.phpt b/ext/opcache/tests/static_cache_explicit_cache_complex_store_001.phpt new file mode 100644 index 000000000000..127af903b27e --- /dev/null +++ b/ext/opcache/tests/static_cache_explicit_cache_complex_store_001.phpt @@ -0,0 +1,104 @@ +--TEST-- +OPcache explicit cache stores handle mixed complex payloads +--EXTENSIONS-- +opcache +--INI-- +opcache.enable=1 +opcache.enable_cli=1 +opcache.static_cache.volatile_size_mb=32 +opcache.static_cache.persistent_size_mb=32 +--FILE-- + $this->id, 'name' => $this->name]; + } + + public function __unserialize(array $data): void + { + self::$unserializeCount++; + $this->id = $data['id']; + $this->name = $data['name']; + } + + public function info(): string + { + return $this->id . ':' . $this->name; + } +} + +$routePayload = [ + 'nested' => ['alpha' => [1, 2, 3], 'beta' => ['x' => 'y']], + 'headers' => ['cache-control' => 'public, max-age=60'], +]; + +var_dump(OPcache\volatile_store_array([ + 'route' => $routePayload, + 'meta' => new ExplicitPreparedUser('Alice', 30), + 'serial' => new ExplicitPreparedSerializableUser(7, 'Bob'), + 'internal' => new DateTimeImmutable('2026-06-15 09:30:00', new DateTimeZone('UTC')), +])); + +$route = OPcache\volatile_fetch('route'); +$meta = OPcache\volatile_fetch('meta'); +$serial = OPcache\volatile_fetch('serial'); +$internal = OPcache\volatile_fetch('internal'); + +var_dump($route['nested']['alpha'][2]); +var_dump($meta instanceof ExplicitPreparedUser); +var_dump($meta->name); +var_dump($meta->age); +var_dump($serial instanceof ExplicitPreparedSerializableUser); +var_dump($serial->info()); +var_dump(ExplicitPreparedSerializableUser::$serializeCount); +var_dump(ExplicitPreparedSerializableUser::$unserializeCount); +var_dump($internal instanceof DateTimeImmutable); +var_dump($internal->format('Y-m-d H:i:s')); + +OPcache\persistent_store('persistent_user', new ExplicitPreparedUser('Carol', 40)); +$persistent = OPcache\persistent_fetch('persistent_user'); + +var_dump($persistent instanceof ExplicitPreparedUser); +var_dump($persistent->name); +var_dump($persistent->age); + +var_dump(OPcache\volatile_store_array([])); + +?> +--EXPECT-- +bool(true) +int(3) +bool(true) +string(5) "Alice" +int(30) +bool(true) +string(5) "7:Bob" +int(1) +int(1) +bool(true) +string(19) "2026-06-15 09:30:00" +bool(true) +string(5) "Carol" +int(40) +bool(true) diff --git a/ext/opcache/tests/static_cache_explicit_cache_destructive_mutators_no_deadlock_001.phpt b/ext/opcache/tests/static_cache_explicit_cache_destructive_mutators_no_deadlock_001.phpt new file mode 100644 index 000000000000..1a4dfc3f16b5 --- /dev/null +++ b/ext/opcache/tests/static_cache_explicit_cache_destructive_mutators_no_deadlock_001.phpt @@ -0,0 +1,233 @@ +--TEST-- +OPcache explicit cache destructive mutators do not wait on reservation locks +--EXTENSIONS-- +opcache +pcntl +--SKIPIF-- + +--INI-- +opcache.enable=1 +opcache.enable_cli=1 +opcache.static_cache.volatile_size_mb=32 +opcache.static_cache.persistent_size_mb=32 +--FILE-- += $deadline) { + throw new RuntimeException("timed out waiting for {$path}"); + } + usleep(1000); + } +} + +function wait_for_result(string $path, float $seconds): bool +{ + $deadline = microtime(true) + $seconds; + while (!file_exists($path)) { + if (microtime(true) >= $deadline) { + return false; + } + usleep(1000); + } + + return true; +} + +function cache_clear(string $backend): void +{ + if ($backend === 'volatile') { + OPcache\volatile_clear(); + } else { + OPcache\persistent_clear(); + } +} + +function cache_store(string $backend, string $key, mixed $value): void +{ + if ($backend === 'volatile') { + OPcache\volatile_store($key, $value); + } else { + OPcache\persistent_store($key, $value); + } +} + +function cache_fetch(string $backend, string $key, mixed $default = null): mixed +{ + return $backend === 'volatile' + ? OPcache\volatile_fetch($key, $default) + : OPcache\persistent_fetch($key, $default); +} + +function cache_lock(string $backend, string $key): bool +{ + return $backend === 'volatile' + ? OPcache\volatile_lock($key) + : OPcache\persistent_lock($key); +} + +function cache_delete(string $backend, string $key): void +{ + if ($backend === 'volatile') { + OPcache\volatile_delete($key); + } else { + OPcache\persistent_delete($key); + } +} + +function run_reservation_cycle(string $backend, string $label, callable $operation): void +{ + $prefix = sys_get_temp_dir() . '/opcache_destructive_mutator_no_deadlock_' . getmypid() . '_' . $backend . '_' . $label; + $readyFile = $prefix . '.ready'; + $operationFile = $prefix . '.operation'; + $deleteFile = $prefix . '.delete'; + @unlink($readyFile); + @unlink($operationFile); + @unlink($deleteFile); + + $reservedKey = key_for_stripe($prefix . ':reserved', 200); + $blockedKey = key_for_stripe($prefix . ':blocked', 1); + + cache_clear($backend); + if (!cache_lock($backend, $reservedKey)) { + throw new RuntimeException("failed to reserve {$backend} key"); + } + + $operationPid = pcntl_fork(); + if ($operationPid === -1) { + throw new RuntimeException('pcntl_fork() failed'); + } + + if ($operationPid === 0) { + file_put_contents($readyFile, 'ready'); + $operation(); + file_put_contents($operationFile, "{$label}: done\n"); + exit(0); + } + + wait_for_file($readyFile); + usleep(200000); + + $deletePid = pcntl_fork(); + if ($deletePid === -1) { + throw new RuntimeException('pcntl_fork() failed'); + } + + if ($deletePid === 0) { + cache_delete($backend, $blockedKey); + file_put_contents($deleteFile, 'delete-low: done'); + exit(0); + } + + $operationDone = wait_for_result($operationFile, 2.0); + $deleteDone = wait_for_result($deleteFile, 2.0); + if (!$operationDone || !$deleteDone) { + echo "{$backend} {$label}: timeout\n"; + } + + cache_store($backend, $reservedKey, 'release'); + pcntl_waitpid($operationPid, $status); + pcntl_waitpid($deletePid, $status); + + if ($operationDone && $deleteDone) { + echo "{$backend} {$label}: no deadlock\n"; + } + + @unlink($readyFile); + @unlink($operationFile); + @unlink($deleteFile); +} + +function run_delete_wait_cycle(string $backend): void +{ + $prefix = sys_get_temp_dir() . '/opcache_destructive_mutator_no_deadlock_' . getmypid() . '_' . $backend . '_delete'; + $resultFile = $prefix . '.result'; + @unlink($resultFile); + + $key = key_for_stripe($prefix . ':reserved-delete', 17); + + cache_clear($backend); + cache_store($backend, $key, 'value'); + if (!cache_lock($backend, $key)) { + throw new RuntimeException("failed to reserve {$backend} delete key"); + } + + $pid = pcntl_fork(); + if ($pid === -1) { + throw new RuntimeException('pcntl_fork() failed'); + } + + if ($pid === 0) { + cache_delete($backend, $key); + file_put_contents($resultFile, 'delete: done'); + exit(0); + } + + $deleteDone = wait_for_result($resultFile, 2.0); + if (!$deleteDone) { + echo "{$backend} delete: timeout\n"; + } + + if ($deleteDone) { + echo "{$backend} delete: no wait\n"; + echo "{$backend} delete fetch: ", cache_fetch($backend, $key, 'MISS'), "\n"; + } + + cache_store($backend, $key, 'release'); + pcntl_waitpid($pid, $status); + + @unlink($resultFile); +} + +run_reservation_cycle('volatile', 'clear', static fn () => cache_clear('volatile')); +run_reservation_cycle('persistent', 'clear', static fn () => cache_clear('persistent')); +run_reservation_cycle('volatile', 'reset', static fn () => opcache_reset()); +run_delete_wait_cycle('volatile'); +run_delete_wait_cycle('persistent'); + +?> +--EXPECT-- +volatile clear: no deadlock +persistent clear: no deadlock +volatile reset: no deadlock +volatile delete: no wait +volatile delete fetch: MISS +persistent delete: no wait +persistent delete fetch: MISS +--CLEAN-- + diff --git a/ext/opcache/tests/static_cache_explicit_cache_fetch_array_001.phpt b/ext/opcache/tests/static_cache_explicit_cache_fetch_array_001.phpt new file mode 100644 index 000000000000..280acdbee447 --- /dev/null +++ b/ext/opcache/tests/static_cache_explicit_cache_fetch_array_001.phpt @@ -0,0 +1,115 @@ +--TEST-- +OPcache explicit fetch_array APIs handle hits, missing keys, and default values +--EXTENSIONS-- +opcache +--INI-- +opcache.enable=1 +opcache.enable_cli=1 +opcache.static_cache.volatile_size_mb=32 +opcache.static_cache.persistent_size_mb=32 +--FILE-- + 'value', + 'null' => null, + ]); + cache_store($backend, '42', 'numeric'); + + var_dump(cache_fetch_array($backend, ['hit', 'missing', 'null'])); + var_dump(cache_fetch_array($backend, ['hit', 'missing', 'null'], ['fallback'])); + var_dump(cache_fetch_array($backend, [42])); + var_dump(cache_fetch_array($backend, [])); +} + +?> +--EXPECT-- +volatile +array(3) { + ["hit"]=> + string(5) "value" + ["missing"]=> + NULL + ["null"]=> + NULL +} +array(3) { + ["hit"]=> + string(5) "value" + ["missing"]=> + array(1) { + [0]=> + string(8) "fallback" + } + ["null"]=> + NULL +} +array(1) { + ["42"]=> + string(7) "numeric" +} +array(0) { +} +persistent +array(3) { + ["hit"]=> + string(5) "value" + ["missing"]=> + NULL + ["null"]=> + NULL +} +array(3) { + ["hit"]=> + string(5) "value" + ["missing"]=> + array(1) { + [0]=> + string(8) "fallback" + } + ["null"]=> + NULL +} +array(1) { + ["42"]=> + string(7) "numeric" +} +array(0) { +} diff --git a/ext/opcache/tests/static_cache_explicit_cache_fetch_userland_outside_lock_001.phpt b/ext/opcache/tests/static_cache_explicit_cache_fetch_userland_outside_lock_001.phpt new file mode 100644 index 000000000000..bfaee7454dc2 --- /dev/null +++ b/ext/opcache/tests/static_cache_explicit_cache_fetch_userland_outside_lock_001.phpt @@ -0,0 +1,62 @@ +--TEST-- +OPcache explicit cache fetch materializes userland objects outside the cache read lock +--EXTENSIONS-- +opcache +--INI-- +opcache.enable=1 +opcache.enable_cli=1 +opcache.static_cache.volatile_size_mb=32 +opcache.static_cache.persistent_size_mb=32 +--FILE-- + $this->backend]; + } + + public function __unserialize(array $data): void + { + $this->backend = $data['backend']; + echo "unserialize-", $this->backend, "\n"; + + if ($this->backend === 'volatile') { + OPcache\volatile_store('fetch_inner_volatile', 'ok'); + } else { + OPcache\persistent_store('fetch_inner_persistent', 'ok'); + } + } +} + +foreach (['volatile', 'persistent'] as $backend) { + echo $backend, "\n"; + + if ($backend === 'volatile') { + OPcache\volatile_clear(); + OPcache\volatile_store('fetch_payload', new ReentrantFetchPayload($backend)); + var_dump(OPcache\volatile_fetch('fetch_payload') instanceof ReentrantFetchPayload); + var_dump(OPcache\volatile_fetch('fetch_inner_volatile')); + } else { + OPcache\persistent_clear(); + OPcache\persistent_store('fetch_payload', new ReentrantFetchPayload($backend)); + var_dump(OPcache\persistent_fetch('fetch_payload') instanceof ReentrantFetchPayload); + var_dump(OPcache\persistent_fetch('fetch_inner_persistent')); + } +} + +?> +--EXPECT-- +volatile +unserialize-volatile +bool(true) +string(2) "ok" +persistent +unserialize-persistent +bool(true) +string(2) "ok" diff --git a/ext/opcache/tests/static_cache_explicit_cache_fragmentation_relocation_001.phpt b/ext/opcache/tests/static_cache_explicit_cache_fragmentation_relocation_001.phpt new file mode 100644 index 000000000000..36b3dc103ec2 --- /dev/null +++ b/ext/opcache/tests/static_cache_explicit_cache_fragmentation_relocation_001.phpt @@ -0,0 +1,93 @@ +--TEST-- +OPcache explicit volatile and persistent caches relocate fragmented payload blocks before store failure +--EXTENSIONS-- +opcache +--INI-- +opcache.enable=1 +opcache.enable_cli=1 +opcache.static_cache.volatile_size_mb=8 +opcache.static_cache.persistent_size_mb=8 +--FILE-- + +--EXPECT-- +-- volatile -- +bool(true) +bool(true) +bool(true) +bool(true) +bool(true) +int(2400000) +int(1200000) +int(1200000) +int(1200000) +-- persistent -- +bool(true) +bool(true) +bool(true) +bool(true) +bool(true) +int(2400000) +int(1200000) +int(1200000) +int(1200000) diff --git a/ext/opcache/tests/static_cache_explicit_cache_fragmentation_relocation_skip_001.phpt b/ext/opcache/tests/static_cache_explicit_cache_fragmentation_relocation_skip_001.phpt new file mode 100644 index 000000000000..75d316599f01 --- /dev/null +++ b/ext/opcache/tests/static_cache_explicit_cache_fragmentation_relocation_skip_001.phpt @@ -0,0 +1,107 @@ +--TEST-- +OPcache explicit caches skip relocation when fragmented free memory still cannot satisfy the store +--EXTENSIONS-- +opcache +--INI-- +opcache.enable=1 +opcache.enable_cli=1 +opcache.static_cache.volatile_size_mb=8 +opcache.static_cache.persistent_size_mb=8 +--FILE-- +getMessage(), "\n"; + } + + var_dump(cache_fetch($kind, $prefix . 'probe') === $probe); + var_dump(strlen(cache_fetch($kind, $prefix . 'first'))); + var_dump(strlen(cache_fetch($kind, $prefix . 'third'))); + var_dump(strlen(cache_fetch($kind, $prefix . 'fourth'))); +} + +run_relocation_skip('volatile'); +run_relocation_skip('persistent'); + +?> +--EXPECT-- +-- volatile -- +bool(true) +bool(true) +bool(true) +bool(true) +bool(true) +bool(false) +bool(false) +int(1200000) +int(1200000) +int(1200000) +-- persistent -- +bool(true) +bool(true) +bool(true) +bool(true) +bool(true) +OPcache\StaticCacheException: not enough shared memory left +bool(false) +int(1200000) +int(1200000) +int(1200000) diff --git a/ext/opcache/tests/static_cache_explicit_cache_key_validation_001.phpt b/ext/opcache/tests/static_cache_explicit_cache_key_validation_001.phpt new file mode 100644 index 000000000000..6f15683579a0 --- /dev/null +++ b/ext/opcache/tests/static_cache_explicit_cache_key_validation_001.phpt @@ -0,0 +1,80 @@ +--TEST-- +OPcache explicit cache APIs reject empty and non-scalar cache keys +--EXTENSIONS-- +opcache +--INI-- +opcache.enable=1 +opcache.enable_cli=1 +opcache.static_cache.volatile_size_mb=32 +opcache.static_cache.persistent_size_mb=32 +--FILE-- +getMessage(), "\n"; + } +} + +class StringableKey +{ + public function __toString(): string + { + echo "__toString called\n"; + + return "stringable"; + } +} + +foreach (['volatile', 'persistent'] as $backend) { + echo $backend, "\n"; + + $store = $backend === 'volatile' ? OPcache\volatile_store(...) : OPcache\persistent_store(...); + $storeArray = $backend === 'volatile' ? OPcache\volatile_store_array(...) : OPcache\persistent_store_array(...); + $fetch = $backend === 'volatile' ? OPcache\volatile_fetch(...) : OPcache\persistent_fetch(...); + $fetchArray = $backend === 'volatile' ? OPcache\volatile_fetch_array(...) : OPcache\persistent_fetch_array(...); + $exists = $backend === 'volatile' ? OPcache\volatile_exists(...) : OPcache\persistent_exists(...); + $lock = $backend === 'volatile' ? OPcache\volatile_lock(...) : OPcache\persistent_lock(...); + $delete = $backend === 'volatile' ? OPcache\volatile_delete(...) : OPcache\persistent_delete(...); + $deleteArray = $backend === 'volatile' ? OPcache\volatile_delete_array(...) : OPcache\persistent_delete_array(...); + + dump_error('store-empty', static fn () => $store('', 'value')); + dump_error('store-array-empty', static fn () => $storeArray(['' => 'value'])); + dump_error('fetch-empty', static fn () => $fetch('')); + dump_error('fetch-array-empty', static fn () => $fetchArray([''])); + dump_error('fetch-array-object', static fn () => $fetchArray([new StringableKey()])); + dump_error('exists-empty', static fn () => $exists('')); + dump_error('lock-empty', static fn () => $lock('')); + dump_error('delete-empty', static fn () => $delete('')); + dump_error('delete-array-empty', static fn () => $deleteArray([''])); + dump_error('delete-array-object', static fn () => $deleteArray([new StringableKey()])); +} + +?> +--EXPECTF-- +volatile +store-empty: ValueError: OPcache\volatile_store(): Argument #1 ($key) must be a non-empty string +store-array-empty: ValueError: OPcache\volatile_store_array(): Argument #1 ($values) must be an array with non-empty string keys +fetch-empty: ValueError: OPcache\volatile_fetch(): Argument #1 ($key) must be a non-empty string +fetch-array-empty: ValueError: OPcache\volatile_fetch_array(): Argument #1 ($keys) must contain only non-empty string or int cache keys +fetch-array-object: ValueError: OPcache\volatile_fetch_array(): Argument #1 ($keys) must contain only non-empty string or int cache keys +exists-empty: ValueError: OPcache\volatile_exists(): Argument #1 ($key) must be a non-empty string +lock-empty: ValueError: OPcache\volatile_lock(): Argument #1 ($key) must be a non-empty string +delete-empty: ValueError: OPcache\volatile_delete(): Argument #1 ($key) must be a non-empty string +delete-array-empty: ValueError: OPcache\volatile_delete_array(): Argument #1 ($keys) must contain only non-empty string or int cache keys +delete-array-object: ValueError: OPcache\volatile_delete_array(): Argument #1 ($keys) must contain only non-empty string or int cache keys +persistent +store-empty: ValueError: OPcache\persistent_store(): Argument #1 ($key) must be a non-empty string +store-array-empty: ValueError: OPcache\persistent_store_array(): Argument #1 ($values) must be an array with non-empty string keys +fetch-empty: ValueError: OPcache\persistent_fetch(): Argument #1 ($key) must be a non-empty string +fetch-array-empty: ValueError: OPcache\persistent_fetch_array(): Argument #1 ($keys) must contain only non-empty string or int cache keys +fetch-array-object: ValueError: OPcache\persistent_fetch_array(): Argument #1 ($keys) must contain only non-empty string or int cache keys +exists-empty: ValueError: OPcache\persistent_exists(): Argument #1 ($key) must be a non-empty string +lock-empty: ValueError: OPcache\persistent_lock(): Argument #1 ($key) must be a non-empty string +delete-empty: ValueError: OPcache\persistent_delete(): Argument #1 ($key) must be a non-empty string +delete-array-empty: ValueError: OPcache\persistent_delete_array(): Argument #1 ($keys) must contain only non-empty string or int cache keys +delete-array-object: ValueError: OPcache\persistent_delete_array(): Argument #1 ($keys) must contain only non-empty string or int cache keys diff --git a/ext/opcache/tests/static_cache_explicit_cache_lock_clear_reset_001.phpt b/ext/opcache/tests/static_cache_explicit_cache_lock_clear_reset_001.phpt new file mode 100644 index 000000000000..307425c6ffc3 --- /dev/null +++ b/ext/opcache/tests/static_cache_explicit_cache_lock_clear_reset_001.phpt @@ -0,0 +1,73 @@ +--TEST-- +OPcache explicit cache clear and reset release cache locks +--EXTENSIONS-- +opcache +--INI-- +opcache.enable=1 +opcache.enable_cli=1 +opcache.static_cache.volatile_size_mb=32 +opcache.static_cache.persistent_size_mb=32 +--FILE-- + +--EXPECT-- +volatile +bool(true) +bool(true) +bool(true) +persistent +bool(true) +bool(true) +bool(true) +bool(true) +bool(true) +bool(true) +bool(true) +bool(true) diff --git a/ext/opcache/tests/static_cache_explicit_cache_lock_fork_001.phpt b/ext/opcache/tests/static_cache_explicit_cache_lock_fork_001.phpt new file mode 100644 index 000000000000..dd9f5121d578 --- /dev/null +++ b/ext/opcache/tests/static_cache_explicit_cache_lock_fork_001.phpt @@ -0,0 +1,202 @@ +--TEST-- +OPcache explicit cache locks are non-blocking for locked keys +--EXTENSIONS-- +opcache +pcntl +--SKIPIF-- + +--INI-- +opcache.enable=1 +opcache.enable_cli=1 +opcache.static_cache.volatile_size_mb=32 +opcache.static_cache.persistent_size_mb=32 +--FILE-- += $deadline) { + throw new RuntimeException("timed out waiting for {$path}"); + } + usleep(1000); + } +} + +function cache_clear(string $backend): void +{ + if ($backend === 'volatile') { + OPcache\volatile_clear(); + } else { + OPcache\persistent_clear(); + } +} + +function cache_store(string $backend, string $key, mixed $value): mixed +{ + if ($backend === 'volatile') { + return OPcache\volatile_store($key, $value); + } + + OPcache\persistent_store($key, $value); + return null; +} + +function cache_fetch(string $backend, string $key, mixed $default = null): mixed +{ + return $backend === 'volatile' + ? OPcache\volatile_fetch($key, $default) + : OPcache\persistent_fetch($key, $default); +} + +function cache_exists(string $backend, string $key): bool +{ + return $backend === 'volatile' + ? OPcache\volatile_exists($key) + : OPcache\persistent_exists($key); +} + +function cache_lock(string $backend, string $key): bool +{ + return $backend === 'volatile' + ? OPcache\volatile_lock($key) + : OPcache\persistent_lock($key); +} + +function run_child(string $backend, string $label, callable $operation): string +{ + $prefix = sys_get_temp_dir() . '/opcache_explicit_exists_lock_fork_' . getmypid() . '_' . $backend . '_' . $label; + $readyFile = $prefix . '.ready'; + $resultFile = $prefix . '.result'; + @unlink($readyFile); + @unlink($resultFile); + + $pid = pcntl_fork(); + if ($pid === -1) { + throw new RuntimeException('pcntl_fork() failed'); + } + + if ($pid === 0) { + pcntl_async_signals(true); + pcntl_signal(SIGALRM, static function () use ($resultFile): void { + file_put_contents($resultFile, "timeout\n"); + exit(1); + }); + pcntl_alarm(3); + file_put_contents($readyFile, 'ready'); + file_put_contents($resultFile, $operation()); + pcntl_alarm(0); + exit(0); + } + + wait_for_file($readyFile); + usleep(100000); + + return $pid . ':' . $resultFile . ':' . $readyFile; +} + +function finish_child(string $child): string +{ + [$pid, $resultFile, $readyFile] = explode(':', $child); + pcntl_waitpid((int) $pid, $status); + $result = file_get_contents($resultFile); + @unlink($readyFile); + @unlink($resultFile); + + return $result; +} + +function run_missing_lock_returns_false_when_locked(string $backend): void +{ + $key = $backend . '_missing_lock_blocks_' . getmypid(); + + cache_clear($backend); + var_dump(cache_lock($backend, $key)); + + $child = run_child($backend, 'missing_lock', static function () use ($backend, $key): string { + $locked = cache_lock($backend, $key); + $exists = cache_exists($backend, $key); + $value = cache_fetch($backend, $key, 'MISS'); + + return ($locked ? 'locked' : 'not locked') . "\n" . ($exists ? 'true' : 'false') . "\n" . $value . "\n"; + }); + + [, $resultFile] = explode(':', $child); + echo 'missing-lock blocked: ', file_exists($resultFile) ? 'no' : 'yes', "\n"; + cache_store($backend, $key, 'built'); + echo finish_child($child); +} + +function run_missing_without_lock_does_not_reserve(string $backend): void +{ + $key = $backend . '_missing_without_lock_' . getmypid(); + + cache_clear($backend); + var_dump(cache_exists($backend, $key)); + + $child = run_child($backend, 'missing_no_lock', static function () use ($backend, $key): string { + cache_store($backend, $key, 'child'); + + return cache_fetch($backend, $key, 'MISS') . "\n"; + }); + + echo finish_child($child); +} + +function run_existing_exists_does_not_reserve(string $backend): void +{ + $key = $backend . '_existing_lock_' . getmypid(); + + cache_clear($backend); + cache_store($backend, $key, 'owner'); + var_dump(cache_exists($backend, $key)); + + $child = run_child($backend, 'existing_lock', static function () use ($backend, $key): string { + cache_store($backend, $key, 'child'); + + return cache_fetch($backend, $key, 'MISS') . "\n"; + }); + + echo finish_child($child); +} + +foreach (['volatile', 'persistent'] as $backend) { + echo $backend, "\n"; + run_missing_lock_returns_false_when_locked($backend); + run_missing_without_lock_does_not_reserve($backend); + run_existing_exists_does_not_reserve($backend); +} + +?> +--EXPECT-- +volatile +bool(true) +missing-lock blocked: no +not locked +false +MISS +bool(false) +child +bool(true) +child +persistent +bool(true) +missing-lock blocked: no +not locked +false +MISS +bool(false) +child +bool(true) +child +--CLEAN-- + diff --git a/ext/opcache/tests/static_cache_explicit_cache_lock_parent_survives_child_shutdown_001.phpt b/ext/opcache/tests/static_cache_explicit_cache_lock_parent_survives_child_shutdown_001.phpt new file mode 100644 index 000000000000..398b62680da1 --- /dev/null +++ b/ext/opcache/tests/static_cache_explicit_cache_lock_parent_survives_child_shutdown_001.phpt @@ -0,0 +1,96 @@ +--TEST-- +OPcache explicit cache parent locks survive forked child shutdown +--EXTENSIONS-- +opcache +pcntl +--SKIPIF-- + +--INI-- +opcache.enable=1 +opcache.enable_cli=1 +opcache.static_cache.volatile_size_mb=32 +opcache.static_cache.persistent_size_mb=32 +--FILE-- + +--EXPECT-- +volatile +bool(true) +not locked +not locked +locked +persistent +bool(true) +not locked +not locked +locked +--CLEAN-- + diff --git a/ext/opcache/tests/static_cache_explicit_cache_lock_public_mutators_fork_001.phpt b/ext/opcache/tests/static_cache_explicit_cache_lock_public_mutators_fork_001.phpt new file mode 100644 index 000000000000..9c14aa053086 --- /dev/null +++ b/ext/opcache/tests/static_cache_explicit_cache_lock_public_mutators_fork_001.phpt @@ -0,0 +1,291 @@ +--TEST-- +OPcache explicit cache locks make builders wait while destructive mutators bypass reservations +--EXTENSIONS-- +opcache +pcntl +--SKIPIF-- + +--INI-- +opcache.enable=1 +opcache.enable_cli=1 +opcache.static_cache.volatile_size_mb=32 +opcache.static_cache.persistent_size_mb=32 +--FILE-- += $deadline) { + throw new RuntimeException("timed out waiting for {$path}"); + } + usleep(1000); + } +} + +function result_to_string(mixed $value): string +{ + return match (true) { + $value === null => 'null', + is_bool($value) => $value ? 'true' : 'false', + default => (string) $value, + }; +} + +function cache_clear(string $backend): void +{ + if ($backend === 'volatile') { + OPcache\volatile_clear(); + } else { + OPcache\persistent_clear(); + } +} + +function cache_store(string $backend, string $key, mixed $value): mixed +{ + if ($backend === 'volatile') { + return OPcache\volatile_store($key, $value); + } + + OPcache\persistent_store($key, $value); + return null; +} + +function cache_store_array(string $backend, array $values): mixed +{ + if ($backend === 'volatile') { + return OPcache\volatile_store_array($values); + } + + OPcache\persistent_store_array($values); + return null; +} + +function cache_fetch(string $backend, string $key, mixed $default = null): mixed +{ + return $backend === 'volatile' + ? OPcache\volatile_fetch($key, $default) + : OPcache\persistent_fetch($key, $default); +} + +function cache_lock(string $backend, string $key): bool +{ + return $backend === 'volatile' + ? OPcache\volatile_lock($key) + : OPcache\persistent_lock($key); +} + +function cache_delete(string $backend, string $key): void +{ + if ($backend === 'volatile') { + OPcache\volatile_delete($key); + } else { + OPcache\persistent_delete($key); + } +} + +function cache_delete_array(string $backend, array $keys): void +{ + if ($backend === 'volatile') { + OPcache\volatile_delete_array($keys); + } else { + OPcache\persistent_delete_array($keys); + } +} + +function run_blocked_mutator(string $backend, string $label, callable $operation, callable $release, callable $after): void +{ + $prefix = sys_get_temp_dir() . '/opcache_explicit_exists_lock_public_mutators_' . getmypid() . '_' . $backend . '_' . $label; + $readyFile = $prefix . '.ready'; + $resultFile = $prefix . '.result'; + @unlink($readyFile); + @unlink($resultFile); + + $pid = pcntl_fork(); + if ($pid === -1) { + throw new RuntimeException('pcntl_fork() failed'); + } + + if ($pid === 0) { + pcntl_async_signals(true); + pcntl_signal(SIGALRM, static function () use ($resultFile): void { + file_put_contents($resultFile, "timeout\n"); + exit(1); + }); + pcntl_alarm(3); + file_put_contents($readyFile, 'ready'); + $result = $operation(); + pcntl_alarm(0); + file_put_contents($resultFile, $label . ' result: ' . result_to_string($result) . "\n"); + exit(0); + } + + wait_for_file($readyFile); + usleep(200000); + echo $label, ' blocked: ', file_exists($resultFile) ? 'no' : 'yes', "\n"; + $release(); + pcntl_waitpid($pid, $status); + echo file_get_contents($resultFile); + $after(); + + @unlink($readyFile); + @unlink($resultFile); +} + +function reserve_missing(string $backend, string $key): void +{ + cache_clear($backend); + var_dump(cache_lock($backend, $key)); +} + +function run_backend_mutators(string $backend): void +{ + $baseKey = $backend . '_exists_lock_public_mutators_' . getmypid(); + + echo $backend, "\n"; + + $key = $baseKey . '_store'; + reserve_missing($backend, $key); + run_blocked_mutator( + $backend, + 'store', + static fn () => cache_store($backend, $key, 'child'), + static fn () => cache_store($backend, $key, 'owner'), + static fn () => print 'store value: ' . cache_fetch($backend, $key, 'MISS') . "\n", + ); + + $key = $baseKey . '_store_array'; + reserve_missing($backend, $key); + run_blocked_mutator( + $backend, + 'store_array', + static fn () => cache_store_array($backend, [$key => 'child']), + static fn () => cache_store($backend, $key, 'owner'), + static fn () => print 'store_array value: ' . cache_fetch($backend, $key, 'MISS') . "\n", + ); + + $key = $baseKey . '_delete'; + reserve_missing($backend, $key); + run_blocked_mutator( + $backend, + 'delete', + static fn () => cache_delete($backend, $key), + static fn () => cache_store($backend, $key, 'owner'), + static fn () => print 'delete value: ' . cache_fetch($backend, $key, 'MISS') . "\n", + ); + + $key = $baseKey . '_delete_array'; + reserve_missing($backend, $key); + run_blocked_mutator( + $backend, + 'delete_array', + static fn () => cache_delete_array($backend, [$key]), + static fn () => cache_store($backend, $key, 'owner'), + static fn () => print 'delete_array value: ' . cache_fetch($backend, $key, 'MISS') . "\n", + ); + + if ($backend === 'persistent') { + $key = $baseKey . '_atomic'; + reserve_missing($backend, $key); + run_blocked_mutator( + $backend, + 'atomic', + static fn () => OPcache\persistent_atomic_increment($key, 2), + static fn () => cache_store($backend, $key, 10), + static fn () => print 'atomic value: ' . cache_fetch($backend, $key, 'MISS') . "\n", + ); + + $key = $baseKey . '_atomic_decrement'; + reserve_missing($backend, $key); + run_blocked_mutator( + $backend, + 'atomic_decrement', + static fn () => OPcache\persistent_atomic_decrement($key, 3), + static fn () => cache_store($backend, $key, 10), + static fn () => print 'atomic_decrement value: ' . cache_fetch($backend, $key, 'MISS') . "\n", + ); + } + + $key = $baseKey . '_clear'; + $survivorKey = $baseKey . '_clear_survivor'; + cache_clear($backend); + cache_store($backend, $survivorKey, 'survives'); + echo "stored survivor\n"; + var_dump(cache_lock($backend, $key)); + run_blocked_mutator( + $backend, + 'clear', + static fn () => cache_clear($backend), + static fn () => cache_store($backend, $key, 'owner'), + static fn () => print 'clear values: ' . cache_fetch($backend, $key, 'MISS') . ',' . cache_fetch($backend, $survivorKey, 'MISS') . "\n", + ); +} + +run_backend_mutators('volatile'); +run_backend_mutators('persistent'); + +?> +--EXPECT-- +volatile +bool(true) +store blocked: yes +store result: true +store value: child +bool(true) +store_array blocked: yes +store_array result: true +store_array value: child +bool(true) +delete blocked: no +delete result: null +delete value: owner +bool(true) +delete_array blocked: no +delete_array result: null +delete_array value: owner +stored survivor +bool(true) +clear blocked: no +clear result: null +clear values: owner,MISS +persistent +bool(true) +store blocked: yes +store result: null +store value: child +bool(true) +store_array blocked: yes +store_array result: null +store_array value: child +bool(true) +delete blocked: no +delete result: null +delete value: owner +bool(true) +delete_array blocked: no +delete_array result: null +delete_array value: owner +bool(true) +atomic blocked: yes +atomic result: 12 +atomic value: 12 +bool(true) +atomic_decrement blocked: yes +atomic_decrement result: 7 +atomic_decrement value: 7 +stored survivor +bool(true) +clear blocked: no +clear result: null +clear values: owner,MISS +--CLEAN-- + diff --git a/ext/opcache/tests/static_cache_explicit_cache_lock_rshutdown_001.phpt b/ext/opcache/tests/static_cache_explicit_cache_lock_rshutdown_001.phpt new file mode 100644 index 000000000000..b796b82593ea --- /dev/null +++ b/ext/opcache/tests/static_cache_explicit_cache_lock_rshutdown_001.phpt @@ -0,0 +1,120 @@ +--TEST-- +OPcache explicit cache locks are released when request exits without store +--EXTENSIONS-- +opcache +pcntl +--SKIPIF-- + +--INI-- +opcache.enable=1 +opcache.enable_cli=1 +opcache.static_cache.volatile_size_mb=32 +opcache.static_cache.persistent_size_mb=32 +--FILE-- += $deadline) { + throw new RuntimeException("timed out waiting for {$path}"); + } + usleep(1000); + } +} + +function cache_clear(string $backend): void +{ + if ($backend === 'volatile') { + OPcache\volatile_clear(); + } else { + OPcache\persistent_clear(); + } +} + +function cache_store(string $backend, string $key, mixed $value): void +{ + if ($backend === 'volatile') { + OPcache\volatile_store($key, $value); + } else { + OPcache\persistent_store($key, $value); + } +} + +function cache_fetch(string $backend, string $key, mixed $default = null): mixed +{ + return $backend === 'volatile' + ? OPcache\volatile_fetch($key, $default) + : OPcache\persistent_fetch($key, $default); +} + +function cache_lock(string $backend, string $key): bool +{ + return $backend === 'volatile' + ? OPcache\volatile_lock($key) + : OPcache\persistent_lock($key); +} + +foreach (['volatile', 'persistent'] as $backend) { + $prefix = sys_get_temp_dir() . '/opcache_explicit_exists_lock_rshutdown_' . getmypid() . '_' . $backend; + $readyFile = $prefix . '.ready'; + $resultFile = $prefix . '.result'; + $key = $backend . '_exists_lock_rshutdown_' . getmypid(); + @unlink($readyFile); + @unlink($resultFile); + + echo $backend, "\n"; + cache_clear($backend); + + $pid = pcntl_fork(); + if ($pid === -1) { + throw new RuntimeException('pcntl_fork() failed'); + } + + if ($pid === 0) { + $locked = cache_lock($backend, $key); + file_put_contents($resultFile, ($locked ? 'true' : 'false') . "\n"); + file_put_contents($readyFile, 'ready'); + exit(0); + } + + wait_for_file($readyFile); + pcntl_waitpid($pid, $status); + echo file_get_contents($resultFile); + + pcntl_async_signals(true); + pcntl_signal(SIGALRM, static function (): void { + echo "timeout\n"; + exit(1); + }); + pcntl_alarm(3); + var_dump(cache_lock($backend, $key)); + pcntl_alarm(0); + cache_store($backend, $key, 'after'); + var_dump(cache_fetch($backend, $key)); + + @unlink($readyFile); + @unlink($resultFile); +} + +?> +--EXPECT-- +volatile +true +bool(true) +string(5) "after" +persistent +true +bool(true) +string(5) "after" +--CLEAN-- + diff --git a/ext/opcache/tests/static_cache_explicit_cache_lock_store_001.phpt b/ext/opcache/tests/static_cache_explicit_cache_lock_store_001.phpt new file mode 100644 index 000000000000..7779bc02631a --- /dev/null +++ b/ext/opcache/tests/static_cache_explicit_cache_lock_store_001.phpt @@ -0,0 +1,77 @@ +--TEST-- +OPcache explicit cache locks can reserve keys until store +--EXTENSIONS-- +opcache +--INI-- +opcache.enable=1 +opcache.enable_cli=1 +opcache.static_cache.volatile_size_mb=32 +opcache.static_cache.persistent_size_mb=32 +--FILE-- + +--EXPECT-- +volatile +bool(true) +bool(true) +bool(true) +string(5) "built" +persistent +bool(true) +bool(true) +bool(true) +string(5) "built" diff --git a/ext/opcache/tests/static_cache_explicit_cache_lock_stripe_collision_001.phpt b/ext/opcache/tests/static_cache_explicit_cache_lock_stripe_collision_001.phpt new file mode 100644 index 000000000000..f65c6739cab0 --- /dev/null +++ b/ext/opcache/tests/static_cache_explicit_cache_lock_stripe_collision_001.phpt @@ -0,0 +1,129 @@ +--TEST-- +OPcache explicit cache lock stripes conservatively serialize colliding keys +--EXTENSIONS-- +opcache +pcntl +--SKIPIF-- + +--INI-- +opcache.enable=1 +opcache.enable_cli=1 +opcache.static_cache.volatile_size_mb=32 +opcache.static_cache.persistent_size_mb=32 +--FILE-- += $deadline) { + throw new RuntimeException("timed out waiting for {$path}"); + } + usleep(1000); + } +} + +function run_collision_case(string $backend): void +{ + $prefix = sys_get_temp_dir() . '/opcache_lock_stripe_collision_' . getmypid() . '_' . $backend; + $resultFile = $prefix . '.result'; + $releaseFile = $prefix . '.release'; + @unlink($resultFile); + @unlink($releaseFile); + + cache_clear($backend); + + $baseKey = $backend . '_lock_stripe_base_' . getmypid(); + var_dump(cache_lock($backend, $baseKey)); + + $pid = pcntl_fork(); + if ($pid === -1) { + throw new RuntimeException('pcntl_fork() failed'); + } + + if ($pid === 0) { + $blockedKey = null; + for ($i = 0; $i < 4096; $i++) { + $key = $backend . '_lock_stripe_candidate_' . getmypid() . '_' . $i; + if (cache_lock($backend, $key)) { + cache_store($backend, $key, 'release'); + continue; + } + $blockedKey = $key; + break; + } + + if ($blockedKey === null) { + file_put_contents($resultFile, "no-collision\n"); + exit(1); + } + + file_put_contents($resultFile, "blocked\n"); + wait_for_file($releaseFile); + file_put_contents($resultFile, (cache_lock($backend, $blockedKey) ? "released\n" : "still-blocked\n"), FILE_APPEND); + cache_store($backend, $blockedKey, 'released'); + exit(0); + } + + wait_for_file($resultFile); + cache_store($backend, $baseKey, 'parent-release'); + file_put_contents($releaseFile, 'release'); + pcntl_waitpid($pid, $status); + + echo $backend, "\n"; + echo file_get_contents($resultFile); + @unlink($resultFile); + @unlink($releaseFile); +} + +run_collision_case('volatile'); +run_collision_case('persistent'); + +?> +--EXPECT-- +bool(true) +volatile +blocked +released +bool(true) +persistent +blocked +released +--CLEAN-- + diff --git a/ext/opcache/tests/static_cache_explicit_cache_prepare_nested_object_reuse_001.phpt b/ext/opcache/tests/static_cache_explicit_cache_prepare_nested_object_reuse_001.phpt new file mode 100644 index 000000000000..0201c531a4e0 --- /dev/null +++ b/ext/opcache/tests/static_cache_explicit_cache_prepare_nested_object_reuse_001.phpt @@ -0,0 +1,79 @@ +--TEST-- +OPcache explicit cache tracks nested userland object mutations across repeated stores +--EXTENSIONS-- +opcache +--INI-- +opcache.enable=1 +opcache.enable_cli=1 +opcache.static_cache.volatile_size_mb=32 +opcache.static_cache.persistent_size_mb=32 +--FILE-- + 'first', + 'leaf' => new PreparedNestedLeaf('leaf-first', 1), + 'rows' => [['state' => 'alpha']], +]; + +var_dump(OPcache\volatile_store('nested_volatile_first', $payload)); +OPcache\persistent_store('nested_persistent_first', $payload); + +$payload['name'] = 'second'; +$payload['leaf']->label = 'leaf-second'; +$payload['leaf']->revision = 2; +$payload['rows'][0]['state'] = 'beta'; + +var_dump(OPcache\volatile_store('nested_volatile_second', $payload)); +OPcache\persistent_store('nested_persistent_second', $payload); + +$volatileFirst = OPcache\volatile_fetch('nested_volatile_first'); +$volatileSecond = OPcache\volatile_fetch('nested_volatile_second'); +$persistentFirst = OPcache\persistent_fetch('nested_persistent_first'); +$persistentSecond = OPcache\persistent_fetch('nested_persistent_second'); + +echo $volatileFirst['name'], "\n"; +echo $volatileFirst['leaf']->label, "\n"; +echo $volatileFirst['leaf']->revision, "\n"; +echo $volatileFirst['rows'][0]['state'], "\n"; +echo $volatileSecond['name'], "\n"; +echo $volatileSecond['leaf']->label, "\n"; +echo $volatileSecond['leaf']->revision, "\n"; +echo $volatileSecond['rows'][0]['state'], "\n"; +echo $persistentFirst['name'], "\n"; +echo $persistentFirst['leaf']->label, "\n"; +echo $persistentFirst['leaf']->revision, "\n"; +echo $persistentFirst['rows'][0]['state'], "\n"; +echo $persistentSecond['name'], "\n"; +echo $persistentSecond['leaf']->label, "\n"; +echo $persistentSecond['leaf']->revision, "\n"; +echo $persistentSecond['rows'][0]['state'], "\n"; + +?> +--EXPECT-- +bool(true) +bool(true) +first +leaf-first +1 +alpha +second +leaf-second +2 +beta +first +leaf-first +1 +alpha +second +leaf-second +2 +beta diff --git a/ext/opcache/tests/static_cache_explicit_cache_prepare_safe_direct_mutation_001.phpt b/ext/opcache/tests/static_cache_explicit_cache_prepare_safe_direct_mutation_001.phpt new file mode 100644 index 000000000000..f9c909abee1e --- /dev/null +++ b/ext/opcache/tests/static_cache_explicit_cache_prepare_safe_direct_mutation_001.phpt @@ -0,0 +1,52 @@ +--TEST-- +OPcache explicit cache does not reuse prepared shared graphs across safe-direct object mutations +--EXTENSIONS-- +opcache +--INI-- +opcache.enable=1 +opcache.enable_cli=1 +opcache.static_cache.volatile_size_mb=32 +opcache.static_cache.persistent_size_mb=32 +--FILE-- + 'first', + 'date' => new DateTime('2026-05-01 10:30:45', new DateTimeZone('UTC')), +]; + +var_dump(OPcache\volatile_store('safe_direct_volatile_first', $payload)); +OPcache\persistent_store('safe_direct_persistent_first', $payload); + +$payload['name'] = 'second'; +$payload['date']->modify('+2 days'); + +var_dump(OPcache\volatile_store('safe_direct_volatile_second', $payload)); +OPcache\persistent_store('safe_direct_persistent_second', $payload); + +$volatileFirst = OPcache\volatile_fetch('safe_direct_volatile_first'); +$volatileSecond = OPcache\volatile_fetch('safe_direct_volatile_second'); +$persistentFirst = OPcache\persistent_fetch('safe_direct_persistent_first'); +$persistentSecond = OPcache\persistent_fetch('safe_direct_persistent_second'); + +echo $volatileFirst['name'], "\n"; +echo $volatileFirst['date']->format('Y-m-d H:i:s'), "\n"; +echo $volatileSecond['name'], "\n"; +echo $volatileSecond['date']->format('Y-m-d H:i:s'), "\n"; +echo $persistentFirst['name'], "\n"; +echo $persistentFirst['date']->format('Y-m-d H:i:s'), "\n"; +echo $persistentSecond['name'], "\n"; +echo $persistentSecond['date']->format('Y-m-d H:i:s'), "\n"; + +?> +--EXPECT-- +bool(true) +bool(true) +first +2026-05-01 10:30:45 +second +2026-05-03 10:30:45 +first +2026-05-01 10:30:45 +second +2026-05-03 10:30:45 diff --git a/ext/opcache/tests/static_cache_explicit_cache_prepare_scratch_reuse_001.phpt b/ext/opcache/tests/static_cache_explicit_cache_prepare_scratch_reuse_001.phpt new file mode 100644 index 000000000000..e3c8685497d5 --- /dev/null +++ b/ext/opcache/tests/static_cache_explicit_cache_prepare_scratch_reuse_001.phpt @@ -0,0 +1,84 @@ +--TEST-- +OPcache explicit cache preserves earlier snapshots across repeated stores from one source value +--EXTENSIONS-- +opcache +--INI-- +opcache.enable=1 +opcache.enable_cli=1 +opcache.static_cache.volatile_size_mb=32 +opcache.static_cache.persistent_size_mb=32 +--FILE-- + $i, + 'label' => str_repeat($prefix, 24), + 'nested' => ['value' => $i * $multiplier], + ]; + } + + return $rows; +} + +OPcache\volatile_clear(); +OPcache\persistent_clear(); + +$payload = new PreparedScratchPayload('first', build_rows('A', 3)); + +var_dump(OPcache\volatile_store('scratch_volatile_first', $payload)); +OPcache\persistent_store('scratch_persistent_first', $payload); + +$payload->name = 'second'; +$payload->rows[12]['label'] = str_repeat('B', 24); +$payload->rows[12]['nested']['value'] = 777; + +var_dump(OPcache\volatile_store('scratch_volatile_second', $payload)); +OPcache\persistent_store('scratch_persistent_second', $payload); + +$volatileFirst = OPcache\volatile_fetch('scratch_volatile_first'); +$volatileSecond = OPcache\volatile_fetch('scratch_volatile_second'); +$persistentFirst = OPcache\persistent_fetch('scratch_persistent_first'); +$persistentSecond = OPcache\persistent_fetch('scratch_persistent_second'); + +echo $volatileFirst->name, "\n"; +echo $volatileFirst->rows[12]['label'], "\n"; +echo $volatileFirst->rows[12]['nested']['value'], "\n"; +echo $volatileSecond->name, "\n"; +echo $volatileSecond->rows[12]['label'], "\n"; +echo $volatileSecond->rows[12]['nested']['value'], "\n"; +echo $persistentFirst->name, "\n"; +echo $persistentFirst->rows[12]['label'], "\n"; +echo $persistentFirst->rows[12]['nested']['value'], "\n"; +echo $persistentSecond->name, "\n"; +echo $persistentSecond->rows[12]['label'], "\n"; +echo $persistentSecond->rows[12]['nested']['value'], "\n"; + +?> +--EXPECT-- +bool(true) +bool(true) +first +AAAAAAAAAAAAAAAAAAAAAAAA +36 +second +BBBBBBBBBBBBBBBBBBBBBBBB +777 +first +AAAAAAAAAAAAAAAAAAAAAAAA +36 +second +BBBBBBBBBBBBBBBBBBBBBBBB +777 diff --git a/ext/opcache/tests/static_cache_explicit_cache_request_local_object_copy_001.phpt b/ext/opcache/tests/static_cache_explicit_cache_request_local_object_copy_001.phpt new file mode 100644 index 000000000000..038a95600fd8 --- /dev/null +++ b/ext/opcache/tests/static_cache_explicit_cache_request_local_object_copy_001.phpt @@ -0,0 +1,145 @@ +--TEST-- +OPcache explicit object fetch returns independent request-local object copies +--EXTENSIONS-- +opcache +--INI-- +opcache.enable=1 +opcache.enable_cli=1 +opcache.static_cache.volatile_size_mb=32 +opcache.static_cache.persistent_size_mb=32 +--FILE-- +label = 'cloned'; + } +} + +final class ExplicitFetchDateTimeCloneHookPayload extends DateTime +{ + public static int $cloneCount = 0; + + public string $label = ''; + + public function __clone() + { + self::$cloneCount++; + $this->label = 'cloned'; + $this->modify('+10 years'); + } +} + +function check_fetch_copies(string $name, callable $store, callable $fetch): void +{ + $store('request_local_copy', new ExplicitFetchCopyPayload($name . '-stored')); + + $first = $fetch('request_local_copy'); + $first->label = $name . '-first-mutated'; + $second = $fetch('request_local_copy'); + var_dump($first === $second); + echo $name, '-second-after-first-mutate=', $second->label, "\n"; + + $second->label = $name . '-mutated'; + echo $name, '-first-after-second-mutate=', $first->label, "\n"; + + $third = $fetch('request_local_copy'); + var_dump($first === $third); + echo $name, '-third=', $third->label, "\n"; + + ExplicitFetchCloneHookPayload::$cloneCount = 0; + $store('request_local_clone_hook', new ExplicitFetchCloneHookPayload($name . '-stored')); + $hookFirst = $fetch('request_local_clone_hook'); + $hookSecond = $fetch('request_local_clone_hook'); + var_dump($hookFirst === $hookSecond); + echo $name, '-clone-hook=', ExplicitFetchCloneHookPayload::$cloneCount, ':', + $hookFirst->label, ':', $hookSecond->label, "\n"; + + ExplicitFetchDateTimeCloneHookPayload::$cloneCount = 0; + $date = new ExplicitFetchDateTimeCloneHookPayload('2026-05-15 12:34:56', new DateTimeZone('Asia/Tokyo')); + $date->label = $name . '-stored'; + $store('request_local_datetime_clone_hook', $date); + $dateFirst = $fetch('request_local_datetime_clone_hook'); + $dateSecond = $fetch('request_local_datetime_clone_hook'); + var_dump($dateFirst === $dateSecond); + echo $name, '-datetime-clone-hook=', ExplicitFetchDateTimeCloneHookPayload::$cloneCount, ':', + $dateFirst->label, ':', $dateSecond->label, "\n"; + echo $name, '-datetime-second-before=', $dateSecond->format(DateTimeInterface::ATOM), "\n"; + + $dateFirst->modify('+1 day'); + $dateFirst->label = $name . '-datetime-first-mutated'; + echo $name, '-datetime-second-after-first-mutate=', $dateSecond->format(DateTimeInterface::ATOM), ':', + $dateSecond->label, "\n"; + + $dateSecond->modify('+2 days'); + $dateSecond->label = $name . '-datetime-second-mutated'; + echo $name, '-datetime-first-after-second-mutate=', $dateFirst->format(DateTimeInterface::ATOM), ':', + $dateFirst->label, "\n"; + + $dateThird = $fetch('request_local_datetime_clone_hook'); + var_dump($dateFirst === $dateThird); + echo $name, '-datetime-third=', $dateThird->format(DateTimeInterface::ATOM), ':', + $dateThird->label, "\n"; +} + +OPcache\volatile_clear(); +OPcache\persistent_clear(); + +check_fetch_copies( + 'volatile', + static fn (string $key, mixed $payload): bool => OPcache\volatile_store($key, $payload), + static fn (string $key): mixed => OPcache\volatile_fetch($key), +); + +check_fetch_copies( + 'persistent', + static function (string $key, mixed $payload): bool { + OPcache\persistent_store($key, $payload); + + return true; + }, + static fn (string $key): mixed => OPcache\persistent_fetch($key), +); + +?> +--EXPECT-- +bool(false) +volatile-second-after-first-mutate=volatile-stored +volatile-first-after-second-mutate=volatile-first-mutated +bool(false) +volatile-third=volatile-stored +bool(false) +volatile-clone-hook=0:volatile-stored:volatile-stored +bool(false) +volatile-datetime-clone-hook=0:volatile-stored:volatile-stored +volatile-datetime-second-before=2026-05-15T12:34:56+09:00 +volatile-datetime-second-after-first-mutate=2026-05-15T12:34:56+09:00:volatile-stored +volatile-datetime-first-after-second-mutate=2026-05-16T12:34:56+09:00:volatile-datetime-first-mutated +bool(false) +volatile-datetime-third=2026-05-15T12:34:56+09:00:volatile-stored +bool(false) +persistent-second-after-first-mutate=persistent-stored +persistent-first-after-second-mutate=persistent-first-mutated +bool(false) +persistent-third=persistent-stored +bool(false) +persistent-clone-hook=0:persistent-stored:persistent-stored +bool(false) +persistent-datetime-clone-hook=0:persistent-stored:persistent-stored +persistent-datetime-second-before=2026-05-15T12:34:56+09:00 +persistent-datetime-second-after-first-mutate=2026-05-15T12:34:56+09:00:persistent-stored +persistent-datetime-first-after-second-mutate=2026-05-16T12:34:56+09:00:persistent-datetime-first-mutated +bool(false) +persistent-datetime-third=2026-05-15T12:34:56+09:00:persistent-stored diff --git a/ext/opcache/tests/static_cache_explicit_cache_request_local_safe_direct_slot_001.phpt b/ext/opcache/tests/static_cache_explicit_cache_request_local_safe_direct_slot_001.phpt new file mode 100644 index 000000000000..e580dfe9fc32 --- /dev/null +++ b/ext/opcache/tests/static_cache_explicit_cache_request_local_safe_direct_slot_001.phpt @@ -0,0 +1,140 @@ +--TEST-- +OPcache explicit fetch request-local slots clone __DirectCacheSafe objects +--EXTENSIONS-- +opcache +spl +--INI-- +opcache.enable=1 +opcache.enable_cli=1 +opcache.static_cache.volatile_size_mb=32 +opcache.static_cache.persistent_size_mb=32 +--FILE-- +peer = $peer; + + store_value($backend, $backend . '-date', [ + 'date' => $date, + 'again' => $date, + 'peer' => $peer, + ]); + + $first = fetch_value($backend, $backend . '-date'); + $warm = fetch_value($backend, $backend . '-date'); + echo $backend, '-date-same-object=', $warm['date'] === $warm['again'] ? 'yes' : 'no', "\n"; + echo $backend, '-date-shared-peer=', $warm['date']->peer === $warm['peer'] ? 'yes' : 'no', "\n"; + echo $backend, '-date-clone-calls=', SafeDirectSlotDateTime::$cloneCalls, "\n"; + + $first['date']->modify('+1 day'); + $first['date']->peer->label = $backend . '-changed'; + $second = fetch_value($backend, $backend . '-date'); + echo $backend, '-date-second=', $second['date']->format('Y-m-d'), ':', $second['date']->peer->label, "\n"; + echo $backend, '-date-second-is-first=', $second['date'] === $first['date'] ? 'yes' : 'no', "\n"; + echo $backend, '-date-clone-calls=', SafeDirectSlotDateTime::$cloneCalls, "\n"; + + $collectionPeer = new SafeDirectSlotPeer($backend . '-collection-stored'); + $collection = new SafeDirectSlotArrayObject(['peer' => $collectionPeer]); + + store_value($backend, $backend . '-collection', [ + 'collection' => $collection, + 'again' => $collection, + 'peer' => $collectionPeer, + ]); + + $collectionFirst = fetch_value($backend, $backend . '-collection'); + $collectionWarm = fetch_value($backend, $backend . '-collection'); + echo $backend, '-collection-same-object=', $collectionWarm['collection'] === $collectionWarm['again'] ? 'yes' : 'no', "\n"; + echo $backend, '-collection-shared-peer=', $collectionWarm['collection']['peer'] === $collectionWarm['peer'] ? 'yes' : 'no', "\n"; + echo $backend, '-collection-clone-calls=', SafeDirectSlotArrayObject::$cloneCalls, "\n"; + + $collectionFirst['collection']['peer']->label = $backend . '-collection-changed'; + $collectionFirst['collection']['local'] = 'local-only'; + $collectionSecond = fetch_value($backend, $backend . '-collection'); + echo $backend, '-collection-second=', $collectionSecond['collection']['peer']->label, ':', count($collectionSecond['collection']), "\n"; + echo $backend, '-collection-second-is-first=', $collectionSecond['collection'] === $collectionFirst['collection'] ? 'yes' : 'no', "\n"; + echo $backend, '-collection-clone-calls=', SafeDirectSlotArrayObject::$cloneCalls, "\n"; +} + +OPcache\volatile_clear(); +OPcache\persistent_clear(); + +run_safe_direct_slot_scenario('volatile'); +run_safe_direct_slot_scenario('persistent'); + +?> +--EXPECT-- +bool(true) +volatile-date-same-object=no +volatile-date-shared-peer=no +volatile-date-clone-calls=0 +volatile-date-second=2026-01-01:volatile-stored +volatile-date-second-is-first=no +volatile-date-clone-calls=0 +bool(true) +volatile-collection-same-object=no +volatile-collection-shared-peer=no +volatile-collection-clone-calls=0 +volatile-collection-second=volatile-collection-stored:1 +volatile-collection-second-is-first=no +volatile-collection-clone-calls=0 +persistent-stored +persistent-date-same-object=no +persistent-date-shared-peer=no +persistent-date-clone-calls=0 +persistent-date-second=2026-01-01:persistent-stored +persistent-date-second-is-first=no +persistent-date-clone-calls=0 +persistent-stored +persistent-collection-same-object=no +persistent-collection-shared-peer=no +persistent-collection-clone-calls=0 +persistent-collection-second=persistent-collection-stored:1 +persistent-collection-second-is-first=no +persistent-collection-clone-calls=0 diff --git a/ext/opcache/tests/static_cache_explicit_cache_request_local_slot_001.phpt b/ext/opcache/tests/static_cache_explicit_cache_request_local_slot_001.phpt new file mode 100644 index 000000000000..e7d188a51eeb --- /dev/null +++ b/ext/opcache/tests/static_cache_explicit_cache_request_local_slot_001.phpt @@ -0,0 +1,130 @@ +--TEST-- +OPcache explicit fetch clones object-bearing request-local results +--EXTENSIONS-- +opcache +spl +--INI-- +opcache.enable=1 +opcache.enable_cli=1 +opcache.static_cache.volatile_size_mb=32 +opcache.static_cache.persistent_size_mb=32 +--FILE-- +label, "\n"; +} + +OPcache\volatile_clear(); +OPcache\persistent_clear(); + +$volatilePayload = ['object' => new ExplicitRequestLocalSlotPayload('volatile-stored')]; +var_dump(OPcache\volatile_store('request_local_slot', $volatilePayload)); + +$volatileFirst = OPcache\volatile_fetch('request_local_slot'); +$volatileWarm = OPcache\volatile_fetch('request_local_slot'); +var_dump($volatileFirst['object'] === $volatileWarm['object']); +$volatileFirst['object']->label = 'volatile-mutated-in-request'; +$volatileSecond = OPcache\volatile_fetch('request_local_slot'); + +var_dump($volatileFirst['object'] === $volatileSecond['object']); +dump_label('volatile-second', $volatileSecond['object']); + +var_dump(OPcache\volatile_store('request_local_slot', ['object' => new ExplicitRequestLocalSlotPayload('volatile-replaced')])); +$volatileReplaced = OPcache\volatile_fetch('request_local_slot'); +dump_label('volatile-replaced', $volatileReplaced['object']); + +OPcache\volatile_delete('request_local_slot'); +var_dump(OPcache\volatile_fetch('request_local_slot', 'volatile-missing')); + +var_dump(OPcache\volatile_store('request_local_ttl', ['object' => new ExplicitRequestLocalSlotPayload('volatile-ttl')], 1)); +$volatileTtl = OPcache\volatile_fetch('request_local_ttl'); +dump_label('volatile-ttl-first', $volatileTtl['object']); +sleep(2); +var_dump(OPcache\volatile_fetch('request_local_ttl', 'volatile-expired')); + +var_dump(OPcache\volatile_store('request_local_datetime', new DateTime('2026-01-01 00:00:00', new DateTimeZone('UTC')))); +$volatileDateFirst = OPcache\volatile_fetch('request_local_datetime'); +$volatileDateWarm = OPcache\volatile_fetch('request_local_datetime'); +var_dump($volatileDateFirst === $volatileDateWarm); +$volatileDateFirst->modify('+1 day'); +$volatileDateSecond = OPcache\volatile_fetch('request_local_datetime'); +var_dump($volatileDateFirst === $volatileDateSecond); +echo $volatileDateSecond->format('Y-m-d'), "\n"; + +var_dump(OPcache\volatile_store('request_local_array_object', new ArrayObject(['label' => 'stored']))); +$volatileArrayObjectFirst = OPcache\volatile_fetch('request_local_array_object'); +$volatileArrayObjectWarm = OPcache\volatile_fetch('request_local_array_object'); +var_dump($volatileArrayObjectFirst === $volatileArrayObjectWarm); +$volatileArrayObjectFirst->exchangeArray(['label' => 'changed']); +$volatileArrayObjectSecond = OPcache\volatile_fetch('request_local_array_object'); +var_dump($volatileArrayObjectFirst === $volatileArrayObjectSecond); +echo $volatileArrayObjectSecond['label'], "\n"; + +$volatileList = new ExplicitRequestLocalSlotUnsupportedList(); +$volatileList->push('stored'); +var_dump(OPcache\volatile_store('request_local_unsupported_internal', $volatileList)); +$volatileListFirst = OPcache\volatile_fetch('request_local_unsupported_internal'); +$volatileListWarm = OPcache\volatile_fetch('request_local_unsupported_internal'); +var_dump($volatileListFirst === $volatileListWarm); +$volatileListFirst->push('changed'); +$volatileListSecond = OPcache\volatile_fetch('request_local_unsupported_internal'); +var_dump($volatileListFirst === $volatileListSecond); +echo $volatileListSecond->count(), ':', $volatileListSecond->bottom(), "\n"; + +OPcache\persistent_store('request_local_slot', ['object' => new ExplicitRequestLocalSlotPayload('persistent-stored')]); +$persistentFirst = OPcache\persistent_fetch('request_local_slot'); +$persistentWarm = OPcache\persistent_fetch('request_local_slot'); +var_dump($persistentFirst['object'] === $persistentWarm['object']); +$persistentFirst['object']->label = 'persistent-mutated-in-request'; +$persistentSecond = OPcache\persistent_fetch('request_local_slot'); + +var_dump($persistentFirst['object'] === $persistentSecond['object']); +dump_label('persistent-second', $persistentSecond['object']); + +OPcache\persistent_store('request_local_slot', ['object' => new ExplicitRequestLocalSlotPayload('persistent-replaced')]); +$persistentReplaced = OPcache\persistent_fetch('request_local_slot'); +dump_label('persistent-replaced', $persistentReplaced['object']); + +OPcache\persistent_delete('request_local_slot'); +var_dump(OPcache\persistent_fetch('request_local_slot', 'persistent-missing')); + +?> +--EXPECT-- +bool(true) +bool(false) +bool(false) +volatile-second=volatile-stored +bool(true) +volatile-replaced=volatile-replaced +string(16) "volatile-missing" +bool(true) +volatile-ttl-first=volatile-ttl +string(16) "volatile-expired" +bool(true) +bool(false) +bool(false) +2026-01-01 +bool(true) +bool(false) +bool(false) +stored +bool(true) +bool(false) +bool(false) +1:stored +bool(false) +bool(false) +persistent-second=persistent-stored +persistent-replaced=persistent-replaced +string(18) "persistent-missing" diff --git a/ext/opcache/tests/static_cache_explicit_cache_shared_graph_array_001.phpt b/ext/opcache/tests/static_cache_explicit_cache_shared_graph_array_001.phpt new file mode 100644 index 000000000000..f9ccc3867918 --- /dev/null +++ b/ext/opcache/tests/static_cache_explicit_cache_shared_graph_array_001.phpt @@ -0,0 +1,66 @@ +--TEST-- +OPcache explicit cache stores nested array shared graphs without relocation-sensitive layout +--EXTENSIONS-- +opcache +--INI-- +opcache.enable=1 +opcache.enable_cli=1 +opcache.static_cache.volatile_size_mb=32 +opcache.static_cache.persistent_size_mb=32 +--FILE-- + [ + ['path' => '/users', 'methods' => ['GET', 'POST']], + ['path' => '/posts', 'defaults' => ['page' => 1, 'sort' => 'desc']], + ], + 'nodes' => [ + new RelocatableArrayNode(['flags' => ['auth' => true, 'cache' => false]]), + new RelocatableArrayNode(['flags' => ['auth' => false, 'cache' => true]]), + ], +]; + +var_dump(OPcache\volatile_store('relocatable-array-payload', $payload)); +$copy = OPcache\volatile_fetch('relocatable-array-payload'); + +var_dump($copy['routes'][0]['methods'][1]); +var_dump($copy['routes'][1]['defaults']['page']); +var_dump($copy['nodes'][0] instanceof RelocatableArrayNode); +var_dump($copy['nodes'][0]->metadata['flags']['auth']); +var_dump($copy['nodes'][1]->metadata['flags']['cache']); + +OPcache\persistent_store('relocatable-array-object', new RelocatableArrayNode([ + 'matrix' => [[1, 2], [3, 4]], +])); +$global = OPcache\persistent_fetch('relocatable-array-object'); + +var_dump($global instanceof RelocatableArrayNode); +var_dump($global->metadata['matrix'][1][0]); + +var_dump(OPcache\volatile_store('relocatable-array-payload', [ + 'routes' => [['path' => '/health', 'methods' => ['GET']]], +])); +$updated = OPcache\volatile_fetch('relocatable-array-payload'); + +var_dump($updated['routes'][0]['path']); +var_dump($updated['routes'][0]['methods'][0]); + +?> +--EXPECT-- +bool(true) +string(4) "POST" +int(1) +bool(true) +bool(true) +bool(true) +bool(true) +int(3) +bool(true) +string(7) "/health" +string(3) "GET" diff --git a/ext/opcache/tests/static_cache_explicit_cache_signatures_001.phpt b/ext/opcache/tests/static_cache_explicit_cache_signatures_001.phpt new file mode 100644 index 000000000000..da6e31c55e0d --- /dev/null +++ b/ext/opcache/tests/static_cache_explicit_cache_signatures_001.phpt @@ -0,0 +1,68 @@ +--TEST-- +OPcache explicit cache signatures expose storable values and array fallbacks +--EXTENSIONS-- +opcache +--FILE-- +getName(); + + return $type->allowsNull() && $name !== 'null' && $name !== 'mixed' ? '?' . $name : $name; + } + + $order = [ + 'null' => 0, + 'bool' => 1, + 'int' => 2, + 'float' => 3, + 'string' => 4, + 'array' => 5, + 'object' => 6, + ]; + $names = array_map( + static fn (ReflectionNamedType $named): string => $named->getName(), + $type->getTypes(), + ); + usort($names, static fn (string $left, string $right): int => ($order[$left] ?? 99) <=> ($order[$right] ?? 99)); + + return implode('|', $names); +} + +foreach ([ + 'OPcache\\volatile_store' => ['value'], + 'OPcache\\volatile_fetch' => ['default'], + 'OPcache\\volatile_fetch_array' => ['default'], + 'OPcache\\persistent_store' => ['value'], + 'OPcache\\persistent_fetch' => ['default'], + 'OPcache\\persistent_fetch_array' => ['default'], +] as $function => $parameters) { + $reflection = new ReflectionFunction($function); + $parts = [$function]; + $parameterMap = []; + foreach ($reflection->getParameters() as $parameter) { + $parameterMap[$parameter->getName()] = $parameter; + } + foreach ($parameters as $parameterName) { + $parameter = $parameterMap[$parameterName]; + $parts[] = '$' . $parameterName . '=' . describe_type($parameter->getType()); + } + $parts[] = 'params=' . $reflection->getNumberOfRequiredParameters() . '/' . $reflection->getNumberOfParameters(); + $parts[] = 'return=' . describe_type($reflection->getReturnType()); + echo implode(' ', $parts), "\n"; +} + +?> +--EXPECT-- +OPcache\volatile_store $value=null|bool|int|float|string|array|object params=2/3 return=bool +OPcache\volatile_fetch $default=null|bool|int|float|string|array|object params=1/2 return=null|bool|int|float|string|array|object +OPcache\volatile_fetch_array $default=?array params=1/2 return=?array +OPcache\persistent_store $value=null|bool|int|float|string|array|object params=2/2 return=void +OPcache\persistent_fetch $default=null|bool|int|float|string|array|object params=1/2 return=null|bool|int|float|string|array|object +OPcache\persistent_fetch_array $default=?array params=1/2 return=?array diff --git a/ext/opcache/tests/static_cache_explicit_cache_store_array_invalid_key_001.phpt b/ext/opcache/tests/static_cache_explicit_cache_store_array_invalid_key_001.phpt new file mode 100644 index 000000000000..7e34a185aa43 --- /dev/null +++ b/ext/opcache/tests/static_cache_explicit_cache_store_array_invalid_key_001.phpt @@ -0,0 +1,39 @@ +--TEST-- +OPcache explicit store_array APIs reject non-string keys before storing any entries +--EXTENSIONS-- +opcache +--INI-- +opcache.enable=1 +opcache.enable_cli=1 +opcache.static_cache.volatile_size_mb=32 +opcache.static_cache.persistent_size_mb=32 +--FILE-- + ['OPcache\\volatile_store_array', 'OPcache\\volatile_fetch'], + 'persistent' => ['OPcache\\persistent_store_array', 'OPcache\\persistent_fetch'], +] as $label => [$storeArray, $fetch]) { + $key = $label . '_first'; + + try { + $storeArray([ + $key => 'stored', + 0 => 'bad', + ]); + } catch (ValueError $exception) { + echo $label, ': ', $exception->getMessage(), "\n"; + } + + var_dump($fetch($key, 'missing')); +} + +?> +--EXPECT-- +volatile: OPcache\volatile_store_array(): Argument #1 ($values) must be an array with non-empty string keys +string(7) "missing" +persistent: OPcache\persistent_store_array(): Argument #1 ($values) must be an array with non-empty string keys +string(7) "missing" diff --git a/ext/opcache/tests/static_cache_explicit_cache_store_delete_fork_reuse_001.phpt b/ext/opcache/tests/static_cache_explicit_cache_store_delete_fork_reuse_001.phpt new file mode 100644 index 000000000000..324ceebb59a9 --- /dev/null +++ b/ext/opcache/tests/static_cache_explicit_cache_store_delete_fork_reuse_001.phpt @@ -0,0 +1,109 @@ +--TEST-- +OPcache explicit volatile and persistent delete frees payload memory across processes +--EXTENSIONS-- +opcache +pcntl +--SKIPIF-- + +--INI-- +opcache.enable=1 +opcache.enable_cli=1 +opcache.static_cache.volatile_size_mb=8 +opcache.static_cache.persistent_size_mb=8 +--FILE-- + +--EXPECT-- +-- volatile -- +bool(true) +bool(true) +bool(true) +string(7) "missing" +bool(true) +int(1800000) +int(1800000) +int(1500000) +-- persistent -- +bool(true) +bool(true) +bool(true) +string(7) "missing" +bool(true) +int(1800000) +int(1800000) +int(1500000) diff --git a/ext/opcache/tests/static_cache_explicit_cache_store_delete_request_reuse_001.phpt b/ext/opcache/tests/static_cache_explicit_cache_store_delete_request_reuse_001.phpt new file mode 100644 index 000000000000..916d1621780b --- /dev/null +++ b/ext/opcache/tests/static_cache_explicit_cache_store_delete_request_reuse_001.phpt @@ -0,0 +1,122 @@ +--TEST-- +OPcache explicit volatile and persistent delete frees payload memory across requests +--EXTENSIONS-- +opcache +--CONFLICTS-- +server +--FILE-- + +--CLEAN-- + +--EXPECT-- +-- volatile -- +bool(true) +bool(true) +bool(true) +string(7) "missing" +bool(true) +int(1800000) +int(1800000) +int(1500000) +-- persistent -- +bool(true) +bool(true) +bool(true) +string(7) "missing" +bool(true) +int(1800000) +int(1800000) +int(1500000) diff --git a/ext/opcache/tests/static_cache_explicit_cache_store_delete_zts_threads_001.phpt b/ext/opcache/tests/static_cache_explicit_cache_store_delete_zts_threads_001.phpt new file mode 100644 index 000000000000..ab3fa6fd11c0 --- /dev/null +++ b/ext/opcache/tests/static_cache_explicit_cache_store_delete_zts_threads_001.phpt @@ -0,0 +1,246 @@ +--TEST-- +OPcache explicit volatile and persistent delete frees payload memory across threads +--EXTENSIONS-- +opcache +--SKIPIF-- + +--FILE-- + MAX_COMMAND_OUTPUT_BYTES) { + return substr($contents, 0, MAX_COMMAND_OUTPUT_BYTES) . "\n...[output truncated]\n"; + } + + return $contents; +} + +function runCommand(array $command): array +{ + $stdoutFile = tempnam(sys_get_temp_dir(), 'opcache-explicit-zts-out-'); + $stderrFile = tempnam(sys_get_temp_dir(), 'opcache-explicit-zts-err-'); + if ($stdoutFile === false || $stderrFile === false) { + throw new RuntimeException('failed to create temporary files'); + } + + $descriptorSpec = [ + 0 => ['pipe', 'r'], + 1 => ['file', $stdoutFile, 'w'], + 2 => ['file', $stderrFile, 'w'], + ]; + + $process = proc_open($command, $descriptorSpec, $pipes); + if (!is_resource($process)) { + @unlink($stdoutFile); + @unlink($stderrFile); + throw new RuntimeException('proc_open failed'); + } + + fclose($pipes[0]); + $status = proc_close($process); + + $stdout = readCommandOutput($stdoutFile); + $stderr = readCommandOutput($stderrFile); + @unlink($stdoutFile); + @unlink($stderrFile); + + return [$status, $stdout, $stderr]; +} + +function parseBuildFlags(string $flags): array +{ + $tokens = preg_split('/\s+/', trim($flags)); + if ($tokens === false) { + return []; + } + + return array_values(array_filter($tokens, static function (string $token): bool { + return $token !== '' && !str_contains($token, '$('); + })); +} + +function resolveBuildFlags(string $buildRoot, array $variables): array +{ + $makefile = @file_get_contents($buildRoot . '/Makefile'); + $flags = []; + + if ($makefile === false) { + return $flags; + } + + foreach ($variables as $variable) { + if (preg_match('/^' . preg_quote($variable, '/') . '\s*=\s*(.+)$/m', $makefile, $matches)) { + $flags = array_merge($flags, parseBuildFlags($matches[1])); + } + } + + return $flags; +} + +function resolveBuildCommand(string $buildRoot, string $variable, array $fallback): array +{ + $value = getenv($variable); + if (is_string($value) && $value !== '') { + $tokens = parseBuildFlags($value); + if ($tokens !== []) { + return $tokens; + } + } + + $tokens = resolveBuildFlags($buildRoot, [$variable]); + return $tokens !== [] ? $tokens : $fallback; +} + +function resolveExtraLibs(string $buildRoot): array +{ + $makefile = @file_get_contents($buildRoot . '/Makefile'); + if ($makefile !== false && preg_match('/^EXTRA_LIBS\s*=\s*(.+)$/m', $makefile, $matches)) { + $libs = preg_split('/\s+/', trim($matches[1])); + if ($libs !== false && $libs !== ['']) { + return $libs; + } + } + + return [ + '-lm', + '-lpthread', + '-ldl', + '-lresolv', + '-lutil', + ]; +} + +function resolveBuildRoot(string $root): string +{ + $buildRoot = null; + $php = getenv('TEST_PHP_EXECUTABLE') ?: PHP_BINARY; + if ($php) { + $php = realpath($php); + if ($php !== false) { + $candidate = dirname($php, 3); + if (file_exists($candidate . '/.libs/libphp.a') || file_exists($candidate . '/libs/libphp.a')) { + $buildRoot = $candidate; + } + } + } + + if ($buildRoot === null && (file_exists($root . '/.libs/libphp.a') || file_exists($root . '/libs/libphp.a'))) { + $buildRoot = $root; + } + + if ($buildRoot === null) { + throw new RuntimeException('Failed to resolve build root'); + } + + return $buildRoot; +} + +$root = realpath(__DIR__ . '/../../..'); +if ($root === false) { + throw new RuntimeException('Failed to resolve source root'); +} + +$buildRoot = resolveBuildRoot($root); +$source = __DIR__ . '/helpers/explicit_cache_store_delete_zts_threads_001.c'; +$binary = __DIR__ . '/helpers/explicit_cache_store_delete_zts_threads_001.out'; +$compiler = resolveBuildCommand($buildRoot, 'CC', ['gcc']); +$buildFlags = resolveBuildFlags($buildRoot, ['CPPFLAGS', 'CFLAGS_CLEAN']); +$extraLibs = resolveExtraLibs($buildRoot); + +$compileCommand = [ + ...$compiler, + ...$buildFlags, + '-DZEND_ENABLE_STATIC_TSRMLS_CACHE=1', + '-DHAVE_CONFIG_H', + '-I' . $buildRoot, + '-I' . $buildRoot . '/main', + '-I' . $buildRoot . '/Zend', + '-I' . $buildRoot . '/TSRM', + '-I' . $buildRoot . '/sapi/embed', + '-I' . $root, + '-I' . $root . '/main', + '-I' . $root . '/Zend', + '-I' . $root . '/TSRM', + '-I' . $root . '/sapi/embed', + '-o', + $binary, + $source, + $root . '/sapi/embed/php_embed.c', + file_exists($buildRoot . '/.libs/libphp.a') ? $buildRoot . '/.libs/libphp.a' : $buildRoot . '/libs/libphp.a', +]; +$compileCommand = array_merge($compileCommand, $extraLibs); + +[$status, $stdout, $stderr] = runCommand($compileCommand); +if ($status !== 0) { + echo "compile failed: {$status}\n"; + echo $stdout; + echo $stderr; + return; +} + +[$status, $stdout, $stderr] = runCommand([$binary]); +echo $stdout; +if ($status !== 0) { + echo "run failed: {$status}\n"; + echo $stderr; +} + +?> +--CLEAN-- + +--EXPECT-- +ok diff --git a/ext/opcache/tests/static_cache_persistent_cache_api_001.phpt b/ext/opcache/tests/static_cache_persistent_cache_api_001.phpt new file mode 100644 index 000000000000..536430a39c7b --- /dev/null +++ b/ext/opcache/tests/static_cache_persistent_cache_api_001.phpt @@ -0,0 +1,55 @@ +--TEST-- +OPcache persistent_store, persistent_fetch, and persistent_exists public API +--EXTENSIONS-- +opcache +--CONFLICTS-- +server +--FILE-- + 1]); + var_dump(OPcache\persistent_exists('shared')); + return; +} + +var_dump(OPcache\persistent_exists('shared')); +var_dump(OPcache\persistent_fetch('shared', 'fallback')); +var_dump(OPcache\persistent_fetch('missing')); +var_dump(OPcache\persistent_fetch('missing', 'fallback')); + +OPcache\persistent_store('null', null); +var_dump(OPcache\persistent_fetch('null', 'fallback')); +PHP); + +$php = getenv('TEST_PHP_EXECUTABLE'); +if ($php) { + $php = realpath(__DIR__ . '/../../../' . $php) ?: $php; + putenv('TEST_PHP_EXECUTABLE=' . $php); +} + +include 'php_cli_server.inc'; +php_cli_server_start('-d opcache.enable=1 -d opcache.enable_cli=1 -d opcache.static_cache.persistent_size_mb=32 -d opcache.file_update_protection=0'); + +echo file_get_contents('http://' . PHP_CLI_SERVER_ADDRESS . '/persistent_cache_api_001.php?action=write'); +echo file_get_contents('http://' . PHP_CLI_SERVER_ADDRESS . '/persistent_cache_api_001.php?action=read'); + +?> +--CLEAN-- + +--EXPECT-- +bool(true) +bool(true) +array(1) { + ["v"]=> + int(1) +} +NULL +string(8) "fallback" +NULL diff --git a/ext/opcache/tests/static_cache_persistent_cache_array_mutation_overflow_001.phpt b/ext/opcache/tests/static_cache_persistent_cache_array_mutation_overflow_001.phpt new file mode 100644 index 000000000000..793209aed14c --- /dev/null +++ b/ext/opcache/tests/static_cache_persistent_cache_array_mutation_overflow_001.phpt @@ -0,0 +1,31 @@ +--TEST-- +OPcache PersistentStatic throws an exception when array mutation exhausts persistent cache shared memory +--EXTENSIONS-- +opcache +--INI-- +opcache.enable=1 +opcache.enable_cli=1 +opcache.static_cache.persistent_size_mb=8 +opcache.optimization_level=0 +opcache.file_update_protection=0 +opcache.jit=0 +--FILE-- +getMessage(), "\n"; + OPcache\persistent_clear(); +} + +?> +--EXPECT-- +OPcache\StaticCacheException: not enough shared memory left diff --git a/ext/opcache/tests/static_cache_persistent_cache_atomic_increment_create_001.phpt b/ext/opcache/tests/static_cache_persistent_cache_atomic_increment_create_001.phpt new file mode 100644 index 000000000000..d2d085885e14 --- /dev/null +++ b/ext/opcache/tests/static_cache_persistent_cache_atomic_increment_create_001.phpt @@ -0,0 +1,49 @@ +--TEST-- +OPcache persistent atomic increment creates missing keys without TTL +--EXTENSIONS-- +opcache +--INI-- +opcache.enable=1 +opcache.enable_cli=1 +opcache.static_cache.persistent_size_mb=32 +--FILE-- +getMessage(), "\n"; +} + +try { + OPcache\persistent_atomic_increment('extra', 1, 1); +} catch (ArgumentCountError $exception) { + echo "too-many-args\n"; +} + +?> +--EXPECT-- +int(7) +int(7) +int(10) +int(10) +bool(true) +int(11) +int(11) +bool(true) +Cache key "missing_down" was not found +too-many-args diff --git a/ext/opcache/tests/static_cache_persistent_cache_atomic_non_integer_001.phpt b/ext/opcache/tests/static_cache_persistent_cache_atomic_non_integer_001.phpt new file mode 100644 index 000000000000..4251faa18f31 --- /dev/null +++ b/ext/opcache/tests/static_cache_persistent_cache_atomic_non_integer_001.phpt @@ -0,0 +1,31 @@ +--TEST-- +OPcache persistent atomic operations reject non-integer entries +--EXTENSIONS-- +opcache +--INI-- +opcache.enable=1 +opcache.enable_cli=1 +opcache.static_cache.persistent_size_mb=32 +--FILE-- +getMessage(), "\n"; +} + +try { + OPcache\persistent_atomic_decrement('text'); +} catch (StaticCacheException $exception) { + echo $exception->getMessage(), "\n"; +} + +?> +--EXPECT-- +Atomic increment requires an integer value +Atomic decrement requires an integer value diff --git a/ext/opcache/tests/static_cache_persistent_cache_batch_atomic_001.phpt b/ext/opcache/tests/static_cache_persistent_cache_batch_atomic_001.phpt new file mode 100644 index 000000000000..04fcfe2124c7 --- /dev/null +++ b/ext/opcache/tests/static_cache_persistent_cache_batch_atomic_001.phpt @@ -0,0 +1,62 @@ +--TEST-- +OPcache persistent cache batch and atomic public API +--EXTENSIONS-- +opcache +--INI-- +opcache.enable=1 +opcache.enable_cli=1 +opcache.static_cache.persistent_size_mb=32 +--FILE-- + 10, + 'name' => 'php', + 'null' => null, +]); + +$fallback = ['fallback']; + +var_dump(OPcache\persistent_fetch_array(['count', 'name', 'missing'], $fallback)); +var_dump(OPcache\persistent_atomic_increment('count')); +var_dump(OPcache\persistent_atomic_decrement('count', 3)); + +OPcache\persistent_delete_array(['name', 'missing']); +var_dump(OPcache\persistent_fetch_array(['count', 'name', 'null'], $fallback)); + +OPcache\persistent_clear(); +var_dump(OPcache\persistent_fetch_array(['count'], $fallback)); + +?> +--EXPECT-- +array(3) { + ["count"]=> + int(10) + ["name"]=> + string(3) "php" + ["missing"]=> + array(1) { + [0]=> + string(8) "fallback" + } +} +int(11) +int(8) +array(3) { + ["count"]=> + int(8) + ["name"]=> + array(1) { + [0]=> + string(8) "fallback" + } + ["null"]=> + NULL +} +array(1) { + ["count"]=> + array(1) { + [0]=> + string(8) "fallback" + } +} diff --git a/ext/opcache/tests/static_cache_persistent_cache_clear_001.phpt b/ext/opcache/tests/static_cache_persistent_cache_clear_001.phpt new file mode 100644 index 000000000000..8838a2a07297 --- /dev/null +++ b/ext/opcache/tests/static_cache_persistent_cache_clear_001.phpt @@ -0,0 +1,58 @@ +--TEST-- +OPcache persistent_clear clears only the persistent cache API namespace +--EXTENSIONS-- +opcache +--CONFLICTS-- +server +--FILE-- + +--CLEAN-- + +--EXPECT-- +bool(true) +string(14) "volatile-value" +string(18) "missing-persistent" +string(14) "volatile-value" +string(18) "missing-persistent" diff --git a/ext/opcache/tests/static_cache_persistent_cache_clear_combined_001.phpt b/ext/opcache/tests/static_cache_persistent_cache_clear_combined_001.phpt new file mode 100644 index 000000000000..c8c9d1e16dd1 --- /dev/null +++ b/ext/opcache/tests/static_cache_persistent_cache_clear_combined_001.phpt @@ -0,0 +1,110 @@ +--TEST-- +OPcache persistent_clear drops combined persistent entries without corrupting refill or volatile entries +--EXTENSIONS-- +opcache +--CONFLICTS-- +server +--FILE-- + $i, + 'text' => str_repeat($prefix, 32), + 'nested' => ['value' => $i * $multiplier], + ]; + } + + return $rows; +} + +function build_graph(string $prefix, int $multiplier): PersistentClearCombinedNode +{ + return new PersistentClearCombinedNode(build_rows($prefix, $multiplier)); +} + +$action = $_GET['action'] ?? 'seed'; + +if ($action === 'seed') { + OPcache\volatile_clear(); + OPcache\persistent_clear(); + var_dump(OPcache\volatile_store('local-string', str_repeat('L', 400000))); + OPcache\persistent_store('persistent-string', str_repeat('G', 400000)); + OPcache\persistent_store('persistent-graph', build_graph('O', 3)); + $graph = OPcache\persistent_fetch('persistent-graph'); + var_dump(strlen(OPcache\persistent_fetch('persistent-string'))); + var_dump($graph->rows[123]['text']); + return; +} + +if ($action === 'clear') { + OPcache\persistent_clear(); + var_dump(strlen(OPcache\volatile_fetch('local-string', 'missing-volatile'))); + var_dump(OPcache\persistent_fetch('persistent-string', 'missing-persistent')); + var_dump(OPcache\persistent_fetch('persistent-graph', 'missing-graph')); + return; +} + +if ($action === 'refill') { + OPcache\persistent_store('persistent-string', str_repeat('H', 400000)); + OPcache\persistent_store('persistent-graph', build_graph('N', 7)); + $graph = OPcache\persistent_fetch('persistent-graph'); + var_dump(strlen(OPcache\persistent_fetch('persistent-string'))); + var_dump($graph->rows[123]['text']); + var_dump($graph->rows[123]['nested']['value']); + return; +} + +$graph = OPcache\persistent_fetch('persistent-graph'); +var_dump(strlen(OPcache\volatile_fetch('local-string', 'missing-volatile'))); +var_dump(strlen(OPcache\persistent_fetch('persistent-string'))); +var_dump($graph->rows[123]['text']); +var_dump($graph->rows[123]['nested']['value']); +PHP); + +$php = getenv('TEST_PHP_EXECUTABLE'); +if ($php) { + $php = realpath(__DIR__ . '/../../../' . $php) ?: $php; + putenv('TEST_PHP_EXECUTABLE=' . $php); +} + +include 'php_cli_server.inc'; +php_cli_server_start('-d opcache.enable=1 -d opcache.enable_cli=1 -d opcache.static_cache.volatile_size_mb=8 -d opcache.static_cache.persistent_size_mb=8 -d opcache.file_update_protection=0'); + +$base = 'http://' . PHP_CLI_SERVER_ADDRESS . '/persistent_cache_clear_combined_001.php'; +echo file_get_contents($base . '?action=seed'); +echo file_get_contents($base . '?action=clear'); +echo file_get_contents($base . '?action=refill'); +echo file_get_contents($base . '?action=read'); + +?> +--CLEAN-- + +--EXPECT-- +bool(true) +int(400000) +string(32) "OOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOO" +int(400000) +string(18) "missing-persistent" +string(13) "missing-graph" +int(400000) +string(32) "NNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNN" +int(861) +int(400000) +int(400000) +string(32) "NNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNN" +int(861) diff --git a/ext/opcache/tests/static_cache_persistent_cache_info_001.phpt b/ext/opcache/tests/static_cache_persistent_cache_info_001.phpt new file mode 100644 index 000000000000..047fc02558c7 --- /dev/null +++ b/ext/opcache/tests/static_cache_persistent_cache_info_001.phpt @@ -0,0 +1,60 @@ +--TEST-- +OPcache volatile_cache_info and persistent_cache_info report separate cache backends +--EXTENSIONS-- +opcache +--CONFLICTS-- +server +--FILE-- + +--CLEAN-- + +--EXPECT-- +1 +1,1,volatile-value,2 +bool(true) +bool(true) +int(33554432) +int(33554432) diff --git a/ext/opcache/tests/static_cache_persistent_cache_lock_atomic_increment_001.phpt b/ext/opcache/tests/static_cache_persistent_cache_lock_atomic_increment_001.phpt new file mode 100644 index 000000000000..0f557af2ee34 --- /dev/null +++ b/ext/opcache/tests/static_cache_persistent_cache_lock_atomic_increment_001.phpt @@ -0,0 +1,79 @@ +--TEST-- +OPcache persistent_lock is released by persistent_atomic_increment +--EXTENSIONS-- +opcache +pcntl +--SKIPIF-- + +--INI-- +opcache.enable=1 +opcache.enable_cli=1 +opcache.static_cache.persistent_size_mb=32 +--FILE-- += $deadline) { + throw new RuntimeException("timed out waiting for {$path}"); + } + usleep(1000); + } +} + +$prefix = sys_get_temp_dir() . '/opcache_persistent_exists_lock_' . getmypid(); +$readyFile = $prefix . '.ready'; +$resultFile = $prefix . '.result'; +@unlink($readyFile); +@unlink($resultFile); + +$key = 'persistent_exists_lock_' . getmypid(); +OPcache\persistent_clear(); + +var_dump(OPcache\persistent_lock($key)); +var_dump(OPcache\persistent_atomic_increment($key, 9)); + +$pid = pcntl_fork(); +if ($pid === -1) { + throw new RuntimeException('pcntl_fork() failed'); +} + +if ($pid === 0) { + pcntl_async_signals(true); + pcntl_signal(SIGALRM, static function () use ($resultFile): void { + file_put_contents($resultFile, "timeout\n"); + exit(1); + }); + pcntl_alarm(3); + file_put_contents($readyFile, 'ready'); + $exists = OPcache\persistent_lock($key); + $value = OPcache\persistent_fetch($key); + pcntl_alarm(0); + file_put_contents($resultFile, ($exists ? 'true' : 'false') . "\n" . $value . "\n"); + exit(0); +} + +wait_for_file($readyFile); +pcntl_waitpid($pid, $status); +echo file_get_contents($resultFile); +@unlink($readyFile); +@unlink($resultFile); + +?> +--EXPECT-- +bool(true) +int(9) +true +9 +--CLEAN-- + diff --git a/ext/opcache/tests/static_cache_persistent_cache_overflow_001.phpt b/ext/opcache/tests/static_cache_persistent_cache_overflow_001.phpt new file mode 100644 index 000000000000..37cb7bcab62e --- /dev/null +++ b/ext/opcache/tests/static_cache_persistent_cache_overflow_001.phpt @@ -0,0 +1,30 @@ +--TEST-- +OPcache PersistentStatic throws an exception when property assignment exhausts persistent cache shared memory +--EXTENSIONS-- +opcache +--INI-- +opcache.enable=1 +opcache.enable_cli=1 +opcache.static_cache.persistent_size_mb=8 +opcache.optimization_level=0 +opcache.file_update_protection=0 +opcache.jit=0 +--FILE-- +getMessage(), "\n"; + OPcache\persistent_clear(); +} + +?> +--EXPECT-- +OPcache\StaticCacheException: not enough shared memory left diff --git a/ext/opcache/tests/static_cache_persistent_cache_store_overflow_001.phpt b/ext/opcache/tests/static_cache_persistent_cache_store_overflow_001.phpt new file mode 100644 index 000000000000..e58ca6a82307 --- /dev/null +++ b/ext/opcache/tests/static_cache_persistent_cache_store_overflow_001.phpt @@ -0,0 +1,29 @@ +--TEST-- +OPcache persistent_store throws StaticCacheException when persistent cache memory is exhausted +--EXTENSIONS-- +opcache +--INI-- +opcache.enable=1 +opcache.enable_cli=1 +opcache.static_cache.persistent_size_mb=8 +--FILE-- +getMessage(), "\n"; +} + +var_dump(OPcache\persistent_fetch('small', 'fallback')); +var_dump(OPcache\persistent_fetch('missing', 'fallback')); + +?> +--EXPECT-- +OPcache\StaticCacheException: not enough shared memory left +string(2) "ok" +string(8) "fallback" diff --git a/ext/opcache/tests/static_cache_persistent_static_array_mutation_fast_path_001.phpt b/ext/opcache/tests/static_cache_persistent_static_array_mutation_fast_path_001.phpt new file mode 100644 index 000000000000..ca6ad6ff1976 --- /dev/null +++ b/ext/opcache/tests/static_cache_persistent_static_array_mutation_fast_path_001.phpt @@ -0,0 +1,111 @@ +--TEST-- +OPcache PersistentStatic publishes class, property, and method array mutations immediately +--EXTENSIONS-- +opcache +--CONFLICTS-- +server +--FILE-- + +--CLEAN-- + +--EXPECT-- +reset +seed-class=foo,bar +seed-property=foo,bar +seed-method=foo,bar +read-class=foo,bar +read-property=foo,bar +read-method=foo,bar +mutate-class=foo,bar,baz +mutate-property=foo,bar,baz +mutate-method=foo,bar,baz +read-class=foo,bar,baz +read-property=foo,bar,baz +read-method=foo,bar,baz diff --git a/ext/opcache/tests/static_cache_persistent_static_array_mutation_jit_001.phpt b/ext/opcache/tests/static_cache_persistent_static_array_mutation_jit_001.phpt new file mode 100644 index 000000000000..de81f6623d37 --- /dev/null +++ b/ext/opcache/tests/static_cache_persistent_static_array_mutation_jit_001.phpt @@ -0,0 +1,121 @@ +--TEST-- +OPcache PersistentStatic array mutations remain visible through tracing JIT static slot reads +--EXTENSIONS-- +opcache +--CONFLICTS-- +server +--SKIPIF-- + +--FILE-- + +--CLEAN-- + +--EXPECT-- +reset +seed-before=1,1000,1000 +seed-after=1,2000,2000 +read=1,2000,2000 diff --git a/ext/opcache/tests/static_cache_persistent_static_attribute_scope_separation_001.phpt b/ext/opcache/tests/static_cache_persistent_static_attribute_scope_separation_001.phpt new file mode 100644 index 000000000000..faf2af133f21 --- /dev/null +++ b/ext/opcache/tests/static_cache_persistent_static_attribute_scope_separation_001.phpt @@ -0,0 +1,104 @@ +--TEST-- +OPcache PersistentStatic keeps class, property, and method attribute scopes separate +--EXTENSIONS-- +opcache +--CONFLICTS-- +server +--FILE-- + +--CLEAN-- + +--EXPECT-- +reset +1,1,1,1,1,1 +2,2,2,1,2,1 +2,3,2,3,0,0,1,3 diff --git a/ext/opcache/tests/static_cache_persistent_static_capture_miss_001.phpt b/ext/opcache/tests/static_cache_persistent_static_capture_miss_001.phpt new file mode 100644 index 000000000000..d7828b8dbfdf --- /dev/null +++ b/ext/opcache/tests/static_cache_persistent_static_capture_miss_001.phpt @@ -0,0 +1,122 @@ +--TEST-- +OPcache static attribute decode capture is cleared after cache miss +--EXTENSIONS-- +opcache +--CONFLICTS-- +server +--FILE-- +bag[] = 'seed'; + var_dump(OPcache\volatile_store('capture_miss_explicit', $probe)); + echo 'volatile_entries=', OPcache\volatile_cache_info()['entry_count'], "\n"; + return; +} + +if ($action === 'miss_then_explicit_mutate') { + match ($backend) { + 'cached' => CaptureMissCachedMethod::touch(), + 'persistent' => CaptureMissPersistentMethod::touch(), + default => throw new RuntimeException('unknown backend'), + }; + + $explicit = OPcache\volatile_fetch('capture_miss_explicit'); + $explicit->bag[] = 'mutated outside static attribute'; + + echo 'explicit_bag=', count($explicit->bag), "\n"; + echo 'volatile_entries=', OPcache\volatile_cache_info()['entry_count'], "\n"; + echo 'persistent_entries=', OPcache\persistent_cache_info()['entry_count'], "\n"; + return; +} + +if ($action === 'read_static') { + match ($backend) { + 'cached' => CaptureMissCachedMethod::touch(), + 'persistent' => CaptureMissPersistentMethod::touch(), + default => throw new RuntimeException('unknown backend'), + }; + echo 'volatile_entries=', OPcache\volatile_cache_info()['entry_count'], "\n"; + echo 'persistent_entries=', OPcache\persistent_cache_info()['entry_count'], "\n"; + return; +} + +throw new RuntimeException('unknown action'); +PHP); + +$php = getenv('TEST_PHP_EXECUTABLE'); +if ($php) { + $php = realpath(__DIR__ . '/../../../' . $php) ?: $php; + putenv('TEST_PHP_EXECUTABLE=' . $php); +} + +include 'php_cli_server.inc'; +php_cli_server_start('-d opcache.enable=1 -d opcache.enable_cli=1 -d opcache.static_cache.volatile_size_mb=32 -d opcache.static_cache.persistent_size_mb=32 -d opcache.file_update_protection=0 -d opcache.jit=0'); + +$base = 'http://' . PHP_CLI_SERVER_ADDRESS . '/persistent_static_capture_miss_001.php'; +foreach (['cached', 'persistent'] as $backend) { + echo file_get_contents($base . '?action=reset'); + echo file_get_contents($base . '?action=seed'); + echo file_get_contents($base . '?action=miss_then_explicit_mutate&backend=' . $backend); + echo file_get_contents($base . '?action=read_static&backend=' . $backend); +} + +?> +--CLEAN-- + +--EXPECT-- +reset +bool(true) +volatile_entries=1 +explicit_bag=2 +volatile_entries=1 +persistent_entries=0 +volatile_entries=1 +persistent_entries=0 +reset +bool(true) +volatile_entries=1 +explicit_bag=2 +volatile_entries=1 +persistent_entries=0 +volatile_entries=1 +persistent_entries=0 diff --git a/ext/opcache/tests/static_cache_persistent_static_capture_tracking_001.phpt b/ext/opcache/tests/static_cache_persistent_static_capture_tracking_001.phpt new file mode 100644 index 000000000000..32c1010f9870 --- /dev/null +++ b/ext/opcache/tests/static_cache_persistent_static_capture_tracking_001.phpt @@ -0,0 +1,240 @@ +--TEST-- +OPcache static attribute decoded graph tracking follows publication kind +--EXTENSIONS-- +opcache +--CONFLICTS-- +server +--FILE-- + [], + 'sparse' => [0 => new CaptureTrackingChild(['seed']), 3 => new CaptureTrackingChild(['gap'])], + ], + new CaptureTrackingChild(), + ); +} + +class CaptureTrackingCachedMethodA +{ + #[OPcache\VolatileStatic(strategy: OPcache\CacheStrategy::Tracking)] + public static function value(): CaptureTrackingPayload + { + static $value = null; + + $value ??= capture_tracking_payload('cached_method_a'); + return $value; + } +} + +class CaptureTrackingCachedMethodB +{ + #[OPcache\VolatileStatic(strategy: OPcache\CacheStrategy::Tracking)] + public static function value(): CaptureTrackingPayload + { + static $value = null; + + $value ??= capture_tracking_payload('cached_method_b'); + return $value; + } +} + +class CaptureTrackingCachedPropertyA +{ + #[OPcache\VolatileStatic(strategy: OPcache\CacheStrategy::Tracking)] + public static ?CaptureTrackingPayload $value = null; + + public static function value(): CaptureTrackingPayload + { + self::$value ??= capture_tracking_payload('cached_property_a'); + return self::$value; + } +} + +class CaptureTrackingCachedPropertyB +{ + #[OPcache\VolatileStatic(strategy: OPcache\CacheStrategy::Tracking)] + public static ?CaptureTrackingPayload $value = null; + + public static function value(): CaptureTrackingPayload + { + self::$value ??= capture_tracking_payload('cached_property_b'); + return self::$value; + } +} + +class CaptureTrackingPersistentMethodA +{ + #[OPcache\PersistentStatic] + public static function value(): CaptureTrackingPayload + { + static $value = null; + + $value ??= capture_tracking_payload('persistent_method_a'); + return $value; + } +} + +class CaptureTrackingPersistentMethodB +{ + #[OPcache\PersistentStatic] + public static function value(): CaptureTrackingPayload + { + static $value = null; + + $value ??= capture_tracking_payload('persistent_method_b'); + return $value; + } +} + +class CaptureTrackingPersistentPropertyA +{ + #[OPcache\PersistentStatic] + public static ?CaptureTrackingPayload $value = null; + + public static function value(): CaptureTrackingPayload + { + self::$value ??= capture_tracking_payload('persistent_property_a'); + return self::$value; + } +} + +class CaptureTrackingPersistentPropertyB +{ + #[OPcache\PersistentStatic] + public static ?CaptureTrackingPayload $value = null; + + public static function value(): CaptureTrackingPayload + { + self::$value ??= capture_tracking_payload('persistent_property_b'); + return self::$value; + } +} + +function capture_tracking_pair(string $kind): array +{ + return match ($kind) { + 'cached_method' => [CaptureTrackingCachedMethodA::value(), CaptureTrackingCachedMethodB::value()], + 'cached_property' => [CaptureTrackingCachedPropertyA::value(), CaptureTrackingCachedPropertyB::value()], + 'persistent_method' => [CaptureTrackingPersistentMethodA::value(), CaptureTrackingPersistentMethodB::value()], + 'persistent_property' => [CaptureTrackingPersistentPropertyA::value(), CaptureTrackingPersistentPropertyB::value()], + default => throw new RuntimeException('unknown kind'), + }; +} + +function capture_tracking_mutate(CaptureTrackingPayload $payload, string $tag): void +{ + $payload->rows['events'][] = $tag; + $payload->rows['sparse'][3]->items[] = $tag; + $payload->child->items[] = $tag; +} + +function capture_tracking_dump(string $kind): void +{ + [$first, $second] = capture_tracking_pair($kind); + echo $kind, ':', + count($first->rows['events']), ',', + count($first->rows['sparse'][3]->items), ',', + count($first->child->items), '|', + count($second->rows['events']), ',', + count($second->rows['sparse'][3]->items), ',', + count($second->child->items), "\n"; +} + +$action = $_GET['action'] ?? 'read'; +$kind = $_GET['kind'] ?? 'cached_method'; + +if ($action === 'reset') { + OPcache\volatile_clear(); + opcache_reset(); + echo "reset\n"; + return; +} + +if ($action === 'seed') { + [$first, $second] = capture_tracking_pair($kind); + capture_tracking_mutate($first, 'seed-a'); + capture_tracking_mutate($second, 'seed-b'); + capture_tracking_dump($kind); + return; +} + +if ($action === 'mutate_after_fetch') { + [$first, $second] = capture_tracking_pair($kind); + capture_tracking_mutate($first, 'fetch-a'); + capture_tracking_mutate($second, 'fetch-b'); + capture_tracking_dump($kind); + return; +} + +if ($action === 'read') { + capture_tracking_dump($kind); + return; +} + +throw new RuntimeException('unknown action'); +PHP); + +$php = getenv('TEST_PHP_EXECUTABLE'); +if ($php) { + $php = realpath(__DIR__ . '/../../../' . $php) ?: $php; + putenv('TEST_PHP_EXECUTABLE=' . $php); +} + +include 'php_cli_server.inc'; +php_cli_server_start('-d opcache.enable=1 -d opcache.enable_cli=1 -d opcache.static_cache.volatile_size_mb=32 -d opcache.static_cache.persistent_size_mb=32 -d opcache.file_update_protection=0 -d opcache.jit=0'); + +$base = 'http://' . PHP_CLI_SERVER_ADDRESS . '/persistent_static_capture_tracking_001.php'; +foreach (['cached_method', 'cached_property', 'persistent_method', 'persistent_property'] as $kind) { + echo file_get_contents($base . '?action=reset'); + echo file_get_contents($base . '?action=seed&kind=' . $kind); + echo file_get_contents($base . '?action=mutate_after_fetch&kind=' . $kind); + echo file_get_contents($base . '?action=read&kind=' . $kind); +} + +?> +--CLEAN-- + +--EXPECT-- +reset +cached_method:1,2,1|1,2,1 +cached_method:2,3,2|2,3,2 +cached_method:2,3,2|2,3,2 +reset +cached_property:1,2,1|1,2,1 +cached_property:2,3,2|2,3,2 +cached_property:2,3,2|2,3,2 +reset +persistent_method:1,2,1|1,2,1 +persistent_method:1,2,1|1,2,1 +persistent_method:0,1,0|0,1,0 +reset +persistent_property:1,2,1|1,2,1 +persistent_property:1,2,1|1,2,1 +persistent_property:0,1,0|0,1,0 diff --git a/ext/opcache/tests/static_cache_persistent_static_class_blob_dynamic_method_statics_jit_001.phpt b/ext/opcache/tests/static_cache_persistent_static_class_blob_dynamic_method_statics_jit_001.phpt new file mode 100644 index 000000000000..313d26741e42 --- /dev/null +++ b/ext/opcache/tests/static_cache_persistent_static_class_blob_dynamic_method_statics_jit_001.phpt @@ -0,0 +1,103 @@ +--TEST-- +OPcache PersistentStatic class blob survives dynamic method statics with JIT enabled +--EXTENSIONS-- +opcache +--CONFLICTS-- +server +--SKIPIF-- + +--FILE-- + +--CLEAN-- + +--EXPECT-- +1,7,6,21,1 +1,6,21,1,1 +11,7,33,1 +1,7,33,1,1 diff --git a/ext/opcache/tests/static_cache_persistent_static_class_blob_readonly_skip_001.phpt b/ext/opcache/tests/static_cache_persistent_static_class_blob_readonly_skip_001.phpt new file mode 100644 index 000000000000..a7ea7b5f4c37 --- /dev/null +++ b/ext/opcache/tests/static_cache_persistent_static_class_blob_readonly_skip_001.phpt @@ -0,0 +1,94 @@ +--TEST-- +OPcache PersistentStatic skips class blob publish for read-only cached requests +--EXTENSIONS-- +opcache +--CONFLICTS-- +server +--FILE-- +logFile, "serialize\n", FILE_APPEND); + return ['logFile' => $this->logFile, 'value' => $this->value]; + } + + public function __unserialize(array $data): void + { + $this->logFile = $data['logFile']; + $this->value = $data['value']; + } +} + +#[OPcache\PersistentStatic] +class ReadOnlyBlobState +{ + public static function value(string $logFile): int + { + static $probe = null; + + if ($probe === null) { + $probe = new PublishProbe($logFile, 7); + } + + return $probe->value; + } +} + +$logFile = __DIR__ . '/persistent_static_009.log'; +$action = $_GET['action'] ?? 'value'; + +if ($action === 'reset') { + @unlink($logFile); + opcache_reset(); + echo "reset\n"; + return; +} + +if ($action === 'count') { + echo is_file($logFile) ? count(file($logFile, FILE_IGNORE_NEW_LINES)) : 0, "\n"; + return; +} + +echo ReadOnlyBlobState::value($logFile), "\n"; +PHP); + +$php = getenv('TEST_PHP_EXECUTABLE'); +if ($php) { + $php = realpath(__DIR__ . '/../../../' . $php) ?: $php; + putenv('TEST_PHP_EXECUTABLE=' . $php); +} + +@unlink(__DIR__ . '/persistent_static_009.log'); + +include 'php_cli_server.inc'; +php_cli_server_start('-d opcache.enable=1 -d opcache.enable_cli=1 -d opcache.static_cache.persistent_size_mb=32 -d opcache.file_update_protection=0'); + +echo file_get_contents('http://' . PHP_CLI_SERVER_ADDRESS . '/persistent_static_009.php?action=reset'); +echo file_get_contents('http://' . PHP_CLI_SERVER_ADDRESS . '/persistent_static_009.php?action=value'); +echo file_get_contents('http://' . PHP_CLI_SERVER_ADDRESS . '/persistent_static_009.php?action=count'); +echo file_get_contents('http://' . PHP_CLI_SERVER_ADDRESS . '/persistent_static_009.php?action=value'); +echo file_get_contents('http://' . PHP_CLI_SERVER_ADDRESS . '/persistent_static_009.php?action=count'); + +?> +--CLEAN-- + +--EXPECT-- +reset +7 +1 +7 +1 diff --git a/ext/opcache/tests/static_cache_persistent_static_class_blob_single_key_001.phpt b/ext/opcache/tests/static_cache_persistent_static_class_blob_single_key_001.phpt new file mode 100644 index 000000000000..1c375286a70a --- /dev/null +++ b/ext/opcache/tests/static_cache_persistent_static_class_blob_single_key_001.phpt @@ -0,0 +1,61 @@ +--TEST-- +OPcache PersistentStatic class attribute stores class-wide state under one cache key +--EXTENSIONS-- +opcache +--CONFLICTS-- +server +--FILE-- + +--CLEAN-- + +--EXPECT-- +1,1 +1,10,2,1 +2,21,3 diff --git a/ext/opcache/tests/static_cache_persistent_static_class_property_persistence_001.phpt b/ext/opcache/tests/static_cache_persistent_static_class_property_persistence_001.phpt new file mode 100644 index 000000000000..b08738ee1db1 --- /dev/null +++ b/ext/opcache/tests/static_cache_persistent_static_class_property_persistence_001.phpt @@ -0,0 +1,51 @@ +--TEST-- +OPcache PersistentStatic persists class static properties across requests +--EXTENSIONS-- +opcache +--CONFLICTS-- +server +--FILE-- + +--CLEAN-- + +--EXPECT-- +1,1,1 +2,2,1 diff --git a/ext/opcache/tests/static_cache_persistent_static_complex_value_persistence_001.phpt b/ext/opcache/tests/static_cache_persistent_static_complex_value_persistence_001.phpt new file mode 100644 index 000000000000..04ccf180b7d5 --- /dev/null +++ b/ext/opcache/tests/static_cache_persistent_static_complex_value_persistence_001.phpt @@ -0,0 +1,71 @@ +--TEST-- +OPcache PersistentStatic persists complex static values across requests +--EXTENSIONS-- +opcache +--CONFLICTS-- +server +--FILE-- + new CounterBox(100), 'gap' => []]; + $state['gap'][4] = 'seed'; + unset($state['gap'][4]); + } + + $state['box']->value++; + $state['gap'][] = 'tail'; + + return $state['box']->value . ':' . array_key_last($state['gap']); + } +} + +if (ComplexState::$box === null) { + ComplexState::$box = new CounterBox(1); + ComplexState::$gap[3] = 'seed'; + unset(ComplexState::$gap[3]); +} + +ComplexState::$box->value++; +ComplexState::$gap[] = 'tail'; + +echo ComplexState::$box->value, ',', array_key_last(ComplexState::$gap), ',', ComplexState::methodState(), "\n"; +PHP); + +$php = getenv('TEST_PHP_EXECUTABLE'); +if ($php) { + $php = realpath(__DIR__ . '/../../../' . $php) ?: $php; + putenv('TEST_PHP_EXECUTABLE=' . $php); +} + +include 'php_cli_server.inc'; +php_cli_server_start('-d opcache.enable=1 -d opcache.enable_cli=1 -d opcache.static_cache.persistent_size_mb=32'); + +echo file_get_contents('http://' . PHP_CLI_SERVER_ADDRESS . '/persistent_static_003.php'); +echo file_get_contents('http://' . PHP_CLI_SERVER_ADDRESS . '/persistent_static_003.php'); + +?> +--CLEAN-- + +--EXPECT-- +2,4,101:5 +3,5,102:6 diff --git a/ext/opcache/tests/static_cache_persistent_static_disabled_without_persistent_cache_memory_001.phpt b/ext/opcache/tests/static_cache_persistent_static_disabled_without_persistent_cache_memory_001.phpt new file mode 100644 index 000000000000..8f1f2a970153 --- /dev/null +++ b/ext/opcache/tests/static_cache_persistent_static_disabled_without_persistent_cache_memory_001.phpt @@ -0,0 +1,40 @@ +--TEST-- +OPcache PersistentStatic stays disabled when persistent cache memory is 0 +--EXTENSIONS-- +opcache +--CONFLICTS-- +server +--FILE-- + +--CLEAN-- + +--EXPECT-- +1 +1 diff --git a/ext/opcache/tests/static_cache_persistent_static_inherited_attributes_001.phpt b/ext/opcache/tests/static_cache_persistent_static_inherited_attributes_001.phpt new file mode 100644 index 000000000000..6df412bbe026 --- /dev/null +++ b/ext/opcache/tests/static_cache_persistent_static_inherited_attributes_001.phpt @@ -0,0 +1,62 @@ +--TEST-- +OPcache class-level static cache attributes are not inherited +--EXTENSIONS-- +opcache +--CONFLICTS-- +server +--FILE-- + +--CLEAN-- + +--EXPECT-- +1,1,1,1 +2,2,1,1 diff --git a/ext/opcache/tests/static_cache_persistent_static_invalidate_001.phpt b/ext/opcache/tests/static_cache_persistent_static_invalidate_001.phpt new file mode 100644 index 000000000000..8456d6830471 --- /dev/null +++ b/ext/opcache/tests/static_cache_persistent_static_invalidate_001.phpt @@ -0,0 +1,102 @@ +--TEST-- +OPcache PersistentStatic state is deleted only for the invalidated script +--EXTENSIONS-- +opcache +--CONFLICTS-- +server +--FILE-- + +--CLEAN-- + +--EXPECT-- +1,1,1,1 +2,2,2,2 +1 +2 +bool(true) +keep +keep-persistent +1,1,1,1 +3 diff --git a/ext/opcache/tests/static_cache_persistent_static_invalidate_after_access_001.phpt b/ext/opcache/tests/static_cache_persistent_static_invalidate_after_access_001.phpt new file mode 100644 index 000000000000..473787fc02ed --- /dev/null +++ b/ext/opcache/tests/static_cache_persistent_static_invalidate_after_access_001.phpt @@ -0,0 +1,60 @@ +--TEST-- +OPcache PersistentStatic is not republished after opcache_invalidate() in the same request +--EXTENSIONS-- +opcache +--CONFLICTS-- +server +--FILE-- + +--CLEAN-- + +--EXPECT-- +1,1 +2,2 +3,3 +bool(true) +1,1 diff --git a/ext/opcache/tests/static_cache_persistent_static_local_copy_array_mutation_publish_001.phpt b/ext/opcache/tests/static_cache_persistent_static_local_copy_array_mutation_publish_001.phpt new file mode 100644 index 000000000000..b79b4d337efe --- /dev/null +++ b/ext/opcache/tests/static_cache_persistent_static_local_copy_array_mutation_publish_001.phpt @@ -0,0 +1,128 @@ +--TEST-- +OPcache PersistentStatic skips nested array local-copy mutation publish +--EXTENSIONS-- +opcache +--CONFLICTS-- +server +--FILE-- +logFile, "serialize\n", FILE_APPEND); + return ['logFile' => $this->logFile, 'value' => $this->value]; + } + + public function __unserialize(array $data): void + { + $this->logFile = $data['logFile']; + $this->value = $data['value']; + } +} + +class PersistentStaticLocalCopyArrayState +{ + #[OPcache\PersistentStatic] + public static array $value = []; +} + +function local_copy_log_file(): string +{ + return __DIR__ . '/persistent_static_local_copy_array_mutation_publish_001.log'; +} + +function local_copy_log_count(): int +{ + $logFile = local_copy_log_file(); + return is_file($logFile) ? count(file($logFile, FILE_IGNORE_NEW_LINES)) : 0; +} + +$action = $_GET['action'] ?? 'read'; +$logFile = local_copy_log_file(); + +if ($action === 'reset') { + @unlink($logFile); + OPcache\persistent_clear(); + opcache_reset(); + echo "reset\n"; + return; +} + +if ($action === 'seed') { + PersistentStaticLocalCopyArrayState::$value = [ + 'nested' => ['seed'], + 'probe' => new PersistentStaticLocalCopyPublishProbe($logFile, 42), + ]; + echo 'seed-static=', count(PersistentStaticLocalCopyArrayState::$value['nested']), "\n"; + echo 'seed-log=', local_copy_log_count(), "\n"; + return; +} + +if ($action === 'local') { + $copy = PersistentStaticLocalCopyArrayState::$value['nested']; + $copy[] = 'local'; + echo 'local-copy=', count($copy), "\n"; + echo 'local-static=', count(PersistentStaticLocalCopyArrayState::$value['nested']), "\n"; + echo 'local-log=', local_copy_log_count(), "\n"; + return; +} + +if ($action === 'direct') { + PersistentStaticLocalCopyArrayState::$value['nested'][] = 'direct'; + echo 'direct-static=', count(PersistentStaticLocalCopyArrayState::$value['nested']), "\n"; + echo 'direct-log=', local_copy_log_count(), "\n"; + return; +} + +echo 'read-static=', count(PersistentStaticLocalCopyArrayState::$value['nested']), "\n"; +echo 'read-log=', local_copy_log_count(), "\n"; +PHP); + +$php = getenv('TEST_PHP_EXECUTABLE'); +if ($php) { + $php = realpath(__DIR__ . '/../../../' . $php) ?: $php; + putenv('TEST_PHP_EXECUTABLE=' . $php); +} + +@unlink(__DIR__ . '/persistent_static_local_copy_array_mutation_publish_001.log'); + +include 'php_cli_server.inc'; +php_cli_server_start('-d opcache.enable=1 -d opcache.enable_cli=1 -d opcache.static_cache.persistent_size_mb=32 -d opcache.optimization_level=0 -d opcache.file_update_protection=0 -d opcache.jit=0'); + +$base = 'http://' . PHP_CLI_SERVER_ADDRESS . '/persistent_static_local_copy_array_mutation_publish_001.php'; +echo file_get_contents($base . '?action=reset'); +echo file_get_contents($base . '?action=seed'); +echo file_get_contents($base . '?action=local'); +echo file_get_contents($base . '?action=read'); +echo file_get_contents($base . '?action=direct'); +echo file_get_contents($base . '?action=read'); + +?> +--CLEAN-- + +--EXPECT-- +reset +seed-static=1 +seed-log=1 +local-copy=2 +local-static=1 +local-log=1 +read-static=1 +read-log=1 +direct-static=2 +direct-log=2 +read-static=2 +read-log=2 diff --git a/ext/opcache/tests/static_cache_persistent_static_method_static_persistence_001.phpt b/ext/opcache/tests/static_cache_persistent_static_method_static_persistence_001.phpt new file mode 100644 index 000000000000..f6bdb65374a6 --- /dev/null +++ b/ext/opcache/tests/static_cache_persistent_static_method_static_persistence_001.phpt @@ -0,0 +1,72 @@ +--TEST-- +OPcache PersistentStatic persists method static variables across requests +--EXTENSIONS-- +opcache +--CONFLICTS-- +server +--FILE-- + +--CLEAN-- + +--EXPECT-- +1,2,1,1 +2,3,1,1 diff --git a/ext/opcache/tests/static_cache_persistent_static_object_assignment_fast_path_001.phpt b/ext/opcache/tests/static_cache_persistent_static_object_assignment_fast_path_001.phpt new file mode 100644 index 000000000000..858fb5101e7b --- /dev/null +++ b/ext/opcache/tests/static_cache_persistent_static_object_assignment_fast_path_001.phpt @@ -0,0 +1,113 @@ +--TEST-- +OPcache PersistentStatic class object assignments publish a snapshot without following later object mutations +--EXTENSIONS-- +opcache +--CONFLICTS-- +server +--FILE-- +name = 'mutated'; + echo 'global-in-request=', PersistentStaticAssignmentFastPathState::$payload->name, "\n"; + return; +} + +if ($action === 'read-global') { + echo 'global-after=', PersistentStaticAssignmentFastPathState::$payload?->name ?? 'missing', "\n"; + return; +} + +if ($action === 'seed-global-property') { + PersistentStaticPropertyAssignmentFastPathState::$payload = new PersistentStaticAssignmentFastPathPayload('assigned'); + PersistentStaticPropertyAssignmentFastPathState::$payload->name = 'mutated'; + echo 'global-property-in-request=', PersistentStaticPropertyAssignmentFastPathState::$payload->name, "\n"; + return; +} + +if ($action === 'read-global-property') { + echo 'global-property-after=', PersistentStaticPropertyAssignmentFastPathState::$payload?->name ?? 'missing', "\n"; + return; +} + +if ($action === 'seed-cached') { + VolatileStaticAssignmentTrackedState::$payload = new PersistentStaticAssignmentFastPathPayload('assigned'); + VolatileStaticAssignmentTrackedState::$payload->name = 'mutated'; + echo 'cached-in-request=', VolatileStaticAssignmentTrackedState::$payload->name, "\n"; + return; +} + +echo 'cached-after=', VolatileStaticAssignmentTrackedState::$payload?->name ?? 'missing', "\n"; +PHP); + +$php = getenv('TEST_PHP_EXECUTABLE'); +if ($php) { + $php = realpath(__DIR__ . '/../../../' . $php) ?: $php; + putenv('TEST_PHP_EXECUTABLE=' . $php); +} + +include 'php_cli_server.inc'; +php_cli_server_start('-d opcache.enable=1 -d opcache.enable_cli=1 -d opcache.static_cache.volatile_size_mb=32 -d opcache.static_cache.persistent_size_mb=32 -d opcache.file_update_protection=0 -d opcache.jit=0'); + +$base = 'http://' . PHP_CLI_SERVER_ADDRESS . '/persistent_static_object_assignment_fast_path_001.php'; +echo file_get_contents($base . '?action=reset'); +echo file_get_contents($base . '?action=seed-global'); +echo file_get_contents($base . '?action=read-global'); +echo file_get_contents($base . '?action=reset'); +echo file_get_contents($base . '?action=seed-global-property'); +echo file_get_contents($base . '?action=read-global-property'); +echo file_get_contents($base . '?action=reset'); +echo file_get_contents($base . '?action=seed-cached'); +echo file_get_contents($base . '?action=read-cached'); + +?> +--CLEAN-- + +--EXPECT-- +reset +global-in-request=mutated +global-after=assigned +reset +global-property-in-request=mutated +global-property-after=assigned +reset +cached-in-request=mutated +cached-after=mutated diff --git a/ext/opcache/tests/static_cache_persistent_static_object_assignment_snapshot_001.phpt b/ext/opcache/tests/static_cache_persistent_static_object_assignment_snapshot_001.phpt new file mode 100644 index 000000000000..ed2a25b1fbc7 --- /dev/null +++ b/ext/opcache/tests/static_cache_persistent_static_object_assignment_snapshot_001.phpt @@ -0,0 +1,40 @@ +--TEST-- +OPcache PersistentStatic snapshots object assignments without following object property writes +--EXTENSIONS-- +opcache +--INI-- +opcache.enable=1 +opcache.enable_cli=1 +opcache.static_cache.persistent_size_mb=32 +opcache.optimization_level=0 +opcache.file_update_protection=0 +opcache.jit=0 +--FILE-- + 1]; +var_dump(NestedObjectPropertyState::$propertyState->count); +var_dump(OPcache\persistent_cache_info()['entry_count']); + +NestedObjectPropertyState::$propertyState->count++; +var_dump(NestedObjectPropertyState::$propertyState->count); +var_dump(OPcache\persistent_cache_info()['entry_count']); + +NestedObjectPropertyState::$propertyState->count = 10; +var_dump(NestedObjectPropertyState::$propertyState->count); +var_dump(OPcache\persistent_cache_info()['entry_count']); + +?> +--EXPECT-- +int(1) +int(1) +int(2) +int(1) +int(10) +int(1) diff --git a/ext/opcache/tests/static_cache_persistent_static_object_dim_mutation_001.phpt b/ext/opcache/tests/static_cache_persistent_static_object_dim_mutation_001.phpt new file mode 100644 index 000000000000..5e5186b3dd06 --- /dev/null +++ b/ext/opcache/tests/static_cache_persistent_static_object_dim_mutation_001.phpt @@ -0,0 +1,110 @@ +--TEST-- +OPcache static attributes handle ArrayObject dimension writes by publication kind +--EXTENSIONS-- +opcache +spl +--CONFLICTS-- +server +--FILE-- + 0, 'events' => [], 'remove' => 'seed']); + return $object; + } +} + +class PersistentStaticArrayObjectMethodState +{ + #[OPcache\PersistentStatic] + public static function value(): ArrayObject + { + static $object = null; + + $object ??= new ArrayObject(['count' => 0, 'events' => [], 'remove' => 'seed']); + return $object; + } +} + +class PersistentStaticArrayObjectPropertyState +{ + #[OPcache\PersistentStatic] + public static ?ArrayObject $object = null; + + public static function value(): ArrayObject + { + self::$object ??= new ArrayObject(['count' => 0, 'events' => [], 'remove' => 'seed']); + return self::$object; + } +} + +$action = $_GET['action'] ?? 'read'; +$backend = $_GET['backend'] ?? 'volatile_static'; + +if ($action === 'reset') { + OPcache\volatile_clear(); + opcache_reset(); + echo "reset\n"; + return; +} + +$object = match ($backend) { + 'volatile_static' => VolatileStaticArrayObjectMethodState::value(), + 'persistent_static' => PersistentStaticArrayObjectMethodState::value(), + 'persistent_static_property' => PersistentStaticArrayObjectPropertyState::value(), + default => throw new RuntimeException('unknown backend'), +}; + +if ($action === 'mutate') { + $object['count'] += 1; + $object['events'] = ['x']; + unset($object['remove']); +} + +echo $backend, '=', (int) $object['count'], ',', count($object['events']), ',', ($object->offsetExists('remove') ? 1 : 0), "\n"; +PHP); + +$php = getenv('TEST_PHP_EXECUTABLE'); +if ($php) { + $php = realpath(__DIR__ . '/../../../' . $php) ?: $php; + putenv('TEST_PHP_EXECUTABLE=' . $php); +} + +include 'php_cli_server.inc'; +php_cli_server_start('-d opcache.enable=1 -d opcache.enable_cli=1 -d opcache.static_cache.volatile_size_mb=32 -d opcache.static_cache.persistent_size_mb=32 -d opcache.file_update_protection=0 -d opcache.jit=0'); + +$base = 'http://' . PHP_CLI_SERVER_ADDRESS . '/persistent_static_object_dim_mutation_001.php'; +foreach (['volatile_static', 'persistent_static', 'persistent_static_property'] as $backend) { + $query = 'backend=' . $backend; + echo file_get_contents($base . '?action=reset'); + echo file_get_contents($base . '?action=read&' . $query); + echo file_get_contents($base . '?action=mutate&' . $query); + echo file_get_contents($base . '?action=read&' . $query); +} + +?> +--CLEAN-- + +--EXPECT-- +reset +volatile_static=0,0,1 +volatile_static=1,1,0 +volatile_static=1,1,0 +reset +persistent_static=0,0,1 +persistent_static=1,1,0 +persistent_static=0,0,1 +reset +persistent_static_property=0,0,1 +persistent_static_property=1,1,0 +persistent_static_property=0,0,1 diff --git a/ext/opcache/tests/static_cache_persistent_static_object_property_mutation_001.phpt b/ext/opcache/tests/static_cache_persistent_static_object_property_mutation_001.phpt new file mode 100644 index 000000000000..13c167e2555a --- /dev/null +++ b/ext/opcache/tests/static_cache_persistent_static_object_property_mutation_001.phpt @@ -0,0 +1,171 @@ +--TEST-- +OPcache static attributes handle object property array writes by publication kind +--EXTENSIONS-- +opcache +--CONFLICTS-- +server +--FILE-- + VolatileStaticPlainMethodState::value(), + 'volatile_static:safe_direct' => VolatileStaticSafeDirectMethodState::value(), + 'persistent_static:plain' => PersistentStaticPlainMethodState::value(), + 'persistent_static:safe_direct' => PersistentStaticSafeDirectMethodState::value(), + 'persistent_static_property:plain' => PersistentStaticPlainPropertyState::value(), + 'persistent_static_property:safe_direct' => PersistentStaticSafeDirectPropertyState::value(), + default => throw new RuntimeException('unknown backend or kind'), +}; + +if ($action === 'mutate') { + $object->bag[] = 'x'; +} + +echo $backend, '/', $kind, '=', count($object->bag), "\n"; +PHP); + +$php = getenv('TEST_PHP_EXECUTABLE'); +if ($php) { + $php = realpath(__DIR__ . '/../../../' . $php) ?: $php; + putenv('TEST_PHP_EXECUTABLE=' . $php); +} + +include 'php_cli_server.inc'; +php_cli_server_start('-d opcache.enable=1 -d opcache.enable_cli=1 -d opcache.static_cache.volatile_size_mb=32 -d opcache.static_cache.persistent_size_mb=32 -d opcache.file_update_protection=0'); + +$base = 'http://' . PHP_CLI_SERVER_ADDRESS . '/persistent_static_object_property_mutation_001.php'; +foreach (['volatile_static', 'persistent_static', 'persistent_static_property'] as $backend) { + foreach (['plain', 'safe_direct'] as $kind) { + $query = 'backend=' . $backend . '&kind=' . $kind; + echo file_get_contents($base . '?action=reset'); + echo file_get_contents($base . '?action=read&' . $query); + echo file_get_contents($base . '?action=mutate&' . $query); + echo file_get_contents($base . '?action=read&' . $query); + } +} + +?> +--CLEAN-- + +--EXPECT-- +reset +volatile_static/plain=0 +volatile_static/plain=1 +volatile_static/plain=1 +reset +volatile_static/safe_direct=0 +volatile_static/safe_direct=1 +volatile_static/safe_direct=1 +reset +persistent_static/plain=0 +persistent_static/plain=1 +persistent_static/plain=0 +reset +persistent_static/safe_direct=0 +persistent_static/safe_direct=1 +persistent_static/safe_direct=0 +reset +persistent_static_property/plain=0 +persistent_static_property/plain=1 +persistent_static_property/plain=0 +reset +persistent_static_property/safe_direct=0 +persistent_static_property/safe_direct=1 +persistent_static_property/safe_direct=0 diff --git a/ext/opcache/tests/static_cache_persistent_static_property_array_mutation_publish_001.phpt b/ext/opcache/tests/static_cache_persistent_static_property_array_mutation_publish_001.phpt new file mode 100644 index 000000000000..6ffff4972b54 --- /dev/null +++ b/ext/opcache/tests/static_cache_persistent_static_property_array_mutation_publish_001.phpt @@ -0,0 +1,71 @@ +--TEST-- +OPcache PersistentStatic publishes property array mutations immediately +--EXTENSIONS-- +opcache +--CONFLICTS-- +server +--FILE-- + [1]]; + NestedSnapshotPropertyState::$propertyState['numbers'][] = 2; + echo 'seed=', implode(',', NestedSnapshotPropertyState::$propertyState['numbers']), "\n"; + echo 'entries=', OPcache\persistent_cache_info()['entry_count'], "\n"; + return; +} + +if ($action === 'mutate') { + NestedSnapshotPropertyState::$propertyState['numbers'][] = 3; + echo 'mutate=', implode(',', NestedSnapshotPropertyState::$propertyState['numbers']), "\n"; + return; +} + +echo 'read=', implode(',', NestedSnapshotPropertyState::$propertyState['numbers'] ?? []), "\n"; +PHP); + +$php = getenv('TEST_PHP_EXECUTABLE'); +if ($php) { + $php = realpath(__DIR__ . '/../../../' . $php) ?: $php; + putenv('TEST_PHP_EXECUTABLE=' . $php); +} + +include 'php_cli_server.inc'; +php_cli_server_start('-d opcache.enable=1 -d opcache.enable_cli=1 -d opcache.static_cache.persistent_size_mb=32 -d opcache.optimization_level=0 -d opcache.file_update_protection=0 -d opcache.jit=0'); + +$base = 'http://' . PHP_CLI_SERVER_ADDRESS . '/persistent_static_004.php'; +echo file_get_contents($base . '?action=reset'); +echo file_get_contents($base . '?action=seed'); +echo file_get_contents($base . '?action=read'); +echo file_get_contents($base . '?action=mutate'); +echo file_get_contents($base . '?action=read'); + +?> +--CLEAN-- + +--EXPECT-- +reset +seed=1,2 +entries=1 +read=1,2 +mutate=1,2,3 +read=1,2,3 diff --git a/ext/opcache/tests/static_cache_persistent_static_readonly_publish_001.phpt b/ext/opcache/tests/static_cache_persistent_static_readonly_publish_001.phpt new file mode 100644 index 000000000000..0af6ed85bb4f --- /dev/null +++ b/ext/opcache/tests/static_cache_persistent_static_readonly_publish_001.phpt @@ -0,0 +1,175 @@ +--TEST-- +OPcache VolatileStatic tracking skips read-only shutdown publish like PersistentStatic +--EXTENSIONS-- +opcache +--CONFLICTS-- +server +--FILE-- +logFile, "serialize\n", FILE_APPEND); + return ['logFile' => $this->logFile, 'value' => $this->value]; + } + + public function __unserialize(array $data): void + { + $this->logFile = $data['logFile']; + $this->value = $data['value']; + } +} + +class CachedReadOnlyMethodState +{ + #[OPcache\VolatileStatic(strategy: OPcache\CacheStrategy::Tracking)] + public static function value(string $logFile): int + { + static $probe = null; + + if ($probe === null) { + $probe = new ReadOnlyPublishProbe($logFile, 11); + } + + return $probe->value; + } +} + +class PersistentReadOnlyMethodState +{ + #[OPcache\PersistentStatic] + public static function value(string $logFile): int + { + static $probe = null; + + if ($probe === null) { + $probe = new ReadOnlyPublishProbe($logFile, 13); + } + + return $probe->value; + } +} + +class CachedReadOnlyPropertyState +{ + #[OPcache\VolatileStatic(strategy: OPcache\CacheStrategy::Tracking)] + public static ?ReadOnlyPublishProbe $probe = null; + + public static function value(string $logFile): int + { + self::$probe ??= new ReadOnlyPublishProbe($logFile, 17); + return self::$probe->value; + } +} + +class PersistentReadOnlyPropertyState +{ + #[OPcache\PersistentStatic] + public static ?ReadOnlyPublishProbe $probe = null; + + public static function value(string $logFile): int + { + self::$probe ??= new ReadOnlyPublishProbe($logFile, 19); + return self::$probe->value; + } +} + +function readonly_publish_log_file(string $kind): string +{ + return __DIR__ . '/persistent_static_readonly_publish_001_' . $kind . '.log'; +} + +$kinds = [ + 'cached_method', + 'cached_property', + 'persistent_method', + 'persistent_property', +]; +$action = $_GET['action'] ?? 'value'; +$kind = $_GET['kind'] ?? 'cached_method'; + +if ($action === 'reset') { + foreach ($kinds as $logKind) { + @unlink(readonly_publish_log_file($logKind)); + } + OPcache\volatile_clear(); + opcache_reset(); + echo "reset\n"; + return; +} + +$logFile = readonly_publish_log_file($kind); +if ($action === 'count') { + echo $kind, '_count=', is_file($logFile) ? count(file($logFile, FILE_IGNORE_NEW_LINES)) : 0, "\n"; + return; +} + +$value = match ($kind) { + 'cached_method' => CachedReadOnlyMethodState::value($logFile), + 'cached_property' => CachedReadOnlyPropertyState::value($logFile), + 'persistent_method' => PersistentReadOnlyMethodState::value($logFile), + 'persistent_property' => PersistentReadOnlyPropertyState::value($logFile), + default => throw new RuntimeException('Unknown kind: ' . $kind), +}; + +echo $kind, '=', $value, "\n"; +PHP); + +$php = getenv('TEST_PHP_EXECUTABLE'); +if ($php) { + $php = realpath(__DIR__ . '/../../../' . $php) ?: $php; + putenv('TEST_PHP_EXECUTABLE=' . $php); +} + +foreach (['cached_method', 'cached_property', 'persistent_method', 'persistent_property'] as $kind) { + @unlink(__DIR__ . '/persistent_static_readonly_publish_001_' . $kind . '.log'); +} + +include 'php_cli_server.inc'; +php_cli_server_start('-d opcache.enable=1 -d opcache.enable_cli=1 -d opcache.static_cache.volatile_size_mb=32 -d opcache.static_cache.persistent_size_mb=32 -d opcache.file_update_protection=0'); + +$base = 'http://' . PHP_CLI_SERVER_ADDRESS . '/persistent_static_readonly_publish_001.php'; +echo file_get_contents($base . '?action=reset'); +foreach (['cached_method', 'cached_property', 'persistent_method', 'persistent_property'] as $kind) { + echo file_get_contents($base . '?action=value&kind=' . $kind); + echo file_get_contents($base . '?action=count&kind=' . $kind); + echo file_get_contents($base . '?action=value&kind=' . $kind); + echo file_get_contents($base . '?action=count&kind=' . $kind); +} + +?> +--CLEAN-- + +--EXPECT-- +reset +cached_method=11 +cached_method_count=1 +cached_method=11 +cached_method_count=1 +cached_property=17 +cached_property_count=1 +cached_property=17 +cached_property_count=1 +persistent_method=13 +persistent_method_count=1 +persistent_method=13 +persistent_method_count=1 +persistent_property=19 +persistent_property_count=1 +persistent_property=19 +persistent_property_count=1 diff --git a/ext/opcache/tests/static_cache_persistent_static_recursive_shared_graph_snapshot_001.phpt b/ext/opcache/tests/static_cache_persistent_static_recursive_shared_graph_snapshot_001.phpt new file mode 100644 index 000000000000..43f2fae5d74f --- /dev/null +++ b/ext/opcache/tests/static_cache_persistent_static_recursive_shared_graph_snapshot_001.phpt @@ -0,0 +1,76 @@ +--TEST-- +OPcache PersistentStatic snapshots recursive shared-graph state across requests +--EXTENSIONS-- +opcache +spl +--CONFLICTS-- +server +--FILE-- + new GraphNode(new GraphChild(1), ['hits' => []]), + 'serial' => new ArrayObject([ + 'box' => new GraphChild(10), + 'nested' => ['hits' => []], + ]), + ]; +} + +$state = &RecursivePropertyState::$state; +$state['graph']->child->count++; +$state['graph']->trail['hits'][] = $state['graph']->child->count; + +$serial = $state['serial']; +$serial['box']->count++; +$nested = $serial['nested']; +$nested['hits'][] = $serial['box']->count; +$serial['nested'] = $nested; + +echo $state['graph']->child->count, ',', count($state['graph']->trail['hits']), ',', $serial['box']->count, ',', count($serial['nested']['hits']), "\n"; +PHP); + +$php = getenv('TEST_PHP_EXECUTABLE'); +if ($php) { + $php = realpath(__DIR__ . '/../../../' . $php) ?: $php; + putenv('TEST_PHP_EXECUTABLE=' . $php); +} + +include 'php_cli_server.inc'; +php_cli_server_start('-d opcache.enable=1 -d opcache.enable_cli=1 -d opcache.static_cache.persistent_size_mb=32 -d opcache.file_update_protection=0 -d opcache.jit=0'); + +echo file_get_contents('http://' . PHP_CLI_SERVER_ADDRESS . '/persistent_static_011.php'); +echo file_get_contents('http://' . PHP_CLI_SERVER_ADDRESS . '/persistent_static_011.php'); +echo file_get_contents('http://' . PHP_CLI_SERVER_ADDRESS . '/persistent_static_011.php'); + +?> +--CLEAN-- + +--EXPECT-- +2,1,11,1 +2,1,11,1 +2,1,11,1 diff --git a/ext/opcache/tests/static_cache_persistent_static_reset_001.phpt b/ext/opcache/tests/static_cache_persistent_static_reset_001.phpt new file mode 100644 index 000000000000..0479b9bb1195 --- /dev/null +++ b/ext/opcache/tests/static_cache_persistent_static_reset_001.phpt @@ -0,0 +1,83 @@ +--TEST-- +OPcache PersistentStatic and explicit static caches are deleted by opcache_reset() +--EXTENSIONS-- +opcache +--CONFLICTS-- +server +--FILE-- + +--CLEAN-- + +--EXPECT-- +1,1,1,1 +2,2,2,2 +bool(true) +missing-volatile +missing-persistent +1,1,1,1 diff --git a/ext/opcache/tests/static_cache_persistent_static_reset_after_access_001.phpt b/ext/opcache/tests/static_cache_persistent_static_reset_after_access_001.phpt new file mode 100644 index 000000000000..006b1b05031b --- /dev/null +++ b/ext/opcache/tests/static_cache_persistent_static_reset_after_access_001.phpt @@ -0,0 +1,60 @@ +--TEST-- +OPcache PersistentStatic is not republished after opcache_reset() in the same request +--EXTENSIONS-- +opcache +--CONFLICTS-- +server +--FILE-- + +--CLEAN-- + +--EXPECT-- +1,1 +2,2 +3,3 +bool(true) +1,1 diff --git a/ext/opcache/tests/static_cache_persistent_static_store_failure_exception_001.phpt b/ext/opcache/tests/static_cache_persistent_static_store_failure_exception_001.phpt new file mode 100644 index 000000000000..af6b64e2258c --- /dev/null +++ b/ext/opcache/tests/static_cache_persistent_static_store_failure_exception_001.phpt @@ -0,0 +1,117 @@ +--TEST-- +OPcache PersistentStatic storage failures throw StaticCacheException +--EXTENSIONS-- +opcache +--INI-- +opcache.enable=1 +opcache.enable_cli=1 +opcache.static_cache.persistent_size_mb=8 +opcache.optimization_level=0 +opcache.file_update_protection=0 +opcache.jit=0 +--FILE-- +getMessage(), "\n"; + } +} + +class PersistentStaticStoreFailureProperty +{ + #[OPcache\PersistentStatic] + public static mixed $value = null; +} + +#[OPcache\PersistentStatic] +class PersistentStaticStoreFailureClass +{ + public static mixed $value = null; +} + +class PersistentStaticStoreFailureMethod +{ + #[OPcache\PersistentStatic] + public static function assign(mixed $value): void + { + static $state = null; + + $state = $value; + } +} + +class PersistentStaticStoreFailureArray +{ + #[OPcache\PersistentStatic] + public static array $value = []; +} + +class PersistentStaticStoreFailureUnsupportedValueBox +{ + public function __construct(public mixed $value) + { + } +} + +OPcache\persistent_clear(); + +dump_static_cache_exception('property-resource', function (): void { + $resource = fopen('/dev/null', 'r'); + try { + PersistentStaticStoreFailureProperty::$value = $resource; + } finally { + if (is_resource($resource)) { + fclose($resource); + } + } +}); + +dump_static_cache_exception('property-closure', function (): void { + PersistentStaticStoreFailureProperty::$value = static fn () => null; +}); + +dump_static_cache_exception('property-object-resource', function (): void { + $resource = fopen('/dev/null', 'r'); + try { + PersistentStaticStoreFailureProperty::$value = new PersistentStaticStoreFailureUnsupportedValueBox($resource); + } finally { + if (is_resource($resource)) { + fclose($resource); + } + } +}); + +dump_static_cache_exception('property-object-closure', function (): void { + PersistentStaticStoreFailureProperty::$value = new PersistentStaticStoreFailureUnsupportedValueBox(static fn () => null); +}); + +$unused = PersistentStaticStoreFailureClass::$value; +dump_static_cache_exception('class-closure', function (): void { + PersistentStaticStoreFailureClass::$value = static fn () => null; +}); + +dump_static_cache_exception('method-closure', function (): void { + PersistentStaticStoreFailureMethod::assign(static fn () => null); +}); + +PersistentStaticStoreFailureArray::$value = []; +dump_static_cache_exception('array-mutation-overflow', function (): void { + PersistentStaticStoreFailureArray::$value[] = str_repeat('X', 12 * 1024 * 1024); +}); + +OPcache\persistent_clear(); + +?> +--EXPECT-- +property-resource: OPcache\StaticCacheException: resources cannot be stored in the static cache +property-closure: OPcache\StaticCacheException: Closure objects cannot be stored in the static cache +property-object-resource: OPcache\StaticCacheException: resources cannot be stored in the static cache +property-object-closure: OPcache\StaticCacheException: Closure objects cannot be stored in the static cache +class-closure: OPcache\StaticCacheException: Closure objects cannot be stored in the static cache +method-closure: OPcache\StaticCacheException: Closure objects cannot be stored in the static cache +array-mutation-overflow: OPcache\StaticCacheException: not enough shared memory left diff --git a/ext/opcache/tests/static_cache_persistent_static_timestamp_invalidate_001.phpt b/ext/opcache/tests/static_cache_persistent_static_timestamp_invalidate_001.phpt new file mode 100644 index 000000000000..3990d880a945 --- /dev/null +++ b/ext/opcache/tests/static_cache_persistent_static_timestamp_invalidate_001.phpt @@ -0,0 +1,53 @@ +--TEST-- +OPcache PersistentStatic state is discarded when a cached class definition changes +--EXTENSIONS-- +opcache +--CONFLICTS-- +server +--FILE-- + +--CLEAN-- + +--EXPECT-- +v1:1 +v1:2 +v2:1 diff --git a/ext/opcache/tests/static_cache_persistent_static_zts_threads_001.phpt b/ext/opcache/tests/static_cache_persistent_static_zts_threads_001.phpt new file mode 100644 index 000000000000..05d970de4407 --- /dev/null +++ b/ext/opcache/tests/static_cache_persistent_static_zts_threads_001.phpt @@ -0,0 +1,302 @@ +--TEST-- +OPcache PersistentStatic class, property, and method state handles cross-thread writes on ZTS builds +--EXTENSIONS-- +opcache +--SKIPIF-- + +--FILE-- + MAX_COMMAND_OUTPUT_BYTES) { + return substr($contents, 0, MAX_COMMAND_OUTPUT_BYTES) . "\n...[output truncated]\n"; + } + + return $contents; +} + +function runCommand(array $command): array +{ + $stdoutFile = tempnam(sys_get_temp_dir(), 'opcache-zts-out-'); + $stderrFile = tempnam(sys_get_temp_dir(), 'opcache-zts-err-'); + if ($stdoutFile === false || $stderrFile === false) { + throw new RuntimeException('failed to create temporary files'); + } + + $descriptorSpec = [ + 0 => ['pipe', 'r'], + 1 => ['file', $stdoutFile, 'w'], + 2 => ['file', $stderrFile, 'w'], + ]; + + $process = proc_open($command, $descriptorSpec, $pipes); + if (!is_resource($process)) { + @unlink($stdoutFile); + @unlink($stderrFile); + throw new RuntimeException('proc_open failed'); + } + + fclose($pipes[0]); + $status = proc_close($process); + + $stdout = readCommandOutput($stdoutFile); + $stderr = readCommandOutput($stderrFile); + @unlink($stdoutFile); + @unlink($stderrFile); + + return [$status, $stdout, $stderr]; +} + +function parseBuildFlags(string $flags): array +{ + $tokens = preg_split('/\s+/', trim($flags)); + if ($tokens === false) { + return []; + } + + return array_values(array_filter($tokens, static function (string $token): bool { + return $token !== '' && !str_contains($token, '$('); + })); +} + +function resolveBuildFlags(string $buildRoot, array $variables): array +{ + $makefile = @file_get_contents($buildRoot . '/Makefile'); + $flags = []; + + if ($makefile === false) { + return $flags; + } + + foreach ($variables as $variable) { + if (preg_match('/^' . preg_quote($variable, '/') . '\s*=\s*(.+)$/m', $makefile, $matches)) { + $flags = array_merge($flags, parseBuildFlags($matches[1])); + } + } + + return $flags; +} + +function resolveBuildCommand(string $buildRoot, string $variable, array $fallback): array +{ + $value = getenv($variable); + if (is_string($value) && $value !== '') { + $tokens = parseBuildFlags($value); + if ($tokens !== []) { + return $tokens; + } + } + + $tokens = resolveBuildFlags($buildRoot, [$variable]); + return $tokens !== [] ? $tokens : $fallback; +} + +function resolveBuildRoot(string $root): string +{ + $buildRoot = null; + $php = getenv('TEST_PHP_EXECUTABLE') ?: PHP_BINARY; + if ($php) { + $php = realpath($php); + if ($php !== false) { + if (PHP_OS_FAMILY === 'Windows') { + $candidate = dirname($php); + if (glob($candidate . '/php*embed.lib')) { + $buildRoot = $candidate; + } + } else { + $candidate = dirname($php, 3); + if (file_exists($candidate . '/.libs/libphp.a') || file_exists($candidate . '/libs/libphp.a')) { + $buildRoot = $candidate; + } + } + } + } + + if ($buildRoot === null && PHP_OS_FAMILY !== 'Windows' && (file_exists($root . '/.libs/libphp.a') || file_exists($root . '/libs/libphp.a'))) { + $buildRoot = $root; + } + + if ($buildRoot === null) { + throw new RuntimeException('Failed to resolve build root'); + } + + return $buildRoot; +} + +function resolveExtraLibs(string $buildRoot): array +{ + if (PHP_OS_FAMILY === 'Windows') { + return []; + } + + $makefile = @file_get_contents($buildRoot . '/Makefile'); + if ($makefile !== false && preg_match('/^EXTRA_LIBS\s*=\s*(.+)$/m', $makefile, $matches)) { + $libs = preg_split('/\s+/', trim($matches[1])); + if ($libs !== false && $libs !== ['']) { + return $libs; + } + } + + return [ + '-lm', + '-lpthread', + '-ldl', + '-lresolv', + '-lutil', + ]; +} + +function resolveWindowsEmbedLib(string $buildRoot): string +{ + $candidates = glob($buildRoot . '/php*embed.lib'); + if ($candidates !== false && $candidates !== []) { + return $candidates[0]; + } + + throw new RuntimeException('Failed to resolve Windows embed library'); +} + +$root = realpath(__DIR__ . '/../../..'); +if ($root === false) { + throw new RuntimeException('Failed to resolve source root'); +} + +$buildRoot = resolveBuildRoot($root); +$source = __DIR__ . '/helpers/persistent_static_zts_threads_001.c'; +$binary = __DIR__ . '/helpers/persistent_static_zts_threads_001' . (PHP_OS_FAMILY === 'Windows' ? '.exe' : '.out'); +$scenario = __DIR__ . '/helpers/persistent_static_zts_threads_001.inc'; +$compiler = resolveBuildCommand($buildRoot, 'CC', PHP_OS_FAMILY === 'Windows' ? ['cl'] : ['gcc']); +$buildFlags = PHP_OS_FAMILY === 'Windows' ? [] : resolveBuildFlags($buildRoot, ['CPPFLAGS', 'CFLAGS_CLEAN']); +$extraLibs = resolveExtraLibs($buildRoot); + +if (PHP_OS_FAMILY === 'Windows') { + $compileCommand = [ + ...$compiler, + '/nologo', + '/DZEND_ENABLE_STATIC_TSRMLS_CACHE=1', + '/DHAVE_CONFIG_H', + '/I' . $buildRoot, + '/I' . $buildRoot . '/main', + '/I' . $buildRoot . '/Zend', + '/I' . $buildRoot . '/TSRM', + '/I' . $buildRoot . '/sapi/embed', + '/I' . $root, + '/I' . $root . '/main', + '/I' . $root . '/Zend', + '/I' . $root . '/TSRM', + '/I' . $root . '/sapi/embed', + '/Fe:' . $binary, + $source, + '/link', + resolveWindowsEmbedLib($buildRoot), + ]; +} else { + $compileCommand = [ + ...$compiler, + ...$buildFlags, + '-DZEND_ENABLE_STATIC_TSRMLS_CACHE=1', + '-DHAVE_CONFIG_H', + '-I' . $buildRoot, + '-I' . $buildRoot . '/main', + '-I' . $buildRoot . '/Zend', + '-I' . $buildRoot . '/TSRM', + '-I' . $buildRoot . '/sapi/embed', + '-I' . $root, + '-I' . $root . '/main', + '-I' . $root . '/Zend', + '-I' . $root . '/TSRM', + '-I' . $root . '/sapi/embed', + '-o', + $binary, + $source, + $root . '/sapi/embed/php_embed.c', + file_exists($buildRoot . '/.libs/libphp.a') ? $buildRoot . '/.libs/libphp.a' : $buildRoot . '/libs/libphp.a', + ]; + $compileCommand = array_merge($compileCommand, $extraLibs); +} + +[$status, $stdout, $stderr] = runCommand($compileCommand); +if ($status !== 0) { + echo $stdout, $stderr; + exit(1); +} + +@chmod($binary, 0755); +[$status, $stdout, $stderr] = runCommand([$binary, $scenario]); +echo $stdout; +if ($status !== 0) { + echo $stderr; + exit(1); +} + +?> +--CLEAN-- + +--EXPECT-- +ok diff --git a/ext/opcache/tests/static_cache_preload_001.inc b/ext/opcache/tests/static_cache_preload_001.inc new file mode 100644 index 000000000000..0578cbf21703 --- /dev/null +++ b/ext/opcache/tests/static_cache_preload_001.inc @@ -0,0 +1,25 @@ + true]; + + public static function next(): int + { + static $value = 0; + + return ++$value; + } +} + +class StaticCachePreloadMethodState +{ + #[OPcache\VolatileStatic] + public static function value(): array + { + static $value = ['preload' => true]; + + return $value; + } +} diff --git a/ext/opcache/tests/static_cache_preload_001.phpt b/ext/opcache/tests/static_cache_preload_001.phpt new file mode 100644 index 000000000000..990e70f7620e --- /dev/null +++ b/ext/opcache/tests/static_cache_preload_001.phpt @@ -0,0 +1,32 @@ +--TEST-- +OPcache static cache attributes work with preloaded classes +--EXTENSIONS-- +opcache +--SKIPIF-- + +--INI-- +opcache.enable=1 +opcache.enable_cli=1 +opcache.static_cache.volatile_size_mb=32 +opcache.static_cache.persistent_size_mb=32 +opcache.preload={PWD}/static_cache_preload_001.inc +--FILE-- + +--EXPECT-- +array(1) { + ["preload"]=> + bool(true) +} +int(1) +array(1) { + ["preload"]=> + bool(true) +} diff --git a/ext/opcache/tests/static_cache_startup_failure_001.phpt b/ext/opcache/tests/static_cache_startup_failure_001.phpt new file mode 100644 index 000000000000..fea81428e60c --- /dev/null +++ b/ext/opcache/tests/static_cache_startup_failure_001.phpt @@ -0,0 +1,63 @@ +--TEST-- +OPcache static cache disables the subsystem after startup failure +--EXTENSIONS-- +opcache +--ENV-- +OPCACHE_STATIC_CACHE_FORCE_STARTUP_FAILURE=1 +--INI-- +opcache.enable=1 +opcache.enable_cli=1 +opcache.static_cache.volatile_size_mb=32 +opcache.static_cache.persistent_size_mb=32 +--FILE-- +getMessage(), "\n"; +} + +try { + OPcache\persistent_store('key', 'value'); +} catch (Throwable $e) { + echo get_class($e), ': ', $e->getMessage(), "\n"; +} + +?> +--EXPECT-- +bool(false) +bool(false) +bool(true) +bool(false) +int(33554432) +bool(false) +bool(false) +bool(true) +bool(false) +int(33554432) +bool(true) +bool(true) +string(42) "Unable to initialize shared memory backend" +string(42) "Unable to initialize shared memory backend" +OPcache\StaticCacheException: Unable to initialize shared memory backend +OPcache\StaticCacheException: Unable to initialize shared memory backend diff --git a/ext/opcache/tests/static_cache_volatile_cache_allocator_001.phpt b/ext/opcache/tests/static_cache_volatile_cache_allocator_001.phpt new file mode 100644 index 000000000000..4ac630110d1c --- /dev/null +++ b/ext/opcache/tests/static_cache_volatile_cache_allocator_001.phpt @@ -0,0 +1,39 @@ +--TEST-- +OPcache volatile cache reuses freed payload memory +--EXTENSIONS-- +opcache +--INI-- +opcache.enable=1 +opcache.enable_cli=1 +opcache.static_cache.volatile_size_mb=8 +--FILE-- + +--EXPECT-- +bool(true) +bool(true) +bool(true) +int(2400000) +bool(true) +bool(true) +bool(true) +int(2400000) diff --git a/ext/opcache/tests/static_cache_volatile_cache_allocator_002.phpt b/ext/opcache/tests/static_cache_volatile_cache_allocator_002.phpt new file mode 100644 index 000000000000..bd1c38159888 --- /dev/null +++ b/ext/opcache/tests/static_cache_volatile_cache_allocator_002.phpt @@ -0,0 +1,33 @@ +--TEST-- +OPcache volatile cache coalesces adjacent freed payload blocks +--EXTENSIONS-- +opcache +--INI-- +opcache.enable=1 +opcache.enable_cli=1 +opcache.static_cache.volatile_size_mb=8 +--FILE-- + +--EXPECT-- +bool(true) +bool(true) +bool(true) +bool(true) +int(3500000) diff --git a/ext/opcache/tests/static_cache_volatile_cache_basic_001.phpt b/ext/opcache/tests/static_cache_volatile_cache_basic_001.phpt new file mode 100644 index 000000000000..1e9fcdc51a2e --- /dev/null +++ b/ext/opcache/tests/static_cache_volatile_cache_basic_001.phpt @@ -0,0 +1,48 @@ +--TEST-- +OPcache volatile cache basic KV operations +--EXTENSIONS-- +opcache +--INI-- +opcache.enable=1 +opcache.enable_cli=1 +opcache.static_cache.volatile_size_mb=32 +--FILE-- + 1, 'y' => true])); +var_dump(OPcache\volatile_fetch('a')); + +var_dump(OPcache\volatile_store_array(['foo' => 'bar', 'baz' => true])); +var_dump(OPcache\volatile_fetch('foo')); +var_dump(OPcache\volatile_fetch('baz')); + +OPcache\volatile_delete_array(['foo', 'baz']); +var_dump(OPcache\volatile_fetch('foo', 'fallback')); + +OPcache\volatile_clear(); +var_dump(OPcache\volatile_fetch('n', 'fallback')); + +?> +--EXPECT-- +bool(true) +int(41) +bool(true) +string(3) "php" +bool(true) +array(2) { + ["x"]=> + int(1) + ["y"]=> + bool(true) +} +bool(true) +string(3) "bar" +bool(true) +string(8) "fallback" +string(8) "fallback" diff --git a/ext/opcache/tests/static_cache_volatile_cache_combined_delete_clear_001.phpt b/ext/opcache/tests/static_cache_volatile_cache_combined_delete_clear_001.phpt new file mode 100644 index 000000000000..51fd5764c292 --- /dev/null +++ b/ext/opcache/tests/static_cache_volatile_cache_combined_delete_clear_001.phpt @@ -0,0 +1,111 @@ +--TEST-- +OPcache volatile cache combined entries survive delete and clear without corrupting fetched values +--EXTENSIONS-- +opcache +--INI-- +opcache.enable=1 +opcache.enable_cli=1 +opcache.static_cache.volatile_size_mb=8 +--FILE-- + $i, + 'text' => str_repeat($prefix, 64), + 'nested' => ['value' => $i * $multiplier], + ]; + } + + return $rows; +} + +function build_graph(string $prefix, int $multiplier): CombinedDeleteClearNode +{ + return new CombinedDeleteClearNode(build_rows($prefix, $multiplier)); +} + +function build_refs(): array +{ + $payload = ['value' => str_repeat('R', 256)]; + $payload['alias'] =& $payload['value']; + + return $payload; +} + +OPcache\volatile_clear(); + +var_dump(OPcache\volatile_store('keep-string', str_repeat('S', 1200000))); +var_dump(OPcache\volatile_store('victim-graph', build_graph('O', 3))); +var_dump(OPcache\volatile_store('keep-refs', build_refs())); + +$fetched = OPcache\volatile_fetch('victim-graph'); +OPcache\volatile_delete('victim-graph'); + +var_dump(OPcache\volatile_fetch('victim-graph', 'missing')); +var_dump($fetched instanceof CombinedDeleteClearNode); +var_dump($fetched->rows[123]['text']); + +$fetched->rows[123]['text'] = 'local-after-delete'; +var_dump($fetched->rows[123]['text']); + +var_dump(OPcache\volatile_store('replacement-graph', build_graph('N', 7))); + +$replacement = OPcache\volatile_fetch('replacement-graph'); +$refs = OPcache\volatile_fetch('keep-refs'); +$refs['alias'] = 'mutated-via-alias'; + +var_dump($replacement->rows[123]['text']); +var_dump($replacement->rows[123]['nested']['value']); +var_dump($refs['value']); + +$cleared = OPcache\volatile_fetch('replacement-graph'); +OPcache\volatile_clear(); + +var_dump(OPcache\volatile_fetch('keep-string', 'missing')); +var_dump(OPcache\volatile_fetch('replacement-graph', 'missing')); +var_dump($cleared->rows[321]['text']); + +$cleared->rows[321]['text'] = 'local-after-clear'; +var_dump($cleared->rows[321]['text']); + +var_dump(OPcache\volatile_store('after-clear-string', str_repeat('A', 1200000))); +var_dump(OPcache\volatile_store('after-clear-graph', build_graph('C', 11))); + +$afterClear = OPcache\volatile_fetch('after-clear-graph'); + +var_dump(strlen(OPcache\volatile_fetch('after-clear-string'))); +var_dump($afterClear->rows[321]['text']); +var_dump($afterClear->rows[321]['nested']['value']); + +?> +--EXPECT-- +bool(true) +bool(true) +bool(true) +string(7) "missing" +bool(true) +string(64) "OOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOO" +string(18) "local-after-delete" +bool(true) +string(64) "NNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNN" +int(861) +string(17) "mutated-via-alias" +string(7) "missing" +string(7) "missing" +string(64) "NNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNN" +string(17) "local-after-clear" +bool(true) +bool(true) +int(1200000) +string(64) "CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC" +int(3531) diff --git a/ext/opcache/tests/static_cache_volatile_cache_complex_001.phpt b/ext/opcache/tests/static_cache_volatile_cache_complex_001.phpt new file mode 100644 index 000000000000..c44d157a37c2 --- /dev/null +++ b/ext/opcache/tests/static_cache_volatile_cache_complex_001.phpt @@ -0,0 +1,115 @@ +--TEST-- +OPcache volatile cache complex values use native encoding with lossless fallback +--EXTENSIONS-- +opcache +--INI-- +opcache.enable=1 +opcache.enable_cli=1 +opcache.static_cache.volatile_size_mb=32 +--FILE-- +id = $id; + $this->name = $name; + } + + public function __serialize(): array + { + self::$serializeCount++; + + return ['id' => $this->id, 'name' => $this->name]; + } + + public function __unserialize(array $data): void + { + self::$unserializeCount++; + $this->id = $data['id']; + $this->name = $data['name']; + } + + public function info(): string + { + return $this->id . ':' . $this->name; + } +} + +$gap = []; +$gap[4] = 'seed'; +unset($gap[4]); + +$payload = [ + 'nested' => ['alpha' => [1, 2, 3], 'beta' => ['x' => 'y']], + 'props' => new SimpleUser('Alice', 30), + 'serialize' => new SerUser(7, 'Bob'), + 'internal' => new DateTimeImmutable('2026-06-15 09:30:00', new DateTimeZone('UTC')), + 'gap' => $gap, +]; + +var_dump(OPcache\volatile_store('complex', $payload)); + +$fetched = OPcache\volatile_fetch('complex'); +var_dump($fetched['props'] instanceof SimpleUser); +var_dump($fetched['props']->name); +var_dump($fetched['props']->age); +var_dump($fetched['serialize'] instanceof SerUser); +var_dump($fetched['serialize']->info()); +var_dump(SerUser::$serializeCount); +var_dump(SerUser::$unserializeCount); +var_dump($fetched['internal'] instanceof DateTimeImmutable); +var_dump($fetched['internal']->format('Y-m-d H:i:s')); + +$fetched['gap'][] = 'tail'; +var_dump(array_key_last($fetched['gap'])); + +$shared = new stdClass(); +$shared->value = 42; +var_dump(OPcache\volatile_store('shared_pair', [$shared, $shared])); + +$pair = OPcache\volatile_fetch('shared_pair'); +var_dump($pair[0] instanceof stdClass); +var_dump(spl_object_id($pair[0]) === spl_object_id($pair[1])); + +$refs = ['value' => 1]; +$refs['alias'] =& $refs['value']; +var_dump(OPcache\volatile_store('refs', $refs)); + +$fetchedRefs = OPcache\volatile_fetch('refs'); +$fetchedRefs['alias'] = 7; +var_dump($fetchedRefs['value']); + +?> +--EXPECT-- +bool(true) +bool(true) +string(5) "Alice" +int(30) +bool(true) +string(5) "7:Bob" +int(1) +int(1) +bool(true) +string(19) "2026-06-15 09:30:00" +int(5) +bool(true) +bool(true) +bool(true) +bool(true) +int(7) diff --git a/ext/opcache/tests/static_cache_volatile_cache_destructive_mutators_retire_shared_graph_001.phpt b/ext/opcache/tests/static_cache_volatile_cache_destructive_mutators_retire_shared_graph_001.phpt new file mode 100644 index 000000000000..06bdecdf2adf --- /dev/null +++ b/ext/opcache/tests/static_cache_volatile_cache_destructive_mutators_retire_shared_graph_001.phpt @@ -0,0 +1,139 @@ +--TEST-- +OPcache volatile cache shared graph survives destructive mutators and reuse +--EXTENSIONS-- +opcache +pcntl +--SKIPIF-- + +--INI-- +opcache.enable=1 +opcache.enable_cli=1 +opcache.static_cache.volatile_size_mb=32 +--FILE-- + $i, + 'text' => str_repeat($prefix, 64), + 'nested' => ['value' => $i * $multiplier], + ]; + } + + return $rows; +} + +function wait_for_file(string $path): void +{ + $deadline = microtime(true) + 5.0; + while (!file_exists($path)) { + if (microtime(true) >= $deadline) { + throw new RuntimeException("timed out waiting for {$path}"); + } + usleep(1000); + } +} + +function run_destructive_mutator(string $operation): void +{ + $key = 'destructive_mutator_retire_' . $operation . '_' . getmypid(); + $overwriteKey = 'destructive_mutator_retire_overwrite_' . $operation . '_' . getmypid(); + $prefix = sys_get_temp_dir() . '/opcache_destructive_mutator_retire_' . getmypid() . '_' . $operation; + $readyFile = $prefix . '.ready'; + $doneFile = $prefix . '.done'; + $resultFile = $prefix . '.result'; + @unlink($readyFile); + @unlink($doneFile); + @unlink($resultFile); + + OPcache\volatile_clear(); + if (!OPcache\volatile_store($key, new DestructiveMutatorPayload(build_rows('T', 3)))) { + throw new RuntimeException('store failed'); + } + + $pid = pcntl_fork(); + if ($pid === -1) { + throw new RuntimeException('pcntl_fork() failed'); + } + + if ($pid === 0) { + $fetched = OPcache\volatile_fetch($key); + file_put_contents($readyFile, 'ready'); + wait_for_file($doneFile); + + $before = $fetched->rows[123]['text']; + $fetched->rows[123]['text'] = 'changed after ' . $operation; + $after = $fetched->rows[123]['text']; + $nested = $fetched->rows[123]['nested']['value']; + $refetch = OPcache\volatile_fetch($key, 'MISS') === 'MISS' ? 'MISS' : 'HIT'; + + file_put_contents($resultFile, $before . "\n" . $after . "\n" . $nested . "\n" . $refetch); + exit(0); + } + + wait_for_file($readyFile); + switch ($operation) { + case 'delete': + OPcache\volatile_delete($key); + break; + case 'clear': + OPcache\volatile_clear(); + break; + case 'reset': + opcache_reset(); + break; + } + + if (!OPcache\volatile_store($overwriteKey, new DestructiveMutatorPayload(build_rows('X', 7)))) { + throw new RuntimeException('overwrite store failed'); + } + file_put_contents($doneFile, 'done'); + pcntl_waitpid($pid, $status); + + echo $operation, "\n", file_get_contents($resultFile), "\n"; + + @unlink($readyFile); + @unlink($doneFile); + @unlink($resultFile); +} + +run_destructive_mutator('delete'); +run_destructive_mutator('clear'); +run_destructive_mutator('reset'); + +?> +--EXPECT-- +delete +TTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTT +changed after delete +369 +MISS +clear +TTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTT +changed after clear +369 +MISS +reset +TTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTT +changed after reset +369 +MISS +--CLEAN-- + diff --git a/ext/opcache/tests/static_cache_volatile_cache_direct_cache_safe_001.phpt b/ext/opcache/tests/static_cache_volatile_cache_direct_cache_safe_001.phpt new file mode 100644 index 000000000000..be70c3dc6095 --- /dev/null +++ b/ext/opcache/tests/static_cache_volatile_cache_direct_cache_safe_001.phpt @@ -0,0 +1,191 @@ +--TEST-- +OPcache __DirectCacheSafe uses a direct DateTime path for safe subclasses and keeps fallback for wakeup hooks +--EXTENSIONS-- +opcache +--INI-- +opcache.enable=1 +opcache.enable_cli=1 +opcache.static_cache.volatile_size_mb=32 +--FILE-- +getAttributes(OPcache\__DirectCacheSafe::class))); +var_dump(count((new ReflectionClass(DateTimeImmutable::class))->getAttributes(OPcache\__DirectCacheSafe::class))); + +$immutable = new DateTimeImmutable('2026-06-15 11:00:00.111111', new DateTimeZone('UTC')); + +var_dump(OPcache\volatile_store('immutable_datetime', $immutable)); + +$immutableCopy = OPcache\volatile_fetch('immutable_datetime'); +var_dump($immutableCopy instanceof DateTimeImmutable); +var_dump($immutableCopy->format('Y-m-d H:i:s.u e')); + +$offsetImmutable = new DateTimeImmutable('2023-10-27 10:00:00 +05:30'); + +var_dump(OPcache\volatile_store('offset_immutable_datetime', $offsetImmutable)); + +$offsetImmutableCopy = OPcache\volatile_fetch('offset_immutable_datetime'); +var_dump($offsetImmutableCopy instanceof DateTimeImmutable); +var_dump($offsetImmutableCopy->format('Y-m-d H:i:s P T')); +var_dump($offsetImmutableCopy->getTimezone()->getName()); + +$abbrImmutable = new DateTimeImmutable('2023-10-27 10:00:00 EST'); + +var_dump(OPcache\volatile_store('abbr_immutable_datetime', $abbrImmutable)); + +$abbrImmutableCopy = OPcache\volatile_fetch('abbr_immutable_datetime'); +var_dump($abbrImmutableCopy instanceof DateTimeImmutable); +var_dump($abbrImmutableCopy->format('Y-m-d H:i:s P T')); +var_dump($abbrImmutableCopy->getTimezone()->getName()); + +class EventDateTime extends DateTime +{ + private string $label; + protected int $revision; + + public function __construct(string $time, DateTimeZone $timezone, string $label, int $revision) + { + parent::__construct($time, $timezone); + $this->label = $label; + $this->revision = $revision; + } + + public function describe(): string + { + return $this->label . ':' . $this->revision; + } +} + +$event = new EventDateTime('2026-06-15 09:30:00.123456', new DateTimeZone('Europe/Paris'), 'launch', 7); + +var_dump(OPcache\volatile_store('event_datetime', $event)); + +$eventCopy = OPcache\volatile_fetch('event_datetime'); +var_dump($eventCopy instanceof EventDateTime); +var_dump($eventCopy->format('Y-m-d H:i:s.u e')); +var_dump($eventCopy->describe()); + +class CustomSerializedDateTime extends DateTime +{ + public static int $serializeCount = 0; + public static int $unserializeCount = 0; + + private string $label; + + public function __construct(string $time, DateTimeZone $timezone, string $label) + { + parent::__construct($time, $timezone); + $this->label = $label; + } + + public function __serialize(): array + { + self::$serializeCount++; + + return parent::__serialize() + ['label' => $this->label]; + } + + public function __unserialize(array $data): void + { + self::$unserializeCount++; + parent::__unserialize($data); + $this->label = $data['label']; + } + + public function label(): string + { + return $this->label; + } +} + +$custom = new CustomSerializedDateTime('2026-06-15 10:45:00.654321', new DateTimeZone('UTC'), 'fallback'); + +var_dump(OPcache\volatile_store('custom_datetime', $custom)); + +$customCopy = OPcache\volatile_fetch('custom_datetime'); +var_dump($customCopy instanceof CustomSerializedDateTime); +var_dump($customCopy->format('Y-m-d H:i:s.u e')); +var_dump($customCopy->label()); +var_dump(CustomSerializedDateTime::$serializeCount); +var_dump(CustomSerializedDateTime::$unserializeCount); + +class WakefulSerializedDateTime extends DateTime +{ + public static int $serializeCount = 0; + public static int $unserializeCount = 0; + + private string $label; + + public function __construct(string $time, DateTimeZone $timezone, string $label) + { + parent::__construct($time, $timezone); + $this->label = $label; + } + + public function __serialize(): array + { + self::$serializeCount++; + + return parent::__serialize() + ['label' => $this->label]; + } + + public function __unserialize(array $data): void + { + self::$unserializeCount++; + parent::__unserialize($data); + $this->label = $data['label']; + } + + public function __sleep(): array + { + return ['label']; + } + + public function label(): string + { + return $this->label; + } +} + +$wakeful = new WakefulSerializedDateTime('2026-06-15 12:15:00.987654', new DateTimeZone('UTC'), 'fallback'); + +var_dump(OPcache\volatile_store('wakeful_datetime', $wakeful)); + +$wakefulCopy = OPcache\volatile_fetch('wakeful_datetime'); +var_dump($wakefulCopy instanceof WakefulSerializedDateTime); +var_dump($wakefulCopy->format('Y-m-d H:i:s.u e')); +var_dump($wakefulCopy->label()); +var_dump(WakefulSerializedDateTime::$serializeCount); +var_dump(WakefulSerializedDateTime::$unserializeCount); + +?> +--EXPECT-- +int(1) +int(1) +bool(true) +bool(true) +string(30) "2026-06-15 11:00:00.111111 UTC" +bool(true) +bool(true) +string(35) "2023-10-27 10:00:00 +05:30 GMT+0530" +string(6) "+05:30" +bool(true) +bool(true) +string(30) "2023-10-27 10:00:00 -05:00 EST" +string(3) "EST" +bool(true) +bool(true) +string(39) "2026-06-15 09:30:00.123456 Europe/Paris" +string(8) "launch:7" +bool(true) +bool(true) +string(30) "2026-06-15 10:45:00.654321 UTC" +string(8) "fallback" +int(0) +int(0) +bool(true) +bool(true) +string(30) "2026-06-15 12:15:00.987654 UTC" +string(8) "fallback" +int(1) +int(1) diff --git a/ext/opcache/tests/static_cache_volatile_cache_direct_cache_safe_002.phpt b/ext/opcache/tests/static_cache_volatile_cache_direct_cache_safe_002.phpt new file mode 100644 index 000000000000..65713eb13cf2 --- /dev/null +++ b/ext/opcache/tests/static_cache_volatile_cache_direct_cache_safe_002.phpt @@ -0,0 +1,111 @@ +--TEST-- +OPcache __DirectCacheSafe covers DateTimeZone and DateInterval direct paths +--EXTENSIONS-- +opcache +--INI-- +opcache.enable=1 +opcache.enable_cli=1 +opcache.static_cache.volatile_size_mb=32 +--FILE-- +getAttributes(OPcache\__DirectCacheSafe::class))); +var_dump(count((new ReflectionClass(DateInterval::class))->getAttributes(OPcache\__DirectCacheSafe::class))); + +class TaggedTimeZone extends DateTimeZone +{ + private string $label; + + public function __construct(string $timezone, string $label) + { + parent::__construct($timezone); + $this->label = $label; + } + + public function label(): string + { + return $this->label; + } +} + +$timezone = new TaggedTimeZone('Europe/Paris', 'paris'); + +var_dump(OPcache\volatile_store('safe_direct_timezone', $timezone)); + +$timezoneCopy = OPcache\volatile_fetch('safe_direct_timezone'); +var_dump($timezoneCopy instanceof TaggedTimeZone); +var_dump($timezoneCopy->getName()); +var_dump($timezoneCopy->label()); + +$offsetTimezone = (new DateTimeImmutable('2023-10-27 10:00:00 +05:30'))->getTimezone(); + +var_dump(OPcache\volatile_store('safe_direct_offset_timezone', $offsetTimezone)); + +$offsetTimezoneCopy = OPcache\volatile_fetch('safe_direct_offset_timezone'); +var_dump($offsetTimezoneCopy instanceof DateTimeZone); +var_dump($offsetTimezoneCopy->getName()); + +$abbrTimezone = (new DateTimeImmutable('2023-10-27 10:00:00 EST'))->getTimezone(); + +var_dump(OPcache\volatile_store('safe_direct_abbr_timezone', $abbrTimezone)); + +$abbrTimezoneCopy = OPcache\volatile_fetch('safe_direct_abbr_timezone'); +var_dump($abbrTimezoneCopy instanceof DateTimeZone); +var_dump($abbrTimezoneCopy->getName()); + +class TaggedInterval extends DateInterval +{ + private string $label; + protected int $revision; + + public function __construct(string $duration, string $label, int $revision) + { + parent::__construct($duration); + $this->label = $label; + $this->revision = $revision; + } + + public function describe(): string + { + return $this->label . ':' . $this->revision; + } +} + +$interval = new TaggedInterval('P1Y2M3DT4H5M6S', 'window', 9); + +var_dump(OPcache\volatile_store('safe_direct_interval', $interval)); + +$intervalCopy = OPcache\volatile_fetch('safe_direct_interval'); +var_dump($intervalCopy instanceof TaggedInterval); +var_dump($intervalCopy->format('%y-%m-%d %h:%i:%s')); +var_dump($intervalCopy->describe()); + +$relativeInterval = DateInterval::createFromDateString('2 days 4 hours'); + +var_dump(OPcache\volatile_store('safe_direct_relative_interval', $relativeInterval)); + +$relativeIntervalCopy = OPcache\volatile_fetch('safe_direct_relative_interval'); +var_dump($relativeIntervalCopy instanceof DateInterval); +var_dump($relativeIntervalCopy->format('%d %h')); + +?> +--EXPECT-- +int(1) +int(1) +bool(true) +bool(true) +string(12) "Europe/Paris" +string(5) "paris" +bool(true) +bool(true) +string(6) "+05:30" +bool(true) +bool(true) +string(3) "EST" +bool(true) +bool(true) +string(11) "1-2-3 4:5:6" +string(8) "window:9" +bool(true) +bool(true) +string(3) "2 4" diff --git a/ext/opcache/tests/static_cache_volatile_cache_direct_cache_safe_003.phpt b/ext/opcache/tests/static_cache_volatile_cache_direct_cache_safe_003.phpt new file mode 100644 index 000000000000..58015f64cdcb --- /dev/null +++ b/ext/opcache/tests/static_cache_volatile_cache_direct_cache_safe_003.phpt @@ -0,0 +1,203 @@ +--TEST-- +OPcache __DirectCacheSafe covers SPL direct paths +--EXTENSIONS-- +opcache +spl +--INI-- +opcache.enable=1 +opcache.enable_cli=1 +opcache.static_cache.volatile_size_mb=32 +--FILE-- +getAttributes(OPcache\__DirectCacheSafe::class))); +var_dump(count((new ReflectionClass(ArrayObject::class))->getAttributes(OPcache\__DirectCacheSafe::class))); +var_dump(count((new ReflectionClass(ArrayIterator::class))->getAttributes(OPcache\__DirectCacheSafe::class))); +var_dump(count((new ReflectionClass(RecursiveArrayIterator::class))->getAttributes(OPcache\__DirectCacheSafe::class))); + +class TaggedFixedArray extends SplFixedArray +{ + private string $tag; + protected int $version; + + public function __construct(int $size, string $tag, int $version) + { + parent::__construct($size); + $this->tag = $tag; + $this->version = $version; + } + + public function describe(): string + { + return $this->tag . ':' . $this->version; + } +} + +$fixed = new TaggedFixedArray(3, 'vec', 7); +$fixed[0] = 'a'; +$fixed[1] = ['nested' => 1]; +$fixed[2] = 42; + +var_dump(OPcache\volatile_store('safe_direct_spl_fixed', $fixed)); + +$fixedCopy = OPcache\volatile_fetch('safe_direct_spl_fixed'); +var_dump($fixedCopy instanceof TaggedFixedArray); +var_dump($fixedCopy->getSize()); +var_dump($fixedCopy[0]); +var_dump($fixedCopy[1]['nested']); +var_dump($fixedCopy[2]); +var_dump($fixedCopy->describe()); + +class LabelIterator extends ArrayIterator +{ +} + +class TaggedCollection extends ArrayObject +{ + private string $type; + + public function __construct(array $data, string $type, string $iteratorClass) + { + parent::__construct($data, 0, $iteratorClass); + $this->type = $type; + } + + public function type(): string + { + return $this->type; + } +} + +$collection = new TaggedCollection(['alpha' => 10, 'beta' => 20], 'metric', LabelIterator::class); + +var_dump(OPcache\volatile_store('safe_direct_array_object', $collection)); + +$collectionCopy = OPcache\volatile_fetch('safe_direct_array_object'); +$collectionIterator = $collectionCopy->getIterator(); +var_dump($collectionCopy instanceof TaggedCollection); +var_dump($collectionIterator instanceof LabelIterator); +var_dump($collectionCopy['alpha']); +var_dump($collectionCopy['beta']); +var_dump($collectionCopy->type()); + +class TaggedIterator extends ArrayIterator +{ + private string $label; + + public function __construct(array $data, string $label) + { + parent::__construct($data); + $this->label = $label; + } + + public function label(): string + { + return $this->label; + } +} + +$iterator = new TaggedIterator([3, 5, 8], 'fib'); + +var_dump(OPcache\volatile_store('safe_direct_array_iterator', $iterator)); + +$iteratorCopy = OPcache\volatile_fetch('safe_direct_array_iterator'); +$iteratorCopy->rewind(); +var_dump($iteratorCopy instanceof TaggedIterator); +var_dump($iteratorCopy->count()); +var_dump($iteratorCopy->current()); +var_dump($iteratorCopy->label()); + +class TaggedRecursiveIterator extends RecursiveArrayIterator +{ + private string $name; + + public function __construct(array $data, string $name) + { + parent::__construct($data); + $this->name = $name; + } + + public function name(): string + { + return $this->name; + } +} + +$recursive = new TaggedRecursiveIterator(['leaf' => ['value' => 99]], 'tree'); + +var_dump(OPcache\volatile_store('safe_direct_recursive_array_iterator', $recursive)); + +$recursiveCopy = OPcache\volatile_fetch('safe_direct_recursive_array_iterator'); +$recursiveCopy->rewind(); +var_dump($recursiveCopy instanceof TaggedRecursiveIterator); +var_dump($recursiveCopy->count()); +var_dump($recursiveCopy->hasChildren()); +var_dump($recursiveCopy->name()); + +class CustomSerializedArrayObject extends ArrayObject +{ + public static int $serializeCalls = 0; + public static int $unserializeCalls = 0; + + public function __construct(array $data) + { + parent::__construct($data); + } + + public function __serialize(): array + { + self::$serializeCalls++; + return ['payload' => parent::__serialize()]; + } + + public function __unserialize(array $data): void + { + self::$unserializeCalls++; + parent::__unserialize($data['payload']); + } +} + +$custom = new CustomSerializedArrayObject(['x' => 1]); + +var_dump(OPcache\volatile_store('safe_direct_array_override', $custom)); + +$customCopy = OPcache\volatile_fetch('safe_direct_array_override'); +var_dump($customCopy instanceof CustomSerializedArrayObject); +var_dump(CustomSerializedArrayObject::$serializeCalls); +var_dump(CustomSerializedArrayObject::$unserializeCalls); +var_dump($customCopy['x']); + +?> +--EXPECT-- +int(1) +int(1) +int(1) +int(1) +bool(true) +bool(true) +int(3) +string(1) "a" +int(1) +int(42) +string(5) "vec:7" +bool(true) +bool(true) +bool(true) +int(10) +int(20) +string(6) "metric" +bool(true) +bool(true) +int(3) +int(3) +string(3) "fib" +bool(true) +bool(true) +int(1) +bool(true) +string(4) "tree" +bool(true) +bool(true) +int(1) +int(1) +int(1) diff --git a/ext/opcache/tests/static_cache_volatile_cache_direct_cache_safe_004.phpt b/ext/opcache/tests/static_cache_volatile_cache_direct_cache_safe_004.phpt new file mode 100644 index 000000000000..3f61fac97ea7 --- /dev/null +++ b/ext/opcache/tests/static_cache_volatile_cache_direct_cache_safe_004.phpt @@ -0,0 +1,101 @@ +--TEST-- +OPcache __DirectCacheSafe covers SPL edge branches +--EXTENSIONS-- +opcache +spl +--INI-- +opcache.enable=1 +opcache.enable_cli=1 +opcache.static_cache.volatile_size_mb=32 +--FILE-- +getSize()); +var_dump($plainFixedCopy[0]); +var_dump($plainFixedCopy[1]); + +$emptyFixed = new SplFixedArray(0); + +var_dump(OPcache\volatile_store('safe_direct_empty_fixed', $emptyFixed)); + +$emptyFixedCopy = OPcache\volatile_fetch('safe_direct_empty_fixed'); +var_dump($emptyFixedCopy instanceof SplFixedArray); +var_dump($emptyFixedCopy->getSize()); + +$flagged = new ArrayObject(['name' => 'val'], ArrayObject::ARRAY_AS_PROPS); + +var_dump(OPcache\volatile_store('safe_direct_array_props', $flagged)); + +$flaggedCopy = OPcache\volatile_fetch('safe_direct_array_props'); +var_dump($flaggedCopy instanceof ArrayObject); +var_dump($flaggedCopy->getFlags()); +var_dump($flaggedCopy->name); +var_dump($flaggedCopy['name']); +var_dump($flaggedCopy->getIteratorClass()); + +error_reporting(E_ALL & ~E_DEPRECATED); + +class SelfBackedCollection extends ArrayObject +{ + public int $slot = 0; +} + +$selfBacked = new SelfBackedCollection(['alpha' => 1]); +$selfBacked->exchangeArray($selfBacked); +$selfBacked['slot'] = 7; + +var_dump(OPcache\volatile_store('safe_direct_self_array_object', $selfBacked)); + +$selfBackedCopy = OPcache\volatile_fetch('safe_direct_self_array_object'); +var_dump($selfBackedCopy instanceof SelfBackedCollection); +var_dump($selfBackedCopy->slot); +var_dump($selfBackedCopy->count()); +var_dump(isset($selfBackedCopy['slot'])); +var_dump($selfBackedCopy->getIteratorClass()); + +$backedIterator = (new ArrayObject(['x' => 1]))->getIterator(); + +var_dump(OPcache\volatile_store('safe_direct_object_backed_iterator', $backedIterator)); + +$backedIteratorCopy = OPcache\volatile_fetch('safe_direct_object_backed_iterator'); +$backedIteratorCopy->rewind(); +var_dump($backedIteratorCopy instanceof ArrayIterator); +var_dump($backedIteratorCopy->count()); +var_dump($backedIteratorCopy->key()); +var_dump($backedIteratorCopy->current()); + +?> +--EXPECT-- +bool(true) +bool(true) +int(2) +string(5) "first" +string(6) "second" +bool(true) +bool(true) +int(0) +bool(true) +bool(true) +int(2) +string(3) "val" +string(3) "val" +string(13) "ArrayIterator" +bool(true) +bool(true) +int(7) +int(1) +bool(true) +string(13) "ArrayIterator" +bool(true) +bool(true) +int(1) +string(1) "x" +int(1) diff --git a/ext/opcache/tests/static_cache_volatile_cache_direct_cache_safe_fork_001.phpt b/ext/opcache/tests/static_cache_volatile_cache_direct_cache_safe_fork_001.phpt new file mode 100644 index 000000000000..82d08922509a --- /dev/null +++ b/ext/opcache/tests/static_cache_volatile_cache_direct_cache_safe_fork_001.phpt @@ -0,0 +1,147 @@ +--TEST-- +OPcache __DirectCacheSafe subclasses survive forked fetch and safe serializer overrides stay direct +--EXTENSIONS-- +opcache +pcntl +spl +--SKIPIF-- + +--INI-- +opcache.enable=1 +opcache.enable_cli=1 +opcache.static_cache.volatile_size_mb=32 +--FILE-- +label = $label; + $this->revision = $revision; + } + + public function describe(): string + { + return $this->label . ':' . $this->revision; + } +} + +class LabelIterator extends ArrayIterator +{ +} + +class TaggedCollection extends ArrayObject +{ + private string $type; + + public function __construct(array $data, string $type, string $iteratorClass) + { + parent::__construct($data, 0, $iteratorClass); + $this->type = $type; + } + + public function type(): string + { + return $this->type; + } +} + +class CustomSerializedDateTime extends DateTime +{ + public static int $unserializeCount = 0; + + private string $label; + + public function __construct(string $time, DateTimeZone $timezone, string $label) + { + parent::__construct($time, $timezone); + $this->label = $label; + } + + public function __serialize(): array + { + return parent::__serialize() + ['label' => $this->label]; + } + + public function __unserialize(array $data): void + { + self::$unserializeCount++; + parent::__unserialize($data); + $this->label = $data['label']; + } + + public function label(): string + { + return $this->label; + } +} + +$readyFile = sys_get_temp_dir() . '/opcache_volatile_cache_direct_cache_safe_fork_' . getmypid() . '.ready'; +@unlink($readyFile); + +$pid = pcntl_fork(); +if ($pid === -1) { + throw new RuntimeException('pcntl_fork() failed'); +} + +if ($pid === 0) { + $deadline = microtime(true) + 5.0; + + while (!file_exists($readyFile)) { + if (microtime(true) >= $deadline) { + fwrite(STDERR, "parent did not signal readiness\n"); + exit(1); + } + + usleep(1000); + } + + $event = OPcache\volatile_fetch('safe_direct_event_datetime'); + $collection = OPcache\volatile_fetch('safe_direct_tagged_collection'); + $fallback = OPcache\volatile_fetch('safe_direct_custom_datetime'); + $iterator = $collection->getIterator(); + + echo $event->format('Y-m-d H:i:s.u e'), ',', $event->describe(), "\n"; + echo $collection->type(), ',', ($iterator instanceof LabelIterator ? 'LabelIterator' : get_debug_type($iterator)), ',', $collection['alpha'], ',', $collection['beta'], "\n"; + echo $fallback->format('Y-m-d H:i:s.u e'), ',', $fallback->label(), ',', CustomSerializedDateTime::$unserializeCount, "\n"; + exit(0); +} + +$event = new EventDateTime('2026-06-15 09:30:00.123456', new DateTimeZone('Europe/Paris'), 'launch', 7); +$collection = new TaggedCollection(['alpha' => 10, 'beta' => 20], 'metric', LabelIterator::class); +$fallback = new CustomSerializedDateTime('2026-06-15 10:45:00.654321', new DateTimeZone('UTC'), 'fallback'); + +if (!OPcache\volatile_store('safe_direct_event_datetime', $event)) { + throw new RuntimeException('Failed to store safe_direct_event_datetime'); +} +if (!OPcache\volatile_store('safe_direct_tagged_collection', $collection)) { + throw new RuntimeException('Failed to store safe_direct_tagged_collection'); +} +if (!OPcache\volatile_store('safe_direct_custom_datetime', $fallback)) { + throw new RuntimeException('Failed to store safe_direct_custom_datetime'); +} + +file_put_contents($readyFile, 'ready'); +pcntl_waitpid($pid, $status); +@unlink($readyFile); + +?> +--EXPECT-- +2026-06-15 09:30:00.123456 Europe/Paris,launch:7 +metric,LabelIterator,10,20 +2026-06-15 10:45:00.654321 UTC,fallback,0 +--CLEAN-- + diff --git a/ext/opcache/tests/static_cache_volatile_cache_direct_cache_safe_unstorable_001.phpt b/ext/opcache/tests/static_cache_volatile_cache_direct_cache_safe_unstorable_001.phpt new file mode 100644 index 000000000000..9c95f9f77954 --- /dev/null +++ b/ext/opcache/tests/static_cache_volatile_cache_direct_cache_safe_unstorable_001.phpt @@ -0,0 +1,55 @@ +--TEST-- +OPcache __DirectCacheSafe rejects unstorable values in SPL subclass state +--EXTENSIONS-- +opcache +spl +--INI-- +opcache.enable=1 +opcache.enable_cli=1 +opcache.static_cache.volatile_size_mb=32 +opcache.static_cache.persistent_size_mb=32 +--FILE-- +getMessage(), "\n"; + } +} + +$resource = fopen(__FILE__, 'r'); +$fixed_array = new SafeDirectUnstorableFixedArray(1); +$fixed_array[0] = $resource; +$array_object = new SafeDirectUnstorableArrayObject(['value' => static fn () => true]); + +var_dump(OPcache\volatile_store('fixed-resource', $fixed_array)); +var_dump(OPcache\volatile_fetch('fixed-resource', 'missing')); +var_dump(OPcache\volatile_store('array-object-closure', $array_object)); +var_dump(OPcache\volatile_fetch('array-object-closure', 'missing')); + +dump_persistent_exception(static fn () => OPcache\persistent_store('fixed-resource', $fixed_array)); +dump_persistent_exception(static fn () => OPcache\persistent_store('array-object-closure', $array_object)); + +fclose($resource); + +?> +--EXPECT-- +bool(false) +string(7) "missing" +bool(false) +string(7) "missing" +resources and Closure objects cannot be stored in the static cache +resources and Closure objects cannot be stored in the static cache diff --git a/ext/opcache/tests/static_cache_volatile_cache_direct_cache_safe_visibility_001.inc b/ext/opcache/tests/static_cache_volatile_cache_direct_cache_safe_visibility_001.inc new file mode 100644 index 000000000000..2cc073a96833 --- /dev/null +++ b/ext/opcache/tests/static_cache_volatile_cache_direct_cache_safe_visibility_001.inc @@ -0,0 +1,6 @@ +isInternal()); +var_dump($marker->isFinal()); + +var_dump(count((new ReflectionClass(DateTimeImmutable::class))->getAttributes(OPcache\__DirectCacheSafe::class))); + +$value = new DateTimeImmutable('2026-01-02 03:04:05', new DateTimeZone('UTC')); +var_dump(OPcache\volatile_store('hidden_safe_direct_datetime', $value)); +var_dump(OPcache\volatile_fetch('hidden_safe_direct_datetime')->format('Y-m-d H:i:s e')); + +require __DIR__ . '/static_cache_volatile_cache_direct_cache_safe_visibility_001.inc'; + +?> +--EXPECTF-- +bool(true) +bool(true) +bool(true) +bool(true) +int(1) +bool(true) +string(23) "2026-01-02 03:04:05 UTC" + +Fatal error: Only internal classes can be marked with #[OPcache\__DirectCacheSafe] in %sstatic_cache_volatile_cache_direct_cache_safe_visibility_001.inc on line %d diff --git a/ext/opcache/tests/static_cache_volatile_cache_expunge_before_relocation_001.phpt b/ext/opcache/tests/static_cache_volatile_cache_expunge_before_relocation_001.phpt new file mode 100644 index 000000000000..849466fcf66b --- /dev/null +++ b/ext/opcache/tests/static_cache_volatile_cache_expunge_before_relocation_001.phpt @@ -0,0 +1,48 @@ +--TEST-- +OPcache volatile cache expunges expired entries before relocating fragmented payload blocks +--EXTENSIONS-- +opcache +--INI-- +opcache.enable=1 +opcache.enable_cli=1 +opcache.static_cache.volatile_size_mb=8 +--FILE-- +name); +var_dump(OPcache\volatile_fetch('expired-tail', 'expired')); +var_dump(strlen(OPcache\volatile_fetch('after-expired'))); +var_dump(strlen(OPcache\volatile_fetch('first'))); +var_dump(strlen(OPcache\volatile_fetch('third'))); + +?> +--EXPECT-- +bool(true) +bool(true) +bool(true) +bool(true) +bool(true) +bool(true) +string(5) "probe" +string(7) "expired" +int(2400000) +int(1200000) +int(1200000) diff --git a/ext/opcache/tests/static_cache_volatile_cache_fetch_exists_001.phpt b/ext/opcache/tests/static_cache_volatile_cache_fetch_exists_001.phpt new file mode 100644 index 000000000000..b78e88e942d5 --- /dev/null +++ b/ext/opcache/tests/static_cache_volatile_cache_fetch_exists_001.phpt @@ -0,0 +1,40 @@ +--TEST-- +OPcache volatile_fetch default value and volatile_exists +--EXTENSIONS-- +opcache +--INI-- +opcache.enable=1 +opcache.enable_cli=1 +opcache.static_cache.volatile_size_mb=32 +--FILE-- + +--EXPECT-- +bool(false) +NULL +string(8) "fallback" +bool(true) +NULL +bool(true) +bool(true) +bool(false) +bool(true) +bool(false) diff --git a/ext/opcache/tests/static_cache_volatile_cache_info_surface_001.phpt b/ext/opcache/tests/static_cache_volatile_cache_info_surface_001.phpt new file mode 100644 index 000000000000..961ea89fc0b4 --- /dev/null +++ b/ext/opcache/tests/static_cache_volatile_cache_info_surface_001.phpt @@ -0,0 +1,40 @@ +--TEST-- +OPcache volatile cache status and info surface +--EXTENSIONS-- +opcache +--INI-- +opcache.enable=1 +opcache.enable_cli=1 +opcache.static_cache.volatile_size_mb=32 +--FILE-- + 0); +var_dump(is_string($status['volatile_cache']['shared_model'])); +var_dump($info == $status['volatile_cache']); +var_dump(array_key_exists('failure_reason', $info)); + +?> +--EXPECTF-- +int(33554432) +bool(true) +bool(true) +bool(false) +bool(true) +int(33554432) +int(33554432) +bool(true) +bool(true) +bool(true) +bool(false) diff --git a/ext/opcache/tests/static_cache_volatile_cache_lookup_cache_001.phpt b/ext/opcache/tests/static_cache_volatile_cache_lookup_cache_001.phpt new file mode 100644 index 000000000000..7b22a7382de5 --- /dev/null +++ b/ext/opcache/tests/static_cache_volatile_cache_lookup_cache_001.phpt @@ -0,0 +1,37 @@ +--TEST-- +OPcache volatile cache request-local lookup cache invalidates on writes +--EXTENSIONS-- +opcache +--INI-- +opcache.enable=1 +opcache.enable_cli=1 +opcache.static_cache.volatile_size_mb=32 +--FILE-- + +--EXPECT-- +string(4) "MISS" +bool(true) +string(5) "value" +bool(true) +string(4) "gone" +string(4) "MISS" +bool(true) +string(8) "clear me" +string(4) "MISS" diff --git a/ext/opcache/tests/static_cache_volatile_cache_lookup_cache_fork_001.phpt b/ext/opcache/tests/static_cache_volatile_cache_lookup_cache_fork_001.phpt new file mode 100644 index 000000000000..b0e0b08600fa --- /dev/null +++ b/ext/opcache/tests/static_cache_volatile_cache_lookup_cache_fork_001.phpt @@ -0,0 +1,117 @@ +--TEST-- +OPcache volatile cache request-local lookup cache sees cross-process updates on next fetch +--EXTENSIONS-- +opcache +pcntl +--SKIPIF-- + +--INI-- +opcache.enable=1 +opcache.enable_cli=1 +opcache.static_cache.volatile_size_mb=32 +--FILE-- += $deadline) { + throw new RuntimeException("timed out waiting for {$path}"); + } + + usleep(1000); + } +} + +function cleanup_files(string ...$paths): void +{ + foreach ($paths as $path) { + @unlink($path); + } +} + +function run_lookup_cache_scenario(string $key, ?string $initialValue, string $action, ?string $updatedValue): void +{ + $prefix = sys_get_temp_dir() . '/opcache_lookup_cache_' . $key . '_' . getmypid(); + $readyFile = $prefix . '.ready'; + $doneFile = $prefix . '.done'; + $resultFile = $prefix . '.result'; + + cleanup_files($readyFile, $doneFile, $resultFile); + + if ($initialValue === null) { + OPcache\volatile_delete($key); + } else { + OPcache\volatile_store($key, $initialValue); + } + + $pid = pcntl_fork(); + if ($pid === -1) { + throw new RuntimeException('pcntl_fork() failed'); + } + + if ($pid === 0) { + $first = fetch_or_miss($key); + file_put_contents($readyFile, 'ready'); + wait_for_file($doneFile); + $second = fetch_or_miss($key); + file_put_contents($resultFile, $first . "\n" . $second); + exit(0); + } + + wait_for_file($readyFile); + switch ($action) { + case 'store': + OPcache\volatile_store($key, $updatedValue); + break; + case 'delete': + OPcache\volatile_delete($key); + break; + case 'clear': + OPcache\volatile_clear(); + break; + default: + throw new RuntimeException("unknown action {$action}"); + } + file_put_contents($doneFile, 'done'); + pcntl_waitpid($pid, $status); + + echo trim((string) file_get_contents($resultFile)), "\n"; + cleanup_files($readyFile, $doneFile, $resultFile); +} + +OPcache\volatile_clear(); + +run_lookup_cache_scenario('lookup_hit_store_key', 'old', 'store', 'new'); +run_lookup_cache_scenario('lookup_miss_store_key', null, 'store', 'created'); +run_lookup_cache_scenario('lookup_hit_delete_key', 'old', 'delete', null); +run_lookup_cache_scenario('lookup_hit_clear_key', 'old', 'clear', null); + +?> +--EXPECT-- +old +new +MISS +created +old +MISS +old +MISS +--CLEAN-- + diff --git a/ext/opcache/tests/static_cache_volatile_cache_materialization_clear_fork_001.phpt b/ext/opcache/tests/static_cache_volatile_cache_materialization_clear_fork_001.phpt new file mode 100644 index 000000000000..394c0a7ca9c7 --- /dev/null +++ b/ext/opcache/tests/static_cache_volatile_cache_materialization_clear_fork_001.phpt @@ -0,0 +1,105 @@ +--TEST-- +OPcache volatile cache fetched shared graph survives cross-process volatile_clear() +--EXTENSIONS-- +opcache +pcntl +--SKIPIF-- + +--INI-- +opcache.enable=1 +opcache.enable_cli=1 +opcache.static_cache.volatile_size_mb=32 +--FILE-- + $i, + 'text' => str_repeat(chr(65 + ($i % 26)), 64), + 'nested' => ['value' => $i * 3], + ]; + } + + return $rows; +} + +function wait_for_file(string $path): void +{ + $deadline = microtime(true) + 5.0; + + while (!file_exists($path)) { + if (microtime(true) >= $deadline) { + throw new RuntimeException("timed out waiting for {$path}"); + } + + usleep(1000); + } +} + +$key = 'materialized_clear_payload'; +$prefix = sys_get_temp_dir() . '/opcache_volatile_cache_materialized_clear_' . getmypid(); +$readyFile = $prefix . '.ready'; +$doneFile = $prefix . '.done'; +$resultFile = $prefix . '.result'; +@unlink($readyFile); +@unlink($doneFile); +@unlink($resultFile); + +OPcache\volatile_clear(); +if (!OPcache\volatile_store($key, new ClearPayload(build_rows()))) { + throw new RuntimeException('store failed'); +} + +$pid = pcntl_fork(); +if ($pid === -1) { + throw new RuntimeException('pcntl_fork() failed'); +} + +if ($pid === 0) { + $fetched = OPcache\volatile_fetch($key); + $before = $fetched->rows[123]['text']; + file_put_contents($readyFile, 'ready'); + wait_for_file($doneFile); + $fetched->rows[123]['text'] = 'changed after clear'; + $after = $fetched->rows[123]['text']; + + $refetch = OPcache\volatile_fetch($key, 'MISS') === 'MISS' ? 'MISS' : 'HIT'; + + file_put_contents($resultFile, $before . "\n" . $after . "\n" . $refetch); + exit(0); +} + +wait_for_file($readyFile); +OPcache\volatile_clear(); +file_put_contents($doneFile, 'done'); +pcntl_waitpid($pid, $status); + +echo file_get_contents($resultFile), "\n"; +@unlink($readyFile); +@unlink($doneFile); +@unlink($resultFile); + +?> +--EXPECT-- +TTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTT +changed after clear +MISS +--CLEAN-- + diff --git a/ext/opcache/tests/static_cache_volatile_cache_materialization_clear_reuse_fork_001.phpt b/ext/opcache/tests/static_cache_volatile_cache_materialization_clear_reuse_fork_001.phpt new file mode 100644 index 000000000000..f7ecdce8a696 --- /dev/null +++ b/ext/opcache/tests/static_cache_volatile_cache_materialization_clear_reuse_fork_001.phpt @@ -0,0 +1,112 @@ +--TEST-- +OPcache volatile cache fetched shared graph survives cross-process clear and reuse +--EXTENSIONS-- +opcache +pcntl +--SKIPIF-- + +--INI-- +opcache.enable=1 +opcache.enable_cli=1 +opcache.static_cache.volatile_size_mb=32 +--FILE-- + $i, + 'text' => str_repeat($prefix, 64), + 'nested' => ['value' => $i * $multiplier], + ]; + } + + return $rows; +} + +function wait_for_file(string $path): void +{ + $deadline = microtime(true) + 5.0; + + while (!file_exists($path)) { + if (microtime(true) >= $deadline) { + throw new RuntimeException("timed out waiting for {$path}"); + } + + usleep(1000); + } +} + +$key = 'materialized_clear_reuse_payload'; +$overwriteKey = 'materialized_clear_reuse_overwrite'; +$prefix = sys_get_temp_dir() . '/opcache_volatile_cache_materialized_clear_reuse_' . getmypid(); +$readyFile = $prefix . '.ready'; +$doneFile = $prefix . '.done'; +$resultFile = $prefix . '.result'; +@unlink($readyFile); +@unlink($doneFile); +@unlink($resultFile); + +OPcache\volatile_clear(); +if (!OPcache\volatile_store($key, new ClearReusePayload(build_rows('T', 3)))) { + throw new RuntimeException('store failed'); +} + +$pid = pcntl_fork(); +if ($pid === -1) { + throw new RuntimeException('pcntl_fork() failed'); +} + +if ($pid === 0) { + $fetched = OPcache\volatile_fetch($key); + file_put_contents($readyFile, 'ready'); + wait_for_file($doneFile); + + $before = $fetched->rows[123]['text']; + $fetched->rows[123]['text'] = 'changed after clear and reuse'; + $after = $fetched->rows[123]['text']; + $nested = $fetched->rows[123]['nested']['value']; + + $refetch = OPcache\volatile_fetch($key, 'MISS') === 'MISS' ? 'MISS' : 'HIT'; + + file_put_contents($resultFile, $before . "\n" . $after . "\n" . $nested . "\n" . $refetch); + exit(0); +} + +wait_for_file($readyFile); +OPcache\volatile_clear(); +if (!OPcache\volatile_store($overwriteKey, new ClearReusePayload(build_rows('X', 7)))) { + throw new RuntimeException('overwrite store failed'); +} +file_put_contents($doneFile, 'done'); +pcntl_waitpid($pid, $status); + +echo file_get_contents($resultFile), "\n"; +@unlink($readyFile); +@unlink($doneFile); +@unlink($resultFile); + +?> +--EXPECT-- +TTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTT +changed after clear and reuse +369 +MISS +--CLEAN-- + diff --git a/ext/opcache/tests/static_cache_volatile_cache_materialization_fork_001.phpt b/ext/opcache/tests/static_cache_volatile_cache_materialization_fork_001.phpt new file mode 100644 index 000000000000..6ed87af4ee69 --- /dev/null +++ b/ext/opcache/tests/static_cache_volatile_cache_materialization_fork_001.phpt @@ -0,0 +1,101 @@ +--TEST-- +OPcache volatile cache should isolate post-mutation object fetches across processes +--EXTENSIONS-- +opcache +pcntl +--SKIPIF-- + +--INI-- +opcache.enable=1 +opcache.enable_cli=1 +opcache.static_cache.volatile_size_mb=32 +--FILE-- + $i, + 'text' => str_repeat(chr(65 + ($i % 26)), 96), + 'flags' => [$i, $i + 1, $i + 2, $i + 3], + ]; + } + + return $rows; +} + +$readyFile = sys_get_temp_dir() . '/opcache_volatile_cache_materialization_plain_' . getmypid() . '.ready'; +@unlink($readyFile); + +$pid = pcntl_fork(); +if ($pid === -1) { + throw new RuntimeException('pcntl_fork() failed'); +} + +if ($pid === 0) { + $deadline = microtime(true) + 5.0; + $expectedText = str_repeat(chr(65 + (100 % 26)), 96); + $canMeasureRequestAllocations = getenv('USE_ZEND_ALLOC') !== '0'; + + while (!file_exists($readyFile)) { + if (microtime(true) >= $deadline) { + fwrite(STDERR, "parent did not signal readiness\n"); + exit(1); + } + + usleep(1000); + } + + $before = memory_get_usage(); + $fetched = OPcache\volatile_fetch('materialization_plain_payload'); + $readOnly = $fetched->rows[100]['text']; + $afterFetch = memory_get_usage(); + $fetched->rows[100]['text'] = 'changed'; + $afterMutate = memory_get_usage(); + $secondFetch = OPcache\volatile_fetch('materialization_plain_payload'); + + var_dump($readOnly === $expectedText + && (!$canMeasureRequestAllocations || ($afterFetch - $before) < 262144)); + var_dump($fetched->rows[100]['text'] === 'changed' + && $secondFetch->rows[100]['text'] === $expectedText + && (!$canMeasureRequestAllocations || ($afterMutate - $afterFetch) > 131072)); + exit(0); +} + +if (!OPcache\volatile_store('materialization_plain_payload', new LargePayload(build_rows(), 'plain'))) { + throw new RuntimeException('Failed to store materialization_plain_payload'); +} + +file_put_contents($readyFile, 'ready'); +pcntl_waitpid($pid, $status); +@unlink($readyFile); + +$fetched = OPcache\volatile_fetch('materialization_plain_payload'); +var_dump($fetched->rows[100]['text'] === str_repeat(chr(65 + (100 % 26)), 96)); + +?> +--EXPECT-- +bool(true) +bool(true) +bool(true) +--CLEAN-- + diff --git a/ext/opcache/tests/static_cache_volatile_cache_materialization_fork_002.phpt b/ext/opcache/tests/static_cache_volatile_cache_materialization_fork_002.phpt new file mode 100644 index 000000000000..05d1c3df1e64 --- /dev/null +++ b/ext/opcache/tests/static_cache_volatile_cache_materialization_fork_002.phpt @@ -0,0 +1,115 @@ +--TEST-- +OPcache volatile cache should isolate nested post-mutation fetches even with __DirectCacheSafe properties +--EXTENSIONS-- +opcache +pcntl +--SKIPIF-- + +--INI-- +opcache.enable=1 +opcache.enable_cli=1 +opcache.static_cache.volatile_size_mb=32 +--FILE-- + $i, + 'text' => str_repeat(chr(65 + ($i % 26)), 96), + 'flags' => [$i, $i + 1, $i + 2, $i + 3], + ]; + } + + return $rows; +} + +$readyFile = sys_get_temp_dir() . '/opcache_volatile_cache_materialization_nested_' . getmypid() . '.ready'; +@unlink($readyFile); + +$pid = pcntl_fork(); +if ($pid === -1) { + throw new RuntimeException('pcntl_fork() failed'); +} + +if ($pid === 0) { + $deadline = microtime(true) + 5.0; + $expectedText = str_repeat(chr(65 + (100 % 26)), 96); + $expectedPrefix = '2026-06-15T09:30:00+00:00|'; + $canMeasureRequestAllocations = getenv('USE_ZEND_ALLOC') !== '0'; + + while (!file_exists($readyFile)) { + if (microtime(true) >= $deadline) { + fwrite(STDERR, "parent did not signal readiness\n"); + exit(1); + } + + usleep(1000); + } + + $before = memory_get_usage(); + $fetched = OPcache\volatile_fetch('materialization_nested_payload'); + $readOnly = $fetched->timestamp->format(DateTimeInterface::ATOM) . '|' . $fetched->payload->rows[100]['text']; + $afterFetch = memory_get_usage(); + $fetched->payload->rows[100]['text'] = 'changed'; + $afterMutate = memory_get_usage(); + $secondFetch = OPcache\volatile_fetch('materialization_nested_payload'); + + var_dump($readOnly === $expectedPrefix . $expectedText + && (!$canMeasureRequestAllocations || ($afterFetch - $before) < 262144)); + var_dump($fetched->payload->rows[100]['text'] === 'changed' + && $secondFetch->payload->rows[100]['text'] === $expectedText + && (!$canMeasureRequestAllocations || ($afterMutate - $afterFetch) > 131072)); + exit(0); +} + +$payload = new WrappedPayload( + new LargePayload(build_rows(), 'nested'), + new DateTimeImmutable('2026-06-15 09:30:00', new DateTimeZone('UTC')), +); + +if (!OPcache\volatile_store('materialization_nested_payload', $payload)) { + throw new RuntimeException('Failed to store materialization_nested_payload'); +} + +file_put_contents($readyFile, 'ready'); +pcntl_waitpid($pid, $status); +@unlink($readyFile); + +$fetched = OPcache\volatile_fetch('materialization_nested_payload'); +var_dump($fetched->payload->rows[100]['text'] === str_repeat(chr(65 + (100 % 26)), 96)); + +?> +--EXPECT-- +bool(true) +bool(true) +bool(true) +--CLEAN-- + diff --git a/ext/opcache/tests/static_cache_volatile_cache_materialization_overwrite_reuse_001.phpt b/ext/opcache/tests/static_cache_volatile_cache_materialization_overwrite_reuse_001.phpt new file mode 100644 index 000000000000..4e221ab6384b --- /dev/null +++ b/ext/opcache/tests/static_cache_volatile_cache_materialization_overwrite_reuse_001.phpt @@ -0,0 +1,74 @@ +--TEST-- +OPcache volatile cache fetched shared graph survives same-request overwrite and reuse +--EXTENSIONS-- +opcache +--INI-- +opcache.enable=1 +opcache.enable_cli=1 +opcache.static_cache.volatile_size_mb=32 +--FILE-- + $i, + 'text' => str_repeat($prefix, 64), + 'nested' => ['value' => $i * $multiplier], + ]; + } + + return $rows; +} + +function build_payload(string $prefix, int $multiplier): array +{ + return [ + 'name' => 'payload-' . $prefix, + 'rows' => build_rows($prefix, $multiplier), + ]; +} + +$key = 'materialized_overwrite_payload'; +$reuseKey = 'materialized_overwrite_reuse_payload'; + +OPcache\volatile_clear(); +if (!OPcache\volatile_store($key, build_payload('A', 3))) { + throw new RuntimeException('initial store failed'); +} + +$fetched = OPcache\volatile_fetch($key); +$before = $fetched['rows'][123]['text']; + +if (!OPcache\volatile_store($key, build_payload('B', 7))) { + throw new RuntimeException('overwrite store failed'); +} +if (!OPcache\volatile_store($reuseKey, build_payload('C', 11))) { + throw new RuntimeException('reuse store failed'); +} + +$after = $fetched['rows'][123]['text']; +$nested = $fetched['rows'][123]['nested']['value']; +$refetched = OPcache\volatile_fetch($key); +$reused = OPcache\volatile_fetch($reuseKey); + +echo $before, "\n"; +echo $after, "\n"; +echo $nested, "\n"; +echo $refetched['rows'][123]['text'], "\n"; +echo $refetched['rows'][123]['nested']['value'], "\n"; +echo $reused['rows'][123]['text'], "\n"; +echo $reused['rows'][123]['nested']['value'], "\n"; + +?> +--EXPECT-- +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +369 +BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB +861 +CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC +1353 \ No newline at end of file diff --git a/ext/opcache/tests/static_cache_volatile_cache_no_atomic_001.phpt b/ext/opcache/tests/static_cache_volatile_cache_no_atomic_001.phpt new file mode 100644 index 000000000000..686cdce68681 --- /dev/null +++ b/ext/opcache/tests/static_cache_volatile_cache_no_atomic_001.phpt @@ -0,0 +1,22 @@ +--TEST-- +OPcache volatile cache does not expose atomic public API +--EXTENSIONS-- +opcache +--INI-- +opcache.enable=1 +opcache.enable_cli=1 +opcache.static_cache.volatile_size_mb=32 +--FILE-- + +--EXPECT-- +bool(false) +bool(false) +bool(true) +bool(true) diff --git a/ext/opcache/tests/static_cache_volatile_cache_overflow_001.phpt b/ext/opcache/tests/static_cache_volatile_cache_overflow_001.phpt new file mode 100644 index 000000000000..59dc36415a4a --- /dev/null +++ b/ext/opcache/tests/static_cache_volatile_cache_overflow_001.phpt @@ -0,0 +1,40 @@ +--TEST-- +OPcache volatile cache expunges entries under memory pressure without wiping oversized writes +--EXTENSIONS-- +opcache +--INI-- +opcache.enable=1 +opcache.enable_cli=1 +opcache.static_cache.volatile_size_mb=8 +--FILE-- + +--EXPECT-- +bool(true) +bool(true) +bool(true) +bool(true) +bool(true) +bool(true) +int(3000000) +bool(true) +bool(false) +string(8) "survives" diff --git a/ext/opcache/tests/static_cache_volatile_cache_process_lock_mode_001.phpt b/ext/opcache/tests/static_cache_volatile_cache_process_lock_mode_001.phpt new file mode 100644 index 000000000000..a6f349e0378e --- /dev/null +++ b/ext/opcache/tests/static_cache_volatile_cache_process_lock_mode_001.phpt @@ -0,0 +1,244 @@ +--TEST-- +OPcache volatile cache process lock blocks writers behind active readers +--EXTENSIONS-- +opcache +--SKIPIF-- + +--FILE-- + MAX_COMMAND_OUTPUT_BYTES) { + return substr($contents, 0, MAX_COMMAND_OUTPUT_BYTES) . "\n...[output truncated]\n"; + } + + return $contents; +} + +function runCommand(array $command): array +{ + $stdoutFile = tempnam(sys_get_temp_dir(), 'opcache-process-lock-out-'); + $stderrFile = tempnam(sys_get_temp_dir(), 'opcache-process-lock-err-'); + if ($stdoutFile === false || $stderrFile === false) { + throw new RuntimeException('failed to create temporary files'); + } + + $descriptorSpec = [ + 0 => ['pipe', 'r'], + 1 => ['file', $stdoutFile, 'w'], + 2 => ['file', $stderrFile, 'w'], + ]; + + $process = proc_open($command, $descriptorSpec, $pipes); + if (!is_resource($process)) { + @unlink($stdoutFile); + @unlink($stderrFile); + throw new RuntimeException('proc_open failed'); + } + + fclose($pipes[0]); + $status = proc_close($process); + + $stdout = readCommandOutput($stdoutFile); + $stderr = readCommandOutput($stderrFile); + @unlink($stdoutFile); + @unlink($stderrFile); + + return [$status, $stdout, $stderr]; +} + +function parseBuildFlags(string $flags): array +{ + $tokens = preg_split('/\s+/', trim($flags)); + if ($tokens === false) { + return []; + } + + return array_values(array_filter($tokens, static function (string $token): bool { + return $token !== '' && !str_contains($token, '$('); + })); +} + +function resolveBuildFlags(string $buildRoot, array $variables): array +{ + $makefile = @file_get_contents($buildRoot . '/Makefile'); + $flags = []; + + if ($makefile === false) { + return $flags; + } + + foreach ($variables as $variable) { + if (preg_match('/^' . preg_quote($variable, '/') . '\s*=\s*(.+)$/m', $makefile, $matches)) { + $flags = array_merge($flags, parseBuildFlags($matches[1])); + } + } + + return $flags; +} + +function resolveBuildCommand(string $buildRoot, string $variable, array $fallback): array +{ + $value = getenv($variable); + if (is_string($value) && $value !== '') { + $tokens = parseBuildFlags($value); + if ($tokens !== []) { + return $tokens; + } + } + + $tokens = resolveBuildFlags($buildRoot, [$variable]); + return $tokens !== [] ? $tokens : $fallback; +} + +function resolveBuildRoot(string $root): string +{ + $buildRoot = null; + $php = getenv('TEST_PHP_EXECUTABLE') ?: PHP_BINARY; + if ($php) { + $php = realpath($php); + if ($php !== false) { + $candidate = dirname($php, 3); + if (file_exists($candidate . '/.libs/libphp.a') || file_exists($candidate . '/libs/libphp.a')) { + $buildRoot = $candidate; + } + } + } + + if ($buildRoot === null && (file_exists($root . '/.libs/libphp.a') || file_exists($root . '/libs/libphp.a'))) { + $buildRoot = $root; + } + + if ($buildRoot === null) { + throw new RuntimeException('Failed to resolve build root'); + } + + return $buildRoot; +} + +function resolveExtraLibs(string $buildRoot): array +{ + $makefile = @file_get_contents($buildRoot . '/Makefile'); + if ($makefile !== false && preg_match('/^EXTRA_LIBS\s*=\s*(.+)$/m', $makefile, $matches)) { + $libs = preg_split('/\s+/', trim($matches[1])); + if ($libs !== false && $libs !== ['']) { + return $libs; + } + } + + return [ + '-lm', + '-lpthread', + '-ldl', + '-lresolv', + '-lutil', + ]; +} + +$root = realpath(__DIR__ . '/../../..'); +if ($root === false) { + throw new RuntimeException('Failed to resolve source root'); +} + +$buildRoot = resolveBuildRoot($root); +$source = __DIR__ . '/helpers/volatile_cache_process_lock_mode_001.c'; +$binary = __DIR__ . '/helpers/volatile_cache_process_lock_mode_001.out'; +$compiler = resolveBuildCommand($buildRoot, 'CC', ['gcc']); +$buildFlags = resolveBuildFlags($buildRoot, ['CPPFLAGS', 'CFLAGS_CLEAN']); +$extraLibs = resolveExtraLibs($buildRoot); + +$compileCommand = [ + ...$compiler, + ...$buildFlags, + '-DZEND_ENABLE_STATIC_TSRMLS_CACHE=1', + '-DHAVE_CONFIG_H', + '-I' . $buildRoot, + '-I' . $buildRoot . '/main', + '-I' . $buildRoot . '/Zend', + '-I' . $buildRoot . '/TSRM', + '-I' . $buildRoot . '/sapi/embed', + '-I' . $buildRoot . '/ext/opcache', + '-I' . $buildRoot . '/ext/date/lib', + '-I' . $root, + '-I' . $root . '/main', + '-I' . $root . '/Zend', + '-I' . $root . '/TSRM', + '-I' . $root . '/sapi/embed', + '-I' . $root . '/ext/opcache', + '-o', + $binary, + $source, + $root . '/sapi/embed/php_embed.c', + file_exists($buildRoot . '/.libs/libphp.a') ? $buildRoot . '/.libs/libphp.a' : $buildRoot . '/libs/libphp.a', +]; +$compileCommand = array_merge($compileCommand, $extraLibs); + +[$status, $stdout, $stderr] = runCommand($compileCommand); +if ($status !== 0) { + echo $stdout, $stderr; + exit(1); +} + +@chmod($binary, 0755); +[$status, $stdout, $stderr] = runCommand([$binary]); +echo $stdout; +if ($status !== 0) { + echo $stderr; + exit(1); +} + +?> +--CLEAN-- + +--EXPECT-- +ok diff --git a/ext/opcache/tests/static_cache_volatile_cache_retired_shared_graph_free_001.phpt b/ext/opcache/tests/static_cache_volatile_cache_retired_shared_graph_free_001.phpt new file mode 100644 index 000000000000..3c54e5e19527 --- /dev/null +++ b/ext/opcache/tests/static_cache_volatile_cache_retired_shared_graph_free_001.phpt @@ -0,0 +1,240 @@ +--TEST-- +OPcache volatile cache frees retired shared graph payloads after releasing the read lock +--EXTENSIONS-- +opcache +--SKIPIF-- + +--FILE-- + MAX_COMMAND_OUTPUT_BYTES) { + return substr($contents, 0, MAX_COMMAND_OUTPUT_BYTES) . "\n...[output truncated]\n"; + } + + return $contents; +} + +function runCommand(array $command): array +{ + $stdoutFile = tempnam(sys_get_temp_dir(), 'opcache-retired-graph-out-'); + $stderrFile = tempnam(sys_get_temp_dir(), 'opcache-retired-graph-err-'); + if ($stdoutFile === false || $stderrFile === false) { + throw new RuntimeException('failed to create temporary files'); + } + + $descriptorSpec = [ + 0 => ['pipe', 'r'], + 1 => ['file', $stdoutFile, 'w'], + 2 => ['file', $stderrFile, 'w'], + ]; + + $process = proc_open($command, $descriptorSpec, $pipes); + if (!is_resource($process)) { + @unlink($stdoutFile); + @unlink($stderrFile); + throw new RuntimeException('proc_open failed'); + } + + fclose($pipes[0]); + $status = proc_close($process); + + $stdout = readCommandOutput($stdoutFile); + $stderr = readCommandOutput($stderrFile); + @unlink($stdoutFile); + @unlink($stderrFile); + + return [$status, $stdout, $stderr]; +} + +function parseBuildFlags(string $flags): array +{ + $tokens = preg_split('/\s+/', trim($flags)); + if ($tokens === false) { + return []; + } + + return array_values(array_filter($tokens, static function (string $token): bool { + return $token !== '' && !str_contains($token, '$('); + })); +} + +function resolveBuildFlags(string $buildRoot, array $variables): array +{ + $makefile = @file_get_contents($buildRoot . '/Makefile'); + $flags = []; + + if ($makefile === false) { + return $flags; + } + + foreach ($variables as $variable) { + if (preg_match('/^' . preg_quote($variable, '/') . '\s*=\s*(.+)$/m', $makefile, $matches)) { + $flags = array_merge($flags, parseBuildFlags($matches[1])); + } + } + + return $flags; +} + +function resolveBuildCommand(string $buildRoot, string $variable, array $fallback): array +{ + $value = getenv($variable); + if (is_string($value) && $value !== '') { + $tokens = parseBuildFlags($value); + if ($tokens !== []) { + return $tokens; + } + } + + $tokens = resolveBuildFlags($buildRoot, [$variable]); + return $tokens !== [] ? $tokens : $fallback; +} + +function resolveBuildRoot(string $root): string +{ + $buildRoot = null; + $php = getenv('TEST_PHP_EXECUTABLE') ?: PHP_BINARY; + if ($php) { + $php = realpath($php); + if ($php !== false) { + $candidate = dirname($php, 3); + if (file_exists($candidate . '/.libs/libphp.a') || file_exists($candidate . '/libs/libphp.a')) { + $buildRoot = $candidate; + } + } + } + + if ($buildRoot === null && (file_exists($root . '/.libs/libphp.a') || file_exists($root . '/libs/libphp.a'))) { + $buildRoot = $root; + } + + if ($buildRoot === null) { + throw new RuntimeException('Failed to resolve build root'); + } + + return $buildRoot; +} + +function resolveExtraLibs(string $buildRoot): array +{ + $makefile = @file_get_contents($buildRoot . '/Makefile'); + if ($makefile !== false && preg_match('/^EXTRA_LIBS\s*=\s*(.+)$/m', $makefile, $matches)) { + $libs = preg_split('/\s+/', trim($matches[1])); + if ($libs !== false && $libs !== ['']) { + return $libs; + } + } + + return [ + '-lm', + '-lpthread', + '-ldl', + '-lresolv', + '-lutil', + ]; +} + +$root = realpath(__DIR__ . '/../../..'); +if ($root === false) { + throw new RuntimeException('Failed to resolve source root'); +} + +$buildRoot = resolveBuildRoot($root); +$source = __DIR__ . '/helpers/volatile_cache_retired_shared_graph_free_001.c'; +$binary = __DIR__ . '/helpers/volatile_cache_retired_shared_graph_free_001.out'; +$compiler = resolveBuildCommand($buildRoot, 'CC', ['gcc']); +$buildFlags = resolveBuildFlags($buildRoot, ['CPPFLAGS', 'CFLAGS_CLEAN']); +$extraLibs = resolveExtraLibs($buildRoot); + +$compileCommand = [ + ...$compiler, + ...$buildFlags, + '-DZEND_ENABLE_STATIC_TSRMLS_CACHE=1', + '-DHAVE_CONFIG_H', + '-I' . $buildRoot, + '-I' . $buildRoot . '/main', + '-I' . $buildRoot . '/Zend', + '-I' . $buildRoot . '/TSRM', + '-I' . $buildRoot . '/sapi/embed', + '-I' . $buildRoot . '/ext/opcache', + '-I' . $buildRoot . '/ext/date/lib', + '-I' . $root, + '-I' . $root . '/main', + '-I' . $root . '/Zend', + '-I' . $root . '/TSRM', + '-I' . $root . '/sapi/embed', + '-I' . $root . '/ext/opcache', + '-o', + $binary, + $source, + $root . '/sapi/embed/php_embed.c', + file_exists($buildRoot . '/.libs/libphp.a') ? $buildRoot . '/.libs/libphp.a' : $buildRoot . '/libs/libphp.a', +]; +$compileCommand = array_merge($compileCommand, $extraLibs); + +[$status, $stdout, $stderr] = runCommand($compileCommand); +if ($status !== 0) { + echo $stdout, $stderr; + exit(1); +} + +@chmod($binary, 0755); +[$status, $stdout, $stderr] = runCommand([$binary]); +echo $stdout; +if ($status !== 0) { + echo $stderr; + exit(1); +} + +?> +--CLEAN-- + +--EXPECT-- +ok diff --git a/ext/opcache/tests/static_cache_volatile_cache_shared_graph_dynamic_array_001.phpt b/ext/opcache/tests/static_cache_volatile_cache_shared_graph_dynamic_array_001.phpt new file mode 100644 index 000000000000..c0ab084f246a --- /dev/null +++ b/ext/opcache/tests/static_cache_volatile_cache_shared_graph_dynamic_array_001.phpt @@ -0,0 +1,62 @@ +--TEST-- +OPcache volatile cache shared graph supports packed arrays with object elements +--EXTENSIONS-- +opcache +--INI-- +opcache.enable=1 +opcache.enable_cli=1 +opcache.static_cache.volatile_size_mb=32 +--FILE-- + $index, 'bucket' => $index % 4]); + } + + return $items; +} + +$items = build_packed_graph_items(); +$payload = new PackedGraphPayload($items); + +var_dump(OPcache\volatile_store('packed_graph_payload', $payload)); +var_dump(OPcache\volatile_store('packed_graph_root', $items)); + +$payloadCopy = OPcache\volatile_fetch('packed_graph_payload'); +$rootCopy = OPcache\volatile_fetch('packed_graph_root'); + +var_dump($payloadCopy instanceof PackedGraphPayload); +var_dump($payloadCopy->items[7] instanceof PackedGraphItem); +var_dump($payloadCopy->items[7]->name); +var_dump($payloadCopy->items[7]->metadata['bucket']); +var_dump($rootCopy[12] instanceof PackedGraphItem); +var_dump($rootCopy[12]->name); +var_dump($rootCopy[12]->metadata['index']); + +?> +--EXPECT-- +bool(true) +bool(true) +bool(true) +bool(true) +string(6) "item-7" +int(3) +bool(true) +string(7) "item-12" +int(12) diff --git a/ext/opcache/tests/static_cache_volatile_cache_storable_values_001.phpt b/ext/opcache/tests/static_cache_volatile_cache_storable_values_001.phpt new file mode 100644 index 000000000000..171873cfd6f8 --- /dev/null +++ b/ext/opcache/tests/static_cache_volatile_cache_storable_values_001.phpt @@ -0,0 +1,155 @@ +--TEST-- +OPcache static cache rejects resource and Closure values +--EXTENSIONS-- +opcache +spl +--INI-- +opcache.enable=1 +opcache.enable_cli=1 +opcache.static_cache.volatile_size_mb=32 +opcache.static_cache.persistent_size_mb=32 +--FILE-- + true; +$resource_object = new StaticCacheUnsupportedValueBox($resource); +$closure_object = new StaticCacheUnsupportedValueBox($closure); +$resource_fixed_array = new SplFixedArray(1); +$resource_fixed_array[0] = $resource; +$closure_fixed_array = new SplFixedArray(1); +$closure_fixed_array[0] = $closure; +$resource_array_object = new ArrayObject(['value' => $resource]); +$closure_array_object = new ArrayObject(['value' => $closure]); + +class StaticCacheUnsupportedSerializedPayload +{ + public static mixed $value = null; + + public function __serialize(): array + { + return ['value' => self::$value]; + } +} + +$serialized_payload = new StaticCacheUnsupportedSerializedPayload(); + +function dump_type_error(Closure $callback): void +{ + try { + $callback(); + } catch (TypeError $e) { + echo $e->getMessage(), "\n"; + } +} + +function dump_static_cache_exception(Closure $callback): void +{ + try { + $callback(); + } catch (StaticCacheException $e) { + echo get_class($e), ': ', $e->getMessage(), "\n"; + } +} + +dump_type_error(static fn () => OPcache\volatile_store('resource', $resource)); +var_dump(OPcache\volatile_fetch('resource', 'missing')); +dump_type_error(static fn () => OPcache\volatile_store('closure', $closure)); +var_dump(OPcache\volatile_fetch('closure', 'missing')); + +var_dump(OPcache\volatile_store_array(['nested-resource' => ['value' => $resource]])); +var_dump(OPcache\volatile_fetch('nested-resource', 'missing')); +var_dump(OPcache\volatile_store_array(['nested-closure' => ['value' => $closure]])); +var_dump(OPcache\volatile_fetch('nested-closure', 'missing')); +var_dump(OPcache\volatile_store('object-resource', $resource_object)); +var_dump(OPcache\volatile_fetch('object-resource', 'missing')); +var_dump(OPcache\volatile_store('object-closure', $closure_object)); +var_dump(OPcache\volatile_fetch('object-closure', 'missing')); +var_dump(OPcache\volatile_store('spl-resource', $resource_fixed_array)); +var_dump(OPcache\volatile_fetch('spl-resource', 'missing')); +var_dump(OPcache\volatile_store('spl-closure', $closure_fixed_array)); +var_dump(OPcache\volatile_fetch('spl-closure', 'missing')); +var_dump(OPcache\volatile_store('array-object-resource', $resource_array_object)); +var_dump(OPcache\volatile_fetch('array-object-resource', 'missing')); +var_dump(OPcache\volatile_store('array-object-closure', $closure_array_object)); +var_dump(OPcache\volatile_fetch('array-object-closure', 'missing')); +StaticCacheUnsupportedSerializedPayload::$value = $resource; +var_dump(OPcache\volatile_store('serialized-resource', $serialized_payload)); +var_dump(OPcache\volatile_fetch('serialized-resource', 'missing')); +StaticCacheUnsupportedSerializedPayload::$value = $closure; +var_dump(OPcache\volatile_store('serialized-closure', $serialized_payload)); +var_dump(OPcache\volatile_fetch('serialized-closure', 'missing')); + +dump_type_error(static fn () => OPcache\volatile_fetch('missing', $resource)); +dump_type_error(static fn () => OPcache\volatile_fetch('missing', $closure)); + +dump_type_error(static fn () => OPcache\persistent_store('resource', $resource)); +dump_type_error(static fn () => OPcache\persistent_store('closure', $closure)); +dump_static_cache_exception(static fn () => OPcache\persistent_store_array(['nested-resource' => ['value' => $resource]])); +dump_static_cache_exception(static fn () => OPcache\persistent_store_array(['nested-closure' => ['value' => $closure]])); +dump_static_cache_exception(static fn () => OPcache\persistent_store('object-resource', $resource_object)); +dump_static_cache_exception(static fn () => OPcache\persistent_store('object-closure', $closure_object)); +dump_static_cache_exception(static fn () => OPcache\persistent_store('spl-resource', $resource_fixed_array)); +dump_static_cache_exception(static fn () => OPcache\persistent_store('spl-closure', $closure_fixed_array)); +dump_static_cache_exception(static fn () => OPcache\persistent_store('array-object-resource', $resource_array_object)); +dump_static_cache_exception(static fn () => OPcache\persistent_store('array-object-closure', $closure_array_object)); +StaticCacheUnsupportedSerializedPayload::$value = $resource; +dump_static_cache_exception(static fn () => OPcache\persistent_store('serialized-resource', $serialized_payload)); +StaticCacheUnsupportedSerializedPayload::$value = $closure; +dump_static_cache_exception(static fn () => OPcache\persistent_store('serialized-closure', $serialized_payload)); +dump_type_error(static fn () => OPcache\persistent_fetch('missing', $resource)); +dump_type_error(static fn () => OPcache\persistent_fetch('missing', $closure)); + +fclose($resource); + +?> +--EXPECT-- +OPcache\volatile_store(): Argument #2 ($value) must be of type object|array|string|int|float|bool|null, resource given +string(7) "missing" +OPcache\volatile_store(): Argument #2 ($value) must not be a Closure object +string(7) "missing" +bool(false) +string(7) "missing" +bool(false) +string(7) "missing" +bool(false) +string(7) "missing" +bool(false) +string(7) "missing" +bool(false) +string(7) "missing" +bool(false) +string(7) "missing" +bool(false) +string(7) "missing" +bool(false) +string(7) "missing" +bool(false) +string(7) "missing" +bool(false) +string(7) "missing" +OPcache\volatile_fetch(): Argument #2 ($default) must be of type object|array|string|int|float|bool|null, resource given +OPcache\volatile_fetch(): Argument #2 ($default) must not be a Closure object +OPcache\persistent_store(): Argument #2 ($value) must be of type object|array|string|int|float|bool|null, resource given +OPcache\persistent_store(): Argument #2 ($value) must not be a Closure object +OPcache\StaticCacheException: resources cannot be stored in the static cache +OPcache\StaticCacheException: Closure objects cannot be stored in the static cache +OPcache\StaticCacheException: resources cannot be stored in the static cache +OPcache\StaticCacheException: Closure objects cannot be stored in the static cache +OPcache\StaticCacheException: resources and Closure objects cannot be stored in the static cache +OPcache\StaticCacheException: resources and Closure objects cannot be stored in the static cache +OPcache\StaticCacheException: resources and Closure objects cannot be stored in the static cache +OPcache\StaticCacheException: resources and Closure objects cannot be stored in the static cache +OPcache\StaticCacheException: resources and Closure objects cannot be stored in the static cache +OPcache\StaticCacheException: resources and Closure objects cannot be stored in the static cache +OPcache\persistent_fetch(): Argument #2 ($default) must be of type object|array|string|int|float|bool|null, resource given +OPcache\persistent_fetch(): Argument #2 ($default) must not be a Closure object diff --git a/ext/opcache/tests/static_cache_volatile_cache_tracked_reference_assignment_001.phpt b/ext/opcache/tests/static_cache_volatile_cache_tracked_reference_assignment_001.phpt new file mode 100644 index 000000000000..89b6cadf5ccb --- /dev/null +++ b/ext/opcache/tests/static_cache_volatile_cache_tracked_reference_assignment_001.phpt @@ -0,0 +1,81 @@ +--TEST-- +OPcache VolatileStatic Immediate publishes scalar assignments through tracked references +--EXTENSIONS-- +opcache +--CONFLICTS-- +server +--FILE-- + +--CLEAN-- + +--EXPECT-- +reset +seed=one +read=one +mutate=two +read=two diff --git a/ext/opcache/tests/static_cache_volatile_cache_ttl_001.phpt b/ext/opcache/tests/static_cache_volatile_cache_ttl_001.phpt new file mode 100644 index 000000000000..861a4144d8bc --- /dev/null +++ b/ext/opcache/tests/static_cache_volatile_cache_ttl_001.phpt @@ -0,0 +1,28 @@ +--TEST-- +OPcache volatile cache TTL expiry and reuse +--EXTENSIONS-- +opcache +--INI-- +opcache.enable=1 +opcache.enable_cli=1 +opcache.static_cache.volatile_size_mb=32 +--FILE-- + +--EXPECT-- +bool(true) +string(3) "one" +string(7) "expired" +bool(true) +string(3) "two" diff --git a/ext/opcache/tests/static_cache_volatile_cache_ttl_invalid_001.phpt b/ext/opcache/tests/static_cache_volatile_cache_ttl_invalid_001.phpt new file mode 100644 index 000000000000..dfbfcdf9e258 --- /dev/null +++ b/ext/opcache/tests/static_cache_volatile_cache_ttl_invalid_001.phpt @@ -0,0 +1,35 @@ +--TEST-- +OPcache volatile cache store APIs reject negative TTL before storing entries +--EXTENSIONS-- +opcache +--INI-- +opcache.enable=1 +opcache.enable_cli=1 +opcache.static_cache.volatile_size_mb=32 +--FILE-- +getMessage(), "\n"; +} + +var_dump(OPcache\volatile_fetch('single', 'missing')); + +try { + OPcache\volatile_store_array(['array_first' => 'stored'], -1); +} catch (ValueError $exception) { + echo $exception->getMessage(), "\n"; +} + +var_dump(OPcache\volatile_fetch('array_first', 'missing')); + +?> +--EXPECT-- +OPcache\volatile_store(): Argument #3 ($ttl) must be greater than or equal to 0 +string(7) "missing" +OPcache\volatile_store_array(): Argument #2 ($ttl) must be greater than or equal to 0 +string(7) "missing" diff --git a/ext/opcache/tests/static_cache_volatile_cache_ttl_large_001.phpt b/ext/opcache/tests/static_cache_volatile_cache_ttl_large_001.phpt new file mode 100644 index 000000000000..042877c94a26 --- /dev/null +++ b/ext/opcache/tests/static_cache_volatile_cache_ttl_large_001.phpt @@ -0,0 +1,20 @@ +--TEST-- +OPcache volatile cache stores large TTL values without 32-bit expiry truncation +--EXTENSIONS-- +opcache +--INI-- +opcache.enable=1 +opcache.enable_cli=1 +opcache.static_cache.volatile_size_mb=32 +--FILE-- + +--EXPECT-- +bool(true) +string(5) "value" diff --git a/ext/opcache/tests/static_cache_volatile_cache_zts_request_shared_graph_refs_001.phpt b/ext/opcache/tests/static_cache_volatile_cache_zts_request_shared_graph_refs_001.phpt new file mode 100644 index 000000000000..85aa31eb1617 --- /dev/null +++ b/ext/opcache/tests/static_cache_volatile_cache_zts_request_shared_graph_refs_001.phpt @@ -0,0 +1,305 @@ +--TEST-- +OPcache volatile cache releases request-scoped shared graph refs across repeated ZTS requests +--EXTENSIONS-- +opcache +--SKIPIF-- + +--FILE-- + MAX_COMMAND_OUTPUT_BYTES) { + return substr($contents, 0, MAX_COMMAND_OUTPUT_BYTES) . "\n...[output truncated]\n"; + } + + return $contents; +} + +function runCommand(array $command): array +{ + $stdoutFile = tempnam(sys_get_temp_dir(), 'opcache-zts-out-'); + $stderrFile = tempnam(sys_get_temp_dir(), 'opcache-zts-err-'); + if ($stdoutFile === false || $stderrFile === false) { + throw new RuntimeException('failed to create temporary files'); + } + + $descriptorSpec = [ + 0 => ['pipe', 'r'], + 1 => ['file', $stdoutFile, 'w'], + 2 => ['file', $stderrFile, 'w'], + ]; + + $process = proc_open($command, $descriptorSpec, $pipes); + if (!is_resource($process)) { + @unlink($stdoutFile); + @unlink($stderrFile); + throw new RuntimeException('proc_open failed'); + } + + fclose($pipes[0]); + $status = proc_close($process); + + $stdout = readCommandOutput($stdoutFile); + $stderr = readCommandOutput($stderrFile); + @unlink($stdoutFile); + @unlink($stderrFile); + + return [$status, $stdout, $stderr]; +} + +function parseBuildFlags(string $flags): array +{ + $tokens = preg_split('/\s+/', trim($flags)); + if ($tokens === false) { + return []; + } + + return array_values(array_filter($tokens, static function (string $token): bool { + return $token !== '' && !str_contains($token, '$('); + })); +} + +function resolveBuildFlags(string $buildRoot, array $variables): array +{ + $makefile = @file_get_contents($buildRoot . '/Makefile'); + $flags = []; + + if ($makefile === false) { + return $flags; + } + + foreach ($variables as $variable) { + if (preg_match('/^' . preg_quote($variable, '/') . '\s*=\s*(.+)$/m', $makefile, $matches)) { + $flags = array_merge($flags, parseBuildFlags($matches[1])); + } + } + + return $flags; +} + +function resolveBuildCommand(string $buildRoot, string $variable, array $fallback): array +{ + $value = getenv($variable); + if (is_string($value) && $value !== '') { + $tokens = parseBuildFlags($value); + if ($tokens !== []) { + return $tokens; + } + } + + $tokens = resolveBuildFlags($buildRoot, [$variable]); + return $tokens !== [] ? $tokens : $fallback; +} + +function resolveBuildRoot(string $root): string +{ + $buildRoot = null; + $php = getenv('TEST_PHP_EXECUTABLE') ?: PHP_BINARY; + if ($php) { + $php = realpath($php); + if ($php !== false) { + if (PHP_OS_FAMILY === 'Windows') { + $candidate = dirname($php); + if (glob($candidate . '/php*embed.lib')) { + $buildRoot = $candidate; + } + } else { + $candidate = dirname($php, 3); + if (file_exists($candidate . '/.libs/libphp.a') || file_exists($candidate . '/libs/libphp.a')) { + $buildRoot = $candidate; + } + } + } + } + + if ($buildRoot === null && PHP_OS_FAMILY !== 'Windows' && (file_exists($root . '/.libs/libphp.a') || file_exists($root . '/libs/libphp.a'))) { + $buildRoot = $root; + } + + if ($buildRoot === null) { + throw new RuntimeException('Failed to resolve build root'); + } + + return $buildRoot; +} + +function resolveExtraLibs(string $buildRoot): array +{ + if (PHP_OS_FAMILY === 'Windows') { + return []; + } + + $makefile = @file_get_contents($buildRoot . '/Makefile'); + if ($makefile !== false && preg_match('/^EXTRA_LIBS\s*=\s*(.+)$/m', $makefile, $matches)) { + $libs = preg_split('/\s+/', trim($matches[1])); + if ($libs !== false && $libs !== ['']) { + return $libs; + } + } + + return [ + '-lm', + '-lpthread', + '-ldl', + '-lresolv', + '-lutil', + ]; +} + +function resolveWindowsEmbedLib(string $buildRoot): string +{ + $candidates = glob($buildRoot . '/php*embed.lib'); + if ($candidates !== false && $candidates !== []) { + return $candidates[0]; + } + + throw new RuntimeException('Failed to resolve Windows embed library'); +} + +$root = realpath(__DIR__ . '/../../..'); +if ($root === false) { + throw new RuntimeException('Failed to resolve source root'); +} + +$buildRoot = resolveBuildRoot($root); +$source = __DIR__ . '/helpers/volatile_cache_zts_request_shared_graph_refs_001.c'; +$binary = __DIR__ . '/helpers/volatile_cache_zts_request_shared_graph_refs_001' . (PHP_OS_FAMILY === 'Windows' ? '.exe' : '.out'); +$compiler = resolveBuildCommand($buildRoot, 'CC', PHP_OS_FAMILY === 'Windows' ? ['cl'] : ['gcc']); +$buildFlags = PHP_OS_FAMILY === 'Windows' ? [] : resolveBuildFlags($buildRoot, ['CPPFLAGS', 'CFLAGS_CLEAN']); +$extraLibs = resolveExtraLibs($buildRoot); + +if (PHP_OS_FAMILY === 'Windows') { + $compileCommand = [ + ...$compiler, + '/nologo', + '/EHsc', + '/I' . $buildRoot, + '/I' . $buildRoot . '/main', + '/I' . $buildRoot . '/Zend', + '/I' . $buildRoot . '/TSRM', + '/I' . $buildRoot . '/sapi/embed', + '/I' . $buildRoot . '/ext/opcache', + '/I' . $buildRoot . '/ext/date/lib', + '/I' . $buildRoot . '/ext/opcache', + '/I' . $root, + '/I' . $root . '/main', + '/I' . $root . '/Zend', + '/I' . $root . '/TSRM', + '/I' . $root . '/sapi/embed', + '/I' . $root . '/ext/date/lib', + '/I' . $root . '/ext/opcache', + '/Fe:' . $binary, + $source, + resolveWindowsEmbedLib($buildRoot), + ]; +} else { + $compileCommand = [ + ...$compiler, + ...$buildFlags, + '-DZEND_ENABLE_STATIC_TSRMLS_CACHE=1', + '-DHAVE_CONFIG_H', + '-I' . $buildRoot, + '-I' . $buildRoot . '/main', + '-I' . $buildRoot . '/Zend', + '-I' . $buildRoot . '/TSRM', + '-I' . $buildRoot . '/sapi/embed', + '-I' . $buildRoot . '/ext/opcache', + '-I' . $buildRoot . '/ext/date/lib', + '-I' . $root, + '-I' . $root . '/main', + '-I' . $root . '/Zend', + '-I' . $root . '/TSRM', + '-I' . $root . '/sapi/embed', + '-I' . $root . '/ext/date/lib', + '-I' . $root . '/ext/opcache', + '-o', + $binary, + $source, + file_exists($buildRoot . '/.libs/libphp.a') ? $buildRoot . '/.libs/libphp.a' : $buildRoot . '/libs/libphp.a', + ]; + $compileCommand = array_merge($compileCommand, $extraLibs); +} + +[$status, $stdout, $stderr] = runCommand($compileCommand); +if ($status !== 0) { + echo $stdout, $stderr; + exit(1); +} + +@chmod($binary, 0755); +[$status, $stdout, $stderr] = runCommand([$binary]); +echo $stdout; +if ($status !== 0) { + echo $stderr; + exit(1); +} + +?> +--CLEAN-- + +--EXPECT-- +ok diff --git a/ext/opcache/tests/static_cache_volatile_cache_zts_threads_001.phpt b/ext/opcache/tests/static_cache_volatile_cache_zts_threads_001.phpt new file mode 100644 index 000000000000..39faffc62df7 --- /dev/null +++ b/ext/opcache/tests/static_cache_volatile_cache_zts_threads_001.phpt @@ -0,0 +1,289 @@ +--TEST-- +OPcache volatile cache is shared across threads on ZTS builds +--EXTENSIONS-- +opcache +--SKIPIF-- + +--FILE-- + MAX_COMMAND_OUTPUT_BYTES) { + return substr($contents, 0, MAX_COMMAND_OUTPUT_BYTES) . "\n...[output truncated]\n"; + } + + return $contents; +} + +function runCommand(array $command): array +{ + $stdoutFile = tempnam(sys_get_temp_dir(), 'opcache-zts-out-'); + $stderrFile = tempnam(sys_get_temp_dir(), 'opcache-zts-err-'); + if ($stdoutFile === false || $stderrFile === false) { + throw new RuntimeException('failed to create temporary files'); + } + + $descriptorSpec = [ + 0 => ['pipe', 'r'], + 1 => ['file', $stdoutFile, 'w'], + 2 => ['file', $stderrFile, 'w'], + ]; + + $process = proc_open($command, $descriptorSpec, $pipes); + if (!is_resource($process)) { + @unlink($stdoutFile); + @unlink($stderrFile); + throw new RuntimeException('proc_open failed'); + } + + fclose($pipes[0]); + $status = proc_close($process); + + $stdout = readCommandOutput($stdoutFile); + $stderr = readCommandOutput($stderrFile); + @unlink($stdoutFile); + @unlink($stderrFile); + + return [$status, $stdout, $stderr]; +} + +function parseBuildFlags(string $flags): array +{ + $tokens = preg_split('/\s+/', trim($flags)); + if ($tokens === false) { + return []; + } + + return array_values(array_filter($tokens, static function (string $token): bool { + return $token !== '' && !str_contains($token, '$('); + })); +} + +function resolveBuildFlags(string $buildRoot, array $variables): array +{ + $makefile = @file_get_contents($buildRoot . '/Makefile'); + $flags = []; + + if ($makefile === false) { + return $flags; + } + + foreach ($variables as $variable) { + if (preg_match('/^' . preg_quote($variable, '/') . '\s*=\s*(.+)$/m', $makefile, $matches)) { + $flags = array_merge($flags, parseBuildFlags($matches[1])); + } + } + + return $flags; +} + +function resolveBuildCommand(string $buildRoot, string $variable, array $fallback): array +{ + $value = getenv($variable); + if (is_string($value) && $value !== '') { + $tokens = parseBuildFlags($value); + if ($tokens !== []) { + return $tokens; + } + } + + $tokens = resolveBuildFlags($buildRoot, [$variable]); + return $tokens !== [] ? $tokens : $fallback; +} + +function resolveExtraLibs(string $buildRoot): array +{ + if (PHP_OS_FAMILY === 'Windows') { + return []; + } + + $makefile = @file_get_contents($buildRoot . '/Makefile'); + if ($makefile !== false && preg_match('/^EXTRA_LIBS\s*=\s*(.+)$/m', $makefile, $matches)) { + $libs = preg_split('/\s+/', trim($matches[1])); + if ($libs !== false && $libs !== ['']) { + return $libs; + } + } + + return [ + '-lm', + '-lpthread', + '-ldl', + '-lresolv', + '-lutil', + ]; +} + +function resolveWindowsEmbedLib(string $buildRoot): string +{ + $candidates = glob($buildRoot . '/php*embed.lib'); + if ($candidates !== false && $candidates !== []) { + return $candidates[0]; + } + + throw new RuntimeException('Failed to resolve Windows embed library'); +} + +$root = realpath(__DIR__ . '/../../..'); +$buildRoot = null; +$php = getenv('TEST_PHP_EXECUTABLE') ?: PHP_BINARY; +if ($php) { + $php = realpath($php); + if ($php !== false) { + if (PHP_OS_FAMILY === 'Windows') { + $candidate = dirname($php); + if (glob($candidate . '/php*embed.lib')) { + $buildRoot = $candidate; + } + } else { + $candidate = dirname($php, 3); + if (file_exists($candidate . '/.libs/libphp.a') || file_exists($candidate . '/libs/libphp.a')) { + $buildRoot = $candidate; + } + } + } +} + +if ($buildRoot === null && $root !== false && PHP_OS_FAMILY !== 'Windows' && (file_exists($root . '/.libs/libphp.a') || file_exists($root . '/libs/libphp.a'))) { + $buildRoot = $root; +} + +if ($root === false || $buildRoot === null) { + throw new RuntimeException('Failed to resolve build root'); +} + +$source = __DIR__ . '/helpers/volatile_cache_zts_threads_001.c'; +$binary = __DIR__ . '/helpers/volatile_cache_zts_threads_001' . (PHP_OS_FAMILY === 'Windows' ? '.exe' : '.out'); +$compiler = resolveBuildCommand($buildRoot, 'CC', PHP_OS_FAMILY === 'Windows' ? ['cl'] : ['gcc']); +$buildFlags = PHP_OS_FAMILY === 'Windows' ? [] : resolveBuildFlags($buildRoot, ['CPPFLAGS', 'CFLAGS_CLEAN']); +$extraLibs = resolveExtraLibs($buildRoot); + +if (PHP_OS_FAMILY === 'Windows') { + $compileCommand = [ + ...$compiler, + '/nologo', + '/DZEND_ENABLE_STATIC_TSRMLS_CACHE=1', + '/DHAVE_CONFIG_H', + '/I' . $buildRoot, + '/I' . $buildRoot . '/main', + '/I' . $buildRoot . '/Zend', + '/I' . $buildRoot . '/TSRM', + '/I' . $buildRoot . '/sapi/embed', + '/I' . $root, + '/I' . $root . '/main', + '/I' . $root . '/Zend', + '/I' . $root . '/TSRM', + '/I' . $root . '/sapi/embed', + '/Fe:' . $binary, + $source, + '/link', + resolveWindowsEmbedLib($buildRoot), + ]; +} else { + $compileCommand = [ + ...$compiler, + ...$buildFlags, + '-DZEND_ENABLE_STATIC_TSRMLS_CACHE=1', + '-DHAVE_CONFIG_H', + '-I' . $buildRoot, + '-I' . $buildRoot . '/main', + '-I' . $buildRoot . '/Zend', + '-I' . $buildRoot . '/TSRM', + '-I' . $buildRoot . '/sapi/embed', + '-I' . $root, + '-I' . $root . '/main', + '-I' . $root . '/Zend', + '-I' . $root . '/TSRM', + '-I' . $root . '/sapi/embed', + '-o', + $binary, + $source, + $root . '/sapi/embed/php_embed.c', + file_exists($buildRoot . '/.libs/libphp.a') ? $buildRoot . '/.libs/libphp.a' : $buildRoot . '/libs/libphp.a', + ]; + $compileCommand = array_merge($compileCommand, $extraLibs); +} + +[$status, $stdout, $stderr] = runCommand($compileCommand); +if ($status !== 0) { + echo $stdout, $stderr; + exit(1); +} + +@chmod($binary, 0755); +[$status, $stdout, $stderr] = runCommand([$binary]); +echo $stdout; +if ($status !== 0) { + echo $stderr; + exit(1); +} + +?> +--CLEAN-- + +--EXPECT-- +ok diff --git a/ext/opcache/tests/static_cache_volatile_cache_zts_threads_002.phpt b/ext/opcache/tests/static_cache_volatile_cache_zts_threads_002.phpt new file mode 100644 index 000000000000..1c5f3de00261 --- /dev/null +++ b/ext/opcache/tests/static_cache_volatile_cache_zts_threads_002.phpt @@ -0,0 +1,310 @@ +--TEST-- +OPcache volatile cache validates __DirectCacheSafe and copy-on-mutate behavior across ZTS threads +--EXTENSIONS-- +opcache +--SKIPIF-- + +--FILE-- + MAX_COMMAND_OUTPUT_BYTES) { + return substr($contents, 0, MAX_COMMAND_OUTPUT_BYTES) . "\n...[output truncated]\n"; + } + + return $contents; +} + +function runCommand(array $command): array +{ + $stdoutFile = tempnam(sys_get_temp_dir(), 'opcache-zts-out-'); + $stderrFile = tempnam(sys_get_temp_dir(), 'opcache-zts-err-'); + if ($stdoutFile === false || $stderrFile === false) { + throw new RuntimeException('failed to create temporary files'); + } + + $descriptorSpec = [ + 0 => ['pipe', 'r'], + 1 => ['file', $stdoutFile, 'w'], + 2 => ['file', $stderrFile, 'w'], + ]; + + $process = proc_open($command, $descriptorSpec, $pipes); + if (!is_resource($process)) { + @unlink($stdoutFile); + @unlink($stderrFile); + throw new RuntimeException('proc_open failed'); + } + + fclose($pipes[0]); + $status = proc_close($process); + + $stdout = readCommandOutput($stdoutFile); + $stderr = readCommandOutput($stderrFile); + @unlink($stdoutFile); + @unlink($stderrFile); + + return [$status, $stdout, $stderr]; +} + +function parseBuildFlags(string $flags): array +{ + $tokens = preg_split('/\s+/', trim($flags)); + if ($tokens === false) { + return []; + } + + return array_values(array_filter($tokens, static function (string $token): bool { + return $token !== '' && !str_contains($token, '$('); + })); +} + +function resolveBuildFlags(string $buildRoot, array $variables): array +{ + $makefile = @file_get_contents($buildRoot . '/Makefile'); + $flags = []; + + if ($makefile === false) { + return $flags; + } + + foreach ($variables as $variable) { + if (preg_match('/^' . preg_quote($variable, '/') . '\s*=\s*(.+)$/m', $makefile, $matches)) { + $flags = array_merge($flags, parseBuildFlags($matches[1])); + } + } + + return $flags; +} + +function resolveBuildCommand(string $buildRoot, string $variable, array $fallback): array +{ + $value = getenv($variable); + if (is_string($value) && $value !== '') { + $tokens = parseBuildFlags($value); + if ($tokens !== []) { + return $tokens; + } + } + + $tokens = resolveBuildFlags($buildRoot, [$variable]); + return $tokens !== [] ? $tokens : $fallback; +} + +function resolveBuildRoot(string $root): string +{ + $buildRoot = null; + $php = getenv('TEST_PHP_EXECUTABLE') ?: PHP_BINARY; + if ($php) { + $php = realpath($php); + if ($php !== false) { + if (PHP_OS_FAMILY === 'Windows') { + $candidate = dirname($php); + if (glob($candidate . '/php*embed.lib')) { + $buildRoot = $candidate; + } + } else { + $candidate = dirname($php, 3); + if (file_exists($candidate . '/.libs/libphp.a') || file_exists($candidate . '/libs/libphp.a')) { + $buildRoot = $candidate; + } + } + } + } + + if ($buildRoot === null && PHP_OS_FAMILY !== 'Windows' && (file_exists($root . '/.libs/libphp.a') || file_exists($root . '/libs/libphp.a'))) { + $buildRoot = $root; + } + + if ($buildRoot === null) { + throw new RuntimeException('Failed to resolve build root'); + } + + return $buildRoot; +} + +function resolveExtraLibs(string $buildRoot): array +{ + if (PHP_OS_FAMILY === 'Windows') { + return []; + } + + $makefile = @file_get_contents($buildRoot . '/Makefile'); + if ($makefile !== false && preg_match('/^EXTRA_LIBS\s*=\s*(.+)$/m', $makefile, $matches)) { + $libs = preg_split('/\s+/', trim($matches[1])); + if ($libs !== false && $libs !== ['']) { + return $libs; + } + } + + return [ + '-lm', + '-lpthread', + '-ldl', + '-lresolv', + '-lutil', + ]; +} + +function resolveWindowsEmbedLib(string $buildRoot): string +{ + $candidates = glob($buildRoot . '/php*embed.lib'); + if ($candidates !== false && $candidates !== []) { + return $candidates[0]; + } + + throw new RuntimeException('Failed to resolve Windows embed library'); +} + +function runScenario(string $binary, string $scenario, string $label): void +{ + [$status, $stdout, $stderr] = runCommand([$binary, $scenario]); + if ($status !== 0) { + echo $stdout, $stderr; + exit(1); + } + + echo $label, ': ', trim($stdout), "\n"; +} + +$root = realpath(__DIR__ . '/../../..'); +if ($root === false) { + throw new RuntimeException('Failed to resolve source root'); +} + +$buildRoot = resolveBuildRoot($root); +$source = __DIR__ . '/helpers/volatile_cache_zts_threads_002.c'; +$binary = __DIR__ . '/helpers/volatile_cache_zts_threads_002' . (PHP_OS_FAMILY === 'Windows' ? '.exe' : '.out'); +$compiler = resolveBuildCommand($buildRoot, 'CC', PHP_OS_FAMILY === 'Windows' ? ['cl'] : ['gcc']); +$buildFlags = PHP_OS_FAMILY === 'Windows' ? [] : resolveBuildFlags($buildRoot, ['CPPFLAGS', 'CFLAGS_CLEAN']); +$extraLibs = resolveExtraLibs($buildRoot); + +if (PHP_OS_FAMILY === 'Windows') { + $compileCommand = [ + ...$compiler, + '/nologo', + '/DZEND_ENABLE_STATIC_TSRMLS_CACHE=1', + '/DHAVE_CONFIG_H', + '/I' . $buildRoot, + '/I' . $buildRoot . '/main', + '/I' . $buildRoot . '/Zend', + '/I' . $buildRoot . '/TSRM', + '/I' . $buildRoot . '/sapi/embed', + '/I' . $root, + '/I' . $root . '/main', + '/I' . $root . '/Zend', + '/I' . $root . '/TSRM', + '/I' . $root . '/sapi/embed', + '/Fe:' . $binary, + $source, + '/link', + resolveWindowsEmbedLib($buildRoot), + ]; +} else { + $compileCommand = [ + ...$compiler, + ...$buildFlags, + '-DZEND_ENABLE_STATIC_TSRMLS_CACHE=1', + '-DHAVE_CONFIG_H', + '-I' . $buildRoot, + '-I' . $buildRoot . '/main', + '-I' . $buildRoot . '/Zend', + '-I' . $buildRoot . '/TSRM', + '-I' . $buildRoot . '/sapi/embed', + '-I' . $root, + '-I' . $root . '/main', + '-I' . $root . '/Zend', + '-I' . $root . '/TSRM', + '-I' . $root . '/sapi/embed', + '-o', + $binary, + $source, + $root . '/sapi/embed/php_embed.c', + file_exists($buildRoot . '/.libs/libphp.a') ? $buildRoot . '/.libs/libphp.a' : $buildRoot . '/libs/libphp.a', + ]; + $compileCommand = array_merge($compileCommand, $extraLibs); +} + +[$status, $stdout, $stderr] = runCommand($compileCommand); +if ($status !== 0) { + echo $stdout, $stderr; + exit(1); +} + +@chmod($binary, 0755); + +runScenario($binary, __DIR__ . '/helpers/volatile_cache_zts_threads_002_safe_direct.inc', 'safe_direct'); +runScenario($binary, __DIR__ . '/helpers/volatile_cache_zts_threads_002_materialization_plain.inc', 'materialization_plain'); +runScenario($binary, __DIR__ . '/helpers/volatile_cache_zts_threads_002_materialization_nested.inc', 'materialization_nested'); + +?> +--CLEAN-- + +--EXPECT-- +safe_direct: ok +materialization_plain: ok +materialization_nested: ok diff --git a/ext/opcache/tests/static_cache_volatile_cache_zts_threads_003.phpt b/ext/opcache/tests/static_cache_volatile_cache_zts_threads_003.phpt new file mode 100644 index 000000000000..74a7b18bc8eb --- /dev/null +++ b/ext/opcache/tests/static_cache_volatile_cache_zts_threads_003.phpt @@ -0,0 +1,316 @@ +--TEST-- +OPcache volatile cache request-local lookup cache sees cross-thread updates on next fetch +--EXTENSIONS-- +opcache +--SKIPIF-- + +--FILE-- + MAX_COMMAND_OUTPUT_BYTES) { + return substr($contents, 0, MAX_COMMAND_OUTPUT_BYTES) . "\n...[output truncated]\n"; + } + + return $contents; +} + +function runCommand(array $command): array +{ + $stdoutFile = tempnam(sys_get_temp_dir(), 'opcache-zts-out-'); + $stderrFile = tempnam(sys_get_temp_dir(), 'opcache-zts-err-'); + if ($stdoutFile === false || $stderrFile === false) { + throw new RuntimeException('failed to create temporary files'); + } + + $descriptorSpec = [ + 0 => ['pipe', 'r'], + 1 => ['file', $stdoutFile, 'w'], + 2 => ['file', $stderrFile, 'w'], + ]; + + $process = proc_open($command, $descriptorSpec, $pipes); + if (!is_resource($process)) { + @unlink($stdoutFile); + @unlink($stderrFile); + throw new RuntimeException('proc_open failed'); + } + + fclose($pipes[0]); + $status = proc_close($process); + + $stdout = readCommandOutput($stdoutFile); + $stderr = readCommandOutput($stderrFile); + @unlink($stdoutFile); + @unlink($stderrFile); + + return [$status, $stdout, $stderr]; +} + +function parseBuildFlags(string $flags): array +{ + $tokens = preg_split('/\s+/', trim($flags)); + if ($tokens === false) { + return []; + } + + return array_values(array_filter($tokens, static function (string $token): bool { + return $token !== '' && !str_contains($token, '$('); + })); +} + +function resolveBuildFlags(string $buildRoot, array $variables): array +{ + $makefile = @file_get_contents($buildRoot . '/Makefile'); + $flags = []; + + if ($makefile === false) { + return $flags; + } + + foreach ($variables as $variable) { + if (preg_match('/^' . preg_quote($variable, '/') . '\s*=\s*(.+)$/m', $makefile, $matches)) { + $flags = array_merge($flags, parseBuildFlags($matches[1])); + } + } + + return $flags; +} + +function resolveBuildCommand(string $buildRoot, string $variable, array $fallback): array +{ + $value = getenv($variable); + if (is_string($value) && $value !== '') { + $tokens = parseBuildFlags($value); + if ($tokens !== []) { + return $tokens; + } + } + + $tokens = resolveBuildFlags($buildRoot, [$variable]); + return $tokens !== [] ? $tokens : $fallback; +} + +function resolveBuildRoot(string $root): string +{ + $buildRoot = null; + $php = getenv('TEST_PHP_EXECUTABLE') ?: PHP_BINARY; + if ($php) { + $php = realpath($php); + if ($php !== false) { + if (PHP_OS_FAMILY === 'Windows') { + $candidate = dirname($php); + if (glob($candidate . '/php*embed.lib')) { + $buildRoot = $candidate; + } + } else { + $candidate = dirname($php, 3); + if (file_exists($candidate . '/.libs/libphp.a') || file_exists($candidate . '/libs/libphp.a')) { + $buildRoot = $candidate; + } + } + } + } + + if ($buildRoot === null && PHP_OS_FAMILY !== 'Windows' && (file_exists($root . '/.libs/libphp.a') || file_exists($root . '/libs/libphp.a'))) { + $buildRoot = $root; + } + + if ($buildRoot === null) { + throw new RuntimeException('Failed to resolve build root'); + } + + return $buildRoot; +} + +function resolveExtraLibs(string $buildRoot): array +{ + if (PHP_OS_FAMILY === 'Windows') { + return []; + } + + $makefile = @file_get_contents($buildRoot . '/Makefile'); + if ($makefile !== false && preg_match('/^EXTRA_LIBS\s*=\s*(.+)$/m', $makefile, $matches)) { + $libs = preg_split('/\s+/', trim($matches[1])); + if ($libs !== false && $libs !== ['']) { + return $libs; + } + } + + return [ + '-lm', + '-lpthread', + '-ldl', + '-lresolv', + '-lutil', + ]; +} + +function resolveWindowsEmbedLib(string $buildRoot): string +{ + $candidates = glob($buildRoot . '/php*embed.lib'); + if ($candidates !== false && $candidates !== []) { + return $candidates[0]; + } + + throw new RuntimeException('Failed to resolve Windows embed library'); +} + +function runScenario(string $binary, string $scenario, string $label): void +{ + [$status, $stdout, $stderr] = runCommand([$binary, $scenario]); + if ($status !== 0) { + echo $stdout, $stderr; + exit(1); + } + + echo $label, ': ', trim($stdout), "\n"; +} + +$root = realpath(__DIR__ . '/../../..'); +if ($root === false) { + throw new RuntimeException('Failed to resolve source root'); +} + +$buildRoot = resolveBuildRoot($root); +$source = __DIR__ . '/helpers/volatile_cache_zts_threads_003.c'; +$binary = __DIR__ . '/helpers/volatile_cache_zts_threads_003' . (PHP_OS_FAMILY === 'Windows' ? '.exe' : '.out'); +$compiler = resolveBuildCommand($buildRoot, 'CC', PHP_OS_FAMILY === 'Windows' ? ['cl'] : ['gcc']); +$buildFlags = PHP_OS_FAMILY === 'Windows' ? [] : resolveBuildFlags($buildRoot, ['CPPFLAGS', 'CFLAGS_CLEAN']); +$extraLibs = resolveExtraLibs($buildRoot); + +if (PHP_OS_FAMILY === 'Windows') { + $compileCommand = [ + ...$compiler, + '/nologo', + '/DZEND_ENABLE_STATIC_TSRMLS_CACHE=1', + '/DHAVE_CONFIG_H', + '/I' . $buildRoot, + '/I' . $buildRoot . '/main', + '/I' . $buildRoot . '/Zend', + '/I' . $buildRoot . '/TSRM', + '/I' . $buildRoot . '/sapi/embed', + '/I' . $root, + '/I' . $root . '/main', + '/I' . $root . '/Zend', + '/I' . $root . '/TSRM', + '/I' . $root . '/sapi/embed', + '/Fe:' . $binary, + $source, + '/link', + resolveWindowsEmbedLib($buildRoot), + ]; +} else { + $compileCommand = [ + ...$compiler, + ...$buildFlags, + '-DZEND_ENABLE_STATIC_TSRMLS_CACHE=1', + '-DHAVE_CONFIG_H', + '-I' . $buildRoot, + '-I' . $buildRoot . '/main', + '-I' . $buildRoot . '/Zend', + '-I' . $buildRoot . '/TSRM', + '-I' . $buildRoot . '/sapi/embed', + '-I' . $root, + '-I' . $root . '/main', + '-I' . $root . '/Zend', + '-I' . $root . '/TSRM', + '-I' . $root . '/sapi/embed', + '-o', + $binary, + $source, + $root . '/sapi/embed/php_embed.c', + file_exists($buildRoot . '/.libs/libphp.a') ? $buildRoot . '/.libs/libphp.a' : $buildRoot . '/libs/libphp.a', + ]; + $compileCommand = array_merge($compileCommand, $extraLibs); +} + +[$status, $stdout, $stderr] = runCommand($compileCommand); +if ($status !== 0) { + echo $stdout, $stderr; + exit(1); +} + +@chmod($binary, 0755); + +runScenario($binary, __DIR__ . '/helpers/volatile_cache_zts_threads_003_hit.inc', 'lookup_hit'); +runScenario($binary, __DIR__ . '/helpers/volatile_cache_zts_threads_003_miss.inc', 'lookup_miss'); +runScenario($binary, __DIR__ . '/helpers/volatile_cache_zts_threads_003_delete.inc', 'lookup_delete'); +runScenario($binary, __DIR__ . '/helpers/volatile_cache_zts_threads_003_clear.inc', 'lookup_clear'); +runScenario($binary, __DIR__ . '/helpers/volatile_cache_zts_threads_003_materialized_clear.inc', 'materialized_clear'); +runScenario($binary, __DIR__ . '/helpers/volatile_cache_zts_threads_003_volatile_static_class.inc', 'volatile_static_class'); + +?> +--CLEAN-- + +--EXPECT-- +lookup_hit: ok +lookup_miss: ok +lookup_delete: ok +lookup_clear: ok +materialized_clear: ok +volatile_static_class: ok diff --git a/ext/opcache/tests/static_cache_volatile_static_clear_after_access_001.phpt b/ext/opcache/tests/static_cache_volatile_static_clear_after_access_001.phpt new file mode 100644 index 000000000000..5c6c08eb5b6d --- /dev/null +++ b/ext/opcache/tests/static_cache_volatile_static_clear_after_access_001.phpt @@ -0,0 +1,61 @@ +--TEST-- +OPcache VolatileStatic is not republished after volatile_clear() in the same request +--EXTENSIONS-- +opcache +--CONFLICTS-- +server +--FILE-- + +--CLEAN-- + +--EXPECT-- +1,1 +2,2 +3,3 +clear +1,1 diff --git a/ext/opcache/tests/static_cache_volatile_static_clear_isolated_from_persistent_static_001.phpt b/ext/opcache/tests/static_cache_volatile_static_clear_isolated_from_persistent_static_001.phpt new file mode 100644 index 000000000000..4aa933f35298 --- /dev/null +++ b/ext/opcache/tests/static_cache_volatile_static_clear_isolated_from_persistent_static_001.phpt @@ -0,0 +1,71 @@ +--TEST-- +OPcache VolatileStatic persists through volatile cache and can be cleared independently of PersistentStatic +--EXTENSIONS-- +opcache +--CONFLICTS-- +server +--FILE-- + +--CLEAN-- + +--EXPECT-- +1,1,1,1 +2,2,2,2 +clear +1,1,3,3 diff --git a/ext/opcache/tests/static_cache_volatile_static_strategy_001.phpt b/ext/opcache/tests/static_cache_volatile_static_strategy_001.phpt new file mode 100644 index 000000000000..2c18d31cf0b2 --- /dev/null +++ b/ext/opcache/tests/static_cache_volatile_static_strategy_001.phpt @@ -0,0 +1,327 @@ +--TEST-- +OPcache VolatileStatic strategies control publication timing without changing volatile-cache backend +--EXTENSIONS-- +opcache +--CONFLICTS-- +server +--FILE-- +ttl, ':', $defaultAttribute->strategy->value, ',', $trackingAttribute->ttl, ':', $trackingAttribute->strategy->value, ',', $trackingAttribute->strategy->name, ',', $ttlAttribute->ttl, ':', $ttlAttribute->strategy->value, "\n"; +echo 'cases=', implode(',', array_map(static fn (OPcache\CacheStrategy $case): string => $case->name . ':' . $case->value, OPcache\CacheStrategy::cases())), "\n"; + +try { + new OPcache\VolatileStatic(ttl: -1); +} catch (ValueError) { + echo "ttl-value-error\n"; +} + +file_put_contents(__DIR__ . '/volatile_static_strategy_001.php', <<<'PHP' +events[] = 'x'; +} + +#[OPcache\VolatileStatic] +class CachedStrategyClassDefault +{ + public static ?CachedStrategyPayload $property = null; + + public static function property(): CachedStrategyPayload + { + self::$property ??= cached_strategy_payload('class_default_property'); + return self::$property; + } + + public static function method(): CachedStrategyPayload + { + static $value = null; + + $value ??= cached_strategy_payload('class_default_method'); + return $value; + } +} + +#[OPcache\VolatileStatic(strategy: OPcache\CacheStrategy::Immediate)] +class CachedStrategyClassImmediate +{ + public static ?CachedStrategyPayload $property = null; + + public static function property(): CachedStrategyPayload + { + self::$property ??= cached_strategy_payload('class_immediate_property'); + return self::$property; + } + + public static function method(): CachedStrategyPayload + { + static $value = null; + + $value ??= cached_strategy_payload('class_immediate_method'); + return $value; + } +} + +#[OPcache\VolatileStatic(strategy: OPcache\CacheStrategy::Tracking)] +class CachedStrategyClassTracking +{ + public static ?CachedStrategyPayload $property = null; + + public static function property(): CachedStrategyPayload + { + self::$property ??= cached_strategy_payload('class_tracking_property'); + return self::$property; + } + + public static function method(): CachedStrategyPayload + { + static $value = null; + + $value ??= cached_strategy_payload('class_tracking_method'); + return $value; + } +} + +class CachedStrategyPropertyDefault +{ + #[OPcache\VolatileStatic] + public static ?CachedStrategyPayload $value = null; + + public static function value(): CachedStrategyPayload + { + self::$value ??= cached_strategy_payload('property_default'); + return self::$value; + } +} + +class CachedStrategyPropertyImmediate +{ + #[OPcache\VolatileStatic(strategy: OPcache\CacheStrategy::Immediate)] + public static ?CachedStrategyPayload $value = null; + + public static function value(): CachedStrategyPayload + { + self::$value ??= cached_strategy_payload('property_immediate'); + return self::$value; + } +} + +class CachedStrategyPropertyTracking +{ + #[OPcache\VolatileStatic(strategy: OPcache\CacheStrategy::Tracking)] + public static ?CachedStrategyPayload $value = null; + + public static function value(): CachedStrategyPayload + { + self::$value ??= cached_strategy_payload('property_tracking'); + return self::$value; + } +} + +class CachedStrategyMethodDefault +{ + #[OPcache\VolatileStatic] + public static function value(): CachedStrategyPayload + { + static $value = null; + + $value ??= cached_strategy_payload('method_default'); + return $value; + } +} + +class CachedStrategyMethodImmediate +{ + #[OPcache\VolatileStatic(strategy: OPcache\CacheStrategy::Immediate)] + public static function value(): CachedStrategyPayload + { + static $value = null; + + $value ??= cached_strategy_payload('method_immediate'); + return $value; + } +} + +class CachedStrategyMethodTracking +{ + #[OPcache\VolatileStatic(strategy: OPcache\CacheStrategy::Tracking)] + public static function value(): CachedStrategyPayload + { + static $value = null; + + $value ??= cached_strategy_payload('method_tracking'); + return $value; + } +} + +function cached_strategy_values(string $case): array +{ + return match ($case) { + 'class_default' => [CachedStrategyClassDefault::property(), CachedStrategyClassDefault::method()], + 'class_immediate' => [CachedStrategyClassImmediate::property(), CachedStrategyClassImmediate::method()], + 'class_tracking' => [CachedStrategyClassTracking::property(), CachedStrategyClassTracking::method()], + 'property_default' => [CachedStrategyPropertyDefault::value()], + 'property_immediate' => [CachedStrategyPropertyImmediate::value()], + 'property_tracking' => [CachedStrategyPropertyTracking::value()], + 'method_default' => [CachedStrategyMethodDefault::value()], + 'method_immediate' => [CachedStrategyMethodImmediate::value()], + 'method_tracking' => [CachedStrategyMethodTracking::value()], + default => throw new RuntimeException('unknown case'), + }; +} + +function cached_strategy_dump(string $case, array $values): void +{ + echo $case, ':', implode('|', array_map( + static fn (CachedStrategyPayload $payload): int => count($payload->events), + $values, + )), "\n"; +} + +$action = $_GET['action'] ?? 'read'; +$case = $_GET['case'] ?? 'class_default'; + +if ($action === 'reset') { + OPcache\volatile_clear(); + OPcache\persistent_clear(); + opcache_reset(); + echo "reset\n"; + return; +} + +$values = cached_strategy_values($case); + +if ($action === 'seed' || $action === 'mutate_after_fetch') { + foreach ($values as $value) { + cached_strategy_mutate($value); + } +} + +cached_strategy_dump($case, $values); + +if ($action === 'read') { + echo 'cache=', OPcache\volatile_cache_info()['entry_count'], ',', OPcache\persistent_cache_info()['entry_count'], "\n"; +} +PHP); + +$php = getenv('TEST_PHP_EXECUTABLE'); +if ($php) { + $php = realpath(__DIR__ . '/../../../' . $php) ?: $php; + putenv('TEST_PHP_EXECUTABLE=' . $php); +} + +include 'php_cli_server.inc'; +php_cli_server_start('-d opcache.enable=1 -d opcache.enable_cli=1 -d opcache.static_cache.volatile_size_mb=32 -d opcache.static_cache.persistent_size_mb=32 -d opcache.file_update_protection=0 -d opcache.jit=0'); + +$base = 'http://' . PHP_CLI_SERVER_ADDRESS . '/volatile_static_strategy_001.php'; +$cases = [ + 'class_default', + 'class_immediate', + 'class_tracking', + 'property_default', + 'property_immediate', + 'property_tracking', + 'method_default', + 'method_immediate', + 'method_tracking', +]; + +foreach ($cases as $case) { + echo file_get_contents($base . '?action=reset'); + echo file_get_contents($base . '?action=seed&case=' . $case); + echo file_get_contents($base . '?action=read&case=' . $case); + echo file_get_contents($base . '?action=mutate_after_fetch&case=' . $case); + echo file_get_contents($base . '?action=read&case=' . $case); +} + +?> +--CLEAN-- + +--EXPECT-- +attribute=0:0,0:1,Tracking,1:0 +cases=Immediate:0,Tracking:1 +ttl-value-error +reset +class_default:1|1 +class_default:0|0 +cache=1,0 +class_default:1|1 +class_default:0|0 +cache=1,0 +reset +class_immediate:1|1 +class_immediate:0|0 +cache=1,0 +class_immediate:1|1 +class_immediate:0|0 +cache=1,0 +reset +class_tracking:1|1 +class_tracking:1|1 +cache=1,0 +class_tracking:2|2 +class_tracking:2|2 +cache=1,0 +reset +property_default:1 +property_default:0 +cache=1,0 +property_default:1 +property_default:0 +cache=1,0 +reset +property_immediate:1 +property_immediate:0 +cache=1,0 +property_immediate:1 +property_immediate:0 +cache=1,0 +reset +property_tracking:1 +property_tracking:1 +cache=1,0 +property_tracking:2 +property_tracking:2 +cache=1,0 +reset +method_default:1 +method_default:0 +cache=1,0 +method_default:1 +method_default:0 +cache=1,0 +reset +method_immediate:1 +method_immediate:0 +cache=1,0 +method_immediate:1 +method_immediate:0 +cache=1,0 +reset +method_tracking:1 +method_tracking:1 +cache=1,0 +method_tracking:2 +method_tracking:2 +cache=1,0 diff --git a/ext/opcache/tests/static_cache_volatile_static_tracking_alias_value_kinds_001.phpt b/ext/opcache/tests/static_cache_volatile_static_tracking_alias_value_kinds_001.phpt new file mode 100644 index 000000000000..66d986f34f7a --- /dev/null +++ b/ext/opcache/tests/static_cache_volatile_static_tracking_alias_value_kinds_001.phpt @@ -0,0 +1,185 @@ +--TEST-- +OPcache VolatileStatic tracking follows PHP alias semantics across scalar and array assignments +--EXTENSIONS-- +opcache +--CONFLICTS-- +server +--FILE-- + '1']; + VolatileStaticTrackingArrayCopyState::$value = $value; + $value['bar'] = '2'; + echo 'seed=', VolatileStaticTrackingArrayCopyState::$value['bar'], "\n"; + return; + + case 'array_reference': + $value = '1'; + $array = ['bar' => &$value]; + VolatileStaticTrackingArrayReferenceState::$value = $array; + $value = '2'; + echo 'seed=', VolatileStaticTrackingArrayReferenceState::$value['bar'], "\n"; + return; + + case 'scalar_reference_call': + $value = '1'; + VolatileStaticTrackingScalarReferenceCallState::$value = $value; + volatile_static_tracking_alias_mutate_string($value); + echo 'seed=', VolatileStaticTrackingScalarReferenceCallState::$value, "\n"; + return; + + case 'object_reference': + $value = new VolatileStaticTrackingObjectReferenceValue(); + $value->bar = '1'; + VolatileStaticTrackingObjectReferenceState::$value = $value; + $value->bar = '2'; + echo 'seed=', VolatileStaticTrackingObjectReferenceState::$value?->bar ?? 'null', "\n"; + return; + + default: + throw new RuntimeException('unknown case'); + } +} + +function volatile_static_tracking_alias_read(string $case): void +{ + switch ($case) { + case 'scalar_copy': + echo 'read=', VolatileStaticTrackingScalarCopyState::$value ?? 'null', "\n"; + return; + + case 'array_copy': + echo 'read=', VolatileStaticTrackingArrayCopyState::$value['bar'] ?? 'null', "\n"; + return; + + case 'array_reference': + echo 'read=', VolatileStaticTrackingArrayReferenceState::$value['bar'] ?? 'null', "\n"; + return; + + case 'scalar_reference_call': + echo 'read=', VolatileStaticTrackingScalarReferenceCallState::$value ?? 'null', "\n"; + return; + + case 'object_reference': + echo 'read=', VolatileStaticTrackingObjectReferenceState::$value?->bar ?? 'null', "\n"; + return; + + default: + throw new RuntimeException('unknown case'); + } +} + +$action = $_GET['action'] ?? 'read'; +$case = $_GET['case'] ?? 'scalar_copy'; + +if ($action === 'reset') { + OPcache\volatile_clear(); + opcache_reset(); + echo "reset\n"; + return; +} + +if ($action === 'seed') { + volatile_static_tracking_alias_seed($case); + return; +} + +volatile_static_tracking_alias_read($case); +PHP); + +$php = getenv('TEST_PHP_EXECUTABLE'); +if ($php) { + $php = realpath(__DIR__ . '/../../../' . $php) ?: $php; + putenv('TEST_PHP_EXECUTABLE=' . $php); +} + +include 'php_cli_server.inc'; +php_cli_server_start('-d opcache.enable=1 -d opcache.enable_cli=1 -d opcache.static_cache.volatile_size_mb=32 -d opcache.file_update_protection=0 -d opcache.jit=0'); + +$base = 'http://' . PHP_CLI_SERVER_ADDRESS . '/static_cache_volatile_static_tracking_alias_value_kinds_001.php'; +foreach (['scalar_copy', 'array_copy', 'array_reference', 'scalar_reference_call', 'object_reference'] as $case) { + echo file_get_contents($base . '?action=reset'); + echo $case, "\n"; + echo file_get_contents($base . '?action=seed&case=' . $case); + echo file_get_contents($base . '?action=read&case=' . $case); +} + +?> +--CLEAN-- + +--EXPECT-- +reset +scalar_copy +seed=1 +read=1 +reset +array_copy +seed=1 +read=1 +reset +array_reference +seed=2 +read=2 +reset +scalar_reference_call +seed=1 +read=1 +reset +object_reference +seed=2 +read=2 diff --git a/ext/opcache/tests/static_cache_volatile_static_tracking_clear_invalidate_after_access_001.phpt b/ext/opcache/tests/static_cache_volatile_static_tracking_clear_invalidate_after_access_001.phpt new file mode 100644 index 000000000000..481642f59414 --- /dev/null +++ b/ext/opcache/tests/static_cache_volatile_static_tracking_clear_invalidate_after_access_001.phpt @@ -0,0 +1,291 @@ +--TEST-- +OPcache VolatileStatic tracking reference graphs are not republished after volatile_clear() or opcache_invalidate() in the same request +--EXTENSIONS-- +opcache +--CONFLICTS-- +server +--FILE-- +holder = new stdClass(); + } +} + +#[OPcache\VolatileStatic(strategy: OPcache\CacheStrategy::Tracking)] +class TrackingClearInvalidateClassState +{ + public static ?TrackingClearInvalidatePayload $payload = null; +} + +class TrackingClearInvalidatePropertyState +{ + #[OPcache\VolatileStatic(strategy: OPcache\CacheStrategy::Tracking)] + public static ?TrackingClearInvalidatePayload $payload = null; +} + +class TrackingClearInvalidateMethodState +{ + #[OPcache\VolatileStatic(strategy: OPcache\CacheStrategy::Tracking)] + public static function payload(?TrackingClearInvalidatePayload $payload = null): ?TrackingClearInvalidatePayload + { + static $value = null; + + if ($payload !== null) { + $value = $payload; + } + + return $value; + } +} + +function tracking_clear_invalidate_set(string $kind, ?TrackingClearInvalidatePayload $payload): void +{ + switch ($kind) { + case 'class': + TrackingClearInvalidateClassState::$payload = $payload; + return; + + case 'property': + TrackingClearInvalidatePropertyState::$payload = $payload; + return; + + case 'method': + TrackingClearInvalidateMethodState::payload($payload); + return; + + default: + throw new RuntimeException('unknown kind'); + } +} + +function tracking_clear_invalidate_get(string $kind): ?TrackingClearInvalidatePayload +{ + return match ($kind) { + 'class' => TrackingClearInvalidateClassState::$payload, + 'property' => TrackingClearInvalidatePropertyState::$payload, + 'method' => TrackingClearInvalidateMethodState::payload(), + default => throw new RuntimeException('unknown kind'), + }; +} + +function tracking_clear_invalidate_apply(TrackingClearInvalidatePayload $payload, string $nodeName, string $childName, string $label): void +{ + $payload->objectRef->name = $nodeName; + $payload->objectRef->events[] = $label . '-object'; + $payload->arrayRef['child']->name = $childName; + $payload->arrayRef['child']->events[] = $label . '-child'; + $payload->holder->alias->events[] = $label . '-holder'; + $payload->log[] = $label; +} + +function tracking_clear_invalidate_seed(string $kind): void +{ + $leaf = new TrackingClearInvalidateNode('seed'); + $leaf->child = new TrackingClearInvalidateChild('child'); + + $payload = new TrackingClearInvalidatePayload(); + $payload->objectRef =& $leaf; + $payload->arrayRef = [ + 'alias' => &$leaf, + 'child' => &$leaf->child, + ]; + $payload->holder->alias =& $leaf; + $payload->holder->child =& $leaf->child; + $payload->log[] = 'seed'; + + tracking_clear_invalidate_set($kind, $payload); + tracking_clear_invalidate_apply($payload, 'seed-node', 'seed-child', 'seed'); +} + +function tracking_clear_invalidate_dump(string $kind, string $label): void +{ + $payload = tracking_clear_invalidate_get($kind); + if (!$payload instanceof TrackingClearInvalidatePayload) { + echo $kind, '@', $label, '=missing', "\n"; + return; + } + + echo $kind, '@', $label, '=', + $payload->objectRef->name, ',', + count($payload->objectRef->events), ',', + $payload->objectRef->child->name, ',', + count($payload->objectRef->child->events), ',', + count($payload->log), ',', + ($payload->objectRef === $payload->arrayRef['alias'] ? 'same' : 'copy'), ',', + ($payload->objectRef->child === $payload->arrayRef['child'] ? 'same' : 'copy'), ',', + ($payload->holder->alias === $payload->objectRef ? 'same' : 'copy'), ',', + ($payload->holder->child === $payload->objectRef->child ? 'same' : 'copy'), ',', + ($payload->holder->alias->child === $payload->objectRef->child ? 'same' : 'copy'), + "\n"; +} + +$action = $_GET['action'] ?? 'read'; +$kind = $_GET['kind'] ?? 'class'; + +if ($action === 'reset') { + OPcache\volatile_clear(); + opcache_reset(); + echo "reset\n"; + return; +} + +if ($action === 'seed') { + tracking_clear_invalidate_seed($kind); + tracking_clear_invalidate_dump($kind, 'seed'); + return; +} + +if ($action === 'clear_after_access') { + $payload = tracking_clear_invalidate_get($kind); + if (!$payload instanceof TrackingClearInvalidatePayload) { + throw new RuntimeException('missing payload'); + } + + tracking_clear_invalidate_apply($payload, 'clear-node', 'clear-child', 'clear'); + tracking_clear_invalidate_dump($kind, 'clear-before'); + OPcache\volatile_clear(); + echo "clear\n"; + return; +} + +if ($action === 'reset_after_access') { + $payload = tracking_clear_invalidate_get($kind); + if (!$payload instanceof TrackingClearInvalidatePayload) { + throw new RuntimeException('missing payload'); + } + + tracking_clear_invalidate_apply($payload, 'reset-node', 'reset-child', 'reset'); + tracking_clear_invalidate_dump($kind, 'reset-before'); + opcache_reset(); + echo "reset-after\n"; + return; +} + +if ($action === 'invalidate_after_access') { + $payload = tracking_clear_invalidate_get($kind); + if (!$payload instanceof TrackingClearInvalidatePayload) { + throw new RuntimeException('missing payload'); + } + + tracking_clear_invalidate_apply($payload, 'invalidate-node', 'invalidate-child', 'invalidate'); + tracking_clear_invalidate_dump($kind, 'invalidate-before'); + var_dump(opcache_invalidate(__FILE__, true)); + return; +} + +if ($action === 'read') { + tracking_clear_invalidate_dump($kind, 'read'); + return; +} + +throw new RuntimeException('unknown action'); +PHP); + +$php = getenv('TEST_PHP_EXECUTABLE'); +if ($php) { + $php = realpath(__DIR__ . '/../../../' . $php) ?: $php; + putenv('TEST_PHP_EXECUTABLE=' . $php); +} + +include 'php_cli_server.inc'; +php_cli_server_start('-d opcache.enable=1 -d opcache.enable_cli=1 -d opcache.static_cache.volatile_size_mb=32 -d opcache.file_update_protection=0 -d opcache.jit=0'); + +$base = 'http://' . PHP_CLI_SERVER_ADDRESS . '/volatile_static_tracking_clear_invalidate_after_access_001.php'; +echo file_get_contents($base . '?action=reset'); +foreach (['class', 'property', 'method'] as $kind) { + echo file_get_contents($base . '?action=seed&kind=' . $kind); + echo file_get_contents($base . '?action=read&kind=' . $kind); + echo file_get_contents($base . '?action=clear_after_access&kind=' . $kind); + echo file_get_contents($base . '?action=read&kind=' . $kind); + echo file_get_contents($base . '?action=seed&kind=' . $kind); + echo file_get_contents($base . '?action=read&kind=' . $kind); + echo file_get_contents($base . '?action=reset_after_access&kind=' . $kind); + echo file_get_contents($base . '?action=read&kind=' . $kind); + echo file_get_contents($base . '?action=seed&kind=' . $kind); + echo file_get_contents($base . '?action=read&kind=' . $kind); + echo file_get_contents($base . '?action=invalidate_after_access&kind=' . $kind); + echo file_get_contents($base . '?action=read&kind=' . $kind); +} + +?> +--CLEAN-- + +--EXPECT-- +reset +class@seed=seed-node,2,seed-child,1,2,same,same,same,same,same +class@read=seed-node,2,seed-child,1,2,same,same,same,same,same +class@clear-before=clear-node,4,clear-child,2,3,same,same,same,same,same +clear +class@read=missing +class@seed=seed-node,2,seed-child,1,2,same,same,same,same,same +class@read=seed-node,2,seed-child,1,2,same,same,same,same,same +class@reset-before=reset-node,4,reset-child,2,3,same,same,same,same,same +reset-after +class@read=missing +class@seed=seed-node,2,seed-child,1,2,same,same,same,same,same +class@read=seed-node,2,seed-child,1,2,same,same,same,same,same +class@invalidate-before=invalidate-node,4,invalidate-child,2,3,same,same,same,same,same +bool(true) +class@read=missing +property@seed=seed-node,2,seed-child,1,2,same,same,same,same,same +property@read=seed-node,2,seed-child,1,2,same,same,same,same,same +property@clear-before=clear-node,4,clear-child,2,3,same,same,same,same,same +clear +property@read=missing +property@seed=seed-node,2,seed-child,1,2,same,same,same,same,same +property@read=seed-node,2,seed-child,1,2,same,same,same,same,same +property@reset-before=reset-node,4,reset-child,2,3,same,same,same,same,same +reset-after +property@read=missing +property@seed=seed-node,2,seed-child,1,2,same,same,same,same,same +property@read=seed-node,2,seed-child,1,2,same,same,same,same,same +property@invalidate-before=invalidate-node,4,invalidate-child,2,3,same,same,same,same,same +bool(true) +property@read=missing +method@seed=seed-node,2,seed-child,1,2,same,same,same,same,same +method@read=seed-node,2,seed-child,1,2,same,same,same,same,same +method@clear-before=clear-node,4,clear-child,2,3,same,same,same,same,same +clear +method@read=missing +method@seed=seed-node,2,seed-child,1,2,same,same,same,same,same +method@read=seed-node,2,seed-child,1,2,same,same,same,same,same +method@reset-before=reset-node,4,reset-child,2,3,same,same,same,same,same +reset-after +method@read=missing +method@seed=seed-node,2,seed-child,1,2,same,same,same,same,same +method@read=seed-node,2,seed-child,1,2,same,same,same,same,same +method@invalidate-before=invalidate-node,4,invalidate-child,2,3,same,same,same,same,same +bool(true) +method@read=missing \ No newline at end of file diff --git a/ext/opcache/tests/static_cache_volatile_static_tracking_reference_object_graph_001.phpt b/ext/opcache/tests/static_cache_volatile_static_tracking_reference_object_graph_001.phpt new file mode 100644 index 000000000000..ad787004ce7a --- /dev/null +++ b/ext/opcache/tests/static_cache_volatile_static_tracking_reference_object_graph_001.phpt @@ -0,0 +1,230 @@ +--TEST-- +OPcache VolatileStatic tracking persists deep mutations through referenced object graphs +--EXTENSIONS-- +opcache +--CONFLICTS-- +server +--FILE-- +holder = new stdClass(); + } +} + +#[OPcache\VolatileStatic(strategy: OPcache\CacheStrategy::Tracking)] +class TrackingReferenceClassState +{ + public static ?TrackingReferencePayload $payload = null; +} + +class TrackingReferencePropertyState +{ + #[OPcache\VolatileStatic(strategy: OPcache\CacheStrategy::Tracking)] + public static ?TrackingReferencePayload $payload = null; +} + +class TrackingReferenceMethodState +{ + #[OPcache\VolatileStatic(strategy: OPcache\CacheStrategy::Tracking)] + public static function payload(?TrackingReferencePayload $payload = null): ?TrackingReferencePayload + { + static $value = null; + + if ($payload !== null) { + $value = $payload; + } + + return $value; + } +} + +function tracking_reference_set(string $kind, ?TrackingReferencePayload $payload): void +{ + switch ($kind) { + case 'class': + TrackingReferenceClassState::$payload = $payload; + return; + + case 'property': + TrackingReferencePropertyState::$payload = $payload; + return; + + case 'method': + TrackingReferenceMethodState::payload($payload); + return; + + default: + throw new RuntimeException('unknown kind'); + } +} + +function tracking_reference_get(string $kind): ?TrackingReferencePayload +{ + return match ($kind) { + 'class' => TrackingReferenceClassState::$payload, + 'property' => TrackingReferencePropertyState::$payload, + 'method' => TrackingReferenceMethodState::payload(), + default => throw new RuntimeException('unknown kind'), + }; +} + +function tracking_reference_seed(string $kind): void +{ + $leaf = new TrackingReferenceNode('seed'); + $leaf->child = new TrackingReferenceChild('child'); + + $payload = new TrackingReferencePayload(); + $payload->objectRef =& $leaf; + $payload->arrayRef = [ + 'alias' => &$leaf, + 'child' => &$leaf->child, + ]; + $payload->holder->alias =& $leaf; + $payload->holder->child =& $leaf->child; + $payload->log[] = 'seed'; + + tracking_reference_set($kind, $payload); + + $leaf->name = 'seed-updated'; + $leaf->events[] = 'source-object'; + $leaf->child->name = 'child-updated'; + $leaf->child->events[] = 'source-child'; + $payload->arrayRef['alias']->events[] = 'array-alias'; + $payload->holder->alias->child->events[] = 'holder-alias-child'; + $payload->log[] = 'seed-mutated'; +} + +function tracking_reference_mutate(string $kind): void +{ + $payload = tracking_reference_get($kind); + if (!$payload instanceof TrackingReferencePayload) { + throw new RuntimeException('missing payload'); + } + + $payload->objectRef->name = 'mutated-again'; + $payload->objectRef->events[] = 'object-ref'; + $payload->arrayRef['child']->name = 'child-mutated-again'; + $payload->arrayRef['child']->events[] = 'array-child'; + $payload->holder->alias->events[] = 'holder-alias'; + $payload->holder->alias->child->events[] = 'holder-alias-child'; + $payload->log[] = 'mutated'; +} + +function tracking_reference_dump(string $kind, string $label): void +{ + $payload = tracking_reference_get($kind); + if (!$payload instanceof TrackingReferencePayload) { + echo $kind, '@', $label, '=missing', "\n"; + return; + } + + echo $kind, '@', $label, '=', + $payload->objectRef->name, ',', + count($payload->objectRef->events), ',', + $payload->objectRef->child->name, ',', + count($payload->objectRef->child->events), ',', + count($payload->log), ',', + ($payload->objectRef === $payload->arrayRef['alias'] ? 'same' : 'copy'), ',', + ($payload->objectRef->child === $payload->arrayRef['child'] ? 'same' : 'copy'), ',', + ($payload->holder->alias === $payload->objectRef ? 'same' : 'copy'), ',', + ($payload->holder->child === $payload->objectRef->child ? 'same' : 'copy'), ',', + ($payload->holder->alias->child === $payload->objectRef->child ? 'same' : 'copy'), + "\n"; +} + +$action = $_GET['action'] ?? 'read'; +$kind = $_GET['kind'] ?? 'class'; + +if ($action === 'reset') { + OPcache\volatile_clear(); + opcache_reset(); + echo "reset\n"; + return; +} + +if ($action === 'seed') { + tracking_reference_seed($kind); + tracking_reference_dump($kind, 'seed'); + return; +} + +if ($action === 'mutate') { + tracking_reference_mutate($kind); + tracking_reference_dump($kind, 'mutate'); + return; +} + +if ($action === 'read') { + tracking_reference_dump($kind, 'read'); + return; +} + +throw new RuntimeException('unknown action'); +PHP); + +$php = getenv('TEST_PHP_EXECUTABLE'); +if ($php) { + $php = realpath(__DIR__ . '/../../../' . $php) ?: $php; + putenv('TEST_PHP_EXECUTABLE=' . $php); +} + +include 'php_cli_server.inc'; +php_cli_server_start('-d opcache.enable=1 -d opcache.enable_cli=1 -d opcache.static_cache.volatile_size_mb=32 -d opcache.file_update_protection=0 -d opcache.jit=0'); + +$base = 'http://' . PHP_CLI_SERVER_ADDRESS . '/volatile_static_tracking_reference_object_graph_001.php'; +echo file_get_contents($base . '?action=reset'); +foreach (['class', 'property', 'method'] as $kind) { + echo file_get_contents($base . '?action=seed&kind=' . $kind); + echo file_get_contents($base . '?action=read&kind=' . $kind); + echo file_get_contents($base . '?action=mutate&kind=' . $kind); + echo file_get_contents($base . '?action=read&kind=' . $kind); +} + +?> +--CLEAN-- + +--EXPECT-- +reset +class@seed=seed-updated,2,child-updated,2,2,same,same,same,same,same +class@read=seed-updated,2,child-updated,2,2,same,same,same,same,same +class@mutate=mutated-again,4,child-mutated-again,4,3,same,same,same,same,same +class@read=mutated-again,4,child-mutated-again,4,3,same,same,same,same,same +property@seed=seed-updated,2,child-updated,2,2,same,same,same,same,same +property@read=seed-updated,2,child-updated,2,2,same,same,same,same,same +property@mutate=mutated-again,4,child-mutated-again,4,3,same,same,same,same,same +property@read=mutated-again,4,child-mutated-again,4,3,same,same,same,same,same +method@seed=seed-updated,2,child-updated,2,2,same,same,same,same,same +method@read=seed-updated,2,child-updated,2,2,same,same,same,same,same +method@mutate=mutated-again,4,child-mutated-again,4,3,same,same,same,same,same +method@read=mutated-again,4,child-mutated-again,4,3,same,same,same,same,same diff --git a/ext/opcache/tests/static_cache_volatile_static_tracking_shared_dependency_001.phpt b/ext/opcache/tests/static_cache_volatile_static_tracking_shared_dependency_001.phpt new file mode 100644 index 000000000000..c33e853d44ff --- /dev/null +++ b/ext/opcache/tests/static_cache_volatile_static_tracking_shared_dependency_001.phpt @@ -0,0 +1,260 @@ +--TEST-- +OPcache VolatileStatic tracking marks all roots sharing a mutated dependency dirty +--EXTENSIONS-- +opcache +--CONFLICTS-- +server +--FILE-- + ['count' => 0, 'leaf' => $leaf], 'events' => []]; + $payload = new TrackingSharedPayload($leaf, $box); + $payload->refs['leaf'] = $leaf; + $payload->refs['box'] =& $payload->box; + + return $payload; +} + +function tracking_shared_pair(string $kind): array +{ + return match ($kind) { + 'class' => [TrackingSharedClassA::$payload, TrackingSharedClassB::$payload], + 'property' => [TrackingSharedPropertyA::$payload, TrackingSharedPropertyB::$payload], + 'method' => [TrackingSharedMethodA::payload(), TrackingSharedMethodB::payload()], + 'blob' => [TrackingSharedBlobState::$first, TrackingSharedBlobState::$second], + default => throw new RuntimeException('unknown kind'), + }; +} + +function tracking_shared_seed(string $kind): void +{ + $payload = tracking_shared_payload($kind); + + match ($kind) { + 'class' => [TrackingSharedClassA::$payload = $payload, TrackingSharedClassB::$payload = $payload], + 'property' => [TrackingSharedPropertyA::$payload = $payload, TrackingSharedPropertyB::$payload = $payload], + 'method' => [TrackingSharedMethodA::payload($payload), TrackingSharedMethodB::payload($payload)], + 'blob' => [TrackingSharedBlobState::$first = $payload, TrackingSharedBlobState::$second = $payload], + default => throw new RuntimeException('unknown kind'), + }; +} + +function tracking_shared_mutate(string $kind): void +{ + [$first, $second] = tracking_shared_pair($kind); + if (!$first instanceof TrackingSharedPayload || !$second instanceof TrackingSharedPayload) { + throw new RuntimeException('missing payload'); + } + + if ($kind !== 'blob' && $first !== $second) { + $second = $first; + match ($kind) { + 'class' => TrackingSharedClassB::$payload = $second, + 'property' => TrackingSharedPropertyB::$payload = $second, + 'method' => TrackingSharedMethodB::payload($second), + default => throw new RuntimeException('unknown kind'), + }; + } + + $first->leaf->events[] = $kind . '-leaf'; + $first->box['nested']['count']++; + $first->box['nested']['leaf']->events[] = $kind . '-nested-leaf'; + $first->refs['box']['events'][] = $kind . '-ref-array'; +} + +function tracking_shared_dump(string $kind): void +{ + [$first, $second] = tracking_shared_pair($kind); + echo $kind, '@pair=', ($first === $second ? 'same' : 'copy'), "\n"; + foreach ([$first, $second] as $index => $payload) { + if (!$payload instanceof TrackingSharedPayload) { + echo $kind, '#', $index, '=missing', "\n"; + continue; + } + + echo $kind, '#', $index, '=', + count($payload->leaf->events), ',', + $payload->box['nested']['count'], ',', + count($payload->box['nested']['leaf']->events), ',', + count($payload->box['events']), ',', + ($payload->refs['leaf'] === $payload->leaf ? 'same' : 'copy'), ',', + ($payload->refs['box'] === $payload->box ? 'same' : 'copy'), + "\n"; + } +} + +$action = $_GET['action'] ?? 'read'; +$kind = $_GET['kind'] ?? 'class'; + +if ($action === 'reset') { + OPcache\volatile_clear(); + opcache_reset(); + echo "reset\n"; + return; +} + +if ($action === 'seed') { + tracking_shared_seed($kind); + tracking_shared_dump($kind); + return; +} + +if ($action === 'mutate') { + tracking_shared_mutate($kind); + tracking_shared_dump($kind); + return; +} + +if ($action === 'read') { + tracking_shared_dump($kind); + return; +} + +throw new RuntimeException('unknown action'); +PHP); + +$php = getenv('TEST_PHP_EXECUTABLE'); +if ($php) { + $php = realpath(__DIR__ . '/../../../' . $php) ?: $php; + putenv('TEST_PHP_EXECUTABLE=' . $php); +} + +include 'php_cli_server.inc'; +php_cli_server_start('-d opcache.enable=1 -d opcache.enable_cli=1 -d opcache.static_cache.volatile_size_mb=32 -d opcache.file_update_protection=0 -d opcache.jit=0'); + +$base = 'http://' . PHP_CLI_SERVER_ADDRESS . '/volatile_static_tracking_shared_dependency_001.php'; +echo file_get_contents($base . '?action=reset'); +foreach (['class', 'property', 'method', 'blob'] as $kind) { + echo file_get_contents($base . '?action=seed&kind=' . $kind); + echo file_get_contents($base . '?action=mutate&kind=' . $kind); + echo file_get_contents($base . '?action=read&kind=' . $kind); +} + +?> +--CLEAN-- + +--EXPECT-- +reset +class@pair=same +class#0=0,0,0,0,same,same +class#1=0,0,0,0,same,same +class@pair=same +class#0=2,1,2,1,same,same +class#1=2,1,2,1,same,same +class@pair=copy +class#0=2,1,2,1,same,same +class#1=2,1,2,1,same,same +property@pair=same +property#0=0,0,0,0,same,same +property#1=0,0,0,0,same,same +property@pair=same +property#0=2,1,2,1,same,same +property#1=2,1,2,1,same,same +property@pair=copy +property#0=2,1,2,1,same,same +property#1=2,1,2,1,same,same +method@pair=same +method#0=0,0,0,0,same,same +method#1=0,0,0,0,same,same +method@pair=same +method#0=2,1,2,1,same,same +method#1=2,1,2,1,same,same +method@pair=copy +method#0=2,1,2,1,same,same +method#1=2,1,2,1,same,same +blob@pair=same +blob#0=0,0,0,0,same,same +blob#1=0,0,0,0,same,same +blob@pair=same +blob#0=2,1,2,1,same,same +blob#1=2,1,2,1,same,same +blob@pair=same +blob#0=2,1,2,1,same,same +blob#1=2,1,2,1,same,same diff --git a/ext/opcache/tests/static_cache_volatile_static_tracking_shared_reference_cell_001.phpt b/ext/opcache/tests/static_cache_volatile_static_tracking_shared_reference_cell_001.phpt new file mode 100644 index 000000000000..a5af664ca906 --- /dev/null +++ b/ext/opcache/tests/static_cache_volatile_static_tracking_shared_reference_cell_001.phpt @@ -0,0 +1,301 @@ +--TEST-- +OPcache VolatileStatic tracking publishes shared reference-cell mutations before restore and detaches roots after restore +--EXTENSIONS-- +opcache +--CONFLICTS-- +server +--FILE-- +holder = new stdClass(); + } +} + +#[OPcache\VolatileStatic(strategy: OPcache\CacheStrategy::Tracking)] +class TrackingSharedReferenceClassA +{ + public static ?TrackingSharedReferencePayload $payload = null; +} + +#[OPcache\VolatileStatic(strategy: OPcache\CacheStrategy::Tracking)] +class TrackingSharedReferenceClassB +{ + public static ?TrackingSharedReferencePayload $payload = null; +} + +class TrackingSharedReferencePropertyA +{ + #[OPcache\VolatileStatic(strategy: OPcache\CacheStrategy::Tracking)] + public static ?TrackingSharedReferencePayload $payload = null; +} + +class TrackingSharedReferencePropertyB +{ + #[OPcache\VolatileStatic(strategy: OPcache\CacheStrategy::Tracking)] + public static ?TrackingSharedReferencePayload $payload = null; +} + +class TrackingSharedReferenceMethodA +{ + #[OPcache\VolatileStatic(strategy: OPcache\CacheStrategy::Tracking)] + public static function payload(?TrackingSharedReferencePayload $payload = null): ?TrackingSharedReferencePayload + { + static $value = null; + + if ($payload !== null) { + $value = $payload; + } + + return $value; + } +} + +class TrackingSharedReferenceMethodB +{ + #[OPcache\VolatileStatic(strategy: OPcache\CacheStrategy::Tracking)] + public static function payload(?TrackingSharedReferencePayload $payload = null): ?TrackingSharedReferencePayload + { + static $value = null; + + if ($payload !== null) { + $value = $payload; + } + + return $value; + } +} + +#[OPcache\VolatileStatic(strategy: OPcache\CacheStrategy::Tracking)] +class TrackingSharedReferenceBlob +{ + public static ?TrackingSharedReferencePayload $first = null; + public static ?TrackingSharedReferencePayload $second = null; +} + +function tracking_shared_reference_make_payload(string $label, TrackingSharedReferenceNode &$shared): TrackingSharedReferencePayload +{ + $payload = new TrackingSharedReferencePayload($label); + $payload->nodeRef =& $shared; + $payload->refs['node'] =& $shared; + $payload->refs['child'] =& $shared->child; + $payload->holder->node =& $shared; + $payload->holder->child =& $shared->child; + $payload->labels[] = $label; + + return $payload; +} + +function tracking_shared_reference_pair(string $kind): array +{ + return match ($kind) { + 'class' => [TrackingSharedReferenceClassA::$payload, TrackingSharedReferenceClassB::$payload], + 'property' => [TrackingSharedReferencePropertyA::$payload, TrackingSharedReferencePropertyB::$payload], + 'method' => [TrackingSharedReferenceMethodA::payload(), TrackingSharedReferenceMethodB::payload()], + 'blob' => [TrackingSharedReferenceBlob::$first, TrackingSharedReferenceBlob::$second], + default => throw new RuntimeException('unknown kind'), + }; +} + +function tracking_shared_reference_seed(string $kind): void +{ + $shared = new TrackingSharedReferenceNode('seed'); + $shared->child = new TrackingSharedReferenceChild('child'); + + $first = tracking_shared_reference_make_payload($kind . '-A', $shared); + $second = tracking_shared_reference_make_payload($kind . '-B', $shared); + + match ($kind) { + 'class' => [TrackingSharedReferenceClassA::$payload = $first, TrackingSharedReferenceClassB::$payload = $second], + 'property' => [TrackingSharedReferencePropertyA::$payload = $first, TrackingSharedReferencePropertyB::$payload = $second], + 'method' => [TrackingSharedReferenceMethodA::payload($first), TrackingSharedReferenceMethodB::payload($second)], + 'blob' => [TrackingSharedReferenceBlob::$first = $first, TrackingSharedReferenceBlob::$second = $second], + default => throw new RuntimeException('unknown kind'), + }; + + $first->nodeRef->name = 'seed-updated'; + $first->nodeRef->events[] = 'seed-node'; + $first->refs['child']->name = 'child-updated'; + $first->holder->child->events[] = 'seed-child'; + $first->labels[] = 'seed-mutated'; + $second->labels[] = 'seed-observed'; +} + +function tracking_shared_reference_mutate(string $kind): void +{ + [$first, $second] = tracking_shared_reference_pair($kind); + if (!$first instanceof TrackingSharedReferencePayload || !$second instanceof TrackingSharedReferencePayload) { + throw new RuntimeException('missing payload'); + } + + $first->holder->node->name = 'mutated-again'; + $first->holder->node->events[] = 'mutate-node'; + $first->nodeRef->child->name = 'child-mutated-again'; + $first->nodeRef->child->events[] = 'mutate-child'; + $first->labels[] = 'mutated-first'; +} + +function tracking_shared_reference_dump_payload(string $kind, int $index, ?TrackingSharedReferencePayload $payload): void +{ + if (!$payload instanceof TrackingSharedReferencePayload) { + echo $kind, '#', $index, '=missing', "\n"; + return; + } + + echo $kind, '#', $index, '=', + $payload->label, ',', + $payload->nodeRef->name, ',', + count($payload->nodeRef->events), ',', + $payload->nodeRef->child->name, ',', + count($payload->nodeRef->child->events), ',', + count($payload->labels), ',', + ($payload->nodeRef === $payload->refs['node'] ? 'same' : 'copy'), ',', + ($payload->nodeRef === $payload->holder->node ? 'same' : 'copy'), ',', + ($payload->nodeRef->child === $payload->refs['child'] ? 'same' : 'copy'), ',', + ($payload->nodeRef->child === $payload->holder->child ? 'same' : 'copy'), + "\n"; +} + +function tracking_shared_reference_dump(string $kind): void +{ + [$first, $second] = tracking_shared_reference_pair($kind); + echo $kind, '@cross=', + ($first instanceof TrackingSharedReferencePayload && $second instanceof TrackingSharedReferencePayload && $first->nodeRef === $second->nodeRef ? 'same' : 'copy'), ',', + ($first instanceof TrackingSharedReferencePayload && $second instanceof TrackingSharedReferencePayload && $first->nodeRef->child === $second->nodeRef->child ? 'same' : 'copy'), + "\n"; + tracking_shared_reference_dump_payload($kind, 0, $first); + tracking_shared_reference_dump_payload($kind, 1, $second); +} + +$action = $_GET['action'] ?? 'read'; +$kind = $_GET['kind'] ?? 'class'; + +if ($action === 'reset') { + OPcache\volatile_clear(); + opcache_reset(); + echo "reset\n"; + return; +} + +if ($action === 'seed') { + tracking_shared_reference_seed($kind); + tracking_shared_reference_dump($kind); + return; +} + +if ($action === 'mutate') { + tracking_shared_reference_mutate($kind); + tracking_shared_reference_dump($kind); + return; +} + +if ($action === 'read') { + tracking_shared_reference_dump($kind); + return; +} + +throw new RuntimeException('unknown action'); +PHP); + +$php = getenv('TEST_PHP_EXECUTABLE'); +if ($php) { + $php = realpath(__DIR__ . '/../../../' . $php) ?: $php; + putenv('TEST_PHP_EXECUTABLE=' . $php); +} + +include 'php_cli_server.inc'; +php_cli_server_start('-d opcache.enable=1 -d opcache.enable_cli=1 -d opcache.static_cache.volatile_size_mb=32 -d opcache.file_update_protection=0 -d opcache.jit=0'); + +$base = 'http://' . PHP_CLI_SERVER_ADDRESS . '/volatile_static_tracking_shared_reference_cell_001.php'; +echo file_get_contents($base . '?action=reset'); +foreach (['class', 'property', 'method', 'blob'] as $kind) { + echo file_get_contents($base . '?action=seed&kind=' . $kind); + echo file_get_contents($base . '?action=read&kind=' . $kind); + echo file_get_contents($base . '?action=mutate&kind=' . $kind); + echo file_get_contents($base . '?action=read&kind=' . $kind); +} + +?> +--CLEAN-- + +--EXPECT-- +reset +class@cross=same,same +class#0=class-A,seed-updated,1,child-updated,1,2,same,same,same,same +class#1=class-B,seed-updated,1,child-updated,1,2,same,same,same,same +class@cross=copy,copy +class#0=class-A,seed-updated,1,child-updated,1,2,same,same,same,same +class#1=class-B,seed-updated,1,child-updated,1,2,same,same,same,same +class@cross=copy,copy +class#0=class-A,mutated-again,2,child-mutated-again,2,3,same,same,same,same +class#1=class-B,seed-updated,1,child-updated,1,2,same,same,same,same +class@cross=copy,copy +class#0=class-A,mutated-again,2,child-mutated-again,2,3,same,same,same,same +class#1=class-B,seed-updated,1,child-updated,1,2,same,same,same,same +property@cross=same,same +property#0=property-A,seed-updated,1,child-updated,1,2,same,same,same,same +property#1=property-B,seed-updated,1,child-updated,1,2,same,same,same,same +property@cross=copy,copy +property#0=property-A,seed-updated,1,child-updated,1,2,same,same,same,same +property#1=property-B,seed-updated,1,child-updated,1,2,same,same,same,same +property@cross=copy,copy +property#0=property-A,mutated-again,2,child-mutated-again,2,3,same,same,same,same +property#1=property-B,seed-updated,1,child-updated,1,2,same,same,same,same +property@cross=copy,copy +property#0=property-A,mutated-again,2,child-mutated-again,2,3,same,same,same,same +property#1=property-B,seed-updated,1,child-updated,1,2,same,same,same,same +method@cross=same,same +method#0=method-A,seed-updated,1,child-updated,1,2,same,same,same,same +method#1=method-B,seed-updated,1,child-updated,1,2,same,same,same,same +method@cross=copy,copy +method#0=method-A,seed-updated,1,child-updated,1,2,same,same,same,same +method#1=method-B,seed-updated,1,child-updated,1,2,same,same,same,same +method@cross=copy,copy +method#0=method-A,mutated-again,2,child-mutated-again,2,3,same,same,same,same +method#1=method-B,seed-updated,1,child-updated,1,2,same,same,same,same +method@cross=copy,copy +method#0=method-A,mutated-again,2,child-mutated-again,2,3,same,same,same,same +method#1=method-B,seed-updated,1,child-updated,1,2,same,same,same,same +blob@cross=same,same +blob#0=blob-A,seed-updated,1,child-updated,1,2,same,same,same,same +blob#1=blob-B,seed-updated,1,child-updated,1,2,same,same,same,same +blob@cross=copy,copy +blob#0=blob-A,seed-updated,1,child-updated,1,2,same,same,same,same +blob#1=blob-B,seed-updated,1,child-updated,1,2,same,same,same,same +blob@cross=copy,copy +blob#0=blob-A,mutated-again,2,child-mutated-again,2,3,same,same,same,same +blob#1=blob-B,seed-updated,1,child-updated,1,2,same,same,same,same +blob@cross=copy,copy +blob#0=blob-A,mutated-again,2,child-mutated-again,2,3,same,same,same,same +blob#1=blob-B,seed-updated,1,child-updated,1,2,same,same,same,same diff --git a/ext/opcache/tests/static_cache_volatile_static_ttl_001.phpt b/ext/opcache/tests/static_cache_volatile_static_ttl_001.phpt new file mode 100644 index 000000000000..29a10721f6a1 --- /dev/null +++ b/ext/opcache/tests/static_cache_volatile_static_ttl_001.phpt @@ -0,0 +1,123 @@ +--TEST-- +OPcache VolatileStatic TTL expires class, property, and method state +--EXTENSIONS-- +opcache +--CONFLICTS-- +server +--FILE-- + +--CLEAN-- + +--EXPECT-- +reset +first=1,1,1,1,1,1,1,1 +second=2,2,2,2,2,2,2,2 +expired=1,1,1,1,1,1,1,1 +again=2,2,2,2,2,2,2,2 diff --git a/ext/opcache/tests/static_cache_volatile_static_ttl_invalid_attribute_001.phpt b/ext/opcache/tests/static_cache_volatile_static_ttl_invalid_attribute_001.phpt new file mode 100644 index 000000000000..6a26daf7b324 --- /dev/null +++ b/ext/opcache/tests/static_cache_volatile_static_ttl_invalid_attribute_001.phpt @@ -0,0 +1,19 @@ +--TEST-- +OPcache VolatileStatic rejects negative TTL attributes +--EXTENSIONS-- +opcache +--INI-- +opcache.enable=1 +opcache.enable_cli=1 +opcache.static_cache.volatile_size_mb=32 +--FILE-- + +--EXPECTF-- +Fatal error: OPcache\VolatileStatic ttl must be greater than or equal to 0 in %s on line %d diff --git a/ext/opcache/tests/static_cache_volatile_static_ttl_refresh_001.phpt b/ext/opcache/tests/static_cache_volatile_static_ttl_refresh_001.phpt new file mode 100644 index 000000000000..374ed041742e --- /dev/null +++ b/ext/opcache/tests/static_cache_volatile_static_ttl_refresh_001.phpt @@ -0,0 +1,68 @@ +--TEST-- +OPcache VolatileStatic TTL is refreshed by publication +--EXTENSIONS-- +opcache +--CONFLICTS-- +server +--FILE-- + +--CLEAN-- + +--EXPECT-- +reset +first=1,1 +refreshed=2,2 +still-live=3,3 diff --git a/ext/opcache/tests/static_cache_windows_backend_001.phpt b/ext/opcache/tests/static_cache_windows_backend_001.phpt new file mode 100644 index 000000000000..ed1003fd101e --- /dev/null +++ b/ext/opcache/tests/static_cache_windows_backend_001.phpt @@ -0,0 +1,60 @@ +--TEST-- +OPcache static cache uses the Win32 shared memory and lock backend +--EXTENSIONS-- +opcache +--SKIPIF-- + +--INI-- +opcache.enable=1 +opcache.enable_cli=1 +opcache.static_cache.volatile_size_mb=32 +opcache.static_cache.persistent_size_mb=32 +--FILE-- + 'volatile'])); +var_dump(OPcache\persistent_lock($key)); +OPcache\persistent_store($key, ['backend' => 'persistent']); + +var_dump(OPcache\volatile_fetch($key)); +var_dump(OPcache\persistent_fetch($key)); + +?> +--EXPECT-- +bool(true) +bool(true) +string(5) "win32" +string(5) "win32" +int(33554432) +int(33554432) +bool(true) +bool(true) +bool(true) +array(1) { + ["backend"]=> + string(8) "volatile" +} +array(1) { + ["backend"]=> + string(10) "persistent" +} diff --git a/ext/opcache/zend_accelerator_module.c b/ext/opcache/zend_accelerator_module.c index 465b15cd9576..efa7e93ed77d 100644 --- a/ext/opcache/zend_accelerator_module.c +++ b/ext/opcache/zend_accelerator_module.c @@ -20,7 +20,10 @@ #include #include "php.h" +#include "php_ini.h" + #include "ZendAccelerator.h" +#include "Zend/zend_attributes.h" #include "zend_API.h" #include "zend_closures.h" #include "zend_extensions.h" @@ -28,12 +31,13 @@ #include "zend_shared_alloc.h" #include "zend_accelerator_blacklist.h" #include "zend_file_cache.h" -#include "php_ini.h" -#include "SAPI.h" #include "zend_virtual_cwd.h" +#include "zend_static_cache.h" + +#include "SAPI.h" + #include "ext/standard/info.h" #include "ext/date/php_date.h" -#include "opcache_arginfo.h" #ifdef HAVE_JIT #include "jit/zend_jit.h" @@ -59,7 +63,7 @@ static zif_handler orig_file_exists = NULL; static zif_handler orig_is_file = NULL; static zif_handler orig_is_readable = NULL; -static bool validate_api_restriction(void) +bool zend_opcache_validate_api_restriction(void) { if (ZCG(accel_directives).restrict_api && *ZCG(accel_directives).restrict_api) { size_t len = strlen(ZCG(accel_directives).restrict_api); @@ -74,6 +78,80 @@ static bool validate_api_restriction(void) return true; } +static ZEND_INI_MH(OnUpdateStaticCacheVolatileSizeMb) +{ + zend_long *p, memsize; + + if (accel_startup_ok) { + if (strcmp(sapi_module.name, "fpm-fcgi") == 0) { + zend_accel_error(ACCEL_LOG_WARNING, "opcache.static_cache.volatile_size_mb cannot be changed when OPcache is already set up. Are you using php_admin_value[opcache.static_cache.volatile_size_mb] in an individual pool's configuration?\n"); + } else { + zend_accel_error(ACCEL_LOG_WARNING, "opcache.static_cache.volatile_size_mb cannot be changed when OPcache is already set up.\n"); + } + return FAILURE; + } + + p = ZEND_INI_GET_ADDR(); + memsize = atoi(ZSTR_VAL(new_value)); + + /* zero disables the volatile cache */ + if (memsize == 0) { + *p = 0; + return SUCCESS; + } + + /* sanity check we must use at least 8 MB */ + if (memsize < 8) { + zend_accel_error(ACCEL_LOG_WARNING, "opcache.static_cache.volatile_size_mb is set below the required 8MB.\n"); + return FAILURE; + } + + if (UNEXPECTED(memsize > ZEND_LONG_MAX / (1024 * 1024))) { + *p = ZEND_LONG_MAX & ~(1024 * 1024 - 1); + } else { + *p = memsize * (1024 * 1024); + } + + return SUCCESS; +} + +static ZEND_INI_MH(OnUpdateStaticCachePersistentSizeMb) +{ + zend_long *p, memsize; + + if (accel_startup_ok) { + if (strcmp(sapi_module.name, "fpm-fcgi") == 0) { + zend_accel_error(ACCEL_LOG_WARNING, "opcache.static_cache.persistent_size_mb cannot be changed when OPcache is already set up. Are you using php_admin_value[opcache.static_cache.persistent_size_mb] in an individual pool's configuration?\n"); + } else { + zend_accel_error(ACCEL_LOG_WARNING, "opcache.static_cache.persistent_size_mb cannot be changed when OPcache is already set up.\n"); + } + return FAILURE; + } + + p = ZEND_INI_GET_ADDR(); + memsize = atoi(ZSTR_VAL(new_value)); + + /* zero disables the persistent cache */ + if (memsize == 0) { + *p = 0; + return SUCCESS; + } + + /* sanity check we must use at least 8 MB */ + if (memsize < 8) { + zend_accel_error(ACCEL_LOG_WARNING, "opcache.static_cache.persistent_size_mb is set below the required 8MB.\n"); + return FAILURE; + } + + if (UNEXPECTED(memsize > ZEND_LONG_MAX / (1024 * 1024))) { + *p = ZEND_LONG_MAX & ~(1024 * 1024 - 1); + } else { + *p = memsize * (1024 * 1024); + } + + return SUCCESS; +} + static ZEND_INI_MH(OnUpdateMemoryConsumption) { if (accel_startup_ok) { @@ -292,6 +370,8 @@ ZEND_INI_BEGIN() STD_PHP_INI_ENTRY("opcache.log_verbosity_level" , "1" , PHP_INI_SYSTEM, OnUpdateLong, accel_directives.log_verbosity_level, zend_accel_globals, accel_globals) STD_PHP_INI_ENTRY("opcache.memory_consumption" , "128" , PHP_INI_SYSTEM, OnUpdateMemoryConsumption, accel_directives.memory_consumption, zend_accel_globals, accel_globals) + STD_PHP_INI_ENTRY("opcache.static_cache.volatile_size_mb", "0", PHP_INI_SYSTEM, OnUpdateStaticCacheVolatileSizeMb, accel_directives.static_cache_volatile_size_mb, zend_accel_globals, accel_globals) + STD_PHP_INI_ENTRY("opcache.static_cache.persistent_size_mb", "0", PHP_INI_SYSTEM, OnUpdateStaticCachePersistentSizeMb, accel_directives.static_cache_persistent_size_mb, zend_accel_globals, accel_globals) STD_PHP_INI_ENTRY("opcache.interned_strings_buffer", "8" , PHP_INI_SYSTEM, OnUpdateInternedStringsBuffer, accel_directives.interned_strings_buffer, zend_accel_globals, accel_globals) STD_PHP_INI_ENTRY("opcache.max_accelerated_files" , "10000", PHP_INI_SYSTEM, OnUpdateMaxAcceleratedFiles, accel_directives.max_accelerated_files, zend_accel_globals, accel_globals) STD_PHP_INI_ENTRY("opcache.max_wasted_percentage" , "5" , PHP_INI_SYSTEM, OnUpdateMaxWastedPercentage, accel_directives.max_wasted_percentage, zend_accel_globals, accel_globals) @@ -443,11 +523,21 @@ static ZEND_NAMED_FUNCTION(accel_is_readable) static ZEND_MINIT_FUNCTION(zend_accelerator) { + if (zend_opcache_register_functions(type) == FAILURE) { + return FAILURE; + } + start_accel_extension(); + zend_opcache_static_cache_minit(); return SUCCESS; } +static ZEND_RSHUTDOWN_FUNCTION(zend_accelerator) +{ + return zend_opcache_static_cache_rshutdown(); +} + void zend_accel_register_ini_entries(void) { zend_module_entry *module = zend_hash_str_find_ptr_lc(&module_registry, @@ -484,6 +574,7 @@ static ZEND_MSHUTDOWN_FUNCTION(zend_accelerator) { (void)type; /* keep the compiler happy */ + zend_opcache_static_cache_mshutdown(); UNREGISTER_INI_ENTRIES(); accel_shutdown(); @@ -499,6 +590,28 @@ void zend_accel_info(ZEND_MODULE_INFO_FUNC_ARGS) } else { php_info_print_table_row(2, "Opcode Caching", "Disabled"); } + + if (!zend_opcache_static_cache_volatile_is_enabled()) { + php_info_print_table_row(2, "Volatile Static Cache", "Disabled"); + } else if (zend_opcache_static_cache_volatile_is_available()) { + php_info_print_table_row(2, "Volatile Static Cache", "Enabled"); + } else { + php_info_print_table_row(2, "Volatile Static Cache", "Unavailable"); + if (zend_opcache_static_cache_volatile_failure_reason()) { + php_info_print_table_row(2, "Volatile Static Cache Failure", zend_opcache_static_cache_volatile_failure_reason()); + } + } + if (!zend_opcache_static_cache_persistent_is_enabled()) { + php_info_print_table_row(2, "Persistent Static Cache", "Disabled"); + } else if (zend_opcache_static_cache_persistent_is_available()) { + php_info_print_table_row(2, "Persistent Static Cache", "Enabled"); + } else { + php_info_print_table_row(2, "Persistent Static Cache", "Unavailable"); + if (zend_opcache_static_cache_persistent_failure_reason()) { + php_info_print_table_row(2, "Persistent Static Cache Failure", zend_opcache_static_cache_persistent_failure_reason()); + } + } + if (ZCG(enabled) && accel_startup_ok && ZCG(accel_directives).optimization_level) { php_info_print_table_row(2, "Optimization", "Enabled"); } else { @@ -602,11 +715,11 @@ void zend_accel_info(ZEND_MODULE_INFO_FUNC_ARGS) zend_module_entry opcache_module_entry = { STANDARD_MODULE_HEADER, ACCELERATOR_PRODUCT_NAME, - ext_functions, + NULL, ZEND_MINIT(zend_accelerator), ZEND_MSHUTDOWN(zend_accelerator), ZEND_RINIT(zend_accelerator), - NULL, + ZEND_RSHUTDOWN(zend_accelerator), zend_accel_info, PHP_VERSION, NO_MODULE_GLOBALS, @@ -665,14 +778,14 @@ static int accelerator_get_scripts(zval *return_value) ZEND_FUNCTION(opcache_get_status) { zend_long reqs; - zval memory_usage, statistics, scripts; + zval memory_usage, statistics, scripts, volatile_cache, persistent_cache; bool fetch_scripts = true; if (zend_parse_parameters(ZEND_NUM_ARGS(), "|b", &fetch_scripts) == FAILURE) { RETURN_THROWS(); } - if (!validate_api_restriction()) { + if (!zend_opcache_validate_api_restriction()) { RETURN_FALSE; } @@ -688,6 +801,13 @@ ZEND_FUNCTION(opcache_get_status) if (ZCG(accel_directives).file_cache) { add_assoc_string(return_value, "file_cache", ZCG(accel_directives).file_cache); } + + /* Static cache */ + zend_opcache_static_cache_volatile_get_status(&volatile_cache); + add_assoc_zval(return_value, "volatile_cache", &volatile_cache); + zend_opcache_static_cache_persistent_get_status(&persistent_cache); + add_assoc_zval(return_value, "persistent_cache", &persistent_cache); + if (file_cache_only) { add_assoc_bool(return_value, "file_cache_only", 1); return; @@ -801,7 +921,7 @@ ZEND_FUNCTION(opcache_get_configuration) ZEND_PARSE_PARAMETERS_NONE(); - if (!validate_api_restriction()) { + if (!zend_opcache_validate_api_restriction()) { RETURN_FALSE; } @@ -822,6 +942,8 @@ ZEND_FUNCTION(opcache_get_configuration) add_assoc_long(&directives, "opcache.log_verbosity_level", ZCG(accel_directives).log_verbosity_level); add_assoc_long(&directives, "opcache.memory_consumption", ZCG(accel_directives).memory_consumption); + add_assoc_long(&directives, "opcache.static_cache.volatile_size_mb", ZCG(accel_directives).static_cache_volatile_size_mb); + add_assoc_long(&directives, "opcache.static_cache.persistent_size_mb", ZCG(accel_directives).static_cache_persistent_size_mb); add_assoc_long(&directives, "opcache.interned_strings_buffer",ZCG(accel_directives).interned_strings_buffer); add_assoc_long(&directives, "opcache.max_accelerated_files", ZCG(accel_directives).max_accelerated_files); add_assoc_double(&directives, "opcache.max_wasted_percentage", ZCG(accel_directives).max_wasted_percentage); @@ -906,7 +1028,7 @@ ZEND_FUNCTION(opcache_reset) { ZEND_PARSE_PARAMETERS_NONE(); - if (!validate_api_restriction()) { + if (!zend_opcache_validate_api_restriction()) { RETURN_FALSE; } @@ -918,6 +1040,8 @@ ZEND_FUNCTION(opcache_reset) RETURN_FALSE; } + zend_opcache_static_cache_invalidate_all(); + /* exclusive lock */ zend_shared_alloc_lock(); zend_accel_schedule_restart(ACCEL_RESTART_USER); @@ -935,7 +1059,7 @@ ZEND_FUNCTION(opcache_invalidate) RETURN_THROWS(); } - if (!validate_api_restriction()) { + if (!zend_opcache_validate_api_restriction()) { RETURN_FALSE; } @@ -1017,7 +1141,7 @@ ZEND_FUNCTION(opcache_is_script_cached) Z_PARAM_STR(script_name) ZEND_PARSE_PARAMETERS_END(); - if (!validate_api_restriction()) { + if (!zend_opcache_validate_api_restriction()) { RETURN_FALSE; } @@ -1037,7 +1161,7 @@ ZEND_FUNCTION(opcache_is_script_cached_in_file_cache) Z_PARAM_STR(script_name) ZEND_PARSE_PARAMETERS_END(); - if (!validate_api_restriction()) { + if (!zend_opcache_validate_api_restriction()) { RETURN_FALSE; } diff --git a/ext/opcache/zend_accelerator_module.h b/ext/opcache/zend_accelerator_module.h index cd46e07d1d27..e6c062dd138d 100644 --- a/ext/opcache/zend_accelerator_module.h +++ b/ext/opcache/zend_accelerator_module.h @@ -29,4 +29,6 @@ void zend_accel_register_ini_entries(void); void zend_accel_override_file_functions(void); +bool zend_opcache_validate_api_restriction(void); + #endif /* _ZEND_ACCELERATOR_MODULE_H */ diff --git a/ext/opcache/zend_opcache_serializer.h b/ext/opcache/zend_opcache_serializer.h new file mode 100644 index 000000000000..2b05285f3751 --- /dev/null +++ b/ext/opcache/zend_opcache_serializer.h @@ -0,0 +1,2053 @@ +/* + +----------------------------------------------------------------------+ + | Zend OPcache | + +----------------------------------------------------------------------+ + | Copyright © The PHP Group and Contributors. | + +----------------------------------------------------------------------+ + | This source file is subject to the Modified BSD License that is | + | bundled with this package in the file LICENSE, and is available | + | through the World Wide Web at . | + | | + | SPDX-License-Identifier: BSD-3-Clause | + +----------------------------------------------------------------------+ + | Author: Go Kudo | + +----------------------------------------------------------------------+ +*/ + +#ifndef ZEND_OPCACHE_SERIALIZER_H +#define ZEND_OPCACHE_SERIALIZER_H + +#include + +#include "Zend/zend_attributes.h" +#include "Zend/zend_closures.h" + +#define ZEND_OPCACHE_SERIALIZER_ALIGN8(size) (((size) + 7UL) & ~7UL) + +#if defined(_MSC_VER) +# define ZEND_OPCACHE_SERIALIZER_PREFETCH_R(ptr) ((void) 0) +#else +# define ZEND_OPCACHE_SERIALIZER_PREFETCH_R(ptr) __builtin_prefetch((ptr), 0, 3) +#endif + +#define ZEND_OPCACHE_SERIALIZER_MAX_DEPTH 128 + +#define ZEND_OPCACHE_SERIALIZER_TYPE_UNDEF 0 +#define ZEND_OPCACHE_SERIALIZER_TYPE_NULL 1 +#define ZEND_OPCACHE_SERIALIZER_TYPE_FALSE 2 +#define ZEND_OPCACHE_SERIALIZER_TYPE_TRUE 3 +#define ZEND_OPCACHE_SERIALIZER_TYPE_LONG 4 +#define ZEND_OPCACHE_SERIALIZER_TYPE_DOUBLE 5 +#define ZEND_OPCACHE_SERIALIZER_TYPE_STRING 6 +#define ZEND_OPCACHE_SERIALIZER_TYPE_ARRAY 7 +#define ZEND_OPCACHE_SERIALIZER_TYPE_OBJECT 8 + +#define ZEND_OPCACHE_SERIALIZER_ARRAY_MAP 0x00 +#define ZEND_OPCACHE_SERIALIZER_ARRAY_PACKED 0x01 + +#define ZEND_OPCACHE_STATIC_CACHE_DIRECT_CACHE_SAFE_ATTRIBUTE "opcache\\__directcachesafe" + +#define ZEND_OPCACHE_SERIALIZER_OBJ_SERIALIZE 0x01 +#define ZEND_OPCACHE_SERIALIZER_OBJ_PROPS 0x02 +#define ZEND_OPCACHE_SERIALIZER_OBJ_PHP_SER 0x04 +#define ZEND_OPCACHE_SERIALIZER_OBJ_SAFE_DIRECT 0x08 + +typedef struct _zend_opcache_serializer_hdr_t { + uint8_t type; + uint8_t flags; + uint16_t _pad; + uint32_t data_size; +} zend_opcache_serializer_hdr_t; + +typedef struct _zend_opcache_serializer_wbuf_t { + unsigned char *data; + size_t len; + size_t cap; + uint32_t depth; + bool failed_unstorable; + HashTable visited_arrays; + HashTable seen_objects; +} zend_opcache_serializer_wbuf_t; + +typedef void (*zend_opcache_serializer_capture_func_t)(zval *value); + +typedef struct _zend_opcache_serializer_rbuf_t { + const unsigned char *data; + zend_opcache_serializer_capture_func_t capture_decoded_value; + zend_opcache_serializer_capture_func_t capture_decoded_reachable_value; + size_t len; + size_t pos; + uint32_t depth; + uint32_t capture_suppression; + bool skip_decoded_value_capture; +} zend_opcache_serializer_rbuf_t; + +static zend_always_inline void zend_opcache_serializer_capture_decoded_value(zend_opcache_serializer_rbuf_t *rb, zval *value) +{ + if (rb->capture_decoded_value != NULL) { + rb->capture_decoded_value(value); + } +} + +static zend_always_inline void zend_opcache_serializer_capture_decoded_reachable_value(zend_opcache_serializer_rbuf_t *rb, zval *value) +{ + if (rb->capture_decoded_reachable_value != NULL) { + rb->capture_decoded_reachable_value(value); + } +} + +static zend_always_inline bool zend_opcache_serializer_try_u32(size_t value, uint32_t *out) +{ + if (value > UINT32_MAX) { + return false; + } + + *out = (uint32_t) value; + + return true; +} + +static zend_always_inline void zend_opcache_serializer_wbuf_init(zend_opcache_serializer_wbuf_t *wb, size_t initial_cap) +{ + wb->data = emalloc(initial_cap); + wb->len = 0; + wb->cap = initial_cap; + wb->depth = 0; + wb->failed_unstorable = false; + + zend_hash_init(&wb->visited_arrays, 8, NULL, NULL, 0); + zend_hash_init(&wb->seen_objects, 8, NULL, NULL, 0); +} + +static zend_always_inline void zend_opcache_serializer_wbuf_destroy(zend_opcache_serializer_wbuf_t *wb) +{ + zend_hash_destroy(&wb->visited_arrays); + zend_hash_destroy(&wb->seen_objects); +} + +static zend_always_inline void zend_opcache_serializer_wbuf_grow(zend_opcache_serializer_wbuf_t *wb, size_t need) +{ + size_t new_cap; + + if (wb->len + need <= wb->cap) { + return; + } + + new_cap = wb->cap; + if (new_cap == 0) { + new_cap = need; + } + + while (new_cap < wb->len + need) { + new_cap = new_cap + (new_cap >> 1); + } + + wb->data = erealloc(wb->data, new_cap); + wb->cap = new_cap; +} + +static zend_always_inline void *zend_opcache_serializer_wbuf_reserve(zend_opcache_serializer_wbuf_t *wb, size_t size) +{ + size_t aligned_size; + void *ptr; + + aligned_size = ZEND_OPCACHE_SERIALIZER_ALIGN8(size); + zend_opcache_serializer_wbuf_grow(wb, aligned_size); + ptr = wb->data + wb->len; + wb->len += aligned_size; + + return ptr; +} + +static zend_always_inline void zend_opcache_serializer_wbuf_zero_padding(void *ptr, size_t size) +{ + size_t aligned_size = ZEND_OPCACHE_SERIALIZER_ALIGN8(size); + + if (aligned_size > size) { + memset((unsigned char *) ptr + size, 0, aligned_size - size); + } +} + +static zend_always_inline void zend_opcache_serializer_wbuf_write(zend_opcache_serializer_wbuf_t *wb, const void *src, size_t size) +{ + void *dst; + + dst = zend_opcache_serializer_wbuf_reserve(wb, size); + memcpy(dst, src, size); + zend_opcache_serializer_wbuf_zero_padding(dst, size); +} + +static zend_always_inline zend_opcache_serializer_hdr_t *zend_opcache_serializer_wbuf_write_hdr( + zend_opcache_serializer_wbuf_t *wb, uint8_t type, uint8_t flags, uint32_t data_size) +{ + zend_opcache_serializer_hdr_t *hdr; + + hdr = (zend_opcache_serializer_hdr_t *) zend_opcache_serializer_wbuf_reserve(wb, sizeof(zend_opcache_serializer_hdr_t)); + hdr->type = type; + hdr->flags = flags; + hdr->_pad = 0; + hdr->data_size = data_size; + + return hdr; +} + +static zend_always_inline bool zend_opcache_serializer_patch_hdr(zend_opcache_serializer_wbuf_t *wb, size_t hdr_offset) +{ + zend_opcache_serializer_hdr_t *hdr; + uint32_t data_size; + size_t payload_size; + + if (wb->len < hdr_offset + ZEND_OPCACHE_SERIALIZER_ALIGN8(sizeof(zend_opcache_serializer_hdr_t))) { + return false; + } + + payload_size = wb->len - hdr_offset - ZEND_OPCACHE_SERIALIZER_ALIGN8(sizeof(zend_opcache_serializer_hdr_t)); + if (!zend_opcache_serializer_try_u32(payload_size, &data_size)) { + return false; + } + + hdr = (zend_opcache_serializer_hdr_t *) (wb->data + hdr_offset); + hdr->data_size = data_size; + + return true; +} + +static zend_always_inline bool zend_opcache_serializer_write_bytes_entry(zend_opcache_serializer_wbuf_t *wb, + const char *bytes, uint32_t byte_len) +{ + uint32_t reserved = 0; + size_t entry_size; + unsigned char *dst; + + entry_size = sizeof(uint32_t) + sizeof(uint32_t) + (size_t) byte_len + 1; + if (entry_size > UINT32_MAX) { + return false; + } + + dst = (unsigned char *) zend_opcache_serializer_wbuf_reserve(wb, entry_size); + memcpy(dst, &byte_len, sizeof(uint32_t)); + memcpy(dst + sizeof(uint32_t), &reserved, sizeof(uint32_t)); + memcpy(dst + sizeof(uint32_t) + sizeof(uint32_t), bytes, byte_len); + dst[sizeof(uint32_t) + sizeof(uint32_t) + byte_len] = '\0'; + zend_opcache_serializer_wbuf_zero_padding(dst, entry_size); + + return true; +} + +static zend_always_inline bool zend_opcache_serializer_encode_string(zend_opcache_serializer_wbuf_t *wb, + const zend_string *str) +{ + uint32_t string_len; + size_t payload_size; + + if (!zend_opcache_serializer_try_u32(ZSTR_LEN(str), &string_len)) { + return false; + } + + payload_size = sizeof(uint32_t) + sizeof(uint32_t) + (size_t) string_len + 1; + if (payload_size > UINT32_MAX) { + return false; + } + + zend_opcache_serializer_wbuf_write_hdr(wb, ZEND_OPCACHE_SERIALIZER_TYPE_STRING, 0, (uint32_t) payload_size); + + return zend_opcache_serializer_write_bytes_entry(wb, ZSTR_VAL(str), string_len); +} + +static zend_always_inline bool zend_opcache_serializer_write_string_key_entry(zend_opcache_serializer_wbuf_t *wb, + const char *key_str, uint32_t key_len) +{ + uint32_t is_string = 1; + size_t entry_size; + unsigned char *dst; + + entry_size = 2 * sizeof(uint32_t) + (size_t) key_len + 1; + if (entry_size > UINT32_MAX) { + return false; + } + + dst = (unsigned char *) zend_opcache_serializer_wbuf_reserve(wb, entry_size); + memcpy(dst, &is_string, sizeof(uint32_t)); + memcpy(dst + sizeof(uint32_t), &key_len, sizeof(uint32_t)); + memcpy(dst + 2 * sizeof(uint32_t), key_str, key_len); + dst[2 * sizeof(uint32_t) + key_len] = '\0'; + zend_opcache_serializer_wbuf_zero_padding(dst, entry_size); + + return true; +} + +static zend_always_inline bool zend_opcache_serializer_enter_array(zend_opcache_serializer_wbuf_t *wb, const HashTable *ht) +{ + zend_ulong key; + zval tmp; + + key = (zend_ulong) (uintptr_t) ht; + if (zend_hash_index_exists(&wb->visited_arrays, key)) { + return false; + } + + ZVAL_NULL(&tmp); + zend_hash_index_add_new(&wb->visited_arrays, key, &tmp); + + return true; +} + +static zend_always_inline void zend_opcache_serializer_leave_array(zend_opcache_serializer_wbuf_t *wb, const HashTable *ht) +{ + zend_hash_index_del(&wb->visited_arrays, (zend_ulong) (uintptr_t) ht); +} + +static zend_always_inline bool zend_opcache_serializer_mark_object(zend_opcache_serializer_wbuf_t *wb, const zval *zv) +{ + zend_ulong key; + zval tmp; + + key = (zend_ulong) (uintptr_t) Z_OBJ_P(zv); + if (zend_hash_index_exists(&wb->seen_objects, key)) { + return false; + } + + ZVAL_NULL(&tmp); + zend_hash_index_add_new(&wb->seen_objects, key, &tmp); + + return true; +} + +static zend_always_inline size_t zend_opcache_serializer_sleep_property_count(HashTable *sleep_ht) +{ + zval *sleep_entry; + size_t count = 0; + + ZEND_HASH_FOREACH_VAL(sleep_ht, sleep_entry) { + if (Z_TYPE_P(sleep_entry) == IS_STRING) { + count++; + } + } ZEND_HASH_FOREACH_END(); + + return count; +} + +static zend_always_inline zval *zend_opcache_serializer_find_sleep_property(HashTable *props, + zend_class_entry *ce, zend_string *sleep_name, zend_string **resolved_name_out, + bool *resolved_name_owned_out) +{ + zend_property_info *prop_info; + zend_string *mangled_name; + zval *found_val; + + found_val = zend_hash_find(props, sleep_name); + if (found_val != NULL) { + *resolved_name_out = sleep_name; + *resolved_name_owned_out = false; + + return found_val; + } + + prop_info = zend_hash_find_ptr(&ce->properties_info, sleep_name); + if (prop_info != NULL) { + if ((prop_info->flags & ZEND_ACC_PRIVATE) != 0) { + mangled_name = zend_mangle_property_name(ZSTR_VAL(prop_info->ce->name), + ZSTR_LEN(prop_info->ce->name), ZSTR_VAL(sleep_name), ZSTR_LEN(sleep_name), 0) + ; + } else if ((prop_info->flags & ZEND_ACC_PROTECTED) != 0) { + mangled_name = zend_mangle_property_name("*", 1, + ZSTR_VAL(sleep_name), ZSTR_LEN(sleep_name), 0) + ; + } else { + mangled_name = NULL; + } + + if (mangled_name != NULL) { + found_val = zend_hash_find(props, mangled_name); + if (found_val != NULL) { + *resolved_name_out = mangled_name; + *resolved_name_owned_out = true; + + return found_val; + } + + zend_string_release(mangled_name); + } + } + + mangled_name = zend_mangle_property_name(ZSTR_VAL(ce->name), ZSTR_LEN(ce->name), + ZSTR_VAL(sleep_name), ZSTR_LEN(sleep_name), 0) + ; + + found_val = zend_hash_find(props, mangled_name); + if (found_val != NULL) { + *resolved_name_out = mangled_name; + *resolved_name_owned_out = true; + + return found_val; + } + + zend_string_release(mangled_name); + + mangled_name = zend_mangle_property_name("*", 1, + ZSTR_VAL(sleep_name), ZSTR_LEN(sleep_name), 0) + ; + + found_val = zend_hash_find(props, mangled_name); + if (found_val != NULL) { + *resolved_name_out = mangled_name; + *resolved_name_owned_out = true; + + return found_val; + } + + zend_string_release(mangled_name); + + *resolved_name_out = sleep_name; + *resolved_name_owned_out = false; + + return NULL; +} + +static zend_always_inline bool zend_opcache_serializer_safe_direct_cache_has_attribute(const HashTable *attributes) +{ + return attributes != NULL && + zend_get_attribute_str(attributes, ZEND_STRL(ZEND_OPCACHE_STATIC_CACHE_DIRECT_CACHE_SAFE_ATTRIBUTE)) != NULL + ; +} + +static zend_always_inline zend_class_entry *zend_opcache_serializer_find_safe_direct_cache_base(zend_class_entry *ce) +{ + zend_class_entry *base_ce = NULL; + + if (zend_opcache_static_cache_safe_direct_copy_func(ce, &base_ce) == NULL) { + return NULL; + } + + return base_ce; +} + +static zend_always_inline bool zend_opcache_serializer_class_magic_method_changed(zend_class_entry *ce, + zend_class_entry *base_ce, const char *name, size_t name_len) +{ + zend_function *func = zend_hash_str_find_ptr(&ce->function_table, name, name_len); + zend_function *base_func = zend_hash_str_find_ptr(&base_ce->function_table, name, name_len); + + if (func == NULL) { + return base_func != NULL; + } + + if (func == base_func || func->common.scope == base_ce) { + return false; + } + + return true; +} + +static zend_always_inline bool zend_opcache_serializer_safe_direct_cache_allows_custom_serializers( + zend_class_entry *base_ce) +{ + return zend_opcache_static_cache_safe_direct_allows_custom_serializers(base_ce); +} + +static zend_always_inline bool zend_opcache_serializer_has_safe_direct_cache_overrides(zend_class_entry *ce, + zend_class_entry *base_ce) +{ + if (ce == base_ce) { + return false; + } + + if ((ce->__serialize != base_ce->__serialize || ce->__unserialize != base_ce->__unserialize) && + !zend_opcache_serializer_safe_direct_cache_allows_custom_serializers(base_ce) + ) { + return true; + } + + return zend_opcache_serializer_class_magic_method_changed(ce, base_ce, ZEND_STRL("__sleep")) || + zend_opcache_serializer_class_magic_method_changed(ce, base_ce, ZEND_STRL("__wakeup")) + ; +} + +static bool zend_opcache_serializer_value_has_unstorable_ex( + const zval *value, + HashTable *seen_arrays, + HashTable *seen_objects); + +typedef struct _zend_opcache_serializer_has_unstorable_context { + HashTable *seen_arrays; + HashTable *seen_objects; +} zend_opcache_serializer_has_unstorable_context; + +static bool zend_opcache_serializer_safe_direct_value_has_unstorable_callback( + void *context, + const zval *value) +{ + zend_opcache_serializer_has_unstorable_context *has_unstorable_context = + (zend_opcache_serializer_has_unstorable_context *) context; + + return zend_opcache_serializer_value_has_unstorable_ex( + value, + has_unstorable_context->seen_arrays, + has_unstorable_context->seen_objects + ); +} + +static bool zend_opcache_serializer_hash_has_unstorable_ex( + const HashTable *hash, + HashTable *seen_arrays, + HashTable *seen_objects) +{ + zval *element; + + if (hash == NULL) { + return false; + } + + ZEND_HASH_FOREACH_VAL((HashTable *) hash, element) { + if (Z_TYPE_P(element) == IS_INDIRECT) { + element = Z_INDIRECT_P(element); + if (Z_TYPE_P(element) == IS_UNDEF) { + continue; + } + } + + if (zend_opcache_serializer_value_has_unstorable_ex(element, seen_arrays, seen_objects)) { + return true; + } + } ZEND_HASH_FOREACH_END(); + + return false; +} + +static bool zend_opcache_serializer_safe_direct_state_has_unstorable( + const zval *value, + HashTable *seen_arrays, + HashTable *seen_objects) +{ + zend_class_entry *base_ce = NULL; + zend_opcache_static_cache_safe_direct_state_copy_func_t copy_func; + zend_opcache_static_cache_safe_direct_state_has_unstorable_func_t state_has_unstorable_func; + zend_opcache_serializer_has_unstorable_context context; + + copy_func = zend_opcache_static_cache_safe_direct_copy_func(Z_OBJCE_P(value), &base_ce); + if (copy_func == NULL || + zend_opcache_serializer_has_safe_direct_cache_overrides(Z_OBJCE_P(value), base_ce) + ) { + return false; + } + + state_has_unstorable_func = + zend_opcache_static_cache_safe_direct_state_has_unstorable_func(Z_OBJCE_P(value)); + if (state_has_unstorable_func == NULL) { + return false; + } + + context.seen_arrays = seen_arrays; + context.seen_objects = seen_objects; + + return state_has_unstorable_func( + &context, + value, + zend_opcache_serializer_safe_direct_value_has_unstorable_callback + ); +} + +static bool zend_opcache_serializer_value_has_unstorable_ex( + const zval *value, + HashTable *seen_arrays, + HashTable *seen_objects) +{ + zend_object *object; + zend_ulong key; + zval *property, *end; + + ZVAL_DEREF(value); + switch (Z_TYPE_P(value)) { + case IS_RESOURCE: + return true; + case IS_OBJECT: + if (Z_OBJCE_P(value) == zend_ce_closure) { + return true; + } + + object = Z_OBJ_P(value); + key = (zend_ulong) (uintptr_t) object; + if (zend_hash_index_exists(seen_objects, key)) { + return false; + } + zend_hash_index_add_empty_element(seen_objects, key); + + if (zend_opcache_serializer_safe_direct_state_has_unstorable(value, seen_arrays, seen_objects)) { + return true; + } + + if (object->ce->default_properties_count != 0) { + property = object->properties_table; + end = property + object->ce->default_properties_count; + do { + if (Z_TYPE_P(property) != IS_UNDEF && + zend_opcache_serializer_value_has_unstorable_ex(property, seen_arrays, seen_objects) + ) { + return true; + } + property++; + } while (property != end); + } + + return object->properties != NULL && + zend_opcache_serializer_hash_has_unstorable_ex(object->properties, seen_arrays, seen_objects); + case IS_ARRAY: + key = (zend_ulong) (uintptr_t) Z_ARR_P(value); + if (zend_hash_index_exists(seen_arrays, key)) { + return false; + } + zend_hash_index_add_empty_element(seen_arrays, key); + + return zend_opcache_serializer_hash_has_unstorable_ex(Z_ARRVAL_P(value), seen_arrays, seen_objects); + default: + return false; + } +} + +static zend_always_inline bool zend_opcache_serializer_value_has_unstorable(const zval *value) +{ + HashTable seen_arrays, seen_objects; + bool result; + + zend_hash_init(&seen_arrays, 8, NULL, NULL, 0); + zend_hash_init(&seen_objects, 8, NULL, NULL, 0); + result = zend_opcache_serializer_value_has_unstorable_ex(value, &seen_arrays, &seen_objects); + zend_hash_destroy(&seen_objects); + zend_hash_destroy(&seen_arrays); + + return result; +} + +static zend_always_inline bool zend_opcache_serializer_hash_has_unstorable(const HashTable *hash) +{ + HashTable seen_arrays, seen_objects; + bool result; + + if (hash == NULL) { + return false; + } + + zend_hash_init(&seen_arrays, 8, NULL, NULL, 0); + zend_hash_init(&seen_objects, 8, NULL, NULL, 0); + result = zend_opcache_serializer_hash_has_unstorable_ex(hash, &seen_arrays, &seen_objects); + zend_hash_destroy(&seen_objects); + zend_hash_destroy(&seen_arrays); + + return result; +} + +static zend_always_inline bool zend_opcache_serializer_encode_zval(zend_opcache_serializer_wbuf_t *wb, + const zval *zv); + +static zend_always_inline bool zend_opcache_serializer_encode_property_table(zend_opcache_serializer_wbuf_t *wb, + const HashTable *props) +{ + zend_string *prop_key; + zval *prop_val; + uint32_t prop_count, arr_meta[2] = {0, 0}, key_len; + size_t prop_count_tmp = 0, arr_hdr_offset; + + ZEND_HASH_FOREACH_STR_KEY_VAL(props, prop_key, prop_val) { + if (prop_key != NULL) { + prop_count_tmp++; + } + } ZEND_HASH_FOREACH_END(); + + if (!zend_opcache_serializer_try_u32(prop_count_tmp, &prop_count)) { + return false; + } + + arr_hdr_offset = wb->len; + zend_opcache_serializer_wbuf_write_hdr(wb, ZEND_OPCACHE_SERIALIZER_TYPE_ARRAY, + ZEND_OPCACHE_SERIALIZER_ARRAY_MAP, 0); + arr_meta[0] = prop_count; + zend_opcache_serializer_wbuf_write(wb, arr_meta, sizeof(arr_meta)); + + ZEND_HASH_FOREACH_STR_KEY_VAL(props, prop_key, prop_val) { + if (prop_key == NULL) { + continue; + } + + if (Z_TYPE_P(prop_val) == IS_INDIRECT) { + prop_val = Z_INDIRECT_P(prop_val); + } + + if (!zend_opcache_serializer_try_u32(ZSTR_LEN(prop_key), &key_len) || + !zend_opcache_serializer_write_string_key_entry(wb, ZSTR_VAL(prop_key), key_len) + ) { + return false; + } + + if (Z_TYPE_P(prop_val) == IS_UNDEF) { + zend_opcache_serializer_wbuf_write_hdr(wb, ZEND_OPCACHE_SERIALIZER_TYPE_UNDEF, 0, 0); + } else if (!zend_opcache_serializer_encode_zval(wb, prop_val)) { + return false; + } + } ZEND_HASH_FOREACH_END(); + + return zend_opcache_serializer_patch_hdr(wb, arr_hdr_offset); +} + +static zend_always_inline bool zend_opcache_serializer_update_object_property(zend_object *object, + zend_string *key, zval *prop_val) +{ + const char *class_name, *prop_name; + zend_string *cname; + zend_class_entry *ce; + size_t prop_name_len; + + if (ZSTR_LEN(key) > 0 && ZSTR_VAL(key)[0] == '\0') { + if (zend_unmangle_property_name_ex(key, &class_name, &prop_name, &prop_name_len) == SUCCESS) { + if (class_name[0] != '*') { + cname = zend_string_init(class_name, strlen(class_name), 0); + ce = zend_lookup_class(cname); + if (ce != NULL) { + zend_update_property(ce, object, prop_name, prop_name_len, prop_val); + } + + zend_string_release_ex(cname, 0); + } else { + zend_update_property(object->ce, object, prop_name, prop_name_len, prop_val); + } + } + } else { + zend_update_property(object->ce, object, ZSTR_VAL(key), ZSTR_LEN(key), prop_val); + } + + return !EG(exception); +} + +static zend_always_inline bool zend_opcache_serializer_restore_object_properties(zval *object, const HashTable *props) +{ + zend_string *prop_name; + zval *prop_val; + + ZEND_HASH_FOREACH_STR_KEY_VAL(props, prop_name, prop_val) { + if (prop_name == NULL || Z_TYPE_P(prop_val) == IS_REFERENCE) { + continue; + } + + if (!zend_opcache_serializer_update_object_property(Z_OBJ_P(object), prop_name, prop_val)) { + return false; + } + } ZEND_HASH_FOREACH_END(); + + return true; +} + +static zend_always_inline bool zend_opcache_serializer_encode_safe_direct_state_payload( + zend_opcache_serializer_wbuf_t *wb, zval *state_zv, const HashTable *props) +{ + bool has_unstorable_state, has_unstorable_props, ok; + + has_unstorable_state = zend_opcache_serializer_value_has_unstorable(state_zv); + has_unstorable_props = zend_opcache_serializer_hash_has_unstorable(props); + if (has_unstorable_state || has_unstorable_props) { + wb->failed_unstorable = true; + ok = false; + } else { + ok = zend_opcache_serializer_encode_zval(wb, state_zv) && + zend_opcache_serializer_encode_property_table(wb, props); + } + + zval_ptr_dtor(state_zv); + ZVAL_UNDEF(state_zv); + + return ok; +} + +static zend_always_inline bool zend_opcache_serializer_encode_safe_direct_registered_payload( + zend_opcache_serializer_wbuf_t *wb, const zval *zv) +{ + zend_opcache_static_cache_safe_direct_state_serialize_func_t serialize_func; + zval state_zv; + HashTable *props, empty_props; + bool ok; + + serialize_func = zend_opcache_static_cache_safe_direct_state_serialize_func(Z_OBJCE_P(zv)); + if (serialize_func == NULL) { + return false; + } + + ZVAL_UNDEF(&state_zv); + if (!serialize_func(zv, &state_zv) || Z_TYPE(state_zv) != IS_ARRAY) { + if (Z_TYPE(state_zv) != IS_UNDEF) { + zval_ptr_dtor(&state_zv); + } + + return false; + } + + if (zend_opcache_static_cache_safe_direct_state_includes_properties(Z_OBJCE_P(zv))) { + zend_hash_init(&empty_props, 0, NULL, NULL, 0); + ok = zend_opcache_serializer_encode_safe_direct_state_payload(wb, &state_zv, &empty_props); + zend_hash_destroy(&empty_props); + + return ok; + } + + props = zend_std_get_properties(Z_OBJ_P(zv)); + + return zend_opcache_serializer_encode_safe_direct_state_payload(wb, &state_zv, props); +} + +static zend_always_inline bool zend_opcache_serializer_try_encode_safe_direct_object( + zend_opcache_serializer_wbuf_t *wb, const zval *zv, size_t hdr_offset, + zend_class_entry *ce, zend_string *class_name, uint32_t name_len) +{ + zend_class_entry *base_ce = NULL; + zend_opcache_static_cache_safe_direct_state_copy_func_t copy_func; + bool ok; + + copy_func = zend_opcache_static_cache_safe_direct_copy_func(ce, &base_ce); + if (copy_func == NULL || + zend_opcache_serializer_has_safe_direct_cache_overrides(ce, base_ce) + ) { + return false; + } + + zend_opcache_serializer_wbuf_write_hdr(wb, ZEND_OPCACHE_SERIALIZER_TYPE_OBJECT, + ZEND_OPCACHE_SERIALIZER_OBJ_SAFE_DIRECT, 0); + + ok = zend_opcache_serializer_write_bytes_entry(wb, ZSTR_VAL(class_name), name_len); + if (ok) { + ok = zend_opcache_serializer_encode_safe_direct_registered_payload(wb, zv); + } + + if (!ok) { + if (EG(exception)) { + return false; + } + + wb->len = hdr_offset; + + return false; + } + + return zend_opcache_serializer_patch_hdr(wb, hdr_offset); +} + +static zend_always_inline bool zend_opcache_serializer_array_can_use_packed(const HashTable *ht) +{ + zend_string *key; + zend_ulong expected_idx = 0, h; + zval *val; + + if (!HT_IS_PACKED(ht)) { + return false; + } + + ZEND_HASH_FOREACH_KEY_VAL((HashTable *) ht, h, key, val) { + (void) val; + + if (key != NULL || h != expected_idx++) { + return false; + } + } ZEND_HASH_FOREACH_END(); + + return true; +} + +static bool zend_opcache_serializer_encode_array(zend_opcache_serializer_wbuf_t *wb, + const HashTable *ht) +{ + zend_opcache_serializer_hdr_t *hdr; + zend_ulong h; + zend_string *key; + zval *val; + uint64_t idx_value; + uint32_t count, arr_meta[2], key_len; + uint8_t flags; + size_t hdr_offset; + bool use_packed; + unsigned char key_buf[16]; + + if (!zend_opcache_serializer_try_u32(zend_hash_num_elements(ht), &count)) { + return false; + } + + if (ht->nNextFreeElement > UINT32_MAX) { + return false; + } + + if (!zend_opcache_serializer_enter_array(wb, ht)) { + return false; + } + + use_packed = zend_opcache_serializer_array_can_use_packed(ht); + flags = use_packed ? ZEND_OPCACHE_SERIALIZER_ARRAY_PACKED : ZEND_OPCACHE_SERIALIZER_ARRAY_MAP; + hdr_offset = wb->len; + hdr = zend_opcache_serializer_wbuf_write_hdr(wb, ZEND_OPCACHE_SERIALIZER_TYPE_ARRAY, flags, 0); + ZEND_ASSERT(hdr != NULL); + + arr_meta[0] = count; + arr_meta[1] = (uint32_t) ht->nNextFreeElement; + zend_opcache_serializer_wbuf_write(wb, arr_meta, sizeof(arr_meta)); + + if (use_packed) { + ZEND_HASH_PACKED_FOREACH_VAL((HashTable *) ht, val) { + if (!zend_opcache_serializer_encode_zval(wb, val)) { + zend_opcache_serializer_leave_array(wb, ht); + + return false; + } + } ZEND_HASH_FOREACH_END(); + } else { + ZEND_HASH_FOREACH_KEY_VAL((HashTable *) ht, h, key, val) { + if (key != NULL) { + if (!zend_opcache_serializer_try_u32(ZSTR_LEN(key), &key_len) || + !zend_opcache_serializer_write_string_key_entry(wb, ZSTR_VAL(key), key_len) + ) { + zend_opcache_serializer_leave_array(wb, ht); + + return false; + } + } else { + memset(key_buf, 0, sizeof(key_buf)); + idx_value = (uint64_t) h; + memcpy(key_buf + 8, &idx_value, sizeof(uint64_t)); + + zend_opcache_serializer_wbuf_write(wb, key_buf, sizeof(key_buf)); + } + + if (!zend_opcache_serializer_encode_zval(wb, val)) { + zend_opcache_serializer_leave_array(wb, ht); + + return false; + } + } ZEND_HASH_FOREACH_END(); + } + + zend_opcache_serializer_leave_array(wb, ht); + + return zend_opcache_serializer_patch_hdr(wb, hdr_offset); +} + +static bool zend_opcache_serializer_encode_object(zend_opcache_serializer_wbuf_t *wb, + const zval *zv) +{ + zend_class_entry *ce; + zend_string *class_name, *resolved_name, *prop_key; + zval retval, sleep_rv, func_name, *sleep_entry, *found_val, *prop_val; + php_serialize_data_t var_hash; + HashTable *props, *sleep_ht; + smart_str ser = {0}; + uint32_t name_len, prop_count, arr_meta[2] = {0, 0}, key_len, ser_len; + size_t hdr_offset, arr_hdr_offset, property_count; + bool has_sleep, resolved_name_owned; + int call_ret; + + ce = Z_OBJCE_P(zv); + + class_name = ce->name; + if (!zend_opcache_serializer_try_u32(ZSTR_LEN(class_name), &name_len)) { + return false; + } + + if (!zend_opcache_serializer_mark_object(wb, zv)) { + return false; + } + + hdr_offset = wb->len; + ZVAL_UNDEF(&retval); + if (zend_opcache_serializer_try_encode_safe_direct_object(wb, zv, hdr_offset, ce, class_name, name_len)) { + return true; + } + + if (EG(exception)) { + return false; + } + + if (wb->failed_unstorable) { + wb->len = hdr_offset; + + return false; + } + + if (ce->__serialize != NULL) { + zend_call_known_instance_method_with_0_params(ce->__serialize, Z_OBJ_P(zv), &retval); + if (EG(exception)) { + if (Z_TYPE(retval) != IS_UNDEF) { + zval_ptr_dtor(&retval); + } + + return false; + } + + if (Z_TYPE(retval) == IS_ARRAY) { + if (zend_opcache_serializer_value_has_unstorable(&retval)) { + wb->failed_unstorable = true; + zval_ptr_dtor(&retval); + wb->len = hdr_offset; + + return false; + } + + zend_opcache_serializer_wbuf_write_hdr(wb, ZEND_OPCACHE_SERIALIZER_TYPE_OBJECT, + ZEND_OPCACHE_SERIALIZER_OBJ_SERIALIZE, 0); + if (!zend_opcache_serializer_write_bytes_entry(wb, ZSTR_VAL(class_name), name_len) || + !zend_opcache_serializer_encode_array(wb, Z_ARRVAL(retval)) + ) { + zval_ptr_dtor(&retval); + + if (EG(exception)) { + return false; + } + + if (wb->failed_unstorable) { + wb->len = hdr_offset; + + return false; + } + + wb->len = hdr_offset; + + goto fallback_php; + } + + zval_ptr_dtor(&retval); + + return zend_opcache_serializer_patch_hdr(wb, hdr_offset); + } + + if (Z_TYPE(retval) != IS_UNDEF) { + zval_ptr_dtor(&retval); + } + + wb->len = hdr_offset; + + goto fallback_php; + } + + props = NULL; + sleep_ht = NULL; + has_sleep = false; + ZVAL_UNDEF(&sleep_rv); + + if (zend_hash_str_exists(&ce->function_table, "__sleep", sizeof("__sleep") - 1)) { + ZVAL_STRINGL(&func_name, "__sleep", sizeof("__sleep") - 1); + call_ret = call_user_function(CG(function_table), (zval *) zv, &func_name, &sleep_rv, 0, NULL); + zval_ptr_dtor_str(&func_name); + + if (EG(exception)) { + if (Z_TYPE(sleep_rv) != IS_UNDEF) { + zval_ptr_dtor(&sleep_rv); + } + + return false; + } + + if (call_ret == SUCCESS && Z_TYPE(sleep_rv) == IS_ARRAY) { + has_sleep = true; + sleep_ht = Z_ARRVAL(sleep_rv); + } else if (Z_TYPE(sleep_rv) != IS_UNDEF) { + zval_ptr_dtor(&sleep_rv); + ZVAL_UNDEF(&sleep_rv); + } + } + + props = zend_get_properties_for((zval *) zv, ZEND_PROP_PURPOSE_SERIALIZE); + if (props != NULL) { + property_count = has_sleep + ? zend_opcache_serializer_sleep_property_count(sleep_ht) + : zend_hash_num_elements(props) + ; + if (!zend_opcache_serializer_try_u32(property_count, &prop_count)) { + zend_release_properties(props); + + if (has_sleep) { + zval_ptr_dtor(&sleep_rv); + } + + return false; + } + + if (!has_sleep && zend_opcache_serializer_hash_has_unstorable(props)) { + wb->failed_unstorable = true; + zend_release_properties(props); + wb->len = hdr_offset; + + return false; + } + + zend_opcache_serializer_wbuf_write_hdr(wb, ZEND_OPCACHE_SERIALIZER_TYPE_OBJECT, + ZEND_OPCACHE_SERIALIZER_OBJ_PROPS, 0); + if (!zend_opcache_serializer_write_bytes_entry(wb, ZSTR_VAL(class_name), name_len)) { + zend_release_properties(props); + + if (has_sleep) { + zval_ptr_dtor(&sleep_rv); + } + + return false; + } + + arr_hdr_offset = wb->len; + zend_opcache_serializer_wbuf_write_hdr(wb, ZEND_OPCACHE_SERIALIZER_TYPE_ARRAY, + ZEND_OPCACHE_SERIALIZER_ARRAY_MAP, 0 + ); + arr_meta[0] = prop_count; + zend_opcache_serializer_wbuf_write(wb, arr_meta, sizeof(arr_meta)); + + if (has_sleep) { + ZEND_HASH_FOREACH_VAL(sleep_ht, sleep_entry) { + if (Z_TYPE_P(sleep_entry) != IS_STRING) { + continue; + } + + resolved_name = Z_STR_P(sleep_entry); + resolved_name_owned = false; + found_val = zend_opcache_serializer_find_sleep_property(props, ce, + Z_STR_P(sleep_entry), &resolved_name, &resolved_name_owned) + ; + + if (!zend_opcache_serializer_try_u32(ZSTR_LEN(resolved_name), &key_len) || + !zend_opcache_serializer_write_string_key_entry(wb, ZSTR_VAL(resolved_name), key_len) + ) { + if (resolved_name_owned) { + zend_string_release(resolved_name); + } + + zend_release_properties(props); + zval_ptr_dtor(&sleep_rv); + + return false; + } + + if (resolved_name_owned) { + zend_string_release(resolved_name); + } + + if (found_val != NULL && Z_TYPE_P(found_val) == IS_INDIRECT) { + found_val = Z_INDIRECT_P(found_val); + } + + if (found_val != NULL && + Z_TYPE_P(found_val) != IS_UNDEF && + zend_opcache_serializer_value_has_unstorable(found_val) + ) { + wb->failed_unstorable = true; + zend_release_properties(props); + zval_ptr_dtor(&sleep_rv); + wb->len = hdr_offset; + + return false; + } + + if (found_val == NULL || Z_TYPE_P(found_val) == IS_UNDEF) { + zend_opcache_serializer_wbuf_write_hdr(wb, ZEND_OPCACHE_SERIALIZER_TYPE_UNDEF, 0, 0); + } else if (!zend_opcache_serializer_encode_zval(wb, found_val)) { + zend_release_properties(props); + zval_ptr_dtor(&sleep_rv); + + if (EG(exception)) { + return false; + } + + if (wb->failed_unstorable) { + wb->len = hdr_offset; + + return false; + } + + wb->len = hdr_offset; + + goto fallback_php; + } + } ZEND_HASH_FOREACH_END(); + } else { + ZEND_HASH_FOREACH_STR_KEY_VAL(props, prop_key, prop_val) { + if (prop_key == NULL) { + continue; + } + + if (Z_TYPE_P(prop_val) == IS_INDIRECT) { + prop_val = Z_INDIRECT_P(prop_val); + } + + if (!zend_opcache_serializer_try_u32(ZSTR_LEN(prop_key), &key_len) || + !zend_opcache_serializer_write_string_key_entry(wb, ZSTR_VAL(prop_key), key_len)) { + zend_release_properties(props); + + return false; + } + + if (Z_TYPE_P(prop_val) == IS_UNDEF) { + zend_opcache_serializer_wbuf_write_hdr(wb, ZEND_OPCACHE_SERIALIZER_TYPE_UNDEF, 0, 0); + } else if (!zend_opcache_serializer_encode_zval(wb, prop_val)) { + zend_release_properties(props); + + if (EG(exception)) { + return false; + } + + if (wb->failed_unstorable) { + wb->len = hdr_offset; + + return false; + } + + wb->len = hdr_offset; + + goto fallback_php; + } + } ZEND_HASH_FOREACH_END(); + + zend_release_properties(props); + + if (!zend_opcache_serializer_patch_hdr(wb, arr_hdr_offset)) { + return false; + } + + return zend_opcache_serializer_patch_hdr(wb, hdr_offset); + } + + if (has_sleep) { + zval_ptr_dtor(&sleep_rv); + } + + zend_release_properties(props); + + if (!zend_opcache_serializer_patch_hdr(wb, arr_hdr_offset)) { + return false; + } + + return zend_opcache_serializer_patch_hdr(wb, hdr_offset); + } + +fallback_php: + if (zend_opcache_serializer_value_has_unstorable(zv)) { + wb->failed_unstorable = true; + + return false; + } + + PHP_VAR_SERIALIZE_INIT(var_hash); + php_var_serialize(&ser, (zval *) zv, &var_hash); + PHP_VAR_SERIALIZE_DESTROY(var_hash); + if (ser.s == NULL) { + return false; + } + + if (!zend_opcache_serializer_try_u32(ZSTR_LEN(ser.s), &ser_len)) { + smart_str_free(&ser); + + return false; + } + + zend_opcache_serializer_wbuf_write_hdr(wb, ZEND_OPCACHE_SERIALIZER_TYPE_OBJECT, + ZEND_OPCACHE_SERIALIZER_OBJ_PHP_SER, 0); + + if (!zend_opcache_serializer_write_bytes_entry(wb, ZSTR_VAL(class_name), name_len) || + !zend_opcache_serializer_write_bytes_entry(wb, ZSTR_VAL(ser.s), ser_len) + ) { + smart_str_free(&ser); + + return false; + } + + smart_str_free(&ser); + + return zend_opcache_serializer_patch_hdr(wb, hdr_offset); +} + +static zend_always_inline bool zend_opcache_serializer_encode_zval(zend_opcache_serializer_wbuf_t *wb, const zval *zv) +{ + int64_t lval; + double dval; + + if (wb->depth >= ZEND_OPCACHE_SERIALIZER_MAX_DEPTH) { + return false; + } + + wb->depth++; + + switch (Z_TYPE_P(zv)) { + case IS_UNDEF: + zend_opcache_serializer_wbuf_write_hdr(wb, ZEND_OPCACHE_SERIALIZER_TYPE_UNDEF, 0, 0); + break; + case IS_NULL: + zend_opcache_serializer_wbuf_write_hdr(wb, ZEND_OPCACHE_SERIALIZER_TYPE_NULL, 0, 0); + break; + case IS_FALSE: + zend_opcache_serializer_wbuf_write_hdr(wb, ZEND_OPCACHE_SERIALIZER_TYPE_FALSE, 0, 0); + break; + case IS_TRUE: + zend_opcache_serializer_wbuf_write_hdr(wb, ZEND_OPCACHE_SERIALIZER_TYPE_TRUE, 0, 0); + break; + case IS_LONG: + zend_opcache_serializer_wbuf_write_hdr(wb, ZEND_OPCACHE_SERIALIZER_TYPE_LONG, 0, sizeof(int64_t)); + lval = (int64_t) Z_LVAL_P(zv); + zend_opcache_serializer_wbuf_write(wb, &lval, sizeof(int64_t)); + + break; + case IS_DOUBLE: + zend_opcache_serializer_wbuf_write_hdr(wb, ZEND_OPCACHE_SERIALIZER_TYPE_DOUBLE, 0, sizeof(double)); + dval = Z_DVAL_P(zv); + zend_opcache_serializer_wbuf_write(wb, &dval, sizeof(double)); + + break; + case IS_STRING: + if (!zend_opcache_serializer_encode_string(wb, Z_STR_P(zv))) { + wb->depth--; + + return false; + } + + break; + case IS_ARRAY: + if (!zend_opcache_serializer_encode_array(wb, Z_ARRVAL_P(zv))) { + wb->depth--; + + return false; + } + + break; + case IS_OBJECT: + if (!zend_opcache_serializer_encode_object(wb, zv)) { + wb->depth--; + + return false; + } + + break; + case IS_RESOURCE: + wb->failed_unstorable = true; + wb->depth--; + + return false; + default: + wb->depth--; + + return false; + } + + wb->depth--; + + return true; +} + +static zend_always_inline bool zend_opcache_serialize_ex( + unsigned char **buf, + size_t *buf_len, + const zval *value, + bool *failed_unstorable) +{ + zend_opcache_serializer_wbuf_t wb; + bool ok; + + zend_opcache_serializer_wbuf_init(&wb, 256); + ok = zend_opcache_serializer_encode_zval(&wb, value); + if (failed_unstorable != NULL) { + *failed_unstorable = wb.failed_unstorable; + } + zend_opcache_serializer_wbuf_destroy(&wb); + + if (!ok) { + efree(wb.data); + *buf = NULL; + *buf_len = 0; + + return false; + } + + *buf = wb.data; + *buf_len = wb.len; + + return true; +} + +static zend_always_inline bool zend_opcache_serialize(unsigned char **buf, size_t *buf_len, + const zval *value) +{ + return zend_opcache_serialize_ex(buf, buf_len, value, NULL); +} + +static zend_always_inline bool zend_opcache_serializer_rbuf_check(const zend_opcache_serializer_rbuf_t *rb, size_t need) +{ + return rb->pos + need <= rb->len; +} + +static zend_always_inline const void *zend_opcache_serializer_rbuf_read(zend_opcache_serializer_rbuf_t *rb, size_t size) +{ + const void *ptr; + size_t aligned_size; + + aligned_size = ZEND_OPCACHE_SERIALIZER_ALIGN8(size); + if (!zend_opcache_serializer_rbuf_check(rb, aligned_size)) { + return NULL; + } + + ptr = rb->data + rb->pos; + rb->pos += aligned_size; + + return ptr; +} + +static zend_always_inline bool zend_opcache_serializer_rbuf_read_copy( + zend_opcache_serializer_rbuf_t *rb, void *dst, size_t size) +{ + const void *src; + + src = zend_opcache_serializer_rbuf_read(rb, size); + if (src == NULL) { + return false; + } + + memcpy(dst, src, size); + + return true; +} + +static zend_always_inline bool zend_opcache_serializer_rbuf_read_hdr( + zend_opcache_serializer_rbuf_t *rb, zend_opcache_serializer_hdr_t *hdr) +{ + return zend_opcache_serializer_rbuf_read_copy(rb, hdr, sizeof(*hdr)); +} + +static zend_always_inline bool zend_opcache_serializer_rbuf_skip(zend_opcache_serializer_rbuf_t *rb, size_t size) +{ + size_t aligned_size; + + aligned_size = ZEND_OPCACHE_SERIALIZER_ALIGN8(size); + if (!zend_opcache_serializer_rbuf_check(rb, aligned_size)) { + return false; + } + + rb->pos += aligned_size; + + return true; +} + +static zend_always_inline bool zend_opcache_serializer_decode_zval(zend_opcache_serializer_rbuf_t *rb, + zval *dst); + +static inline bool zend_opcache_serializer_decode_zval_suppressed(zend_opcache_serializer_rbuf_t *rb, zval *dst) +{ + bool result; + + rb->capture_suppression++; + result = zend_opcache_serializer_decode_zval(rb, dst); + rb->capture_suppression--; + + return result; +} + +static zend_always_inline bool zend_opcache_serializer_decode_string_to_zval(zend_opcache_serializer_rbuf_t *rb, + zval *dst, const zend_opcache_serializer_hdr_t *hdr) +{ + const unsigned char *payload; + uint32_t string_len; + + if (hdr->data_size < 9) { + return false; + } + + payload = (const unsigned char *) zend_opcache_serializer_rbuf_read(rb, hdr->data_size); + if (payload == NULL) { + return false; + } + + memcpy(&string_len, payload, sizeof(uint32_t)); + if ((size_t) string_len + 9 > hdr->data_size) { + return false; + } + + ZVAL_STRINGL(dst, (const char *) (payload + 8), string_len); + + return true; +} + +static zend_always_inline bool zend_opcache_serializer_read_object_class_name(zend_opcache_serializer_rbuf_t *rb, + const char **name_str_out, uint32_t *name_len_out) +{ + const unsigned char *name_data; + const char *name_str; + uint32_t name_len; + + name_data = (const unsigned char *) zend_opcache_serializer_rbuf_read(rb, 8); + if (name_data == NULL) { + return false; + } + + memcpy(&name_len, name_data, sizeof(uint32_t)); + name_str = (const char *) zend_opcache_serializer_rbuf_read(rb, (size_t) name_len + 1); + if (name_str == NULL) { + return false; + } + + *name_str_out = name_str; + *name_len_out = name_len; + + return true; +} + +static zend_always_inline bool zend_opcache_serializer_decode_object_payload_with_ce( + zend_opcache_serializer_rbuf_t *rb, zval *dst, uint8_t obj_flags, zend_class_entry *ce) +{ + const unsigned char *arr_meta, *key_data; + const char *key_str; + zend_opcache_serializer_hdr_t arr_hdr; + zend_class_entry *base_ce = NULL; + zend_property_info *prop_info; + zend_opcache_static_cache_safe_direct_state_copy_func_t copy_func; + zend_opcache_static_cache_safe_direct_state_unserialize_func_t unserialize_func; + zval props_zv, state_zv, data_arr, + val, *existing, *target, func_name, wakeup_rv; + HashTable *obj_ht; + uint32_t prop_count, index, key_len; + uint8_t key_type; + bool ok; + + if ((obj_flags & ZEND_OPCACHE_SERIALIZER_OBJ_SAFE_DIRECT) != 0) { + copy_func = zend_opcache_static_cache_safe_direct_copy_func(ce, &base_ce); + if (copy_func == NULL || + zend_opcache_serializer_has_safe_direct_cache_overrides(ce, base_ce) + ) { + ZVAL_NULL(dst); + + return false; + } + + unserialize_func = zend_opcache_static_cache_safe_direct_state_unserialize_func(ce); + if (unserialize_func == NULL) { + ZVAL_NULL(dst); + + return false; + } + + ZVAL_UNDEF(&state_zv); + ZVAL_UNDEF(&props_zv); + if (!zend_opcache_serializer_decode_zval_suppressed(rb, &state_zv) || + !zend_opcache_serializer_decode_zval_suppressed(rb, &props_zv) || + Z_TYPE(state_zv) != IS_ARRAY || + Z_TYPE(props_zv) != IS_ARRAY + ) { + if (Z_TYPE(state_zv) != IS_UNDEF) { + zval_ptr_dtor(&state_zv); + } + + if (Z_TYPE(props_zv) != IS_UNDEF) { + zval_ptr_dtor(&props_zv); + } + + ZVAL_NULL(dst); + + return false; + } + + if (object_init_ex(dst, ce) != SUCCESS) { + zval_ptr_dtor(&state_zv); + zval_ptr_dtor(&props_zv); + ZVAL_NULL(dst); + + return false; + } + + ok = unserialize_func(dst, &state_zv); + zval_ptr_dtor(&state_zv); + if (!ok || EG(exception)) { + zval_ptr_dtor(&props_zv); + zval_ptr_dtor(dst); + ZVAL_NULL(dst); + + return false; + } + + ok = zend_opcache_serializer_restore_object_properties(dst, Z_ARRVAL(props_zv)); + zval_ptr_dtor(&props_zv); + if (!ok) { + zval_ptr_dtor(dst); + ZVAL_NULL(dst); + + return false; + } + + zend_opcache_serializer_capture_decoded_reachable_value(rb, dst); + + return true; + } + + if ((obj_flags & ZEND_OPCACHE_SERIALIZER_OBJ_SERIALIZE) != 0) { + ZVAL_UNDEF(&data_arr); + if (!zend_opcache_serializer_decode_zval_suppressed(rb, &data_arr) || Z_TYPE(data_arr) != IS_ARRAY) { + if (Z_TYPE(data_arr) != IS_UNDEF) { + zval_ptr_dtor(&data_arr); + } + ZVAL_NULL(dst); + + return false; + } + + if (object_init_ex(dst, ce) != SUCCESS) { + zval_ptr_dtor(&data_arr); + ZVAL_NULL(dst); + + return false; + } + + if (ce->__unserialize != NULL) { + zend_call_known_instance_method_with_1_params(ce->__unserialize, Z_OBJ_P(dst), NULL, &data_arr); + if (EG(exception)) { + zval_ptr_dtor(&data_arr); + zval_ptr_dtor(dst); + ZVAL_NULL(dst); + + return false; + } + } + + zval_ptr_dtor(&data_arr); + zend_opcache_serializer_capture_decoded_reachable_value(rb, dst); + + return true; + } + + if ((obj_flags & ZEND_OPCACHE_SERIALIZER_OBJ_PROPS) != 0) { + if (object_init_ex(dst, ce) != SUCCESS) { + ZVAL_NULL(dst); + + return false; + } + + if (!zend_opcache_serializer_rbuf_read_hdr(rb, &arr_hdr) || arr_hdr.type != ZEND_OPCACHE_SERIALIZER_TYPE_ARRAY) { + zval_ptr_dtor(dst); + ZVAL_NULL(dst); + + return false; + } + + arr_meta = (const unsigned char *) zend_opcache_serializer_rbuf_read(rb, 8); + if (arr_meta == NULL) { + zval_ptr_dtor(dst); + ZVAL_NULL(dst); + + return false; + } + + memcpy(&prop_count, arr_meta, sizeof(uint32_t)); + obj_ht = Z_OBJ_P(dst)->handlers->get_properties(Z_OBJ_P(dst)); + if (obj_ht == NULL) { + obj_ht = zend_std_get_properties(Z_OBJ_P(dst)); + } + + for (index = 0; index < prop_count; index++) { + if (!zend_opcache_serializer_rbuf_check(rb, 8)) { + zval_ptr_dtor(dst); + ZVAL_UNDEF(dst); + + return false; + } + + key_type = rb->data[rb->pos]; + if (key_type != 1) { + zval_ptr_dtor(dst); + ZVAL_UNDEF(dst); + + return false; + } + + key_data = (const unsigned char *) zend_opcache_serializer_rbuf_read(rb, 8); + if (key_data == NULL) { + zval_ptr_dtor(dst); + ZVAL_UNDEF(dst); + + return false; + } + + memcpy(&key_len, key_data + 4, sizeof(uint32_t)); + + key_str = (const char *) zend_opcache_serializer_rbuf_read(rb, (size_t) key_len + 1); + if (key_str == NULL) { + zval_ptr_dtor(dst); + ZVAL_UNDEF(dst); + + return false; + } + + ZVAL_UNDEF(&val); + if (!zend_opcache_serializer_decode_zval(rb, &val)) { + zval_ptr_dtor(dst); + ZVAL_UNDEF(dst); + + return false; + } + + existing = zend_hash_str_find(obj_ht, key_str, key_len); + if (existing != NULL) { + target = existing; + if (Z_TYPE_P(target) == IS_INDIRECT) { + target = Z_INDIRECT_P(target); + } + + if (Z_TYPE(val) != IS_UNDEF) { + prop_info = zend_get_typed_property_info_for_slot(Z_OBJ_P(dst), target); + if (prop_info != NULL && !zend_verify_prop_assignable_by_ref(prop_info, &val, true)) { + zval_ptr_dtor(&val); + zval_ptr_dtor(dst); + ZVAL_UNDEF(dst); + + return false; + } + + zval_ptr_dtor(target); + ZVAL_COPY_VALUE(target, &val); + } else { + zval_ptr_dtor(&val); + } + } else if (Z_TYPE(val) != IS_UNDEF) { + zend_hash_str_add_new(obj_ht, key_str, key_len, &val); + } else { + zval_ptr_dtor(&val); + } + } + + if (zend_hash_str_exists(&ce->function_table, "__wakeup", sizeof("__wakeup") - 1) && + !EG(exception) + ) { + ZVAL_UNDEF(&wakeup_rv); + ZVAL_STRINGL(&func_name, "__wakeup", sizeof("__wakeup") - 1); + call_user_function(CG(function_table), dst, &func_name, &wakeup_rv, 0, NULL); + zval_ptr_dtor_str(&func_name); + + if (Z_TYPE(wakeup_rv) != IS_UNDEF) { + zval_ptr_dtor(&wakeup_rv); + } + + if (EG(exception)) { + zval_ptr_dtor(dst); + ZVAL_NULL(dst); + + return false; + } + } + + return true; + } + + ZVAL_NULL(dst); + + return false; +} + +static bool zend_opcache_serializer_decode_object(zend_opcache_serializer_rbuf_t *rb, + zval *dst, const zend_opcache_serializer_hdr_t *hdr) +{ + const unsigned char *ser_meta, *ser_data, *position, *end; + const char *name_str; + zend_string *class_name; + zend_class_entry *ce; + php_unserialize_data_t var_hash; + uint32_t name_len, ser_len; + uint8_t obj_flags; + int result; + + name_str = NULL; + name_len = 0; + obj_flags = hdr->flags; + + if (!zend_opcache_serializer_read_object_class_name(rb, &name_str, &name_len)) { + return false; + } + + if ((obj_flags & ZEND_OPCACHE_SERIALIZER_OBJ_PHP_SER) != 0) { + ser_meta = (const unsigned char *) zend_opcache_serializer_rbuf_read(rb, 8); + if (ser_meta == NULL) { + return false; + } + + memcpy(&ser_len, ser_meta, sizeof(uint32_t)); + + ser_data = (const unsigned char *) zend_opcache_serializer_rbuf_read(rb, (size_t) ser_len + 1); + if (ser_data == NULL) { + return false; + } + + position = ser_data; + end = position + ser_len; + PHP_VAR_UNSERIALIZE_INIT(var_hash); + result = php_var_unserialize(dst, &position, end, &var_hash); + PHP_VAR_UNSERIALIZE_DESTROY(var_hash); + if (result != 1 || position != end) { + ZVAL_NULL(dst); + + return false; + } + + zend_opcache_serializer_capture_decoded_reachable_value(rb, dst); + + return true; + } + + class_name = zend_string_init(name_str, name_len, 0); + ce = zend_lookup_class(class_name); + zend_string_release(class_name); + if (ce == NULL) { + ZVAL_NULL(dst); + + return false; + } + + return zend_opcache_serializer_decode_object_payload_with_ce(rb, dst, obj_flags, ce); +} + +static bool zend_opcache_serializer_decode_array(zend_opcache_serializer_rbuf_t *rb, zval *dst, + const zend_opcache_serializer_hdr_t *hdr) +{ + const char *key_str; + const unsigned char *meta, *key_data, *key_block; + zval elem; + uint64_t int_key; + uint32_t count, next_free, index, key_len; + uint8_t key_type; + bool is_packed; + + if (hdr->data_size < 8) { + return false; + } + + meta = (const unsigned char *) zend_opcache_serializer_rbuf_read(rb, 8); + if (meta == NULL) { + return false; + } + + memcpy(&count, meta, sizeof(uint32_t)); + memcpy(&next_free, meta + 4, sizeof(uint32_t)); + is_packed = (hdr->flags & ZEND_OPCACHE_SERIALIZER_ARRAY_PACKED) != 0; + + array_init_size(dst, count); + + if (is_packed) { + for (index = 0; index < count; index++) { + ZVAL_UNDEF(&elem); + + if (index + 1 < count && zend_opcache_serializer_rbuf_check(rb, 64)) { + ZEND_OPCACHE_SERIALIZER_PREFETCH_R(rb->data + rb->pos + 64); + } + + if (!zend_opcache_serializer_decode_zval(rb, &elem)) { + zval_ptr_dtor(dst); + ZVAL_UNDEF(dst); + + return false; + } + + zend_hash_next_index_insert_new(Z_ARRVAL_P(dst), &elem); + } + } else { + for (index = 0; index < count; index++) { + if (zend_opcache_serializer_rbuf_check(rb, 64)) { + ZEND_OPCACHE_SERIALIZER_PREFETCH_R(rb->data + rb->pos + 64); + } + + if (!zend_opcache_serializer_rbuf_check(rb, 8)) { + zval_ptr_dtor(dst); + ZVAL_UNDEF(dst); + + return false; + } + + key_type = rb->data[rb->pos]; + if (key_type == 0) { + key_block = (const unsigned char *) zend_opcache_serializer_rbuf_read(rb, 16); + if (key_block == NULL) { + zval_ptr_dtor(dst); + ZVAL_UNDEF(dst); + + return false; + } + + memcpy(&int_key, key_block + 8, sizeof(uint64_t)); + + ZVAL_UNDEF(&elem); + if (!zend_opcache_serializer_decode_zval(rb, &elem)) { + zval_ptr_dtor(dst); + ZVAL_UNDEF(dst); + + return false; + } + + zend_hash_index_add_new(Z_ARRVAL_P(dst), (zend_ulong) int_key, &elem); + } else if (key_type == 1) { + key_data = (const unsigned char *) zend_opcache_serializer_rbuf_read(rb, 8); + if (key_data == NULL) { + zval_ptr_dtor(dst); + ZVAL_UNDEF(dst); + + return false; + } + + memcpy(&key_len, key_data + 4, sizeof(uint32_t)); + + key_str = (const char *) zend_opcache_serializer_rbuf_read(rb, (size_t) key_len + 1); + if (key_str == NULL) { + zval_ptr_dtor(dst); + ZVAL_UNDEF(dst); + + return false; + } + + ZVAL_UNDEF(&elem); + if (!zend_opcache_serializer_decode_zval(rb, &elem)) { + zval_ptr_dtor(dst); + ZVAL_UNDEF(dst); + + return false; + } + + zend_hash_str_add_new(Z_ARRVAL_P(dst), key_str, key_len, &elem); + } else { + zval_ptr_dtor(dst); + ZVAL_UNDEF(dst); + + return false; + } + } + } + + Z_ARRVAL_P(dst)->nNextFreeElement = next_free; + + return true; +} + +static zend_always_inline bool zend_opcache_serializer_decode_zval(zend_opcache_serializer_rbuf_t *rb, zval *dst) +{ + zend_opcache_serializer_hdr_t hdr; + const void *payload; + int64_t lval; + double dval; + bool previous_skip_decoded_value_capture, skip_decoded_value_capture; + + if (rb->depth >= ZEND_OPCACHE_SERIALIZER_MAX_DEPTH) { + ZVAL_NULL(dst); + + return false; + } + + previous_skip_decoded_value_capture = rb->skip_decoded_value_capture; + rb->skip_decoded_value_capture = false; + rb->depth++; + + if (!zend_opcache_serializer_rbuf_read_hdr(rb, &hdr)) { + ZVAL_NULL(dst); + rb->depth--; + + return false; + } + + switch (hdr.type) { + case ZEND_OPCACHE_SERIALIZER_TYPE_UNDEF: + ZVAL_UNDEF(dst); + break; + case ZEND_OPCACHE_SERIALIZER_TYPE_NULL: + ZVAL_NULL(dst); + break; + case ZEND_OPCACHE_SERIALIZER_TYPE_FALSE: + ZVAL_FALSE(dst); + break; + case ZEND_OPCACHE_SERIALIZER_TYPE_TRUE: + ZVAL_TRUE(dst); + break; + case ZEND_OPCACHE_SERIALIZER_TYPE_LONG: { + payload = zend_opcache_serializer_rbuf_read(rb, sizeof(int64_t)); + if (payload == NULL) { + ZVAL_NULL(dst); + rb->depth--; + + return false; + } + + memcpy(&lval, payload, sizeof(int64_t)); + + ZVAL_LONG(dst, (zend_long) lval); + break; + } + case ZEND_OPCACHE_SERIALIZER_TYPE_DOUBLE: { + payload = zend_opcache_serializer_rbuf_read(rb, sizeof(double)); + if (payload == NULL) { + ZVAL_NULL(dst); + rb->depth--; + + return false; + } + + memcpy(&dval, payload, sizeof(double)); + + ZVAL_DOUBLE(dst, dval); + break; + } + case ZEND_OPCACHE_SERIALIZER_TYPE_STRING: + if (!zend_opcache_serializer_decode_string_to_zval(rb, dst, &hdr)) { + ZVAL_NULL(dst); + rb->depth--; + + return false; + } + break; + case ZEND_OPCACHE_SERIALIZER_TYPE_ARRAY: + if (!zend_opcache_serializer_decode_array(rb, dst, &hdr)) { + rb->depth--; + + return false; + } + break; + case ZEND_OPCACHE_SERIALIZER_TYPE_OBJECT: + if (!zend_opcache_serializer_decode_object(rb, dst, &hdr)) { + rb->depth--; + + return false; + } + break; + default: + ZVAL_NULL(dst); + rb->depth--; + + return false; + } + + skip_decoded_value_capture = rb->skip_decoded_value_capture; + rb->skip_decoded_value_capture = previous_skip_decoded_value_capture; + if (rb->capture_suppression == 0 && + !skip_decoded_value_capture && + (Z_TYPE_P(dst) == IS_ARRAY || Z_TYPE_P(dst) == IS_OBJECT) + ) { + zend_opcache_serializer_capture_decoded_value(rb, dst); + } + + rb->depth--; + + return true; +} + +static zend_always_inline bool zend_opcache_unserialize_ex( + zval *dst, + const unsigned char *buf, + size_t buf_len, + zend_opcache_serializer_capture_func_t capture_decoded_value, + zend_opcache_serializer_capture_func_t capture_decoded_reachable_value +) +{ + zend_opcache_serializer_rbuf_t rb; + + rb.data = buf; + rb.capture_decoded_value = capture_decoded_value; + rb.capture_decoded_reachable_value = capture_decoded_reachable_value; + rb.len = buf_len; + rb.pos = 0; + rb.depth = 0; + rb.capture_suppression = 0; + rb.skip_decoded_value_capture = false; + + if (!zend_opcache_serializer_decode_zval(&rb, dst)) { + return false; + } + + return rb.pos == rb.len; +} + +static zend_always_inline bool zend_opcache_unserialize(zval *dst, const unsigned char *buf, size_t buf_len) +{ + return zend_opcache_unserialize_ex(dst, buf, buf_len, NULL, NULL); +} + +#endif diff --git a/ext/opcache/zend_static_cache.c b/ext/opcache/zend_static_cache.c new file mode 100644 index 000000000000..280ba86d47ae --- /dev/null +++ b/ext/opcache/zend_static_cache.c @@ -0,0 +1,1911 @@ +/* + +----------------------------------------------------------------------+ + | Zend OPcache | + +----------------------------------------------------------------------+ + | Copyright © The PHP Group and Contributors. | + +----------------------------------------------------------------------+ + | This source file is subject to the Modified BSD License that is | + | bundled with this package in the file LICENSE, and is available | + | through the World Wide Web at . | + | | + | SPDX-License-Identifier: BSD-3-Clause | + +----------------------------------------------------------------------+ + | Author: Go Kudo | + +----------------------------------------------------------------------+ +*/ + +#include "php.h" + +#include "Zend/zend_attributes.h" +#include "Zend/zend_atomic.h" +#include "Zend/zend_closures.h" +#include "Zend/zend_exceptions.h" + +#include "ZendAccelerator.h" +#include "zend_accelerator_module.h" +#include "zend_shared_alloc.h" +#include "zend_static_cache.h" +#include "zend_smart_str.h" + +#include "ext/date/php_date.h" +#include "ext/spl/spl_array.h" +#include "ext/spl/spl_fixedarray.h" +#include "ext/standard/php_var.h" + +#include "SAPI.h" + +#include "zend_static_cache_internal.h" +#include "zend_opcache_serializer.h" + +#include "opcache_arginfo.h" + +#define ZEND_OPCACHE_STATIC_CACHE_API_VALUE_TYPE "object|array|string|int|float|bool|null" + +zend_class_entry *zend_opcache_static_cache_exception_ce; +zend_class_entry *zend_opcache_static_cache_strategy_ce; + +static zend_class_entry *zend_opcache_static_cache_persistent_attribute_ce; +static zend_class_entry *zend_opcache_static_cache_volatile_static_attribute_ce; +static zend_class_entry *zend_opcache_static_cache_safe_direct_attribute_ce; + +zend_opcache_static_cache_context zend_opcache_static_cache_volatile_context_state = { + {0}, {0}, "volatile cache", "opcache_volatile_cache_lock", +#ifndef ZEND_WIN32 + ZEND_OPCACHE_STATIC_CACHE_VOLATILE_SEM_FILENAME_PREFIX, +#endif + true, false +}; + +zend_opcache_static_cache_context zend_opcache_static_cache_persistent_context_state = { + {0}, {0}, "persistent cache", "opcache_persistent_cache_lock", +#ifndef ZEND_WIN32 + ZEND_OPCACHE_STATIC_CACHE_PERSISTENT_SEM_FILENAME_PREFIX, +#endif + false, true +}; + +bool zend_opcache_static_cache_safe_direct_classes_marked = false; +bool zend_opcache_static_cache_subsystem_disabled = false; +const char *zend_opcache_static_cache_subsystem_failure_reason = NULL; + +static HashTable zend_opcache_static_cache_safe_direct_handler_table; +static bool zend_opcache_static_cache_safe_direct_handlers_initialized = false; + +#ifdef ZTS +ZEND_EXT_TLS bool zend_opcache_static_cache_zts_lock_is_write = false; +#endif +ZEND_EXT_TLS HashTable zend_opcache_static_cache_attribute_classes; +ZEND_EXT_TLS bool zend_opcache_static_cache_attribute_classes_initialized = false; +ZEND_EXT_TLS HashTable zend_opcache_static_cache_ignored_classes; +ZEND_EXT_TLS bool zend_opcache_static_cache_ignored_classes_initialized = false; +ZEND_EXT_TLS HashTable zend_opcache_static_cache_function_statics; +ZEND_EXT_TLS bool zend_opcache_static_cache_function_statics_initialized = false; +ZEND_EXT_TLS HashTable zend_opcache_static_cache_class_blob_handles; +ZEND_EXT_TLS bool zend_opcache_static_cache_class_blob_handles_initialized = false; +ZEND_EXT_TLS zend_opcache_static_cache_runtime zend_opcache_static_cache_volatile_runtime_state = {0}; +ZEND_EXT_TLS zend_opcache_static_cache_runtime zend_opcache_static_cache_persistent_runtime_state = {0}; +ZEND_EXT_TLS zend_opcache_static_cache_context *zend_opcache_static_cache_active_context_ptr = NULL; +ZEND_EXT_TLS HashTable *zend_opcache_static_cache_tracked_roots = NULL; +ZEND_EXT_TLS HashTable *zend_opcache_static_cache_tracked_references = NULL; +ZEND_EXT_TLS HashTable *zend_opcache_static_cache_tracked_arrays = NULL; +ZEND_EXT_TLS HashTable *zend_opcache_static_cache_tracked_objects = NULL; +ZEND_EXT_TLS bool zend_opcache_static_cache_has_tracked_arrays = false; +ZEND_EXT_TLS bool zend_opcache_static_cache_has_tracked_objects = false; +ZEND_EXT_TLS bool zend_opcache_static_cache_volatile_skip_publish = false; +ZEND_EXT_TLS bool zend_opcache_static_cache_persistent_skip_publish = false; +ZEND_EXT_TLS zend_opcache_static_cache_shared_graph_ref *zend_opcache_static_cache_shared_graph_refs = NULL; +ZEND_EXT_TLS uint32_t zend_opcache_static_cache_shared_graph_ref_count = 0; +ZEND_EXT_TLS uint32_t zend_opcache_static_cache_shared_graph_ref_capacity = 0; +ZEND_EXT_TLS zend_opcache_static_cache_shared_graph_ref *zend_opcache_static_cache_retired_shared_graphs = NULL; +ZEND_EXT_TLS uint32_t zend_opcache_static_cache_retired_shared_graph_count = 0; +ZEND_EXT_TLS uint32_t zend_opcache_static_cache_retired_shared_graph_capacity = 0; +/* Single-slot pointer caches that short-circuit the hash lookup performed by + * the mutation hooks. The caches are invalidated whenever the tracked-pointer + * map mutates. */ +ZEND_EXT_TLS HashTable *zend_opcache_static_cache_last_array_ht = NULL; +ZEND_EXT_TLS zend_opcache_static_cache_tracked_dependency *zend_opcache_static_cache_last_array_dependency = NULL; +ZEND_EXT_TLS zend_object *zend_opcache_static_cache_last_object_obj = NULL; +ZEND_EXT_TLS zend_opcache_static_cache_tracked_dependency *zend_opcache_static_cache_last_object_dependency = NULL; +ZEND_EXT_TLS zend_opcache_static_cache_pending_mutation zend_opcache_static_cache_pending_mutation_state; +ZEND_EXT_TLS bool zend_opcache_static_cache_capture_active = false; +ZEND_EXT_TLS bool zend_opcache_static_cache_capture_available = false; +ZEND_EXT_TLS zend_opcache_static_cache_static_slot_handle *zend_opcache_static_cache_capture_handle = NULL; +ZEND_EXT_TLS HashTable *zend_opcache_static_cache_capture_arrays = NULL; +ZEND_EXT_TLS HashTable *zend_opcache_static_cache_capture_objects = NULL; +ZEND_EXT_TLS zend_opcache_static_cache_lookup_entry zend_opcache_static_cache_volatile_lookup_entry_storage[ZEND_OPCACHE_STATIC_CACHE_LOOKUP_BUCKETS]; +ZEND_EXT_TLS zend_opcache_static_cache_lookup_entry zend_opcache_static_cache_persistent_lookup_entry_storage[ZEND_OPCACHE_STATIC_CACHE_LOOKUP_BUCKETS]; +ZEND_EXT_TLS HashTable *zend_opcache_static_cache_volatile_request_local_slots = NULL; +ZEND_EXT_TLS HashTable *zend_opcache_static_cache_persistent_request_local_slots = NULL; +ZEND_EXT_TLS HashTable *zend_opcache_static_cache_volatile_entry_locks = NULL; +ZEND_EXT_TLS HashTable *zend_opcache_static_cache_persistent_entry_locks = NULL; +ZEND_EXT_TLS uint32_t zend_opcache_static_cache_volatile_entry_lock_counts[ZEND_OPCACHE_STATIC_CACHE_ENTRY_LOCK_STRIPES]; +ZEND_EXT_TLS uint32_t zend_opcache_static_cache_persistent_entry_lock_counts[ZEND_OPCACHE_STATIC_CACHE_ENTRY_LOCK_STRIPES]; + +static zend_always_inline bool zend_opcache_static_cache_validate_key(zend_string *key, uint32_t arg_num) +{ + if (ZSTR_LEN(key) == 0) { + zend_argument_value_error(arg_num, "must be a non-empty string"); + + return false; + } + + return true; +} + +static zend_always_inline bool zend_opcache_static_cache_validate_store_array_keys(HashTable *values, uint32_t arg_num) +{ + zend_string *key; + + ZEND_HASH_FOREACH_STR_KEY(values, key) { + if (key == NULL || ZSTR_LEN(key) == 0) { + zend_argument_value_error(arg_num, "must be an array with non-empty string keys"); + + return false; + } + } ZEND_HASH_FOREACH_END(); + + return true; +} + +static zend_always_inline void zend_opcache_static_cache_release_key_list(zend_string **keys, uint32_t key_count) +{ + uint32_t index; + + if (keys == NULL) { + return; + } + + for (index = 0; index < key_count; index++) { + zend_string_release(keys[index]); + } + + efree(keys); +} + +static zend_always_inline bool zend_opcache_static_cache_prepare_key_list( + HashTable *keys, + zend_string ***prepared_keys, + uint32_t *prepared_key_count, + uint32_t arg_num) +{ + zend_string **prepared; + zval *value; + uint32_t count, index = 0; + + ZEND_ASSERT(prepared_keys != NULL); + ZEND_ASSERT(prepared_key_count != NULL); + + count = zend_hash_num_elements(keys); + *prepared_keys = NULL; + *prepared_key_count = 0; + + if (count == 0) { + return true; + } + + prepared = safe_emalloc(count, sizeof(zend_string *), 0); + ZEND_HASH_FOREACH_VAL(keys, value) { + ZVAL_DEREF(value); + if (Z_TYPE_P(value) == IS_STRING) { + if (Z_STRLEN_P(value) == 0) { + zend_argument_value_error(arg_num, "must contain only non-empty string or int cache keys"); + zend_opcache_static_cache_release_key_list(prepared, index); + + return false; + } + + prepared[index++] = zend_string_copy(Z_STR_P(value)); + } else if (Z_TYPE_P(value) == IS_LONG) { + prepared[index++] = zend_long_to_str(Z_LVAL_P(value)); + } else { + zend_argument_value_error(arg_num, "must contain only non-empty string or int cache keys"); + zend_opcache_static_cache_release_key_list(prepared, index); + + return false; + } + } ZEND_HASH_FOREACH_END(); + + *prepared_keys = prepared; + *prepared_key_count = index; + + return true; +} + +static zend_always_inline bool zend_opcache_static_cache_validate_api_value(zval *value, uint32_t arg_num) +{ + ZVAL_DEREF(value); + if (Z_TYPE_P(value) == IS_RESOURCE) { + zend_argument_type_error(arg_num, "must be of type " ZEND_OPCACHE_STATIC_CACHE_API_VALUE_TYPE ", resource given"); + + return false; + } + + if (Z_TYPE_P(value) == IS_OBJECT && Z_OBJCE_P(value) == zend_ce_closure) { + zend_argument_type_error(arg_num, "must not be a Closure object"); + + return false; + } + + return true; +} + +static void zend_opcache_static_cache_register_accelerator_handlers(void); + +static void zend_opcache_static_cache_safe_direct_handlers_dtor(zval *zv) +{ + pefree(Z_PTR_P(zv), true); +} + +void zend_opcache_static_cache_safe_direct_handlers_init(void) +{ + if (zend_opcache_static_cache_safe_direct_handlers_initialized) { + return; + } + + zend_hash_init( + &zend_opcache_static_cache_safe_direct_handler_table, + 8, + NULL, + zend_opcache_static_cache_safe_direct_handlers_dtor, + true + ); + zend_opcache_static_cache_safe_direct_handlers_initialized = true; +} + +void zend_opcache_static_cache_safe_direct_handlers_destroy(void) +{ + if (!zend_opcache_static_cache_safe_direct_handlers_initialized) { + return; + } + + zend_hash_destroy(&zend_opcache_static_cache_safe_direct_handler_table); + zend_opcache_static_cache_safe_direct_handlers_initialized = false; +} + +void zend_opcache_static_cache_safe_direct_register_class( + zend_class_entry *ce, + const zend_opcache_static_cache_safe_direct_handlers *handlers) +{ + zend_opcache_static_cache_safe_direct_handlers handlers_copy; + + if (ce == NULL || + zend_opcache_static_cache_safe_direct_attribute_ce == NULL || + handlers == NULL || + handlers->copy == NULL || + handlers->state_serialize == NULL || + handlers->state_unserialize == NULL + ) { + return; + } + + if (!zend_opcache_serializer_safe_direct_cache_has_attribute(ce->attributes)) { + zend_add_class_attribute(ce, zend_opcache_static_cache_safe_direct_attribute_ce->name, 0); + } + + zend_opcache_static_cache_safe_direct_handlers_init(); + handlers_copy = *handlers; + zend_hash_index_update_mem( + &zend_opcache_static_cache_safe_direct_handler_table, + (zend_ulong) (uintptr_t) ce, + &handlers_copy, + sizeof(handlers_copy) + ); +} + +static const zend_opcache_static_cache_safe_direct_handlers *zend_opcache_static_cache_safe_direct_find_handlers( + zend_class_entry *ce, + zend_class_entry **base_ce_ptr) +{ + const zend_opcache_static_cache_safe_direct_handlers *handlers; + + if (!zend_opcache_static_cache_safe_direct_handlers_initialized) { + return NULL; + } + + while (ce != NULL) { + handlers = zend_hash_index_find_ptr( + &zend_opcache_static_cache_safe_direct_handler_table, + (zend_ulong) (uintptr_t) ce + ); + if (handlers != NULL) { + if (base_ce_ptr != NULL) { + *base_ce_ptr = ce; + } + + return handlers; + } + + ce = ce->parent; + } + + return NULL; +} + +static bool zend_opcache_static_cache_safe_direct_serializer_path_serialize( + const zval *object, + zval *state) +{ + const zend_opcache_static_cache_safe_direct_handlers *handlers = + zend_opcache_static_cache_safe_direct_find_handlers(Z_OBJCE_P(object), NULL) + ; + + ZVAL_UNDEF(state); + if (handlers == NULL || handlers->serializer_path.serialize == NULL) { + return false; + } + + zend_call_known_instance_method_with_0_params(handlers->serializer_path.serialize, Z_OBJ_P(object), state); + if (EG(exception) || Z_TYPE_P(state) != IS_ARRAY) { + if (Z_TYPE_P(state) != IS_UNDEF) { + zval_ptr_dtor(state); + ZVAL_UNDEF(state); + } + + return false; + } + + return true; +} + +static bool zend_opcache_static_cache_safe_direct_serializer_path_unserialize( + zval *object, + zval *state) +{ + const zend_opcache_static_cache_safe_direct_handlers *handlers = + zend_opcache_static_cache_safe_direct_find_handlers(Z_OBJCE_P(object), NULL) + ; + + if (handlers == NULL || handlers->serializer_path.unserialize == NULL || Z_TYPE_P(state) != IS_ARRAY) { + return false; + } + + zend_call_known_instance_method_with_1_params(handlers->serializer_path.unserialize, Z_OBJ_P(object), NULL, state); + + return !EG(exception); +} + +static bool zend_opcache_static_cache_safe_direct_serializer_path_copy( + void *context, + zend_object *old_object, + zend_object *new_object, + zend_opcache_static_cache_safe_direct_clone_value_func_t clone_value) +{ + zval old_zv, new_zv, state_zv, cloned_state_zv; + bool result = false; + + if (clone_value == NULL) { + return false; + } + + ZVAL_OBJ(&old_zv, old_object); + ZVAL_OBJ(&new_zv, new_object); + ZVAL_UNDEF(&state_zv); + ZVAL_UNDEF(&cloned_state_zv); + + if (!zend_opcache_static_cache_safe_direct_serializer_path_serialize(&old_zv, &state_zv)) { + goto cleanup; + } + + if (!clone_value(context, &cloned_state_zv, &state_zv) || + Z_TYPE(cloned_state_zv) != IS_ARRAY) { + goto cleanup; + } + + result = zend_opcache_static_cache_safe_direct_serializer_path_unserialize( + &new_zv, + &cloned_state_zv + ); + +cleanup: + if (Z_TYPE(cloned_state_zv) != IS_UNDEF) { + zval_ptr_dtor(&cloned_state_zv); + } + if (Z_TYPE(state_zv) != IS_UNDEF) { + zval_ptr_dtor(&state_zv); + } + + return result && !EG(exception); +} + +static bool zend_opcache_static_cache_safe_direct_serializer_path_has_unstorable( + void *context, + const zval *value, + zend_opcache_static_cache_safe_direct_value_has_unstorable_func_t value_has_unstorable) +{ + zval state_zv; + bool result; + + if (value_has_unstorable == NULL) { + return false; + } + + ZVAL_UNDEF(&state_zv); + if (!zend_opcache_static_cache_safe_direct_serializer_path_serialize(value, &state_zv)) { + return true; + } + + result = value_has_unstorable(context, &state_zv); + zval_ptr_dtor(&state_zv); + + return result; +} + +zend_opcache_static_cache_safe_direct_state_copy_func_t zend_opcache_static_cache_safe_direct_copy_func( + zend_class_entry *ce, + zend_class_entry **base_ce_ptr) +{ + const zend_opcache_static_cache_safe_direct_handlers *handlers = + zend_opcache_static_cache_safe_direct_find_handlers(ce, base_ce_ptr); + + return handlers != NULL ? handlers->copy : NULL; +} + +zend_opcache_static_cache_safe_direct_state_has_unstorable_func_t zend_opcache_static_cache_safe_direct_state_has_unstorable_func( + zend_class_entry *ce) +{ + const zend_opcache_static_cache_safe_direct_handlers *handlers = + zend_opcache_static_cache_safe_direct_find_handlers(ce, NULL); + + return handlers != NULL ? handlers->state_has_unstorable : NULL; +} + +zend_opcache_static_cache_safe_direct_state_serialize_func_t zend_opcache_static_cache_safe_direct_state_serialize_func( + zend_class_entry *ce) +{ + const zend_opcache_static_cache_safe_direct_handlers *handlers = + zend_opcache_static_cache_safe_direct_find_handlers(ce, NULL); + + return handlers != NULL ? handlers->state_serialize : NULL; +} + +zend_opcache_static_cache_safe_direct_state_unserialize_func_t zend_opcache_static_cache_safe_direct_state_unserialize_func( + zend_class_entry *ce) +{ + const zend_opcache_static_cache_safe_direct_handlers *handlers = + zend_opcache_static_cache_safe_direct_find_handlers(ce, NULL); + + return handlers != NULL ? handlers->state_unserialize : NULL; +} + +bool zend_opcache_static_cache_safe_direct_allows_custom_serializers(zend_class_entry *ce) +{ + const zend_opcache_static_cache_safe_direct_handlers *handlers = + zend_opcache_static_cache_safe_direct_find_handlers(ce, NULL); + + return handlers != NULL && handlers->allows_custom_serializers; +} + +bool zend_opcache_static_cache_safe_direct_state_includes_properties(zend_class_entry *ce) +{ + const zend_opcache_static_cache_safe_direct_handlers *handlers = + zend_opcache_static_cache_safe_direct_find_handlers(ce, NULL); + + return handlers != NULL && handlers->serializer_path.state_includes_properties; +} + +static zend_always_inline zend_string *zend_opcache_static_cache_validate_volatile_static_attribute( + zend_attribute *attr, + uint32_t target ZEND_ATTRIBUTE_UNUSED, + zend_class_entry *scope) +{ + zend_opcache_static_cache_volatile_static_attribute_config config; + zend_string *error = NULL; + + if (!zend_opcache_static_cache_volatile_static_attribute_config_from_attribute(attr, scope, &config, &error)) { + return error; + } + + return NULL; +} + +static zend_always_inline zend_string *zend_opcache_static_cache_validate_direct_cache_safe_attribute( + zend_attribute *attr ZEND_ATTRIBUTE_UNUSED, + uint32_t target, + zend_class_entry *scope) +{ + if (target != ZEND_ATTRIBUTE_TARGET_CLASS) { + return ZSTR_INIT_LITERAL("Only classes can be marked with #[OPcache\\__DirectCacheSafe]", 0); + } + + if (scope == NULL || scope->type != ZEND_INTERNAL_CLASS) { + return ZSTR_INIT_LITERAL("Only internal classes can be marked with #[OPcache\\__DirectCacheSafe]", 0); + } + + return NULL; +} + +static zend_always_inline void zend_opcache_static_cache_register_classes(void) +{ + zend_internal_attribute *attribute; + + if (zend_opcache_static_cache_persistent_attribute_ce != NULL) { + return; + } + + zend_opcache_static_cache_persistent_attribute_ce = register_class_OPcache_PersistentStatic(); + zend_mark_internal_attribute(zend_opcache_static_cache_persistent_attribute_ce); + zend_opcache_static_cache_strategy_ce = register_class_OPcache_CacheStrategy(); + zend_opcache_static_cache_volatile_static_attribute_ce = register_class_OPcache_VolatileStatic(); + attribute = zend_mark_internal_attribute(zend_opcache_static_cache_volatile_static_attribute_ce); + attribute->validator = zend_opcache_static_cache_validate_volatile_static_attribute; + zend_opcache_static_cache_safe_direct_attribute_ce = register_class_OPcache___DirectCacheSafe(); + attribute = zend_mark_internal_attribute(zend_opcache_static_cache_safe_direct_attribute_ce); + attribute->validator = zend_opcache_static_cache_validate_direct_cache_safe_attribute; + zend_opcache_static_cache_exception_ce = register_class_OPcache_StaticCacheException(zend_ce_exception); +} + +static zend_always_inline void zend_opcache_static_cache_safe_direct_register_serializer_class( + zend_class_entry *ce, + bool allows_custom_serializers) +{ + zend_opcache_static_cache_safe_direct_handlers handlers; + + if (ce == NULL || ce->__serialize == NULL || ce->__unserialize == NULL) { + return; + } + + handlers.allows_custom_serializers = allows_custom_serializers; + handlers.serializer_path.state_includes_properties = true; + handlers.serializer_path.serialize = ce->__serialize; + handlers.serializer_path.unserialize = ce->__unserialize; + handlers.copy = zend_opcache_static_cache_safe_direct_serializer_path_copy; + handlers.state_has_unstorable = zend_opcache_static_cache_safe_direct_serializer_path_has_unstorable; + handlers.state_serialize = zend_opcache_static_cache_safe_direct_serializer_path_serialize; + handlers.state_unserialize = zend_opcache_static_cache_safe_direct_serializer_path_unserialize; + + zend_opcache_static_cache_safe_direct_register_class(ce, &handlers); +} + +static zend_always_inline void zend_opcache_static_cache_safe_direct_register_internal_classes(void) +{ + zend_class_entry *date_ce, *immutable_ce, *timezone_ce, *interval_ce, *fixedarray_ce, *arrayobject_ce, *arrayiterator_ce, *recursive_arrayiterator_ce; + const zend_opcache_static_cache_safe_direct_handlers *date_handlers, *spl_array_handlers, *spl_fixedarray_handlers; + + if (zend_opcache_static_cache_safe_direct_classes_marked) { + return; + } + + date_ce = php_date_get_date_ce(); + immutable_ce = php_date_get_immutable_ce(); + timezone_ce = php_date_get_timezone_ce(); + interval_ce = php_date_get_interval_ce(); + fixedarray_ce = spl_ce_SplFixedArray; + arrayobject_ce = spl_ce_ArrayObject; + arrayiterator_ce = spl_ce_ArrayIterator; + recursive_arrayiterator_ce = spl_ce_RecursiveArrayIterator; + if (date_ce == NULL || immutable_ce == NULL || timezone_ce == NULL || interval_ce == NULL || + fixedarray_ce == NULL || arrayobject_ce == NULL || arrayiterator_ce == NULL || + recursive_arrayiterator_ce == NULL + ) { + return; + } + + date_handlers = php_date_get_direct_cache_handlers(); + spl_fixedarray_handlers = spl_fixedarray_object_get_direct_cache_handlers(); + spl_array_handlers = spl_array_object_get_direct_cache_handlers(); + if (date_handlers != NULL) { + zend_opcache_static_cache_safe_direct_register_class(date_ce, date_handlers); + zend_opcache_static_cache_safe_direct_register_class(immutable_ce, date_handlers); + zend_opcache_static_cache_safe_direct_register_class(timezone_ce, date_handlers); + zend_opcache_static_cache_safe_direct_register_class(interval_ce, date_handlers); + } else { + zend_opcache_static_cache_safe_direct_register_serializer_class(date_ce, true); + zend_opcache_static_cache_safe_direct_register_serializer_class(immutable_ce, true); + zend_opcache_static_cache_safe_direct_register_serializer_class(timezone_ce, true); + zend_opcache_static_cache_safe_direct_register_serializer_class(interval_ce, true); + } + + if (spl_fixedarray_handlers != NULL) { + zend_opcache_static_cache_safe_direct_register_class(fixedarray_ce, spl_fixedarray_handlers); + } else { + zend_opcache_static_cache_safe_direct_register_serializer_class(fixedarray_ce, false); + } + + if (spl_array_handlers != NULL) { + zend_opcache_static_cache_safe_direct_register_class(arrayobject_ce, spl_array_handlers); + zend_opcache_static_cache_safe_direct_register_class(arrayiterator_ce, spl_array_handlers); + zend_opcache_static_cache_safe_direct_register_class(recursive_arrayiterator_ce, spl_array_handlers); + } else { + zend_opcache_static_cache_safe_direct_register_serializer_class(arrayobject_ce, false); + zend_opcache_static_cache_safe_direct_register_serializer_class(arrayiterator_ce, false); + zend_opcache_static_cache_safe_direct_register_serializer_class(recursive_arrayiterator_ce, false); + } + + zend_opcache_static_cache_safe_direct_classes_marked = true; +} + +static zend_always_inline void zend_opcache_static_cache_invalidate_script_context(zend_opcache_static_cache_context *context, zend_persistent_script *persistent_script) +{ + zend_opcache_static_cache_context *previous_context; + + if (!zend_opcache_static_cache_context_runtime(context)->available || persistent_script == NULL) { + return; + } + + previous_context = zend_opcache_static_cache_activate_context(context); + if (!zend_opcache_static_cache_wlock()) { + zend_opcache_static_cache_restore_context(previous_context); + return; + } + + if (zend_opcache_static_cache_header_init_locked()) { + zend_opcache_static_cache_delete_script_keys_locked(persistent_script); + } + + zend_opcache_static_cache_unlock(); + zend_opcache_static_cache_mark_publish_skipped(context); + zend_opcache_static_cache_restore_context(previous_context); +} + +static zend_always_inline void zend_opcache_static_cache_invalidate_all_context(zend_opcache_static_cache_context *context) +{ + zend_opcache_static_cache_context *previous_context; + + if (!zend_opcache_static_cache_context_runtime(context)->available) { + return; + } + + previous_context = zend_opcache_static_cache_activate_context(context); + if (!zend_opcache_static_cache_wlock()) { + zend_opcache_static_cache_restore_context(previous_context); + + return; + } + + if (!zend_opcache_static_cache_header_init_locked()) { + zend_opcache_static_cache_unlock(); + zend_opcache_static_cache_restore_context(previous_context); + + return; + } + + if (!zend_opcache_static_cache_clear_locked()) { + zend_opcache_static_cache_unlock(); + zend_opcache_static_cache_restore_context(previous_context); + + return; + } + + zend_opcache_static_cache_unlock(); + zend_opcache_static_cache_release_active_entry_locks(); + zend_opcache_static_cache_mark_publish_skipped(context); + zend_opcache_static_cache_restore_context(previous_context); +} + +static zend_always_inline bool zend_opcache_static_cache_parse_ttl(zend_long ttl, uint32_t arg_num) +{ + if (ttl < 0) { + zend_argument_value_error(arg_num, "must be greater than or equal to 0"); + + return false; + } + + return true; +} + +static zend_always_inline bool zend_opcache_static_cache_require_available_read(void) +{ + zend_opcache_static_cache_context *context = zend_opcache_static_cache_active_context(); + + if (!zend_opcache_validate_api_restriction()) { + zend_throw_exception_ex(zend_opcache_static_cache_exception_ce, 0, ACCELERATOR_PRODUCT_NAME " API access is restricted"); + + return false; + } + + if (!zend_opcache_static_cache_active_runtime()->available) { + if (zend_opcache_static_cache_active_runtime()->failure_reason) { + zend_throw_exception_ex(zend_opcache_static_cache_exception_ce, 0, "%s", zend_opcache_static_cache_active_runtime()->failure_reason); + } else { + zend_throw_exception_ex(zend_opcache_static_cache_exception_ce, 0, "OPcache %s is disabled", context->name); + } + + return false; + } + + if (!zend_opcache_static_cache_rlock()) { + zend_throw_exception_ex(zend_opcache_static_cache_exception_ce, 0, "Unable to acquire the %s read lock", context->name); + + return false; + } + + if (!zend_opcache_static_cache_header_is_initialized_locked()) { + zend_opcache_static_cache_unlock(); + zend_throw_exception_ex(zend_opcache_static_cache_exception_ce, 0, "Unable to access the %s header", context->name); + + return false; + } + + return true; +} + +static zend_always_inline bool zend_opcache_static_cache_validate_available_write(void) +{ + zend_opcache_static_cache_context *context = zend_opcache_static_cache_active_context(); + + if (!zend_opcache_validate_api_restriction()) { + zend_throw_exception_ex(zend_opcache_static_cache_exception_ce, 0, ACCELERATOR_PRODUCT_NAME " API access is restricted"); + + return false; + } + + if (!zend_opcache_static_cache_active_runtime()->available) { + if (zend_opcache_static_cache_active_runtime()->failure_reason) { + zend_throw_exception_ex(zend_opcache_static_cache_exception_ce, 0, "%s", zend_opcache_static_cache_active_runtime()->failure_reason); + } else { + zend_throw_exception_ex(zend_opcache_static_cache_exception_ce, 0, "OPcache %s is disabled", context->name); + } + + return false; + } + + return true; +} + +static zend_always_inline bool zend_opcache_static_cache_acquire_write_lock(void) +{ + zend_opcache_static_cache_context *context = zend_opcache_static_cache_active_context(); + + if (!zend_opcache_static_cache_wlock()) { + zend_throw_exception_ex(zend_opcache_static_cache_exception_ce, 0, "Unable to acquire the %s lock", context->name); + + return false; + } + + if (!zend_opcache_static_cache_header_init_locked()) { + zend_opcache_static_cache_unlock(); + zend_throw_exception_ex(zend_opcache_static_cache_exception_ce, 0, "Unable to initialize the %s header", context->name); + + return false; + } + + return true; +} + +static zend_always_inline bool zend_opcache_static_cache_require_available_write(void) +{ + if (!zend_opcache_static_cache_validate_available_write()) { + return false; + } + + return zend_opcache_static_cache_acquire_write_lock(); +} + +static zend_always_inline bool zend_opcache_static_cache_begin_entry_mutation(zend_string *key, bool *release_entry_lock) +{ + ZEND_ASSERT(release_entry_lock != NULL); + *release_entry_lock = false; + + if (!zend_opcache_static_cache_has_entry_lock(key)) { + if (!zend_opcache_static_cache_acquire_entry_lock(key)) { + return false; + } + + *release_entry_lock = true; + } + + return true; +} + +static zend_always_inline void zend_opcache_static_cache_finish_entry_mutation(zend_string *key, bool release_entry_lock, bool successful_mutation) +{ + if (successful_mutation || release_entry_lock) { + zend_opcache_static_cache_release_entry_lock(key); + } +} + +static zend_always_inline bool zend_opcache_static_cache_explicit_delete_prevalidated(zend_string *key) +{ + bool deleted, release_entry_lock; + + release_entry_lock = zend_opcache_static_cache_has_entry_lock(key); + + if (!zend_opcache_static_cache_acquire_write_lock()) { + return false; + } + + deleted = zend_opcache_static_cache_delete_locked(key); + zend_opcache_static_cache_unlock(); + if (deleted && release_entry_lock) { + zend_opcache_static_cache_release_entry_lock(key); + } + + return deleted; +} + +static zend_always_inline bool zend_opcache_static_cache_explicit_clear_prevalidated(void) +{ + bool cleared; + + if (!zend_opcache_static_cache_acquire_write_lock()) { + return false; + } + + cleared = zend_opcache_static_cache_clear_locked(); + zend_opcache_static_cache_unlock(); + if (cleared) { + zend_opcache_static_cache_release_active_entry_locks(); + } + + return cleared; +} + +static zend_always_inline bool zend_opcache_static_cache_explicit_store_prevalidated(zend_string *key, zval *value, zend_long ttl, bool throw_on_failure) +{ + zend_opcache_static_cache_prepared_value prepared; + bool stored, release_entry_lock = false; + + if (!zend_opcache_static_cache_prepare_value(&prepared, key, value, throw_on_failure, false)) { + return false; + } + + if (!zend_opcache_static_cache_begin_entry_mutation(key, &release_entry_lock)) { + zend_opcache_static_cache_destroy_prepared_value(&prepared); + + return false; + } + + /* Callers validate availability before staging the value so large shared-graph + * builds stay outside the write lock. Once that preflight passed, the commit + * path only needs to acquire the lock and publish the prepared payload. */ + if (!zend_opcache_static_cache_acquire_write_lock()) { + zend_opcache_static_cache_destroy_prepared_value(&prepared); + if (release_entry_lock) { + zend_opcache_static_cache_release_entry_lock(key); + } + + return false; + } + + stored = zend_opcache_static_cache_store_prepared_locked(key, value, &prepared, ttl, throw_on_failure); + zend_opcache_static_cache_unlock(); + zend_opcache_static_cache_destroy_prepared_value(&prepared); + zend_opcache_static_cache_finish_entry_mutation(key, release_entry_lock, stored); + + return stored; +} + +static zend_always_inline zend_result zend_opcache_static_cache_fetch_api( + zend_opcache_static_cache_context *context, + zend_string *key, + zval *default_value, + zval *return_value) +{ + zend_opcache_static_cache_context *previous_context; + bool fetched, found = false; + + previous_context = zend_opcache_static_cache_activate_context(context); + if (!zend_opcache_static_cache_require_available_read()) { + zend_opcache_static_cache_restore_context(previous_context); + + return FAILURE; + } + + fetched = zend_opcache_static_cache_fetch_locked(key, return_value, false, &found, true); + zend_opcache_static_cache_unlock(); + zend_opcache_static_cache_restore_context(previous_context); + + if (!fetched) { + if (!found) { + ZVAL_COPY(return_value, default_value); + + return SUCCESS; + } + + if (!EG(exception)) { + zend_throw_exception_ex(zend_opcache_static_cache_exception_ce, 0, "Stored %s value for key \"%s\" could not be decoded", context->name, ZSTR_VAL(key)); + } + + return FAILURE; + } + + return SUCCESS; +} + +static zend_always_inline zend_result zend_opcache_static_cache_fetch_array_api( + zend_opcache_static_cache_context *context, + HashTable *keys, + zval *default_value, + zval *return_value) +{ + zend_opcache_static_cache_context *previous_context; + zend_string **prepared_keys, *key; + zval fetched_value; + uint32_t key_count, index; + bool fetched, found; + + if (!zend_opcache_static_cache_prepare_key_list(keys, &prepared_keys, &key_count, 1)) { + return FAILURE; + } + + previous_context = zend_opcache_static_cache_activate_context(context); + if (!zend_opcache_static_cache_require_available_read()) { + zend_opcache_static_cache_restore_context(previous_context); + zend_opcache_static_cache_release_key_list(prepared_keys, key_count); + + return FAILURE; + } + + array_init_size(return_value, key_count); + + for (index = 0; index < key_count; index++) { + key = prepared_keys[index]; + fetched = zend_opcache_static_cache_fetch_locked(key, &fetched_value, false, &found, false); + if (!fetched) { + if (!found) { + ZVAL_COPY(&fetched_value, default_value); + } else { + if (!EG(exception)) { + zend_throw_exception_ex(zend_opcache_static_cache_exception_ce, 0, "Stored %s value for key \"%s\" could not be decoded", context->name, ZSTR_VAL(key)); + } + zval_ptr_dtor(return_value); + ZVAL_UNDEF(return_value); + zend_opcache_static_cache_unlock(); + zend_opcache_static_cache_restore_context(previous_context); + zend_opcache_static_cache_release_key_list(prepared_keys, key_count); + + return FAILURE; + } + } + + zend_hash_update(Z_ARRVAL_P(return_value), key, &fetched_value); + } + + zend_opcache_static_cache_unlock(); + zend_opcache_static_cache_restore_context(previous_context); + zend_opcache_static_cache_release_key_list(prepared_keys, key_count); + + return SUCCESS; +} + +static zend_always_inline zend_result zend_opcache_static_cache_exists_api(zend_opcache_static_cache_context *context, zend_string *key, bool *exists) +{ + zend_opcache_static_cache_context *previous_context; + + previous_context = zend_opcache_static_cache_activate_context(context); + if (!zend_opcache_static_cache_require_available_read()) { + zend_opcache_static_cache_restore_context(previous_context); + + return FAILURE; + } + + *exists = zend_opcache_static_cache_exists_locked(key); + zend_opcache_static_cache_unlock(); + zend_opcache_static_cache_restore_context(previous_context); + + return SUCCESS; +} + +static zend_always_inline zend_result zend_opcache_static_cache_lock_api(zend_opcache_static_cache_context *context, zend_string *key, bool *locked) +{ + zend_opcache_static_cache_context *previous_context; + + previous_context = zend_opcache_static_cache_activate_context(context); + if (!zend_opcache_static_cache_validate_available_write()) { + zend_opcache_static_cache_restore_context(previous_context); + + return FAILURE; + } + + *locked = zend_opcache_static_cache_try_acquire_entry_lock(key); + zend_opcache_static_cache_restore_context(previous_context); + + return SUCCESS; +} + +static zend_always_inline void zend_opcache_static_cache_disable_subsystem(const char *failure_reason) +{ + zend_opcache_static_cache_context *previous_context; + + zend_opcache_static_cache_subsystem_disabled = true; + zend_opcache_static_cache_subsystem_failure_reason = failure_reason != NULL + ? failure_reason + : "OPcache static cache startup failed" + ; + + zend_opcache_static_cache_unregister_hooks(); + + previous_context = zend_opcache_static_cache_activate_context(&zend_opcache_static_cache_volatile_context_state); + zend_opcache_static_cache_shutdown_storage(); + zend_opcache_static_cache_reset_runtime(); + zend_opcache_static_cache_activate_context(&zend_opcache_static_cache_persistent_context_state); + zend_opcache_static_cache_shutdown_storage(); + zend_opcache_static_cache_reset_runtime(); + zend_opcache_static_cache_restore_context(previous_context); +} + +zend_result zend_opcache_register_functions(int module_type) +{ + return zend_register_functions(NULL, ext_functions, NULL, module_type); +} + +zend_result zend_opcache_static_cache_minit(void) +{ + zend_opcache_static_cache_context *previous_context; + + zend_opcache_static_cache_subsystem_disabled = false; + zend_opcache_static_cache_subsystem_failure_reason = NULL; + zend_opcache_static_cache_safe_direct_classes_marked = false; + + zend_opcache_static_cache_register_classes(); + zend_opcache_static_cache_safe_direct_handlers_init(); + zend_opcache_static_cache_register_accelerator_handlers(); + + previous_context = zend_opcache_static_cache_activate_context(&zend_opcache_static_cache_volatile_context_state); + zend_opcache_static_cache_reset_storage(); + + zend_opcache_static_cache_activate_context(&zend_opcache_static_cache_persistent_context_state); + zend_opcache_static_cache_reset_storage(); + + zend_opcache_static_cache_restore_context(previous_context); + + return SUCCESS; +} + +static void zend_opcache_static_cache_startup(void) +{ + const char *failure_reason; + zend_opcache_static_cache_context *previous_context; + + previous_context = zend_opcache_static_cache_activate_context(&zend_opcache_static_cache_volatile_context_state); + zend_opcache_static_cache_reset_runtime(); + if (zend_opcache_static_cache_active_runtime()->enabled && ZCG(enabled) && accel_startup_ok && !file_cache_only) { + if (!zend_opcache_static_cache_startup_storage_before_request()) { + failure_reason = zend_opcache_static_cache_active_runtime()->failure_reason; + zend_opcache_static_cache_restore_context(previous_context); + zend_opcache_static_cache_disable_subsystem(failure_reason); + + return; + } + } + + zend_opcache_static_cache_activate_context(&zend_opcache_static_cache_persistent_context_state); + zend_opcache_static_cache_reset_runtime(); + if (zend_opcache_static_cache_active_runtime()->enabled && ZCG(enabled) && accel_startup_ok && !file_cache_only) { + if (!zend_opcache_static_cache_startup_storage_before_request()) { + failure_reason = zend_opcache_static_cache_active_runtime()->failure_reason; + zend_opcache_static_cache_restore_context(previous_context); + zend_opcache_static_cache_disable_subsystem(failure_reason); + + return; + } + } + + zend_opcache_static_cache_restore_context(previous_context); + + if (!zend_opcache_static_cache_subsystem_disabled && + ( + ZCG(accel_directives).static_cache_volatile_size_mb != 0 || + ZCG(accel_directives).static_cache_persistent_size_mb != 0 + ) + ) { + zend_opcache_static_cache_register_hooks(); + } +} + +static void zend_opcache_static_cache_post_startup(void) +{ + const char *failure_reason; + zend_opcache_static_cache_context *previous_context; + + previous_context = zend_opcache_static_cache_activate_context(&zend_opcache_static_cache_volatile_context_state); + zend_opcache_static_cache_reset_runtime(); + if (!(zend_opcache_static_cache_active_context()->storage).initialized && + zend_opcache_static_cache_active_runtime()->enabled && + ZCG(enabled) && + accel_startup_ok && + !file_cache_only + ) { + if (!zend_opcache_static_cache_startup_storage_before_request()) { + failure_reason = zend_opcache_static_cache_active_runtime()->failure_reason; + zend_opcache_static_cache_restore_context(previous_context); + zend_opcache_static_cache_disable_subsystem(failure_reason); + + return; + } + } + + zend_opcache_static_cache_activate_context(&zend_opcache_static_cache_persistent_context_state); + zend_opcache_static_cache_reset_runtime(); + if (!(zend_opcache_static_cache_active_context()->storage).initialized && + zend_opcache_static_cache_active_runtime()->enabled && + ZCG(enabled) && + accel_startup_ok && + !file_cache_only + ) { + if (!zend_opcache_static_cache_startup_storage_before_request()) { + failure_reason = zend_opcache_static_cache_active_runtime()->failure_reason; + zend_opcache_static_cache_restore_context(previous_context); + zend_opcache_static_cache_disable_subsystem(failure_reason); + + return; + } + } + + zend_opcache_static_cache_restore_context(previous_context); + + if (!zend_opcache_static_cache_subsystem_disabled && + ( + ZCG(accel_directives).static_cache_volatile_size_mb != 0 || + ZCG(accel_directives).static_cache_persistent_size_mb != 0 + ) + ) { + zend_opcache_static_cache_safe_direct_register_internal_classes(); + } +} + +void zend_opcache_static_cache_mshutdown(void) +{ + zend_opcache_static_cache_context *previous_context; + + zend_opcache_static_cache_unregister_hooks(); + + previous_context = zend_opcache_static_cache_activate_context(&zend_opcache_static_cache_volatile_context_state); + zend_opcache_static_cache_shutdown_storage(); + zend_opcache_static_cache_reset_runtime(); + zend_opcache_static_cache_activate_context(&zend_opcache_static_cache_persistent_context_state); + zend_opcache_static_cache_shutdown_storage(); + zend_opcache_static_cache_reset_runtime(); + zend_opcache_static_cache_restore_context(previous_context); + + zend_opcache_static_cache_subsystem_disabled = false; + zend_opcache_static_cache_subsystem_failure_reason = NULL; + zend_opcache_static_cache_safe_direct_classes_marked = false; + zend_opcache_static_cache_safe_direct_handlers_destroy(); + zend_accel_register_static_cache_handlers(NULL); +} + +static zend_result zend_opcache_static_cache_rinit(void) +{ + zend_opcache_static_cache_context *previous_context; + bool static_cache_available; + + previous_context = zend_opcache_static_cache_activate_context(&zend_opcache_static_cache_volatile_context_state); + zend_opcache_static_cache_ensure_ready(); + zend_opcache_static_cache_activate_context(&zend_opcache_static_cache_persistent_context_state); + zend_opcache_static_cache_ensure_ready(); + zend_opcache_static_cache_restore_context(previous_context); + zend_opcache_static_cache_clear_lookup_caches(); + zend_opcache_static_cache_reset_publish_skips(); + + EG(static_cache_class_access_active) = false; + EG(tracked_mutation_hooks_active) = false; + + static_cache_available = zend_opcache_static_cache_context_runtime(&zend_opcache_static_cache_volatile_context_state)->available || + zend_opcache_static_cache_context_runtime(&zend_opcache_static_cache_persistent_context_state)->available + ; + if (!static_cache_available) { + return SUCCESS; + } + + zend_opcache_static_cache_register_hooks(); + + EG(static_cache_class_access_active) = true; + zend_opcache_static_cache_request_init(); + + return SUCCESS; +} + +zend_result zend_opcache_static_cache_rshutdown(void) +{ + zend_opcache_static_cache_clear_lookup_caches(); + zend_opcache_static_cache_request_shutdown(); + zend_opcache_static_cache_release_request_entry_locks(); + zend_opcache_static_cache_release_request_local_slots(); + zend_opcache_static_cache_release_request_shared_graph_refs(); + + EG(static_cache_class_access_active) = false; + EG(tracked_mutation_hooks_active) = false; + + return SUCCESS; +} + +static void zend_opcache_static_cache_invalidate_script(zend_persistent_script *persistent_script) +{ + zend_opcache_static_cache_invalidate_script_context(&zend_opcache_static_cache_persistent_context_state, persistent_script); + zend_opcache_static_cache_invalidate_script_context(&zend_opcache_static_cache_volatile_context_state, persistent_script); +} + +static void zend_opcache_static_cache_register_accelerator_handlers(void) +{ + static const zend_opcache_static_cache_accelerator_handlers handlers = { + zend_opcache_static_cache_startup, + zend_opcache_static_cache_post_startup, + zend_opcache_static_cache_rinit, + zend_opcache_static_cache_invalidate_script + }; + + zend_accel_register_static_cache_handlers(&handlers); +} + +void zend_opcache_static_cache_invalidate_all(void) +{ + zend_opcache_static_cache_invalidate_all_context(&zend_opcache_static_cache_persistent_context_state); + zend_opcache_static_cache_invalidate_all_context(&zend_opcache_static_cache_volatile_context_state); +} + +void zend_opcache_static_cache_volatile_get_status(zval *return_value) +{ + zend_opcache_static_cache_context *previous_context = zend_opcache_static_cache_activate_context(&zend_opcache_static_cache_volatile_context_state); + + zend_opcache_static_cache_populate_array(return_value); + zend_opcache_static_cache_restore_context(previous_context); +} + +void zend_opcache_static_cache_persistent_get_status(zval *return_value) +{ + zend_opcache_static_cache_context *previous_context = zend_opcache_static_cache_activate_context(&zend_opcache_static_cache_persistent_context_state); + + zend_opcache_static_cache_populate_array(return_value); + zend_opcache_static_cache_restore_context(previous_context); +} + +bool zend_opcache_static_cache_volatile_is_enabled(void) +{ + return zend_opcache_static_cache_context_runtime(&zend_opcache_static_cache_volatile_context_state)->enabled; +} + +bool zend_opcache_static_cache_volatile_is_available(void) +{ + return zend_opcache_static_cache_context_runtime(&zend_opcache_static_cache_volatile_context_state)->available; +} + +const char *zend_opcache_static_cache_volatile_failure_reason(void) +{ + return zend_opcache_static_cache_context_runtime(&zend_opcache_static_cache_volatile_context_state)->failure_reason; +} + +bool zend_opcache_static_cache_persistent_is_enabled(void) +{ + return zend_opcache_static_cache_context_runtime(&zend_opcache_static_cache_persistent_context_state)->enabled; +} + +bool zend_opcache_static_cache_persistent_is_available(void) +{ + return zend_opcache_static_cache_context_runtime(&zend_opcache_static_cache_persistent_context_state)->available; +} + +const char *zend_opcache_static_cache_persistent_failure_reason(void) +{ + return zend_opcache_static_cache_context_runtime(&zend_opcache_static_cache_persistent_context_state)->failure_reason; +} + +ZEND_METHOD(OPcache_VolatileStatic, __construct) +{ + zend_long ttl = 0; + zval default_strategy, *strategy = NULL; + + ZVAL_UNDEF(&default_strategy); + + ZEND_PARSE_PARAMETERS_START(0, 2) + Z_PARAM_OPTIONAL + Z_PARAM_LONG(ttl) + Z_PARAM_OBJECT_OF_CLASS(strategy, zend_opcache_static_cache_strategy_ce) + ZEND_PARSE_PARAMETERS_END(); + + if (!zend_opcache_static_cache_parse_ttl(ttl, 1)) { + RETURN_THROWS(); + } + + if (strategy == NULL) { + ZVAL_OBJ_COPY(&default_strategy, zend_enum_get_case_cstr(zend_opcache_static_cache_strategy_ce, "Immediate")); + strategy = &default_strategy; + } + + zend_update_property_long(zend_opcache_static_cache_volatile_static_attribute_ce, Z_OBJ_P(ZEND_THIS), ZEND_STRL("ttl"), ttl); + zend_update_property(zend_opcache_static_cache_volatile_static_attribute_ce, Z_OBJ_P(ZEND_THIS), ZEND_STRL("strategy"), strategy); + zval_ptr_dtor(&default_strategy); +} + +ZEND_FUNCTION(OPcache_volatile_store) +{ + zend_string *key; + zend_long ttl = 0; + zval *value; + + ZEND_PARSE_PARAMETERS_START(2, 3) + Z_PARAM_STR(key) + Z_PARAM_ZVAL(value) + Z_PARAM_OPTIONAL + Z_PARAM_LONG(ttl) + ZEND_PARSE_PARAMETERS_END(); + + if (!zend_opcache_static_cache_validate_key(key, 1)) { + RETURN_THROWS(); + } + + if (!zend_opcache_static_cache_validate_api_value(value, 2)) { + RETURN_THROWS(); + } + + if (!zend_opcache_static_cache_parse_ttl(ttl, 3) || !zend_opcache_static_cache_validate_available_write()) { + RETURN_THROWS(); + } + + if (!zend_opcache_static_cache_explicit_store_prevalidated(key, value, ttl, false)) { + if (EG(exception)) { + RETURN_THROWS(); + } + + RETURN_FALSE; + } + + RETURN_TRUE; +} + +ZEND_FUNCTION(OPcache_volatile_store_array) +{ + zend_string *key; + zend_long ttl = 0; + zval *value; + HashTable *values; + + ZEND_PARSE_PARAMETERS_START(1, 2) + Z_PARAM_ARRAY_HT(values) + Z_PARAM_OPTIONAL + Z_PARAM_LONG(ttl) + ZEND_PARSE_PARAMETERS_END(); + + if (!zend_opcache_static_cache_parse_ttl(ttl, 2) || !zend_opcache_static_cache_validate_available_write()) { + RETURN_THROWS(); + } + + if (!zend_opcache_static_cache_validate_store_array_keys(values, 1)) { + RETURN_THROWS(); + } + + ZEND_HASH_FOREACH_STR_KEY_VAL(values, key, value) { + ZEND_ASSERT(key != NULL); + + if (!zend_opcache_static_cache_explicit_store_prevalidated(key, value, ttl, false)) { + if (EG(exception)) { + RETURN_THROWS(); + } + + RETURN_FALSE; + } + } ZEND_HASH_FOREACH_END(); + + RETURN_TRUE; +} + +ZEND_FUNCTION(OPcache_volatile_fetch) +{ + zend_string *key; + zval *default_value = NULL, default_null; + + ZEND_PARSE_PARAMETERS_START(1, 2) + Z_PARAM_STR(key) + Z_PARAM_OPTIONAL + Z_PARAM_ZVAL(default_value) + ZEND_PARSE_PARAMETERS_END(); + + if (!zend_opcache_static_cache_validate_key(key, 1)) { + RETURN_THROWS(); + } + + if (default_value == NULL) { + ZVAL_NULL(&default_null); + default_value = &default_null; + } + + if (!zend_opcache_static_cache_validate_api_value(default_value, 2)) { + RETURN_THROWS(); + } + + if (zend_opcache_static_cache_fetch_api(&zend_opcache_static_cache_volatile_context_state, key, default_value, return_value) == FAILURE) { + RETURN_THROWS(); + } +} + +ZEND_FUNCTION(OPcache_volatile_fetch_array) +{ + HashTable *keys; + zval *default_value = NULL, default_null; + + ZEND_PARSE_PARAMETERS_START(1, 2) + Z_PARAM_ARRAY_HT(keys) + Z_PARAM_OPTIONAL + Z_PARAM_ARRAY_OR_NULL(default_value) + ZEND_PARSE_PARAMETERS_END(); + + if (default_value == NULL) { + ZVAL_NULL(&default_null); + default_value = &default_null; + } + + if (zend_opcache_static_cache_fetch_array_api(&zend_opcache_static_cache_volatile_context_state, keys, default_value, return_value) == FAILURE) { + RETURN_THROWS(); + } +} + +ZEND_FUNCTION(OPcache_volatile_exists) +{ + zend_string *key; + bool exists; + + ZEND_PARSE_PARAMETERS_START(1, 1) + Z_PARAM_STR(key) + ZEND_PARSE_PARAMETERS_END(); + + if (!zend_opcache_static_cache_validate_key(key, 1)) { + RETURN_THROWS(); + } + + if (zend_opcache_static_cache_exists_api(&zend_opcache_static_cache_volatile_context_state, key, &exists) == FAILURE) { + RETURN_THROWS(); + } + + RETURN_BOOL(exists); +} + +ZEND_FUNCTION(OPcache_volatile_lock) +{ + zend_string *key; + bool locked; + + ZEND_PARSE_PARAMETERS_START(1, 1) + Z_PARAM_STR(key) + ZEND_PARSE_PARAMETERS_END(); + + if (!zend_opcache_static_cache_validate_key(key, 1)) { + RETURN_THROWS(); + } + + if (zend_opcache_static_cache_lock_api(&zend_opcache_static_cache_volatile_context_state, key, &locked) == FAILURE) { + RETURN_THROWS(); + } + + RETURN_BOOL(locked); +} + +ZEND_FUNCTION(OPcache_volatile_delete) +{ + zend_string *key; + + ZEND_PARSE_PARAMETERS_START(1, 1) + Z_PARAM_STR(key) + ZEND_PARSE_PARAMETERS_END(); + + if (!zend_opcache_static_cache_validate_key(key, 1)) { + RETURN_THROWS(); + } + + if (!zend_opcache_static_cache_validate_available_write()) { + RETURN_THROWS(); + } + + if (!zend_opcache_static_cache_explicit_delete_prevalidated(key)) { + RETURN_THROWS(); + } +} + +ZEND_FUNCTION(OPcache_volatile_delete_array) +{ + zend_string **prepared_keys; + HashTable *keys; + uint32_t key_count, index; + + ZEND_PARSE_PARAMETERS_START(1, 1) + Z_PARAM_ARRAY_HT(keys) + ZEND_PARSE_PARAMETERS_END(); + + if (!zend_opcache_static_cache_prepare_key_list(keys, &prepared_keys, &key_count, 1)) { + RETURN_THROWS(); + } + + if (!zend_opcache_static_cache_validate_available_write()) { + zend_opcache_static_cache_release_key_list(prepared_keys, key_count); + + RETURN_THROWS(); + } + + for (index = 0; index < key_count; index++) { + if (!zend_opcache_static_cache_explicit_delete_prevalidated(prepared_keys[index])) { + zend_opcache_static_cache_release_key_list(prepared_keys, key_count); + + RETURN_THROWS(); + } + } + + zend_opcache_static_cache_release_key_list(prepared_keys, key_count); +} + +ZEND_FUNCTION(OPcache_volatile_clear) +{ + ZEND_PARSE_PARAMETERS_NONE(); + + if (!zend_opcache_static_cache_validate_available_write()) { + RETURN_THROWS(); + } + + if (!zend_opcache_static_cache_explicit_clear_prevalidated()) { + zend_throw_exception_ex(zend_opcache_static_cache_exception_ce, 0, "Unable to clear the volatile cache"); + + RETURN_THROWS(); + } + + zend_opcache_static_cache_mark_publish_skipped(&zend_opcache_static_cache_volatile_context_state); +} + +ZEND_FUNCTION(OPcache_volatile_cache_info) +{ + zend_opcache_static_cache_context *previous_context; + + ZEND_PARSE_PARAMETERS_NONE(); + + if (!zend_opcache_validate_api_restriction()) { + zend_throw_exception_ex(zend_opcache_static_cache_exception_ce, 0, ACCELERATOR_PRODUCT_NAME " API access is restricted"); + + RETURN_THROWS(); + } + + previous_context = zend_opcache_static_cache_activate_context(&zend_opcache_static_cache_volatile_context_state); + zend_opcache_static_cache_populate_array(return_value); + zend_opcache_static_cache_restore_context(previous_context); +} + +ZEND_FUNCTION(OPcache_persistent_store) +{ + zend_opcache_static_cache_context *previous_context; + zend_string *key; + zval *value; + bool stored; + + ZEND_PARSE_PARAMETERS_START(2, 2) + Z_PARAM_STR(key) + Z_PARAM_ZVAL(value) + ZEND_PARSE_PARAMETERS_END(); + + if (!zend_opcache_static_cache_validate_key(key, 1)) { + RETURN_THROWS(); + } + + if (!zend_opcache_static_cache_validate_api_value(value, 2)) { + RETURN_THROWS(); + } + + previous_context = zend_opcache_static_cache_activate_context(&zend_opcache_static_cache_persistent_context_state); + if (!zend_opcache_static_cache_validate_available_write()) { + zend_opcache_static_cache_restore_context(previous_context); + + RETURN_THROWS(); + } + + stored = zend_opcache_static_cache_explicit_store_prevalidated(key, value, 0, true); + zend_opcache_static_cache_restore_context(previous_context); + + if (!stored) { + RETURN_THROWS(); + } +} + +ZEND_FUNCTION(OPcache_persistent_store_array) +{ + zend_opcache_static_cache_context *previous_context; + zend_string *key; + zval *value; + HashTable *values; + + ZEND_PARSE_PARAMETERS_START(1, 1) + Z_PARAM_ARRAY_HT(values) + ZEND_PARSE_PARAMETERS_END(); + + previous_context = zend_opcache_static_cache_activate_context(&zend_opcache_static_cache_persistent_context_state); + if (!zend_opcache_static_cache_validate_available_write()) { + zend_opcache_static_cache_restore_context(previous_context); + + RETURN_THROWS(); + } + + if (!zend_opcache_static_cache_validate_store_array_keys(values, 1)) { + zend_opcache_static_cache_restore_context(previous_context); + + RETURN_THROWS(); + } + + ZEND_HASH_FOREACH_STR_KEY_VAL(values, key, value) { + ZEND_ASSERT(key != NULL); + + if (!zend_opcache_static_cache_explicit_store_prevalidated(key, value, 0, true)) { + zend_opcache_static_cache_restore_context(previous_context); + + RETURN_THROWS(); + } + } ZEND_HASH_FOREACH_END(); + + zend_opcache_static_cache_restore_context(previous_context); +} + +ZEND_FUNCTION(OPcache_persistent_fetch) +{ + zend_string *key; + zval *default_value = NULL, default_null; + + ZEND_PARSE_PARAMETERS_START(1, 2) + Z_PARAM_STR(key) + Z_PARAM_OPTIONAL + Z_PARAM_ZVAL(default_value) + ZEND_PARSE_PARAMETERS_END(); + + if (!zend_opcache_static_cache_validate_key(key, 1)) { + RETURN_THROWS(); + } + + if (default_value == NULL) { + ZVAL_NULL(&default_null); + default_value = &default_null; + } + + if (!zend_opcache_static_cache_validate_api_value(default_value, 2)) { + RETURN_THROWS(); + } + + if (zend_opcache_static_cache_fetch_api(&zend_opcache_static_cache_persistent_context_state, key, default_value, return_value) == FAILURE) { + RETURN_THROWS(); + } +} + +ZEND_FUNCTION(OPcache_persistent_fetch_array) +{ + HashTable *keys; + zval *default_value = NULL, default_null; + + ZEND_PARSE_PARAMETERS_START(1, 2) + Z_PARAM_ARRAY_HT(keys) + Z_PARAM_OPTIONAL + Z_PARAM_ARRAY_OR_NULL(default_value) + ZEND_PARSE_PARAMETERS_END(); + + if (default_value == NULL) { + ZVAL_NULL(&default_null); + default_value = &default_null; + } + + if (zend_opcache_static_cache_fetch_array_api(&zend_opcache_static_cache_persistent_context_state, keys, default_value, return_value) == FAILURE) { + RETURN_THROWS(); + } +} + +ZEND_FUNCTION(OPcache_persistent_exists) +{ + zend_string *key; + bool exists; + + ZEND_PARSE_PARAMETERS_START(1, 1) + Z_PARAM_STR(key) + ZEND_PARSE_PARAMETERS_END(); + + if (!zend_opcache_static_cache_validate_key(key, 1)) { + RETURN_THROWS(); + } + + if (zend_opcache_static_cache_exists_api(&zend_opcache_static_cache_persistent_context_state, key, &exists) == FAILURE) { + RETURN_THROWS(); + } + + RETURN_BOOL(exists); +} + +ZEND_FUNCTION(OPcache_persistent_lock) +{ + zend_string *key; + bool locked; + + ZEND_PARSE_PARAMETERS_START(1, 1) + Z_PARAM_STR(key) + ZEND_PARSE_PARAMETERS_END(); + + if (!zend_opcache_static_cache_validate_key(key, 1)) { + RETURN_THROWS(); + } + + if (zend_opcache_static_cache_lock_api(&zend_opcache_static_cache_persistent_context_state, key, &locked) == FAILURE) { + RETURN_THROWS(); + } + + RETURN_BOOL(locked); +} + +ZEND_FUNCTION(OPcache_persistent_delete) +{ + zend_opcache_static_cache_context *previous_context; + zend_string *key; + + ZEND_PARSE_PARAMETERS_START(1, 1) + Z_PARAM_STR(key) + ZEND_PARSE_PARAMETERS_END(); + + if (!zend_opcache_static_cache_validate_key(key, 1)) { + RETURN_THROWS(); + } + + previous_context = zend_opcache_static_cache_activate_context(&zend_opcache_static_cache_persistent_context_state); + if (!zend_opcache_static_cache_validate_available_write()) { + zend_opcache_static_cache_restore_context(previous_context); + + RETURN_THROWS(); + } + + if (!zend_opcache_static_cache_explicit_delete_prevalidated(key)) { + zend_opcache_static_cache_restore_context(previous_context); + + RETURN_THROWS(); + } + zend_opcache_static_cache_restore_context(previous_context); +} + +ZEND_FUNCTION(OPcache_persistent_delete_array) +{ + zend_opcache_static_cache_context *previous_context; + zend_string **prepared_keys; + HashTable *keys; + uint32_t key_count, index; + + ZEND_PARSE_PARAMETERS_START(1, 1) + Z_PARAM_ARRAY_HT(keys) + ZEND_PARSE_PARAMETERS_END(); + + if (!zend_opcache_static_cache_prepare_key_list(keys, &prepared_keys, &key_count, 1)) { + RETURN_THROWS(); + } + + previous_context = zend_opcache_static_cache_activate_context(&zend_opcache_static_cache_persistent_context_state); + if (!zend_opcache_static_cache_validate_available_write()) { + zend_opcache_static_cache_restore_context(previous_context); + zend_opcache_static_cache_release_key_list(prepared_keys, key_count); + + RETURN_THROWS(); + } + + for (index = 0; index < key_count; index++) { + if (!zend_opcache_static_cache_explicit_delete_prevalidated(prepared_keys[index])) { + zend_opcache_static_cache_restore_context(previous_context); + zend_opcache_static_cache_release_key_list(prepared_keys, key_count); + + RETURN_THROWS(); + } + } + + zend_opcache_static_cache_restore_context(previous_context); + zend_opcache_static_cache_release_key_list(prepared_keys, key_count); +} + +ZEND_FUNCTION(OPcache_persistent_clear) +{ + zend_opcache_static_cache_context *previous_context; + + ZEND_PARSE_PARAMETERS_NONE(); + + previous_context = zend_opcache_static_cache_activate_context(&zend_opcache_static_cache_persistent_context_state); + if (!zend_opcache_static_cache_validate_available_write()) { + zend_opcache_static_cache_restore_context(previous_context); + + RETURN_THROWS(); + } + + if (!zend_opcache_static_cache_explicit_clear_prevalidated()) { + zend_opcache_static_cache_restore_context(previous_context); + zend_throw_exception_ex(zend_opcache_static_cache_exception_ce, 0, "Unable to clear the persistent cache"); + + RETURN_THROWS(); + } + + zend_opcache_static_cache_mark_publish_skipped(&zend_opcache_static_cache_persistent_context_state); + zend_opcache_static_cache_restore_context(previous_context); +} + +ZEND_FUNCTION(OPcache_persistent_atomic_increment) +{ + zend_opcache_static_cache_context *previous_context; + zend_long step = 1, new_value; + zend_string *key; + bool release_entry_lock = false; + + ZEND_PARSE_PARAMETERS_START(1, 2) + Z_PARAM_STR(key) + Z_PARAM_OPTIONAL + Z_PARAM_LONG(step) + ZEND_PARSE_PARAMETERS_END(); + + if (!zend_opcache_static_cache_validate_key(key, 1)) { + RETURN_THROWS(); + } + + previous_context = zend_opcache_static_cache_activate_context(&zend_opcache_static_cache_persistent_context_state); + if (!zend_opcache_static_cache_begin_entry_mutation(key, &release_entry_lock)) { + zend_opcache_static_cache_restore_context(previous_context); + + RETURN_THROWS(); + } + + if (!zend_opcache_static_cache_require_available_write()) { + if (release_entry_lock) { + zend_opcache_static_cache_release_entry_lock(key); + } + zend_opcache_static_cache_restore_context(previous_context); + + RETURN_THROWS(); + } + + if (!zend_opcache_static_cache_atomic_update_locked(key, step, false, true, &new_value, "Atomic increment requires an integer value")) { + zend_opcache_static_cache_unlock(); + if (release_entry_lock) { + zend_opcache_static_cache_release_entry_lock(key); + } + zend_opcache_static_cache_restore_context(previous_context); + + RETURN_THROWS(); + } + + RETVAL_LONG(new_value); + zend_opcache_static_cache_unlock(); + zend_opcache_static_cache_finish_entry_mutation(key, release_entry_lock, true); + zend_opcache_static_cache_restore_context(previous_context); +} + +ZEND_FUNCTION(OPcache_persistent_atomic_decrement) +{ + zend_opcache_static_cache_context *previous_context; + zend_string *key; + zend_long step = 1, new_value; + bool release_entry_lock = false; + + ZEND_PARSE_PARAMETERS_START(1, 2) + Z_PARAM_STR(key) + Z_PARAM_OPTIONAL + Z_PARAM_LONG(step) + ZEND_PARSE_PARAMETERS_END(); + + if (!zend_opcache_static_cache_validate_key(key, 1)) { + RETURN_THROWS(); + } + + previous_context = zend_opcache_static_cache_activate_context(&zend_opcache_static_cache_persistent_context_state); + if (!zend_opcache_static_cache_begin_entry_mutation(key, &release_entry_lock)) { + zend_opcache_static_cache_restore_context(previous_context); + + RETURN_THROWS(); + } + + if (!zend_opcache_static_cache_require_available_write()) { + if (release_entry_lock) { + zend_opcache_static_cache_release_entry_lock(key); + } + zend_opcache_static_cache_restore_context(previous_context); + + RETURN_THROWS(); + } + + if (!zend_opcache_static_cache_atomic_update_locked(key, step, true, false, &new_value, "Atomic decrement requires an integer value")) { + zend_opcache_static_cache_unlock(); + if (release_entry_lock) { + zend_opcache_static_cache_release_entry_lock(key); + } + zend_opcache_static_cache_restore_context(previous_context); + + RETURN_THROWS(); + } + + RETVAL_LONG(new_value); + zend_opcache_static_cache_unlock(); + zend_opcache_static_cache_finish_entry_mutation(key, release_entry_lock, true); + zend_opcache_static_cache_restore_context(previous_context); +} + +ZEND_FUNCTION(OPcache_persistent_cache_info) +{ + zend_opcache_static_cache_context *previous_context; + + ZEND_PARSE_PARAMETERS_NONE(); + + if (!zend_opcache_validate_api_restriction()) { + zend_throw_exception_ex(zend_opcache_static_cache_exception_ce, 0, ACCELERATOR_PRODUCT_NAME " API access is restricted"); + + RETURN_THROWS(); + } + + previous_context = zend_opcache_static_cache_activate_context(&zend_opcache_static_cache_persistent_context_state); + zend_opcache_static_cache_populate_array(return_value); + zend_opcache_static_cache_restore_context(previous_context); +} diff --git a/ext/opcache/zend_static_cache.h b/ext/opcache/zend_static_cache.h new file mode 100644 index 000000000000..9d748a64ec95 --- /dev/null +++ b/ext/opcache/zend_static_cache.h @@ -0,0 +1,98 @@ +/* + +----------------------------------------------------------------------+ + | Zend OPcache | + +----------------------------------------------------------------------+ + | Copyright © The PHP Group and Contributors. | + +----------------------------------------------------------------------+ + | This source file is subject to the Modified BSD License that is | + | bundled with this package in the file LICENSE, and is available | + | through the World Wide Web at . | + | | + | SPDX-License-Identifier: BSD-3-Clause | + +----------------------------------------------------------------------+ + | Author: Go Kudo | + +----------------------------------------------------------------------+ +*/ + +#ifndef ZEND_STATIC_CACHE_H +#define ZEND_STATIC_CACHE_H + +#include "php.h" + +typedef bool (*zend_opcache_static_cache_safe_direct_clone_value_func_t)( + void *context, + zval *dst, + zval *src +); + +typedef bool (*zend_opcache_static_cache_safe_direct_value_has_unstorable_func_t)( + void *context, + const zval *value +); + +typedef bool (*zend_opcache_static_cache_safe_direct_state_copy_func_t)( + void *context, + zend_object *old_object, + zend_object *new_object, + zend_opcache_static_cache_safe_direct_clone_value_func_t clone_value +); + +typedef bool (*zend_opcache_static_cache_safe_direct_state_has_unstorable_func_t)( + void *context, + const zval *value, + zend_opcache_static_cache_safe_direct_value_has_unstorable_func_t value_has_unstorable +); + +typedef bool (*zend_opcache_static_cache_safe_direct_state_serialize_func_t)( + const zval *object, + zval *state +); + +typedef bool (*zend_opcache_static_cache_safe_direct_state_unserialize_func_t)( + zval *object, + zval *state +); + +#ifndef ZEND_OPCACHE_STATIC_CACHE_SAFE_DIRECT_HANDLERS_FWD +# define ZEND_OPCACHE_STATIC_CACHE_SAFE_DIRECT_HANDLERS_FWD +typedef struct _zend_opcache_static_cache_safe_direct_serializer_path zend_opcache_static_cache_safe_direct_serializer_path; +typedef struct _zend_opcache_static_cache_safe_direct_handlers zend_opcache_static_cache_safe_direct_handlers; +#endif + +#ifndef ZEND_OPCACHE_STATIC_CACHE_SAFE_DIRECT_HANDLERS_DEFINED +# define ZEND_OPCACHE_STATIC_CACHE_SAFE_DIRECT_HANDLERS_DEFINED +struct _zend_opcache_static_cache_safe_direct_serializer_path { + bool state_includes_properties; + zend_function *serialize; + zend_function *unserialize; +}; + +struct _zend_opcache_static_cache_safe_direct_handlers { + bool allows_custom_serializers; + zend_opcache_static_cache_safe_direct_serializer_path serializer_path; + zend_opcache_static_cache_safe_direct_state_copy_func_t copy; + zend_opcache_static_cache_safe_direct_state_has_unstorable_func_t state_has_unstorable; + zend_opcache_static_cache_safe_direct_state_serialize_func_t state_serialize; + zend_opcache_static_cache_safe_direct_state_unserialize_func_t state_unserialize; +}; +#endif + +BEGIN_EXTERN_C() + +zend_result zend_opcache_register_functions(int module_type); +zend_result zend_opcache_static_cache_minit(void); +void zend_opcache_static_cache_mshutdown(void); +zend_result zend_opcache_static_cache_rshutdown(void); +void zend_opcache_static_cache_invalidate_all(void); +void zend_opcache_static_cache_volatile_get_status(zval *return_value); +void zend_opcache_static_cache_persistent_get_status(zval *return_value); +bool zend_opcache_static_cache_volatile_is_enabled(void); +bool zend_opcache_static_cache_volatile_is_available(void); +const char *zend_opcache_static_cache_volatile_failure_reason(void); +bool zend_opcache_static_cache_persistent_is_enabled(void); +bool zend_opcache_static_cache_persistent_is_available(void); +const char *zend_opcache_static_cache_persistent_failure_reason(void); + +END_EXTERN_C() + +#endif /* ZEND_STATIC_CACHE_H */ diff --git a/ext/opcache/zend_static_cache_entries.c b/ext/opcache/zend_static_cache_entries.c new file mode 100644 index 000000000000..743938da518a --- /dev/null +++ b/ext/opcache/zend_static_cache_entries.c @@ -0,0 +1,2038 @@ +/* + +----------------------------------------------------------------------+ + | Zend OPcache | + +----------------------------------------------------------------------+ + | Copyright © The PHP Group and Contributors. | + +----------------------------------------------------------------------+ + | This source file is subject to the Modified BSD License that is | + | bundled with this package in the file LICENSE, and is available | + | through the World Wide Web at . | + | | + | SPDX-License-Identifier: BSD-3-Clause | + +----------------------------------------------------------------------+ + | Author: Go Kudo | + +----------------------------------------------------------------------+ +*/ + +#include "zend_static_cache_internal.h" +#include "zend_opcache_serializer.h" + +#include "Zend/zend_closures.h" +#include "Zend/zend_objects.h" + +typedef struct _zend_opcache_static_cache_request_local_clone_context { + HashTable arrays; + HashTable objects; + HashTable references; +} zend_opcache_static_cache_request_local_clone_context; + +/* Value graph cloning is mutually recursive across arrays, references, and objects. */ +static bool zend_opcache_static_cache_clone_request_local_value( + zend_opcache_static_cache_request_local_clone_context *context, + zval *dst, + zval *src +); + +static zend_always_inline void zend_opcache_static_cache_release_value_storage_locked(uint8_t value_type, uint32_t value_offset) +{ + if (value_offset == 0) { + return; + } + + if (value_type == ZEND_OPCACHE_STATIC_CACHE_VALUE_SHARED_GRAPH) { + if (zend_opcache_static_cache_shared_graph_retire_payload_locked(value_offset)) { + zend_opcache_static_cache_free_locked(value_offset); + } + } else if (zend_opcache_static_cache_value_uses_offset(value_type)) { + zend_opcache_static_cache_free_locked(value_offset); + } +} + +static zend_always_inline void zend_opcache_static_cache_release_entry_storage_locked(zend_opcache_static_cache_entry *entry) +{ + bool uses_combined_value_key = (entry->reserved & ZEND_OPCACHE_STATIC_CACHE_ENTRY_RESERVED_COMBINED_VALUE_KEY) != 0; + + if (entry->key_offset != 0 && !uses_combined_value_key) { + zend_opcache_static_cache_free_locked(entry->key_offset); + } + + zend_opcache_static_cache_release_value_storage_locked(entry->value_type, entry->value_offset); +} + +static zend_always_inline void zend_opcache_static_cache_delete_entry_locked(zend_opcache_static_cache_entry *entry, zend_opcache_static_cache_header *header) +{ + if (entry->state == ZEND_OPCACHE_STATIC_CACHE_ENTRY_USED && header->count != 0) { + header->count--; + } + + zend_opcache_static_cache_release_entry_storage_locked(entry); + entry->hash = 0; + entry->key_offset = 0; + entry->key_len = 0; + entry->state = ZEND_OPCACHE_STATIC_CACHE_ENTRY_TOMBSTONE; + entry->value_type = ZEND_OPCACHE_STATIC_CACHE_VALUE_NULL; + entry->value_offset = 0; + entry->value_len = 0; + entry->reserved = 0; + entry->expires_at = 0; + entry->long_value = 0; + entry->double_value = 0; + zend_opcache_static_cache_bump_mutation_epoch_locked(header); +} + +static void zend_opcache_static_cache_request_local_slot_dtor(zval *slot_zv) +{ + zend_opcache_static_cache_request_local_slot *slot = Z_PTR_P(slot_zv); + + if (!Z_ISUNDEF(slot->value)) { + zval_ptr_dtor(&slot->value); + } + efree(slot); +} + +static bool zend_opcache_static_cache_value_needs_request_local_clone_inner( + zval *value, + HashTable *visited_arrays) +{ + zend_ulong array_key; + zval *element; + + if (Z_ISREF_P(value)) { + return true; + } + + switch (Z_TYPE_P(value)) { + case IS_OBJECT: + return true; + case IS_ARRAY: + array_key = (zend_ulong) (uintptr_t) Z_ARRVAL_P(value); + if (zend_hash_index_exists(visited_arrays, array_key)) { + return false; + } + + zend_hash_index_add_empty_element(visited_arrays, array_key); + ZEND_HASH_FOREACH_VAL(Z_ARRVAL_P(value), element) { + if (zend_opcache_static_cache_value_needs_request_local_clone_inner(element, visited_arrays)) { + return true; + } + } ZEND_HASH_FOREACH_END(); + + return false; + default: + return false; + } +} + +static bool zend_opcache_static_cache_value_needs_request_local_clone(zval *value) +{ + HashTable visited_arrays; + bool result; + + if (Z_ISREF_P(value)) { + return true; + } + if (Z_TYPE_P(value) == IS_OBJECT) { + return true; + } + if (Z_TYPE_P(value) != IS_ARRAY) { + return false; + } + + zend_hash_init(&visited_arrays, 8, NULL, NULL, 0); + result = zend_opcache_static_cache_value_needs_request_local_clone_inner(value, &visited_arrays); + zend_hash_destroy(&visited_arrays); + + return result; +} + +static void zend_opcache_static_cache_request_local_clone_object_dtor(zval *object_zv) +{ + zend_object *object = Z_PTR_P(object_zv); + + OBJ_RELEASE(object); +} + +static void zend_opcache_static_cache_request_local_clone_array_dtor(zval *array_zv) +{ + zend_array *array = Z_PTR_P(array_zv); + + zend_array_release(array); +} + +static void zend_opcache_static_cache_request_local_clone_reference_dtor(zval *reference_zv) +{ + zval ref_zv; + + ZVAL_REF(&ref_zv, (zend_reference *) Z_PTR_P(reference_zv)); + zval_ptr_dtor(&ref_zv); +} + +static void zend_opcache_static_cache_request_local_clone_context_init( + zend_opcache_static_cache_request_local_clone_context *context) +{ + zend_hash_init(&context->arrays, 8, NULL, zend_opcache_static_cache_request_local_clone_array_dtor, 0); + zend_hash_init(&context->objects, 8, NULL, zend_opcache_static_cache_request_local_clone_object_dtor, 0); + zend_hash_init(&context->references, 8, NULL, zend_opcache_static_cache_request_local_clone_reference_dtor, 0); +} + +static void zend_opcache_static_cache_request_local_clone_context_destroy( + zend_opcache_static_cache_request_local_clone_context *context) +{ + zend_hash_destroy(&context->references); + zend_hash_destroy(&context->objects); + zend_hash_destroy(&context->arrays); +} + +static bool zend_opcache_static_cache_clone_request_local_array_ex( + zend_opcache_static_cache_request_local_clone_context *context, + zval *dst, + zval *src, + bool known_needs_clone) +{ + zend_ulong key = (zend_ulong) (uintptr_t) Z_ARRVAL_P(src); + zend_array *array = zend_hash_index_find_ptr(&context->arrays, key); + zval *element, cloned_element; + + if (!known_needs_clone && !zend_opcache_static_cache_value_needs_request_local_clone(src)) { + ZVAL_COPY(dst, src); + + return true; + } + + if (array != NULL) { + GC_ADDREF(array); + ZVAL_ARR(dst, array); + + return true; + } + + array = zend_array_dup(Z_ARRVAL_P(src)); + zend_hash_index_update_ptr(&context->arrays, key, array); + GC_ADDREF(array); + ZVAL_ARR(dst, array); + ZEND_HASH_FOREACH_VAL(Z_ARRVAL_P(dst), element) { + if (Z_TYPE_P(element) == IS_INDIRECT || + !zend_opcache_static_cache_value_needs_request_local_clone(element) + ) { + continue; + } + + if (!zend_opcache_static_cache_clone_request_local_value(context, &cloned_element, element)) { + zval_ptr_dtor(dst); + ZVAL_UNDEF(dst); + + return false; + } + + zval_ptr_dtor(element); + ZVAL_COPY_VALUE(element, &cloned_element); + } ZEND_HASH_FOREACH_END(); + + return true; +} + +static bool zend_opcache_static_cache_clone_request_local_array( + zend_opcache_static_cache_request_local_clone_context *context, + zval *dst, + zval *src) +{ + return zend_opcache_static_cache_clone_request_local_array_ex(context, dst, src, false); +} + +static bool zend_opcache_static_cache_clone_request_local_reference( + zend_opcache_static_cache_request_local_clone_context *context, + zval *dst, + zend_reference *src_ref) +{ + zend_ulong key = (zend_ulong) (uintptr_t) src_ref; + zend_reference *new_ref = zend_hash_index_find_ptr(&context->references, key); + zval inner; + + if (new_ref != NULL) { + GC_ADDREF(new_ref); + ZVAL_REF(dst, new_ref); + + return true; + } + + ZVAL_NEW_EMPTY_REF(dst); + new_ref = Z_REF_P(dst); + ZVAL_UNDEF(&new_ref->val); + zend_hash_index_update_ptr(&context->references, key, new_ref); + + if (!zend_opcache_static_cache_clone_request_local_value(context, &inner, &src_ref->val)) { + ZVAL_UNDEF(dst); + + return false; + } + + ZVAL_COPY_VALUE(&new_ref->val, &inner); + GC_ADDREF(new_ref); + ZVAL_REF(dst, new_ref); + + return true; +} + +static bool zend_opcache_static_cache_clone_request_local_object_members( + zend_opcache_static_cache_request_local_clone_context *context, + zend_object *old_object, + zend_object *new_object) +{ + zend_ulong num_key; + zend_string *key; + zval *src, *dst, *end, *prop, new_prop; + + if (old_object->ce->default_properties_count) { + src = old_object->properties_table; + dst = new_object->properties_table; + end = src + old_object->ce->default_properties_count; + do { + if (!zend_opcache_static_cache_clone_request_local_value(context, &new_prop, src)) { + return false; + } + + zval_ptr_dtor(dst); + ZVAL_COPY_VALUE(dst, &new_prop); + Z_PROP_FLAG_P(dst) = Z_PROP_FLAG_P(src); + src++; + dst++; + } while (src != end); + } + + if (old_object->properties != NULL && zend_hash_num_elements(old_object->properties) != 0) { + if (new_object->properties != NULL) { + zend_hash_clean(new_object->properties); + zend_hash_extend(new_object->properties, zend_hash_num_elements(old_object->properties), 0); + } else { + new_object->properties = zend_new_array(zend_hash_num_elements(old_object->properties)); + zend_hash_real_init_mixed(new_object->properties); + } + + HT_FLAGS(new_object->properties) |= + HT_FLAGS(old_object->properties) & HASH_FLAG_HAS_EMPTY_IND; + + ZEND_HASH_MAP_FOREACH_KEY_VAL(old_object->properties, num_key, key, prop) { + if (Z_TYPE_P(prop) == IS_INDIRECT) { + ZVAL_INDIRECT( + &new_prop, + new_object->properties_table + (Z_INDIRECT_P(prop) - old_object->properties_table) + ); + } else if (!zend_opcache_static_cache_clone_request_local_value(context, &new_prop, prop)) { + return false; + } + + if (key != NULL) { + _zend_hash_append(new_object->properties, key, &new_prop); + } else { + zend_hash_index_add_new(new_object->properties, num_key, &new_prop); + } + } ZEND_HASH_FOREACH_END(); + } + + return true; +} + +static bool zend_opcache_static_cache_clone_request_local_std_object( + zend_opcache_static_cache_request_local_clone_context *context, + zend_object *old_object, + zend_object **new_object_ptr) +{ + zend_object *new_object; + zval *dst, *end; + + new_object = zend_objects_new(old_object->ce); + + if (new_object->ce->default_properties_count) { + dst = new_object->properties_table; + end = dst + new_object->ce->default_properties_count; + do { + ZVAL_UNDEF(dst); + dst++; + } while (dst != end); + } + + zend_hash_index_update_ptr( + &context->objects, + (zend_ulong) (uintptr_t) old_object, + new_object + ); + + if (!zend_opcache_static_cache_clone_request_local_object_members(context, old_object, new_object)) { + return false; + } + + *new_object_ptr = new_object; + + return true; +} + +static bool zend_opcache_static_cache_clone_request_local_value_callback( + void *context, + zval *dst, + zval *src) +{ + return zend_opcache_static_cache_clone_request_local_value( + (zend_opcache_static_cache_request_local_clone_context *) context, + dst, + src + ); +} + +static bool zend_opcache_static_cache_clone_request_local_safe_direct_object( + zend_opcache_static_cache_request_local_clone_context *context, + zend_object *old_object, + zend_object **new_object_ptr) +{ + zend_class_entry *ce = old_object->ce, *base_ce = NULL; + zend_opcache_static_cache_safe_direct_state_copy_func_t copy_func; + zend_object *new_object; + zend_ulong key; + zval new_zv; + + copy_func = zend_opcache_static_cache_safe_direct_copy_func(ce, &base_ce); + if (copy_func == NULL || zend_opcache_serializer_has_safe_direct_cache_overrides(ce, base_ce)) { + return false; + } + + ZVAL_UNDEF(&new_zv); + if (object_init_ex(&new_zv, ce) != SUCCESS) { + return false; + } + + new_object = Z_OBJ(new_zv); + key = (zend_ulong) (uintptr_t) old_object; + zend_hash_index_update_ptr(&context->objects, key, new_object); + + if (!copy_func( + context, + old_object, + new_object, + zend_opcache_static_cache_clone_request_local_value_callback + )) { + return false; + } + + if (!zend_opcache_static_cache_clone_request_local_object_members(context, old_object, new_object)) { + return false; + } + + *new_object_ptr = new_object; + + return true; +} + +static bool zend_opcache_static_cache_clone_request_local_object( + zend_opcache_static_cache_request_local_clone_context *context, + zend_object *old_object, + zend_object **new_object_ptr) +{ + zend_object *new_object; + zend_ulong key; + + if (old_object == NULL || zend_object_is_lazy(old_object)) { + return false; + } + + key = (zend_ulong) (uintptr_t) old_object; + new_object = zend_hash_index_find_ptr(&context->objects, key); + + if (new_object != NULL) { + *new_object_ptr = new_object; + + return true; + } + + if (old_object->handlers == zend_get_std_object_handlers()) { + return zend_opcache_static_cache_clone_request_local_std_object(context, old_object, new_object_ptr); + } + + return zend_opcache_static_cache_clone_request_local_safe_direct_object(context, old_object, new_object_ptr); +} + +static bool zend_opcache_static_cache_clone_request_local_value( + zend_opcache_static_cache_request_local_clone_context *context, + zval *dst, + zval *src) +{ + zend_object *object; + + if (Z_ISREF_P(src)) { + return zend_opcache_static_cache_clone_request_local_reference(context, dst, Z_REF_P(src)); + } + + switch (Z_TYPE_P(src)) { + case IS_ARRAY: + return zend_opcache_static_cache_clone_request_local_array(context, dst, src); + case IS_OBJECT: + if (!zend_opcache_static_cache_clone_request_local_object(context, Z_OBJ_P(src), &object)) { + return false; + } + + GC_ADDREF(object); + ZVAL_OBJ(dst, object); + + return true; + default: + ZVAL_COPY(dst, src); + + return true; + } +} + +static bool zend_opcache_static_cache_clone_request_local_slot_value(zval *dst, zval *src) +{ + zend_opcache_static_cache_request_local_clone_context context; + bool result; + + if (Z_TYPE_P(src) == IS_ARRAY) { + if (!zend_opcache_static_cache_value_needs_request_local_clone(src)) { + ZVAL_COPY(dst, src); + + return true; + } + + zend_opcache_static_cache_request_local_clone_context_init(&context); + result = zend_opcache_static_cache_clone_request_local_array_ex(&context, dst, src, true); + zend_opcache_static_cache_request_local_clone_context_destroy(&context); + + return result; + } + + if (!zend_opcache_static_cache_value_needs_request_local_clone(src)) { + ZVAL_COPY(dst, src); + + return true; + } + + zend_opcache_static_cache_request_local_clone_context_init(&context); + result = zend_opcache_static_cache_clone_request_local_value(&context, dst, src); + zend_opcache_static_cache_request_local_clone_context_destroy(&context); + + return result; +} + +static HashTable *zend_opcache_static_cache_request_local_slots(void) +{ + HashTable **slots_ptr = zend_opcache_static_cache_active_request_local_slots_ptr(); + + if (*slots_ptr == NULL) { + ALLOC_HASHTABLE(*slots_ptr); + zend_hash_init(*slots_ptr, 0, NULL, zend_opcache_static_cache_request_local_slot_dtor, 0); + } + + return *slots_ptr; +} + +static zend_never_inline void zend_opcache_static_cache_reacquire_read_lock_or_fail(const char *cache_name) +{ + if (!zend_opcache_static_cache_rlock()) { + zend_error_noreturn(E_ERROR, "Unable to reacquire the %s read lock after userland execution", cache_name); + } +} + +static zend_never_inline bool zend_opcache_static_cache_reacquire_write_lock_or_fail(const char *cache_name) +{ + if (!zend_opcache_static_cache_wlock()) { + zend_error_noreturn(E_ERROR, "Unable to reacquire the %s write lock after userland execution", cache_name); + } + + if (!zend_opcache_static_cache_header_init_locked()) { + zend_throw_exception_ex(zend_opcache_static_cache_exception_ce, 0, "Unable to initialize the %s header", cache_name); + + return false; + } + + return true; +} + +static bool zend_opcache_static_cache_materialize_payload_locked( + zend_string *key, + uint8_t value_type, + uint32_t value_offset, + uint32_t value_len, + zval *return_value, + bool throw_if_missing, + const char *cache_name) +{ + const unsigned char *position, *end; + php_unserialize_data_t var_hash; + uint32_t copied_value_len; + bool ref_registered; + unsigned char *payload_copy; + int result; + + switch (value_type) { + case ZEND_OPCACHE_STATIC_CACHE_VALUE_SERIALIZED: + copied_value_len = value_len; + payload_copy = emalloc(copied_value_len); + memcpy(payload_copy, zend_opcache_static_cache_ptr(value_offset), copied_value_len); + zend_opcache_static_cache_unlock(); + + ZVAL_UNDEF(return_value); + position = payload_copy; + end = position + copied_value_len; + PHP_VAR_UNSERIALIZE_INIT(var_hash); + result = php_var_unserialize(return_value, &position, end, &var_hash); + PHP_VAR_UNSERIALIZE_DESTROY(var_hash); + efree(payload_copy); + + zend_opcache_static_cache_reacquire_read_lock_or_fail(cache_name); + + if (result != 1 || position != end) { + if (Z_TYPE_P(return_value) != IS_UNDEF) { + zval_ptr_dtor(return_value); + ZVAL_UNDEF(return_value); + } + + if (!EG(exception) && throw_if_missing) { + zend_throw_exception_ex(zend_opcache_static_cache_exception_ce, 0, "Stored %s value for key \"%s\" is corrupted", cache_name, ZSTR_VAL(key)); + } + + return false; + } + + return true; + case ZEND_OPCACHE_STATIC_CACHE_VALUE_OPCACHE_SERIALIZED: + copied_value_len = value_len; + payload_copy = emalloc(copied_value_len); + memcpy(payload_copy, zend_opcache_static_cache_ptr(value_offset), copied_value_len); + zend_opcache_static_cache_unlock(); + + ZVAL_UNDEF(return_value); + result = (zend_opcache_static_cache_capture_active + ? zend_opcache_unserialize_ex( + return_value, + payload_copy, + copied_value_len, + zend_opcache_static_cache_capture_decoded_value, + zend_opcache_static_cache_capture_decoded_reachable_value + ) + : zend_opcache_unserialize( + return_value, + payload_copy, + copied_value_len + ) + ); + efree(payload_copy); + + zend_opcache_static_cache_reacquire_read_lock_or_fail(cache_name); + + if (!result) { + if (Z_TYPE_P(return_value) != IS_UNDEF) { + zval_ptr_dtor(return_value); + ZVAL_UNDEF(return_value); + } + + if (!EG(exception) && throw_if_missing) { + zend_throw_exception_ex(zend_opcache_static_cache_exception_ce, 0, "Stored %s value for key \"%s\" is corrupted", cache_name, ZSTR_VAL(key)); + } + + return false; + } + + return true; + case ZEND_OPCACHE_STATIC_CACHE_VALUE_SHARED_GRAPH: + ref_registered = zend_opcache_static_cache_has_request_shared_graph_ref(value_offset); + if (!ref_registered && !zend_opcache_static_cache_shared_graph_acquire_locked(value_offset)) { + if (throw_if_missing) { + zend_throw_exception_ex(zend_opcache_static_cache_exception_ce, 0, "Stored %s value for key \"%s\" is corrupted", cache_name, ZSTR_VAL(key)); + } + + return false; + } + + zend_opcache_static_cache_unlock(); + ZVAL_UNDEF(return_value); + result = zend_opcache_static_cache_fetch_shared_graph( + (const unsigned char *) zend_opcache_static_cache_ptr(value_offset), + value_len, + return_value + ); + zend_opcache_static_cache_reacquire_read_lock_or_fail(cache_name); + + if (!result) { + if (Z_TYPE_P(return_value) != IS_UNDEF) { + zval_ptr_dtor(return_value); + ZVAL_UNDEF(return_value); + } + + if (!ref_registered && zend_opcache_static_cache_shared_graph_release_ref_locked(value_offset)) { + zend_opcache_static_cache_defer_retired_shared_graph_free(value_offset); + } + + if (!EG(exception) && throw_if_missing) { + zend_throw_exception_ex(zend_opcache_static_cache_exception_ce, 0, "Stored %s value for key \"%s\" is corrupted", cache_name, ZSTR_VAL(key)); + } + + return false; + } + + if (!ref_registered) { + zend_opcache_static_cache_register_shared_graph_ref(value_offset); + } + + return true; + default: + return false; + } +} + +static bool zend_opcache_static_cache_fetch_request_local_slot(zend_string *key, uint64_t mutation_epoch, zval *return_value) +{ + zend_opcache_static_cache_request_local_slot *slot; + HashTable **slots_ptr = zend_opcache_static_cache_active_request_local_slots_ptr(); + + if (*slots_ptr == NULL) { + return false; + } + + slot = zend_hash_find_ptr(*slots_ptr, key); + if (slot == NULL) { + return false; + } + + if (slot->mutation_epoch != mutation_epoch) { + zend_hash_del(*slots_ptr, key); + + return false; + } + + if (!zend_opcache_static_cache_clone_request_local_slot_value(return_value, &slot->value)) { + zend_hash_del(*slots_ptr, key); + + return false; + } + + return true; +} + +static void zend_opcache_static_cache_store_request_local_value_slot(zend_string *key, uint64_t mutation_epoch, zval *value) +{ + zend_opcache_static_cache_request_local_slot *slot; + zval slot_zv; + + slot = emalloc(sizeof(zend_opcache_static_cache_request_local_slot)); + slot->mutation_epoch = mutation_epoch; + ZVAL_UNDEF(&slot->value); + if (!zend_opcache_static_cache_clone_request_local_slot_value(&slot->value, value)) { + ZVAL_PTR(&slot_zv, slot); + zend_opcache_static_cache_request_local_slot_dtor(&slot_zv); + + return; + } + zend_hash_update_ptr(zend_opcache_static_cache_request_local_slots(), key, slot); +} + +static bool zend_opcache_static_cache_fetch_finish( + zend_string *key, + uint64_t mutation_epoch, + zval *return_value, + bool use_request_local_slot) +{ + if (use_request_local_slot) { + /* Keep a request-local prototype. Later fetches clone object handles out + * of this prototype instead of rematerializing from SHM. */ + zend_opcache_static_cache_store_request_local_value_slot(key, mutation_epoch, return_value); + } + + return true; +} + +static void zend_opcache_static_cache_release_request_local_slot_context(HashTable **slots_ptr) +{ + if (*slots_ptr == NULL) { + return; + } + + zend_hash_destroy(*slots_ptr); + FREE_HASHTABLE(*slots_ptr); + *slots_ptr = NULL; +} + +static bool zend_opcache_static_cache_is_expired(const zend_opcache_static_cache_entry *entry, uint64_t now) +{ + return entry->state == ZEND_OPCACHE_STATIC_CACHE_ENTRY_USED && entry->expires_at != 0 && entry->expires_at <= now; +} + +static bool zend_opcache_static_cache_maybe_expired(const zend_opcache_static_cache_entry *entry, uint64_t *now) +{ + if (entry->state != ZEND_OPCACHE_STATIC_CACHE_ENTRY_USED || entry->expires_at == 0) { + return false; + } + + if (*now == 0) { + *now = (uint64_t) time(NULL); + } + + return zend_opcache_static_cache_is_expired(entry, *now); +} + +static bool zend_opcache_static_cache_find_slot_in_header_locked( + zend_opcache_static_cache_header *header, + zend_string *key, + zend_ulong hash, + uint32_t *slot_index, + bool *found, + bool delete_expired) +{ + zend_opcache_static_cache_entry *entries, *entry; + uint64_t now = 0; + uint32_t index, first_tombstone = UINT32_MAX, step; + + if (header == NULL) { + return false; + } + + entries = zend_opcache_static_cache_entries(header); + index = (uint32_t) (hash % header->capacity); + + for (step = 0; step < header->capacity; step++) { + entry = &entries[index]; + + if (entry->state == ZEND_OPCACHE_STATIC_CACHE_ENTRY_EMPTY) { + *slot_index = first_tombstone != UINT32_MAX ? first_tombstone : index; + *found = false; + + return true; + } + + if (entry->state == ZEND_OPCACHE_STATIC_CACHE_ENTRY_TOMBSTONE) { + if (first_tombstone == UINT32_MAX) { + first_tombstone = index; + } + } else if (zend_opcache_static_cache_maybe_expired(entry, &now)) { + if (delete_expired) { + zend_opcache_static_cache_delete_entry_locked(entry, header); + } + + if (first_tombstone == UINT32_MAX) { + first_tombstone = index; + } + } else if (zend_opcache_static_cache_key_equals(entry, key, hash)) { + *slot_index = index; + *found = true; + + return true; + } + + ++index; + + if (index == header->capacity) { + index = 0; + } + } + + if (first_tombstone != UINT32_MAX) { + *slot_index = first_tombstone; + *found = false; + + return true; + } + + return false; +} + +static bool zend_opcache_static_cache_find_slot_locked( + zend_string *key, + zend_ulong hash, + zend_opcache_static_cache_header **header_ptr, + uint32_t *slot_index, + bool *found, + bool delete_expired) +{ + zend_opcache_static_cache_header *header = zend_opcache_static_cache_header_ptr(); + + if (!header || !zend_opcache_static_cache_header_init_locked()) { + return false; + } + + if (header_ptr != NULL) { + *header_ptr = header; + } + + return zend_opcache_static_cache_find_slot_in_header_locked(header, key, hash, slot_index, found, delete_expired); +} + +static bool zend_opcache_static_cache_find_slot_for_read_locked( + zend_string *key, + zend_ulong hash, + zend_opcache_static_cache_header **header_ptr, + uint32_t *slot_index, + bool *found) +{ + return zend_opcache_static_cache_find_slot_locked(key, hash, header_ptr, slot_index, found, false); +} + +static bool zend_opcache_static_cache_find_slot_for_write_locked( + zend_string *key, + zend_ulong hash, + zend_opcache_static_cache_header **header_ptr, + uint32_t *slot_index, + bool *found) +{ + return zend_opcache_static_cache_find_slot_locked(key, hash, header_ptr, slot_index, found, true); +} + +static bool zend_opcache_static_cache_expunge_expired_locked(void) +{ + zend_opcache_static_cache_header *header = zend_opcache_static_cache_header_ptr(); + zend_opcache_static_cache_entry *entries; + uint64_t now; + uint32_t index; + bool removed = false; + + if (!header || !zend_opcache_static_cache_header_init_locked()) { + return false; + } + + now = (uint64_t) time(NULL); + entries = zend_opcache_static_cache_entries(header); + for (index = 0; index < header->capacity; index++) { + if (zend_opcache_static_cache_is_expired(&entries[index], now)) { + zend_opcache_static_cache_delete_entry_locked(&entries[index], header); + removed = true; + } + } + + return removed; +} + +static bool zend_opcache_static_cache_payload_can_fit_locked(size_t size) +{ + zend_opcache_static_cache_header *header = zend_opcache_static_cache_header_ptr(); + size_t total_size; + + if (!header || size == 0 || size > UINT32_MAX - sizeof(zend_opcache_static_cache_block)) { + return false; + } + + total_size = ZEND_ALIGNED_SIZE(sizeof(zend_opcache_static_cache_block) + size); + + return total_size <= UINT32_MAX && total_size <= header->data_size; +} + +static void zend_opcache_static_cache_handle_store_failure(const char *failure_message, bool throw_on_failure) +{ + zend_opcache_static_cache_context *context = zend_opcache_static_cache_active_context(); + + if (context->strict_store_failure && !throw_on_failure) { + zend_opcache_static_cache_mark_publish_skipped(context); + zend_throw_exception_ex(zend_opcache_static_cache_exception_ce, 0, "%s", failure_message); + + return; + } + + if (throw_on_failure) { + zend_throw_exception_ex(zend_opcache_static_cache_exception_ce, 0, "%s", failure_message); + } +} + +static void zend_opcache_static_cache_handle_store_failure_locked(const char *failure_message, bool throw_on_failure) +{ + zend_opcache_static_cache_handle_store_failure(failure_message, throw_on_failure); +} + +static bool zend_opcache_static_cache_find_unstorable_value( + zval *value, + HashTable *seen_arrays, + HashTable *seen_objects, + const char **failure_message) +{ + zend_object *object; + zend_ulong key; + zval *element, *property, *end; + + ZVAL_DEREF(value); + if (Z_TYPE_P(value) == IS_RESOURCE) { + *failure_message = "resources cannot be stored in the static cache"; + + return true; + } + + if (Z_TYPE_P(value) == IS_OBJECT && Z_OBJCE_P(value) == zend_ce_closure) { + *failure_message = "Closure objects cannot be stored in the static cache"; + + return true; + } + + if (Z_TYPE_P(value) == IS_ARRAY) { + key = (zend_ulong) (uintptr_t) Z_ARR_P(value); + if (zend_hash_index_exists(seen_arrays, key)) { + return false; + } + zend_hash_index_add_empty_element(seen_arrays, key); + + ZEND_HASH_FOREACH_VAL(Z_ARRVAL_P(value), element) { + if (zend_opcache_static_cache_find_unstorable_value(element, seen_arrays, seen_objects, failure_message)) { + return true; + } + } ZEND_HASH_FOREACH_END(); + + return false; + } + + if (Z_TYPE_P(value) == IS_OBJECT) { + object = Z_OBJ_P(value); + key = (zend_ulong) (uintptr_t) object; + if (zend_hash_index_exists(seen_objects, key)) { + return false; + } + zend_hash_index_add_empty_element(seen_objects, key); + + if (object->ce->default_properties_count != 0) { + property = object->properties_table; + end = property + object->ce->default_properties_count; + do { + if (Z_TYPE_P(property) != IS_UNDEF && + zend_opcache_static_cache_find_unstorable_value(property, seen_arrays, seen_objects, failure_message) + ) { + return true; + } + property++; + } while (property != end); + } + + if (object->properties != NULL) { + ZEND_HASH_FOREACH_VAL(object->properties, element) { + if (Z_TYPE_P(element) == IS_INDIRECT) { + element = Z_INDIRECT_P(element); + if (Z_TYPE_P(element) == IS_UNDEF) { + continue; + } + } + + if (zend_opcache_static_cache_find_unstorable_value(element, seen_arrays, seen_objects, failure_message)) { + return true; + } + } ZEND_HASH_FOREACH_END(); + } + } + + return false; +} + +static bool zend_opcache_static_cache_validate_storable_value(zval *value, bool throw_on_failure) +{ + const char *failure_message = NULL; + HashTable seen_arrays, seen_objects; + bool found; + + ZVAL_DEREF(value); + if (Z_TYPE_P(value) != IS_ARRAY && Z_TYPE_P(value) != IS_OBJECT) { + return true; + } + + zend_hash_init(&seen_arrays, 8, NULL, NULL, 0); + zend_hash_init(&seen_objects, 8, NULL, NULL, 0); + found = zend_opcache_static_cache_find_unstorable_value(value, &seen_arrays, &seen_objects, &failure_message); + zend_hash_destroy(&seen_objects); + zend_hash_destroy(&seen_arrays); + if (EG(exception)) { + return false; + } + + if (!found) { + return true; + } + + if (failure_message != NULL) { + zend_opcache_static_cache_handle_store_failure(failure_message, throw_on_failure); + } + + return false; +} + +static unsigned char *zend_opcache_static_cache_reserve_combined_offset_value_locked( + uint32_t reusable_offset, + zend_string *key, + size_t payload_size, + uint32_t *value_offset, + uint32_t *key_offset) +{ + uint32_t base_offset; + size_t key_size, total_size; + + key_size = ZSTR_LEN(key) + 1; + if (payload_size > SIZE_MAX - key_size) { + return NULL; + } + + total_size = payload_size + key_size; + if (reusable_offset != 0 && zend_opcache_static_cache_block_payload_capacity(reusable_offset) >= total_size) { + base_offset = reusable_offset; + } else { + base_offset = zend_opcache_static_cache_alloc_locked(total_size, NULL); + if (base_offset == 0) { + return NULL; + } + } + + *value_offset = base_offset; + *key_offset = base_offset + (uint32_t) payload_size; + + return (unsigned char *) zend_opcache_static_cache_ptr(base_offset); +} + +static bool zend_opcache_static_cache_publish_combined_offset_value_locked( + uint32_t reusable_offset, + zend_string *key, + size_t payload_size, + const void *payload_source, + uint32_t *value_offset, + uint32_t *key_offset) +{ + unsigned char *payload; + + if (payload_source == NULL) { + return false; + } + + payload = zend_opcache_static_cache_reserve_combined_offset_value_locked( + reusable_offset, + key, + payload_size, + value_offset, + key_offset + ); + if (payload == NULL) { + return false; + } + + memcpy(payload, payload_source, payload_size); + memcpy(payload + payload_size, ZSTR_VAL(key), ZSTR_LEN(key) + 1); + + return true; +} + +static bool zend_opcache_static_cache_retry_store_after_pressure_locked( + bool *retried_expired, + bool *retried_compact, + bool *retried_clear, + size_t required_payload_size, + bool allow_clear) +{ + if (!*retried_expired) { + *retried_expired = true; + + if (zend_opcache_static_cache_expunge_expired_locked()) { + return true; + } + } + + if (required_payload_size != 0 && !*retried_compact) { + if (zend_opcache_static_cache_compact_to_fit_locked(required_payload_size)) { + *retried_compact = true; + + return true; + } + } + + if (allow_clear && zend_opcache_static_cache_active_context()->clear_on_pressure && !*retried_clear) { + *retried_clear = true; + + return zend_opcache_static_cache_has_all_entry_locks() && zend_opcache_static_cache_clear_locked(); + } + + return false; +} + +static void zend_opcache_static_cache_init_prepared_value(zend_opcache_static_cache_prepared_value *prepared) +{ + memset(prepared, 0, sizeof(*prepared)); + prepared->value_type = ZEND_OPCACHE_STATIC_CACHE_VALUE_NULL; +} + +bool zend_opcache_static_cache_clear_locked(void) +{ + zend_opcache_static_cache_header *header = zend_opcache_static_cache_header_ptr(); + zend_opcache_static_cache_entry *entries; + uint64_t mutation_epoch; + uint32_t index; + + if (!header || !zend_opcache_static_cache_header_init_locked()) { + return false; + } + + mutation_epoch = header->mutation_epoch; + entries = zend_opcache_static_cache_entries(header); + for (index = 0; index < header->capacity; index++) { + if (entries[index].state == ZEND_OPCACHE_STATIC_CACHE_ENTRY_USED) { + zend_opcache_static_cache_release_entry_storage_locked(&entries[index]); + } + } + + memset(entries, 0, sizeof(zend_opcache_static_cache_entry) * header->capacity); + + header->count = 0; + header->mutation_epoch = mutation_epoch + 1; + if (header->mutation_epoch == 0) { + header->mutation_epoch = 1; + } + + zend_opcache_static_cache_mark_publish_skipped(zend_opcache_static_cache_active_context()); + + return true; +} + +bool zend_opcache_static_cache_prepare_value( + zend_opcache_static_cache_prepared_value *prepared, + zend_string *key, + zval *value, + bool throw_on_failure, + bool lock_held) +{ + php_serialize_data_t var_hash; + smart_str serialized = {0}; + size_t shared_graph_len, encoded_len; + unsigned char *encoded; + bool failed_unstorable; + + if (prepared == NULL) { + return false; + } + + zend_opcache_static_cache_init_prepared_value(prepared); + ZVAL_DEREF(value); + prepared->hash = zend_string_hash_val(key); + + if (Z_TYPE_P(value) == IS_RESOURCE) { + zend_opcache_static_cache_handle_store_failure( + "resources cannot be stored in the static cache", + throw_on_failure + ); + + return false; + } + + if (Z_TYPE_P(value) == IS_OBJECT && Z_OBJCE_P(value) == zend_ce_closure) { + zend_opcache_static_cache_handle_store_failure( + "Closure objects cannot be stored in the static cache", + throw_on_failure + ); + + return false; + } + + if (!zend_opcache_static_cache_validate_storable_value(value, throw_on_failure)) { + return false; + } + + switch (Z_TYPE_P(value)) { + case IS_NULL: + prepared->value_type = ZEND_OPCACHE_STATIC_CACHE_VALUE_NULL; + return true; + case IS_TRUE: + prepared->value_type = ZEND_OPCACHE_STATIC_CACHE_VALUE_TRUE; + return true; + case IS_FALSE: + prepared->value_type = ZEND_OPCACHE_STATIC_CACHE_VALUE_FALSE; + return true; + case IS_LONG: + prepared->value_type = ZEND_OPCACHE_STATIC_CACHE_VALUE_LONG; + prepared->long_value = Z_LVAL_P(value); + return true; + case IS_DOUBLE: + prepared->value_type = ZEND_OPCACHE_STATIC_CACHE_VALUE_DOUBLE; + prepared->double_value = Z_DVAL_P(value); + return true; + case IS_STRING: + prepared->value_type = ZEND_OPCACHE_STATIC_CACHE_VALUE_STRING; + prepared->value_len = (uint32_t) Z_STRLEN_P(value); + prepared->payload_size = Z_STRLEN_P(value) + 1; + prepared->payload_used_size = prepared->payload_size; + prepared->payload_source = (const unsigned char *) Z_STRVAL_P(value); + return true; + default: + /* The distinct-key write benchmark stores the same source graph many times + * in one request. Reusing a request-local prepared shared graph is only + * safe when we can prove the source graph stayed clean, so the memo layer + * tracks reachable array/object mutations and excludes internal safe-direct + * objects whose state may change outside those hooks. */ + if (zend_opcache_static_cache_prepare_memo_fetch(value, prepared)) { + return true; + } + + shared_graph_len = 0; + if (zend_opcache_static_cache_calculate_shared_graph_size(value, &shared_graph_len)) { + prepared->value_type = ZEND_OPCACHE_STATIC_CACHE_VALUE_SHARED_GRAPH; + prepared->value_len = (uint32_t) shared_graph_len; + prepared->payload_size = shared_graph_len; + + /* Explicit stores keep shared-graph construction out of the write lock + * regardless of payload size. Even small direct builds lengthen the + * critical section enough to regress contended writers. */ + if (lock_held) { + return true; + } + + prepared->owned_buffer = emalloc(shared_graph_len); + if (zend_opcache_static_cache_build_shared_graph_in_place( + value, + prepared->owned_buffer, + shared_graph_len, + &prepared->payload_used_size + ) + ) { + prepared->payload_source = prepared->owned_buffer; + zend_opcache_static_cache_prepare_memo_store(value, prepared); + + return true; + } + + if (EG(exception)) { + return false; + } + + efree(prepared->owned_buffer); + prepared->owned_buffer = NULL; + } + + if (EG(exception)) { + return false; + } + + encoded = NULL; + encoded_len = 0; + failed_unstorable = false; + if (zend_opcache_serialize_ex(&encoded, &encoded_len, value, &failed_unstorable)) { + prepared->value_type = ZEND_OPCACHE_STATIC_CACHE_VALUE_OPCACHE_SERIALIZED; + prepared->value_len = (uint32_t) encoded_len; + prepared->payload_size = encoded_len; + prepared->payload_used_size = encoded_len; + prepared->owned_buffer = encoded; + prepared->payload_source = encoded; + + return true; + } + + if (failed_unstorable) { + zend_opcache_static_cache_handle_store_failure( + "resources and Closure objects cannot be stored in the static cache", + throw_on_failure + ); + + return false; + } + + if (EG(exception)) { + return false; + } + + PHP_VAR_SERIALIZE_INIT(var_hash); + php_var_serialize(&serialized, value, &var_hash); + PHP_VAR_SERIALIZE_DESTROY(var_hash); + if (serialized.s == NULL) { + if (EG(exception)) { + return false; + } + + zend_opcache_static_cache_handle_store_failure( + "failed to serialize value for cache storage", + throw_on_failure + ); + + return false; + } + + prepared->value_type = ZEND_OPCACHE_STATIC_CACHE_VALUE_SERIALIZED; + prepared->value_len = (uint32_t) ZSTR_LEN(serialized.s); + prepared->payload_size = ZSTR_LEN(serialized.s); + prepared->payload_used_size = prepared->payload_size; + prepared->owned_string = serialized.s; + prepared->payload_source = (const unsigned char *) ZSTR_VAL(serialized.s); + serialized.s = NULL; + + return true; + } +} + +void zend_opcache_static_cache_destroy_prepared_value(zend_opcache_static_cache_prepared_value *prepared) +{ + if (prepared == NULL) { + return; + } + + if (prepared->owned_buffer != NULL) { + efree(prepared->owned_buffer); + } + + if (prepared->owned_string != NULL) { + zend_string_release(prepared->owned_string); + } + + zend_opcache_static_cache_init_prepared_value(prepared); +} + +bool zend_opcache_static_cache_store_prepared_locked( + zend_string *key, + zval *value, + const zend_opcache_static_cache_prepared_value *prepared, + zend_long ttl, + bool throw_on_failure) +{ + const char *failure_message; + zend_opcache_static_cache_header *header; + zend_opcache_static_cache_entry *entries, *entry; + zend_long new_long_value = 0; + php_serialize_data_t var_hash; + smart_str serialized = {0}; + uint64_t expires_at; + uint32_t slot_index, offset = 0, graph_offset = 0, reusable_offset, old_key_offset = 0, old_value_offset = 0, + new_key_offset = 0, new_value_offset = 0, new_value_len = 0, combined_reusable_offset = 0; + uint16_t old_reserved = 0, new_reserved = 0; + uint8_t old_value_type = ZEND_OPCACHE_STATIC_CACHE_VALUE_NULL, new_value_type; + size_t encoded_len, serialized_len, key_payload_size, failed_payload_size; + bool found, retried_expired = false, retried_compact = false, retried_clear = false, allow_clear, old_combined, use_combined_publish, failed_unstorable; + unsigned char *encoded, *combined_payload; + double new_double_value = 0; + + ZVAL_DEREF(value); + + if (prepared == NULL) { + return false; + } + + key_payload_size = ZSTR_LEN(key) + 1; + +retry_store: + old_key_offset = 0; + old_value_type = ZEND_OPCACHE_STATIC_CACHE_VALUE_NULL; + old_value_offset = 0; + old_reserved = 0; + combined_reusable_offset = 0; + new_key_offset = 0; + new_value_offset = 0; + new_value_len = 0; + new_reserved = 0; + new_value_type = prepared->value_type; + new_long_value = 0; + new_double_value = 0; + failed_payload_size = 0; + expires_at = ttl == 0 ? 0 : (uint64_t) time(NULL) + (uint64_t) ttl; + + if (!zend_opcache_static_cache_find_slot_for_write_locked(key, prepared->hash, &header, &slot_index, &found)) { + if (zend_opcache_static_cache_retry_store_after_pressure_locked(&retried_expired, &retried_compact, &retried_clear, 0, true)) { + goto retry_store; + } + + zend_opcache_static_cache_handle_store_failure_locked("cache hash table is full", throw_on_failure); + + return false; + } + + entries = zend_opcache_static_cache_entries(header); + entry = &entries[slot_index]; + + if (found) { + old_key_offset = entry->key_offset; + old_value_type = entry->value_type; + old_value_offset = entry->value_offset; + old_reserved = entry->reserved; + } + + old_combined = found && (old_reserved & ZEND_OPCACHE_STATIC_CACHE_ENTRY_RESERVED_COMBINED_VALUE_KEY) != 0; + reusable_offset = found && old_value_type != ZEND_OPCACHE_STATIC_CACHE_VALUE_SHARED_GRAPH && !old_combined ? old_value_offset : 0; + new_key_offset = found ? old_key_offset : 0; + + /* Offset-backed payloads prefer the combined publish path so a new entry, and + * a slot that was already stored in combined form, can replace value+key with + * one allocator decision. That keeps overwrite bookkeeping local to this slot + * instead of bouncing between separate key and value lifetimes. */ + use_combined_publish = zend_opcache_static_cache_value_uses_offset(prepared->value_type) && (!found || old_combined); + + /* In-place reuse is intentionally narrower than combined publish itself. + * Generic combined payloads only reuse the hot-key case, but prepared shared + * graphs are already built outside the lock. For those explicit-store writes + * we can afford a cheap overwrite check on every matching key to avoid the + * allocator churn that still shows up in distinct-key contention. */ + if (old_combined && old_value_offset != 0) { + if (old_value_type == ZEND_OPCACHE_STATIC_CACHE_VALUE_SHARED_GRAPH) { + if (prepared->payload_source != NULL && + zend_opcache_static_cache_shared_graph_can_overwrite_payload_locked(old_value_offset) && + (prepared->value_type != ZEND_OPCACHE_STATIC_CACHE_VALUE_SHARED_GRAPH || + (prepared->payload_source != NULL && zend_opcache_static_cache_shared_graph_copy_fits_buffer( + prepared->payload_source, + prepared->payload_size, + prepared->payload_used_size, + (const unsigned char *) zend_opcache_static_cache_ptr(old_value_offset), + prepared->payload_size + ) + ) + ) + ) { + combined_reusable_offset = old_value_offset; + } + } else if (header->count == 1) { + combined_reusable_offset = old_value_offset; + } + } + + if (!use_combined_publish && (!found || old_combined)) { + new_key_offset = zend_opcache_static_cache_alloc_locked(key_payload_size, ZSTR_VAL(key)); + if (new_key_offset == 0) { + failure_message = "not enough shared memory left"; + failed_payload_size = key_payload_size; + allow_clear = zend_opcache_static_cache_payload_can_fit_locked(key_payload_size); + + goto store_failed; + } + } + + switch (prepared->value_type) { + case ZEND_OPCACHE_STATIC_CACHE_VALUE_NULL: + new_value_type = ZEND_OPCACHE_STATIC_CACHE_VALUE_NULL; + break; + case ZEND_OPCACHE_STATIC_CACHE_VALUE_TRUE: + new_value_type = ZEND_OPCACHE_STATIC_CACHE_VALUE_TRUE; + break; + case ZEND_OPCACHE_STATIC_CACHE_VALUE_FALSE: + new_value_type = ZEND_OPCACHE_STATIC_CACHE_VALUE_FALSE; + break; + case ZEND_OPCACHE_STATIC_CACHE_VALUE_LONG: + new_value_type = ZEND_OPCACHE_STATIC_CACHE_VALUE_LONG; + new_long_value = prepared->long_value; + break; + case ZEND_OPCACHE_STATIC_CACHE_VALUE_DOUBLE: + new_value_type = ZEND_OPCACHE_STATIC_CACHE_VALUE_DOUBLE; + new_double_value = prepared->double_value; + break; + case ZEND_OPCACHE_STATIC_CACHE_VALUE_STRING: + case ZEND_OPCACHE_STATIC_CACHE_VALUE_OPCACHE_SERIALIZED: + case ZEND_OPCACHE_STATIC_CACHE_VALUE_SERIALIZED: + if (use_combined_publish) { + if (!zend_opcache_static_cache_publish_combined_offset_value_locked( + combined_reusable_offset, + key, + prepared->payload_size, + prepared->payload_source, + &new_value_offset, + &new_key_offset) + ) { + failure_message = "not enough shared memory left"; + failed_payload_size = prepared->payload_size + key_payload_size; + allow_clear = zend_opcache_static_cache_payload_can_fit_locked(prepared->payload_size + key_payload_size); + + goto store_failed; + } + + new_reserved = ZEND_OPCACHE_STATIC_CACHE_ENTRY_RESERVED_COMBINED_VALUE_KEY; + } else { + offset = zend_opcache_static_cache_write_payload_locked( + reusable_offset, + prepared->payload_size, + prepared->payload_source); + if (offset == 0) { + failure_message = "not enough shared memory left"; + failed_payload_size = prepared->payload_size; + allow_clear = zend_opcache_static_cache_payload_can_fit_locked(prepared->payload_size); + + goto store_failed; + } + + new_value_offset = offset; + } + + new_value_type = prepared->value_type; + new_value_len = prepared->value_len; + break; + case ZEND_OPCACHE_STATIC_CACHE_VALUE_SHARED_GRAPH: + if (use_combined_publish) { + /* Shared-graph fetch helpers treat value_offset as the start of the SHM + * payload, so the graph must stay at the block base. Reserve the whole + * combined block first, publish the graph into the base payload, then + * append the key bytes after the graph payload. */ + combined_payload = zend_opcache_static_cache_reserve_combined_offset_value_locked( + prepared->payload_source != NULL ? combined_reusable_offset : 0, + key, + prepared->payload_size, + &new_value_offset, + &new_key_offset + ); + graph_offset = new_value_offset; + + if (combined_payload != NULL) { + /* Prepared shared-graph buffers may contain direct array payloads with + * final-buffer pointers, so the SHM destination must be the build site. */ + if (zend_opcache_static_cache_build_shared_graph_in_place( + value, + combined_payload, + prepared->payload_size, + NULL) + ) { + memcpy(combined_payload + prepared->payload_size, ZSTR_VAL(key), key_payload_size); + new_reserved = ZEND_OPCACHE_STATIC_CACHE_ENTRY_RESERVED_COMBINED_VALUE_KEY; + new_value_type = ZEND_OPCACHE_STATIC_CACHE_VALUE_SHARED_GRAPH; + new_value_offset = graph_offset; + new_value_len = prepared->value_len; + break; + } + + if (graph_offset != combined_reusable_offset) { + zend_opcache_static_cache_free_locked(graph_offset); + } + graph_offset = 0; + new_value_offset = 0; + new_key_offset = found ? old_key_offset : 0; + + if (EG(exception)) { + return false; + } + } else if ( + zend_opcache_static_cache_payload_can_fit_locked(prepared->payload_size + key_payload_size) && + zend_opcache_static_cache_retry_store_after_pressure_locked(&retried_expired, &retried_compact, &retried_clear, prepared->payload_size + key_payload_size, true) + ) { + goto retry_store; + } + } else { + graph_offset = zend_opcache_static_cache_alloc_locked(prepared->payload_size, NULL); + + if (graph_offset != 0) { + /* Prepared shared-graph buffers may contain direct array payloads with + * final-buffer pointers, so the SHM destination must be the build site. */ + if (zend_opcache_static_cache_build_shared_graph_in_place( + value, + (unsigned char *) zend_opcache_static_cache_ptr(graph_offset), + prepared->payload_size, + NULL) + ) { + new_value_type = ZEND_OPCACHE_STATIC_CACHE_VALUE_SHARED_GRAPH; + new_value_offset = graph_offset; + new_value_len = prepared->value_len; + break; + } + + zend_opcache_static_cache_free_locked(graph_offset); + + if (EG(exception)) { + return false; + } + } else if ( + zend_opcache_static_cache_payload_can_fit_locked(prepared->payload_size) && + zend_opcache_static_cache_retry_store_after_pressure_locked(&retried_expired, &retried_compact, &retried_clear, prepared->payload_size, true) + ) { + goto retry_store; + } + } + + offset = reusable_offset; + encoded = NULL; + encoded_len = 0; + failed_unstorable = false; + if (zend_opcache_serialize_ex(&encoded, &encoded_len, value, &failed_unstorable)) { + if (use_combined_publish) { + if (!zend_opcache_static_cache_publish_combined_offset_value_locked( + combined_reusable_offset, + key, + encoded_len, + encoded, + &new_value_offset, + &new_key_offset) + ) { + efree(encoded); + failure_message = "not enough shared memory left"; + failed_payload_size = encoded_len + key_payload_size; + allow_clear = zend_opcache_static_cache_payload_can_fit_locked(encoded_len + key_payload_size); + + goto store_failed; + } + + new_reserved = ZEND_OPCACHE_STATIC_CACHE_ENTRY_RESERVED_COMBINED_VALUE_KEY; + } else { + offset = zend_opcache_static_cache_write_payload_locked(offset, encoded_len, encoded); + if (offset == 0) { + efree(encoded); + failure_message = "not enough shared memory left"; + failed_payload_size = encoded_len; + allow_clear = zend_opcache_static_cache_payload_can_fit_locked(encoded_len); + + goto store_failed; + } + + new_value_offset = offset; + } + efree(encoded); + new_value_type = ZEND_OPCACHE_STATIC_CACHE_VALUE_OPCACHE_SERIALIZED; + new_value_len = (uint32_t) encoded_len; + break; + } + + if (failed_unstorable) { + failure_message = "resources and Closure objects cannot be stored in the static cache"; + allow_clear = false; + + goto store_failed; + } + + if (EG(exception)) { + return false; + } + + PHP_VAR_SERIALIZE_INIT(var_hash); + php_var_serialize(&serialized, value, &var_hash); + PHP_VAR_SERIALIZE_DESTROY(var_hash); + if (serialized.s == NULL) { + if (EG(exception)) { + return false; + } + + failure_message = "failed to serialize value for cache storage"; + allow_clear = false; + + goto store_failed; + } + + serialized_len = ZSTR_LEN(serialized.s); + if (use_combined_publish) { + if (!zend_opcache_static_cache_publish_combined_offset_value_locked( + combined_reusable_offset, + key, + serialized_len, + ZSTR_VAL(serialized.s), + &new_value_offset, + &new_key_offset) + ) { + smart_str_free(&serialized); + failure_message = "not enough shared memory left"; + failed_payload_size = serialized_len + key_payload_size; + allow_clear = zend_opcache_static_cache_payload_can_fit_locked(serialized_len + key_payload_size); + + goto store_failed; + } + + new_reserved = ZEND_OPCACHE_STATIC_CACHE_ENTRY_RESERVED_COMBINED_VALUE_KEY; + } else { + offset = zend_opcache_static_cache_write_payload_locked(reusable_offset, serialized_len, ZSTR_VAL(serialized.s)); + if (offset == 0) { + smart_str_free(&serialized); + failure_message = "not enough shared memory left"; + failed_payload_size = serialized_len; + allow_clear = zend_opcache_static_cache_payload_can_fit_locked(serialized_len); + + goto store_failed; + } + + new_value_offset = offset; + } + smart_str_free(&serialized); + new_value_type = ZEND_OPCACHE_STATIC_CACHE_VALUE_SERIALIZED; + new_value_len = (uint32_t) serialized_len; + break; + default: + ZEND_UNREACHABLE(); + } + + entry->hash = prepared->hash; + entry->key_offset = new_key_offset; + entry->key_len = (uint32_t) ZSTR_LEN(key); + entry->value_offset = new_value_offset; + entry->value_len = new_value_len; + entry->expires_at = expires_at; + entry->state = ZEND_OPCACHE_STATIC_CACHE_ENTRY_USED; + entry->value_type = new_value_type; + entry->reserved = new_reserved; + entry->long_value = new_long_value; + entry->double_value = new_double_value; + + if (found && old_key_offset != 0 && old_key_offset != new_key_offset && !old_combined) { + zend_opcache_static_cache_free_locked(old_key_offset); + } + + if (found && old_value_offset != 0 && old_value_offset != new_value_offset) { + zend_opcache_static_cache_release_value_storage_locked(old_value_type, old_value_offset); + } + + if (!found) { + header->count++; + } + + zend_opcache_static_cache_bump_mutation_epoch_locked(header); + + return true; + +store_failed: + if (new_value_offset != 0 && new_value_offset != old_value_offset) { + zend_opcache_static_cache_free_locked(new_value_offset); + } + + if (new_key_offset != 0 && new_key_offset != old_key_offset && + (new_reserved & ZEND_OPCACHE_STATIC_CACHE_ENTRY_RESERVED_COMBINED_VALUE_KEY) == 0 + ) { + zend_opcache_static_cache_free_locked(new_key_offset); + } + + if (zend_opcache_static_cache_retry_store_after_pressure_locked(&retried_expired, &retried_compact, &retried_clear, failed_payload_size, allow_clear)) { + goto retry_store; + } + + zend_opcache_static_cache_handle_store_failure_locked(failure_message, throw_on_failure); + + return false; +} + +bool zend_opcache_static_cache_store_locked(zend_string *key, zval *value, zend_long ttl, bool throw_on_failure) +{ + const char *cache_name = zend_opcache_static_cache_active_context()->name; + zend_opcache_static_cache_prepared_value prepared; + bool stored; + + /* Value preparation may invoke userland serialization hooks. Keep the caller's + * lock contract by dropping the write lock only for preparation and + * reacquiring it before returning. */ + zend_opcache_static_cache_unlock(); + if (!zend_opcache_static_cache_prepare_value(&prepared, key, value, throw_on_failure, false)) { + zend_opcache_static_cache_reacquire_write_lock_or_fail(cache_name); + + return false; + } + + if (!zend_opcache_static_cache_reacquire_write_lock_or_fail(cache_name)) { + zend_opcache_static_cache_destroy_prepared_value(&prepared); + + return false; + } + + stored = zend_opcache_static_cache_store_prepared_locked(key, value, &prepared, ttl, throw_on_failure); + zend_opcache_static_cache_destroy_prepared_value(&prepared); + + return stored; +} + +bool zend_opcache_static_cache_fetch_locked(zend_string *key, zval *return_value, bool throw_if_missing, bool *found_ptr, bool use_request_local_slot) +{ + const char *cache_name = zend_opcache_static_cache_active_context()->name; + zend_opcache_static_cache_header *header; + zend_opcache_static_cache_entry *entries, *entry; + zend_opcache_static_cache_lookup_entry *lookup_entries, *lookup_entry; + zend_ulong hash; + uint64_t mutation_epoch, now; + uint32_t way, slot_index; + bool found; + + if (found_ptr != NULL) { + *found_ptr = false; + } + + hash = zend_string_hash_val(key); + + header = zend_opcache_static_cache_header_ptr(); + if (!header || !zend_opcache_static_cache_header_is_initialized_locked()) { + if (throw_if_missing) { + zend_throw_exception_ex(zend_opcache_static_cache_exception_ce, 0, "Cache key \"%s\" was not found", ZSTR_VAL(key)); + } + + return false; + } + entries = zend_opcache_static_cache_entries(header); + mutation_epoch = header->mutation_epoch; + lookup_entries = zend_opcache_static_cache_lookup_cache_set(hash); + + /* Request-local lookup cache: repeated fetches of the same key avoid the + * shared hash-table probe. A cached miss is just as valuable for userland + * exists/fetch probes, and the mutation epoch keeps both hit and miss records + * coherent after another writer updates the segment. */ + for (way = 0; way < ZEND_OPCACHE_STATIC_CACHE_LOOKUP_WAYS; way++) { + lookup_entry = &lookup_entries[way]; + if (lookup_entry->state == ZEND_OPCACHE_STATIC_CACHE_LOOKUP_EMPTY || lookup_entry->hash != hash) { + continue; + } + + if (lookup_entry->mutation_epoch != mutation_epoch) { + zend_opcache_static_cache_lookup_cache_reset_entry(lookup_entry); + continue; + } + + if (lookup_entry->state == ZEND_OPCACHE_STATIC_CACHE_LOOKUP_MISS) { + if (throw_if_missing) { + zend_throw_exception_ex(zend_opcache_static_cache_exception_ce, 0, "Cache key \"%s\" was not found", ZSTR_VAL(key)); + } + + return false; + } + + if (lookup_entry->slot_index >= header->capacity) { + zend_opcache_static_cache_lookup_cache_reset_entry(lookup_entry); + continue; + } + + entry = &entries[lookup_entry->slot_index]; + if (!zend_opcache_static_cache_key_equals(entry, key, hash)) { + zend_opcache_static_cache_lookup_cache_reset_entry(lookup_entry); + continue; + } + + now = 0; + if (zend_opcache_static_cache_maybe_expired(entry, &now)) { + zend_opcache_static_cache_lookup_cache_reset_entry(lookup_entry); + zend_opcache_static_cache_lookup_cache_store_miss(lookup_entries, hash, mutation_epoch); + if (throw_if_missing) { + zend_throw_exception_ex(zend_opcache_static_cache_exception_ce, 0, "Cache key \"%s\" was not found", ZSTR_VAL(key)); + } + + return false; + } + + slot_index = lookup_entry->slot_index; + found = true; + + goto value_found; + } + + if (!zend_opcache_static_cache_find_slot_in_header_locked(header, key, hash, &slot_index, &found, false) || !found) { + zend_opcache_static_cache_lookup_cache_store_miss(lookup_entries, hash, mutation_epoch); + if (throw_if_missing) { + zend_throw_exception_ex(zend_opcache_static_cache_exception_ce, 0, "Cache key \"%s\" was not found", ZSTR_VAL(key)); + } + + return false; + } + + lookup_entry = zend_opcache_static_cache_lookup_cache_select_slot(lookup_entries, hash, mutation_epoch, true); + if (lookup_entry != NULL) { + zend_opcache_static_cache_lookup_cache_store_hit(lookup_entry, hash, mutation_epoch, slot_index); + } + +value_found: + if (found_ptr != NULL) { + *found_ptr = true; + } + + entry = &entries[slot_index]; + if (use_request_local_slot) { + if (zend_opcache_static_cache_fetch_request_local_slot(key, mutation_epoch, return_value)) { + return true; + } + if (EG(exception)) { + return false; + } + } + + switch (entry->value_type) { + case ZEND_OPCACHE_STATIC_CACHE_VALUE_NULL: + ZVAL_NULL(return_value); + return zend_opcache_static_cache_fetch_finish(key, mutation_epoch, return_value, use_request_local_slot); + case ZEND_OPCACHE_STATIC_CACHE_VALUE_TRUE: + ZVAL_TRUE(return_value); + return zend_opcache_static_cache_fetch_finish(key, mutation_epoch, return_value, use_request_local_slot); + case ZEND_OPCACHE_STATIC_CACHE_VALUE_FALSE: + ZVAL_FALSE(return_value); + return zend_opcache_static_cache_fetch_finish(key, mutation_epoch, return_value, use_request_local_slot); + case ZEND_OPCACHE_STATIC_CACHE_VALUE_LONG: + ZVAL_LONG(return_value, entry->long_value); + return zend_opcache_static_cache_fetch_finish(key, mutation_epoch, return_value, use_request_local_slot); + case ZEND_OPCACHE_STATIC_CACHE_VALUE_DOUBLE: + ZVAL_DOUBLE(return_value, entry->double_value); + return zend_opcache_static_cache_fetch_finish(key, mutation_epoch, return_value, use_request_local_slot); + case ZEND_OPCACHE_STATIC_CACHE_VALUE_STRING: + ZVAL_STRINGL(return_value, zend_opcache_static_cache_ptr(entry->value_offset), entry->value_len); + return zend_opcache_static_cache_fetch_finish(key, mutation_epoch, return_value, use_request_local_slot); + case ZEND_OPCACHE_STATIC_CACHE_VALUE_SERIALIZED: + case ZEND_OPCACHE_STATIC_CACHE_VALUE_OPCACHE_SERIALIZED: + case ZEND_OPCACHE_STATIC_CACHE_VALUE_SHARED_GRAPH: + if (!zend_opcache_static_cache_materialize_payload_locked( + key, + entry->value_type, + entry->value_offset, + entry->value_len, + return_value, + throw_if_missing, + cache_name) + ) { + return false; + } + + return zend_opcache_static_cache_fetch_finish(key, mutation_epoch, return_value, use_request_local_slot); + default: + if (throw_if_missing) { + zend_throw_exception_ex(zend_opcache_static_cache_exception_ce, 0, "Stored %s value for key \"%s\" has an unknown type", cache_name, ZSTR_VAL(key)); + } + + return false; + } +} + +bool zend_opcache_static_cache_exists_locked(zend_string *key) +{ + zend_ulong hash = zend_string_hash_val(key); + uint32_t slot_index; + bool found; + + if (!zend_opcache_static_cache_find_slot_for_read_locked(key, hash, NULL, &slot_index, &found)) { + return false; + } + + return found; +} + +bool zend_opcache_static_cache_delete_locked(zend_string *key) +{ + zend_opcache_static_cache_header *header; + zend_opcache_static_cache_entry *entries; + zend_ulong hash = zend_string_hash_val(key); + uint32_t slot_index; + bool found; + + if (!zend_opcache_static_cache_find_slot_for_write_locked(key, hash, &header, &slot_index, &found) || !found) { + return true; + } + + entries = zend_opcache_static_cache_entries(header); + zend_opcache_static_cache_delete_entry_locked(&entries[slot_index], header); + + return true; +} + +bool zend_opcache_static_cache_atomic_update_locked(zend_string *key, zend_long step, bool decrement, bool insert_if_missing, zend_long *new_value, const char *type_error_message) +{ + zend_opcache_static_cache_header *header; + zend_opcache_static_cache_entry *entries, *entry; + zend_ulong hash = zend_string_hash_val(key); + zval initial_value = {0}; + uint32_t slot_index; + bool found; + + if (!zend_opcache_static_cache_find_slot_for_write_locked(key, hash, &header, &slot_index, &found) || !found) { + if (insert_if_missing) { + ZVAL_LONG(&initial_value, step); + if (zend_opcache_static_cache_store_locked(key, &initial_value, 0, true)) { + *new_value = step; + + return true; + } + + return false; + } + + zend_throw_exception_ex(zend_opcache_static_cache_exception_ce, 0, "Cache key \"%s\" was not found", ZSTR_VAL(key)); + + return false; + } + + entries = zend_opcache_static_cache_entries(header); + entry = &entries[slot_index]; + if (entry->value_type != ZEND_OPCACHE_STATIC_CACHE_VALUE_LONG) { + if (entry->value_type > ZEND_OPCACHE_STATIC_CACHE_VALUE_SHARED_GRAPH) { + zend_throw_exception_ex(zend_opcache_static_cache_exception_ce, 0, "Stored %s value for key \"%s\" has an unknown type", zend_opcache_static_cache_active_context()->name, ZSTR_VAL(key)); + } else { + zend_throw_exception_ex(zend_opcache_static_cache_exception_ce, 0, "%s", type_error_message); + } + + return false; + } + + if (decrement) { + entry->long_value -= step; + } else { + entry->long_value += step; + } + + zend_opcache_static_cache_bump_mutation_epoch_locked(header); + *new_value = entry->long_value; + + return true; +} + +void zend_opcache_static_cache_release_request_local_slots(void) +{ + zend_opcache_static_cache_release_request_local_slot_context(&zend_opcache_static_cache_volatile_request_local_slots); + zend_opcache_static_cache_release_request_local_slot_context(&zend_opcache_static_cache_persistent_request_local_slots); +} diff --git a/ext/opcache/zend_static_cache_internal.h b/ext/opcache/zend_static_cache_internal.h new file mode 100644 index 000000000000..0456aaf0fb23 --- /dev/null +++ b/ext/opcache/zend_static_cache_internal.h @@ -0,0 +1,1082 @@ +/* + +----------------------------------------------------------------------+ + | Zend OPcache | + +----------------------------------------------------------------------+ + | Copyright © The PHP Group and Contributors. | + +----------------------------------------------------------------------+ + | This source file is subject to the Modified BSD License that is | + | bundled with this package in the file LICENSE, and is available | + | through the World Wide Web at . | + | | + | SPDX-License-Identifier: BSD-3-Clause | + +----------------------------------------------------------------------+ + | Author: Go Kudo | + +----------------------------------------------------------------------+ +*/ + +#ifndef ZEND_STATIC_CACHE_INTERNAL_H +#define ZEND_STATIC_CACHE_INTERNAL_H + +#include "php.h" + +#include +#ifdef ZTS +# include "TSRM/TSRM.h" +#endif + +#include "Zend/zend_attributes.h" +#include "Zend/zend_ast.h" +#include "Zend/zend_atomic.h" +#include "Zend/zend_enum.h" +#include "Zend/zend_exceptions.h" + +#include "ZendAccelerator.h" +#include "zend_accelerator_module.h" +#include "zend_shared_alloc.h" +#include "zend_smart_str.h" +#include "zend_static_cache.h" + +#include "ext/date/php_date.h" +#include "ext/spl/spl_array.h" +#include "ext/spl/spl_fixedarray.h" +#include "ext/standard/php_var.h" + +#include "SAPI.h" + +#define ZEND_OPCACHE_STATIC_CACHE_MAGIC 0xCAC17E01U +#define ZEND_OPCACHE_STATIC_CACHE_VERSION 1U +#define ZEND_OPCACHE_STATIC_CACHE_MIN_CAPACITY 127U +#define ZEND_OPCACHE_STATIC_CACHE_MAX_CAPACITY 65521U +#define ZEND_OPCACHE_STATIC_CACHE_SLOT_BYTES 256U + +#define ZEND_OPCACHE_STATIC_CACHE_ENTRY_EMPTY 0 +#define ZEND_OPCACHE_STATIC_CACHE_ENTRY_USED 1 +#define ZEND_OPCACHE_STATIC_CACHE_ENTRY_TOMBSTONE 2 + +#define ZEND_OPCACHE_STATIC_CACHE_VALUE_NULL 0 +#define ZEND_OPCACHE_STATIC_CACHE_VALUE_TRUE 1 +#define ZEND_OPCACHE_STATIC_CACHE_VALUE_FALSE 2 +#define ZEND_OPCACHE_STATIC_CACHE_VALUE_LONG 3 +#define ZEND_OPCACHE_STATIC_CACHE_VALUE_DOUBLE 4 +#define ZEND_OPCACHE_STATIC_CACHE_VALUE_STRING 5 +#define ZEND_OPCACHE_STATIC_CACHE_VALUE_SERIALIZED 6 +#define ZEND_OPCACHE_STATIC_CACHE_VALUE_OPCACHE_SERIALIZED 7 +#define ZEND_OPCACHE_STATIC_CACHE_VALUE_SHARED_GRAPH 8 + +#define ZEND_OPCACHE_STATIC_CACHE_SHARED_GRAPH_MAGIC 0xCAC17E02U +#define ZEND_OPCACHE_STATIC_CACHE_SHARED_GRAPH_VERSION 1U +#define ZEND_OPCACHE_STATIC_CACHE_SHARED_GRAPH_RETIRED (1 << 30) +#define ZEND_OPCACHE_STATIC_CACHE_SHARED_GRAPH_REFCOUNT_MASK (ZEND_OPCACHE_STATIC_CACHE_SHARED_GRAPH_RETIRED - 1) + +#define ZEND_OPCACHE_STATIC_CACHE_SHARED_GRAPH_VALUE_UNDEF 0 +#define ZEND_OPCACHE_STATIC_CACHE_SHARED_GRAPH_VALUE_NULL 1 +#define ZEND_OPCACHE_STATIC_CACHE_SHARED_GRAPH_VALUE_TRUE 2 +#define ZEND_OPCACHE_STATIC_CACHE_SHARED_GRAPH_VALUE_FALSE 3 +#define ZEND_OPCACHE_STATIC_CACHE_SHARED_GRAPH_VALUE_LONG 4 +#define ZEND_OPCACHE_STATIC_CACHE_SHARED_GRAPH_VALUE_DOUBLE 5 +#define ZEND_OPCACHE_STATIC_CACHE_SHARED_GRAPH_VALUE_STRING 6 +#define ZEND_OPCACHE_STATIC_CACHE_SHARED_GRAPH_VALUE_ARRAY 7 +#define ZEND_OPCACHE_STATIC_CACHE_SHARED_GRAPH_VALUE_OBJECT 8 +#define ZEND_OPCACHE_STATIC_CACHE_SHARED_GRAPH_VALUE_SERIALIZED_OBJECT 9 +#define ZEND_OPCACHE_STATIC_CACHE_SHARED_GRAPH_VALUE_DYNAMIC_ARRAY 10 + +#define ZEND_OPCACHE_STATIC_CACHE_LOOKUP_BUCKETS 256U +#define ZEND_OPCACHE_STATIC_CACHE_LOOKUP_WAYS 2U +#define ZEND_OPCACHE_STATIC_CACHE_LOOKUP_SETS (ZEND_OPCACHE_STATIC_CACHE_LOOKUP_BUCKETS / ZEND_OPCACHE_STATIC_CACHE_LOOKUP_WAYS) +#define ZEND_OPCACHE_STATIC_CACHE_LOOKUP_INVALID_SLOT UINT32_MAX + +#define ZEND_OPCACHE_STATIC_CACHE_ENTRY_LOCK_STRIPES 256U + +#define ZEND_OPCACHE_STATIC_CACHE_LOOKUP_EMPTY 0 +#define ZEND_OPCACHE_STATIC_CACHE_LOOKUP_HIT 1 +#define ZEND_OPCACHE_STATIC_CACHE_LOOKUP_MISS 2 + +#define ZEND_OPCACHE_STATIC_CACHE_ENTRY_RESERVED_COMBINED_VALUE_KEY 0x0001U + +#define ZEND_OPCACHE_STATIC_CACHE_PERSISTENT_ATTRIBUTE "opcache\\persistentstatic" +#define ZEND_OPCACHE_STATIC_CACHE_VOLATILE_STATIC_ATTRIBUTE "opcache\\volatilestatic" + +#define ZEND_OPCACHE_STATIC_CACHE_STRATEGY_IMMEDIATE 0 +#define ZEND_OPCACHE_STATIC_CACHE_STRATEGY_TRACKING 1 + +#define ZEND_OPCACHE_STATIC_CACHE_BLOCK_FREE 1U + +#ifndef ZEND_WIN32 +# define ZEND_OPCACHE_STATIC_CACHE_VOLATILE_SEM_FILENAME_PREFIX ".ZendVolatileCacheSem." +# define ZEND_OPCACHE_STATIC_CACHE_PERSISTENT_SEM_FILENAME_PREFIX ".ZendPersistentCacheSem." +#endif + +typedef enum _zend_opcache_static_cache_kind { + ZEND_OPCACHE_STATIC_CACHE_NONE, + ZEND_OPCACHE_STATIC_CACHE_PERSISTENT, + ZEND_OPCACHE_STATIC_CACHE_VOLATILE_STATIC, + ZEND_OPCACHE_STATIC_CACHE_VOLATILE_STATIC_TRACKING, + ZEND_OPCACHE_STATIC_CACHE_CONFLICT +} zend_opcache_static_cache_kind; + +typedef struct _zend_opcache_static_cache_volatile_static_attribute_config { + zend_long ttl; + zend_long strategy; +} zend_opcache_static_cache_volatile_static_attribute_config; + +typedef struct _zend_opcache_static_cache_runtime { + bool enabled; + bool available; + bool startup_failed; + bool backend_initialized; + size_t configured_memory; + const char *failure_reason; +} zend_opcache_static_cache_runtime; + +typedef struct _zend_opcache_static_cache_storage { + const zend_shared_memory_handlers *handler; + zend_shared_segment **segments; + int segment_count; + size_t size; + const char *model; + bool initialized; + bool initialized_before_request; + bool lock_initialized; + int lock_file; + char lockfile_name[MAXPATHLEN]; +#ifdef ZTS + zend_thread_rwlock_t zts_lock; + MUTEX_T entry_locks[ZEND_OPCACHE_STATIC_CACHE_ENTRY_LOCK_STRIPES]; + bool entry_locks_initialized; +#endif +} zend_opcache_static_cache_storage; + +typedef struct _zend_opcache_static_cache_context { + zend_opcache_static_cache_runtime runtime; + zend_opcache_static_cache_storage storage; + const char *name; + const char *lock_name; +#ifndef ZEND_WIN32 + const char *sem_filename_prefix; +#endif + bool clear_on_pressure; + bool strict_store_failure; +} zend_opcache_static_cache_context; + +/* The same storage primitives serve the explicit volatile cache, VolatileStatic, and + * PersistentStatic. Callers switch this TLS context around short critical sections + * so allocator, lookup-cache, and lock helpers always operate on the right SHM + * segment without threading a context argument through every hot helper. */ +typedef struct _zend_opcache_static_cache_header { + uint32_t magic; + uint32_t version; + uint32_t capacity; + uint32_t count; + uint32_t data_offset; + uint32_t data_size; + uint32_t next_free; + uint32_t free_list; + uint32_t last_block_offset; + uint64_t mutation_epoch; +#ifndef ZEND_WIN32 + uint32_t reserved_lock; +#endif +} zend_opcache_static_cache_header; + +typedef struct _zend_opcache_static_cache_block { + uint32_t size; + uint32_t prev_size; + uint32_t next_free; + uint32_t prev_free; + uint32_t flags; +} zend_opcache_static_cache_block; + +typedef struct _zend_opcache_static_cache_entry { + zend_ulong hash; + uint32_t key_offset; + uint32_t key_len; + uint32_t value_offset; + uint32_t value_len; + uint64_t expires_at; + uint8_t state; + uint8_t value_type; + uint16_t reserved; + zend_long long_value; + double double_value; +} zend_opcache_static_cache_entry; + +typedef struct _zend_opcache_static_cache_lookup_entry { + zend_ulong hash; + uint64_t mutation_epoch; + uint32_t slot_index; + uint8_t state; + uint8_t reserved[3]; +} zend_opcache_static_cache_lookup_entry; + +typedef struct _zend_opcache_static_cache_request_local_slot { + uint64_t mutation_epoch; + zval value; +} zend_opcache_static_cache_request_local_slot; + +typedef struct _zend_opcache_static_cache_prepared_value { + zend_ulong hash; + uint8_t value_type; + uint8_t reserved[3]; + uint32_t value_len; + size_t payload_size; + size_t payload_used_size; + const unsigned char *payload_source; + unsigned char *owned_buffer; + zend_string *owned_string; + zend_long long_value; + double double_value; +} zend_opcache_static_cache_prepared_value; + +typedef struct _zend_opcache_static_cache_class_blob_handle { + zend_class_entry *ce; + zend_string *cache_key; + zend_opcache_static_cache_context *context; + zend_opcache_static_cache_kind kind; + zend_long ttl; + zval root_state; + bool initialized; + bool dirty; + bool request_deferred_publish; +} zend_opcache_static_cache_class_blob_handle; + +typedef struct _zend_opcache_static_cache_function_static_entry { + zval *slot; + zend_string *cache_key; + zend_opcache_static_cache_context *context; + zend_opcache_static_cache_kind kind; + zend_long ttl; + zend_opcache_static_cache_class_blob_handle *class_blob_handle; + zend_string *method_key; + zend_string *var_name; +} zend_opcache_static_cache_function_static_entry; + +typedef struct _zend_opcache_static_cache_static_slot_handle { + zval *slot; + zend_string *cache_key; + zend_opcache_static_cache_context *context; + zend_opcache_static_cache_kind kind; + zend_long ttl; + zend_opcache_static_cache_class_blob_handle *class_blob_handle; + zval original_value; + zend_refcounted *original_root; + uint8_t original_root_type; + bool has_original_value; + bool snapshot_root_published; + bool request_deferred_publish; + bool mutation_dirty; +} zend_opcache_static_cache_static_slot_handle; + +typedef struct _zend_opcache_static_cache_tracked_dependency { + zend_opcache_static_cache_static_slot_handle *single_handle; + HashTable *handles; +} zend_opcache_static_cache_tracked_dependency; + +typedef struct _zend_opcache_static_cache_pending_mutation { + zend_opcache_static_cache_static_slot_handle *handle; + uint32_t depth; + bool dirty; + bool root_array; +} zend_opcache_static_cache_pending_mutation; + +typedef struct _zend_opcache_static_cache_shared_graph_header { + uint32_t magic; + uint32_t version; + uint32_t root_offset; + uint32_t root_type; + zend_atomic_int ref_state; +} zend_opcache_static_cache_shared_graph_header; + +typedef struct _zend_opcache_static_cache_shared_graph_ref { + zend_opcache_static_cache_context *context; + uint32_t payload_offset; +} zend_opcache_static_cache_shared_graph_ref; + +typedef struct _zend_opcache_static_cache_entry_lock { + zend_opcache_static_cache_context *context; + zend_ulong owner_pid; + uint32_t stripe; +} zend_opcache_static_cache_entry_lock; + +typedef struct _zend_opcache_static_cache_shared_graph_value { + uint8_t type; + uint8_t reserved[7]; + union { + zend_long long_value; + double double_value; + uint64_t offset; + } payload; +} zend_opcache_static_cache_shared_graph_value; + +typedef struct _zend_opcache_static_cache_shared_graph_property { + uint32_t name_offset; + uint32_t reserved; + zend_opcache_static_cache_shared_graph_value value; +} zend_opcache_static_cache_shared_graph_property; + +typedef struct _zend_opcache_static_cache_shared_graph_array_element { + zend_ulong h; + uint32_t key_offset; + uint32_t reserved; + zend_opcache_static_cache_shared_graph_value value; +} zend_opcache_static_cache_shared_graph_array_element; + +typedef struct _zend_opcache_static_cache_shared_graph_array { + uint32_t count; + uint32_t next_free; + uint32_t elements_offset; + uint32_t reserved; +} zend_opcache_static_cache_shared_graph_array; + +typedef struct _zend_opcache_static_cache_shared_graph_object { + uint32_t class_name_offset; + uint32_t property_count; + uint32_t properties_offset; + uint32_t reserved; +} zend_opcache_static_cache_shared_graph_object; + +typedef struct _zend_opcache_static_cache_shared_graph_serialized_object { + uint32_t data_len; + uint32_t reserved; + unsigned char data[1]; +} zend_opcache_static_cache_shared_graph_serialized_object; + +typedef struct _zend_opcache_static_cache_shared_graph_calc_context { + size_t size; + HashTable seen_arrays; + HashTable seen_objects; +} zend_opcache_static_cache_shared_graph_calc_context; + +typedef struct _zend_opcache_static_cache_shared_graph_copy_context { + unsigned char *buffer; + size_t size; + size_t position; + HashTable seen_arrays; + HashTable seen_objects; +} zend_opcache_static_cache_shared_graph_copy_context; + +extern zend_class_entry *zend_opcache_static_cache_exception_ce; +extern zend_class_entry *zend_opcache_static_cache_strategy_ce; +extern zend_opcache_static_cache_context zend_opcache_static_cache_volatile_context_state; +extern zend_opcache_static_cache_context zend_opcache_static_cache_persistent_context_state; +extern bool zend_opcache_static_cache_subsystem_disabled; +extern const char *zend_opcache_static_cache_subsystem_failure_reason; +extern ZEND_EXT_TLS zend_opcache_static_cache_runtime zend_opcache_static_cache_volatile_runtime_state; +extern ZEND_EXT_TLS zend_opcache_static_cache_runtime zend_opcache_static_cache_persistent_runtime_state; +extern ZEND_EXT_TLS HashTable zend_opcache_static_cache_attribute_classes; +extern ZEND_EXT_TLS bool zend_opcache_static_cache_attribute_classes_initialized; +extern ZEND_EXT_TLS HashTable zend_opcache_static_cache_ignored_classes; +extern ZEND_EXT_TLS bool zend_opcache_static_cache_ignored_classes_initialized; +extern ZEND_EXT_TLS HashTable zend_opcache_static_cache_function_statics; +extern ZEND_EXT_TLS bool zend_opcache_static_cache_function_statics_initialized; +extern ZEND_EXT_TLS HashTable zend_opcache_static_cache_class_blob_handles; +extern ZEND_EXT_TLS bool zend_opcache_static_cache_class_blob_handles_initialized; +extern bool zend_opcache_static_cache_safe_direct_classes_marked; +extern ZEND_EXT_TLS zend_opcache_static_cache_context *zend_opcache_static_cache_active_context_ptr; +#ifdef ZTS +extern ZEND_EXT_TLS bool zend_opcache_static_cache_zts_lock_is_write; +#endif +extern ZEND_EXT_TLS HashTable *zend_opcache_static_cache_tracked_roots; +extern ZEND_EXT_TLS HashTable *zend_opcache_static_cache_tracked_references; +extern ZEND_EXT_TLS HashTable *zend_opcache_static_cache_tracked_arrays; +extern ZEND_EXT_TLS HashTable *zend_opcache_static_cache_tracked_objects; +extern ZEND_EXT_TLS bool zend_opcache_static_cache_has_tracked_arrays; +extern ZEND_EXT_TLS bool zend_opcache_static_cache_has_tracked_objects; +extern ZEND_EXT_TLS bool zend_opcache_static_cache_volatile_skip_publish; +extern ZEND_EXT_TLS bool zend_opcache_static_cache_persistent_skip_publish; +extern ZEND_EXT_TLS zend_opcache_static_cache_shared_graph_ref *zend_opcache_static_cache_shared_graph_refs; +extern ZEND_EXT_TLS uint32_t zend_opcache_static_cache_shared_graph_ref_count; +extern ZEND_EXT_TLS uint32_t zend_opcache_static_cache_shared_graph_ref_capacity; +extern ZEND_EXT_TLS zend_opcache_static_cache_shared_graph_ref *zend_opcache_static_cache_retired_shared_graphs; +extern ZEND_EXT_TLS uint32_t zend_opcache_static_cache_retired_shared_graph_count; +extern ZEND_EXT_TLS uint32_t zend_opcache_static_cache_retired_shared_graph_capacity; +extern ZEND_EXT_TLS HashTable *zend_opcache_static_cache_last_array_ht; +extern ZEND_EXT_TLS zend_opcache_static_cache_tracked_dependency *zend_opcache_static_cache_last_array_dependency; +extern ZEND_EXT_TLS zend_object *zend_opcache_static_cache_last_object_obj; +extern ZEND_EXT_TLS zend_opcache_static_cache_tracked_dependency *zend_opcache_static_cache_last_object_dependency; +extern ZEND_EXT_TLS zend_opcache_static_cache_pending_mutation zend_opcache_static_cache_pending_mutation_state; +extern ZEND_EXT_TLS bool zend_opcache_static_cache_capture_active; +extern ZEND_EXT_TLS bool zend_opcache_static_cache_capture_available; +extern ZEND_EXT_TLS zend_opcache_static_cache_static_slot_handle *zend_opcache_static_cache_capture_handle; +extern ZEND_EXT_TLS HashTable *zend_opcache_static_cache_capture_arrays; +extern ZEND_EXT_TLS HashTable *zend_opcache_static_cache_capture_objects; +extern ZEND_EXT_TLS zend_opcache_static_cache_lookup_entry zend_opcache_static_cache_volatile_lookup_entry_storage[ZEND_OPCACHE_STATIC_CACHE_LOOKUP_BUCKETS]; +extern ZEND_EXT_TLS zend_opcache_static_cache_lookup_entry zend_opcache_static_cache_persistent_lookup_entry_storage[ZEND_OPCACHE_STATIC_CACHE_LOOKUP_BUCKETS]; +extern ZEND_EXT_TLS HashTable *zend_opcache_static_cache_volatile_request_local_slots; +extern ZEND_EXT_TLS HashTable *zend_opcache_static_cache_persistent_request_local_slots; +extern ZEND_EXT_TLS HashTable *zend_opcache_static_cache_volatile_entry_locks; +extern ZEND_EXT_TLS HashTable *zend_opcache_static_cache_persistent_entry_locks; +extern ZEND_EXT_TLS uint32_t zend_opcache_static_cache_volatile_entry_lock_counts[ZEND_OPCACHE_STATIC_CACHE_ENTRY_LOCK_STRIPES]; +extern ZEND_EXT_TLS uint32_t zend_opcache_static_cache_persistent_entry_lock_counts[ZEND_OPCACHE_STATIC_CACHE_ENTRY_LOCK_STRIPES]; + +void zend_opcache_static_cache_reset_runtime(void); +void zend_opcache_static_cache_reset_storage(void); +bool zend_opcache_static_cache_header_init_locked(void); +void zend_opcache_static_cache_free_locked(uint32_t payload_offset); +uint32_t zend_opcache_static_cache_alloc_locked(size_t size, const void *source); +bool zend_opcache_static_cache_compact_to_fit_locked(size_t size); +bool zend_opcache_static_cache_startup_storage_before_request(void); +void zend_opcache_static_cache_shutdown_storage(void); +void zend_opcache_static_cache_ensure_ready(void); +void zend_opcache_static_cache_populate_array(zval *return_value); +bool zend_opcache_static_cache_rlock(void); +bool zend_opcache_static_cache_wlock(void); +void zend_opcache_static_cache_unlock(void); +bool zend_opcache_static_cache_acquire_entry_lock(zend_string *key); +bool zend_opcache_static_cache_try_acquire_entry_lock(zend_string *key); +bool zend_opcache_static_cache_has_entry_lock(zend_string *key); +void zend_opcache_static_cache_release_entry_lock(zend_string *key); +bool zend_opcache_static_cache_has_all_entry_locks(void); +void zend_opcache_static_cache_release_active_entry_locks(void); +void zend_opcache_static_cache_release_request_entry_locks(void); +void zend_opcache_static_cache_safe_direct_handlers_init(void); +void zend_opcache_static_cache_safe_direct_handlers_destroy(void); +void zend_opcache_static_cache_safe_direct_register_class( + zend_class_entry *ce, + const zend_opcache_static_cache_safe_direct_handlers *handlers); +zend_opcache_static_cache_safe_direct_state_copy_func_t zend_opcache_static_cache_safe_direct_copy_func( + zend_class_entry *ce, + zend_class_entry **base_ce_ptr); +zend_opcache_static_cache_safe_direct_state_has_unstorable_func_t zend_opcache_static_cache_safe_direct_state_has_unstorable_func( + zend_class_entry *ce); +zend_opcache_static_cache_safe_direct_state_serialize_func_t zend_opcache_static_cache_safe_direct_state_serialize_func( + zend_class_entry *ce); +zend_opcache_static_cache_safe_direct_state_unserialize_func_t zend_opcache_static_cache_safe_direct_state_unserialize_func( + zend_class_entry *ce); +bool zend_opcache_static_cache_safe_direct_allows_custom_serializers(zend_class_entry *ce); +bool zend_opcache_static_cache_safe_direct_state_includes_properties(zend_class_entry *ce); +bool zend_opcache_static_cache_calculate_shared_graph_size(const zval *value, size_t *buffer_len); +bool zend_opcache_static_cache_build_shared_graph_in_place(const zval *value, unsigned char *buffer, size_t buffer_len, size_t *graph_len); +bool zend_opcache_static_cache_shared_graph_copy_fits_buffer( + const unsigned char *source_buffer, + size_t source_buffer_len, + size_t source_graph_len, + const unsigned char *target_buffer, + size_t target_buffer_len); +bool zend_opcache_static_cache_fetch_shared_graph(const unsigned char *buffer, size_t buffer_len, zval *destination); +bool zend_opcache_static_cache_shared_graph_can_overwrite_payload_locked(uint32_t payload_offset); +bool zend_opcache_static_cache_shared_graph_acquire_locked(uint32_t payload_offset); +bool zend_opcache_static_cache_shared_graph_retire_payload_locked(uint32_t payload_offset); +bool zend_opcache_static_cache_shared_graph_release_ref_locked(uint32_t payload_offset); +bool zend_opcache_static_cache_has_request_shared_graph_ref(uint32_t payload_offset); +void zend_opcache_static_cache_register_shared_graph_ref(uint32_t payload_offset); +void zend_opcache_static_cache_defer_retired_shared_graph_free(uint32_t payload_offset); +void zend_opcache_static_cache_release_request_shared_graph_refs(void); +bool zend_opcache_static_cache_clear_locked(void); +bool zend_opcache_static_cache_prepare_value( + zend_opcache_static_cache_prepared_value *prepared, + zend_string *key, + zval *value, + bool throw_on_failure, + bool lock_held); +void zend_opcache_static_cache_destroy_prepared_value(zend_opcache_static_cache_prepared_value *prepared); +bool zend_opcache_static_cache_store_prepared_locked( + zend_string *key, + zval *value, + const zend_opcache_static_cache_prepared_value *prepared, + zend_long ttl, + bool throw_on_failure); +bool zend_opcache_static_cache_store_locked(zend_string *key, zval *value, zend_long ttl, bool throw_on_failure); +bool zend_opcache_static_cache_fetch_locked(zend_string *key, zval *return_value, bool throw_if_missing, bool *found_ptr, bool use_request_local_slot); +bool zend_opcache_static_cache_exists_locked(zend_string *key); +bool zend_opcache_static_cache_delete_locked(zend_string *key); +bool zend_opcache_static_cache_atomic_update_locked(zend_string *key, zend_long step, bool decrement, bool insert_if_missing, zend_long *new_value, const char *type_error_message); +void zend_opcache_static_cache_release_request_local_slots(void); +void zend_opcache_static_cache_update_mutation_hook_state(void); +bool zend_opcache_static_cache_prepare_memo_fetch(zval *value, zend_opcache_static_cache_prepared_value *prepared); +void zend_opcache_static_cache_prepare_memo_store(zval *value, zend_opcache_static_cache_prepared_value *prepared); +void zend_opcache_static_cache_delete_script_keys_locked(zend_persistent_script *persistent_script); +void zend_opcache_static_cache_register_hooks(void); +void zend_opcache_static_cache_unregister_hooks(void); +void zend_opcache_static_cache_request_init(void); +void zend_opcache_static_cache_capture_begin_for_handle(zend_opcache_static_cache_static_slot_handle *handle); +void zend_opcache_static_cache_capture_discard(void); +void zend_opcache_static_cache_capture_decoded_reachable_value(zval *value); +void zend_opcache_static_cache_capture_decoded_value(zval *value); +void zend_opcache_static_cache_request_shutdown(void); + +static zend_always_inline zend_opcache_static_cache_context *zend_opcache_static_cache_active_context(void) +{ + return zend_opcache_static_cache_active_context_ptr != NULL + ? zend_opcache_static_cache_active_context_ptr + : &zend_opcache_static_cache_volatile_context_state + ; +} + +static zend_always_inline void *zend_opcache_static_cache_base(void) +{ + zend_opcache_static_cache_storage *storage = &zend_opcache_static_cache_active_context()->storage; + + if (!storage->initialized || storage->segment_count != 1) { + return NULL; + } + + return storage->segments[0]->p; +} + +static zend_always_inline zend_opcache_static_cache_header *zend_opcache_static_cache_header_ptr(void) +{ + return (zend_opcache_static_cache_header *) zend_opcache_static_cache_base(); +} + +static zend_always_inline char *zend_opcache_static_cache_ptr(uint32_t offset) +{ + return (char *) zend_opcache_static_cache_base() + offset; +} + +static zend_always_inline zend_opcache_static_cache_block *zend_opcache_static_cache_block_ptr(uint32_t offset) +{ + return (zend_opcache_static_cache_block *) zend_opcache_static_cache_ptr(offset); +} + +static zend_always_inline uint32_t zend_opcache_static_cache_block_payload_capacity(uint32_t payload_offset) +{ + zend_opcache_static_cache_block *block; + + if (payload_offset < sizeof(zend_opcache_static_cache_block)) { + return 0; + } + + block = zend_opcache_static_cache_block_ptr(payload_offset - sizeof(zend_opcache_static_cache_block)); + if (block->size < sizeof(zend_opcache_static_cache_block)) { + return 0; + } + + return block->size - (uint32_t) sizeof(zend_opcache_static_cache_block); +} + +static zend_always_inline uint32_t zend_opcache_static_cache_write_payload_locked(uint32_t reusable_offset, size_t size, const void *source) +{ + if (reusable_offset != 0 && zend_opcache_static_cache_block_payload_capacity(reusable_offset) >= size) { + memcpy(zend_opcache_static_cache_ptr(reusable_offset), source, size); + + return reusable_offset; + } + + return zend_opcache_static_cache_alloc_locked(size, source); +} + +static zend_always_inline bool zend_opcache_static_cache_strategy_is_valid(zend_long strategy) +{ + return strategy == ZEND_OPCACHE_STATIC_CACHE_STRATEGY_IMMEDIATE || + strategy == ZEND_OPCACHE_STATIC_CACHE_STRATEGY_TRACKING + ; +} + +static zend_always_inline bool zend_opcache_static_cache_strategy_from_zval(zval *value, zend_long *strategy) +{ + zval *backing_value; + + if (value == NULL || + Z_TYPE_P(value) != IS_OBJECT || + Z_OBJCE_P(value) != zend_opcache_static_cache_strategy_ce + ) { + return false; + } + + backing_value = zend_enum_fetch_case_value(Z_OBJ_P(value)); + if (Z_TYPE_P(backing_value) != IS_LONG) { + return false; + } + + *strategy = Z_LVAL_P(backing_value); + + return zend_opcache_static_cache_strategy_is_valid(*strategy); +} + +static zend_always_inline bool zend_opcache_static_cache_strategy_from_attribute_zval(zval *value, zend_long *strategy) +{ + zend_ast *ast, *class_ast, *case_ast; + zend_string *class_name, *case_name; + + if (zend_opcache_static_cache_strategy_from_zval(value, strategy)) { + return true; + } + + if (value == NULL || Z_TYPE_P(value) != IS_CONSTANT_AST) { + return false; + } + + ast = Z_ASTVAL_P(value); + if (ast->kind != ZEND_AST_CLASS_CONST) { + return false; + } + + class_ast = ast->child[0]; + case_ast = ast->child[1]; + if (class_ast == NULL || + case_ast == NULL || + class_ast->kind != ZEND_AST_ZVAL || + case_ast->kind != ZEND_AST_ZVAL || + Z_TYPE_P(zend_ast_get_zval(class_ast)) != IS_STRING || + Z_TYPE_P(zend_ast_get_zval(case_ast)) != IS_STRING + ) { + return false; + } + + class_name = zend_ast_get_str(class_ast); + if (!zend_string_equals_literal_ci(class_name, "OPcache\\CacheStrategy")) { + return false; + } + + case_name = zend_ast_get_str(case_ast); + if (zend_string_equals_literal(case_name, "Immediate")) { + *strategy = ZEND_OPCACHE_STATIC_CACHE_STRATEGY_IMMEDIATE; + + return true; + } + + if (zend_string_equals_literal(case_name, "Tracking")) { + *strategy = ZEND_OPCACHE_STATIC_CACHE_STRATEGY_TRACKING; + + return true; + } + + return false; +} + +static zend_always_inline void zend_opcache_static_cache_volatile_static_attribute_config_init( + zend_opcache_static_cache_volatile_static_attribute_config *config) +{ + config->ttl = 0; + config->strategy = ZEND_OPCACHE_STATIC_CACHE_STRATEGY_IMMEDIATE; +} + +static zend_always_inline bool zend_opcache_static_cache_volatile_static_attribute_parse_ttl( + zend_attribute *attr, + uint32_t arg_num, + zend_class_entry *scope, + zend_long *ttl, + zend_string **error) +{ + zval ttl_zv; + + ZVAL_COPY_OR_DUP(&ttl_zv, &attr->args[arg_num].value); + if (Z_TYPE(ttl_zv) != IS_LONG) { + zval_ptr_dtor(&ttl_zv); + if (zend_get_attribute_value(&ttl_zv, attr, arg_num, scope) == FAILURE) { + *error = ZSTR_INIT_LITERAL("OPcache\\VolatileStatic ttl must be resolvable", 0); + + return false; + } + } + + if (Z_TYPE(ttl_zv) != IS_LONG) { + *error = zend_strpprintf(0, + "OPcache\\VolatileStatic ttl must be of type int, %s given", + zend_zval_value_name(&ttl_zv) + ); + zval_ptr_dtor(&ttl_zv); + + return false; + } + + if (Z_LVAL(ttl_zv) < 0) { + *error = ZSTR_INIT_LITERAL("OPcache\\VolatileStatic ttl must be greater than or equal to 0", 0); + zval_ptr_dtor(&ttl_zv); + + return false; + } + + *ttl = Z_LVAL(ttl_zv); + zval_ptr_dtor(&ttl_zv); + + return true; +} + +static zend_always_inline bool zend_opcache_static_cache_volatile_static_attribute_parse_strategy( + zend_attribute *attr, + uint32_t arg_num, + zend_class_entry *scope, + zend_long *strategy, + zend_string **error) +{ + zval strategy_zv; + + ZVAL_COPY_OR_DUP(&strategy_zv, &attr->args[arg_num].value); + if (!zend_opcache_static_cache_strategy_from_attribute_zval(&strategy_zv, strategy)) { + zval_ptr_dtor(&strategy_zv); + if (zend_get_attribute_value(&strategy_zv, attr, arg_num, scope) == FAILURE) { + *error = ZSTR_INIT_LITERAL("OPcache\\VolatileStatic strategy must be resolvable", 0); + + return false; + } + } + + if (!zend_opcache_static_cache_strategy_from_attribute_zval(&strategy_zv, strategy)) { + *error = zend_strpprintf(0, + "OPcache\\VolatileStatic strategy must be of type OPcache\\CacheStrategy, %s given", + zend_zval_value_name(&strategy_zv) + ); + zval_ptr_dtor(&strategy_zv); + + return false; + } + + zval_ptr_dtor(&strategy_zv); + + return true; +} + +static zend_always_inline bool zend_opcache_static_cache_volatile_static_attribute_config_from_attribute( + zend_attribute *attr, + zend_class_entry *scope, + zend_opcache_static_cache_volatile_static_attribute_config *config, + zend_string **error) +{ + uint32_t i; + bool ttl_seen = false, strategy_seen = false; + + zend_opcache_static_cache_volatile_static_attribute_config_init(config); + *error = NULL; + + if (attr == NULL || attr->argc == 0) { + return true; + } + + if (attr->argc > 2) { + *error = ZSTR_INIT_LITERAL("OPcache\\VolatileStatic expects at most 2 arguments", 0); + + return false; + } + + for (i = 0; i < attr->argc; i++) { + if (attr->args[i].name != NULL) { + if (zend_string_equals_literal(attr->args[i].name, "ttl")) { + if (ttl_seen) { + *error = ZSTR_INIT_LITERAL("Named parameter $ttl overwrites previous argument", 0); + + return false; + } + + ttl_seen = true; + if (!zend_opcache_static_cache_volatile_static_attribute_parse_ttl(attr, i, scope, &config->ttl, error)) { + return false; + } + + continue; + } + + if (zend_string_equals_literal(attr->args[i].name, "strategy")) { + if (strategy_seen) { + *error = ZSTR_INIT_LITERAL("Named parameter $strategy overwrites previous argument", 0); + + return false; + } + + strategy_seen = true; + if (!zend_opcache_static_cache_volatile_static_attribute_parse_strategy(attr, i, scope, &config->strategy, error)) { + return false; + } + + continue; + } + + *error = zend_strpprintf(0, "Unknown named parameter $%s", ZSTR_VAL(attr->args[i].name)); + + return false; + } + + if (i == 0) { + if (ttl_seen) { + *error = ZSTR_INIT_LITERAL("Named parameter $ttl overwrites previous argument", 0); + + return false; + } + + ttl_seen = true; + if (!zend_opcache_static_cache_volatile_static_attribute_parse_ttl(attr, i, scope, &config->ttl, error)) { + return false; + } + + continue; + } + + if (strategy_seen) { + *error = ZSTR_INIT_LITERAL("Named parameter $strategy overwrites previous argument", 0); + + return false; + } + + strategy_seen = true; + if (!zend_opcache_static_cache_volatile_static_attribute_parse_strategy(attr, i, scope, &config->strategy, error)) { + return false; + } + } + + return true; +} + +static zend_always_inline zend_opcache_static_cache_context *zend_opcache_static_cache_activate_context(zend_opcache_static_cache_context *context) +{ + zend_opcache_static_cache_context *previous = zend_opcache_static_cache_active_context_ptr; + + zend_opcache_static_cache_active_context_ptr = context; + + return previous; +} + +static zend_always_inline void zend_opcache_static_cache_restore_context(zend_opcache_static_cache_context *context) +{ + zend_opcache_static_cache_active_context_ptr = context; +} + +static zend_always_inline zend_opcache_static_cache_runtime *zend_opcache_static_cache_context_runtime(zend_opcache_static_cache_context *context) +{ + /* Runtime readiness is request-local under ZTS, while storage remains shared. */ + return context == &zend_opcache_static_cache_persistent_context_state + ? &zend_opcache_static_cache_persistent_runtime_state + : &zend_opcache_static_cache_volatile_runtime_state + ; +} + +static zend_always_inline zend_opcache_static_cache_runtime *zend_opcache_static_cache_active_runtime(void) +{ + return zend_opcache_static_cache_context_runtime(zend_opcache_static_cache_active_context()); +} + +static zend_always_inline zend_opcache_static_cache_lookup_entry *zend_opcache_static_cache_active_lookup_entries(void) +{ + return zend_opcache_static_cache_active_context() == &zend_opcache_static_cache_persistent_context_state + ? zend_opcache_static_cache_persistent_lookup_entry_storage + : zend_opcache_static_cache_volatile_lookup_entry_storage + ; +} + +static zend_always_inline HashTable **zend_opcache_static_cache_active_request_local_slots_ptr(void) +{ + return zend_opcache_static_cache_active_context() == &zend_opcache_static_cache_persistent_context_state + ? &zend_opcache_static_cache_persistent_request_local_slots + : &zend_opcache_static_cache_volatile_request_local_slots + ; +} + +static zend_always_inline zend_opcache_static_cache_lookup_entry *zend_opcache_static_cache_lookup_cache_set(zend_ulong hash) +{ + uint32_t set_index = (uint32_t) (hash & (ZEND_OPCACHE_STATIC_CACHE_LOOKUP_SETS - 1)); + + return &zend_opcache_static_cache_active_lookup_entries()[set_index * ZEND_OPCACHE_STATIC_CACHE_LOOKUP_WAYS]; +} + +static zend_always_inline void zend_opcache_static_cache_lookup_cache_store( + zend_opcache_static_cache_lookup_entry *lookup_entry, + zend_ulong hash, + uint64_t mutation_epoch, + uint32_t slot_index, + uint8_t state) +{ + if (lookup_entry == NULL) { + return; + } + + lookup_entry->hash = hash; + lookup_entry->mutation_epoch = mutation_epoch; + lookup_entry->slot_index = slot_index; + lookup_entry->state = state; +} + +static zend_always_inline zend_opcache_static_cache_lookup_entry *zend_opcache_static_cache_lookup_cache_preferred_way( + zend_opcache_static_cache_lookup_entry *lookup_entries, + zend_ulong hash) +{ + uint32_t way = (uint32_t) ((((uint64_t) hash >> 32) ^ hash) & (ZEND_OPCACHE_STATIC_CACHE_LOOKUP_WAYS - 1)); + + return &lookup_entries[way]; +} + +static zend_always_inline zend_opcache_static_cache_lookup_entry *zend_opcache_static_cache_lookup_cache_select_slot( + zend_opcache_static_cache_lookup_entry *lookup_entries, + zend_ulong hash, + uint64_t mutation_epoch, + bool allow_hit_eviction) +{ + zend_opcache_static_cache_lookup_entry *preferred, *alternate; + + if (lookup_entries == NULL) { + return NULL; + } + + preferred = zend_opcache_static_cache_lookup_cache_preferred_way(lookup_entries, hash); + alternate = preferred == &lookup_entries[0] ? &lookup_entries[1] : &lookup_entries[0]; + + /* Keep each set two-way associative: the hash chooses a stable preferred + * way, while the alternate way gives short collision chains a request-local + * fast path without touching the shared entry table. Mutation epochs make + * stale hit/miss entries self-invalidating after any writer updates SHM. */ + if (preferred->state != ZEND_OPCACHE_STATIC_CACHE_LOOKUP_EMPTY && preferred->hash == hash && preferred->mutation_epoch == mutation_epoch) { + return preferred; + } + if (alternate->state != ZEND_OPCACHE_STATIC_CACHE_LOOKUP_EMPTY && alternate->hash == hash && alternate->mutation_epoch == mutation_epoch) { + return alternate; + } + + if (preferred->state == ZEND_OPCACHE_STATIC_CACHE_LOOKUP_EMPTY || preferred->mutation_epoch != mutation_epoch) { + return preferred; + } + if (alternate->state == ZEND_OPCACHE_STATIC_CACHE_LOOKUP_EMPTY || alternate->mutation_epoch != mutation_epoch) { + return alternate; + } + + if (preferred->state == ZEND_OPCACHE_STATIC_CACHE_LOOKUP_MISS) { + return preferred; + } + if (alternate->state == ZEND_OPCACHE_STATIC_CACHE_LOOKUP_MISS) { + return alternate; + } + + return allow_hit_eviction ? preferred : NULL; +} + +static zend_always_inline void zend_opcache_static_cache_lookup_cache_store_miss( + zend_opcache_static_cache_lookup_entry *lookup_entries, + zend_ulong hash, + uint64_t mutation_epoch) +{ + zend_opcache_static_cache_lookup_entry *victim = zend_opcache_static_cache_lookup_cache_select_slot(lookup_entries, hash, mutation_epoch, false); + + if (victim == NULL) { + return; + } + + zend_opcache_static_cache_lookup_cache_store( + victim, + hash, + mutation_epoch, + ZEND_OPCACHE_STATIC_CACHE_LOOKUP_INVALID_SLOT, + ZEND_OPCACHE_STATIC_CACHE_LOOKUP_MISS + ); +} + +static zend_always_inline void zend_opcache_static_cache_lookup_cache_store_hit( + zend_opcache_static_cache_lookup_entry *lookup_entry, + zend_ulong hash, + uint64_t mutation_epoch, + uint32_t slot_index) +{ + zend_opcache_static_cache_lookup_cache_store( + lookup_entry, + hash, + mutation_epoch, + slot_index, + ZEND_OPCACHE_STATIC_CACHE_LOOKUP_HIT + ); +} + +static zend_always_inline bool zend_opcache_static_cache_header_is_initialized_locked(void) +{ + zend_opcache_static_cache_header *header = zend_opcache_static_cache_header_ptr(); + + return header != NULL && + header->magic == ZEND_OPCACHE_STATIC_CACHE_MAGIC && + header->version == ZEND_OPCACHE_STATIC_CACHE_VERSION + ; +} + +static zend_always_inline void zend_opcache_static_cache_bump_mutation_epoch_locked(zend_opcache_static_cache_header *header) +{ + if (header == NULL) { + return; + } + + /* uint64_t makes wrap-around essentially unreachable in practice, but on + * wrap we skip 0 so a freshly bumped epoch can never coincide with the + * zero-initialized lookup_entry slot value. */ + header->mutation_epoch++; + if (header->mutation_epoch == 0) { + header->mutation_epoch = 1; + } +} + +static zend_always_inline zend_opcache_static_cache_entry *zend_opcache_static_cache_entries(zend_opcache_static_cache_header *header) +{ + return (zend_opcache_static_cache_entry *) ((char *) header + sizeof(zend_opcache_static_cache_header)); +} + +static zend_always_inline bool zend_opcache_static_cache_key_equals( + const zend_opcache_static_cache_entry *entry, + zend_string *key, + zend_ulong hash) +{ + if (entry->state != ZEND_OPCACHE_STATIC_CACHE_ENTRY_USED || entry->hash != hash || entry->key_len != ZSTR_LEN(key)) { + return false; + } + + return memcmp(zend_opcache_static_cache_ptr(entry->key_offset), ZSTR_VAL(key), ZSTR_LEN(key)) == 0; +} + +static zend_always_inline bool zend_opcache_static_cache_value_uses_offset(uint8_t value_type) +{ + return + value_type == ZEND_OPCACHE_STATIC_CACHE_VALUE_STRING || + value_type == ZEND_OPCACHE_STATIC_CACHE_VALUE_SERIALIZED || + value_type == ZEND_OPCACHE_STATIC_CACHE_VALUE_OPCACHE_SERIALIZED || + value_type == ZEND_OPCACHE_STATIC_CACHE_VALUE_SHARED_GRAPH + ; +} + +static zend_always_inline bool zend_opcache_static_cache_block_is_free(const zend_opcache_static_cache_block *block) +{ + return (block->flags & ZEND_OPCACHE_STATIC_CACHE_BLOCK_FREE) != 0; +} + +static zend_always_inline void zend_opcache_static_cache_block_mark_free(zend_opcache_static_cache_block *block) +{ + block->flags |= ZEND_OPCACHE_STATIC_CACHE_BLOCK_FREE; +} + +static zend_always_inline void zend_opcache_static_cache_block_mark_used(zend_opcache_static_cache_block *block) +{ + block->flags &= ~ZEND_OPCACHE_STATIC_CACHE_BLOCK_FREE; + block->next_free = 0; + block->prev_free = 0; +} + +static zend_always_inline void zend_opcache_static_cache_clear_lookup_cache_context(zend_opcache_static_cache_context *context) +{ + zend_opcache_static_cache_lookup_entry *entries = context == &zend_opcache_static_cache_persistent_context_state + ? zend_opcache_static_cache_persistent_lookup_entry_storage + : zend_opcache_static_cache_volatile_lookup_entry_storage + ; + + memset(entries, 0, sizeof(zend_opcache_static_cache_lookup_entry) * ZEND_OPCACHE_STATIC_CACHE_LOOKUP_BUCKETS); +} + +static zend_always_inline void zend_opcache_static_cache_lookup_cache_reset_entry(zend_opcache_static_cache_lookup_entry *lookup_entry) +{ + if (lookup_entry == NULL) { + return; + } + + memset(lookup_entry, 0, sizeof(*lookup_entry)); +} + +static zend_always_inline void zend_opcache_static_cache_clear_lookup_caches(void) +{ + zend_opcache_static_cache_clear_lookup_cache_context(&zend_opcache_static_cache_volatile_context_state); + zend_opcache_static_cache_clear_lookup_cache_context(&zend_opcache_static_cache_persistent_context_state); +} + +static zend_always_inline bool *zend_opcache_static_cache_skip_publish_ptr(zend_opcache_static_cache_context *context) +{ + return context == &zend_opcache_static_cache_persistent_context_state + ? &zend_opcache_static_cache_persistent_skip_publish + : &zend_opcache_static_cache_volatile_skip_publish + ; +} + +static zend_always_inline void zend_opcache_static_cache_mark_publish_skipped(zend_opcache_static_cache_context *context) +{ + *zend_opcache_static_cache_skip_publish_ptr(context) = true; +} + +static zend_always_inline bool zend_opcache_static_cache_publish_skipped(zend_opcache_static_cache_context *context) +{ + return *zend_opcache_static_cache_skip_publish_ptr(context); +} + +static zend_always_inline void zend_opcache_static_cache_reset_publish_skips(void) +{ + zend_opcache_static_cache_volatile_skip_publish = false; + zend_opcache_static_cache_persistent_skip_publish = false; +} + +#endif /* ZEND_STATIC_CACHE_INTERNAL_H */ diff --git a/ext/opcache/zend_static_cache_shared_graph.c b/ext/opcache/zend_static_cache_shared_graph.c new file mode 100644 index 000000000000..bd8d4f5911c5 --- /dev/null +++ b/ext/opcache/zend_static_cache_shared_graph.c @@ -0,0 +1,1583 @@ +/* + +----------------------------------------------------------------------+ + | Zend OPcache | + +----------------------------------------------------------------------+ + | Copyright © The PHP Group and Contributors. | + +----------------------------------------------------------------------+ + | This source file is subject to the Modified BSD License that is | + | bundled with this package in the file LICENSE, and is available | + | through the World Wide Web at . | + | | + | SPDX-License-Identifier: BSD-3-Clause | + +----------------------------------------------------------------------+ + | Author: Go Kudo | + +----------------------------------------------------------------------+ +*/ + +#include "zend_static_cache_internal.h" +#include "zend_opcache_serializer.h" + +/* Shared-graph payloads start inside generic SHM blocks whose payload base may + * be less aligned than zend managed objects require. Align the graph start + * within the payload and treat that aligned layout as the only valid format. */ +static zend_always_inline size_t zend_opcache_static_cache_shared_graph_alignment_padding(const void *buffer) +{ + uintptr_t raw_address, aligned_address; + + raw_address = (uintptr_t) buffer; + aligned_address = (uintptr_t) ZEND_MM_ALIGNED_SIZE(raw_address); + + return (size_t) (aligned_address - raw_address); +} + +static zend_always_inline const unsigned char *zend_opcache_static_cache_shared_graph_locate_buffer( + const unsigned char *buffer, + size_t buffer_len, + size_t *graph_len +) +{ + const zend_opcache_static_cache_shared_graph_header *header; + size_t padding; + + padding = zend_opcache_static_cache_shared_graph_alignment_padding(buffer); + if (padding > buffer_len || buffer_len - padding < sizeof(zend_opcache_static_cache_shared_graph_header)) { + return NULL; + } + + buffer += padding; + buffer_len -= padding; + header = (const zend_opcache_static_cache_shared_graph_header *) buffer; + if (header->magic != ZEND_OPCACHE_STATIC_CACHE_SHARED_GRAPH_MAGIC || + header->version != ZEND_OPCACHE_STATIC_CACHE_SHARED_GRAPH_VERSION + ) { + return NULL; + } + + if (graph_len != NULL) { + *graph_len = buffer_len; + } + + return buffer; +} + +static zend_always_inline void zend_opcache_static_cache_shared_graph_calc_init(zend_opcache_static_cache_shared_graph_calc_context *context) +{ + context->size = 0; + + zend_hash_init(&context->seen_arrays, 8, NULL, NULL, 0); + zend_hash_init(&context->seen_objects, 8, NULL, NULL, 0); +} + +static zend_always_inline void zend_opcache_static_cache_shared_graph_calc_destroy(zend_opcache_static_cache_shared_graph_calc_context *context) +{ + zend_hash_destroy(&context->seen_objects); + zend_hash_destroy(&context->seen_arrays); +} + +static zend_always_inline bool zend_opcache_static_cache_shared_graph_reserve_size(size_t *size, size_t amount) +{ + size_t aligned_amount; + + aligned_amount = ZEND_ALIGNED_SIZE(amount); + if (*size > SIZE_MAX - aligned_amount) { + return false; + } + + *size += aligned_amount; + + return *size <= UINT32_MAX; +} + +static zend_always_inline bool zend_opcache_static_cache_shared_graph_mark_seen(HashTable *seen_objects, zend_object *object) +{ + zend_ulong object_key; + + object_key = (zend_ulong) (uintptr_t) object; + if (zend_hash_index_exists(seen_objects, object_key)) { + return false; + } + + return zend_hash_index_add_empty_element(seen_objects, object_key) != NULL; +} + +static zend_always_inline bool zend_opcache_static_cache_shared_graph_mark_seen_array(HashTable *seen_arrays, const HashTable *array) +{ + zend_ulong array_key; + + array_key = (zend_ulong) (uintptr_t) array; + if (zend_hash_index_exists(seen_arrays, array_key)) { + return false; + } + + return zend_hash_index_add_empty_element(seen_arrays, array_key) != NULL; +} + +static zend_always_inline bool zend_opcache_static_cache_shared_graph_has_custom_serialization(zend_class_entry *ce) +{ + return ce->create_object != NULL || + ce->__serialize != NULL || + ce->__unserialize != NULL || + zend_hash_str_exists(&ce->function_table, ZEND_STRL("__sleep")) || + zend_hash_str_exists(&ce->function_table, ZEND_STRL("__wakeup")) + ; +} + +static zend_always_inline bool zend_opcache_static_cache_shared_graph_can_restore_direct(zend_class_entry *ce) +{ + if (ce->type != ZEND_USER_CLASS) { + return false; + } + + if (zend_opcache_serializer_find_safe_direct_cache_base(ce) != NULL) { + return false; + } + + return !zend_opcache_static_cache_shared_graph_has_custom_serialization(ce); +} + +static bool zend_opcache_static_cache_shared_graph_can_copy_direct_value(HashTable *seen_arrays, const zval *value) +{ + const HashTable *array; + const Bucket *bucket; + const zval *packed_value; + zend_ulong array_key; + uint32_t index; + bool result = true; + + switch (Z_TYPE_P(value)) { + case IS_UNDEF: + case IS_NULL: + case IS_FALSE: + case IS_TRUE: + case IS_LONG: + case IS_DOUBLE: + case IS_STRING: + return true; + case IS_ARRAY: + array = Z_ARRVAL_P(value); + if (array->nNumOfElements == 0) { + return true; + } + + if (!zend_opcache_static_cache_shared_graph_mark_seen_array(seen_arrays, array)) { + return false; + } + + array_key = (zend_ulong) (uintptr_t) array; + if (HT_IS_PACKED(array)) { + for (index = 0; index < array->nNumUsed; index++) { + packed_value = &array->arPacked[index]; + if (!zend_opcache_static_cache_shared_graph_can_copy_direct_value(seen_arrays, packed_value)) { + result = false; + break; + } + } + } else { + bucket = array->arData; + for (index = 0; index < array->nNumUsed; index++) { + if (Z_TYPE(bucket[index].val) != IS_UNDEF && + !zend_opcache_static_cache_shared_graph_can_copy_direct_value(seen_arrays, &bucket[index].val) + ) { + result = false; + break; + } + } + } + + zend_hash_index_del(seen_arrays, array_key); + + return result; + default: + return false; + } +} + +static bool zend_opcache_static_cache_shared_graph_calc_direct_value( + zend_opcache_static_cache_shared_graph_calc_context *context, + const zval *value) +{ + const HashTable *array; + const Bucket *bucket; + const zval *packed_value; + zend_ulong array_key; + uint32_t index; + size_t data_size; + bool result = true; + + switch (Z_TYPE_P(value)) { + case IS_UNDEF: + case IS_NULL: + case IS_FALSE: + case IS_TRUE: + case IS_LONG: + case IS_DOUBLE: + return true; + case IS_STRING: + return zend_opcache_static_cache_shared_graph_reserve_size( + &context->size, + _ZSTR_STRUCT_SIZE(ZSTR_LEN(Z_STR_P(value))) + ); + case IS_ARRAY: + array = Z_ARRVAL_P(value); + if (array->nNumOfElements == 0) { + return true; + } + + if (!zend_opcache_static_cache_shared_graph_mark_seen_array(&context->seen_arrays, array)) { + return false; + } + + array_key = (zend_ulong) (uintptr_t) array; + data_size = HT_IS_PACKED(array) ? HT_PACKED_USED_SIZE(array) : HT_USED_SIZE(array); + if (!zend_opcache_static_cache_shared_graph_reserve_size(&context->size, sizeof(zend_array)) || + !zend_opcache_static_cache_shared_graph_reserve_size(&context->size, data_size) + ) { + result = false; + goto direct_array_done; + } + + if (HT_IS_PACKED(array)) { + for (index = 0; index < array->nNumUsed; index++) { + packed_value = &array->arPacked[index]; + if (!zend_opcache_static_cache_shared_graph_calc_direct_value(context, packed_value)) { + result = false; + break; + } + } + } else { + bucket = array->arData; + for (index = 0; index < array->nNumUsed; index++) { + if (bucket[index].key != NULL && + !zend_opcache_static_cache_shared_graph_reserve_size( + &context->size, + _ZSTR_STRUCT_SIZE(ZSTR_LEN(bucket[index].key)) + ) + ) { + result = false; + break; + } + + if (Z_TYPE(bucket[index].val) != IS_UNDEF && + !zend_opcache_static_cache_shared_graph_calc_direct_value(context, &bucket[index].val) + ) { + result = false; + break; + } + } + } + +direct_array_done: + zend_hash_index_del(&context->seen_arrays, array_key); + + return result; + default: + return false; + } +} + +static bool zend_opcache_static_cache_shared_graph_calc_value( + zend_opcache_static_cache_shared_graph_calc_context *context, + const zval *value +) +{ + const HashTable *array; + zend_object *object; + zend_class_entry *ce, *safe_direct_base; + zend_string *key, *property_name; + zend_ulong array_key; + zval *element, *property_value, *source_value; + HashTable *properties; + uint32_t property_count; + size_t encoded_len; + bool result; + unsigned char *encoded; + + switch (Z_TYPE_P(value)) { + case IS_UNDEF: + case IS_NULL: + case IS_FALSE: + case IS_TRUE: + case IS_LONG: + case IS_DOUBLE: + return true; + case IS_STRING: + return zend_opcache_static_cache_shared_graph_reserve_size(&context->size, _ZSTR_STRUCT_SIZE(ZSTR_LEN(Z_STR_P(value)))); + case IS_ARRAY: + array = Z_ARRVAL_P(value); + + if (array->nNumOfElements == 0) { + return true; + } + + if (zend_opcache_static_cache_shared_graph_can_copy_direct_value(&context->seen_arrays, value)) { + return zend_opcache_static_cache_shared_graph_calc_direct_value(context, value); + } + + if (!zend_opcache_static_cache_shared_graph_mark_seen_array(&context->seen_arrays, array)) { + return false; + } + + array_key = (zend_ulong) (uintptr_t) array; + result = true; + + if (!zend_opcache_static_cache_shared_graph_reserve_size(&context->size, sizeof(zend_opcache_static_cache_shared_graph_array)) || + !zend_opcache_static_cache_shared_graph_reserve_size( + &context->size, + (size_t) array->nNumOfElements * sizeof(zend_opcache_static_cache_shared_graph_array_element) + ) + ) { + result = false; + + goto array_done; + } + + ZEND_HASH_FOREACH_STR_KEY_VAL((HashTable *) array, key, element) { + if (key != NULL && !zend_opcache_static_cache_shared_graph_reserve_size(&context->size, _ZSTR_STRUCT_SIZE(ZSTR_LEN(key)))) { + result = false; + break; + } + + if (!zend_opcache_static_cache_shared_graph_calc_value(context, element)) { + result = false; + break; + } + } ZEND_HASH_FOREACH_END(); + + goto array_done; + case IS_OBJECT: + object = Z_OBJ_P(value); + ce = object->ce; + + safe_direct_base = zend_opcache_serializer_find_safe_direct_cache_base(ce); + if (safe_direct_base != NULL) { + if (zend_opcache_serializer_has_safe_direct_cache_overrides(ce, safe_direct_base)) { + return false; + } + + encoded = NULL; + encoded_len = 0; + if (!zend_opcache_serialize(&encoded, &encoded_len, value)) { + return false; + } + + efree(encoded); + + return zend_opcache_static_cache_shared_graph_reserve_size( + &context->size, + sizeof(zend_opcache_static_cache_shared_graph_serialized_object) + encoded_len - 1 + ); + } + + if (!zend_opcache_static_cache_shared_graph_can_restore_direct(ce)) { + return false; + } + + if (!zend_opcache_static_cache_shared_graph_mark_seen(&context->seen_objects, object)) { + return false; + } + + if (!zend_opcache_static_cache_shared_graph_reserve_size(&context->size, sizeof(zend_opcache_static_cache_shared_graph_object))) { + return false; + } + + if (!zend_opcache_static_cache_shared_graph_reserve_size(&context->size, _ZSTR_STRUCT_SIZE(ZSTR_LEN(ce->name)))) { + return false; + } + + properties = zend_get_properties_for((zval *) value, ZEND_PROP_PURPOSE_SERIALIZE); + property_count = properties != NULL ? properties->nNumOfElements : 0; + if (property_count != 0 && + !zend_opcache_static_cache_shared_graph_reserve_size( + &context->size, + (size_t) property_count * sizeof(zend_opcache_static_cache_shared_graph_property) + ) + ) { + if (properties != NULL) { + zend_release_properties(properties); + } + return false; + } + + if (properties != NULL) { + ZEND_HASH_FOREACH_STR_KEY_VAL(properties, property_name, property_value) { + if (property_name == NULL || !zend_opcache_static_cache_shared_graph_reserve_size(&context->size, _ZSTR_STRUCT_SIZE(ZSTR_LEN(property_name)))) { + zend_release_properties(properties); + return false; + } + + source_value = Z_TYPE_P(property_value) == IS_INDIRECT ? Z_INDIRECT_P(property_value) : property_value; + if (!zend_opcache_static_cache_shared_graph_calc_value(context, source_value)) { + zend_release_properties(properties); + return false; + } + } ZEND_HASH_FOREACH_END(); + + zend_release_properties(properties); + } + + return true; + default: + return false; + } + +array_done: + zend_hash_index_del(&context->seen_arrays, array_key); + + return result; +} + +static void zend_opcache_static_cache_shared_graph_copy_init( + zend_opcache_static_cache_shared_graph_copy_context *context, + unsigned char *buffer, + size_t size +) +{ + context->buffer = buffer; + context->size = size; + context->position = 0; + + zend_hash_init(&context->seen_arrays, 8, NULL, NULL, 0); + zend_hash_init(&context->seen_objects, 8, NULL, NULL, 0); +} + +static void zend_opcache_static_cache_shared_graph_copy_destroy(zend_opcache_static_cache_shared_graph_copy_context *context) +{ + zend_hash_destroy(&context->seen_objects); + zend_hash_destroy(&context->seen_arrays); +} + +static bool zend_opcache_static_cache_shared_graph_copy_alloc( + zend_opcache_static_cache_shared_graph_copy_context *context, + size_t amount, + uint32_t *offset +) +{ + size_t aligned_amount; + + aligned_amount = ZEND_ALIGNED_SIZE(amount); + if (context->position > context->size || aligned_amount > context->size - context->position) { + return false; + } + + *offset = (uint32_t) context->position; + memset(context->buffer + context->position, 0, aligned_amount); + context->position += aligned_amount; + + return true; +} + +static bool zend_opcache_static_cache_shared_graph_copy_string( + zend_opcache_static_cache_shared_graph_copy_context *context, + const zend_string *string, + uint32_t *offset +) +{ + zend_string *new_string; + uint32_t string_offset; + size_t string_size; + + string_size = _ZSTR_STRUCT_SIZE(ZSTR_LEN(string)); + if (!zend_opcache_static_cache_shared_graph_copy_alloc(context, string_size, &string_offset)) { + return false; + } + + new_string = (zend_string *) (context->buffer + string_offset); + memcpy(new_string, string, string_size); + GC_SET_REFCOUNT(new_string, 2); + GC_TYPE_INFO(new_string) = GC_STRING | ((IS_STR_INTERNED | IS_STR_PERMANENT) << GC_FLAGS_SHIFT); + *offset = string_offset; + + return true; +} + +static bool zend_opcache_static_cache_shared_graph_copy_direct_value( + zend_opcache_static_cache_shared_graph_copy_context *context, + const zval *source, + zval *destination) +{ + const HashTable *source_array; + const Bucket *source_bucket; + const zval *source_packed; + zend_array *target; + zend_ulong array_key; + zval *target_packed; + Bucket *target_bucket; + uint32_t string_offset, array_offset, data_offset, key_offset, index; + size_t data_size; + bool result = true; + + switch (Z_TYPE_P(source)) { + case IS_UNDEF: + ZVAL_UNDEF(destination); + return true; + case IS_NULL: + ZVAL_NULL(destination); + return true; + case IS_TRUE: + ZVAL_TRUE(destination); + return true; + case IS_FALSE: + ZVAL_FALSE(destination); + return true; + case IS_LONG: + ZVAL_LONG(destination, Z_LVAL_P(source)); + return true; + case IS_DOUBLE: + ZVAL_DOUBLE(destination, Z_DVAL_P(source)); + return true; + case IS_STRING: + if (!zend_opcache_static_cache_shared_graph_copy_string(context, Z_STR_P(source), &string_offset)) { + return false; + } + + ZVAL_INTERNED_STR(destination, (zend_string *) (void *) (context->buffer + string_offset)); + return true; + case IS_ARRAY: + source_array = Z_ARRVAL_P(source); + if (source_array->nNumOfElements == 0) { + ZVAL_EMPTY_ARRAY(destination); + return true; + } + + if (!zend_opcache_static_cache_shared_graph_mark_seen_array(&context->seen_arrays, source_array)) { + return false; + } + + array_key = (zend_ulong) (uintptr_t) source_array; + data_size = HT_IS_PACKED(source_array) ? HT_PACKED_USED_SIZE(source_array) : HT_USED_SIZE(source_array); + if (!zend_opcache_static_cache_shared_graph_copy_alloc(context, sizeof(zend_array), &array_offset) || + !zend_opcache_static_cache_shared_graph_copy_alloc(context, data_size, &data_offset) + ) { + result = false; + goto copy_direct_array_done; + } + + target = (zend_array *) (context->buffer + array_offset); + memcpy(target, source_array, sizeof(zend_array)); + memcpy(context->buffer + data_offset, HT_GET_DATA_ADDR(source_array), data_size); + GC_SET_REFCOUNT(target, 2); + GC_TYPE_INFO(target) = GC_ARRAY | ((IS_ARRAY_IMMUTABLE | GC_NOT_COLLECTABLE) << GC_FLAGS_SHIFT); + HT_FLAGS(target) |= HASH_FLAG_STATIC_KEYS; + target->pDestructor = NULL; + target->nInternalPointer = 0; + HT_SET_DATA_ADDR(target, context->buffer + data_offset); + + if (HT_IS_PACKED(source_array)) { + target_packed = target->arPacked; + for (index = 0; index < source_array->nNumUsed; index++) { + source_packed = &source_array->arPacked[index]; + if (!zend_opcache_static_cache_shared_graph_copy_direct_value(context, source_packed, &target_packed[index])) { + result = false; + break; + } + } + } else { + source_bucket = source_array->arData; + target_bucket = target->arData; + for (index = 0; index < source_array->nNumUsed; index++) { + if (source_bucket[index].key != NULL) { + if (!zend_opcache_static_cache_shared_graph_copy_string(context, source_bucket[index].key, &key_offset)) { + result = false; + break; + } + + target_bucket[index].key = (zend_string *) (void *) (context->buffer + key_offset); + } else { + target_bucket[index].key = NULL; + } + + if (!zend_opcache_static_cache_shared_graph_copy_direct_value(context, &source_bucket[index].val, &target_bucket[index].val)) { + result = false; + break; + } + } + } + +copy_direct_array_done: + zend_hash_index_del(&context->seen_arrays, array_key); + if (!result) { + return false; + } + + ZVAL_ARR(destination, (zend_array *) (void *) (context->buffer + array_offset)); + Z_TYPE_FLAGS_P(destination) = 0; + return true; + default: + return false; + } +} + +static bool zend_opcache_static_cache_shared_graph_copy_property_value( + zend_opcache_static_cache_shared_graph_copy_context *context, + const zval *source, + zend_opcache_static_cache_shared_graph_value *destination +) +{ + zend_opcache_static_cache_shared_graph_serialized_object *serialized_object; + zend_opcache_static_cache_shared_graph_object *graph_object; + zend_opcache_static_cache_shared_graph_property *graph_properties; + zend_opcache_static_cache_shared_graph_array *graph_array; + zend_opcache_static_cache_shared_graph_array_element *graph_elements, *graph_element; + zend_object *object; + zend_class_entry *ce, *safe_direct_base; + zend_string *property_name, *key; + zend_ulong array_key, h; + zval *property_value, *source_value, *element, array_value; + HashTable *properties; + uint32_t payload_offset, string_offset, object_offset, class_name_offset, properties_offset, property_index, property_count, array_offset, elements_offset, key_offset; + size_t encoded_len, serialized_size; + bool result; + unsigned char *encoded; + + memset(destination, 0, sizeof(*destination)); + + switch (Z_TYPE_P(source)) { + case IS_UNDEF: + destination->type = ZEND_OPCACHE_STATIC_CACHE_SHARED_GRAPH_VALUE_UNDEF; + + return true; + case IS_NULL: + destination->type = ZEND_OPCACHE_STATIC_CACHE_SHARED_GRAPH_VALUE_NULL; + + return true; + case IS_TRUE: + destination->type = ZEND_OPCACHE_STATIC_CACHE_SHARED_GRAPH_VALUE_TRUE; + + return true; + case IS_FALSE: + destination->type = ZEND_OPCACHE_STATIC_CACHE_SHARED_GRAPH_VALUE_FALSE; + + return true; + case IS_LONG: + destination->type = ZEND_OPCACHE_STATIC_CACHE_SHARED_GRAPH_VALUE_LONG; + destination->payload.long_value = Z_LVAL_P(source); + + return true; + case IS_DOUBLE: + destination->type = ZEND_OPCACHE_STATIC_CACHE_SHARED_GRAPH_VALUE_DOUBLE; + destination->payload.double_value = Z_DVAL_P(source); + + return true; + case IS_STRING: + if (!zend_opcache_static_cache_shared_graph_copy_string(context, Z_STR_P(source), &string_offset)) { + return false; + } + + destination->type = ZEND_OPCACHE_STATIC_CACHE_SHARED_GRAPH_VALUE_STRING; + destination->payload.offset = string_offset; + + return true; + case IS_ARRAY: { + result = true; + + if (Z_ARRVAL_P(source)->nNumOfElements == 0) { + destination->type = ZEND_OPCACHE_STATIC_CACHE_SHARED_GRAPH_VALUE_ARRAY; + destination->payload.offset = 0; + + return true; + } + + if (zend_opcache_static_cache_shared_graph_can_copy_direct_value(&context->seen_arrays, source)) { + if (!zend_opcache_static_cache_shared_graph_copy_direct_value(context, source, &array_value)) { + return false; + } + + destination->type = ZEND_OPCACHE_STATIC_CACHE_SHARED_GRAPH_VALUE_ARRAY; + destination->payload.offset = (uint32_t) ((unsigned char *) Z_ARRVAL(array_value) - context->buffer); + + return true; + } + + if (!zend_opcache_static_cache_shared_graph_mark_seen_array(&context->seen_arrays, Z_ARRVAL_P(source))) { + return false; + } + + array_key = (zend_ulong) (uintptr_t) Z_ARRVAL_P(source); + if (!zend_opcache_static_cache_shared_graph_copy_alloc(context, sizeof(*graph_array), &array_offset) || + !zend_opcache_static_cache_shared_graph_copy_alloc( + context, + (size_t) Z_ARRVAL_P(source)->nNumOfElements * sizeof(*graph_elements), + &elements_offset + ) + ) { + result = false; + + goto array_done; + } + + graph_array = (zend_opcache_static_cache_shared_graph_array *) (context->buffer + array_offset); + graph_array->count = Z_ARRVAL_P(source)->nNumOfElements; + graph_array->next_free = (uint32_t) Z_ARRVAL_P(source)->nNextFreeElement; + graph_array->elements_offset = elements_offset; + graph_elements = (zend_opcache_static_cache_shared_graph_array_element *) (context->buffer + elements_offset); + graph_element = graph_elements; + + ZEND_HASH_FOREACH_KEY_VAL(Z_ARRVAL_P(source), h, key, element) { + memset(graph_element, 0, sizeof(*graph_element)); + graph_element->h = h; + if (key != NULL) { + if (!zend_opcache_static_cache_shared_graph_copy_string(context, key, &key_offset)) { + result = false; + break; + } + + graph_element->key_offset = key_offset; + } + + if (!zend_opcache_static_cache_shared_graph_copy_property_value(context, element, &graph_element->value)) { + result = false; + break; + } + + ++graph_element; + } ZEND_HASH_FOREACH_END(); + + if (result) { + destination->type = ZEND_OPCACHE_STATIC_CACHE_SHARED_GRAPH_VALUE_DYNAMIC_ARRAY; + destination->payload.offset = array_offset; + } + + goto array_done; + } + case IS_OBJECT: + ce = Z_OBJCE_P(source); + safe_direct_base = zend_opcache_serializer_find_safe_direct_cache_base(ce); + if (safe_direct_base != NULL) { + if (zend_opcache_serializer_has_safe_direct_cache_overrides(ce, safe_direct_base)) { + return false; + } + + encoded = NULL; + encoded_len = 0; + if (!zend_opcache_serialize(&encoded, &encoded_len, source)) { + return false; + } + + serialized_size = sizeof(zend_opcache_static_cache_shared_graph_serialized_object) + encoded_len - 1; + if (!zend_opcache_static_cache_shared_graph_copy_alloc(context, serialized_size, &payload_offset)) { + efree(encoded); + + return false; + } + + serialized_object = (zend_opcache_static_cache_shared_graph_serialized_object *) (context->buffer + payload_offset); + serialized_object->data_len = (uint32_t) encoded_len; + memcpy(serialized_object->data, encoded, encoded_len); + efree(encoded); + + destination->type = ZEND_OPCACHE_STATIC_CACHE_SHARED_GRAPH_VALUE_SERIALIZED_OBJECT; + destination->payload.offset = payload_offset; + + return true; + } + + object = Z_OBJ_P(source); + if (!zend_opcache_static_cache_shared_graph_can_restore_direct(object->ce)) { + return false; + } + + if (!zend_opcache_static_cache_shared_graph_mark_seen(&context->seen_objects, object)) { + return false; + } + + if (!zend_opcache_static_cache_shared_graph_copy_alloc(context, sizeof(*graph_object), &object_offset)) { + return false; + } + + if (!zend_opcache_static_cache_shared_graph_copy_string(context, object->ce->name, &class_name_offset)) { + return false; + } + + properties = zend_get_properties_for((zval *) source, ZEND_PROP_PURPOSE_SERIALIZE); + property_count = properties != NULL ? properties->nNumOfElements : 0; + properties_offset = 0; + if (property_count != 0 && + !zend_opcache_static_cache_shared_graph_copy_alloc( + context, + ((size_t) property_count * sizeof(zend_opcache_static_cache_shared_graph_property)), + &properties_offset + ) + ) { + if (properties != NULL) { + zend_release_properties(properties); + } + return false; + } + + graph_object = (zend_opcache_static_cache_shared_graph_object *) (context->buffer + object_offset); + graph_object->class_name_offset = class_name_offset; + graph_object->property_count = property_count; + graph_object->properties_offset = properties_offset; + + if (property_count == 0) { + if (properties != NULL) { + zend_release_properties(properties); + } + + destination->type = ZEND_OPCACHE_STATIC_CACHE_SHARED_GRAPH_VALUE_OBJECT; + destination->payload.offset = object_offset; + + return true; + } + + graph_properties = (zend_opcache_static_cache_shared_graph_property *) (context->buffer + properties_offset); + property_index = 0; + ZEND_HASH_FOREACH_STR_KEY_VAL(properties, property_name, property_value) { + if (property_name == NULL || + !zend_opcache_static_cache_shared_graph_copy_string(context, property_name, &graph_properties[property_index].name_offset) + ) { + zend_release_properties(properties); + + return false; + } + + source_value = Z_TYPE_P(property_value) == IS_INDIRECT ? Z_INDIRECT_P(property_value) : property_value; + if (!zend_opcache_static_cache_shared_graph_copy_property_value( + context, + source_value, + &graph_properties[property_index].value + ) + ) { + zend_release_properties(properties); + + return false; + } + + ++property_index; + } ZEND_HASH_FOREACH_END(); + + zend_release_properties(properties); + destination->type = ZEND_OPCACHE_STATIC_CACHE_SHARED_GRAPH_VALUE_OBJECT; + destination->payload.offset = object_offset; + + return true; + default: + return false; + } + +array_done: + zend_hash_index_del(&context->seen_arrays, array_key); + + return result; +} + +static bool zend_opcache_static_cache_shared_graph_try_update_declared_object_property( + zend_object *object, + zend_string *property_name, + zend_property_info *property_info, + zval *property_value, + bool *failed +) +{ + zval *slot, tmp, indirect; + + *failed = false; + + if (property_info == NULL || + property_info == ZEND_WRONG_PROPERTY_INFO || + !zend_string_equals(property_info->name, property_name) || + (property_info->flags & (ZEND_ACC_STATIC|ZEND_ACC_READONLY|ZEND_ACC_PPP_SET_MASK|ZEND_ACC_VIRTUAL)) != 0 || + (property_info->flags & ZEND_ACC_PPP_MASK) != ZEND_ACC_PUBLIC || + property_info->offset == ZEND_VIRTUAL_PROPERTY_OFFSET + ) { + return false; + } + + slot = OBJ_PROP(object, property_info->offset); + ZVAL_COPY_DEREF(&tmp, property_value); + + if (ZEND_TYPE_IS_SET(property_info->type) && + !zend_verify_property_type(property_info, &tmp, true) + ) { + zval_ptr_dtor(&tmp); + + *failed = true; + + return false; + } + + zval_ptr_dtor(slot); + ZVAL_COPY_VALUE(slot, &tmp); + + if (object->properties != NULL) { + ZVAL_INDIRECT(&indirect, slot); + zend_hash_update(object->properties, property_name, &indirect); + } + + return true; +} + +static bool zend_opcache_static_cache_shared_graph_update_object_property( + zval *object_zv, + zend_string *property_name, + zval *property_value +) +{ + zend_object *object; + zend_property_info *property_info; + bool failed; + + object = Z_OBJ_P(object_zv); + if (ZSTR_LEN(property_name) != 0 && ZSTR_VAL(property_name)[0] != '\0') { + property_info = zend_get_property_info(object->ce, property_name, true); + if (zend_opcache_static_cache_shared_graph_try_update_declared_object_property( + object, + property_name, + property_info, + property_value, + &failed) + ) { + return true; + } + + if (failed) { + return false; + } + } + + return zend_opcache_serializer_update_object_property(object, property_name, property_value); +} + +static bool zend_opcache_static_cache_shared_graph_update_object_property_at( + zval *object_zv, + zend_string *property_name, + uint32_t property_index, + zval *property_value +) +{ + zend_object *object; + zend_property_info *property_info; + bool failed; + + object = Z_OBJ_P(object_zv); + if (ZSTR_LEN(property_name) != 0 && ZSTR_VAL(property_name)[0] != '\0' && + object->ce->type == ZEND_USER_CLASS && + object->ce->properties_info_table != NULL && + property_index < object->ce->default_properties_count + ) { + property_info = object->ce->properties_info_table[property_index]; + + if (zend_opcache_static_cache_shared_graph_try_update_declared_object_property( + object, + property_name, + property_info, + property_value, + &failed) + ) { + return true; + } + + if (failed) { + return false; + } + } + + return zend_opcache_static_cache_shared_graph_update_object_property(object_zv, property_name, property_value); +} + +static bool zend_opcache_static_cache_fetch_shared_graph_value( + const unsigned char *buffer, + const zend_opcache_static_cache_shared_graph_value *value, + zval *destination +) +{ + const zend_opcache_static_cache_shared_graph_object *graph_object; + const zend_opcache_static_cache_shared_graph_array *graph_array; + const zend_opcache_static_cache_shared_graph_array_element *graph_elements, *graph_element; + const zend_opcache_static_cache_shared_graph_property *properties, *property; + const zend_opcache_static_cache_shared_graph_serialized_object *serialized_object; + zend_class_entry *ce; + zend_string *class_name, *property_name; + zval property_value; + uint32_t index; + + switch (value->type) { + case ZEND_OPCACHE_STATIC_CACHE_SHARED_GRAPH_VALUE_UNDEF: + ZVAL_UNDEF(destination); + return true; + case ZEND_OPCACHE_STATIC_CACHE_SHARED_GRAPH_VALUE_NULL: + ZVAL_NULL(destination); + return true; + case ZEND_OPCACHE_STATIC_CACHE_SHARED_GRAPH_VALUE_TRUE: + ZVAL_TRUE(destination); + return true; + case ZEND_OPCACHE_STATIC_CACHE_SHARED_GRAPH_VALUE_FALSE: + ZVAL_FALSE(destination); + return true; + case ZEND_OPCACHE_STATIC_CACHE_SHARED_GRAPH_VALUE_LONG: + ZVAL_LONG(destination, value->payload.long_value); + return true; + case ZEND_OPCACHE_STATIC_CACHE_SHARED_GRAPH_VALUE_DOUBLE: + ZVAL_DOUBLE(destination, value->payload.double_value); + return true; + case ZEND_OPCACHE_STATIC_CACHE_SHARED_GRAPH_VALUE_STRING: + ZVAL_INTERNED_STR(destination, (zend_string *) (void *) (buffer + (uint32_t) value->payload.offset)); + return true; + case ZEND_OPCACHE_STATIC_CACHE_SHARED_GRAPH_VALUE_ARRAY: + if ((uint32_t) value->payload.offset == 0) { + ZVAL_EMPTY_ARRAY(destination); + } else { + ZVAL_ARR(destination, (zend_array *) (void *) (buffer + (uint32_t) value->payload.offset)); + Z_TYPE_FLAGS_P(destination) = 0; + } + + zend_opcache_static_cache_capture_decoded_value(destination); + + return true; + case ZEND_OPCACHE_STATIC_CACHE_SHARED_GRAPH_VALUE_DYNAMIC_ARRAY: + graph_array = (const zend_opcache_static_cache_shared_graph_array *) (buffer + (uint32_t) value->payload.offset); + array_init_size(destination, graph_array->count); + graph_elements = (const zend_opcache_static_cache_shared_graph_array_element *) (buffer + graph_array->elements_offset); + + for (index = 0; index < graph_array->count; index++) { + graph_element = &graph_elements[index]; + ZVAL_UNDEF(&property_value); + if (!zend_opcache_static_cache_fetch_shared_graph_value(buffer, &graph_element->value, &property_value)) { + zval_ptr_dtor(destination); + ZVAL_UNDEF(destination); + + return false; + } + + if (graph_element->key_offset != 0) { + property_name = (zend_string *) (void *) (buffer + graph_element->key_offset); + if (zend_hash_add_new(Z_ARRVAL_P(destination), property_name, &property_value) == NULL) { + zval_ptr_dtor(&property_value); + zval_ptr_dtor(destination); + ZVAL_UNDEF(destination); + + return false; + } + } else if (zend_hash_index_add_new(Z_ARRVAL_P(destination), graph_element->h, &property_value) == NULL) { + zval_ptr_dtor(&property_value); + zval_ptr_dtor(destination); + ZVAL_UNDEF(destination); + + return false; + } + } + + Z_ARRVAL_P(destination)->nNextFreeElement = graph_array->next_free; + zend_opcache_static_cache_capture_decoded_value(destination); + + return true; + case ZEND_OPCACHE_STATIC_CACHE_SHARED_GRAPH_VALUE_OBJECT: + graph_object = (const zend_opcache_static_cache_shared_graph_object *) (buffer + (uint32_t) value->payload.offset); + class_name = (zend_string *) (void *) (buffer + graph_object->class_name_offset); + ce = zend_lookup_class(class_name); + if (ce == NULL || object_init_ex(destination, ce) != SUCCESS) { + return false; + } + + if (graph_object->property_count == 0) { + zend_opcache_static_cache_capture_decoded_value(destination); + return true; + } + + properties = (const zend_opcache_static_cache_shared_graph_property *) (buffer + graph_object->properties_offset); + for (index = 0; index < graph_object->property_count; index++) { + property = &properties[index]; + if (property->value.type == ZEND_OPCACHE_STATIC_CACHE_SHARED_GRAPH_VALUE_UNDEF) { + continue; + } + + property_name = (zend_string *) (void *) (buffer + property->name_offset); + ZVAL_UNDEF(&property_value); + if (!zend_opcache_static_cache_fetch_shared_graph_value(buffer, &property->value, &property_value) || + !zend_opcache_static_cache_shared_graph_update_object_property_at(destination, property_name, index, &property_value) + ) { + zval_ptr_dtor(&property_value); + zval_ptr_dtor(destination); + ZVAL_UNDEF(destination); + + return false; + } + + zval_ptr_dtor(&property_value); + } + + zend_opcache_static_cache_capture_decoded_value(destination); + + return true; + case ZEND_OPCACHE_STATIC_CACHE_SHARED_GRAPH_VALUE_SERIALIZED_OBJECT: + serialized_object = (const zend_opcache_static_cache_shared_graph_serialized_object *) (buffer + (uint32_t) value->payload.offset); + + return zend_opcache_static_cache_capture_active + ? zend_opcache_unserialize_ex( + destination, + serialized_object->data, + serialized_object->data_len, + zend_opcache_static_cache_capture_decoded_value, + zend_opcache_static_cache_capture_decoded_reachable_value) + : zend_opcache_unserialize(destination, serialized_object->data, serialized_object->data_len) + ; + default: + return false; + } +} + +static zend_opcache_static_cache_shared_graph_header *zend_opcache_static_cache_shared_graph_payload_header(uint32_t payload_offset) +{ + const unsigned char *graph_buffer; + zend_opcache_static_cache_shared_graph_header *header; + size_t buffer_len; + + if (payload_offset == 0) { + return NULL; + } + + buffer_len = zend_opcache_static_cache_block_payload_capacity(payload_offset); + if (buffer_len == 0) { + return NULL; + } + + graph_buffer = zend_opcache_static_cache_shared_graph_locate_buffer( + (const unsigned char *) zend_opcache_static_cache_ptr(payload_offset), + buffer_len, + NULL + ); + if (graph_buffer == NULL) { + return NULL; + } + + header = (zend_opcache_static_cache_shared_graph_header *) graph_buffer; + if (header->magic != ZEND_OPCACHE_STATIC_CACHE_SHARED_GRAPH_MAGIC || + header->version != ZEND_OPCACHE_STATIC_CACHE_SHARED_GRAPH_VERSION + ) { + return NULL; + } + + return header; +} + +static void zend_opcache_static_cache_free_retired_shared_graphs(void) +{ + zend_opcache_static_cache_shared_graph_ref *ref; + zend_opcache_static_cache_context *previous_context; + uint32_t index; + + if (zend_opcache_static_cache_retired_shared_graph_count == 0) { + return; + } + + for (index = 0; index < zend_opcache_static_cache_retired_shared_graph_count; index++) { + ref = &zend_opcache_static_cache_retired_shared_graphs[index]; + + if (ref->context == NULL) { + continue; + } + + previous_context = zend_opcache_static_cache_activate_context(ref->context); + if (zend_opcache_static_cache_wlock()) { + if (zend_opcache_static_cache_header_is_initialized_locked()) { + zend_opcache_static_cache_free_locked(ref->payload_offset); + } + + zend_opcache_static_cache_unlock(); + } + + zend_opcache_static_cache_restore_context(previous_context); + } + + efree(zend_opcache_static_cache_retired_shared_graphs); + + zend_opcache_static_cache_retired_shared_graphs = NULL; + zend_opcache_static_cache_retired_shared_graph_count = 0; + zend_opcache_static_cache_retired_shared_graph_capacity = 0; +} + +bool zend_opcache_static_cache_calculate_shared_graph_size( + const zval *value, + size_t *buffer_len +) +{ + zend_opcache_static_cache_shared_graph_calc_context calc_context; + bool result; + + if (!buffer_len) { + return false; + } + + *buffer_len = 0; + if (Z_TYPE_P(value) == IS_OBJECT) { + if (!zend_opcache_static_cache_shared_graph_can_restore_direct(Z_OBJCE_P(value))) { + return false; + } + } else if (Z_TYPE_P(value) != IS_ARRAY) { + return false; + } + + zend_opcache_static_cache_shared_graph_calc_init(&calc_context); + + result = zend_opcache_static_cache_shared_graph_reserve_size(&calc_context.size, sizeof(zend_opcache_static_cache_shared_graph_header)); + if (result) { + result = zend_opcache_static_cache_shared_graph_calc_value(&calc_context, value); + } + if (result) { + if (calc_context.size > UINT32_MAX - (ZEND_MM_ALIGNMENT - 1)) { + result = false; + } else { + calc_context.size += ZEND_MM_ALIGNMENT - 1; + } + } + if (result) { + *buffer_len = calc_context.size; + } + + zend_opcache_static_cache_shared_graph_calc_destroy(&calc_context); + + return result; +} + +bool zend_opcache_static_cache_build_shared_graph_in_place( + const zval *value, + unsigned char *buffer, + size_t buffer_len, + size_t *graph_len +) +{ + zend_opcache_static_cache_shared_graph_copy_context copy_context; + zend_opcache_static_cache_shared_graph_header *header; + zend_opcache_static_cache_shared_graph_value root_value; + uint32_t header_offset, root_offset, root_type; + size_t padding; + bool result; + + if (buffer == NULL) { + return false; + } + + padding = zend_opcache_static_cache_shared_graph_alignment_padding(buffer); + if (padding > buffer_len || buffer_len - padding < sizeof(zend_opcache_static_cache_shared_graph_header)) { + return false; + } + if (padding != 0) { + memset(buffer, 0, padding); + } + + buffer += padding; + buffer_len -= padding; + + zend_opcache_static_cache_shared_graph_copy_init(©_context, buffer, buffer_len); + root_offset = 0; + root_type = 0; + result = zend_opcache_static_cache_shared_graph_copy_alloc(©_context, sizeof(*header), &header_offset) && header_offset == 0; + if (result) { + if (Z_TYPE_P(value) == IS_OBJECT) { + result = zend_opcache_static_cache_shared_graph_copy_property_value(©_context, value, &root_value) + && root_value.type == ZEND_OPCACHE_STATIC_CACHE_SHARED_GRAPH_VALUE_OBJECT + ; + } else if (Z_TYPE_P(value) == IS_ARRAY) { + result = zend_opcache_static_cache_shared_graph_copy_property_value(©_context, value, &root_value) && + (root_value.type == ZEND_OPCACHE_STATIC_CACHE_SHARED_GRAPH_VALUE_ARRAY || + root_value.type == ZEND_OPCACHE_STATIC_CACHE_SHARED_GRAPH_VALUE_DYNAMIC_ARRAY) + ; + } else { + result = false; + } + + if (result) { + root_type = root_value.type; + root_offset = (uint32_t) root_value.payload.offset; + } + } + + if (!result) { + zend_opcache_static_cache_shared_graph_copy_destroy(©_context); + + return false; + } + + header = (zend_opcache_static_cache_shared_graph_header *) buffer; + header->magic = ZEND_OPCACHE_STATIC_CACHE_SHARED_GRAPH_MAGIC; + header->version = ZEND_OPCACHE_STATIC_CACHE_SHARED_GRAPH_VERSION; + header->root_offset = root_offset; + header->root_type = root_type; + ZEND_ATOMIC_INT_INIT(&header->ref_state, 0); + if (graph_len != NULL) { + *graph_len = copy_context.position; + } + + zend_opcache_static_cache_shared_graph_copy_destroy(©_context); + + return true; +} + +bool zend_opcache_static_cache_shared_graph_copy_fits_buffer( + const unsigned char *source_buffer, + size_t source_buffer_len, + size_t source_graph_len, + const unsigned char *target_buffer, + size_t target_buffer_len +) +{ + size_t target_padding; + + if (source_buffer == NULL || target_buffer == NULL || source_graph_len == 0 || source_graph_len > source_buffer_len) { + return false; + } + + target_padding = zend_opcache_static_cache_shared_graph_alignment_padding(target_buffer); + return target_padding <= target_buffer_len && source_graph_len <= target_buffer_len - target_padding; +} + +bool zend_opcache_static_cache_fetch_shared_graph( + const unsigned char *buffer, + size_t buffer_len, + zval *destination +) +{ + const zend_opcache_static_cache_shared_graph_header *header; + const unsigned char *graph_buffer; + zend_opcache_static_cache_shared_graph_value root_value; + uint32_t root_type; + bool capture_active, result; + + graph_buffer = zend_opcache_static_cache_shared_graph_locate_buffer(buffer, buffer_len, &buffer_len); + if (graph_buffer == NULL) { + return false; + } + + buffer = graph_buffer; + + header = (const zend_opcache_static_cache_shared_graph_header *) buffer; + if (header->magic != ZEND_OPCACHE_STATIC_CACHE_SHARED_GRAPH_MAGIC || + header->version != ZEND_OPCACHE_STATIC_CACHE_SHARED_GRAPH_VERSION || + (header->root_offset != 0 && header->root_offset >= buffer_len) + ) { + return false; + } + + root_type = header->root_type != 0 ? header->root_type : ZEND_OPCACHE_STATIC_CACHE_SHARED_GRAPH_VALUE_OBJECT; + memset(&root_value, 0, sizeof(root_value)); + root_value.type = (uint8_t) root_type; + root_value.payload.offset = header->root_offset; + capture_active = zend_opcache_static_cache_capture_active; + if (capture_active) { + zend_opcache_static_cache_capture_active = false; + } + + switch (root_type) { + case ZEND_OPCACHE_STATIC_CACHE_SHARED_GRAPH_VALUE_OBJECT: + if (header->root_offset == 0) { + if (capture_active) { + zend_opcache_static_cache_capture_active = true; + } + + return false; + } + + result = zend_opcache_static_cache_fetch_shared_graph_value(buffer, &root_value, destination); + break; + case ZEND_OPCACHE_STATIC_CACHE_SHARED_GRAPH_VALUE_ARRAY: + case ZEND_OPCACHE_STATIC_CACHE_SHARED_GRAPH_VALUE_DYNAMIC_ARRAY: + result = zend_opcache_static_cache_fetch_shared_graph_value(buffer, &root_value, destination); + break; + default: + if (capture_active) { + zend_opcache_static_cache_capture_active = true; + } + + return false; + } + + if (capture_active) { + zend_opcache_static_cache_capture_active = true; + if (result) { + zend_opcache_static_cache_capture_decoded_reachable_value(destination); + } + } + + return result; +} + +bool zend_opcache_static_cache_shared_graph_can_overwrite_payload_locked(uint32_t payload_offset) +{ + zend_opcache_static_cache_shared_graph_header *header = zend_opcache_static_cache_shared_graph_payload_header(payload_offset); + + if (header == NULL) { + return false; + } + + return zend_atomic_int_load_ex(&header->ref_state) == 0; +} + +bool zend_opcache_static_cache_shared_graph_acquire_locked(uint32_t payload_offset) +{ + zend_opcache_static_cache_shared_graph_header *header = zend_opcache_static_cache_shared_graph_payload_header(payload_offset); + int state, refcount, expected; + + if (header == NULL) { + return false; + } + + for (;;) { + state = zend_atomic_int_load_ex(&header->ref_state); + refcount = state & ZEND_OPCACHE_STATIC_CACHE_SHARED_GRAPH_REFCOUNT_MASK; + expected = state; + + if ((state & ZEND_OPCACHE_STATIC_CACHE_SHARED_GRAPH_RETIRED) != 0 || + refcount == ZEND_OPCACHE_STATIC_CACHE_SHARED_GRAPH_REFCOUNT_MASK + ) { + return false; + } + + if (zend_atomic_int_compare_exchange_ex(&header->ref_state, &expected, state + 1)) { + return true; + } + } +} + +bool zend_opcache_static_cache_shared_graph_retire_payload_locked(uint32_t payload_offset) +{ + zend_opcache_static_cache_shared_graph_header *header = zend_opcache_static_cache_shared_graph_payload_header(payload_offset); + int state, refcount, expected; + + if (header == NULL) { + return true; + } + + for (;;) { + state = zend_atomic_int_load_ex(&header->ref_state); + refcount = state & ZEND_OPCACHE_STATIC_CACHE_SHARED_GRAPH_REFCOUNT_MASK; + expected = state; + + if (refcount == 0) { + return true; + } + + if ((state & ZEND_OPCACHE_STATIC_CACHE_SHARED_GRAPH_RETIRED) != 0) { + return false; + } + + if (zend_atomic_int_compare_exchange_ex(&header->ref_state, &expected, state | ZEND_OPCACHE_STATIC_CACHE_SHARED_GRAPH_RETIRED)) { + return false; + } + } +} + +bool zend_opcache_static_cache_shared_graph_release_ref_locked(uint32_t payload_offset) +{ + zend_opcache_static_cache_shared_graph_header *header = zend_opcache_static_cache_shared_graph_payload_header(payload_offset); + int state, refcount, expected, desired; + + if (header == NULL) { + return false; + } + + for (;;) { + state = zend_atomic_int_load_ex(&header->ref_state); + refcount = state & ZEND_OPCACHE_STATIC_CACHE_SHARED_GRAPH_REFCOUNT_MASK; + expected = state; + + if (refcount == 0) { + return false; + } + + desired = (state & ZEND_OPCACHE_STATIC_CACHE_SHARED_GRAPH_RETIRED) | (refcount - 1); + if (zend_atomic_int_compare_exchange_ex(&header->ref_state, &expected, desired)) { + return (desired & ZEND_OPCACHE_STATIC_CACHE_SHARED_GRAPH_RETIRED) != 0 && + (desired & ZEND_OPCACHE_STATIC_CACHE_SHARED_GRAPH_REFCOUNT_MASK) == 0 + ; + } + } +} + +bool zend_opcache_static_cache_has_request_shared_graph_ref(uint32_t payload_offset) +{ + zend_opcache_static_cache_context *context; + uint32_t index; + + context = zend_opcache_static_cache_active_context(); + for (index = 0; index < zend_opcache_static_cache_shared_graph_ref_count; index++) { + if (zend_opcache_static_cache_shared_graph_refs[index].context == context && + zend_opcache_static_cache_shared_graph_refs[index].payload_offset == payload_offset + ) { + return true; + } + } + + return false; +} + +void zend_opcache_static_cache_register_shared_graph_ref(uint32_t payload_offset) +{ + if (zend_opcache_static_cache_shared_graph_ref_count == zend_opcache_static_cache_shared_graph_ref_capacity) { + zend_opcache_static_cache_shared_graph_ref_capacity = zend_opcache_static_cache_shared_graph_ref_capacity == 0 + ? 8 + : zend_opcache_static_cache_shared_graph_ref_capacity * 2 + ; + + zend_opcache_static_cache_shared_graph_refs = erealloc( + zend_opcache_static_cache_shared_graph_refs, + sizeof(zend_opcache_static_cache_shared_graph_ref) * zend_opcache_static_cache_shared_graph_ref_capacity + ); + } + + zend_opcache_static_cache_shared_graph_refs[zend_opcache_static_cache_shared_graph_ref_count].context = zend_opcache_static_cache_active_context(); + zend_opcache_static_cache_shared_graph_refs[zend_opcache_static_cache_shared_graph_ref_count].payload_offset = payload_offset; + zend_opcache_static_cache_shared_graph_ref_count++; +} + +void zend_opcache_static_cache_defer_retired_shared_graph_free(uint32_t payload_offset) +{ + zend_opcache_static_cache_shared_graph_ref *ref; + + if (zend_opcache_static_cache_retired_shared_graph_count == zend_opcache_static_cache_retired_shared_graph_capacity) { + zend_opcache_static_cache_retired_shared_graph_capacity = zend_opcache_static_cache_retired_shared_graph_capacity == 0 + ? 4 + : zend_opcache_static_cache_retired_shared_graph_capacity * 2 + ; + + zend_opcache_static_cache_retired_shared_graphs = erealloc( + zend_opcache_static_cache_retired_shared_graphs, + sizeof(zend_opcache_static_cache_shared_graph_ref) * zend_opcache_static_cache_retired_shared_graph_capacity + ); + } + + ref = &zend_opcache_static_cache_retired_shared_graphs[zend_opcache_static_cache_retired_shared_graph_count++]; + ref->context = zend_opcache_static_cache_active_context(); + ref->payload_offset = payload_offset; +} + +void zend_opcache_static_cache_release_request_shared_graph_refs(void) +{ + zend_opcache_static_cache_shared_graph_ref *ref; + zend_opcache_static_cache_context *previous_context; + uint32_t index; + + if (zend_opcache_static_cache_shared_graph_ref_count == 0) { + zend_opcache_static_cache_free_retired_shared_graphs(); + + return; + } + + for (index = 0; index < zend_opcache_static_cache_shared_graph_ref_count; index++) { + ref = &zend_opcache_static_cache_shared_graph_refs[index]; + + if (ref->context == NULL || !zend_opcache_static_cache_context_runtime(ref->context)->available) { + continue; + } + + previous_context = zend_opcache_static_cache_activate_context(ref->context); + if (zend_opcache_static_cache_wlock()) { + if (zend_opcache_static_cache_header_is_initialized_locked() && + zend_opcache_static_cache_shared_graph_release_ref_locked(ref->payload_offset) + ) { + zend_opcache_static_cache_free_locked(ref->payload_offset); + } + + zend_opcache_static_cache_unlock(); + } + + zend_opcache_static_cache_restore_context(previous_context); + } + + efree(zend_opcache_static_cache_shared_graph_refs); + + zend_opcache_static_cache_shared_graph_refs = NULL; + zend_opcache_static_cache_shared_graph_ref_count = 0; + zend_opcache_static_cache_shared_graph_ref_capacity = 0; + + zend_opcache_static_cache_free_retired_shared_graphs(); +} diff --git a/ext/opcache/zend_static_cache_statics.c b/ext/opcache/zend_static_cache_statics.c new file mode 100644 index 000000000000..8366d580eb20 --- /dev/null +++ b/ext/opcache/zend_static_cache_statics.c @@ -0,0 +1,3717 @@ +/* + +----------------------------------------------------------------------+ + | Zend OPcache | + +----------------------------------------------------------------------+ + | Copyright © The PHP Group and Contributors. | + +----------------------------------------------------------------------+ + | This source file is subject to the Modified BSD License that is | + | bundled with this package in the file LICENSE, and is available | + | through the World Wide Web at . | + | | + | SPDX-License-Identifier: BSD-3-Clause | + +----------------------------------------------------------------------+ + | Author: Go Kudo | + +----------------------------------------------------------------------+ +*/ + +#include "zend_static_cache_internal.h" +#include "zend_opcache_serializer.h" + +typedef struct _zend_opcache_static_cache_prepare_memo_entry { + zval root_snapshot; + unsigned char *payload_buffer; + size_t payload_size; + size_t payload_used_size; + uint32_t value_len; + uint8_t root_type; + bool dirty; +} zend_opcache_static_cache_prepare_memo_entry; + +typedef struct _zend_opcache_static_cache_prepare_memo_dependency { + zend_opcache_static_cache_prepare_memo_entry *single_entry; + HashTable *entries; +} zend_opcache_static_cache_prepare_memo_dependency; + +static ZEND_EXT_TLS HashTable *zend_opcache_static_cache_prepare_memo_entries = NULL; +static ZEND_EXT_TLS HashTable *zend_opcache_static_cache_prepare_memo_array_roots = NULL; +static ZEND_EXT_TLS HashTable *zend_opcache_static_cache_prepare_memo_object_roots = NULL; +static ZEND_EXT_TLS HashTable *zend_opcache_static_cache_prepare_memo_arrays = NULL; +static ZEND_EXT_TLS HashTable *zend_opcache_static_cache_prepare_memo_objects = NULL; +static ZEND_EXT_TLS bool zend_opcache_static_cache_has_prepare_memo_arrays = false; +static ZEND_EXT_TLS bool zend_opcache_static_cache_has_prepare_memo_objects = false; + +static zend_always_inline HashTable *zend_opcache_static_cache_capture_ensure_table(HashTable **table_ptr) +{ + if (*table_ptr == NULL) { + *table_ptr = emalloc(sizeof(HashTable)); + zend_hash_init(*table_ptr, 8, NULL, NULL, 0); + } + + return *table_ptr; +} + +static zend_always_inline void zend_opcache_static_cache_capture_clear(void) +{ + zend_opcache_static_cache_capture_handle = NULL; + + if (zend_opcache_static_cache_capture_arrays != NULL) { + zend_hash_clean(zend_opcache_static_cache_capture_arrays); + } + + if (zend_opcache_static_cache_capture_objects != NULL) { + zend_hash_clean(zend_opcache_static_cache_capture_objects); + } + + zend_opcache_static_cache_capture_available = false; +} + +static void zend_opcache_static_cache_prepare_memo_entry_dtor(zval *zv) +{ + zend_opcache_static_cache_prepare_memo_entry *entry = Z_PTR_P(zv); + + if (entry == NULL) { + return; + } + + if (entry->payload_buffer != NULL) { + efree(entry->payload_buffer); + } + + zval_ptr_dtor(&entry->root_snapshot); + efree(entry); +} + +static void zend_opcache_static_cache_prepare_memo_dependency_dtor(zval *zv) +{ + zend_opcache_static_cache_prepare_memo_dependency *dependency = Z_PTR_P(zv); + + if (dependency == NULL) { + return; + } + + if (dependency->entries != NULL) { + zend_hash_destroy(dependency->entries); + efree(dependency->entries); + } + + efree(dependency); +} + +static zend_opcache_static_cache_kind zend_opcache_static_cache_kind_from_volatile_static_attribute( + zend_attribute *volatile_static, + zend_class_entry *scope, + zend_long *ttl) +{ + zend_opcache_static_cache_volatile_static_attribute_config config; + zend_string *error = NULL; + + if (ttl != NULL) { + *ttl = 0; + } + + if (volatile_static == NULL) { + return ZEND_OPCACHE_STATIC_CACHE_NONE; + } + + if (!zend_opcache_static_cache_volatile_static_attribute_config_from_attribute(volatile_static, scope, &config, &error)) { + zend_error(E_ERROR, "%s", ZSTR_VAL(error)); + + return ZEND_OPCACHE_STATIC_CACHE_NONE; + } + + if (ttl != NULL) { + *ttl = config.ttl; + } + + return config.strategy == ZEND_OPCACHE_STATIC_CACHE_STRATEGY_TRACKING + ? ZEND_OPCACHE_STATIC_CACHE_VOLATILE_STATIC_TRACKING + : ZEND_OPCACHE_STATIC_CACHE_VOLATILE_STATIC + ; +} + +static zend_opcache_static_cache_kind zend_opcache_static_cache_kind_from_attributes( + const HashTable *attributes, + zend_class_entry *scope, + zend_long *ttl) +{ + bool persistent_static; + zend_attribute *volatile_static; + + if (ttl != NULL) { + *ttl = 0; + } + + if (attributes == NULL) { + return ZEND_OPCACHE_STATIC_CACHE_NONE; + } + + persistent_static = zend_get_attribute_str(attributes, ZEND_STRL(ZEND_OPCACHE_STATIC_CACHE_PERSISTENT_ATTRIBUTE)) != NULL; + volatile_static = zend_get_attribute_str(attributes, ZEND_STRL(ZEND_OPCACHE_STATIC_CACHE_VOLATILE_STATIC_ATTRIBUTE)); + if (persistent_static && volatile_static != NULL) { + return ZEND_OPCACHE_STATIC_CACHE_CONFLICT; + } + + if (persistent_static) { + return ZEND_OPCACHE_STATIC_CACHE_PERSISTENT; + } + + if (volatile_static != NULL) { + return zend_opcache_static_cache_kind_from_volatile_static_attribute(volatile_static, scope, ttl); + } + + return ZEND_OPCACHE_STATIC_CACHE_NONE; +} + +static bool zend_opcache_static_cache_handle_attribute_conflict(zend_opcache_static_cache_kind kind) +{ + if (kind == ZEND_OPCACHE_STATIC_CACHE_CONFLICT) { + zend_error(E_ERROR, "OPcache\\PersistentStatic and OPcache\\VolatileStatic cannot be combined on the same target"); + + return false; + } + + return true; +} + +static zend_opcache_static_cache_kind zend_opcache_static_cache_class_blob_kind_and_ttl( + const zend_class_entry *ce, + zend_long *ttl) +{ + if (ce == NULL) { + if (ttl != NULL) { + *ttl = 0; + } + + return ZEND_OPCACHE_STATIC_CACHE_NONE; + } + + return zend_opcache_static_cache_kind_from_attributes(ce->attributes, (zend_class_entry *) ce, ttl); +} + +static zend_opcache_static_cache_kind zend_opcache_static_cache_class_blob_kind(const zend_class_entry *ce) +{ + return zend_opcache_static_cache_class_blob_kind_and_ttl(ce, NULL); +} + +static bool zend_opcache_static_cache_class_blob_enabled(const zend_class_entry *ce) +{ + zend_opcache_static_cache_kind kind = zend_opcache_static_cache_class_blob_kind(ce); + + return zend_opcache_static_cache_handle_attribute_conflict(kind) && + kind != ZEND_OPCACHE_STATIC_CACHE_NONE + ; +} + +static zend_opcache_static_cache_context *zend_opcache_static_cache_context_for_kind(zend_opcache_static_cache_kind kind) +{ + return kind == ZEND_OPCACHE_STATIC_CACHE_VOLATILE_STATIC || kind == ZEND_OPCACHE_STATIC_CACHE_VOLATILE_STATIC_TRACKING + ? &zend_opcache_static_cache_volatile_context_state + : &zend_opcache_static_cache_persistent_context_state + ; +} + +static const char *zend_opcache_static_cache_key_prefix_for_kind(zend_opcache_static_cache_kind kind) +{ + return kind == ZEND_OPCACHE_STATIC_CACHE_PERSISTENT ? "persistent_static" : "volatile_static"; +} + +static bool zend_opcache_static_cache_kind_tracks_reachable_arrays( + zend_opcache_static_cache_kind kind) +{ + return kind == ZEND_OPCACHE_STATIC_CACHE_VOLATILE_STATIC_TRACKING || kind == ZEND_OPCACHE_STATIC_CACHE_PERSISTENT; +} + +static bool zend_opcache_static_cache_kind_tracks_reachable_objects( + zend_opcache_static_cache_kind kind) +{ + return kind == ZEND_OPCACHE_STATIC_CACHE_VOLATILE_STATIC_TRACKING; +} + +static bool zend_opcache_static_cache_kind_tracks_reachable_mutations( + zend_opcache_static_cache_kind kind) +{ + return zend_opcache_static_cache_kind_tracks_reachable_arrays(kind) || + zend_opcache_static_cache_kind_tracks_reachable_objects(kind) + ; +} + +static bool zend_opcache_static_cache_kind_publishes_immediately( + zend_opcache_static_cache_kind kind) +{ + return kind == ZEND_OPCACHE_STATIC_CACHE_PERSISTENT || kind == ZEND_OPCACHE_STATIC_CACHE_VOLATILE_STATIC; +} + +static bool zend_opcache_static_cache_kind_defers_reachable_mutation_publish( + zend_opcache_static_cache_kind kind) +{ + return kind == ZEND_OPCACHE_STATIC_CACHE_VOLATILE_STATIC_TRACKING; +} + +static bool zend_opcache_static_cache_applies_to_static_property(zend_class_entry *ce, zend_property_info *property_info) +{ + zend_opcache_static_cache_kind kind; + + if (!(property_info->flags & ZEND_ACC_STATIC) || property_info->ce != ce) { + return false; + } + + if (zend_opcache_static_cache_class_blob_enabled(ce)) { + return false; + } + + kind = zend_opcache_static_cache_kind_from_attributes(property_info->attributes, ce, NULL); + + return zend_opcache_static_cache_handle_attribute_conflict(kind) && + kind != ZEND_OPCACHE_STATIC_CACHE_NONE + ; +} + +static zend_opcache_static_cache_kind zend_opcache_static_cache_static_property_kind_and_ttl( + zend_class_entry *ce, + zend_property_info *property_info, + zend_long *ttl) +{ + zend_opcache_static_cache_kind kind; + + if (ttl != NULL) { + *ttl = 0; + } + + if (!zend_opcache_static_cache_applies_to_static_property(ce, property_info)) { + return ZEND_OPCACHE_STATIC_CACHE_NONE; + } + + kind = zend_opcache_static_cache_kind_from_attributes(property_info->attributes, ce, ttl); + + return kind == ZEND_OPCACHE_STATIC_CACHE_CONFLICT + ? ZEND_OPCACHE_STATIC_CACHE_NONE + : kind + ; +} + +static zend_opcache_static_cache_kind zend_opcache_static_cache_static_property_kind(zend_class_entry *ce, zend_property_info *property_info) +{ + return zend_opcache_static_cache_static_property_kind_and_ttl(ce, property_info, NULL); +} + +static bool zend_opcache_static_cache_class_blob_applies_to_property(zend_class_entry *ce, zend_property_info *property_info) +{ + return (property_info->flags & ZEND_ACC_STATIC) != 0 && + property_info->ce == ce && + zend_opcache_static_cache_class_blob_enabled(ce) + ; +} + +static bool zend_opcache_static_cache_applies_to_function_static(const zend_op_array *op_array) +{ + zend_opcache_static_cache_kind kind; + + if (op_array->static_variables == NULL || op_array->function_name == NULL || op_array->scope == NULL) { + return false; + } + + if (op_array->scope != NULL && zend_opcache_static_cache_class_blob_enabled(op_array->scope)) { + return false; + } + + kind = zend_opcache_static_cache_kind_from_attributes(op_array->attributes, op_array->scope, NULL); + + return zend_opcache_static_cache_handle_attribute_conflict(kind) && + kind != ZEND_OPCACHE_STATIC_CACHE_NONE + ; +} + +static zend_opcache_static_cache_kind zend_opcache_static_cache_op_array_kind_and_ttl( + const zend_op_array *op_array, + zend_long *ttl) +{ + zend_opcache_static_cache_kind kind; + + if (ttl != NULL) { + *ttl = 0; + } + + if (!zend_opcache_static_cache_applies_to_function_static(op_array)) { + return ZEND_OPCACHE_STATIC_CACHE_NONE; + } + + kind = zend_opcache_static_cache_kind_from_attributes(op_array->attributes, op_array->scope, ttl); + + return kind == ZEND_OPCACHE_STATIC_CACHE_CONFLICT + ? ZEND_OPCACHE_STATIC_CACHE_NONE + : kind + ; +} + +static zend_opcache_static_cache_kind zend_opcache_static_cache_op_array_kind(const zend_op_array *op_array) +{ + return zend_opcache_static_cache_op_array_kind_and_ttl(op_array, NULL); +} + +static bool zend_opcache_static_cache_class_blob_applies_to_function_static(const zend_op_array *op_array) +{ + return op_array->static_variables != NULL && + op_array->function_name != NULL && + op_array->scope != NULL && + zend_opcache_static_cache_class_blob_enabled(op_array->scope) + ; +} + +static void zend_opcache_static_cache_track_attribute_class(zend_class_entry *ce) +{ + if (!zend_opcache_static_cache_attribute_classes_initialized || ce->name == NULL) { + return; + } + + zend_hash_index_update_ptr(&zend_opcache_static_cache_attribute_classes, (zend_ulong)(uintptr_t) ce, ce); +} + +static zend_string *zend_opcache_static_cache_static_property_key(zend_class_entry *ce, zend_string *property_name, zend_opcache_static_cache_kind kind) +{ + if (ce->name == NULL) { + return NULL; + } + + return zend_strpprintf(0, "%s:%s::$%s", zend_opcache_static_cache_key_prefix_for_kind(kind), ZSTR_VAL(ce->name), ZSTR_VAL(property_name)); +} + +static zend_string *zend_opcache_static_cache_class_blob_key(const zend_class_entry *ce, zend_opcache_static_cache_kind kind) +{ + if (ce == NULL || ce->name == NULL) { + return NULL; + } + + return zend_strpprintf(0, "%s_class:%s", zend_opcache_static_cache_key_prefix_for_kind(kind), ZSTR_VAL(ce->name)); +} + +static zend_string *zend_opcache_static_cache_function_variable_key(const zend_op_array *op_array, zend_string *var_name, zend_opcache_static_cache_kind kind) +{ + if (op_array->function_name == NULL) { + return NULL; + } + + if (op_array->scope != NULL && op_array->scope->name != NULL) { + return zend_strpprintf( + 0, + "%s:%s::%s()::$%s", + zend_opcache_static_cache_key_prefix_for_kind(kind), + ZSTR_VAL(op_array->scope->name), + ZSTR_VAL(op_array->function_name), + ZSTR_VAL(var_name) + ); + } + + if (op_array->filename == NULL) { + return NULL; + } + + return zend_strpprintf( + 0, + "%s:%s:%u:%s()::$%s", + zend_opcache_static_cache_key_prefix_for_kind(kind), + ZSTR_VAL(op_array->filename), + op_array->line_start, + ZSTR_VAL(op_array->function_name), + ZSTR_VAL(var_name) + ); +} + +static zend_string *zend_opcache_static_cache_class_blob_method_key(const zend_op_array *op_array) +{ + if (op_array == NULL || op_array->scope == NULL || op_array->scope->name == NULL || op_array->function_name == NULL) { + return NULL; + } + + return zend_strpprintf( + 0, + "%s::%s()", + ZSTR_VAL(op_array->scope->name), + ZSTR_VAL(op_array->function_name) + ); +} + +static zend_opcache_static_cache_class_blob_handle *zend_opcache_static_cache_find_class_blob_handle(zend_class_entry *ce) +{ + if (!zend_opcache_static_cache_class_blob_handles_initialized || ce == NULL) { + return NULL; + } + + return zend_hash_index_find_ptr(&zend_opcache_static_cache_class_blob_handles, (zend_ulong) (uintptr_t) ce); +} + +static zend_reference *zend_opcache_static_cache_create_reference(zval *value) +{ + zend_reference *ref = emalloc(sizeof(zend_reference)); + + GC_SET_REFCOUNT(ref, 1); + GC_TYPE_INFO(ref) = GC_REFERENCE; + ref->sources.ptr = NULL; + ZVAL_COPY_VALUE(&ref->val, value); + + return ref; +} + +static zend_reference *zend_opcache_static_cache_ensure_member_reference(zval *member) +{ + zval copy; + + if (Z_ISREF_P(member)) { + return Z_REF_P(member); + } + + ZVAL_COPY_VALUE(©, member); + ZVAL_REF(member, zend_opcache_static_cache_create_reference(©)); + + return Z_REF_P(member); +} + +static void zend_opcache_static_cache_bind_slot_to_member(zval *slot, zval *member) +{ + zend_reference *ref; + + if (slot == NULL || member == NULL) { + return; + } + + ref = zend_opcache_static_cache_ensure_member_reference(member); + ZVAL_DEINDIRECT(slot); + if (Z_ISREF_P(slot) && Z_REF_P(slot) == ref) { + return; + } + + zval_ptr_dtor(slot); + GC_ADDREF(ref); + ZVAL_REF(slot, ref); +} + +static bool zend_opcache_static_cache_tracks_reachable_mutations( + zend_opcache_static_cache_static_slot_handle *handle) +{ + return handle != NULL && zend_opcache_static_cache_kind_tracks_reachable_mutations(handle->kind); +} + +static bool zend_opcache_static_cache_tracks_reachable_arrays( + zend_opcache_static_cache_static_slot_handle *handle) +{ + return handle != NULL && zend_opcache_static_cache_kind_tracks_reachable_arrays(handle->kind); +} + +static bool zend_opcache_static_cache_tracks_reachable_objects( + zend_opcache_static_cache_static_slot_handle *handle) +{ + return handle != NULL && zend_opcache_static_cache_kind_tracks_reachable_objects(handle->kind); +} + +static void zend_opcache_static_cache_reset_pending_mutation(void) +{ + memset(&zend_opcache_static_cache_pending_mutation_state, 0, sizeof(zend_opcache_static_cache_pending_mutation_state)); +} + +static bool zend_opcache_static_cache_has_mutable_immediate_root( + zend_opcache_static_cache_static_slot_handle *handle) +{ + return handle != NULL && + zend_opcache_static_cache_kind_publishes_immediately(handle->kind) && + (handle->original_root_type == IS_ARRAY || handle->original_root_type == IS_OBJECT) + ; +} + +static void zend_opcache_static_cache_tracked_dependency_dtor(zval *zv) +{ + zend_opcache_static_cache_tracked_dependency *dependency = Z_PTR_P(zv); + + if (dependency == NULL) { + return; + } + + if (dependency->handles != NULL) { + zend_hash_destroy(dependency->handles); + efree(dependency->handles); + } + + efree(dependency); +} + +static void zend_opcache_static_cache_tracked_dependency_add_handle( + zend_opcache_static_cache_tracked_dependency *dependency, + zend_opcache_static_cache_static_slot_handle *handle) +{ + if (dependency == NULL || handle == NULL) { + return; + } + + if (dependency->handles != NULL) { + zend_hash_index_update_ptr(dependency->handles, (zend_ulong) (uintptr_t) handle, handle); + + return; + } + + if (dependency->single_handle == NULL || dependency->single_handle == handle) { + dependency->single_handle = handle; + + return; + } + + dependency->handles = emalloc(sizeof(HashTable)); + zend_hash_init(dependency->handles, 2, NULL, NULL, 0); + zend_hash_index_update_ptr(dependency->handles, (zend_ulong) (uintptr_t) dependency->single_handle, dependency->single_handle); + zend_hash_index_update_ptr(dependency->handles, (zend_ulong) (uintptr_t) handle, handle); + dependency->single_handle = NULL; +} + +static void zend_opcache_static_cache_tracked_dependency_remove_handle( + zend_opcache_static_cache_tracked_dependency *dependency, + zend_opcache_static_cache_static_slot_handle *handle) +{ + if (dependency == NULL || handle == NULL) { + return; + } + + if (dependency->handles != NULL) { + zend_hash_index_del(dependency->handles, (zend_ulong) (uintptr_t) handle); + + return; + } + + if (dependency->single_handle == handle) { + dependency->single_handle = NULL; + } +} + +static void zend_opcache_static_cache_untrack_dependency_handles( + HashTable *dependencies, + zend_opcache_static_cache_static_slot_handle *handle) +{ + zend_opcache_static_cache_tracked_dependency *dependency; + + if (dependencies == NULL || handle == NULL) { + return; + } + + ZEND_HASH_FOREACH_PTR(dependencies, dependency) { + zend_opcache_static_cache_tracked_dependency_remove_handle(dependency, handle); + } ZEND_HASH_FOREACH_END(); +} + +static void zend_opcache_static_cache_untrack_dependencies( + zend_opcache_static_cache_static_slot_handle *handle) +{ + if (handle == NULL) { + return; + } + + zend_opcache_static_cache_untrack_dependency_handles(zend_opcache_static_cache_tracked_arrays, handle); + zend_opcache_static_cache_untrack_dependency_handles(zend_opcache_static_cache_tracked_objects, handle); + zend_opcache_static_cache_last_array_ht = NULL; + zend_opcache_static_cache_last_array_dependency = NULL; + zend_opcache_static_cache_last_object_obj = NULL; + zend_opcache_static_cache_last_object_dependency = NULL; +} + +static zend_opcache_static_cache_tracked_dependency *zend_opcache_static_cache_track_dependency( + HashTable *dependencies, + zend_ulong key, + zend_opcache_static_cache_static_slot_handle *handle) +{ + zend_opcache_static_cache_tracked_dependency *dependency; + + if (dependencies == NULL || handle == NULL) { + return NULL; + } + + dependency = zend_hash_index_find_ptr(dependencies, key); + if (dependency == NULL) { + dependency = emalloc(sizeof(*dependency)); + dependency->single_handle = NULL; + dependency->handles = NULL; + zend_hash_index_update_ptr(dependencies, key, dependency); + } + + zend_opcache_static_cache_tracked_dependency_add_handle(dependency, handle); + + return dependency; +} + +static void zend_opcache_static_cache_prepare_memo_dependency_add_entry( + zend_opcache_static_cache_prepare_memo_dependency *dependency, + zend_opcache_static_cache_prepare_memo_entry *entry) +{ + if (dependency == NULL || entry == NULL) { + return; + } + + if (dependency->entries != NULL) { + zend_hash_index_update_ptr(dependency->entries, (zend_ulong) (uintptr_t) entry, entry); + + return; + } + + if (dependency->single_entry == NULL || dependency->single_entry == entry) { + dependency->single_entry = entry; + + return; + } + + dependency->entries = emalloc(sizeof(HashTable)); + zend_hash_init(dependency->entries, 2, NULL, NULL, 0); + zend_hash_index_update_ptr(dependency->entries, (zend_ulong) (uintptr_t) dependency->single_entry, dependency->single_entry); + zend_hash_index_update_ptr(dependency->entries, (zend_ulong) (uintptr_t) entry, entry); + dependency->single_entry = NULL; +} + +static zend_opcache_static_cache_prepare_memo_dependency *zend_opcache_static_cache_prepare_memo_track_dependency( + HashTable *dependencies, + zend_ulong key, + zend_opcache_static_cache_prepare_memo_entry *entry) +{ + zend_opcache_static_cache_prepare_memo_dependency *dependency; + + if (dependencies == NULL || entry == NULL) { + return NULL; + } + + dependency = zend_hash_index_find_ptr(dependencies, key); + if (dependency == NULL) { + dependency = emalloc(sizeof(*dependency)); + dependency->single_entry = NULL; + dependency->entries = NULL; + zend_hash_index_update_ptr(dependencies, key, dependency); + } + + zend_opcache_static_cache_prepare_memo_dependency_add_entry(dependency, entry); + + return dependency; +} + +static void zend_opcache_static_cache_prepare_memo_mark_dependency_dirty( + zend_opcache_static_cache_prepare_memo_dependency *dependency) +{ + zend_opcache_static_cache_prepare_memo_entry *entry; + + if (dependency == NULL) { + return; + } + + if (dependency->entries != NULL) { + ZEND_HASH_FOREACH_PTR(dependency->entries, entry) { + if (entry != NULL) { + entry->dirty = true; + } + } ZEND_HASH_FOREACH_END(); + + return; + } + + if (dependency->single_entry != NULL) { + dependency->single_entry->dirty = true; + } +} + +static HashTable *zend_opcache_static_cache_prepare_memo_root_table_for_value(zval *value) +{ + if (value == NULL) { + return NULL; + } + + ZVAL_DEREF(value); + if (Z_TYPE_P(value) == IS_ARRAY) { + return zend_opcache_static_cache_prepare_memo_array_roots; + } + + if (Z_TYPE_P(value) == IS_OBJECT) { + return zend_opcache_static_cache_prepare_memo_object_roots; + } + + return NULL; +} + +static zend_ulong zend_opcache_static_cache_prepare_memo_root_key(zval *value) +{ + ZVAL_DEREF(value); + + return Z_TYPE_P(value) == IS_ARRAY + ? (zend_ulong) (uintptr_t) Z_ARRVAL_P(value) + : (zend_ulong) (uintptr_t) Z_OBJ_P(value) + ; +} + +static zend_opcache_static_cache_prepare_memo_entry *zend_opcache_static_cache_prepare_memo_find_root_entry(zval *value) +{ + zend_opcache_static_cache_prepare_memo_entry *entry; + zend_ulong key; + HashTable *roots; + + roots = zend_opcache_static_cache_prepare_memo_root_table_for_value(value); + if (roots == NULL) { + return NULL; + } + + key = zend_opcache_static_cache_prepare_memo_root_key(value); + entry = zend_hash_index_find_ptr(roots, key); + if (entry != NULL && entry->dirty) { + zend_hash_index_del(roots, key); + return NULL; + } + + return entry; +} + +static zend_opcache_static_cache_tracked_dependency *zend_opcache_static_cache_find_tracked_array(HashTable *ht) +{ + zend_opcache_static_cache_tracked_dependency *dependency; + + if (ht == NULL || zend_opcache_static_cache_tracked_arrays == NULL) { + return NULL; + } + + /* Single-slot fast path: same pointer as the previous lookup. The cache + * stores both positive and negative hits and is invalidated whenever the + * tracked-pointer map changes. */ + if (ht == zend_opcache_static_cache_last_array_ht) { + return zend_opcache_static_cache_last_array_dependency; + } + + dependency = zend_hash_index_find_ptr(zend_opcache_static_cache_tracked_arrays, (zend_ulong) (uintptr_t) ht); + zend_opcache_static_cache_last_array_ht = ht; + zend_opcache_static_cache_last_array_dependency = dependency; + + return dependency; +} + +static zend_opcache_static_cache_tracked_dependency *zend_opcache_static_cache_find_tracked_object(zend_object *obj) +{ + zend_opcache_static_cache_tracked_dependency *dependency; + + if (obj == NULL || zend_opcache_static_cache_tracked_objects == NULL) { + return NULL; + } + + /* Single-slot fast path: same pointer as the previous lookup. The cache + * stores both positive and negative hits and is invalidated whenever the + * tracked-pointer map changes. */ + if (obj == zend_opcache_static_cache_last_object_obj) { + return zend_opcache_static_cache_last_object_dependency; + } + + dependency = zend_hash_index_find_ptr(zend_opcache_static_cache_tracked_objects, (zend_ulong) (uintptr_t) obj); + zend_opcache_static_cache_last_object_obj = obj; + zend_opcache_static_cache_last_object_dependency = dependency; + + return dependency; +} + +static bool zend_opcache_static_cache_slot_references(zval *slot, zend_reference *ref) +{ + if (slot == NULL || ref == NULL) { + return false; + } + + ZVAL_DEINDIRECT(slot); + + return Z_ISREF_P(slot) && Z_REF_P(slot) == ref; +} + +static void zend_opcache_static_cache_track_reference( + zval *slot, + zend_opcache_static_cache_static_slot_handle *handle) +{ + zend_reference *ref; + + if (zend_opcache_static_cache_tracked_references == NULL || slot == NULL || handle == NULL) { + return; + } + + ZVAL_DEINDIRECT(slot); + if (!Z_ISREF_P(slot)) { + return; + } + + ref = Z_REF_P(slot); + zend_hash_index_update_ptr(zend_opcache_static_cache_tracked_references, (zend_ulong) (uintptr_t) ref, handle); +} + +static zend_opcache_static_cache_static_slot_handle *zend_opcache_static_cache_find_tracked_reference(zend_reference *ref) +{ + zend_opcache_static_cache_static_slot_handle *handle; + zend_ulong key; + + if (ref == NULL || zend_opcache_static_cache_tracked_roots == NULL) { + return NULL; + } + + key = (zend_ulong) (uintptr_t) ref; + if (zend_opcache_static_cache_tracked_references != NULL) { + handle = zend_hash_index_find_ptr(zend_opcache_static_cache_tracked_references, key); + if (handle != NULL) { + if (zend_opcache_static_cache_slot_references(handle->slot, ref)) { + return handle; + } + + zend_hash_index_del(zend_opcache_static_cache_tracked_references, key); + } + } + + ZEND_HASH_FOREACH_PTR(zend_opcache_static_cache_tracked_roots, handle) { + if (handle != NULL && zend_opcache_static_cache_slot_references(handle->slot, ref)) { + if (zend_opcache_static_cache_tracked_references != NULL) { + zend_hash_index_update_ptr(zend_opcache_static_cache_tracked_references, key, handle); + } + + return handle; + } + } ZEND_HASH_FOREACH_END(); + + return NULL; +} + +static void zend_opcache_static_cache_function_static_entry_dtor(zval *zv) +{ + zend_opcache_static_cache_function_static_entry *entry = Z_PTR_P(zv); + + if (entry == NULL) { + return; + } + + zend_string_release(entry->cache_key); + + if (entry->method_key != NULL) { + zend_string_release(entry->method_key); + } + + if (entry->var_name != NULL) { + zend_string_release(entry->var_name); + } + + efree(entry); +} + +static void zend_opcache_static_cache_class_blob_handle_dtor(zval *zv) +{ + zend_opcache_static_cache_class_blob_handle *handle = Z_PTR_P(zv); + + if (handle == NULL) { + return; + } + + zend_string_release(handle->cache_key); + if (Z_TYPE(handle->root_state) != IS_UNDEF) { + zval_ptr_dtor(&handle->root_state); + } + + efree(handle); +} + +static zend_opcache_static_cache_class_blob_handle *zend_opcache_static_cache_ensure_class_blob_handle(zend_class_entry *ce) +{ + zend_opcache_static_cache_class_blob_handle *handle; + zend_opcache_static_cache_kind kind; + zend_string *cache_key; + zend_long ttl = 0; + + if (!zend_opcache_static_cache_class_blob_handles_initialized || ce == NULL) { + return NULL; + } + + handle = zend_opcache_static_cache_find_class_blob_handle(ce); + if (handle != NULL) { + return handle; + } + + kind = zend_opcache_static_cache_class_blob_kind_and_ttl(ce, &ttl); + if (!zend_opcache_static_cache_handle_attribute_conflict(kind) || + kind == ZEND_OPCACHE_STATIC_CACHE_NONE + ) { + return NULL; + } + + cache_key = zend_opcache_static_cache_class_blob_key(ce, kind); + if (cache_key == NULL) { + return NULL; + } + + handle = ecalloc(1, sizeof(*handle)); + handle->ce = ce; + handle->cache_key = cache_key; + handle->context = zend_opcache_static_cache_context_for_kind(kind); + handle->kind = kind; + handle->ttl = ttl; + + ZVAL_UNDEF(&handle->root_state); + zend_hash_index_update_ptr(&zend_opcache_static_cache_class_blob_handles, (zend_ulong) (uintptr_t) ce, handle); + + return handle; +} + +static void zend_opcache_static_cache_track_function_static_slot( + zval *slot, + zend_string *cache_key, + zend_opcache_static_cache_kind kind, + zend_long ttl) +{ + zend_opcache_static_cache_function_static_entry *entry; + zend_ulong key; + + if (!zend_opcache_static_cache_function_statics_initialized || slot == NULL || cache_key == NULL) { + return; + } + + key = (zend_ulong) (uintptr_t) slot; + if (zend_hash_index_exists(&zend_opcache_static_cache_function_statics, key)) { + return; + } + + entry = emalloc(sizeof(*entry)); + entry->slot = slot; + entry->cache_key = zend_string_copy(cache_key); + entry->context = zend_opcache_static_cache_context_for_kind(kind); + entry->kind = kind; + entry->ttl = ttl; + entry->class_blob_handle = NULL; + entry->method_key = NULL; + entry->var_name = NULL; + + zend_hash_index_update_ptr(&zend_opcache_static_cache_function_statics, key, entry); +} + +static void zend_opcache_static_cache_track_class_blob_function_static_slot( + zval *slot, + zend_opcache_static_cache_class_blob_handle *class_blob_handle, + zend_string *method_key, + zend_string *var_name) +{ + zend_opcache_static_cache_function_static_entry *entry; + zend_ulong key; + + if (!zend_opcache_static_cache_function_statics_initialized || + slot == NULL || + class_blob_handle == NULL || + method_key == NULL || + var_name == NULL + ) { + return; + } + + key = (zend_ulong) (uintptr_t) slot; + if (zend_hash_index_exists(&zend_opcache_static_cache_function_statics, key)) { + return; + } + + entry = emalloc(sizeof(*entry)); + entry->slot = slot; + entry->cache_key = zend_string_copy(class_blob_handle->cache_key); + entry->context = class_blob_handle->context; + entry->kind = class_blob_handle->kind; + entry->ttl = class_blob_handle->ttl; + entry->class_blob_handle = class_blob_handle; + entry->method_key = zend_string_copy(method_key); + entry->var_name = zend_string_copy(var_name); + + zend_hash_index_update_ptr(&zend_opcache_static_cache_function_statics, key, entry); +} + +static void zend_opcache_static_cache_class_blob_ensure_root_arrays( + zend_opcache_static_cache_class_blob_handle *handle) +{ + zval root_props, root_methods; + HashTable *root_ht; + + if (handle == NULL) { + return; + } + + if (Z_TYPE(handle->root_state) != IS_ARRAY) { + if (Z_TYPE(handle->root_state) != IS_UNDEF) { + zval_ptr_dtor(&handle->root_state); + } + + array_init(&handle->root_state); + } + + root_ht = Z_ARRVAL(handle->root_state); + if (zend_hash_str_find(root_ht, ZEND_STRL("properties")) == NULL) { + array_init(&root_props); + zend_hash_str_update(root_ht, ZEND_STRL("properties"), &root_props); + } + + if (zend_hash_str_find(root_ht, ZEND_STRL("methods")) == NULL) { + array_init(&root_methods); + zend_hash_str_update(root_ht, ZEND_STRL("methods"), &root_methods); + } +} + +static zval *zend_opcache_static_cache_class_blob_ensure_section( + zend_opcache_static_cache_class_blob_handle *handle, + const char *name, + size_t name_len) +{ + zval *section, new_section; + + zend_opcache_static_cache_class_blob_ensure_root_arrays(handle); + section = zend_hash_str_find(Z_ARRVAL(handle->root_state), name, name_len); + if (section != NULL && Z_TYPE_P(section) == IS_ARRAY) { + return section; + } + + array_init(&new_section); + + return zend_hash_str_update(Z_ARRVAL(handle->root_state), name, name_len, &new_section); +} + +static HashTable *zend_opcache_static_cache_class_blob_properties_ht( + zend_opcache_static_cache_class_blob_handle *handle) +{ + zval *properties; + + zend_opcache_static_cache_class_blob_ensure_root_arrays(handle); + properties = zend_hash_str_find(Z_ARRVAL(handle->root_state), ZEND_STRL("properties")); + + return (properties != NULL && Z_TYPE_P(properties) == IS_ARRAY) + ? Z_ARRVAL_P(properties) + : NULL + ; +} + +static HashTable *zend_opcache_static_cache_class_blob_methods_ht( + zend_opcache_static_cache_class_blob_handle *handle +) +{ + zval *methods; + + zend_opcache_static_cache_class_blob_ensure_root_arrays(handle); + methods = zend_hash_str_find(Z_ARRVAL(handle->root_state), ZEND_STRL("methods")); + + return (methods != NULL && Z_TYPE_P(methods) == IS_ARRAY) + ? Z_ARRVAL_P(methods) + : NULL + ; +} + +static bool zend_opcache_static_cache_detach_class_blob_root_state( + zend_opcache_static_cache_class_blob_handle *handle +) +{ + zend_string *method_key; + zval new_root, properties_copy, methods_copy, method_copy, *properties, *methods, *method_state; + + if (handle == NULL || Z_TYPE(handle->root_state) != IS_ARRAY) { + return false; + } + + array_init(&new_root); + properties = zend_hash_str_find(Z_ARRVAL(handle->root_state), ZEND_STRL("properties")); + if (properties != NULL && Z_TYPE_P(properties) == IS_ARRAY) { + ZVAL_ARR(&properties_copy, zend_array_dup(Z_ARRVAL_P(properties))); + } else { + array_init(&properties_copy); + } + + zend_hash_str_update(Z_ARRVAL(new_root), ZEND_STRL("properties"), &properties_copy); + + methods = zend_hash_str_find(Z_ARRVAL(handle->root_state), ZEND_STRL("methods")); + array_init_size(&methods_copy, + (methods != NULL && Z_TYPE_P(methods) == IS_ARRAY) + ? zend_hash_num_elements(Z_ARRVAL_P(methods)) + : 0 + ); + if (methods != NULL && Z_TYPE_P(methods) == IS_ARRAY) { + ZEND_HASH_FOREACH_STR_KEY_VAL(Z_ARRVAL_P(methods), method_key, method_state) { + if (method_key == NULL) { + continue; + } + + if (Z_TYPE_P(method_state) == IS_ARRAY) { + ZVAL_ARR(&method_copy, zend_array_dup(Z_ARRVAL_P(method_state))); + } else { + ZVAL_COPY(&method_copy, method_state); + } + + zend_hash_update(Z_ARRVAL(methods_copy), method_key, &method_copy); + } ZEND_HASH_FOREACH_END(); + } + + zend_hash_str_update(Z_ARRVAL(new_root), ZEND_STRL("methods"), &methods_copy); + + zval_ptr_dtor(&handle->root_state); + ZVAL_COPY_VALUE(&handle->root_state, &new_root); + + return true; +} + +static bool zend_opcache_static_cache_build_default_class_blob_state( + zend_opcache_static_cache_class_blob_handle *handle) +{ + zend_string *property_name; + zend_property_info *property_info; + zval copy, *source; + HashTable *properties_ht; + + if (handle == NULL) { + return false; + } + + if (Z_TYPE(handle->root_state) != IS_UNDEF) { + zval_ptr_dtor(&handle->root_state); + } + + array_init(&handle->root_state); + zend_opcache_static_cache_class_blob_ensure_root_arrays(handle); + properties_ht = zend_opcache_static_cache_class_blob_properties_ht(handle); + if (properties_ht == NULL) { + return false; + } + + ZEND_HASH_MAP_FOREACH_STR_KEY_PTR(&handle->ce->properties_info, property_name, property_info) { + if (property_name == NULL || !zend_opcache_static_cache_class_blob_applies_to_property(handle->ce, property_info)) { + continue; + } + + if (CE_STATIC_MEMBERS(handle->ce) != NULL) { + source = CE_STATIC_MEMBERS(handle->ce) + property_info->offset; + ZVAL_DEINDIRECT(source); + } else { + source = &handle->ce->default_static_members_table[property_info->offset]; + } + + ZVAL_COPY_DEREF(©, source); + + zend_hash_update(properties_ht, property_name, ©); + } ZEND_HASH_FOREACH_END(); + + handle->initialized = true; + handle->dirty = true; + handle->request_deferred_publish = false; + + return true; +} + +static bool zend_opcache_static_cache_refresh_class_blob_handle_locked( + zend_opcache_static_cache_class_blob_handle *handle) +{ + zval cached_value; + + if (handle == NULL) { + return false; + } + + if (zend_opcache_static_cache_fetch_locked(handle->cache_key, &cached_value, false, NULL, false)) { + if (Z_TYPE(cached_value) == IS_ARRAY) { + if (Z_TYPE(handle->root_state) != IS_UNDEF) { + zval_ptr_dtor(&handle->root_state); + } + + ZVAL_COPY_VALUE(&handle->root_state, &cached_value); + if (!zend_opcache_static_cache_detach_class_blob_root_state(handle)) { + zval_ptr_dtor(&handle->root_state); + ZVAL_UNDEF(&handle->root_state); + if (!handle->initialized) { + return zend_opcache_static_cache_build_default_class_blob_state(handle); + } + + return false; + } + + handle->initialized = true; + handle->dirty = false; + handle->request_deferred_publish = zend_opcache_static_cache_kind_defers_reachable_mutation_publish(handle->kind); + zend_opcache_static_cache_class_blob_ensure_root_arrays(handle); + + return true; + } + + zval_ptr_dtor(&cached_value); + } + + if (!handle->initialized) { + return zend_opcache_static_cache_build_default_class_blob_state(handle); + } + + zend_opcache_static_cache_class_blob_ensure_root_arrays(handle); + + return true; +} + +static bool zend_opcache_static_cache_class_blob_build_snapshot( + zend_opcache_static_cache_class_blob_handle *handle, + zval *snapshot) +{ + zend_string *key, *var_name; + zval properties_snapshot, methods_snapshot, method_snapshot, copy, *value, *var_value; + HashTable *properties_ht, *methods_ht, *method_ht; + + if (handle == NULL) { + return false; + } + + array_init(snapshot); + array_init(&properties_snapshot); + array_init(&methods_snapshot); + + zend_hash_str_update(Z_ARRVAL_P(snapshot), ZEND_STRL("properties"), &properties_snapshot); + zend_hash_str_update(Z_ARRVAL_P(snapshot), ZEND_STRL("methods"), &methods_snapshot); + + properties_ht = zend_opcache_static_cache_class_blob_properties_ht(handle); + if (properties_ht != NULL) { + ZEND_HASH_FOREACH_STR_KEY_VAL(properties_ht, key, value) { + if (key == NULL) { + continue; + } + + ZVAL_COPY_DEREF(©, value); + + zend_hash_update(Z_ARRVAL(properties_snapshot), key, ©); + } ZEND_HASH_FOREACH_END(); + } + + methods_ht = zend_opcache_static_cache_class_blob_methods_ht(handle); + if (methods_ht != NULL) { + ZEND_HASH_FOREACH_STR_KEY_VAL(methods_ht, key, value) { + if (key == NULL || Z_TYPE_P(value) != IS_ARRAY) { + continue; + } + + array_init(&method_snapshot); + method_ht = Z_ARRVAL_P(value); + ZEND_HASH_FOREACH_STR_KEY_VAL(method_ht, var_name, var_value) { + if (var_name == NULL) { + continue; + } + + ZVAL_COPY_DEREF(©, var_value); + zend_hash_update(Z_ARRVAL(method_snapshot), var_name, ©); + } ZEND_HASH_FOREACH_END(); + + zend_hash_update(Z_ARRVAL(methods_snapshot), key, &method_snapshot); + } ZEND_HASH_FOREACH_END(); + } + + return true; +} + +static bool zend_opcache_static_cache_publish_class_blob_handle_locked( + zend_opcache_static_cache_class_blob_handle *handle) +{ + zval snapshot; + bool result; + + if (handle == NULL) { + return false; + } + + ZVAL_UNDEF(&snapshot); + if (!handle->dirty) { + return true; + } + + result = zend_opcache_static_cache_class_blob_build_snapshot(handle, &snapshot) && + zend_opcache_static_cache_store_locked(handle->cache_key, &snapshot, handle->ttl, false) + ; + + zval_ptr_dtor(&snapshot); + if (result) { + handle->dirty = false; + } + + return result; +} + +static void zend_opcache_static_cache_tracked_root_dtor(zval *zv) +{ + zend_opcache_static_cache_static_slot_handle *handle = Z_PTR_P(zv); + + if (handle == NULL) { + return; + } + + zend_string_release(handle->cache_key); + if (handle->has_original_value) { + zval_ptr_dtor(&handle->original_value); + } + + efree(handle); +} + +static bool zend_opcache_static_cache_value_changed(zval *current, zval *original) +{ + zval current_deref, original_deref; + bool changed; + + ZVAL_COPY_DEREF(¤t_deref, current); + ZVAL_COPY_DEREF(&original_deref, original); + + if (Z_TYPE(current_deref) != Z_TYPE(original_deref)) { + zval_ptr_dtor(¤t_deref); + zval_ptr_dtor(&original_deref); + + return true; + } + + switch (Z_TYPE(current_deref)) { + case IS_UNDEF: + case IS_NULL: + case IS_FALSE: + case IS_TRUE: + zval_ptr_dtor(¤t_deref); + zval_ptr_dtor(&original_deref); + + return false; + case IS_LONG: + changed = Z_LVAL(current_deref) != Z_LVAL(original_deref); + zval_ptr_dtor(¤t_deref); + zval_ptr_dtor(&original_deref); + + return changed; + case IS_DOUBLE: + changed = Z_DVAL(current_deref) != Z_DVAL(original_deref); + zval_ptr_dtor(¤t_deref); + zval_ptr_dtor(&original_deref); + + return changed; + case IS_STRING: + changed = !zend_string_equal_content(Z_STR(current_deref), Z_STR(original_deref)); + zval_ptr_dtor(¤t_deref); + zval_ptr_dtor(&original_deref); + + return changed; + case IS_ARRAY: + changed = Z_ARR(current_deref) != Z_ARR(original_deref); + zval_ptr_dtor(¤t_deref); + zval_ptr_dtor(&original_deref); + + return changed; + case IS_OBJECT: + changed = Z_OBJ(current_deref) != Z_OBJ(original_deref); + zval_ptr_dtor(¤t_deref); + zval_ptr_dtor(&original_deref); + + return changed; + default: + zval_ptr_dtor(¤t_deref); + zval_ptr_dtor(&original_deref); + + return true; + } +} + +static void zend_opcache_static_cache_set_tracked_root_original(zend_opcache_static_cache_static_slot_handle *handle) +{ + zval *value; + + if (handle == NULL || handle->slot == NULL) { + return; + } + + if (handle->has_original_value) { + zval_ptr_dtor(&handle->original_value); + } else { + handle->has_original_value = true; + } + + ZVAL_UNDEF(&handle->original_value); + handle->original_root = NULL; + handle->original_root_type = IS_UNDEF; + handle->snapshot_root_published = false; + + value = handle->slot; + ZVAL_DEINDIRECT(value); + ZVAL_DEREF(value); + if (zend_opcache_static_cache_kind_publishes_immediately(handle->kind) && + (Z_TYPE_P(value) == IS_ARRAY || Z_TYPE_P(value) == IS_OBJECT) + ) { + /* Keep only the root identity for immediate mutable values. Taking a + * strong zval copy would force array COW and make nested writes look like + * root reassignments during request shutdown. */ + handle->original_root = Z_COUNTED_P(value); + handle->original_root_type = Z_TYPE_P(value); + + return; + } + + ZVAL_COPY_DEREF(&handle->original_value, value); +} + +static zend_opcache_static_cache_static_slot_handle *zend_opcache_static_cache_ensure_tracked_root( + zval *slot, + zend_string *cache_key, + zend_opcache_static_cache_context *context, + zend_opcache_static_cache_kind kind, + zend_long ttl, + zend_opcache_static_cache_class_blob_handle *class_blob_handle) +{ + zend_opcache_static_cache_static_slot_handle *handle; + zend_ulong key; + + if (slot == NULL || cache_key == NULL || zend_opcache_static_cache_tracked_roots == NULL) { + return NULL; + } + + key = (zend_ulong) (uintptr_t) slot; + handle = zend_hash_index_find_ptr(zend_opcache_static_cache_tracked_roots, key); + if (handle != NULL) { + handle->class_blob_handle = class_blob_handle; + if (class_blob_handle != NULL) { + handle->context = class_blob_handle->context; + handle->kind = class_blob_handle->kind; + handle->ttl = class_blob_handle->ttl; + handle->request_deferred_publish = class_blob_handle->request_deferred_publish; + } else { + handle->context = context; + handle->kind = kind; + handle->ttl = ttl; + } + zend_opcache_static_cache_track_reference(slot, handle); + + return handle; + } + + handle = emalloc(sizeof(*handle)); + handle->slot = slot; + handle->cache_key = zend_string_copy(cache_key); + handle->context = class_blob_handle != NULL ? class_blob_handle->context : context; + handle->kind = class_blob_handle != NULL ? class_blob_handle->kind : kind; + handle->ttl = class_blob_handle != NULL ? class_blob_handle->ttl : ttl; + handle->class_blob_handle = class_blob_handle; + ZVAL_UNDEF(&handle->original_value); + handle->original_root = NULL; + handle->original_root_type = IS_UNDEF; + handle->has_original_value = false; + handle->snapshot_root_published = false; + handle->request_deferred_publish = class_blob_handle != NULL && class_blob_handle->request_deferred_publish; + handle->mutation_dirty = false; + zend_opcache_static_cache_set_tracked_root_original(handle); + zend_hash_index_update_ptr(zend_opcache_static_cache_tracked_roots, key, handle); + zend_opcache_static_cache_track_reference(slot, handle); + + return handle; +} + +static bool zend_opcache_static_cache_static_slot_value_changed(zend_opcache_static_cache_static_slot_handle *handle) +{ + zval *value; + + if (handle == NULL || !handle->has_original_value || handle->slot == NULL) { + return true; + } + + value = handle->slot; + ZVAL_DEINDIRECT(value); + ZVAL_DEREF(value); + if (handle->original_root_type == IS_ARRAY || handle->original_root_type == IS_OBJECT) { + bool root_changed = Z_TYPE_P(value) != handle->original_root_type || + Z_COUNTED_P(value) != handle->original_root; + + if (handle->snapshot_root_published && zend_opcache_static_cache_has_mutable_immediate_root(handle)) { + return handle->mutation_dirty; + } + + return handle->mutation_dirty || root_changed; + } + + return handle->mutation_dirty || zend_opcache_static_cache_value_changed(value, &handle->original_value); +} + +static bool zend_opcache_static_cache_pending_hash_is_root_array( + zend_opcache_static_cache_static_slot_handle *handle, + HashTable *ht) +{ + zval *value; + + if (handle == NULL || handle->slot == NULL || ht == NULL) { + return false; + } + + value = handle->slot; + ZVAL_DEINDIRECT(value); + ZVAL_DEREF(value); + + return Z_TYPE_P(value) == IS_ARRAY && Z_ARRVAL_P(value) == ht; +} + +static bool zend_opcache_static_cache_value_contains_reachable_array( + zval *value, + HashTable *needle, + HashTable *seen_arrays) +{ + zend_ulong key; + zval *child; + HashTable *ht; + + if (value == NULL || needle == NULL) { + return false; + } + + ZVAL_DEREF(value); + if (Z_TYPE_P(value) != IS_ARRAY) { + return false; + } + + ht = Z_ARRVAL_P(value); + if (ht == needle) { + return true; + } + + key = (zend_ulong) (uintptr_t) ht; + if (zend_hash_index_exists(seen_arrays, key)) { + return false; + } + + zend_hash_index_add_empty_element(seen_arrays, key); + ZEND_HASH_FOREACH_VAL(ht, child) { + if (zend_opcache_static_cache_value_contains_reachable_array(child, needle, seen_arrays)) { + return true; + } + } ZEND_HASH_FOREACH_END(); + + return false; +} + +static bool zend_opcache_static_cache_pending_hash_is_current_reachable_array( + zend_opcache_static_cache_static_slot_handle *handle, + HashTable *ht) +{ + zval *value; + HashTable seen_arrays; + bool reachable; + + if (handle == NULL || + handle->slot == NULL || + ht == NULL || + !zend_opcache_static_cache_tracks_reachable_arrays(handle) + ) { + return false; + } + + value = handle->slot; + ZVAL_DEINDIRECT(value); + + zend_hash_init(&seen_arrays, 8, NULL, NULL, 0); + reachable = zend_opcache_static_cache_value_contains_reachable_array(value, ht, &seen_arrays); + zend_hash_destroy(&seen_arrays); + + return reachable; +} + +static bool zend_opcache_static_cache_tracked_root_changed(zval *slot) +{ + zend_opcache_static_cache_static_slot_handle *handle; + + if (slot == NULL || zend_opcache_static_cache_tracked_roots == NULL) { + return true; + } + + handle = zend_hash_index_find_ptr(zend_opcache_static_cache_tracked_roots, (zend_ulong) (uintptr_t) slot); + + return zend_opcache_static_cache_static_slot_value_changed(handle); +} + +static bool zend_opcache_static_cache_class_blob_changed(zend_opcache_static_cache_class_blob_handle *class_blob_handle) +{ + zend_opcache_static_cache_static_slot_handle *handle; + + if (class_blob_handle == NULL) { + return false; + } + + if (class_blob_handle->dirty) { + return true; + } + + if (zend_opcache_static_cache_tracked_roots == NULL) { + return false; + } + + ZEND_HASH_FOREACH_PTR(zend_opcache_static_cache_tracked_roots, handle) { + if (handle != NULL && + handle->class_blob_handle == class_blob_handle && + zend_opcache_static_cache_static_slot_value_changed(handle) + ) { + return true; + } + } ZEND_HASH_FOREACH_END(); + + return false; +} + +static bool zend_opcache_static_cache_register_captured_values(zend_opcache_static_cache_static_slot_handle *handle) +{ + zend_object *obj; + zend_ulong key; + HashTable *ht; + + if (!zend_opcache_static_cache_capture_available || handle == NULL) { + return false; + } + + if (zend_opcache_static_cache_capture_arrays != NULL && + zend_opcache_static_cache_tracked_arrays != NULL && + zend_opcache_static_cache_tracks_reachable_arrays(handle) + ) { + ZEND_HASH_FOREACH_NUM_KEY(zend_opcache_static_cache_capture_arrays, key) { + ht = (HashTable *) (uintptr_t) key; + zend_opcache_static_cache_track_dependency(zend_opcache_static_cache_tracked_arrays, key, handle); + zend_opcache_static_cache_has_tracked_arrays = true; + if (zend_opcache_static_cache_last_array_ht == ht) { + zend_opcache_static_cache_last_array_ht = NULL; + zend_opcache_static_cache_last_array_dependency = NULL; + } + } ZEND_HASH_FOREACH_END(); + } + + if (zend_opcache_static_cache_capture_objects != NULL && + zend_opcache_static_cache_tracked_objects != NULL && + zend_opcache_static_cache_tracks_reachable_objects(handle) + ) { + ZEND_HASH_FOREACH_NUM_KEY(zend_opcache_static_cache_capture_objects, key) { + obj = (zend_object *) (uintptr_t) key; + zend_opcache_static_cache_track_dependency(zend_opcache_static_cache_tracked_objects, key, handle); + zend_opcache_static_cache_has_tracked_objects = true; + if (zend_opcache_static_cache_last_object_obj == obj) { + zend_opcache_static_cache_last_object_obj = NULL; + zend_opcache_static_cache_last_object_dependency = NULL; + } + } ZEND_HASH_FOREACH_END(); + } + + zend_opcache_static_cache_update_mutation_hook_state(); + zend_opcache_static_cache_capture_clear(); + + return true; +} + +static void zend_opcache_static_cache_track_value_recursive( + zval *value, + zend_opcache_static_cache_static_slot_handle *handle, + HashTable *seen_arrays, + HashTable *seen_objects +) +{ + zend_object *obj; + zend_ulong key; + zend_string *property_name; + zval *property_value, *source_value; + HashTable *ht, *properties; + + if (value == NULL || handle == NULL) { + return; + } + + ZVAL_DEREF(value); + switch (Z_TYPE_P(value)) { + case IS_ARRAY: + ht = Z_ARRVAL_P(value); + key = (zend_ulong) (uintptr_t) ht; + + if (zend_hash_index_exists(seen_arrays, key)) { + return; + } + + if (!zend_opcache_static_cache_tracks_reachable_arrays(handle)) { + return; + } + + zend_hash_index_add_empty_element(seen_arrays, key); + if (zend_opcache_static_cache_tracked_arrays != NULL) { + zend_opcache_static_cache_track_dependency(zend_opcache_static_cache_tracked_arrays, key, handle); + zend_opcache_static_cache_has_tracked_arrays = true; + if (zend_opcache_static_cache_last_array_ht == ht) { + zend_opcache_static_cache_last_array_ht = NULL; + zend_opcache_static_cache_last_array_dependency = NULL; + } + } + + zend_opcache_static_cache_update_mutation_hook_state(); + + ZEND_HASH_FOREACH_VAL(ht, property_value) { + zend_opcache_static_cache_track_value_recursive(property_value, handle, seen_arrays, seen_objects); + } ZEND_HASH_FOREACH_END(); + + return; + case IS_OBJECT: + obj = Z_OBJ_P(value); + + if (!zend_opcache_static_cache_tracks_reachable_objects(handle)) { + /* Immediate attributes snapshot assigned objects at the root. Later object + * mutations require an explicit reassignment to publish. */ + return; + } + + key = (zend_ulong) (uintptr_t) obj; + + if (zend_hash_index_exists(seen_objects, key)) { + return; + } + + zend_hash_index_add_empty_element(seen_objects, key); + + if (zend_opcache_static_cache_tracked_objects != NULL) { + zend_opcache_static_cache_track_dependency(zend_opcache_static_cache_tracked_objects, key, handle); + zend_opcache_static_cache_has_tracked_objects = true; + if (zend_opcache_static_cache_last_object_obj == obj) { + zend_opcache_static_cache_last_object_obj = NULL; + zend_opcache_static_cache_last_object_dependency = NULL; + } + } + + zend_opcache_static_cache_update_mutation_hook_state(); + + properties = zend_get_properties_for(value, ZEND_PROP_PURPOSE_SERIALIZE); + if (properties == NULL) { + return; + } + + ZEND_HASH_FOREACH_STR_KEY_VAL(properties, property_name, property_value) { + if (property_name == NULL) { + continue; + } + + source_value = Z_TYPE_P(property_value) == IS_INDIRECT ? Z_INDIRECT_P(property_value) : property_value; + zend_opcache_static_cache_track_value_recursive(source_value, handle, seen_arrays, seen_objects); + } ZEND_HASH_FOREACH_END(); + + zend_release_properties(properties); + + return; + default: + return; + } +} + +static bool zend_opcache_static_cache_prepare_memo_can_track_object(zend_class_entry *ce) +{ + return ce != NULL && + ce->type == ZEND_USER_CLASS && + zend_opcache_serializer_find_safe_direct_cache_base(ce) == NULL && + ce->create_object == NULL && + ce->__serialize == NULL && + ce->__unserialize == NULL && + !zend_hash_str_exists(&ce->function_table, ZEND_STRL("__sleep")) && + !zend_hash_str_exists(&ce->function_table, ZEND_STRL("__wakeup")); +} + +static bool zend_opcache_static_cache_prepare_memo_can_track_value_recursive( + zval *value, + HashTable *seen_arrays, + HashTable *seen_objects) +{ + zend_object *obj; + zend_ulong key; + zend_string *property_name; + zval *child, *property_value, *source_value; + HashTable *ht, *properties; + + if (value == NULL) { + return false; + } + + ZVAL_DEREF(value); + switch (Z_TYPE_P(value)) { + case IS_UNDEF: + case IS_NULL: + case IS_FALSE: + case IS_TRUE: + case IS_LONG: + case IS_DOUBLE: + case IS_STRING: + return true; + case IS_ARRAY: + ht = Z_ARRVAL_P(value); + key = (zend_ulong) (uintptr_t) ht; + if (zend_hash_index_exists(seen_arrays, key)) { + return true; + } + + zend_hash_index_add_empty_element(seen_arrays, key); + ZEND_HASH_FOREACH_VAL(ht, child) { + if (!zend_opcache_static_cache_prepare_memo_can_track_value_recursive(child, seen_arrays, seen_objects)) { + return false; + } + } ZEND_HASH_FOREACH_END(); + + return true; + case IS_OBJECT: + obj = Z_OBJ_P(value); + if (!zend_opcache_static_cache_prepare_memo_can_track_object(obj->ce)) { + return false; + } + + key = (zend_ulong) (uintptr_t) obj; + if (zend_hash_index_exists(seen_objects, key)) { + return true; + } + + zend_hash_index_add_empty_element(seen_objects, key); + properties = zend_get_properties_for(value, ZEND_PROP_PURPOSE_SERIALIZE); + if (properties == NULL) { + return true; + } + + ZEND_HASH_FOREACH_STR_KEY_VAL(properties, property_name, property_value) { + if (property_name == NULL) { + continue; + } + + source_value = Z_TYPE_P(property_value) == IS_INDIRECT ? Z_INDIRECT_P(property_value) : property_value; + if (!zend_opcache_static_cache_prepare_memo_can_track_value_recursive(source_value, seen_arrays, seen_objects)) { + zend_release_properties(properties); + return false; + } + } ZEND_HASH_FOREACH_END(); + + zend_release_properties(properties); + + return true; + default: + return false; + } +} + +static void zend_opcache_static_cache_prepare_memo_track_value_recursive( + zval *value, + zend_opcache_static_cache_prepare_memo_entry *entry, + HashTable *seen_arrays, + HashTable *seen_objects) +{ + zend_object *obj; + zend_ulong key; + zend_string *property_name; + zval *child, *property_value, *source_value; + HashTable *ht, *properties; + + if (value == NULL || entry == NULL) { + return; + } + + ZVAL_DEREF(value); + switch (Z_TYPE_P(value)) { + case IS_ARRAY: + ht = Z_ARRVAL_P(value); + key = (zend_ulong) (uintptr_t) ht; + if (zend_hash_index_exists(seen_arrays, key)) { + return; + } + + zend_hash_index_add_empty_element(seen_arrays, key); + zend_opcache_static_cache_prepare_memo_track_dependency(zend_opcache_static_cache_prepare_memo_arrays, key, entry); + zend_opcache_static_cache_has_prepare_memo_arrays = true; + ZEND_HASH_FOREACH_VAL(ht, child) { + zend_opcache_static_cache_prepare_memo_track_value_recursive(child, entry, seen_arrays, seen_objects); + } ZEND_HASH_FOREACH_END(); + + return; + case IS_OBJECT: + obj = Z_OBJ_P(value); + key = (zend_ulong) (uintptr_t) obj; + if (zend_hash_index_exists(seen_objects, key)) { + return; + } + + zend_hash_index_add_empty_element(seen_objects, key); + zend_opcache_static_cache_prepare_memo_track_dependency(zend_opcache_static_cache_prepare_memo_objects, key, entry); + zend_opcache_static_cache_has_prepare_memo_objects = true; + properties = zend_get_properties_for(value, ZEND_PROP_PURPOSE_SERIALIZE); + if (properties == NULL) { + return; + } + + ZEND_HASH_FOREACH_STR_KEY_VAL(properties, property_name, property_value) { + if (property_name == NULL) { + continue; + } + + source_value = Z_TYPE_P(property_value) == IS_INDIRECT ? Z_INDIRECT_P(property_value) : property_value; + zend_opcache_static_cache_prepare_memo_track_value_recursive(source_value, entry, seen_arrays, seen_objects); + } ZEND_HASH_FOREACH_END(); + + zend_release_properties(properties); + + return; + default: + return; + } +} +static void zend_opcache_static_cache_register_tracked_value_ex( + zval *slot, + zend_string *cache_key, + zend_opcache_static_cache_context *context, + zend_opcache_static_cache_kind kind, + zend_long ttl, + zend_opcache_static_cache_class_blob_handle *class_blob_handle) +{ + zend_opcache_static_cache_static_slot_handle *handle; + HashTable seen_arrays, seen_objects; + + if (slot == NULL || cache_key == NULL) { + return; + } + + handle = zend_opcache_static_cache_ensure_tracked_root(slot, cache_key, context, kind, ttl, class_blob_handle); + if (handle == NULL) { + return; + } + + zend_opcache_static_cache_untrack_dependencies(handle); + + if (zend_opcache_static_cache_register_captured_values(handle)) { + return; + } + + if (!zend_opcache_static_cache_tracks_reachable_mutations(handle)) { + return; + } + + handle->request_deferred_publish = zend_opcache_static_cache_kind_defers_reachable_mutation_publish(handle->kind); + + zend_hash_init(&seen_arrays, 8, NULL, NULL, 0); + zend_hash_init(&seen_objects, 8, NULL, NULL, 0); + + zend_opcache_static_cache_track_value_recursive(slot, handle, &seen_arrays, &seen_objects); + zend_hash_destroy(&seen_objects); + zend_hash_destroy(&seen_arrays); +} + +static void zend_opcache_static_cache_register_tracked_value(zval *slot, zend_string *cache_key, zend_opcache_static_cache_kind kind, zend_long ttl) +{ + zend_opcache_static_cache_register_tracked_value_ex(slot, cache_key, zend_opcache_static_cache_context_for_kind(kind), kind, ttl, NULL); +} + +static void zend_opcache_static_cache_refresh_class_blob_root_originals( + zend_opcache_static_cache_class_blob_handle *class_blob_handle) +{ + zend_opcache_static_cache_static_slot_handle *handle; + + if (class_blob_handle == NULL || zend_opcache_static_cache_tracked_roots == NULL) { + return; + } + + class_blob_handle->request_deferred_publish = false; + + ZEND_HASH_FOREACH_PTR(zend_opcache_static_cache_tracked_roots, handle) { + if (handle == NULL || handle->class_blob_handle != class_blob_handle) { + continue; + } + + zend_opcache_static_cache_set_tracked_root_original(handle); + handle->mutation_dirty = false; + handle->request_deferred_publish = false; + zend_opcache_static_cache_register_tracked_value_ex( + handle->slot, + handle->cache_key, + handle->context, + handle->kind, + handle->ttl, + class_blob_handle + ); + } ZEND_HASH_FOREACH_END(); +} + +static bool zend_opcache_static_cache_sync_class_blob_function_static_entry( + zend_opcache_static_cache_function_static_entry *entry) +{ + zval new_method_state, value_snapshot, *methods, *method_state, *value, *member; + + if (entry == NULL || + entry->slot == NULL || + entry->class_blob_handle == NULL || + entry->method_key == NULL || + entry->var_name == NULL + ) { + return false; + } + + methods = zend_opcache_static_cache_class_blob_ensure_section(entry->class_blob_handle, ZEND_STRL("methods")); + method_state = zend_hash_find(Z_ARRVAL_P(methods), entry->method_key); + if (method_state == NULL || Z_TYPE_P(method_state) != IS_ARRAY) { + array_init(&new_method_state); + method_state = zend_hash_update(Z_ARRVAL_P(methods), entry->method_key, &new_method_state); + } + + value = entry->slot; + ZVAL_DEINDIRECT(value); + if (Z_TYPE_P(value) == IS_UNDEF) { + return false; + } + + member = zend_hash_find(Z_ARRVAL_P(method_state), entry->var_name); + if (member != NULL && !zend_opcache_static_cache_value_changed(value, member)) { + return false; + } + + ZVAL_COPY_DEREF(&value_snapshot, value); + zend_hash_update(Z_ARRVAL_P(method_state), entry->var_name, &value_snapshot); + entry->class_blob_handle->dirty = true; + + return true; +} + +static void zend_opcache_static_cache_sync_class_blob_function_static_handle( + zend_opcache_static_cache_static_slot_handle *handle) +{ + zend_opcache_static_cache_function_static_entry *entry; + + if (handle == NULL || + handle->slot == NULL || + handle->class_blob_handle == NULL || + !zend_opcache_static_cache_function_statics_initialized + ) { + return; + } + + entry = zend_hash_index_find_ptr(&zend_opcache_static_cache_function_statics, (zend_ulong) (uintptr_t) handle->slot); + if (entry == NULL || entry->class_blob_handle != handle->class_blob_handle) { + return; + } + + zend_opcache_static_cache_sync_class_blob_function_static_entry(entry); +} + +static void zend_opcache_static_cache_publish_immediate_root(zend_opcache_static_cache_static_slot_handle *handle) +{ + zend_opcache_static_cache_context *previous_context; + zval value_snapshot, *slot; + bool published = false; + + if (handle == NULL || + !zend_opcache_static_cache_kind_publishes_immediately(handle->kind) || + !zend_opcache_static_cache_context_runtime(handle->context)->available || + !zend_opcache_static_cache_static_slot_value_changed(handle) + ) { + return; + } + + previous_context = zend_opcache_static_cache_activate_context(handle->context); + if (zend_opcache_static_cache_wlock()) { + if (zend_opcache_static_cache_header_init_locked()) { + if (handle->class_blob_handle != NULL) { + zend_opcache_static_cache_sync_class_blob_function_static_handle(handle); + handle->class_blob_handle->dirty = true; + published = zend_opcache_static_cache_publish_class_blob_handle_locked(handle->class_blob_handle); + } else { + slot = handle->slot; + ZVAL_DEINDIRECT(slot); + if (Z_TYPE_P(slot) == IS_UNDEF) { + published = zend_opcache_static_cache_delete_locked(handle->cache_key); + } else { + ZVAL_COPY_DEREF(&value_snapshot, slot); + published = zend_opcache_static_cache_store_locked(handle->cache_key, &value_snapshot, handle->ttl, false); + zval_ptr_dtor(&value_snapshot); + } + } + } + + zend_opcache_static_cache_unlock(); + } + + zend_opcache_static_cache_restore_context(previous_context); + + if (!published) { + return; + } + + if (handle->class_blob_handle != NULL) { + zend_opcache_static_cache_refresh_class_blob_root_originals(handle->class_blob_handle); + } else { + zend_opcache_static_cache_set_tracked_root_original(handle); + handle->snapshot_root_published = zend_opcache_static_cache_has_mutable_immediate_root(handle); + handle->mutation_dirty = false; + handle->request_deferred_publish = false; + zend_opcache_static_cache_register_tracked_value(handle->slot, handle->cache_key, handle->kind, handle->ttl); + } +} + +static void zend_opcache_static_cache_publish_class_blob_fast(zend_opcache_static_cache_class_blob_handle *class_blob_handle) +{ + zend_opcache_static_cache_context *previous_context; + bool published = false; + + if (class_blob_handle == NULL || + !zend_opcache_static_cache_kind_publishes_immediately(class_blob_handle->kind) || + !class_blob_handle->initialized || + !zend_opcache_static_cache_context_runtime(class_blob_handle->context)->available || + !zend_opcache_static_cache_class_blob_changed(class_blob_handle) + ) { + return; + } + + previous_context = zend_opcache_static_cache_activate_context(class_blob_handle->context); + if (zend_opcache_static_cache_wlock()) { + if (zend_opcache_static_cache_header_init_locked()) { + /* Immediate class blobs publish root assignments immediately. PersistentStatic + * array graphs are re-registered after publication so later array writes + * also fail or succeed at the write site. */ + class_blob_handle->dirty = true; + published = zend_opcache_static_cache_publish_class_blob_handle_locked(class_blob_handle); + } + zend_opcache_static_cache_unlock(); + } + + zend_opcache_static_cache_restore_context(previous_context); + + if (published) { + zend_opcache_static_cache_refresh_class_blob_root_originals(class_blob_handle); + } +} + +static bool zend_opcache_static_cache_install_class_blob_property_refs( + zend_opcache_static_cache_class_blob_handle *handle) +{ + zend_string *property_name; + zend_property_info *property_info; + zval snapshot, *properties, *slot, *member; + + if (handle == NULL || handle->ce == NULL || CE_STATIC_MEMBERS(handle->ce) == NULL) { + return false; + } + + properties = zend_opcache_static_cache_class_blob_ensure_section(handle, ZEND_STRL("properties")); + ZEND_HASH_MAP_FOREACH_STR_KEY_PTR(&handle->ce->properties_info, property_name, property_info) { + if (property_name == NULL || !zend_opcache_static_cache_class_blob_applies_to_property(handle->ce, property_info)) { + continue; + } + + slot = CE_STATIC_MEMBERS(handle->ce) + property_info->offset; + ZVAL_DEINDIRECT(slot); + member = zend_hash_find(Z_ARRVAL_P(properties), property_name); + if (member == NULL) { + ZVAL_COPY_DEREF(&snapshot, slot); + member = zend_hash_update(Z_ARRVAL_P(properties), property_name, &snapshot); + handle->dirty = true; + } + + zend_opcache_static_cache_bind_slot_to_member(slot, member); + zend_opcache_static_cache_register_tracked_value_ex(slot, handle->cache_key, handle->context, handle->kind, handle->ttl, handle); + } ZEND_HASH_FOREACH_END(); + + return true; +} + +static bool zend_opcache_static_cache_bind_class_blob_method_refs( + zend_opcache_static_cache_class_blob_handle *handle, + zend_op_array *op_array, + HashTable *static_variables) +{ + zend_string *method_key, *var_name; + zval new_method_state, snapshot, *methods, *method_state, *slot, *member; + + if (handle == NULL || op_array == NULL || static_variables == NULL) { + return false; + } + + methods = zend_opcache_static_cache_class_blob_ensure_section(handle, ZEND_STRL("methods")); + method_key = zend_opcache_static_cache_class_blob_method_key(op_array); + if (method_key == NULL) { + return false; + } + + method_state = zend_hash_find(Z_ARRVAL_P(methods), method_key); + if (method_state == NULL || Z_TYPE_P(method_state) != IS_ARRAY) { + array_init(&new_method_state); + method_state = zend_hash_update(Z_ARRVAL_P(methods), method_key, &new_method_state); + } + + ZEND_HASH_FOREACH_STR_KEY_VAL(static_variables, var_name, slot) { + if (var_name == NULL) { + continue; + } + + zend_opcache_static_cache_track_class_blob_function_static_slot(slot, handle, method_key, var_name); + + member = zend_hash_find(Z_ARRVAL_P(method_state), var_name); + if (member == NULL) { + if (Z_TYPE_P(slot) == IS_NULL) { + handle->dirty = true; + zend_opcache_static_cache_register_tracked_value_ex(slot, handle->cache_key, handle->context, handle->kind, handle->ttl, handle); + + continue; + } else { + ZVAL_COPY_DEREF(&snapshot, slot); + member = zend_hash_update(Z_ARRVAL_P(method_state), var_name, &snapshot); + + handle->dirty = true; + } + } + + zend_opcache_static_cache_bind_slot_to_member(slot, member); + zend_opcache_static_cache_register_tracked_value_ex(slot, handle->cache_key, handle->context, handle->kind, handle->ttl, handle); + } ZEND_HASH_FOREACH_END(); + + zend_string_release(method_key); + + return true; +} + +static void zend_opcache_static_cache_init_tracking(void) +{ + zend_opcache_static_cache_has_tracked_arrays = false; + zend_opcache_static_cache_has_tracked_objects = false; + zend_opcache_static_cache_has_prepare_memo_arrays = false; + zend_opcache_static_cache_has_prepare_memo_objects = false; + zend_opcache_static_cache_capture_active = false; + zend_opcache_static_cache_capture_clear(); + zend_opcache_static_cache_last_array_ht = NULL; + zend_opcache_static_cache_last_array_dependency = NULL; + zend_opcache_static_cache_last_object_obj = NULL; + zend_opcache_static_cache_last_object_dependency = NULL; + zend_opcache_static_cache_update_mutation_hook_state(); + + if (zend_opcache_static_cache_tracked_roots == NULL) { + zend_opcache_static_cache_tracked_roots = emalloc(sizeof(HashTable)); + zend_hash_init(zend_opcache_static_cache_tracked_roots, 8, NULL, zend_opcache_static_cache_tracked_root_dtor, 0); + } + + if (zend_opcache_static_cache_tracked_references == NULL) { + zend_opcache_static_cache_tracked_references = emalloc(sizeof(HashTable)); + zend_hash_init(zend_opcache_static_cache_tracked_references, 8, NULL, NULL, 0); + } + + if (zend_opcache_static_cache_tracked_arrays == NULL) { + zend_opcache_static_cache_tracked_arrays = emalloc(sizeof(HashTable)); + zend_hash_init(zend_opcache_static_cache_tracked_arrays, 8, NULL, zend_opcache_static_cache_tracked_dependency_dtor, 0); + } + + if (zend_opcache_static_cache_tracked_objects == NULL) { + zend_opcache_static_cache_tracked_objects = emalloc(sizeof(HashTable)); + zend_hash_init(zend_opcache_static_cache_tracked_objects, 8, NULL, zend_opcache_static_cache_tracked_dependency_dtor, 0); + } + + zend_opcache_static_cache_reset_pending_mutation(); +} + +static void zend_opcache_static_cache_destroy_tracking(void) +{ + zend_opcache_static_cache_reset_pending_mutation(); + zend_opcache_static_cache_has_tracked_arrays = false; + zend_opcache_static_cache_has_tracked_objects = false; + zend_opcache_static_cache_has_prepare_memo_arrays = false; + zend_opcache_static_cache_has_prepare_memo_objects = false; + zend_opcache_static_cache_capture_active = false; + zend_opcache_static_cache_capture_available = false; + zend_opcache_static_cache_last_array_ht = NULL; + zend_opcache_static_cache_last_array_dependency = NULL; + zend_opcache_static_cache_last_object_obj = NULL; + zend_opcache_static_cache_last_object_dependency = NULL; + zend_opcache_static_cache_update_mutation_hook_state(); + + if (zend_opcache_static_cache_tracked_roots != NULL) { + zend_hash_destroy(zend_opcache_static_cache_tracked_roots); + efree(zend_opcache_static_cache_tracked_roots); + zend_opcache_static_cache_tracked_roots = NULL; + } + + if (zend_opcache_static_cache_tracked_references != NULL) { + zend_hash_destroy(zend_opcache_static_cache_tracked_references); + efree(zend_opcache_static_cache_tracked_references); + zend_opcache_static_cache_tracked_references = NULL; + } + + if (zend_opcache_static_cache_tracked_arrays != NULL) { + zend_hash_destroy(zend_opcache_static_cache_tracked_arrays); + efree(zend_opcache_static_cache_tracked_arrays); + zend_opcache_static_cache_tracked_arrays = NULL; + } + + if (zend_opcache_static_cache_tracked_objects != NULL) { + zend_hash_destroy(zend_opcache_static_cache_tracked_objects); + efree(zend_opcache_static_cache_tracked_objects); + zend_opcache_static_cache_tracked_objects = NULL; + } + + if (zend_opcache_static_cache_prepare_memo_entries != NULL) { + zend_hash_destroy(zend_opcache_static_cache_prepare_memo_entries); + efree(zend_opcache_static_cache_prepare_memo_entries); + zend_opcache_static_cache_prepare_memo_entries = NULL; + } + + if (zend_opcache_static_cache_prepare_memo_array_roots != NULL) { + zend_hash_destroy(zend_opcache_static_cache_prepare_memo_array_roots); + efree(zend_opcache_static_cache_prepare_memo_array_roots); + zend_opcache_static_cache_prepare_memo_array_roots = NULL; + } + + if (zend_opcache_static_cache_prepare_memo_object_roots != NULL) { + zend_hash_destroy(zend_opcache_static_cache_prepare_memo_object_roots); + efree(zend_opcache_static_cache_prepare_memo_object_roots); + zend_opcache_static_cache_prepare_memo_object_roots = NULL; + } + + if (zend_opcache_static_cache_prepare_memo_arrays != NULL) { + zend_hash_destroy(zend_opcache_static_cache_prepare_memo_arrays); + efree(zend_opcache_static_cache_prepare_memo_arrays); + zend_opcache_static_cache_prepare_memo_arrays = NULL; + } + + if (zend_opcache_static_cache_prepare_memo_objects != NULL) { + zend_hash_destroy(zend_opcache_static_cache_prepare_memo_objects); + efree(zend_opcache_static_cache_prepare_memo_objects); + zend_opcache_static_cache_prepare_memo_objects = NULL; + } + + if (zend_opcache_static_cache_capture_arrays != NULL) { + zend_hash_destroy(zend_opcache_static_cache_capture_arrays); + efree(zend_opcache_static_cache_capture_arrays); + zend_opcache_static_cache_capture_arrays = NULL; + } + + if (zend_opcache_static_cache_capture_objects != NULL) { + zend_hash_destroy(zend_opcache_static_cache_capture_objects); + efree(zend_opcache_static_cache_capture_objects); + zend_opcache_static_cache_capture_objects = NULL; + } +} + +static void zend_opcache_static_cache_mark_dirty_class_blob_handles(void) +{ + zend_opcache_static_cache_static_slot_handle *handle; + + if (zend_opcache_static_cache_tracked_roots == NULL) { + return; + } + + ZEND_HASH_FOREACH_PTR(zend_opcache_static_cache_tracked_roots, handle) { + if (handle == NULL || + handle->class_blob_handle == NULL || + !handle->has_original_value || + handle->slot == NULL + ) { + continue; + } + + if (zend_opcache_static_cache_static_slot_value_changed(handle)) { + handle->class_blob_handle->dirty = true; + } + } ZEND_HASH_FOREACH_END(); +} + +static void zend_opcache_static_cache_flush_pending(void) +{ + zend_opcache_static_cache_static_slot_handle *handle; + zend_opcache_static_cache_context *previous_context; + zval value_snapshot; + bool should_store, published = false; + + if (!zend_opcache_static_cache_pending_mutation_state.dirty || + zend_opcache_static_cache_pending_mutation_state.handle == NULL + ) { + zend_opcache_static_cache_reset_pending_mutation(); + + return; + } + + handle = zend_opcache_static_cache_pending_mutation_state.handle; + if (!zend_opcache_static_cache_context_runtime(handle->context)->available) { + zend_opcache_static_cache_reset_pending_mutation(); + + return; + } + + previous_context = zend_opcache_static_cache_activate_context(handle->context); + if (zend_opcache_static_cache_wlock()) { + if (zend_opcache_static_cache_header_init_locked()) { + if (handle->class_blob_handle != NULL) { + zend_opcache_static_cache_sync_class_blob_function_static_handle(handle); + handle->class_blob_handle->dirty = true; + published = zend_opcache_static_cache_publish_class_blob_handle_locked(handle->class_blob_handle); + } else { + ZVAL_COPY_DEREF(&value_snapshot, handle->slot); + should_store = Z_TYPE(value_snapshot) != IS_UNDEF; + if (should_store) { + published = zend_opcache_static_cache_store_locked(handle->cache_key, &value_snapshot, handle->ttl, false); + } else { + published = zend_opcache_static_cache_delete_locked(handle->cache_key); + } + + zval_ptr_dtor(&value_snapshot); + } + } + + zend_opcache_static_cache_unlock(); + } + + zend_opcache_static_cache_restore_context(previous_context); + + if (handle->class_blob_handle != NULL) { + if (published && zend_opcache_static_cache_kind_publishes_immediately(handle->kind)) { + zend_opcache_static_cache_refresh_class_blob_root_originals(handle->class_blob_handle); + } else { + zend_opcache_static_cache_register_tracked_value_ex(handle->slot, handle->cache_key, handle->context, handle->kind, handle->ttl, handle->class_blob_handle); + } + } else { + if (published && zend_opcache_static_cache_kind_publishes_immediately(handle->kind)) { + zend_opcache_static_cache_set_tracked_root_original(handle); + handle->snapshot_root_published = zend_opcache_static_cache_has_mutable_immediate_root(handle); + handle->mutation_dirty = false; + handle->request_deferred_publish = false; + } + zend_opcache_static_cache_register_tracked_value(handle->slot, handle->cache_key, handle->kind, handle->ttl); + } + + zend_opcache_static_cache_reset_pending_mutation(); +} + +static bool zend_opcache_static_cache_defer_mutation_publish(zend_opcache_static_cache_static_slot_handle *handle) +{ + if (handle == NULL || !handle->request_deferred_publish) { + return false; + } + + if (zend_opcache_static_cache_pending_mutation_state.handle != NULL && + zend_opcache_static_cache_pending_mutation_state.handle != handle + ) { + zend_opcache_static_cache_flush_pending(); + } + + handle->mutation_dirty = true; + + return true; +} + +static bool zend_opcache_static_cache_begin_mutation( + zend_opcache_static_cache_static_slot_handle *handle, + HashTable *ht) +{ + bool root_array; + + if (handle == NULL) { + return false; + } + + if (zend_opcache_static_cache_pending_mutation_state.handle != NULL && + zend_opcache_static_cache_pending_mutation_state.handle != handle + ) { + zend_opcache_static_cache_flush_pending(); + } + + root_array = zend_opcache_static_cache_pending_hash_is_root_array(handle, ht); + if (zend_opcache_static_cache_pending_mutation_state.handle == handle && + zend_opcache_static_cache_pending_mutation_state.dirty + ) { + zend_opcache_static_cache_pending_mutation_state.depth++; + zend_opcache_static_cache_pending_mutation_state.root_array &= root_array; + + return true; + } + + zend_opcache_static_cache_pending_mutation_state.handle = handle; + zend_opcache_static_cache_pending_mutation_state.depth = 1; + zend_opcache_static_cache_pending_mutation_state.dirty = true; + zend_opcache_static_cache_pending_mutation_state.root_array = root_array; + + return true; +} + +static bool zend_opcache_static_cache_defer_dependency_publish( + zend_opcache_static_cache_tracked_dependency *dependency) +{ + zend_opcache_static_cache_static_slot_handle *handle; + bool deferred = false; + + if (dependency == NULL) { + return false; + } + + if (dependency->handles == NULL) { + return zend_opcache_static_cache_defer_mutation_publish(dependency->single_handle); + } + + ZEND_HASH_FOREACH_PTR(dependency->handles, handle) { + if (zend_opcache_static_cache_defer_mutation_publish(handle)) { + deferred = true; + } + } ZEND_HASH_FOREACH_END(); + + return deferred; +} + +static zend_opcache_static_cache_static_slot_handle *zend_opcache_static_cache_tracked_dependency_first_handle( + zend_opcache_static_cache_tracked_dependency *dependency) +{ + zend_opcache_static_cache_static_slot_handle *handle; + + if (dependency == NULL) { + return NULL; + } + + if (dependency->handles == NULL) { + return dependency->single_handle; + } + + ZEND_HASH_FOREACH_PTR(dependency->handles, handle) { + return handle; + } ZEND_HASH_FOREACH_END(); + + return NULL; +} + +static void zend_opcache_static_cache_delete_method_keys_locked(zend_class_entry *ce) +{ + zend_opcache_static_cache_kind kind; + zend_function *function; + zend_op_array *op_array; + zend_string *var_name, *cache_key; + zval *slot; + + ZEND_HASH_MAP_FOREACH_PTR(&ce->function_table, function) { + if (function == NULL || function->type != ZEND_USER_FUNCTION) { + continue; + } + + op_array = &function->op_array; + kind = zend_opcache_static_cache_op_array_kind(op_array); + if (kind == ZEND_OPCACHE_STATIC_CACHE_NONE || + zend_opcache_static_cache_context_for_kind(kind) != zend_opcache_static_cache_active_context() + ) { + continue; + } + + ZEND_HASH_FOREACH_STR_KEY_VAL(op_array->static_variables, var_name, slot) { + (void) slot; + + if (var_name == NULL) { + continue; + } + + cache_key = zend_opcache_static_cache_function_variable_key(op_array, var_name, kind); + if (cache_key == NULL) { + continue; + } + + zend_opcache_static_cache_delete_locked(cache_key); + zend_string_release(cache_key); + } ZEND_HASH_FOREACH_END(); + } ZEND_HASH_FOREACH_END(); +} + +static void zend_opcache_static_cache_delete_class_keys_locked(zend_class_entry *ce) +{ + zend_opcache_static_cache_kind kind; + zend_string *property_name, *cache_key; + zend_property_info *property_info; + + if (ce == NULL || ce->name == NULL) { + return; + } + + if (zend_opcache_static_cache_class_blob_enabled(ce)) { + kind = zend_opcache_static_cache_class_blob_kind(ce); + + if (zend_opcache_static_cache_context_for_kind(kind) != zend_opcache_static_cache_active_context()) { + return; + } + + cache_key = zend_opcache_static_cache_class_blob_key(ce, kind); + if (cache_key != NULL) { + zend_opcache_static_cache_delete_locked(cache_key); + zend_string_release(cache_key); + } + + return; + } + + ZEND_HASH_MAP_FOREACH_STR_KEY_PTR(&ce->properties_info, property_name, property_info) { + if (property_name == NULL) { + continue; + } + + kind = zend_opcache_static_cache_static_property_kind(ce, property_info); + if (kind == ZEND_OPCACHE_STATIC_CACHE_NONE + || zend_opcache_static_cache_context_for_kind(kind) != zend_opcache_static_cache_active_context()) { + continue; + } + + cache_key = zend_opcache_static_cache_static_property_key(ce, property_name, kind); + if (cache_key == NULL) { + continue; + } + + zend_opcache_static_cache_delete_locked(cache_key); + zend_string_release(cache_key); + } ZEND_HASH_FOREACH_END(); + + zend_opcache_static_cache_delete_method_keys_locked(ce); +} + +/* Fast-path filter used by zend_opcache_static_cache_class_access(). The + * filter walks the property table only on first encounter; subsequent calls + * for the same class are answered from the ignored/classes memoization. */ +static bool zend_opcache_static_cache_class_access_filter( + zend_class_entry *ce, + zend_opcache_static_cache_class_blob_handle **blob_out) +{ + zend_opcache_static_cache_class_blob_handle *blob; + zend_string *property_name; + zend_property_info *property_info; + zend_ulong key; + + *blob_out = NULL; + if (UNEXPECTED(ce->name == NULL)) { + return false; + } + + blob = zend_opcache_static_cache_find_class_blob_handle(ce); + if (blob != NULL) { + *blob_out = blob; + + return true; + } + + key = (zend_ulong) (uintptr_t) ce; + if (zend_opcache_static_cache_ignored_classes_initialized && + zend_hash_index_exists(&zend_opcache_static_cache_ignored_classes, key) + ) { + return false; + } + + if (zend_opcache_static_cache_class_blob_enabled(ce)) { + return true; + } + + if (zend_opcache_static_cache_attribute_classes_initialized && + zend_hash_index_exists(&zend_opcache_static_cache_attribute_classes, key) + ) { + return false; + } + + ZEND_HASH_MAP_FOREACH_STR_KEY_PTR(&ce->properties_info, property_name, property_info) { + if (property_name != NULL && zend_opcache_static_cache_applies_to_static_property(ce, property_info)) { + return true; + } + } ZEND_HASH_FOREACH_END(); + + if (zend_opcache_static_cache_ignored_classes_initialized) { + zend_hash_index_add_empty_element(&zend_opcache_static_cache_ignored_classes, key); + } + + return false; +} + +static void zend_opcache_static_cache_publish_persistent_static_properties_fast(zend_class_entry *ce) +{ + zend_opcache_static_cache_context *context, *previous_context; + zend_opcache_static_cache_static_slot_handle *handle; + zend_opcache_static_cache_kind kind; + zend_string *property_name, *cache_key; + zend_property_info *property_info; + zend_ulong class_key; + zend_long ttl; + zval *slot; + bool published; + + if (!zend_opcache_static_cache_attribute_classes_initialized || ce->name == NULL) { + return; + } + + class_key = (zend_ulong) (uintptr_t) ce; + if (!zend_hash_index_exists(&zend_opcache_static_cache_attribute_classes, class_key)) { + return; + } + + ZEND_HASH_MAP_FOREACH_STR_KEY_PTR(&ce->properties_info, property_name, property_info) { + if (property_name == NULL) { + continue; + } + + kind = zend_opcache_static_cache_static_property_kind_and_ttl(ce, property_info, &ttl); + if (!zend_opcache_static_cache_kind_publishes_immediately(kind)) { + continue; + } + + context = zend_opcache_static_cache_context_for_kind(kind); + if (!zend_opcache_static_cache_context_runtime(context)->available) { + continue; + } + + cache_key = zend_opcache_static_cache_static_property_key(ce, property_name, kind); + if (cache_key == NULL) { + continue; + } + + slot = CE_STATIC_MEMBERS(ce) + property_info->offset; + ZVAL_DEINDIRECT(slot); + handle = zend_opcache_static_cache_ensure_tracked_root(slot, cache_key, context, kind, ttl, NULL); + if (!zend_opcache_static_cache_static_slot_value_changed(handle)) { + zend_string_release(cache_key); + continue; + } + + published = false; + previous_context = zend_opcache_static_cache_activate_context(context); + if (zend_opcache_static_cache_wlock()) { + if (zend_opcache_static_cache_header_init_locked()) { + /* Immediate property assignments publish immediately so later + * mutations of the assigned graph do not change the snapshot. */ + if (Z_TYPE_P(slot) == IS_UNDEF) { + published = zend_opcache_static_cache_delete_locked(cache_key); + } else { + published = zend_opcache_static_cache_store_locked(cache_key, slot, ttl, false); + } + } + + zend_opcache_static_cache_unlock(); + } + + zend_opcache_static_cache_restore_context(previous_context); + + if (published) { + zend_opcache_static_cache_set_tracked_root_original(handle); + if (handle != NULL) { + handle->snapshot_root_published = zend_opcache_static_cache_has_mutable_immediate_root(handle); + handle->mutation_dirty = false; + handle->request_deferred_publish = false; + } + } + + zend_opcache_static_cache_register_tracked_value(slot, cache_key, kind, ttl); + zend_string_release(cache_key); + } ZEND_HASH_FOREACH_END(); +} + +static bool zend_opcache_static_cache_context_has_publish_work(zend_opcache_static_cache_context *context) +{ + zend_opcache_static_cache_class_blob_handle *class_blob_handle; + zend_opcache_static_cache_static_slot_handle *handle; + + if (zend_opcache_static_cache_class_blob_handles_initialized) { + ZEND_HASH_FOREACH_PTR(&zend_opcache_static_cache_class_blob_handles, class_blob_handle) { + if (class_blob_handle != NULL && + class_blob_handle->context == context && + zend_opcache_static_cache_class_blob_changed(class_blob_handle) + ) { + return true; + } + } ZEND_HASH_FOREACH_END(); + } + + if (zend_opcache_static_cache_tracked_roots != NULL) { + ZEND_HASH_FOREACH_PTR(zend_opcache_static_cache_tracked_roots, handle) { + if (handle != NULL && handle->context == context && zend_opcache_static_cache_static_slot_value_changed(handle)) { + return true; + } + } ZEND_HASH_FOREACH_END(); + } + + return false; +} + +static void zend_opcache_static_cache_publish_context(zend_opcache_static_cache_context *context) +{ + zend_opcache_static_cache_context *previous_context; + zend_opcache_static_cache_function_static_entry *entry; + zend_opcache_static_cache_class_blob_handle *class_blob_handle; + zend_opcache_static_cache_kind kind; + zend_class_entry *ce; + zend_string *property_name, *cache_key; + zend_property_info *property_info; + zend_long ttl; + zval value_snapshot, *value, *slot; + + if (!zend_opcache_static_cache_context_runtime(context)->available) { + return; + } + + if (zend_opcache_static_cache_publish_skipped(context)) { + return; + } + + if (!zend_opcache_static_cache_context_has_publish_work(context)) { + return; + } + + previous_context = zend_opcache_static_cache_activate_context(context); + if (!zend_opcache_static_cache_wlock()) { + zend_opcache_static_cache_restore_context(previous_context); + + return; + } + + if (!zend_opcache_static_cache_header_init_locked()) { + zend_opcache_static_cache_unlock(); + zend_opcache_static_cache_restore_context(previous_context); + + return; + } + + zend_opcache_static_cache_mark_dirty_class_blob_handles(); + + if (zend_opcache_static_cache_function_statics_initialized) { + ZEND_HASH_FOREACH_PTR(&zend_opcache_static_cache_function_statics, entry) { + if (entry == NULL || entry->slot == NULL || entry->class_blob_handle == NULL || entry->context != context) { + continue; + } + + zend_opcache_static_cache_sync_class_blob_function_static_entry(entry); + } ZEND_HASH_FOREACH_END(); + } + + if (zend_opcache_static_cache_class_blob_handles_initialized) { + ZEND_HASH_FOREACH_PTR(&zend_opcache_static_cache_class_blob_handles, class_blob_handle) { + if (class_blob_handle == NULL || class_blob_handle->context != context) { + continue; + } + + zend_opcache_static_cache_publish_class_blob_handle_locked(class_blob_handle); + } ZEND_HASH_FOREACH_END(); + } + + if (zend_opcache_static_cache_attribute_classes_initialized) { + ZEND_HASH_FOREACH_PTR(&zend_opcache_static_cache_attribute_classes, ce) { + if (ce == NULL || ce->name == NULL || CE_STATIC_MEMBERS(ce) == NULL) { + continue; + } + + ZEND_HASH_MAP_FOREACH_STR_KEY_PTR(&ce->properties_info, property_name, property_info) { + if (property_name == NULL) { + continue; + } + + kind = zend_opcache_static_cache_static_property_kind_and_ttl(ce, property_info, &ttl); + if (kind == ZEND_OPCACHE_STATIC_CACHE_NONE || zend_opcache_static_cache_context_for_kind(kind) != context) { + continue; + } + + slot = CE_STATIC_MEMBERS(ce) + property_info->offset; + ZVAL_DEINDIRECT(slot); + if (!zend_opcache_static_cache_tracked_root_changed(slot)) { + continue; + } + + cache_key = zend_opcache_static_cache_static_property_key(ce, property_name, kind); + if (cache_key == NULL) { + continue; + } + + if (Z_TYPE_P(slot) == IS_UNDEF) { + zend_opcache_static_cache_delete_locked(cache_key); + } else { + zend_opcache_static_cache_store_locked(cache_key, slot, ttl, false); + } + + zend_string_release(cache_key); + } ZEND_HASH_FOREACH_END(); + } ZEND_HASH_FOREACH_END(); + } + + if (zend_opcache_static_cache_function_statics_initialized) { + ZEND_HASH_FOREACH_PTR(&zend_opcache_static_cache_function_statics, entry) { + if (entry == NULL || entry->slot == NULL || entry->class_blob_handle != NULL || entry->context != context) { + continue; + } + + value = entry->slot; + if (!zend_opcache_static_cache_tracked_root_changed(value)) { + continue; + } + + ZVAL_DEINDIRECT(value); + ZVAL_COPY_DEREF(&value_snapshot, value); + if (Z_TYPE(value_snapshot) == IS_UNDEF) { + zend_opcache_static_cache_delete_locked(entry->cache_key); + } else { + zend_opcache_static_cache_store_locked(entry->cache_key, &value_snapshot, entry->ttl, false); + } + + zval_ptr_dtor(&value_snapshot); + } ZEND_HASH_FOREACH_END(); + } + + zend_opcache_static_cache_unlock(); + zend_opcache_static_cache_restore_context(previous_context); +} + +static void zend_opcache_static_cache_capture_decoded_reachable_value_ex( + zval *value, + HashTable *seen_arrays, + HashTable *seen_objects +) +{ + zend_object *obj; + zend_string *property_name; + zend_ulong key; + zval *child, *property_value, *source_value; + HashTable *ht, *properties; + + if (!zend_opcache_static_cache_capture_active || value == NULL) { + return; + } + + if (zend_opcache_static_cache_capture_handle != NULL && + zend_opcache_static_cache_capture_handle->kind == ZEND_OPCACHE_STATIC_CACHE_PERSISTENT + ) { + return; + } + + ZVAL_DEREF(value); + switch (Z_TYPE_P(value)) { + case IS_ARRAY: + ht = Z_ARRVAL_P(value); + key = (zend_ulong) (uintptr_t) ht; + + if (zend_hash_index_exists(seen_arrays, key)) { + return; + } + + zend_hash_index_add_empty_element(seen_arrays, key); + zend_opcache_static_cache_capture_decoded_value(value); + + ZEND_HASH_FOREACH_VAL(ht, child) { + zend_opcache_static_cache_capture_decoded_reachable_value_ex(child, seen_arrays, seen_objects); + } ZEND_HASH_FOREACH_END(); + + return; + case IS_OBJECT: + obj = Z_OBJ_P(value); + key = (zend_ulong) (uintptr_t) obj; + + if (zend_hash_index_exists(seen_objects, key)) { + return; + } + + zend_hash_index_add_empty_element(seen_objects, key); + zend_opcache_static_cache_capture_decoded_value(value); + if (zend_opcache_static_cache_capture_handle != NULL && + !zend_opcache_static_cache_tracks_reachable_objects(zend_opcache_static_cache_capture_handle) + ) { + return; + } + + properties = zend_get_properties_for(value, ZEND_PROP_PURPOSE_SERIALIZE); + if (properties == NULL) { + return; + } + + ZEND_HASH_FOREACH_STR_KEY_VAL(properties, property_name, property_value) { + if (property_name == NULL) { + continue; + } + source_value = Z_TYPE_P(property_value) == IS_INDIRECT ? Z_INDIRECT_P(property_value) : property_value; + zend_opcache_static_cache_capture_decoded_reachable_value_ex(source_value, seen_arrays, seen_objects); + } ZEND_HASH_FOREACH_END(); + + zend_release_properties(properties); + + return; + default: + return; + } +} + +static bool zend_opcache_static_cache_hash_mutation(HashTable *ht, bool publish) +{ + zend_opcache_static_cache_tracked_dependency *dependency; + zend_opcache_static_cache_static_slot_handle *handle; + zend_opcache_static_cache_prepare_memo_dependency *memo_dependency; + + if (publish) { + if (zend_opcache_static_cache_pending_mutation_state.dirty && + zend_opcache_static_cache_pending_mutation_state.handle != NULL && + zend_opcache_static_cache_kind_publishes_immediately( + zend_opcache_static_cache_pending_mutation_state.handle->kind) && + zend_opcache_static_cache_tracks_reachable_arrays( + zend_opcache_static_cache_pending_mutation_state.handle) && + !zend_opcache_static_cache_pending_hash_is_current_reachable_array( + zend_opcache_static_cache_pending_mutation_state.handle, + ht) + ) { + zend_opcache_static_cache_reset_pending_mutation(); + } else { + zend_opcache_static_cache_flush_pending(); + } + + return false; + } + + if (ht != NULL && zend_opcache_static_cache_has_prepare_memo_arrays && zend_opcache_static_cache_prepare_memo_arrays != NULL) { + memo_dependency = zend_hash_index_find_ptr(zend_opcache_static_cache_prepare_memo_arrays, (zend_ulong) (uintptr_t) ht); + zend_opcache_static_cache_prepare_memo_mark_dependency_dirty(memo_dependency); + } + + if (ht == NULL || !zend_opcache_static_cache_has_tracked_arrays) { + return false; + } + + dependency = zend_opcache_static_cache_find_tracked_array(ht); + if (zend_opcache_static_cache_defer_dependency_publish(dependency)) { + return false; + } + + handle = zend_opcache_static_cache_tracked_dependency_first_handle(dependency); + + if (handle == NULL) { + if (zend_opcache_static_cache_pending_mutation_state.dirty && + zend_opcache_static_cache_pending_mutation_state.handle != NULL + ) { + zend_opcache_static_cache_reset_pending_mutation(); + } + + return false; + } + + return zend_opcache_static_cache_begin_mutation(handle, ht); +} + +static void zend_opcache_static_cache_object_mutation(zend_object *obj) +{ + zend_opcache_static_cache_tracked_dependency *dependency; + zend_opcache_static_cache_static_slot_handle *handle; + zend_opcache_static_cache_prepare_memo_dependency *memo_dependency; + + if (obj != NULL && zend_opcache_static_cache_has_prepare_memo_objects && zend_opcache_static_cache_prepare_memo_objects != NULL) { + memo_dependency = zend_hash_index_find_ptr(zend_opcache_static_cache_prepare_memo_objects, (zend_ulong) (uintptr_t) obj); + zend_opcache_static_cache_prepare_memo_mark_dependency_dirty(memo_dependency); + } + + if (obj == NULL || !zend_opcache_static_cache_has_tracked_objects) { + return; + } + + dependency = zend_opcache_static_cache_find_tracked_object(obj); + if (zend_opcache_static_cache_defer_dependency_publish(dependency)) { + return; + } + + handle = zend_opcache_static_cache_tracked_dependency_first_handle(dependency); + + if (!zend_opcache_static_cache_begin_mutation(handle, NULL)) { + return; + } + + zend_opcache_static_cache_flush_pending(); +} + +static void zend_opcache_static_cache_reference_update(zend_reference *ref) +{ + zend_opcache_static_cache_static_slot_handle *handle = zend_opcache_static_cache_find_tracked_reference(ref); + + if (handle == NULL || !zend_opcache_static_cache_kind_publishes_immediately(handle->kind)) { + return; + } + + if (handle->snapshot_root_published && zend_opcache_static_cache_has_mutable_immediate_root(handle)) { + return; + } + + zend_opcache_static_cache_publish_immediate_root(handle); +} + +static void zend_opcache_static_cache_init_class(zend_class_entry *ce) +{ + zend_opcache_static_cache_context *context; + zend_opcache_static_cache_context *previous_context; + zend_opcache_static_cache_class_blob_handle *class_blob_handle; + zend_opcache_static_cache_static_slot_handle *handle; + zend_opcache_static_cache_kind kind; + zend_string *property_name, *cache_key; + zend_property_info *property_info; + zend_long ttl; + zval cached_value, *slot; + bool should_track = false, fetched_cached; + + if (!zend_opcache_static_cache_attribute_classes_initialized || ce->name == NULL || CE_STATIC_MEMBERS(ce) == NULL) { + return; + } + + if (zend_opcache_static_cache_class_blob_enabled(ce)) { + class_blob_handle = zend_opcache_static_cache_ensure_class_blob_handle(ce); + if (class_blob_handle == NULL || !zend_opcache_static_cache_context_runtime(class_blob_handle->context)->available) { + return; + } + + EG(static_cache_class_access_active) = true; + + previous_context = zend_opcache_static_cache_activate_context(class_blob_handle->context); + if (zend_opcache_static_cache_rlock()) { + if (!class_blob_handle->initialized && zend_opcache_static_cache_header_is_initialized_locked()) { + zend_opcache_static_cache_refresh_class_blob_handle_locked(class_blob_handle); + } else if (!class_blob_handle->initialized) { + zend_opcache_static_cache_build_default_class_blob_state(class_blob_handle); + } + + zend_opcache_static_cache_unlock(); + } else if (!class_blob_handle->initialized) { + zend_opcache_static_cache_build_default_class_blob_state(class_blob_handle); + } + + zend_opcache_static_cache_restore_context(previous_context); + zend_opcache_static_cache_install_class_blob_property_refs(class_blob_handle); + + return; + } + + ZEND_HASH_MAP_FOREACH_STR_KEY_PTR(&ce->properties_info, property_name, property_info) { + if (property_name == NULL) { + continue; + } + + fetched_cached = false; + + kind = zend_opcache_static_cache_static_property_kind_and_ttl(ce, property_info, &ttl); + if (kind == ZEND_OPCACHE_STATIC_CACHE_NONE) { + continue; + } + + context = zend_opcache_static_cache_context_for_kind(kind); + if (!zend_opcache_static_cache_context_runtime(context)->available) { + continue; + } + + should_track = true; + + cache_key = zend_opcache_static_cache_static_property_key(ce, property_name, kind); + if (cache_key == NULL) { + continue; + } + + previous_context = zend_opcache_static_cache_activate_context(context); + if (!zend_opcache_static_cache_rlock()) { + zend_opcache_static_cache_restore_context(previous_context); + zend_string_release(cache_key); + continue; + } + + if (!zend_opcache_static_cache_header_is_initialized_locked()) { + zend_opcache_static_cache_unlock(); + zend_opcache_static_cache_restore_context(previous_context); + zend_string_release(cache_key); + continue; + } + + slot = CE_STATIC_MEMBERS(ce) + property_info->offset; + ZVAL_DEINDIRECT(slot); + + handle = zend_opcache_static_cache_ensure_tracked_root(slot, cache_key, context, kind, ttl, NULL); + zend_opcache_static_cache_capture_begin_for_handle(handle); + if (zend_opcache_static_cache_fetch_locked(cache_key, &cached_value, false, NULL, false)) { + fetched_cached = true; + zval_ptr_dtor(slot); + ZVAL_COPY_VALUE(slot, &cached_value); + zend_opcache_static_cache_set_tracked_root_original(handle); + handle->snapshot_root_published = zend_opcache_static_cache_has_mutable_immediate_root(handle); + handle->request_deferred_publish = zend_opcache_static_cache_kind_defers_reachable_mutation_publish(kind); + } + + zend_opcache_static_cache_capture_active = false; + + if (!fetched_cached) { + zend_opcache_static_cache_capture_discard(); + } + + zend_opcache_static_cache_unlock(); + zend_opcache_static_cache_restore_context(previous_context); + zend_opcache_static_cache_register_tracked_value(slot, cache_key, kind, ttl); + zend_string_release(cache_key); + } ZEND_HASH_FOREACH_END(); + + if (should_track) { + zend_opcache_static_cache_track_attribute_class(ce); + } +} + +static void zend_opcache_static_cache_class_access(zend_class_entry *ce) +{ + zend_opcache_static_cache_class_blob_handle *class_blob_handle; + zend_opcache_static_cache_context *previous_context; + + if (ce == NULL || CE_STATIC_MEMBERS(ce) == NULL) { + return; + } + + /* Filter inline so that classes already memoized as ignored short-circuit + * without paying for an extra indirect call from the VM. */ + if (!zend_opcache_static_cache_class_access_filter(ce, &class_blob_handle)) { + return; + } + + if (class_blob_handle == NULL && zend_opcache_static_cache_class_blob_enabled(ce)) { + class_blob_handle = zend_opcache_static_cache_ensure_class_blob_handle(ce); + } + + if (class_blob_handle == NULL) { + zend_opcache_static_cache_init_class(ce); + return; + } + + if (!zend_opcache_static_cache_context_runtime(class_blob_handle->context)->available) { + return; + } + + previous_context = zend_opcache_static_cache_activate_context(class_blob_handle->context); + + if (zend_opcache_static_cache_rlock()) { + if (!class_blob_handle->initialized && zend_opcache_static_cache_header_is_initialized_locked()) { + zend_opcache_static_cache_refresh_class_blob_handle_locked(class_blob_handle); + } else if (!class_blob_handle->initialized) { + zend_opcache_static_cache_build_default_class_blob_state(class_blob_handle); + } + + zend_opcache_static_cache_unlock(); + } else if (!class_blob_handle->initialized) { + zend_opcache_static_cache_build_default_class_blob_state(class_blob_handle); + } + + zend_opcache_static_cache_restore_context(previous_context); + + zend_opcache_static_cache_install_class_blob_property_refs(class_blob_handle); +} + +static void zend_opcache_static_cache_class_update(zend_class_entry *ce) +{ + zend_opcache_static_cache_class_blob_handle *class_blob_handle; + + if (ce == NULL || CE_STATIC_MEMBERS(ce) == NULL) { + return; + } + + class_blob_handle = zend_opcache_static_cache_find_class_blob_handle(ce); + if (class_blob_handle == NULL) { + zend_opcache_static_cache_publish_persistent_static_properties_fast(ce); + + return; + } + + zend_opcache_static_cache_publish_class_blob_fast(class_blob_handle); +} + +static void zend_opcache_static_cache_init_function(zend_execute_data *execute_data) +{ + zend_opcache_static_cache_class_blob_handle *class_blob_handle; + zend_opcache_static_cache_kind kind; + zend_opcache_static_cache_context *context, *previous_context; + zend_opcache_static_cache_static_slot_handle *handle; + zend_op_array *op_array; + zend_string *var_name, *cache_key; + zend_long ttl; + zval cached_value, *slot; + HashTable *ht; + bool already_initialized = false, fetched_cached; + + if (!zend_opcache_static_cache_function_statics_initialized || + execute_data->func->type != ZEND_USER_FUNCTION + ) { + return; + } + + op_array = &execute_data->func->op_array; + /* Fast skip: if the op_array has neither function-level attributes nor a + * class scope carrying attributes, neither static-cache branch + * can fire. This avoids the per-BIND_STATIC has_attribute lookup for every + * ordinary `static $x` declaration. */ + if (op_array->attributes == NULL + && (op_array->scope == NULL || op_array->scope->attributes == NULL) + ) { + return; + } + + if (zend_opcache_static_cache_class_blob_applies_to_function_static(op_array)) { + ht = ZEND_MAP_PTR_GET(op_array->static_variables_ptr); + if (ht == NULL) { + ht = zend_array_dup(op_array->static_variables); + ZEND_MAP_PTR_SET(op_array->static_variables_ptr, ht); + } + + class_blob_handle = zend_opcache_static_cache_ensure_class_blob_handle(op_array->scope); + if (class_blob_handle == NULL || !zend_opcache_static_cache_context_runtime(class_blob_handle->context)->available) { + return; + } + + EG(static_cache_class_access_active) = true; + + previous_context = zend_opcache_static_cache_activate_context(class_blob_handle->context); + if (zend_opcache_static_cache_rlock()) { + if (!class_blob_handle->initialized && zend_opcache_static_cache_header_is_initialized_locked()) { + zend_opcache_static_cache_refresh_class_blob_handle_locked(class_blob_handle); + } else if (!class_blob_handle->initialized) { + zend_opcache_static_cache_build_default_class_blob_state(class_blob_handle); + } + + zend_opcache_static_cache_unlock(); + } else if (!class_blob_handle->initialized) { + zend_opcache_static_cache_build_default_class_blob_state(class_blob_handle); + } + + zend_opcache_static_cache_restore_context(previous_context); + + if (CE_STATIC_MEMBERS(op_array->scope) != NULL) { + zend_opcache_static_cache_install_class_blob_property_refs(class_blob_handle); + } + + zend_opcache_static_cache_bind_class_blob_method_refs(class_blob_handle, op_array, ht); + + return; + } + + if (!zend_opcache_static_cache_applies_to_function_static(op_array)) { + return; + } + + kind = zend_opcache_static_cache_op_array_kind_and_ttl(op_array, &ttl); + context = zend_opcache_static_cache_context_for_kind(kind); + if (!zend_opcache_static_cache_context_runtime(context)->available) { + return; + } + + ht = ZEND_MAP_PTR_GET(op_array->static_variables_ptr); + if (ht == NULL) { + ht = zend_array_dup(op_array->static_variables); + ZEND_MAP_PTR_SET(op_array->static_variables_ptr, ht); + } + + ZEND_HASH_FOREACH_STR_KEY_VAL(ht, var_name, slot) { + if (var_name == NULL) { + continue; + } + + if (zend_hash_index_exists(&zend_opcache_static_cache_function_statics, (zend_ulong) (uintptr_t) slot)) { + already_initialized = true; + break; + } + } ZEND_HASH_FOREACH_END(); + + if (already_initialized) { + return; + } + + previous_context = zend_opcache_static_cache_activate_context(context); + + if (!zend_opcache_static_cache_rlock()) { + zend_opcache_static_cache_restore_context(previous_context); + + return; + } + + if (!zend_opcache_static_cache_header_is_initialized_locked()) { + zend_opcache_static_cache_unlock(); + zend_opcache_static_cache_restore_context(previous_context); + + return; + } + + ZEND_HASH_FOREACH_STR_KEY_VAL(ht, var_name, slot) { + if (var_name == NULL) { + continue; + } + + fetched_cached = false; + + cache_key = zend_opcache_static_cache_function_variable_key(op_array, var_name, kind); + if (cache_key == NULL) { + continue; + } + + handle = zend_opcache_static_cache_ensure_tracked_root(slot, cache_key, context, kind, ttl, NULL); + zend_opcache_static_cache_capture_begin_for_handle(handle); + if (zend_opcache_static_cache_fetch_locked(cache_key, &cached_value, false, NULL, false)) { + fetched_cached = true; + zval_ptr_dtor(slot); + ZVAL_REF(slot, zend_opcache_static_cache_create_reference(&cached_value)); + zend_opcache_static_cache_set_tracked_root_original(handle); + handle->request_deferred_publish = zend_opcache_static_cache_kind_defers_reachable_mutation_publish(kind); + } + + zend_opcache_static_cache_capture_active = false; + + if (!fetched_cached) { + zend_opcache_static_cache_capture_discard(); + } + + zend_opcache_static_cache_track_function_static_slot(slot, cache_key, kind, ttl); + zend_opcache_static_cache_register_tracked_value(slot, cache_key, kind, ttl); + zend_string_release(cache_key); + } ZEND_HASH_FOREACH_END(); + + zend_opcache_static_cache_unlock(); + zend_opcache_static_cache_restore_context(previous_context); +} + +void zend_opcache_static_cache_update_mutation_hook_state(void) +{ + EG(tracked_mutation_hooks_active) = + zend_opcache_static_cache_has_tracked_arrays || + zend_opcache_static_cache_has_tracked_objects || + zend_opcache_static_cache_has_prepare_memo_arrays || + zend_opcache_static_cache_has_prepare_memo_objects + ; +} + +bool zend_opcache_static_cache_prepare_memo_fetch( + zval *value, + zend_opcache_static_cache_prepared_value *prepared) +{ + zend_opcache_static_cache_prepare_memo_entry *entry; + + if (value == NULL || prepared == NULL) { + return false; + } + + entry = zend_opcache_static_cache_prepare_memo_find_root_entry(value); + if (entry == NULL) { + return false; + } + + prepared->value_type = ZEND_OPCACHE_STATIC_CACHE_VALUE_SHARED_GRAPH; + prepared->value_len = entry->value_len; + prepared->payload_size = entry->payload_size; + prepared->payload_used_size = entry->payload_used_size; + prepared->payload_source = entry->payload_buffer; + + return true; +} + +void zend_opcache_static_cache_prepare_memo_store( + zval *value, + zend_opcache_static_cache_prepared_value *prepared) +{ + zend_opcache_static_cache_prepare_memo_entry *entry; + zend_ulong key; + HashTable seen_arrays, seen_objects, *roots; + + if (value == NULL || + prepared == NULL || + prepared->value_type != ZEND_OPCACHE_STATIC_CACHE_VALUE_SHARED_GRAPH || + prepared->owned_buffer == NULL + ) { + return; + } + + ZVAL_DEREF(value); + if (Z_TYPE_P(value) != IS_ARRAY && Z_TYPE_P(value) != IS_OBJECT) { + return; + } + + zend_hash_init(&seen_arrays, 8, NULL, NULL, 0); + zend_hash_init(&seen_objects, 8, NULL, NULL, 0); + if (!zend_opcache_static_cache_prepare_memo_can_track_value_recursive(value, &seen_arrays, &seen_objects)) { + zend_hash_destroy(&seen_objects); + zend_hash_destroy(&seen_arrays); + + return; + } + + zend_hash_destroy(&seen_objects); + zend_hash_destroy(&seen_arrays); + + if (zend_opcache_static_cache_prepare_memo_entries == NULL) { + zend_opcache_static_cache_prepare_memo_entries = emalloc(sizeof(HashTable)); + zend_hash_init(zend_opcache_static_cache_prepare_memo_entries, 8, NULL, zend_opcache_static_cache_prepare_memo_entry_dtor, 0); + } + + if (zend_opcache_static_cache_prepare_memo_array_roots == NULL) { + zend_opcache_static_cache_prepare_memo_array_roots = emalloc(sizeof(HashTable)); + zend_hash_init(zend_opcache_static_cache_prepare_memo_array_roots, 8, NULL, NULL, 0); + } + + if (zend_opcache_static_cache_prepare_memo_object_roots == NULL) { + zend_opcache_static_cache_prepare_memo_object_roots = emalloc(sizeof(HashTable)); + zend_hash_init(zend_opcache_static_cache_prepare_memo_object_roots, 8, NULL, NULL, 0); + } + + if (zend_opcache_static_cache_prepare_memo_arrays == NULL) { + zend_opcache_static_cache_prepare_memo_arrays = emalloc(sizeof(HashTable)); + zend_hash_init(zend_opcache_static_cache_prepare_memo_arrays, 8, NULL, zend_opcache_static_cache_prepare_memo_dependency_dtor, 0); + } + + if (zend_opcache_static_cache_prepare_memo_objects == NULL) { + zend_opcache_static_cache_prepare_memo_objects = emalloc(sizeof(HashTable)); + zend_hash_init(zend_opcache_static_cache_prepare_memo_objects, 8, NULL, zend_opcache_static_cache_prepare_memo_dependency_dtor, 0); + } + + entry = emalloc(sizeof(*entry)); + ZVAL_COPY_DEREF(&entry->root_snapshot, value); + entry->payload_buffer = prepared->owned_buffer; + entry->payload_size = prepared->payload_size; + entry->payload_used_size = prepared->payload_used_size; + entry->value_len = prepared->value_len; + entry->root_type = (uint8_t) Z_TYPE_P(value); + entry->dirty = false; + + prepared->owned_buffer = NULL; + prepared->payload_source = entry->payload_buffer; + + zend_hash_index_update_ptr( + zend_opcache_static_cache_prepare_memo_entries, + (zend_ulong) (uintptr_t) entry, + entry); + + roots = zend_opcache_static_cache_prepare_memo_root_table_for_value(value); + key = zend_opcache_static_cache_prepare_memo_root_key(value); + zend_hash_index_update_ptr(roots, key, entry); + + zend_hash_init(&seen_arrays, 8, NULL, NULL, 0); + zend_hash_init(&seen_objects, 8, NULL, NULL, 0); + + zend_opcache_static_cache_prepare_memo_track_value_recursive(value, entry, &seen_arrays, &seen_objects); + zend_hash_destroy(&seen_objects); + zend_hash_destroy(&seen_arrays); + zend_opcache_static_cache_update_mutation_hook_state(); +} + +void zend_opcache_static_cache_delete_script_keys_locked(zend_persistent_script *persistent_script) +{ + zend_class_entry *ce; + + if (persistent_script == NULL) { + return; + } + + ZEND_HASH_MAP_FOREACH_PTR(&persistent_script->script.class_table, ce) { + zend_opcache_static_cache_delete_class_keys_locked(ce); + } ZEND_HASH_FOREACH_END(); +} + +void zend_opcache_static_cache_register_hooks(void) +{ + zend_class_init_statics_hook = zend_opcache_static_cache_init_class; + zend_function_init_statics_hook = zend_opcache_static_cache_init_function; + zend_class_static_access_hook = zend_opcache_static_cache_class_access; + zend_class_static_update_hook = zend_opcache_static_cache_class_update; + zend_tracked_reference_update_hook = zend_opcache_static_cache_reference_update; + zend_tracked_hash_mutation_hook = zend_opcache_static_cache_hash_mutation; + zend_tracked_object_mutation_hook = zend_opcache_static_cache_object_mutation; +} + +void zend_opcache_static_cache_unregister_hooks(void) +{ + if (zend_class_init_statics_hook == zend_opcache_static_cache_init_class) { + zend_class_init_statics_hook = NULL; + } + + if (zend_function_init_statics_hook == zend_opcache_static_cache_init_function) { + zend_function_init_statics_hook = NULL; + } + + if (zend_class_static_access_hook == zend_opcache_static_cache_class_access) { + zend_class_static_access_hook = NULL; + } + + if (zend_class_static_update_hook == zend_opcache_static_cache_class_update) { + zend_class_static_update_hook = NULL; + } + + if (zend_tracked_reference_update_hook == zend_opcache_static_cache_reference_update) { + zend_tracked_reference_update_hook = NULL; + } + + if (zend_tracked_hash_mutation_hook == zend_opcache_static_cache_hash_mutation) { + zend_tracked_hash_mutation_hook = NULL; + } + + if (zend_tracked_object_mutation_hook == zend_opcache_static_cache_object_mutation) { + zend_tracked_object_mutation_hook = NULL; + } +} + +void zend_opcache_static_cache_request_init(void) +{ + zend_hash_init(&zend_opcache_static_cache_attribute_classes, 8, NULL, NULL, 0); + zend_opcache_static_cache_attribute_classes_initialized = true; + zend_hash_init(&zend_opcache_static_cache_ignored_classes, 8, NULL, NULL, 0); + zend_opcache_static_cache_ignored_classes_initialized = true; + zend_hash_init(&zend_opcache_static_cache_function_statics, 8, NULL, zend_opcache_static_cache_function_static_entry_dtor, 0); + zend_opcache_static_cache_function_statics_initialized = true; + zend_hash_init(&zend_opcache_static_cache_class_blob_handles, 8, NULL, zend_opcache_static_cache_class_blob_handle_dtor, 0); + zend_opcache_static_cache_class_blob_handles_initialized = true; + zend_opcache_static_cache_init_tracking(); +} + +void zend_opcache_static_cache_capture_begin_for_handle(zend_opcache_static_cache_static_slot_handle *handle) +{ + zend_opcache_static_cache_capture_clear(); + if (!zend_opcache_static_cache_tracks_reachable_mutations(handle)) { + return; + } + + zend_opcache_static_cache_capture_handle = handle; + zend_opcache_static_cache_capture_active = true; +} + +void zend_opcache_static_cache_capture_discard(void) +{ + zend_opcache_static_cache_capture_active = false; + zend_opcache_static_cache_capture_clear(); +} + +void zend_opcache_static_cache_capture_decoded_reachable_value(zval *value) +{ + HashTable seen_arrays, seen_objects; + + if (!zend_opcache_static_cache_capture_active || value == NULL) { + return; + } + + zend_hash_init(&seen_arrays, 8, NULL, NULL, 0); + zend_hash_init(&seen_objects, 8, NULL, NULL, 0); + + zend_opcache_static_cache_capture_decoded_reachable_value_ex(value, &seen_arrays, &seen_objects); + + zend_hash_destroy(&seen_objects); + zend_hash_destroy(&seen_arrays); +} + +void zend_opcache_static_cache_capture_decoded_value(zval *value) +{ + zend_ulong key; + HashTable *table; + + if (!zend_opcache_static_cache_capture_active || value == NULL) { + return; + } + + ZVAL_DEREF(value); + switch (Z_TYPE_P(value)) { + case IS_ARRAY: + table = zend_opcache_static_cache_capture_ensure_table(&zend_opcache_static_cache_capture_arrays); + key = (zend_ulong) (uintptr_t) Z_ARRVAL_P(value); + + if (zend_opcache_static_cache_capture_handle != NULL) { + zend_hash_index_update_ptr(table, key, zend_opcache_static_cache_capture_handle); + } else { + zend_hash_index_add_empty_element(table, key); + } + + zend_opcache_static_cache_capture_available = true; + + return; + case IS_OBJECT: + table = zend_opcache_static_cache_capture_ensure_table(&zend_opcache_static_cache_capture_objects); + key = (zend_ulong) (uintptr_t) Z_OBJ_P(value); + + if (zend_opcache_static_cache_capture_handle != NULL) { + zend_hash_index_update_ptr(table, key, zend_opcache_static_cache_capture_handle); + } else { + zend_hash_index_add_empty_element(table, key); + } + + zend_opcache_static_cache_capture_available = true; + + return; + default: + return; + } +} + +void zend_opcache_static_cache_request_shutdown(void) +{ + if (zend_opcache_static_cache_attribute_classes_initialized || zend_opcache_static_cache_function_statics_initialized || zend_opcache_static_cache_class_blob_handles_initialized) { + zend_opcache_static_cache_flush_pending(); + zend_opcache_static_cache_publish_context(&zend_opcache_static_cache_persistent_context_state); + zend_opcache_static_cache_publish_context(&zend_opcache_static_cache_volatile_context_state); + + if (zend_opcache_static_cache_attribute_classes_initialized) { + zend_hash_destroy(&zend_opcache_static_cache_attribute_classes); + zend_opcache_static_cache_attribute_classes_initialized = false; + } + + if (zend_opcache_static_cache_ignored_classes_initialized) { + zend_hash_destroy(&zend_opcache_static_cache_ignored_classes); + zend_opcache_static_cache_ignored_classes_initialized = false; + } + + if (zend_opcache_static_cache_function_statics_initialized) { + zend_hash_destroy(&zend_opcache_static_cache_function_statics); + zend_opcache_static_cache_function_statics_initialized = false; + } + + if (zend_opcache_static_cache_class_blob_handles_initialized) { + zend_hash_destroy(&zend_opcache_static_cache_class_blob_handles); + zend_opcache_static_cache_class_blob_handles_initialized = false; + } + } + + zend_opcache_static_cache_destroy_tracking(); +} diff --git a/ext/opcache/zend_static_cache_storage.c b/ext/opcache/zend_static_cache_storage.c new file mode 100644 index 000000000000..4b824c25a0b8 --- /dev/null +++ b/ext/opcache/zend_static_cache_storage.c @@ -0,0 +1,2141 @@ +/* + +----------------------------------------------------------------------+ + | Zend OPcache | + +----------------------------------------------------------------------+ + | Copyright © The PHP Group and Contributors. | + +----------------------------------------------------------------------+ + | This source file is subject to the Modified BSD License that is | + | bundled with this package in the file LICENSE, and is available | + | through the World Wide Web at . | + | | + | SPDX-License-Identifier: BSD-3-Clause | + +----------------------------------------------------------------------+ + | Author: Go Kudo | + +----------------------------------------------------------------------+ +*/ + +#include "zend_static_cache_internal.h" + +#ifdef ZEND_WIN32 +# include "zend_execute.h" +# include "zend_system_id.h" +# include "win32/ioutil.h" +# include +# include +# include +# include +#else +# include +# include +# include +# include +# ifdef ZTS +# include +# endif +# ifdef HAVE_UNISTD_H +# include +# endif +# if defined(__linux__) && defined(HAVE_MEMFD_CREATE) +# include +# endif +#endif + +#ifdef ZEND_WIN32 +# define ZEND_OPCACHE_STATIC_CACHE_WIN32_MAPPING_PREFIX_SIZE (2 * sizeof(void *)) +# define ZEND_OPCACHE_STATIC_CACHE_WIN32_MAPPING_NAME "ZendOPcache.StaticCache.SharedMemoryArea" +# define ZEND_OPCACHE_STATIC_CACHE_WIN32_MAPPING_MUTEX_NAME "ZendOPcache.StaticCache.SharedMemoryMutex" +# define ZEND_OPCACHE_STATIC_CACHE_WIN32_LOCK_FILE_NAME "ZendOPcache.StaticCache.LockFile" + +typedef struct _zend_opcache_static_cache_win32_segment { + zend_shared_segment segment; + HANDLE memfile; + void *mapping_base; + size_t mapping_size; +} zend_opcache_static_cache_win32_segment; + +static void zend_opcache_static_cache_win32_create_name(char *buffer, size_t buffer_size, const char *name, size_t unique_id) +{ + zend_opcache_static_cache_context *context = zend_opcache_static_cache_active_context(); + const char *sapi_name = sapi_module.name != NULL ? sapi_module.name : ""; + + snprintf( + buffer, + buffer_size, + "%s@%.32s@%.20s@%.32s@%s@%zx", + name, + accel_uname_id, + sapi_name, + zend_system_id, + context->lock_name, + unique_id + ); +} + +static bool zend_opcache_static_cache_win32_set_segment( + zend_opcache_static_cache_win32_segment *segment, + HANDLE memfile, + void *mapping_base, + size_t mapping_size, + size_t requested_size +) +{ + segment->memfile = memfile; + segment->mapping_base = mapping_base; + segment->mapping_size = mapping_size; + segment->segment.p = (char *) mapping_base + ZEND_OPCACHE_STATIC_CACHE_WIN32_MAPPING_PREFIX_SIZE; + segment->segment.pos = 0; + segment->segment.size = requested_size; + + return true; +} + +static int zend_opcache_static_cache_win32_reattach_segment( + zend_opcache_static_cache_win32_segment *segment, + HANDLE memfile, + size_t requested_size, + const char **error_in +) +{ + void *metadata_view, *wanted_mapping_base, *execute_ex_base, *mapping_base; + MEMORY_BASIC_INFORMATION info; + size_t mapping_size; + + if (requested_size > SIZE_MAX - ZEND_OPCACHE_STATIC_CACHE_WIN32_MAPPING_PREFIX_SIZE) { + *error_in = "size overflow"; + return ALLOC_FAILURE; + } + + mapping_size = requested_size + ZEND_OPCACHE_STATIC_CACHE_WIN32_MAPPING_PREFIX_SIZE; + metadata_view = MapViewOfFileEx(memfile, FILE_MAP_READ, 0, 0, ZEND_OPCACHE_STATIC_CACHE_WIN32_MAPPING_PREFIX_SIZE, NULL); + if (metadata_view == NULL) { + *error_in = "MapViewOfFileEx"; + return ALLOC_FAILURE; + } + + wanted_mapping_base = ((void **) metadata_view)[0]; + execute_ex_base = ((void **) metadata_view)[1]; + UnmapViewOfFile(metadata_view); + + if ((void *) execute_ex != execute_ex_base) { + *error_in = "execute_ex"; + return ALLOC_FAILURE; + } + + if (VirtualQuery(wanted_mapping_base, &info, sizeof(info)) == 0 || + info.State != MEM_FREE || + info.RegionSize < mapping_size + ) { + *error_in = "VirtualQuery"; + return ALLOC_FAILURE; + } + + mapping_base = MapViewOfFileEx(memfile, FILE_MAP_ALL_ACCESS, 0, 0, 0, wanted_mapping_base); + if (mapping_base == NULL) { + *error_in = "MapViewOfFileEx"; + return ALLOC_FAILURE; + } + + return zend_opcache_static_cache_win32_set_segment(segment, memfile, mapping_base, mapping_size, requested_size) + ? ALLOC_SUCCESS + : ALLOC_FAILURE; +} + +static int zend_opcache_static_cache_win32_create_segment( + zend_opcache_static_cache_win32_segment *segment, + const char *mapping_name, + size_t requested_size, + const char **error_in +) +{ + HANDLE memfile; + void *mapping_base = NULL; + size_t mapping_size; + DWORD size_high, size_low; + uint32_t index; + void *configured_mapping_base = (void *) -1; +#if defined(_WIN64) + void *mapping_base_set[] = { + (void *) 0x0000100000000000, + (void *) 0x0000200000000000, + (void *) 0x0000300000000000, + (void *) 0x0000400000000000, + (void *) 0x0000500000000000, + (void *) 0x0000600000000000, + (void *) 0x0000700000000000, + NULL, + (void *) -1 + }; +#else + void *mapping_base_set[] = { + (void *) 0x20000000, + (void *) 0x21000000, + (void *) 0x30000000, + (void *) 0x31000000, + (void *) 0x50000000, + NULL, + (void *) -1 + }; +#endif + + if (requested_size > SIZE_MAX - ZEND_OPCACHE_STATIC_CACHE_WIN32_MAPPING_PREFIX_SIZE) { + *error_in = "size overflow"; + return ALLOC_FAILURE; + } + + mapping_size = requested_size + ZEND_OPCACHE_STATIC_CACHE_WIN32_MAPPING_PREFIX_SIZE; +#if defined(_WIN64) + size_high = (DWORD) (mapping_size >> 32); + size_low = (DWORD) (mapping_size & 0xffffffff); +#else + if (mapping_size > UINT32_MAX) { + *error_in = "size overflow"; + return ALLOC_FAILURE; + } + size_high = 0; + size_low = (DWORD) mapping_size; +#endif + + memfile = CreateFileMappingA(INVALID_HANDLE_VALUE, NULL, PAGE_READWRITE | SEC_COMMIT, size_high, size_low, mapping_name); + if (memfile == NULL) { + *error_in = "CreateFileMappingA"; + return ALLOC_FAILURE; + } + + if (GetLastError() == ERROR_ALREADY_EXISTS) { + int result = zend_opcache_static_cache_win32_reattach_segment(segment, memfile, requested_size, error_in); + if (result != ALLOC_SUCCESS) { + CloseHandle(memfile); + } + + return result; + } + + if (ZCG(accel_directives).mmap_base && *ZCG(accel_directives).mmap_base) { + char *mmap_base = ZCG(accel_directives).mmap_base; + + if (mmap_base[0] == '0' && mmap_base[1] == 'x') { + mmap_base += 2; + } + if (sscanf(mmap_base, "%p", &configured_mapping_base) == 1) { + mapping_base = MapViewOfFileEx(memfile, FILE_MAP_ALL_ACCESS, 0, 0, 0, configured_mapping_base); + } + } + + for (index = 0; mapping_base == NULL && mapping_base_set[index] != (void *) -1; index++) { + mapping_base = MapViewOfFileEx(memfile, FILE_MAP_ALL_ACCESS, 0, 0, 0, mapping_base_set[index]); + } + + if (mapping_base == NULL) { + CloseHandle(memfile); + *error_in = "MapViewOfFileEx"; + return ALLOC_FAILURE; + } + + ((void **) mapping_base)[0] = mapping_base; + ((void **) mapping_base)[1] = (void *) execute_ex; + + return zend_opcache_static_cache_win32_set_segment(segment, memfile, mapping_base, mapping_size, requested_size) + ? ALLOC_SUCCESS + : ALLOC_FAILURE; +} + +static int zend_opcache_static_cache_win32_create_segments( + size_t requested_size, + zend_shared_segment ***shared_segments_p, + int *shared_segments_count, + const char **error_in +) +{ + zend_opcache_static_cache_win32_segment *segment; + HANDLE mutex = NULL, memfile = NULL; + DWORD wait_result; + char mapping_name[MAXPATHLEN], mutex_name[MAXPATHLEN]; + bool mutex_acquired = false; + int result = ALLOC_FAILURE; + + *shared_segments_count = 1; + *shared_segments_p = (zend_shared_segment **) calloc( + 1, + sizeof(zend_shared_segment *) + sizeof(zend_opcache_static_cache_win32_segment) + ); + if (*shared_segments_p == NULL) { + *error_in = "calloc"; + return ALLOC_FAILURE; + } + + segment = (zend_opcache_static_cache_win32_segment *) ((char *) *shared_segments_p + sizeof(zend_shared_segment *)); + (*shared_segments_p)[0] = (zend_shared_segment *) segment; + + zend_opcache_static_cache_win32_create_name( + mapping_name, + sizeof(mapping_name), + ZEND_OPCACHE_STATIC_CACHE_WIN32_MAPPING_NAME, + requested_size + ); + zend_opcache_static_cache_win32_create_name( + mutex_name, + sizeof(mutex_name), + ZEND_OPCACHE_STATIC_CACHE_WIN32_MAPPING_MUTEX_NAME, + requested_size + ); + + mutex = CreateMutexA(NULL, FALSE, mutex_name); + if (mutex == NULL) { + *error_in = "CreateMutexA"; + goto failure; + } + + wait_result = WaitForSingleObject(mutex, INFINITE); + if (wait_result != WAIT_OBJECT_0 && wait_result != WAIT_ABANDONED) { + *error_in = "WaitForSingleObject"; + goto failure; + } + mutex_acquired = true; + + memfile = OpenFileMappingA(FILE_MAP_ALL_ACCESS, FALSE, mapping_name); + if (memfile != NULL) { + result = zend_opcache_static_cache_win32_reattach_segment(segment, memfile, requested_size, error_in); + if (result != ALLOC_SUCCESS) { + CloseHandle(memfile); + } + } else { + result = zend_opcache_static_cache_win32_create_segment(segment, mapping_name, requested_size, error_in); + } + + if (mutex_acquired) { + ReleaseMutex(mutex); + mutex_acquired = false; + } + CloseHandle(mutex); + mutex = NULL; + + if (result == ALLOC_SUCCESS) { + return ALLOC_SUCCESS; + } + +failure: + if (mutex_acquired) { + ReleaseMutex(mutex); + } + if (mutex != NULL) { + CloseHandle(mutex); + } + free(*shared_segments_p); + *shared_segments_p = NULL; + *shared_segments_count = 0; + + return ALLOC_FAILURE; +} + +static int zend_opcache_static_cache_win32_detach_segment(zend_shared_segment *shared_segment) +{ + zend_opcache_static_cache_win32_segment *segment = (zend_opcache_static_cache_win32_segment *) shared_segment; + + if (segment->mapping_base != NULL) { + UnmapViewOfFile(segment->mapping_base); + segment->mapping_base = NULL; + } + + if (segment->memfile != NULL) { + CloseHandle(segment->memfile); + segment->memfile = NULL; + } + + return 0; +} + +static size_t zend_opcache_static_cache_win32_segment_type_size(void) +{ + return sizeof(zend_opcache_static_cache_win32_segment); +} + +static const zend_shared_memory_handlers zend_opcache_static_cache_win32_handlers = { + zend_opcache_static_cache_win32_create_segments, + zend_opcache_static_cache_win32_detach_segment, + zend_opcache_static_cache_win32_segment_type_size +}; +#endif + +static const zend_shared_memory_handler_entry zend_opcache_static_cache_handler_table[] = { +#ifdef USE_MMAP + { "mmap", &zend_alloc_mmap_handlers }, +#endif +#ifdef USE_SHM + { "shm", &zend_alloc_shm_handlers }, +#endif +#ifdef USE_SHM_OPEN + { "posix", &zend_alloc_posix_handlers }, +#endif +#ifdef ZEND_WIN32 + { "win32", &zend_opcache_static_cache_win32_handlers }, +#endif + { NULL, NULL } +}; + +#ifndef ZEND_WIN32 +static ZEND_EXT_TLS zend_ulong zend_opcache_static_cache_entry_lock_owner_pid = 0; +#ifdef ZTS +static ZEND_EXT_TLS bool zend_opcache_static_cache_entry_locks_process_is_fork_child = false; +#endif +#endif + +static zend_always_inline bool zend_opcache_static_cache_force_startup_failure(void) +{ + const char *value = getenv("OPCACHE_STATIC_CACHE_FORCE_STARTUP_FAILURE"); + + return value != NULL && value[0] != '\0' && value[0] != '0'; +} + +static zend_always_inline bool zend_opcache_static_cache_requires_pre_request_storage(void) +{ + return sapi_module.name != NULL && strcmp(sapi_module.name, "fpm-fcgi") == 0; +} + +static zend_always_inline void zend_opcache_static_cache_set_unavailable(const char *failure_reason, bool startup_failed) +{ + zend_opcache_static_cache_context *context = zend_opcache_static_cache_active_context(); + zend_opcache_static_cache_runtime *runtime = zend_opcache_static_cache_context_runtime(context); + + runtime->available = false; + runtime->startup_failed = startup_failed; + runtime->backend_initialized = context->storage.initialized; + runtime->failure_reason = failure_reason; +} + +static zend_always_inline void zend_opcache_static_cache_set_available(void) +{ + zend_opcache_static_cache_context *context = zend_opcache_static_cache_active_context(); + zend_opcache_static_cache_runtime *runtime = zend_opcache_static_cache_context_runtime(context); + + runtime->available = true; + runtime->startup_failed = false; + runtime->backend_initialized = context->storage.initialized; + runtime->failure_reason = NULL; +} + +static zend_always_inline void zend_opcache_static_cache_cleanup_segments(const zend_shared_memory_handlers *handler, zend_shared_segment **segments, int segment_count) +{ + int index; + + if (!handler || !segments) { + return; + } + + for (index = 0; index < segment_count; index++) { + if (segments[index]->p && segments[index]->p != (void *) -1) { + handler->detach_segment(segments[index]); + } + } + + free(segments); +} + +#ifdef ZTS +static void zend_opcache_static_cache_entry_locks_shutdown(zend_opcache_static_cache_storage *storage) +{ + uint32_t index; + + if (!storage->entry_locks_initialized) { + return; + } + + for (index = 0; index < ZEND_OPCACHE_STATIC_CACHE_ENTRY_LOCK_STRIPES; index++) { + if (storage->entry_locks[index] != NULL) { + tsrm_mutex_free(storage->entry_locks[index]); + storage->entry_locks[index] = NULL; + } + } + + storage->entry_locks_initialized = false; +} + +static bool zend_opcache_static_cache_entry_locks_startup(zend_opcache_static_cache_storage *storage) +{ + uint32_t index; + + if (storage->entry_locks_initialized) { + return true; + } + + for (index = 0; index < ZEND_OPCACHE_STATIC_CACHE_ENTRY_LOCK_STRIPES; index++) { + storage->entry_locks[index] = tsrm_mutex_alloc(); + if (storage->entry_locks[index] == NULL) { + zend_opcache_static_cache_entry_locks_shutdown(storage); + + return false; + } + } + storage->entry_locks_initialized = true; + + return true; +} +#endif + +#ifndef ZEND_WIN32 +static bool zend_opcache_static_cache_lock_internal(short lock_type) +{ + zend_opcache_static_cache_storage *storage = &zend_opcache_static_cache_active_context()->storage; + struct flock mem_lock; + + if (!storage->lock_initialized) { + return false; + } + +#ifdef ZTS + zend_opcache_static_cache_zts_lock_is_write = lock_type == F_WRLCK; + if (!(lock_type == F_RDLCK + ? zend_thread_rwlock_rdlock(&storage->zts_lock) + : zend_thread_rwlock_wrlock(&storage->zts_lock) + ) + ) { + zend_opcache_static_cache_zts_lock_is_write = false; + + return false; + } +#endif + + mem_lock.l_type = lock_type; + mem_lock.l_whence = SEEK_SET; + mem_lock.l_start = 0; + mem_lock.l_len = 1; + + while (fcntl(storage->lock_file, F_SETLKW, &mem_lock) == -1) { + if (errno != EINTR) { +#ifdef ZTS + if (lock_type == F_RDLCK) { + zend_thread_rwlock_unlock_rd(&storage->zts_lock); + } else { + zend_thread_rwlock_unlock_wr(&storage->zts_lock); + } + zend_opcache_static_cache_zts_lock_is_write = false; +#endif + return false; + } + } + + return true; +} + +static bool zend_opcache_static_cache_lock_startup(void) +{ + zend_opcache_static_cache_context *context = zend_opcache_static_cache_active_context(); + zend_opcache_static_cache_storage *storage = &context->storage; + int val; + + if (storage->lock_initialized) { + return true; + } + +#ifdef ZTS + if (!zend_opcache_static_cache_entry_locks_startup(storage)) { + return false; + } + + if (!zend_thread_rwlock_init(&storage->zts_lock)) { + zend_opcache_static_cache_entry_locks_shutdown(storage); + + return false; + } +#endif + +#if defined(__linux__) && defined(HAVE_MEMFD_CREATE) && defined(MFD_CLOEXEC) + storage->lock_file = memfd_create(context->lock_name, MFD_CLOEXEC); + if (storage->lock_file >= 0) { + storage->lock_initialized = true; + return true; + } +#endif + +#ifdef O_TMPFILE + storage->lock_file = open(ZCG(accel_directives).lockfile_path, O_RDWR | O_TMPFILE | O_EXCL | O_CLOEXEC, 0666); + if (storage->lock_file >= 0) { + storage->lock_initialized = true; + return true; + } +#endif + + snprintf( + storage->lockfile_name, + sizeof(storage->lockfile_name), + "%s/%sXXXXXX", + ZCG(accel_directives).lockfile_path, + context->sem_filename_prefix + ); + + storage->lock_file = mkstemp(storage->lockfile_name); + if (storage->lock_file == -1) { +#ifdef ZTS + zend_thread_rwlock_destroy(&storage->zts_lock); + zend_opcache_static_cache_entry_locks_shutdown(storage); +#endif + + return false; + } + + if (fchmod(storage->lock_file, 0666) == -1) { + close(storage->lock_file); + storage->lock_file = -1; +#ifdef ZTS + zend_thread_rwlock_destroy(&storage->zts_lock); + zend_opcache_static_cache_entry_locks_shutdown(storage); +#endif + return false; + } + + val = fcntl(storage->lock_file, F_GETFD, 0); + val |= FD_CLOEXEC; + fcntl(storage->lock_file, F_SETFD, val); + unlink(storage->lockfile_name); + + storage->lock_initialized = true; + + return true; +} + +static void zend_opcache_static_cache_lock_shutdown(void) +{ + zend_opcache_static_cache_storage *storage = &zend_opcache_static_cache_active_context()->storage; + + if (!storage->lock_initialized) { + return; + } + + if (storage->lock_file >= 0) { + close(storage->lock_file); + storage->lock_file = -1; + } +#ifdef ZTS + zend_thread_rwlock_destroy(&storage->zts_lock); + zend_opcache_static_cache_entry_locks_shutdown(storage); +#endif + storage->lock_initialized = false; +} + +static bool zend_opcache_static_cache_rlock_impl(void) +{ + return zend_opcache_static_cache_lock_internal(F_RDLCK); +} + +static bool zend_opcache_static_cache_wlock_impl(void) +{ + return zend_opcache_static_cache_lock_internal(F_WRLCK); +} + +static void zend_opcache_static_cache_unlock_impl(void) +{ + zend_opcache_static_cache_storage *storage = &zend_opcache_static_cache_active_context()->storage; + struct flock mem_unlock; +#ifdef ZTS + bool zts_lock_is_write = zend_opcache_static_cache_zts_lock_is_write; +#endif + + if (!storage->lock_initialized) { + return; + } + + mem_unlock.l_type = F_UNLCK; + mem_unlock.l_whence = SEEK_SET; + mem_unlock.l_start = 0; + mem_unlock.l_len = 1; + fcntl(storage->lock_file, F_SETLK, &mem_unlock); + +#ifdef ZTS + if (zts_lock_is_write) { + zend_thread_rwlock_unlock_wr(&storage->zts_lock); + } else { + zend_thread_rwlock_unlock_rd(&storage->zts_lock); + } + zend_opcache_static_cache_zts_lock_is_write = false; +#endif +} +#else +static bool zend_opcache_static_cache_win32_open_lock_file_at(zend_opcache_static_cache_storage *storage, const char *directory, const char *base_name) +{ + size_t directory_len; + const char *separator; + + if (directory == NULL || directory[0] == '\0') { + return false; + } + + directory_len = strlen(directory); + separator = directory[directory_len - 1] == '/' || directory[directory_len - 1] == '\\' + ? "" + : "/" + ; + + snprintf( + storage->lockfile_name, + sizeof(storage->lockfile_name), + "%s%s%s.lock", + directory, + separator, + base_name + ); + + storage->lock_file = php_win32_ioutil_open(storage->lockfile_name, O_RDWR | O_CREAT | O_BINARY, 0666); + + return storage->lock_file >= 0; +} + +static bool zend_opcache_static_cache_win32_open_lock_file(zend_opcache_static_cache_context *context) +{ + zend_opcache_static_cache_storage *storage = &context->storage; + char base_name[MAXPATHLEN], temp_path[MAXPATHLEN]; + DWORD temp_path_len; + + zend_opcache_static_cache_win32_create_name( + base_name, + sizeof(base_name), + ZEND_OPCACHE_STATIC_CACHE_WIN32_LOCK_FILE_NAME, + storage->size + ); + + temp_path_len = GetTempPathA(sizeof(temp_path), temp_path); + if (temp_path_len == 0 || temp_path_len >= sizeof(temp_path)) { + return false; + } + + return zend_opcache_static_cache_win32_open_lock_file_at(storage, temp_path, base_name); +} + +static bool zend_opcache_static_cache_win32_lock_range(zend_opcache_static_cache_storage *storage, DWORD offset, bool exclusive, bool blocking) +{ + OVERLAPPED overlapped; + HANDLE file_handle; + DWORD flags = 0; + + if (!storage->lock_initialized || storage->lock_file < 0) { + return false; + } + + file_handle = (HANDLE) _get_osfhandle(storage->lock_file); + if (file_handle == INVALID_HANDLE_VALUE) { + return false; + } + + memset(&overlapped, 0, sizeof(overlapped)); + overlapped.Offset = offset; + + if (exclusive) { + flags |= LOCKFILE_EXCLUSIVE_LOCK; + } + if (!blocking) { + flags |= LOCKFILE_FAIL_IMMEDIATELY; + } + + return LockFileEx(file_handle, flags, 0, 1, 0, &overlapped) == TRUE; +} + +static void zend_opcache_static_cache_win32_unlock_range(zend_opcache_static_cache_storage *storage, DWORD offset) +{ + OVERLAPPED overlapped; + HANDLE file_handle; + + if (!storage->lock_initialized || storage->lock_file < 0) { + return; + } + + file_handle = (HANDLE) _get_osfhandle(storage->lock_file); + if (file_handle == INVALID_HANDLE_VALUE) { + return; + } + + memset(&overlapped, 0, sizeof(overlapped)); + overlapped.Offset = offset; + UnlockFileEx(file_handle, 0, 1, 0, &overlapped); +} + +static bool zend_opcache_static_cache_lock_internal(bool exclusive) +{ + zend_opcache_static_cache_storage *storage = &zend_opcache_static_cache_active_context()->storage; + + if (!storage->lock_initialized) { + return false; + } + +#ifdef ZTS + zend_opcache_static_cache_zts_lock_is_write = exclusive; + if (!(exclusive + ? zend_thread_rwlock_wrlock(&storage->zts_lock) + : zend_thread_rwlock_rdlock(&storage->zts_lock) + ) + ) { + zend_opcache_static_cache_zts_lock_is_write = false; + + return false; + } +#endif + + if (!zend_opcache_static_cache_win32_lock_range(storage, 0, exclusive, true)) { +#ifdef ZTS + if (exclusive) { + zend_thread_rwlock_unlock_wr(&storage->zts_lock); + } else { + zend_thread_rwlock_unlock_rd(&storage->zts_lock); + } + zend_opcache_static_cache_zts_lock_is_write = false; +#endif + return false; + } + + return true; +} + +static bool zend_opcache_static_cache_lock_startup(void) +{ + zend_opcache_static_cache_context *context = zend_opcache_static_cache_active_context(); + zend_opcache_static_cache_storage *storage = &context->storage; + + if (storage->lock_initialized) { + return true; + } + +#ifdef ZTS + if (!zend_opcache_static_cache_entry_locks_startup(storage)) { + return false; + } + + if (!zend_thread_rwlock_init(&storage->zts_lock)) { + zend_opcache_static_cache_entry_locks_shutdown(storage); + + return false; + } +#endif + + if (!zend_opcache_static_cache_win32_open_lock_file(context)) { +#ifdef ZTS + zend_thread_rwlock_destroy(&storage->zts_lock); + zend_opcache_static_cache_entry_locks_shutdown(storage); +#endif + return false; + } + + storage->lock_initialized = true; + + return true; +} + +static void zend_opcache_static_cache_lock_shutdown(void) +{ + zend_opcache_static_cache_storage *storage = &zend_opcache_static_cache_active_context()->storage; + + if (!storage->lock_initialized) { + return; + } + + if (storage->lock_file >= 0) { + php_win32_ioutil_close(storage->lock_file); + storage->lock_file = -1; + } +#ifdef ZTS + zend_thread_rwlock_destroy(&storage->zts_lock); + zend_opcache_static_cache_entry_locks_shutdown(storage); +#endif + storage->lock_initialized = false; +} + +static bool zend_opcache_static_cache_rlock_impl(void) +{ + return zend_opcache_static_cache_lock_internal(false); +} + +static bool zend_opcache_static_cache_wlock_impl(void) +{ + return zend_opcache_static_cache_lock_internal(true); +} + +static void zend_opcache_static_cache_unlock_impl(void) +{ + zend_opcache_static_cache_storage *storage = &zend_opcache_static_cache_active_context()->storage; +#ifdef ZTS + bool zts_lock_is_write = zend_opcache_static_cache_zts_lock_is_write; +#endif + + if (!storage->lock_initialized) { + return; + } + + zend_opcache_static_cache_win32_unlock_range(storage, 0); + +#ifdef ZTS + if (zts_lock_is_write) { + zend_thread_rwlock_unlock_wr(&storage->zts_lock); + } else { + zend_thread_rwlock_unlock_rd(&storage->zts_lock); + } + zend_opcache_static_cache_zts_lock_is_write = false; +#endif +} +#endif + +static HashTable **zend_opcache_static_cache_entry_locks_ptr_for_context(zend_opcache_static_cache_context *context) +{ + return context == &zend_opcache_static_cache_persistent_context_state + ? &zend_opcache_static_cache_persistent_entry_locks + : &zend_opcache_static_cache_volatile_entry_locks + ; +} + +static uint32_t *zend_opcache_static_cache_entry_lock_counts_for_context(zend_opcache_static_cache_context *context) +{ + return context == &zend_opcache_static_cache_persistent_context_state + ? zend_opcache_static_cache_persistent_entry_lock_counts + : zend_opcache_static_cache_volatile_entry_lock_counts + ; +} + +static uint32_t zend_opcache_static_cache_entry_lock_stripe(zend_string *key) +{ + return (uint32_t) (zend_string_hash_val(key) % ZEND_OPCACHE_STATIC_CACHE_ENTRY_LOCK_STRIPES); +} + +#if defined(ZTS) && !defined(ZEND_WIN32) +static void zend_opcache_static_cache_entry_locks_reinit_after_fork(zend_opcache_static_cache_storage *storage) +{ + uint32_t index, allocated = 0; + MUTEX_T old_locks[ZEND_OPCACHE_STATIC_CACHE_ENTRY_LOCK_STRIPES]; + MUTEX_T new_locks[ZEND_OPCACHE_STATIC_CACHE_ENTRY_LOCK_STRIPES]; + + if (!storage->entry_locks_initialized) { + return; + } + + memcpy(old_locks, storage->entry_locks, sizeof(old_locks)); + + /* A child process may inherit a process-local heap mutex whose copied + * state says it was locked by the parent. Replace the child-side copies + * instead of trying to unlock or reuse them. */ + for (index = 0; index < ZEND_OPCACHE_STATIC_CACHE_ENTRY_LOCK_STRIPES; index++) { + new_locks[index] = tsrm_mutex_alloc(); + if (new_locks[index] == NULL) { + while (allocated != 0) { + tsrm_mutex_free(new_locks[--allocated]); + } + for (index = 0; index < ZEND_OPCACHE_STATIC_CACHE_ENTRY_LOCK_STRIPES; index++) { + if (old_locks[index] != NULL) { + tsrm_mutex_free(old_locks[index]); + } + } + memset(storage->entry_locks, 0, sizeof(storage->entry_locks)); + storage->entry_locks_initialized = false; + + return; + } + allocated++; + } + + for (index = 0; index < ZEND_OPCACHE_STATIC_CACHE_ENTRY_LOCK_STRIPES; index++) { + if (old_locks[index] != NULL) { + tsrm_mutex_free(old_locks[index]); + } + } + + memcpy(storage->entry_locks, new_locks, sizeof(storage->entry_locks)); +} +#endif + +static void zend_opcache_static_cache_release_entry_lock_context(HashTable **locks_ptr, uint32_t *counts) +{ + if (*locks_ptr == NULL) { + return; + } + + zend_hash_destroy(*locks_ptr); + FREE_HASHTABLE(*locks_ptr); + *locks_ptr = NULL; + memset(counts, 0, sizeof(uint32_t) * ZEND_OPCACHE_STATIC_CACHE_ENTRY_LOCK_STRIPES); +} + +static void zend_opcache_static_cache_ensure_entry_lock_process(void) +{ +#ifndef ZEND_WIN32 + zend_ulong current_pid = (zend_ulong) getpid(); + + if (zend_opcache_static_cache_entry_lock_owner_pid == 0) { + zend_opcache_static_cache_entry_lock_owner_pid = current_pid; + + return; + } + + if (zend_opcache_static_cache_entry_lock_owner_pid == current_pid) { + return; + } + + /* The request-local lock tables were inherited across fork. Destroy the + * child copies without unlocking stripes owned by the parent process. */ + zend_opcache_static_cache_release_entry_lock_context( + &zend_opcache_static_cache_volatile_entry_locks, + zend_opcache_static_cache_volatile_entry_lock_counts); + zend_opcache_static_cache_release_entry_lock_context( + &zend_opcache_static_cache_persistent_entry_locks, + zend_opcache_static_cache_persistent_entry_lock_counts); +#ifdef ZTS + zend_opcache_static_cache_entry_locks_reinit_after_fork(&zend_opcache_static_cache_volatile_context_state.storage); + zend_opcache_static_cache_entry_locks_reinit_after_fork(&zend_opcache_static_cache_persistent_context_state.storage); + zend_opcache_static_cache_entry_locks_process_is_fork_child = true; +#endif + zend_opcache_static_cache_entry_lock_owner_pid = current_pid; +#endif +} + +#ifndef ZEND_WIN32 +static bool zend_opcache_static_cache_lock_entry_stripe_ex(zend_opcache_static_cache_context *context, uint32_t stripe, bool blocking) +{ + zend_opcache_static_cache_storage *storage = &context->storage; + uint32_t *counts = zend_opcache_static_cache_entry_lock_counts_for_context(context); + struct flock mem_lock; +#ifdef ZTS + int result; +#endif + + if (counts[stripe] != 0) { + counts[stripe]++; + + return true; + } + +#ifdef ZTS + if (!storage->entry_locks_initialized && !zend_opcache_static_cache_entry_locks_startup(storage)) { + return false; + } + + if (storage->entry_locks[stripe] == NULL) { + return false; + } + + result = blocking + ? pthread_mutex_lock(storage->entry_locks[stripe]) + : pthread_mutex_trylock(storage->entry_locks[stripe]) + ; + if (result != 0) { + return false; + } +#endif + + if (!storage->lock_initialized || storage->lock_file < 0) { +#ifdef ZTS + pthread_mutex_unlock(storage->entry_locks[stripe]); +#endif + return false; + } + + mem_lock.l_type = F_WRLCK; + mem_lock.l_whence = SEEK_SET; + mem_lock.l_start = (off_t) stripe + 1; + mem_lock.l_len = 1; + + while (fcntl(storage->lock_file, blocking ? F_SETLKW : F_SETLK, &mem_lock) == -1) { + if (errno != EINTR) { +#ifdef ZTS + pthread_mutex_unlock(storage->entry_locks[stripe]); +#endif + return false; + } + } + + counts[stripe] = 1; + + return true; +} + +static bool zend_opcache_static_cache_lock_entry_stripe(zend_opcache_static_cache_context *context, uint32_t stripe) +{ + return zend_opcache_static_cache_lock_entry_stripe_ex(context, stripe, true); +} + +static bool zend_opcache_static_cache_try_lock_entry_stripe(zend_opcache_static_cache_context *context, uint32_t stripe) +{ + return zend_opcache_static_cache_lock_entry_stripe_ex(context, stripe, false); +} + +static void zend_opcache_static_cache_unlock_entry_stripe(zend_opcache_static_cache_context *context, uint32_t stripe) +{ + zend_opcache_static_cache_storage *storage = &context->storage; + uint32_t *counts = zend_opcache_static_cache_entry_lock_counts_for_context(context); + struct flock mem_unlock; + + ZEND_ASSERT(counts[stripe] != 0); + if (--counts[stripe] != 0) { + return; + } + + if (storage->lock_initialized && storage->lock_file >= 0) { + mem_unlock.l_type = F_UNLCK; + mem_unlock.l_whence = SEEK_SET; + mem_unlock.l_start = (off_t) stripe + 1; + mem_unlock.l_len = 1; + fcntl(storage->lock_file, F_SETLK, &mem_unlock); + } + +#ifdef ZTS + if (storage->entry_locks_initialized && storage->entry_locks[stripe] != NULL) { + tsrm_mutex_unlock(storage->entry_locks[stripe]); + } +#endif +} +#else +static bool zend_opcache_static_cache_lock_entry_stripe_ex(zend_opcache_static_cache_context *context, uint32_t stripe, bool blocking) +{ + zend_opcache_static_cache_storage *storage = &context->storage; + uint32_t *counts = zend_opcache_static_cache_entry_lock_counts_for_context(context); + + if (counts[stripe] != 0) { + counts[stripe]++; + + return true; + } + +#ifdef ZTS + if (!storage->entry_locks_initialized && !zend_opcache_static_cache_entry_locks_startup(storage)) { + return false; + } + + if (storage->entry_locks[stripe] == NULL) { + return false; + } + + if (blocking) { + if (tsrm_mutex_lock(storage->entry_locks[stripe]) != 0) { + return false; + } + } else if (!TryEnterCriticalSection(storage->entry_locks[stripe])) { + return false; + } +#endif + + if (!zend_opcache_static_cache_win32_lock_range(storage, stripe + 1, true, blocking)) { +#ifdef ZTS + tsrm_mutex_unlock(storage->entry_locks[stripe]); +#endif + return false; + } + + counts[stripe] = 1; + + return true; +} + +static bool zend_opcache_static_cache_lock_entry_stripe(zend_opcache_static_cache_context *context, uint32_t stripe) +{ + return zend_opcache_static_cache_lock_entry_stripe_ex(context, stripe, true); +} + +static bool zend_opcache_static_cache_try_lock_entry_stripe(zend_opcache_static_cache_context *context, uint32_t stripe) +{ + return zend_opcache_static_cache_lock_entry_stripe_ex(context, stripe, false); +} + +static void zend_opcache_static_cache_unlock_entry_stripe(zend_opcache_static_cache_context *context, uint32_t stripe) +{ + zend_opcache_static_cache_storage *storage = &context->storage; + uint32_t *counts = zend_opcache_static_cache_entry_lock_counts_for_context(context); + + ZEND_ASSERT(counts[stripe] != 0); + if (--counts[stripe] != 0) { + return; + } + + zend_opcache_static_cache_win32_unlock_range(storage, stripe + 1); + +#ifdef ZTS + if (storage->entry_locks_initialized && storage->entry_locks[stripe] != NULL) { + tsrm_mutex_unlock(storage->entry_locks[stripe]); + } +#endif +} +#endif + +static void zend_opcache_static_cache_entry_lock_dtor(zval *lock_zv) +{ + zend_opcache_static_cache_entry_lock *lock = Z_PTR_P(lock_zv); + + if (lock == NULL) { + return; + } + +#ifdef ZEND_WIN32 + zend_opcache_static_cache_unlock_entry_stripe(lock->context, lock->stripe); +#else + if (lock->owner_pid == (zend_ulong) getpid()) { + zend_opcache_static_cache_unlock_entry_stripe(lock->context, lock->stripe); + } +#endif + efree(lock); +} + +static HashTable *zend_opcache_static_cache_entry_locks(zend_opcache_static_cache_context *context) +{ + HashTable **locks_ptr = zend_opcache_static_cache_entry_locks_ptr_for_context(context); + + if (*locks_ptr == NULL) { + ALLOC_HASHTABLE(*locks_ptr); + zend_hash_init(*locks_ptr, 0, NULL, zend_opcache_static_cache_entry_lock_dtor, 0); + } + + return *locks_ptr; +} + +static uint32_t zend_opcache_static_cache_calculate_capacity(size_t size) +{ + size_t capacity = size / ZEND_OPCACHE_STATIC_CACHE_SLOT_BYTES, data_offset; + + if (capacity < ZEND_OPCACHE_STATIC_CACHE_MIN_CAPACITY) { + capacity = ZEND_OPCACHE_STATIC_CACHE_MIN_CAPACITY; + } else if (capacity > ZEND_OPCACHE_STATIC_CACHE_MAX_CAPACITY) { + capacity = ZEND_OPCACHE_STATIC_CACHE_MAX_CAPACITY; + } + + if ((capacity & 1) == 0) { + capacity--; + } + + for (;;) { + data_offset = ZEND_ALIGNED_SIZE(sizeof(zend_opcache_static_cache_header) + capacity * sizeof(zend_opcache_static_cache_entry)); + if (data_offset < size || capacity == ZEND_OPCACHE_STATIC_CACHE_MIN_CAPACITY) { + break; + } + + capacity >>= 1; + if (capacity < ZEND_OPCACHE_STATIC_CACHE_MIN_CAPACITY) { + capacity = ZEND_OPCACHE_STATIC_CACHE_MIN_CAPACITY; + } + + if ((capacity & 1) == 0) { + capacity--; + } + } + + return (uint32_t) capacity; +} + +static uint32_t zend_opcache_static_cache_used_end_offset_locked(const zend_opcache_static_cache_header *header) +{ + return header->data_offset + header->next_free; +} + +static void zend_opcache_static_cache_free_list_remove_locked(zend_opcache_static_cache_header *header, uint32_t block_offset) +{ + zend_opcache_static_cache_block *block = zend_opcache_static_cache_block_ptr(block_offset); + + if (block->prev_free != 0) { + zend_opcache_static_cache_block_ptr(block->prev_free)->next_free = block->next_free; + } else { + header->free_list = block->next_free; + } + + if (block->next_free != 0) { + zend_opcache_static_cache_block_ptr(block->next_free)->prev_free = block->prev_free; + } + + block->next_free = 0; + block->prev_free = 0; + block->flags &= ~ZEND_OPCACHE_STATIC_CACHE_BLOCK_FREE; +} + +static void zend_opcache_static_cache_free_list_insert_locked(zend_opcache_static_cache_header *header, uint32_t block_offset) +{ + zend_opcache_static_cache_block *block = zend_opcache_static_cache_block_ptr(block_offset); + + block->prev_free = 0; + block->next_free = header->free_list; + + if (header->free_list != 0) { + zend_opcache_static_cache_block_ptr(header->free_list)->prev_free = block_offset; + } + + zend_opcache_static_cache_block_mark_free(block); + header->free_list = block_offset; +} + +static void zend_opcache_static_cache_update_following_prev_size_locked( + zend_opcache_static_cache_header *header, + uint32_t block_offset, + const zend_opcache_static_cache_block *block +) +{ + uint32_t next_offset = block_offset + block->size; + + if (next_offset < zend_opcache_static_cache_used_end_offset_locked(header)) { + zend_opcache_static_cache_block_ptr(next_offset)->prev_size = block->size; + } +} + +static void zend_opcache_static_cache_trim_tail_free_blocks_locked( + zend_opcache_static_cache_header *header, + uint32_t block_offset +) +{ + zend_opcache_static_cache_block *block = zend_opcache_static_cache_block_ptr(block_offset); + uint32_t prev_offset; + + while (block_offset >= header->data_offset && + header->last_block_offset == block_offset && + zend_opcache_static_cache_block_is_free(block) && + block_offset + block->size == zend_opcache_static_cache_used_end_offset_locked(header) + ) { + prev_offset = 0; + zend_opcache_static_cache_free_list_remove_locked(header, block_offset); + header->next_free -= block->size; + if (block->prev_size != 0 && block_offset > header->data_offset) { + prev_offset = block_offset - block->prev_size; + } + + header->last_block_offset = prev_offset; + if (prev_offset == 0) { + break; + } + + block_offset = prev_offset; + block = zend_opcache_static_cache_block_ptr(block_offset); + } +} + +static bool zend_opcache_static_cache_try_handler(const zend_shared_memory_handler_entry *handler_entry) +{ + const char *error_in = NULL; + zend_opcache_static_cache_context *context = zend_opcache_static_cache_active_context(); + zend_opcache_static_cache_runtime *runtime = zend_opcache_static_cache_context_runtime(context); + zend_opcache_static_cache_storage *storage = &context->storage; + zend_shared_segment **segments = NULL; + int segment_count = 0, result; + + result = handler_entry->handler->create_segments( + runtime->configured_memory, + &segments, + &segment_count, + &error_in + ); + if (result != ALLOC_SUCCESS) { + zend_opcache_static_cache_cleanup_segments(handler_entry->handler, segments, segment_count); + return false; + } + + storage->handler = handler_entry->handler; + storage->segments = segments; + storage->segment_count = segment_count; + storage->size = runtime->configured_memory; + storage->model = handler_entry->name; + storage->initialized = true; + + return true; +} + +static bool zend_opcache_static_cache_startup_storage(void) +{ + const zend_shared_memory_handler_entry *handler_entry; + const char *requested_model = ZCG(accel_directives).memory_model; + zend_opcache_static_cache_storage *storage = &zend_opcache_static_cache_active_context()->storage; + + if (storage->initialized) { + return true; + } + + if (requested_model && requested_model[0]) { + if (strcmp(requested_model, "cgi") == 0) { + requested_model = "shm"; + } + + for (handler_entry = zend_opcache_static_cache_handler_table; handler_entry->name; handler_entry++) { + if (strcmp(requested_model, handler_entry->name) == 0 && zend_opcache_static_cache_try_handler(handler_entry)) { + goto storage_ready; + } + } + } + + for (handler_entry = zend_opcache_static_cache_handler_table; handler_entry->name; handler_entry++) { + if (requested_model && requested_model[0] && strcmp(requested_model, handler_entry->name) == 0) { + continue; + } + + if (zend_opcache_static_cache_try_handler(handler_entry)) { + goto storage_ready; + } + } + + return false; + +storage_ready: + if (storage->segment_count != 1) { + zend_opcache_static_cache_cleanup_segments( + storage->handler, + storage->segments, + storage->segment_count + ); + zend_opcache_static_cache_reset_storage(); + + return false; + } + + if (!zend_opcache_static_cache_lock_startup()) { + zend_opcache_static_cache_cleanup_segments( + storage->handler, + storage->segments, + storage->segment_count + ); + zend_opcache_static_cache_reset_storage(); + + return false; + } + + if (!zend_opcache_static_cache_wlock()) { + zend_opcache_static_cache_lock_shutdown(); + zend_opcache_static_cache_cleanup_segments( + storage->handler, + storage->segments, + storage->segment_count + ); + zend_opcache_static_cache_reset_storage(); + + return false; + } + + if (!zend_opcache_static_cache_header_init_locked()) { + zend_opcache_static_cache_unlock(); + zend_opcache_static_cache_lock_shutdown(); + zend_opcache_static_cache_cleanup_segments( + storage->handler, + storage->segments, + storage->segment_count + ); + zend_opcache_static_cache_reset_storage(); + + return false; + } + + zend_opcache_static_cache_unlock(); + + return true; +} + +static bool zend_opcache_static_cache_payload_size_to_block_size(size_t size, uint32_t *block_size) +{ + size_t aligned_size; + + if (size == 0 || size > UINT32_MAX - sizeof(zend_opcache_static_cache_block)) { + return false; + } + + aligned_size = ZEND_ALIGNED_SIZE(sizeof(zend_opcache_static_cache_block) + size); + if (aligned_size > UINT32_MAX) { + return false; + } + + *block_size = (uint32_t) aligned_size; + + return true; +} + +static bool zend_opcache_static_cache_offset_in_block(uint32_t offset, uint32_t block_offset, uint32_t block_size) +{ + return offset >= block_offset + sizeof(zend_opcache_static_cache_block) && offset < block_offset + block_size; +} + +static bool zend_opcache_static_cache_block_is_movable_locked( + zend_opcache_static_cache_header *header, + uint32_t block_offset, + uint32_t block_size +) +{ + zend_opcache_static_cache_entry *entries, *entry; + uint32_t index; + bool referenced = false; + + entries = zend_opcache_static_cache_entries(header); + for (index = 0; index < header->capacity; index++) { + entry = &entries[index]; + if (entry->state != ZEND_OPCACHE_STATIC_CACHE_ENTRY_USED) { + continue; + } + + if (entry->key_offset != 0 && + zend_opcache_static_cache_offset_in_block(entry->key_offset, block_offset, block_size) + ) { + referenced = true; + } + + if (entry->value_offset != 0 && + zend_opcache_static_cache_offset_in_block(entry->value_offset, block_offset, block_size) + ) { + referenced = true; + if (entry->value_type == ZEND_OPCACHE_STATIC_CACHE_VALUE_SHARED_GRAPH) { + /* Shared graphs may contain final-buffer pointers, so keep their block anchored. */ + return false; + } + } + } + + return referenced; +} + +static void zend_opcache_static_cache_update_moved_block_entries_locked( + zend_opcache_static_cache_header *header, + uint32_t old_block_offset, + uint32_t new_block_offset, + uint32_t block_size +) +{ + zend_opcache_static_cache_entry *entries, *entry; + uint32_t index, delta; + + ZEND_ASSERT(new_block_offset <= old_block_offset); + delta = old_block_offset - new_block_offset; + entries = zend_opcache_static_cache_entries(header); + for (index = 0; index < header->capacity; index++) { + entry = &entries[index]; + if (entry->state != ZEND_OPCACHE_STATIC_CACHE_ENTRY_USED) { + continue; + } + + if (entry->key_offset != 0 && + zend_opcache_static_cache_offset_in_block(entry->key_offset, old_block_offset, block_size) + ) { + entry->key_offset -= delta; + } + + if (entry->value_offset != 0 && + zend_opcache_static_cache_offset_in_block(entry->value_offset, old_block_offset, block_size) + ) { + entry->value_offset -= delta; + } + } +} + +static bool zend_opcache_static_cache_compaction_can_fit_locked( + zend_opcache_static_cache_header *header, + uint32_t required_block_size +) +{ + zend_opcache_static_cache_block *block; + uint32_t data_end, used_end, offset, next_offset, block_size, region_start, region_used_size, write_offset, max_free_size = 0, region_free_size; + bool movable, would_move = false; + + data_end = header->data_offset + header->data_size; + used_end = header->data_offset + header->next_free; + offset = header->data_offset; + region_start = header->data_offset; + region_used_size = 0; + write_offset = header->data_offset; + + while (offset < used_end) { + block = zend_opcache_static_cache_block_ptr(offset); + block_size = block->size; + if (block_size < ZEND_ALIGNED_SIZE(sizeof(zend_opcache_static_cache_block) + 1) || + block_size > used_end - offset + ) { + return false; + } + + next_offset = offset + block_size; + if (!zend_opcache_static_cache_block_is_free(block)) { + movable = zend_opcache_static_cache_block_is_movable_locked(header, offset, block_size); + if (movable) { + if (offset != write_offset) { + would_move = true; + } + region_used_size += block_size; + write_offset += block_size; + } else { + if (offset - region_start < region_used_size) { + return false; + } + + region_free_size = offset - region_start - region_used_size; + + if (region_free_size > max_free_size) { + max_free_size = region_free_size; + } + + region_start = next_offset; + region_used_size = 0; + write_offset = next_offset; + } + } + + offset = next_offset; + } + + if (data_end - region_start < region_used_size) { + return false; + } + + if (data_end - region_start - region_used_size > max_free_size) { + max_free_size = data_end - region_start - region_used_size; + } + + return would_move && max_free_size >= required_block_size; +} + +void zend_opcache_static_cache_reset_runtime(void) +{ + zend_opcache_static_cache_context *context = zend_opcache_static_cache_active_context(); + zend_opcache_static_cache_runtime *runtime = zend_opcache_static_cache_context_runtime(context); + + memset(runtime, 0, sizeof(*runtime)); + + runtime->configured_memory = context == &zend_opcache_static_cache_persistent_context_state + ? ZCG(accel_directives).static_cache_persistent_size_mb + : ZCG(accel_directives).static_cache_volatile_size_mb + ; + runtime->enabled = runtime->configured_memory != 0; + + if (zend_opcache_static_cache_subsystem_disabled) { + runtime->enabled = false; + runtime->available = false; + runtime->startup_failed = true; + runtime->backend_initialized = context->storage.initialized; + runtime->failure_reason = zend_opcache_static_cache_subsystem_failure_reason; + } +} + +void zend_opcache_static_cache_reset_storage(void) +{ + zend_opcache_static_cache_storage *storage = &zend_opcache_static_cache_active_context()->storage; + + memset(storage, 0, sizeof(*storage)); + storage->lock_file = -1; +} + +bool zend_opcache_static_cache_header_init_locked(void) +{ + zend_opcache_static_cache_storage *storage = &zend_opcache_static_cache_active_context()->storage; + zend_opcache_static_cache_header *header = zend_opcache_static_cache_header_ptr(); + uint32_t capacity, data_offset; + + if (!header) { + return false; + } + + if (header->magic == ZEND_OPCACHE_STATIC_CACHE_MAGIC && header->version == ZEND_OPCACHE_STATIC_CACHE_VERSION) { + return true; + } + + capacity = zend_opcache_static_cache_calculate_capacity(storage->size); + data_offset = (uint32_t) ZEND_ALIGNED_SIZE(sizeof(zend_opcache_static_cache_header) + capacity * sizeof(zend_opcache_static_cache_entry)); + if (data_offset >= storage->size) { + return false; + } + + /* Only the header and entry table need an eager zero state. Payload pages + * are touched on demand when allocator blocks are first created. */ + memset(header, 0, data_offset); + header->capacity = capacity; + header->data_offset = data_offset; + header->data_size = (uint32_t) (storage->size - data_offset); + header->next_free = 0; + header->free_list = 0; + header->last_block_offset = 0; + header->count = 0; + header->mutation_epoch = 1; + header->magic = ZEND_OPCACHE_STATIC_CACHE_MAGIC; + header->version = ZEND_OPCACHE_STATIC_CACHE_VERSION; + + return true; +} + +void zend_opcache_static_cache_free_locked(uint32_t payload_offset) +{ + zend_opcache_static_cache_header *header = zend_opcache_static_cache_header_ptr(); + zend_opcache_static_cache_block *block, *adjacent; + uint32_t block_offset, original_block_offset, next_offset, prev_offset; + + if (!header || payload_offset < sizeof(zend_opcache_static_cache_block)) { + return; + } + + block_offset = payload_offset - (uint32_t) sizeof(zend_opcache_static_cache_block); + original_block_offset = block_offset; + block = zend_opcache_static_cache_block_ptr(block_offset); + if (zend_opcache_static_cache_block_is_free(block)) { + return; + } + + zend_opcache_static_cache_block_mark_free(block); + + next_offset = block_offset + block->size; + if (next_offset < zend_opcache_static_cache_used_end_offset_locked(header)) { + adjacent = zend_opcache_static_cache_block_ptr(next_offset); + + if (zend_opcache_static_cache_block_is_free(adjacent)) { + zend_opcache_static_cache_free_list_remove_locked(header, next_offset); + block->size += adjacent->size; + if (header->last_block_offset == next_offset) { + header->last_block_offset = block_offset; + } + } + } + + if (block->prev_size != 0 && block_offset > header->data_offset) { + prev_offset = block_offset - block->prev_size; + + adjacent = zend_opcache_static_cache_block_ptr(prev_offset); + if (zend_opcache_static_cache_block_is_free(adjacent)) { + zend_opcache_static_cache_free_list_remove_locked(header, prev_offset); + block->size += adjacent->size; + adjacent->size = block->size; + block = adjacent; + block_offset = prev_offset; + if (header->last_block_offset == original_block_offset) { + header->last_block_offset = block_offset; + } + } + } + + zend_opcache_static_cache_update_following_prev_size_locked(header, block_offset, block); + zend_opcache_static_cache_free_list_insert_locked(header, block_offset); + zend_opcache_static_cache_trim_tail_free_blocks_locked(header, block_offset); +} + +uint32_t zend_opcache_static_cache_alloc_locked(size_t size, const void *source) +{ + zend_opcache_static_cache_header *header = zend_opcache_static_cache_header_ptr(); + zend_opcache_static_cache_block *block, *remainder; + uint32_t total_size, min_split_size, best_offset = 0, best_size = UINT32_MAX, block_offset, *free_offset_ptr; + size_t aligned_size; + + min_split_size = (uint32_t) ZEND_ALIGNED_SIZE(sizeof(zend_opcache_static_cache_block) + 1); + + if (!header || size == 0 || size > UINT32_MAX - sizeof(zend_opcache_static_cache_block)) { + return 0; + } + + aligned_size = ZEND_ALIGNED_SIZE(sizeof(zend_opcache_static_cache_block) + size); + if (aligned_size > UINT32_MAX) { + return 0; + } + + total_size = (uint32_t) aligned_size; + + free_offset_ptr = &header->free_list; + while (*free_offset_ptr != 0) { + block = zend_opcache_static_cache_block_ptr(*free_offset_ptr); + if (block->size >= total_size && block->size < best_size) { + best_offset = *free_offset_ptr; + best_size = block->size; + if (best_size == total_size) { + break; + } + } + free_offset_ptr = &block->next_free; + } + + if (best_offset != 0) { + block = zend_opcache_static_cache_block_ptr(best_offset); + zend_opcache_static_cache_free_list_remove_locked(header, best_offset); + if (block->size >= total_size + min_split_size) { + remainder = zend_opcache_static_cache_block_ptr(best_offset + total_size); + remainder->size = block->size - total_size; + remainder->prev_size = total_size; + remainder->next_free = 0; + remainder->prev_free = 0; + remainder->flags = ZEND_OPCACHE_STATIC_CACHE_BLOCK_FREE; + block->size = total_size; + zend_opcache_static_cache_update_following_prev_size_locked(header, best_offset + total_size, remainder); + zend_opcache_static_cache_free_list_insert_locked(header, best_offset + total_size); + if (header->last_block_offset == best_offset) { + header->last_block_offset = best_offset + total_size; + } + } else { + zend_opcache_static_cache_update_following_prev_size_locked(header, best_offset, block); + } + zend_opcache_static_cache_block_mark_used(block); + if (source != NULL) { + memcpy(zend_opcache_static_cache_ptr(best_offset + sizeof(zend_opcache_static_cache_block)), source, size); + } + + return best_offset + (uint32_t) sizeof(zend_opcache_static_cache_block); + } + + if (header->next_free > header->data_size || total_size > header->data_size - header->next_free) { + return 0; + } + + block_offset = header->data_offset + header->next_free; + block = zend_opcache_static_cache_block_ptr(block_offset); + block->size = total_size; + block->prev_size = header->last_block_offset != 0 ? zend_opcache_static_cache_block_ptr(header->last_block_offset)->size : 0; + block->next_free = 0; + block->prev_free = 0; + block->flags = 0; + if (source != NULL) { + memcpy(zend_opcache_static_cache_ptr(block_offset + sizeof(zend_opcache_static_cache_block)), source, size); + } + header->next_free += total_size; + header->last_block_offset = block_offset; + + return block_offset + (uint32_t) sizeof(zend_opcache_static_cache_block); +} + +bool zend_opcache_static_cache_compact_to_fit_locked(size_t size) +{ + zend_opcache_static_cache_header *header = zend_opcache_static_cache_header_ptr(); + zend_opcache_static_cache_block *block, *free_block; + uint32_t required_block_size, used_end, offset, next_offset, block_size, write_offset, + previous_block_size = 0, last_block_offset = 0, free_size; + bool moved = false, movable; + + if (!header || !zend_opcache_static_cache_header_init_locked()) { + return false; + } + + if (!zend_opcache_static_cache_payload_size_to_block_size(size, &required_block_size) || + required_block_size > header->data_size || + !zend_opcache_static_cache_compaction_can_fit_locked(header, required_block_size) + ) { + return false; + } + + used_end = header->data_offset + header->next_free; + offset = header->data_offset; + write_offset = header->data_offset; + header->free_list = 0; + + while (offset < used_end) { + block = zend_opcache_static_cache_block_ptr(offset); + block_size = block->size; + if (block_size < ZEND_ALIGNED_SIZE(sizeof(zend_opcache_static_cache_block) + 1) || + block_size > used_end - offset + ) { + return false; + } + + next_offset = offset + block_size; + if (zend_opcache_static_cache_block_is_free(block)) { + offset = next_offset; + continue; + } + + movable = zend_opcache_static_cache_block_is_movable_locked(header, offset, block_size); + if (!movable) { + if (write_offset < offset) { + free_block = zend_opcache_static_cache_block_ptr(write_offset); + free_size = offset - write_offset; + + free_block->size = free_size; + free_block->prev_size = previous_block_size; + free_block->next_free = 0; + free_block->prev_free = 0; + free_block->flags = ZEND_OPCACHE_STATIC_CACHE_BLOCK_FREE; + zend_opcache_static_cache_free_list_insert_locked(header, write_offset); + previous_block_size = free_size; + last_block_offset = write_offset; + } + + block->prev_size = previous_block_size; + block->next_free = 0; + block->prev_free = 0; + zend_opcache_static_cache_block_mark_used(block); + previous_block_size = block_size; + last_block_offset = offset; + write_offset = next_offset; + offset = next_offset; + continue; + } + + if (write_offset != offset) { + memmove(zend_opcache_static_cache_ptr(write_offset), block, block_size); + zend_opcache_static_cache_update_moved_block_entries_locked(header, offset, write_offset, block_size); + block = zend_opcache_static_cache_block_ptr(write_offset); + moved = true; + } + + block->prev_size = previous_block_size; + block->next_free = 0; + block->prev_free = 0; + zend_opcache_static_cache_block_mark_used(block); + previous_block_size = block_size; + last_block_offset = write_offset; + write_offset += block_size; + offset = next_offset; + } + + header->next_free = write_offset - header->data_offset; + header->last_block_offset = last_block_offset; + + if (moved) { + zend_opcache_static_cache_bump_mutation_epoch_locked(header); + } + + return moved; +} + +bool zend_opcache_static_cache_startup_storage_before_request(void) +{ + zend_opcache_static_cache_storage *storage = &zend_opcache_static_cache_active_context()->storage; + + if (zend_opcache_static_cache_force_startup_failure()) { + zend_opcache_static_cache_set_unavailable("Unable to initialize shared memory backend", true); + + return false; + } + + if (!zend_opcache_static_cache_startup_storage()) { + zend_opcache_static_cache_set_unavailable("Unable to initialize shared memory backend", true); + + return false; + } + + storage->initialized_before_request = true; + + return true; +} + +void zend_opcache_static_cache_shutdown_storage(void) +{ + zend_opcache_static_cache_storage *storage = &zend_opcache_static_cache_active_context()->storage; + + zend_opcache_static_cache_lock_shutdown(); + zend_opcache_static_cache_cleanup_segments( + storage->handler, + storage->segments, + storage->segment_count + ); + zend_opcache_static_cache_reset_storage(); +} + +void zend_opcache_static_cache_ensure_ready(void) +{ + zend_opcache_static_cache_context *context = zend_opcache_static_cache_active_context(); + zend_opcache_static_cache_runtime *runtime; + zend_opcache_static_cache_storage *storage = &context->storage; + + zend_opcache_static_cache_reset_runtime(); + runtime = zend_opcache_static_cache_context_runtime(context); + if (!runtime->enabled) { + return; + } + + if (!ZCG(enabled)) { + zend_opcache_static_cache_set_unavailable("OPcache is disabled", false); + + return; + } + + if (!accel_startup_ok) { + zend_opcache_static_cache_set_unavailable("OPcache startup failed", true); + + return; + } + + if (file_cache_only) { + zend_opcache_static_cache_set_unavailable("Cache is unavailable in file_cache_only mode", false); + + return; + } + + if (!storage->initialized && + zend_opcache_static_cache_requires_pre_request_storage() + ) { + zend_opcache_static_cache_set_unavailable("Shared memory backend was not initialized before FPM worker startup", true); + + return; + } + + if (!zend_opcache_static_cache_startup_storage()) { + zend_opcache_static_cache_set_unavailable("Unable to initialize shared memory backend", true); + + return; + } + + if (zend_opcache_static_cache_requires_pre_request_storage() && + !storage->initialized_before_request + ) { + zend_opcache_static_cache_set_unavailable("Shared memory backend was initialized after FPM worker startup", true); + + return; + } + + zend_opcache_static_cache_set_available(); +} + +void zend_opcache_static_cache_populate_array(zval *return_value) +{ + zend_opcache_static_cache_context *context = zend_opcache_static_cache_active_context(); + zend_opcache_static_cache_runtime *runtime = zend_opcache_static_cache_context_runtime(context); + zend_opcache_static_cache_storage *storage = &context->storage; + zend_opcache_static_cache_header *header; + zend_long entry_count = 0; + + array_init(return_value); + if (runtime->available && zend_opcache_static_cache_rlock()) { + header = zend_opcache_static_cache_header_ptr(); + + if (zend_opcache_static_cache_header_is_initialized_locked()) { + entry_count = header->count; + } + + zend_opcache_static_cache_unlock(); + } + + add_assoc_bool(return_value, "enabled", runtime->enabled); + add_assoc_bool(return_value, "available", runtime->available); + add_assoc_bool(return_value, "startup_failed", runtime->startup_failed); + add_assoc_bool(return_value, "backend_initialized", runtime->backend_initialized); + add_assoc_long(return_value, "configured_memory", (zend_long) runtime->configured_memory); + add_assoc_long(return_value, "shared_memory", (zend_long) storage->size); + add_assoc_long(return_value, "entry_count", entry_count); + add_assoc_long(return_value, "segment_count", storage->segment_count); + add_assoc_string(return_value, "shared_model", storage->model ? (char *) storage->model : ""); + + if (runtime->failure_reason) { + add_assoc_string(return_value, "failure_reason", (char *) runtime->failure_reason); + } +} + +bool zend_opcache_static_cache_rlock(void) +{ + return zend_opcache_static_cache_rlock_impl(); +} + +bool zend_opcache_static_cache_wlock(void) +{ + return zend_opcache_static_cache_wlock_impl(); +} + +void zend_opcache_static_cache_unlock(void) +{ + zend_opcache_static_cache_unlock_impl(); +} + +bool zend_opcache_static_cache_acquire_entry_lock(zend_string *key) +{ + zend_opcache_static_cache_context *context = zend_opcache_static_cache_active_context(); + zend_opcache_static_cache_entry_lock *lock; + HashTable **locks_ptr = zend_opcache_static_cache_entry_locks_ptr_for_context(context); + uint32_t stripe = zend_opcache_static_cache_entry_lock_stripe(key); + + zend_opcache_static_cache_ensure_entry_lock_process(); + + if (*locks_ptr != NULL && zend_hash_exists(*locks_ptr, key)) { + return true; + } + + if (!zend_opcache_static_cache_lock_entry_stripe(context, stripe)) { + zend_throw_exception_ex(zend_opcache_static_cache_exception_ce, 0, "Unable to acquire the %s entry lock", context->name); + + return false; + } + + lock = emalloc(sizeof(zend_opcache_static_cache_entry_lock)); + lock->context = context; + lock->owner_pid = +#ifndef ZEND_WIN32 + (zend_ulong) getpid(); +#else + 0; +#endif + lock->stripe = stripe; + + if (zend_hash_add_ptr(zend_opcache_static_cache_entry_locks(context), key, lock) == NULL) { + zend_opcache_static_cache_unlock_entry_stripe(context, stripe); + efree(lock); + + return true; + } + + return true; +} + +bool zend_opcache_static_cache_try_acquire_entry_lock(zend_string *key) +{ + zend_opcache_static_cache_context *context = zend_opcache_static_cache_active_context(); + zend_opcache_static_cache_entry_lock *lock; + HashTable **locks_ptr = zend_opcache_static_cache_entry_locks_ptr_for_context(context); + uint32_t stripe = zend_opcache_static_cache_entry_lock_stripe(key); + + zend_opcache_static_cache_ensure_entry_lock_process(); + + if (*locks_ptr != NULL && zend_hash_exists(*locks_ptr, key)) { + return true; + } + + if (!zend_opcache_static_cache_try_lock_entry_stripe(context, stripe)) { + return false; + } + + lock = emalloc(sizeof(zend_opcache_static_cache_entry_lock)); + lock->context = context; + lock->owner_pid = +#ifndef ZEND_WIN32 + (zend_ulong) getpid(); +#else + 0; +#endif + lock->stripe = stripe; + + if (zend_hash_add_ptr(zend_opcache_static_cache_entry_locks(context), key, lock) == NULL) { + zend_opcache_static_cache_unlock_entry_stripe(context, stripe); + efree(lock); + + return true; + } + + return true; +} + +bool zend_opcache_static_cache_has_entry_lock(zend_string *key) +{ + HashTable **locks_ptr = zend_opcache_static_cache_entry_locks_ptr_for_context(zend_opcache_static_cache_active_context()); + + zend_opcache_static_cache_ensure_entry_lock_process(); + + return *locks_ptr != NULL && zend_hash_exists(*locks_ptr, key); +} + +void zend_opcache_static_cache_release_entry_lock(zend_string *key) +{ + HashTable **locks_ptr = zend_opcache_static_cache_entry_locks_ptr_for_context(zend_opcache_static_cache_active_context()); + + zend_opcache_static_cache_ensure_entry_lock_process(); + + if (*locks_ptr == NULL) { + return; + } + + zend_hash_del(*locks_ptr, key); +} + +bool zend_opcache_static_cache_has_all_entry_locks(void) +{ + uint32_t stripe, *counts = zend_opcache_static_cache_entry_lock_counts_for_context(zend_opcache_static_cache_active_context()); + + zend_opcache_static_cache_ensure_entry_lock_process(); + + for (stripe = 0; stripe < ZEND_OPCACHE_STATIC_CACHE_ENTRY_LOCK_STRIPES; stripe++) { + if (counts[stripe] == 0) { + return false; + } + } + + return true; +} + +void zend_opcache_static_cache_release_active_entry_locks(void) +{ + zend_opcache_static_cache_context *context = zend_opcache_static_cache_active_context(); + + zend_opcache_static_cache_release_entry_lock_context( + zend_opcache_static_cache_entry_locks_ptr_for_context(context), + zend_opcache_static_cache_entry_lock_counts_for_context(context)); +} + +void zend_opcache_static_cache_release_request_entry_locks(void) +{ + zend_opcache_static_cache_release_entry_lock_context( + &zend_opcache_static_cache_volatile_entry_locks, + zend_opcache_static_cache_volatile_entry_lock_counts + ); + zend_opcache_static_cache_release_entry_lock_context( + &zend_opcache_static_cache_persistent_entry_locks, + zend_opcache_static_cache_persistent_entry_lock_counts + ); +#if !defined(ZEND_WIN32) && defined(ZTS) + if (zend_opcache_static_cache_entry_locks_process_is_fork_child) { + zend_opcache_static_cache_entry_locks_shutdown(&zend_opcache_static_cache_volatile_context_state.storage); + zend_opcache_static_cache_entry_locks_shutdown(&zend_opcache_static_cache_persistent_context_state.storage); + } +#endif +#ifndef ZEND_WIN32 + zend_opcache_static_cache_entry_lock_owner_pid = 0; +#endif +} diff --git a/ext/spl/spl_array.c b/ext/spl/spl_array.c index 1f9f87d35841..15c695c022d2 100644 --- a/ext/spl/spl_array.c +++ b/ext/spl/spl_array.c @@ -19,6 +19,7 @@ #include "php.h" #include "ext/standard/php_var.h" #include "zend_smart_str.h" +#include "ext/opcache/zend_static_cache.h" #include "zend_interfaces.h" #include "zend_exceptions.h" @@ -36,6 +37,9 @@ PHPAPI zend_class_entry *spl_ce_RecursiveArrayIterator; static zend_object_handlers spl_handler_ArrayObject; PHPAPI zend_class_entry *spl_ce_ArrayObject; +static void spl_array_object_serialize(zval *object, zval *return_value); +static void spl_array_object_unserialize(zval *object, HashTable *data); + typedef struct _spl_array_object { zval array; HashTable *sentinel_array; @@ -1390,11 +1394,16 @@ PHP_METHOD(ArrayObject, unserialize) /* {{{ */ PHP_METHOD(ArrayObject, __serialize) { - spl_array_object *intern = Z_SPLARRAY_P(ZEND_THIS); - zval tmp; - ZEND_PARSE_PARAMETERS_NONE(); + spl_array_object_serialize(ZEND_THIS, return_value); +} + +static void spl_array_object_serialize(zval *object, zval *return_value) +{ + spl_array_object *intern = Z_SPLARRAY_P(object); + zval tmp; + array_init(return_value); /* flags */ @@ -1428,15 +1437,24 @@ PHP_METHOD(ArrayObject, __serialize) /* {{{ */ PHP_METHOD(ArrayObject, __unserialize) { - spl_array_object *intern = Z_SPLARRAY_P(ZEND_THIS); HashTable *data; - zval *flags_zv, *storage_zv, *members_zv, *iterator_class_zv; - zend_long flags; if (zend_parse_parameters(ZEND_NUM_ARGS(), "h", &data) == FAILURE) { RETURN_THROWS(); } + spl_array_object_unserialize(ZEND_THIS, data); + if (EG(exception)) { + RETURN_THROWS(); + } +} + +static void spl_array_object_unserialize(zval *object, HashTable *data) +{ + spl_array_object *intern = Z_SPLARRAY_P(object); + zval *flags_zv, *storage_zv, *members_zv, *iterator_class_zv; + zend_long flags; + flags_zv = zend_hash_index_find(data, 0); storage_zv = zend_hash_index_find(data, 1); members_zv = zend_hash_index_find(data, 2); @@ -1448,7 +1466,7 @@ PHP_METHOD(ArrayObject, __unserialize) Z_TYPE_P(iterator_class_zv) != IS_STRING))) { zend_throw_exception(spl_ce_UnexpectedValueException, "Incomplete or ill-typed serialization data", 0); - RETURN_THROWS(); + return; } flags = Z_LVAL_P(flags_zv); @@ -1462,14 +1480,14 @@ PHP_METHOD(ArrayObject, __unserialize) if (Z_TYPE_P(storage_zv) != IS_OBJECT && Z_TYPE_P(storage_zv) != IS_ARRAY) { /* TODO Use UnexpectedValueException instead? And better error message? */ zend_throw_exception(spl_ce_InvalidArgumentException, "Passed variable is not an array or object", 0); - RETURN_THROWS(); + return; } - spl_array_set_array(ZEND_THIS, intern, storage_zv, 0L, true); + spl_array_set_array(object, intern, storage_zv, 0L, true); } object_properties_load(&intern->std, Z_ARRVAL_P(members_zv)); if (EG(exception)) { - RETURN_THROWS(); + return; } if (iterator_class_zv && Z_TYPE_P(iterator_class_zv) == IS_STRING) { @@ -1479,14 +1497,14 @@ PHP_METHOD(ArrayObject, __unserialize) zend_throw_exception_ex(spl_ce_UnexpectedValueException, 0, "Cannot deserialize ArrayObject with iterator class '%s'; no such class exists", ZSTR_VAL(Z_STR_P(iterator_class_zv))); - RETURN_THROWS(); + return; } if (!instanceof_function(ce, zend_ce_iterator)) { zend_throw_exception_ex(spl_ce_UnexpectedValueException, 0, "Cannot deserialize ArrayObject with iterator class '%s'; this class does not implement the Iterator interface", ZSTR_VAL(Z_STR_P(iterator_class_zv))); - RETURN_THROWS(); + return; } intern->ce_get_iterator = ce; @@ -1494,6 +1512,122 @@ PHP_METHOD(ArrayObject, __unserialize) } /* }}} */ +static bool spl_array_object_copy_direct_cache_state( + void *context, + zend_object *old_object, + zend_object *new_object, + zend_opcache_static_cache_safe_direct_clone_value_func_t clone_value) +{ + zval old_zv, new_zv, state_zv, cloned_state_zv, empty_props_zv; + bool result = false; + + if (clone_value == NULL) { + return false; + } + + ZVAL_OBJ(&old_zv, old_object); + ZVAL_OBJ(&new_zv, new_object); + ZVAL_UNDEF(&state_zv); + ZVAL_UNDEF(&cloned_state_zv); + + spl_array_object_serialize(&old_zv, &state_zv); + if (EG(exception) || Z_TYPE(state_zv) != IS_ARRAY) { + goto cleanup; + } + + array_init(&empty_props_zv); + zend_hash_index_update(Z_ARRVAL(state_zv), 2, &empty_props_zv); + + if (!clone_value(context, &cloned_state_zv, &state_zv) || + Z_TYPE(cloned_state_zv) != IS_ARRAY) { + goto cleanup; + } + + spl_array_object_unserialize(&new_zv, Z_ARRVAL(cloned_state_zv)); + result = !EG(exception); + +cleanup: + if (Z_TYPE(cloned_state_zv) != IS_UNDEF) { + zval_ptr_dtor(&cloned_state_zv); + } + if (Z_TYPE(state_zv) != IS_UNDEF) { + zval_ptr_dtor(&state_zv); + } + + return result; +} + +static bool spl_array_object_direct_cache_state_has_unstorable( + void *context, + const zval *object, + zend_opcache_static_cache_safe_direct_value_has_unstorable_func_t value_has_unstorable) +{ + zval state_zv; + bool result = false; + + if (value_has_unstorable == NULL) { + return false; + } + + ZVAL_UNDEF(&state_zv); + spl_array_object_serialize((zval *) object, &state_zv); + if (EG(exception) || Z_TYPE(state_zv) == IS_UNDEF) { + return true; + } + + result = value_has_unstorable(context, &state_zv); + zval_ptr_dtor(&state_zv); + + return result; +} + +static bool spl_array_object_serialize_direct_cache_state(const zval *object, zval *state) +{ + zval empty_props_zv; + + ZVAL_UNDEF(state); + spl_array_object_serialize((zval *) object, state); + if (EG(exception) || Z_TYPE_P(state) != IS_ARRAY) { + if (Z_TYPE_P(state) != IS_UNDEF) { + zval_ptr_dtor(state); + } + ZVAL_UNDEF(state); + + return false; + } + + ZVAL_UNDEF(&empty_props_zv); + array_init(&empty_props_zv); + zend_hash_index_update(Z_ARRVAL_P(state), 2, &empty_props_zv); + + return true; +} + +static bool spl_array_object_unserialize_direct_cache_state(zval *object, zval *state) +{ + if (Z_TYPE_P(state) != IS_ARRAY) { + return false; + } + + spl_array_object_unserialize(object, Z_ARRVAL_P(state)); + + return !EG(exception); +} + +const zend_opcache_static_cache_safe_direct_handlers *spl_array_object_get_direct_cache_handlers(void) +{ + static const zend_opcache_static_cache_safe_direct_handlers handlers = { + false, + { false, NULL, NULL }, + spl_array_object_copy_direct_cache_state, + spl_array_object_direct_cache_state_has_unstorable, + spl_array_object_serialize_direct_cache_state, + spl_array_object_unserialize_direct_cache_state + }; + + return &handlers; +} + /* {{{ */ PHP_METHOD(ArrayObject, __debugInfo) { diff --git a/ext/spl/spl_array.h b/ext/spl/spl_array.h index f99bb3f6fe88..10ca1b307366 100644 --- a/ext/spl/spl_array.h +++ b/ext/spl/spl_array.h @@ -29,6 +29,12 @@ extern PHPAPI zend_class_entry *spl_ce_ArrayObject; extern PHPAPI zend_class_entry *spl_ce_ArrayIterator; extern PHPAPI zend_class_entry *spl_ce_RecursiveArrayIterator; +#ifndef ZEND_OPCACHE_STATIC_CACHE_SAFE_DIRECT_HANDLERS_FWD +# define ZEND_OPCACHE_STATIC_CACHE_SAFE_DIRECT_HANDLERS_FWD +typedef struct _zend_opcache_static_cache_safe_direct_handlers zend_opcache_static_cache_safe_direct_handlers; +#endif +const zend_opcache_static_cache_safe_direct_handlers *spl_array_object_get_direct_cache_handlers(void); + PHP_MINIT_FUNCTION(spl_array); extern void spl_array_iterator_append(zval *object, zval *append_value); diff --git a/ext/spl/spl_fixedarray.c b/ext/spl/spl_fixedarray.c index f865c101aede..ac0ef4d8f9c5 100644 --- a/ext/spl/spl_fixedarray.c +++ b/ext/spl/spl_fixedarray.c @@ -18,6 +18,7 @@ #endif #include "php.h" +#include "ext/opcache/zend_static_cache.h" #include "zend_interfaces.h" #include "zend_exceptions.h" #include "zend_attributes.h" @@ -30,6 +31,9 @@ static zend_object_handlers spl_handler_SplFixedArray; PHPAPI zend_class_entry *spl_ce_SplFixedArray; +static void spl_fixedarray_object_serialize(zval *object, zval *return_value); +static void spl_fixedarray_object_unserialize(zval *object, HashTable *data); + /* Check if the object is an instance of a subclass of SplFixedArray that overrides method's implementation. * Expect subclassing SplFixedArray to be rare and check that first. */ #define HAS_FIXEDARRAY_ARRAYACCESS_OVERRIDE(object, method) UNEXPECTED((object)->ce != spl_ce_SplFixedArray && (object)->ce->arrayaccess_funcs_ptr->method->common.scope != spl_ce_SplFixedArray) @@ -581,12 +585,17 @@ PHP_METHOD(SplFixedArray, __wakeup) PHP_METHOD(SplFixedArray, __serialize) { - spl_fixedarray_object *intern = Z_SPLFIXEDARRAY_P(ZEND_THIS); + ZEND_PARSE_PARAMETERS_NONE(); + + spl_fixedarray_object_serialize(ZEND_THIS, return_value); +} + +static void spl_fixedarray_object_serialize(zval *object, zval *return_value) +{ + spl_fixedarray_object *intern = Z_SPLFIXEDARRAY_P(object); zval *current; zend_string *key; - ZEND_PARSE_PARAMETERS_NONE(); - HashTable *ht = zend_std_get_properties(&intern->std); uint32_t num_properties = zend_hash_num_elements(ht); array_init_size(return_value, intern->array.size + num_properties); @@ -612,16 +621,25 @@ PHP_METHOD(SplFixedArray, __serialize) PHP_METHOD(SplFixedArray, __unserialize) { - spl_fixedarray_object *intern = Z_SPLFIXEDARRAY_P(ZEND_THIS); HashTable *data; - zval members_zv, *elem; - zend_string *key; - zend_long size; if (zend_parse_parameters(ZEND_NUM_ARGS(), "h", &data) == FAILURE) { RETURN_THROWS(); } + spl_fixedarray_object_unserialize(ZEND_THIS, data); + if (EG(exception)) { + RETURN_THROWS(); + } +} + +static void spl_fixedarray_object_unserialize(zval *object, HashTable *data) +{ + spl_fixedarray_object *intern = Z_SPLFIXEDARRAY_P(object); + zval members_zv, *elem; + zend_string *key; + zend_long size; + if (intern->array.size == 0) { size = zend_hash_num_elements(data); spl_fixedarray_init_non_empty_struct(&intern->array, size); @@ -655,6 +673,140 @@ PHP_METHOD(SplFixedArray, __unserialize) } } +static bool spl_fixedarray_object_copy_direct_cache_state( + void *context, + zend_object *old_object, + zend_object *new_object, + zend_opcache_static_cache_safe_direct_clone_value_func_t clone_value) +{ + zval old_zv, new_zv, state_zv, cloned_state_zv, elements_zv, copied_elem; + zval *elem; + zend_string *key; + bool result = false; + + if (clone_value == NULL) { + return false; + } + + ZVAL_OBJ(&old_zv, old_object); + ZVAL_OBJ(&new_zv, new_object); + ZVAL_UNDEF(&state_zv); + ZVAL_UNDEF(&cloned_state_zv); + + spl_fixedarray_object_serialize(&old_zv, &state_zv); + if (EG(exception) || Z_TYPE(state_zv) != IS_ARRAY) { + goto cleanup; + } + + array_init_size(&elements_zv, zend_hash_num_elements(Z_ARRVAL(state_zv))); + ZEND_HASH_FOREACH_STR_KEY_VAL(Z_ARRVAL(state_zv), key, elem) { + if (key == NULL) { + ZVAL_COPY(&copied_elem, elem); + zend_hash_next_index_insert(Z_ARRVAL(elements_zv), &copied_elem); + } + } ZEND_HASH_FOREACH_END(); + + zval_ptr_dtor(&state_zv); + ZVAL_COPY_VALUE(&state_zv, &elements_zv); + + if (!clone_value(context, &cloned_state_zv, &state_zv) || + Z_TYPE(cloned_state_zv) != IS_ARRAY) { + goto cleanup; + } + + spl_fixedarray_object_unserialize(&new_zv, Z_ARRVAL(cloned_state_zv)); + result = !EG(exception); + +cleanup: + if (Z_TYPE(cloned_state_zv) != IS_UNDEF) { + zval_ptr_dtor(&cloned_state_zv); + } + if (Z_TYPE(state_zv) != IS_UNDEF) { + zval_ptr_dtor(&state_zv); + } + + return result; +} + +static bool spl_fixedarray_object_direct_cache_state_has_unstorable( + void *context, + const zval *object, + zend_opcache_static_cache_safe_direct_value_has_unstorable_func_t value_has_unstorable) +{ + zval state_zv; + bool result = false; + + if (value_has_unstorable == NULL) { + return false; + } + + ZVAL_UNDEF(&state_zv); + spl_fixedarray_object_serialize((zval *) object, &state_zv); + if (EG(exception) || Z_TYPE(state_zv) == IS_UNDEF) { + return true; + } + + result = value_has_unstorable(context, &state_zv); + zval_ptr_dtor(&state_zv); + + return result; +} + +static bool spl_fixedarray_object_serialize_direct_cache_state(const zval *object, zval *state) +{ + zval serialized_state, copied_elem; + zval *elem; + zend_string *key; + + ZVAL_UNDEF(state); + ZVAL_UNDEF(&serialized_state); + spl_fixedarray_object_serialize((zval *) object, &serialized_state); + if (EG(exception) || Z_TYPE(serialized_state) != IS_ARRAY) { + if (Z_TYPE(serialized_state) != IS_UNDEF) { + zval_ptr_dtor(&serialized_state); + } + + return false; + } + + array_init_size(state, zend_hash_num_elements(Z_ARRVAL(serialized_state))); + ZEND_HASH_FOREACH_STR_KEY_VAL(Z_ARRVAL(serialized_state), key, elem) { + if (key == NULL) { + ZVAL_COPY(&copied_elem, elem); + zend_hash_next_index_insert(Z_ARRVAL_P(state), &copied_elem); + } + } ZEND_HASH_FOREACH_END(); + + zval_ptr_dtor(&serialized_state); + + return true; +} + +static bool spl_fixedarray_object_unserialize_direct_cache_state(zval *object, zval *state) +{ + if (Z_TYPE_P(state) != IS_ARRAY) { + return false; + } + + spl_fixedarray_object_unserialize(object, Z_ARRVAL_P(state)); + + return !EG(exception); +} + +const zend_opcache_static_cache_safe_direct_handlers *spl_fixedarray_object_get_direct_cache_handlers(void) +{ + static const zend_opcache_static_cache_safe_direct_handlers handlers = { + false, + { false, NULL, NULL }, + spl_fixedarray_object_copy_direct_cache_state, + spl_fixedarray_object_direct_cache_state_has_unstorable, + spl_fixedarray_object_serialize_direct_cache_state, + spl_fixedarray_object_unserialize_direct_cache_state + }; + + return &handlers; +} + PHP_METHOD(SplFixedArray, count) { zval *object = ZEND_THIS; diff --git a/ext/spl/spl_fixedarray.h b/ext/spl/spl_fixedarray.h index e0661a95d872..3ec260070b7b 100644 --- a/ext/spl/spl_fixedarray.h +++ b/ext/spl/spl_fixedarray.h @@ -18,6 +18,12 @@ extern PHPAPI zend_class_entry *spl_ce_SplFixedArray; +#ifndef ZEND_OPCACHE_STATIC_CACHE_SAFE_DIRECT_HANDLERS_FWD +# define ZEND_OPCACHE_STATIC_CACHE_SAFE_DIRECT_HANDLERS_FWD +typedef struct _zend_opcache_static_cache_safe_direct_handlers zend_opcache_static_cache_safe_direct_handlers; +#endif +const zend_opcache_static_cache_safe_direct_handlers *spl_fixedarray_object_get_direct_cache_handlers(void); + PHP_MINIT_FUNCTION(spl_fixedarray); #endif /* SPL_FIXEDARRAY_H */ diff --git a/sapi/fpm/tests/tester.inc b/sapi/fpm/tests/tester.inc index 907988654337..65b0b0732dd4 100644 --- a/sapi/fpm/tests/tester.inc +++ b/sapi/fpm/tests/tester.inc @@ -548,7 +548,8 @@ class Tester $this->masterProcess = proc_open($cmd, $desc, $pipes, null, $envVars); register_shutdown_function( - function ($masterProcess) use ($configFile) { + /* Fix for LLVM 20 ASan false-positive */ + static function ($masterProcess) use ($configFile) { @unlink($configFile); if (is_resource($masterProcess)) { @proc_terminate($masterProcess); @@ -1154,6 +1155,8 @@ class Tester $this->terminate(); } proc_close($this->masterProcess); + /* Break the Tester -> Response -> Tester cycle before leak checkers run. */ + $this->response = null; } /**