From 347b74f5c14b9734505c56c92699707d4f1743d6 Mon Sep 17 00:00:00 2001 From: Go Kudo Date: Wed, 1 Jul 2026 12:16:53 +0000 Subject: [PATCH] OPcache User Cache Implementation --- ext/date/php_date.c | 404 ++ ext/opcache/ZendAccelerator.c | 3 + ext/opcache/ZendAccelerator.h | 1 + ext/opcache/config.m4 | 4 + ext/opcache/config.w32 | 4 + ext/opcache/opcache.stub.php | 2 + ext/opcache/opcache_arginfo.h | 6 +- ext/opcache/opcache_user_cache.stub.php | 75 + ext/opcache/opcache_user_cache_arginfo.h | 212 + ext/opcache/tests/fpm/CONFLICTS | 1 + ext/opcache/tests/fpm/skipif.inc | 13 + ext/opcache/tests/fpm/tester.inc | 4 + ...che_fpm_complex_value_persistence_001.phpt | 138 + ...r_cache_fpm_decode_failure_delete_001.phpt | 117 + .../fpm/user_cache_fpm_lookup_cache_001.phpt | 94 + .../fpm/user_cache_fpm_opcache_reset_001.phpt | 104 + .../fpm/user_cache_fpm_persistence_001.phpt | 106 + .../fpm/user_cache_fpm_pool_separate_001.phpt | 111 + ...he_fpm_remember_callable_lifetime_001.phpt | 120 + .../fpm/user_cache_fpm_safe_direct_001.phpt | 141 + ...ser_cache_apache2handler_boundary_001.phpt | 209 + ext/opcache/tests/user_cache_atomic.phpt | 64 + ext/opcache/tests/user_cache_basic.phpt | 86 + ext/opcache/tests/user_cache_cgi_001.phpt | 28 + .../tests/user_cache_cgi_boundary_001.phpt | 302 ++ .../tests/user_cache_cgi_no_boundary_001.phpt | 27 + .../user_cache_datetime_safe_direct.phpt | 250 ++ ext/opcache/tests/user_cache_disabled.phpt | 54 + .../tests/user_cache_fuzz_roundtrip.phpt | 435 +++ ext/opcache/tests/user_cache_info_reset.phpt | 65 + .../tests/user_cache_invalid_keys.phpt | 44 + .../user_cache_litespeed_boundary_001.phpt | 345 ++ .../tests/user_cache_lock_key_isolation.phpt | 61 + .../tests/user_cache_opcache_disabled.phpt | 54 + .../tests/user_cache_opcache_reset.phpt | 40 + .../tests/user_cache_overwrite_reuse.phpt | 42 + .../tests/user_cache_pressure_clear.phpt | 30 + .../tests/user_cache_reference_graphs.phpt | 262 ++ .../tests/user_cache_shared_graph.phpt | 40 + .../tests/user_cache_shm_size_units.phpt | 16 + .../tests/user_cache_spl_safe_direct.phpt | 397 ++ .../tests/user_cache_storable_values.phpt | 146 + .../user_cache_store_multiple_rollback.phpt | 46 + .../tests/user_cache_tombstone_rehash.phpt | 47 + .../tests/user_cache_ttl_remember.phpt | 37 + ext/opcache/zend_accelerator_module.c | 54 +- ext/opcache/zend_user_cache.c | 2213 +++++++++++ ext/opcache/zend_user_cache.h | 87 + ext/opcache/zend_user_cache_entries.c | 3478 +++++++++++++++++ ext/opcache/zend_user_cache_internal.h | 1074 +++++ ext/opcache/zend_user_cache_shared_graph.c | 3474 ++++++++++++++++ ext/opcache/zend_user_cache_storage.c | 3090 +++++++++++++++ ext/spl/spl_array.c | 208 + ext/spl/spl_dllist.c | 171 + ext/spl/spl_fixedarray.c | 188 +- ext/spl/spl_heap.c | 306 ++ sapi/apache2handler/sapi_apache2.c | 78 +- sapi/cgi/cgi_main.c | 250 +- sapi/cli/php_cli.c | 12 +- sapi/cli/php_cli_server.c | 15 +- sapi/fpm/fpm/fpm.c | 24 + sapi/fpm/fpm/fpm_children.c | 3 + sapi/fpm/fpm/fpm_worker_pool.h | 2 + sapi/litespeed/lsapi_main.c | 226 ++ sapi/phpdbg/phpdbg.c | 4 + 65 files changed, 19724 insertions(+), 20 deletions(-) create mode 100644 ext/opcache/opcache_user_cache.stub.php create mode 100644 ext/opcache/opcache_user_cache_arginfo.h create mode 100644 ext/opcache/tests/fpm/CONFLICTS create mode 100644 ext/opcache/tests/fpm/skipif.inc create mode 100644 ext/opcache/tests/fpm/tester.inc create mode 100644 ext/opcache/tests/fpm/user_cache_fpm_complex_value_persistence_001.phpt create mode 100644 ext/opcache/tests/fpm/user_cache_fpm_decode_failure_delete_001.phpt create mode 100644 ext/opcache/tests/fpm/user_cache_fpm_lookup_cache_001.phpt create mode 100644 ext/opcache/tests/fpm/user_cache_fpm_opcache_reset_001.phpt create mode 100644 ext/opcache/tests/fpm/user_cache_fpm_persistence_001.phpt create mode 100644 ext/opcache/tests/fpm/user_cache_fpm_pool_separate_001.phpt create mode 100644 ext/opcache/tests/fpm/user_cache_fpm_remember_callable_lifetime_001.phpt create mode 100644 ext/opcache/tests/fpm/user_cache_fpm_safe_direct_001.phpt create mode 100644 ext/opcache/tests/user_cache_apache2handler_boundary_001.phpt create mode 100644 ext/opcache/tests/user_cache_atomic.phpt create mode 100644 ext/opcache/tests/user_cache_basic.phpt create mode 100644 ext/opcache/tests/user_cache_cgi_001.phpt create mode 100644 ext/opcache/tests/user_cache_cgi_boundary_001.phpt create mode 100644 ext/opcache/tests/user_cache_cgi_no_boundary_001.phpt create mode 100644 ext/opcache/tests/user_cache_datetime_safe_direct.phpt create mode 100644 ext/opcache/tests/user_cache_disabled.phpt create mode 100644 ext/opcache/tests/user_cache_fuzz_roundtrip.phpt create mode 100644 ext/opcache/tests/user_cache_info_reset.phpt create mode 100644 ext/opcache/tests/user_cache_invalid_keys.phpt create mode 100644 ext/opcache/tests/user_cache_litespeed_boundary_001.phpt create mode 100644 ext/opcache/tests/user_cache_lock_key_isolation.phpt create mode 100644 ext/opcache/tests/user_cache_opcache_disabled.phpt create mode 100644 ext/opcache/tests/user_cache_opcache_reset.phpt create mode 100644 ext/opcache/tests/user_cache_overwrite_reuse.phpt create mode 100644 ext/opcache/tests/user_cache_pressure_clear.phpt create mode 100644 ext/opcache/tests/user_cache_reference_graphs.phpt create mode 100644 ext/opcache/tests/user_cache_shared_graph.phpt create mode 100644 ext/opcache/tests/user_cache_shm_size_units.phpt create mode 100644 ext/opcache/tests/user_cache_spl_safe_direct.phpt create mode 100644 ext/opcache/tests/user_cache_storable_values.phpt create mode 100644 ext/opcache/tests/user_cache_store_multiple_rollback.phpt create mode 100644 ext/opcache/tests/user_cache_tombstone_rehash.phpt create mode 100644 ext/opcache/tests/user_cache_ttl_remember.phpt create mode 100644 ext/opcache/zend_user_cache.c create mode 100644 ext/opcache/zend_user_cache.h create mode 100644 ext/opcache/zend_user_cache_entries.c create mode 100644 ext/opcache/zend_user_cache_internal.h create mode 100644 ext/opcache/zend_user_cache_shared_graph.c create mode 100644 ext/opcache/zend_user_cache_storage.c diff --git a/ext/date/php_date.c b/ext/date/php_date.c index 1dbcf51e72df..83bb7f7d4c50 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_user_cache.h" #include "php_date.h" #include "zend_attributes.h" #include "zend_interfaces.h" @@ -441,6 +442,8 @@ ZEND_MODULE_POST_ZEND_DEACTIVATE_D(date) #define DATE_TIMEZONEDB php_date_global_timezone_db ? php_date_global_timezone_db : timelib_builtin_db() +static void php_date_register_user_cache_handlers(void); + /* {{{ PHP_MINIT_FUNCTION */ PHP_MINIT_FUNCTION(date) { @@ -451,6 +454,9 @@ PHP_MINIT_FUNCTION(date) php_date_global_timezone_db = NULL; php_date_global_timezone_db_enabled = 0; DATEG(last_errors) = NULL; + + php_date_register_user_cache_handlers(); + return SUCCESS; } /* }}} */ @@ -4735,6 +4741,404 @@ static void php_date_interval_initialize_from_hash(php_interval_obj *intobj, con intobj->initialized = true; } /* }}} */ +#define PHP_DATE_USER_CACHE_STATE_DATE "date" +#define PHP_DATE_USER_CACHE_STATE_DATE_LEN (sizeof(PHP_DATE_USER_CACHE_STATE_DATE) - 1) +#define PHP_DATE_USER_CACHE_STATE_TIMEZONE_TYPE "timezone_type" +#define PHP_DATE_USER_CACHE_STATE_TIMEZONE_TYPE_LEN (sizeof(PHP_DATE_USER_CACHE_STATE_TIMEZONE_TYPE) - 1) +#define PHP_DATE_USER_CACHE_STATE_TIMEZONE "timezone" +#define PHP_DATE_USER_CACHE_STATE_TIMEZONE_LEN (sizeof(PHP_DATE_USER_CACHE_STATE_TIMEZONE) - 1) +#define PHP_DATE_USER_CACHE_STATE_CTIME "ctime" +#define PHP_DATE_USER_CACHE_STATE_CTIME_LEN (sizeof(PHP_DATE_USER_CACHE_STATE_CTIME) - 1) + +static void php_date_user_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 void php_date_user_cache_copy_time_snapshot(timelib_time *dst, const timelib_time *src) +{ + memset(dst, 0, sizeof(*dst)); + + dst->y = src->y; + dst->m = src->m; + dst->d = src->d; + dst->h = src->h; + dst->i = src->i; + dst->s = src->s; + dst->us = src->us; + dst->z = src->z; + dst->dst = src->dst; + dst->relative = src->relative; + dst->sse = src->sse; + dst->have_time = src->have_time; + dst->have_date = src->have_date; + dst->have_zone = src->have_zone; + dst->have_relative = src->have_relative; + dst->have_weeknr_day = src->have_weeknr_day; + dst->sse_uptodate = src->sse_uptodate; + dst->tim_uptodate = src->tim_uptodate; + dst->is_localtime = src->is_localtime; + dst->zone_type = src->zone_type; +} + +static bool php_date_copy_user_cache_state( + void *context, + zend_object *old_object, + zend_object *new_object, + zend_opcache_user_cache_safe_direct_clone_value_func_t clone_value) +{ + php_date_obj *new_obj, *old_obj; + php_timezone_obj *new_tzobj, *old_tzobj; + php_interval_obj *new_intervalobj, *old_intervalobj; + + (void) context; + (void) clone_value; + + if (instanceof_function(old_object->ce, date_ce_date) || + instanceof_function(old_object->ce, date_ce_immutable) + ) { + old_obj = php_date_obj_from_obj(old_object); + 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)) { + old_tzobj = php_timezone_obj_from_obj(old_object); + new_tzobj = php_timezone_obj_from_obj(new_object); + + if (!old_tzobj->initialized) { + return true; + } + + new_tzobj->type = old_tzobj->type; + new_tzobj->initialized = true; + switch (new_tzobj->type) { + case TIMELIB_ZONETYPE_ID: + new_tzobj->tzi.tz = old_tzobj->tzi.tz; + + return true; + case TIMELIB_ZONETYPE_OFFSET: + new_tzobj->tzi.utc_offset = old_tzobj->tzi.utc_offset; + + return true; + case TIMELIB_ZONETYPE_ABBR: + new_tzobj->tzi.z.utc_offset = old_tzobj->tzi.z.utc_offset; + new_tzobj->tzi.z.dst = old_tzobj->tzi.z.dst; + new_tzobj->tzi.z.abbr = old_tzobj->tzi.z.abbr != NULL + ? timelib_strdup(old_tzobj->tzi.z.abbr) + : NULL + ; + + return old_tzobj->tzi.z.abbr == NULL || new_tzobj->tzi.z.abbr != NULL; + default: + return false; + } + } + + if (instanceof_function(old_object->ce, date_ce_interval)) { + old_intervalobj = php_interval_obj_from_obj(old_object); + new_intervalobj = php_interval_obj_from_obj(new_object); + + new_intervalobj->civil_or_wall = old_intervalobj->civil_or_wall; + new_intervalobj->from_string = old_intervalobj->from_string; + + if (old_intervalobj->date_string != NULL) { + new_intervalobj->date_string = zend_string_copy(old_intervalobj->date_string); + } + + new_intervalobj->initialized = old_intervalobj->initialized; + + if (old_intervalobj->diff != NULL) { + new_intervalobj->diff = timelib_rel_time_clone(old_intervalobj->diff); + + return new_intervalobj->diff != NULL; + } + + return true; + } + + return false; +} + +static bool php_date_serialize_datetime_user_cache_state(php_date_obj *dateobj, zval *state) +{ + timelib_time snapshot; + zval zv; + + if (dateobj->time == NULL || !dateobj->time->is_localtime) { + return false; + } + + array_init_size(state, 4); + + php_date_user_cache_copy_time_snapshot(&snapshot, dateobj->time); + + ZVAL_NULL(&zv); + zend_hash_str_update(Z_ARRVAL_P(state), PHP_DATE_USER_CACHE_STATE_DATE, PHP_DATE_USER_CACHE_STATE_DATE_LEN, &zv); + + ZVAL_LONG(&zv, dateobj->time->zone_type); + zend_hash_str_update(Z_ARRVAL_P(state), PHP_DATE_USER_CACHE_STATE_TIMEZONE_TYPE, PHP_DATE_USER_CACHE_STATE_TIMEZONE_TYPE_LEN, &zv); + + switch (dateobj->time->zone_type) { + case TIMELIB_ZONETYPE_ID: + ZVAL_STRING(&zv, dateobj->time->tz_info->name); + break; + case TIMELIB_ZONETYPE_OFFSET: + ZVAL_NEW_STR(&zv, date_create_tz_offset_str(dateobj->time->z)); + break; + case TIMELIB_ZONETYPE_ABBR: + ZVAL_STRING(&zv, dateobj->time->tz_abbr); + break; + default: + zval_ptr_dtor(state); + ZVAL_UNDEF(state); + + return false; + } + zend_hash_str_update(Z_ARRVAL_P(state), PHP_DATE_USER_CACHE_STATE_TIMEZONE, PHP_DATE_USER_CACHE_STATE_TIMEZONE_LEN, &zv); + + ZVAL_STRINGL(&zv, (const char *) &snapshot, sizeof(snapshot)); + zend_hash_str_update(Z_ARRVAL_P(state), PHP_DATE_USER_CACHE_STATE_CTIME, PHP_DATE_USER_CACHE_STATE_CTIME_LEN, &zv); + + return true; +} + +static bool php_date_serialize_user_cache_state(const zval *object, zval *state) +{ + php_date_obj *dateobj; + php_timezone_obj *tzobj; + php_interval_obj *intervalobj; + + ZVAL_UNDEF(state); + + if (instanceof_function(Z_OBJCE_P(object), date_ce_date) || + instanceof_function(Z_OBJCE_P(object), date_ce_immutable) + ) { + dateobj = Z_PHPDATE_P((zval *) object); + + if (dateobj->time == NULL || !dateobj->time->is_localtime) { + return false; + } + + return php_date_serialize_datetime_user_cache_state(dateobj, state); + } + + if (instanceof_function(Z_OBJCE_P(object), date_ce_timezone)) { + 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)) { + 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_user_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_user_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_datetime_user_cache_state(zval *object, zval *state) +{ + php_date_obj *dateobj; + timelib_time *time; + timelib_tzinfo *tzi; + zval *z_ctime, *z_timezone; + + z_ctime = zend_hash_str_find(Z_ARRVAL_P(state), PHP_DATE_USER_CACHE_STATE_CTIME, PHP_DATE_USER_CACHE_STATE_CTIME_LEN); + if (z_ctime == NULL) { + return false; + } + + if (Z_TYPE_P(z_ctime) != IS_STRING || Z_STRLEN_P(z_ctime) != sizeof(timelib_time)) { + return false; + } + + time = timelib_time_ctor(); + if (time == NULL) { + return false; + } + + memcpy(time, Z_STRVAL_P(z_ctime), sizeof(timelib_time)); + time->tz_abbr = NULL; + time->tz_info = NULL; + + switch (time->zone_type) { + case TIMELIB_ZONETYPE_ID: + z_timezone = zend_hash_str_find(Z_ARRVAL_P(state), PHP_DATE_USER_CACHE_STATE_TIMEZONE, PHP_DATE_USER_CACHE_STATE_TIMEZONE_LEN); + if (z_timezone == NULL || Z_TYPE_P(z_timezone) != IS_STRING || + UNEXPECTED(zend_str_has_nul_byte(Z_STR_P(z_timezone))) + ) { + timelib_time_dtor(time); + + return false; + } + + tzi = php_date_parse_tzfile(Z_STRVAL_P(z_timezone), DATE_TIMEZONEDB); + if (tzi == NULL) { + timelib_time_dtor(time); + + return false; + } + time->tz_info = tzi; + break; + case TIMELIB_ZONETYPE_OFFSET: + break; + case TIMELIB_ZONETYPE_ABBR: + z_timezone = zend_hash_str_find(Z_ARRVAL_P(state), PHP_DATE_USER_CACHE_STATE_TIMEZONE, PHP_DATE_USER_CACHE_STATE_TIMEZONE_LEN); + if (z_timezone == NULL || Z_TYPE_P(z_timezone) != IS_STRING || + UNEXPECTED(zend_str_has_nul_byte(Z_STR_P(z_timezone))) + ) { + timelib_time_dtor(time); + + return false; + } + + time->tz_abbr = timelib_strdup(Z_STRVAL_P(z_timezone)); + if (time->tz_abbr == NULL) { + timelib_time_dtor(time); + + return false; + } + break; + default: + timelib_time_dtor(time); + + return false; + } + + dateobj = Z_PHPDATE_P(object); + if (dateobj->time != NULL) { + timelib_time_dtor(dateobj->time); + } + dateobj->time = time; + + return true; +} + +static bool php_date_unserialize_user_cache_state(zval *object, zval *state) +{ + php_date_obj *dateobj; + php_timezone_obj *tzobj; + + 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) + ) { + dateobj = Z_PHPDATE_P(object); + + if (php_date_unserialize_datetime_user_cache_state(object, state)) { + return true; + } + if (EG(exception)) { + return false; + } + + return php_date_initialize_from_hash(&dateobj, Z_ARRVAL_P(state)); + } + + if (instanceof_function(Z_OBJCE_P(object), date_ce_timezone)) { + 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; +} + +static const zend_opcache_user_cache_safe_direct_handlers *php_date_get_user_cache_handlers(void) +{ + static const zend_opcache_user_cache_safe_direct_handlers handlers = { + true, + php_date_copy_user_cache_state, + NULL, + php_date_serialize_user_cache_state, + php_date_unserialize_user_cache_state + }; + + return &handlers; +} + +static void php_date_register_user_cache_handlers(void) +{ + const zend_opcache_user_cache_safe_direct_handlers *handlers; + + handlers = php_date_get_user_cache_handlers(); + zend_opcache_user_cache_safe_direct_register_class(date_ce_date, handlers); + zend_opcache_user_cache_safe_direct_register_class(date_ce_immutable, handlers); + zend_opcache_user_cache_safe_direct_register_class(date_ce_timezone, handlers); + zend_opcache_user_cache_safe_direct_register_class(date_ce_interval, handlers); +} + /* {{{ */ PHP_METHOD(DateInterval, __set_state) { diff --git a/ext/opcache/ZendAccelerator.c b/ext/opcache/ZendAccelerator.c index 83e933b01181..13a1e4d5ba7a 100644 --- a/ext/opcache/ZendAccelerator.c +++ b/ext/opcache/ZendAccelerator.c @@ -45,6 +45,7 @@ #include "zend_accelerator_util_funcs.h" #include "zend_accelerator_hash.h" #include "zend_file_cache.h" +#include "zend_user_cache.h" #include "zend_system_id.h" #include "ext/pcre/php_pcre.h" #include "ext/standard/basic_functions.h" @@ -2820,6 +2821,8 @@ zend_result accel_post_deactivate(void) ZCG(cwd) = NULL; } + zend_opcache_user_cache_post_deactivate(); + if (!ZCG(enabled) || !accel_startup_ok) { return SUCCESS; } diff --git a/ext/opcache/ZendAccelerator.h b/ext/opcache/ZendAccelerator.h index 91642e288d31..834d9fb92d5e 100644 --- a/ext/opcache/ZendAccelerator.h +++ b/ext/opcache/ZendAccelerator.h @@ -141,6 +141,7 @@ typedef struct _zend_persistent_script { typedef struct _zend_accel_directives { zend_long memory_consumption; + zend_long user_cache_shm_size; 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 3798499a4511..5f679dd3f383 100644 --- a/ext/opcache/config.m4 +++ b/ext/opcache/config.m4 @@ -331,6 +331,10 @@ PHP_NEW_EXTENSION([opcache], m4_normalize([ zend_accelerator_hash.c zend_accelerator_module.c zend_accelerator_util_funcs.c + zend_user_cache.c + zend_user_cache_storage.c + zend_user_cache_shared_graph.c + zend_user_cache_entries.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 1ad346b4da31..7df457bcf000 100644 --- a/ext/opcache/config.w32 +++ b/ext/opcache/config.w32 @@ -10,6 +10,10 @@ ZEND_EXTENSION('opcache', "\ zend_accelerator_hash.c \ zend_accelerator_module.c \ zend_accelerator_util_funcs.c \ + zend_user_cache.c \ + zend_user_cache_storage.c \ + zend_user_cache_shared_graph.c \ + zend_user_cache_entries.c \ zend_persist.c \ zend_persist_calc.c \ zend_file_cache.c \ diff --git a/ext/opcache/opcache.stub.php b/ext/opcache/opcache.stub.php index 32673bb1dcee..b9733b6e8cca 100644 --- a/ext/opcache/opcache.stub.php +++ b/ext/opcache/opcache.stub.php @@ -4,6 +4,8 @@ function opcache_reset(): bool {} +function opcache_user_cache_reset(): bool {} + /** * @return array|false * @refcount 1 diff --git a/ext/opcache/opcache_arginfo.h b/ext/opcache/opcache_arginfo.h index 60a1633154c9..2454ce624c1a 100644 --- a/ext/opcache/opcache_arginfo.h +++ b/ext/opcache/opcache_arginfo.h @@ -1,9 +1,11 @@ /* This is a generated file, edit opcache.stub.php instead. - * Stub hash: a8de025fa96a78db3a26d53a18bb2b365d094eca */ + * Stub hash: 1eaa355c203af73871d5c9068de8d4a742847ffa */ ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_opcache_reset, 0, 0, _IS_BOOL, 0) ZEND_END_ARG_INFO() +#define arginfo_opcache_user_cache_reset arginfo_opcache_reset + ZEND_BEGIN_ARG_WITH_RETURN_TYPE_MASK_EX(arginfo_opcache_get_status, 0, 0, MAY_BE_ARRAY|MAY_BE_FALSE) ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, include_scripts, _IS_BOOL, 0, "true") ZEND_END_ARG_INFO() @@ -29,6 +31,7 @@ ZEND_END_ARG_INFO() #define arginfo_opcache_is_script_cached_in_file_cache arginfo_opcache_compile_file ZEND_FUNCTION(opcache_reset); +ZEND_FUNCTION(opcache_user_cache_reset); ZEND_FUNCTION(opcache_get_status); ZEND_FUNCTION(opcache_compile_file); ZEND_FUNCTION(opcache_invalidate); @@ -39,6 +42,7 @@ ZEND_FUNCTION(opcache_is_script_cached_in_file_cache); static const zend_function_entry ext_functions[] = { ZEND_FE(opcache_reset, arginfo_opcache_reset) + ZEND_FE(opcache_user_cache_reset, arginfo_opcache_user_cache_reset) ZEND_FE(opcache_get_status, arginfo_opcache_get_status) ZEND_FE(opcache_compile_file, arginfo_opcache_compile_file) ZEND_FE(opcache_invalidate, arginfo_opcache_invalidate) diff --git a/ext/opcache/opcache_user_cache.stub.php b/ext/opcache/opcache_user_cache.stub.php new file mode 100644 index 000000000000..5370e05057f5 --- /dev/null +++ b/ext/opcache/opcache_user_cache.stub.php @@ -0,0 +1,75 @@ + +--FILE-- +id = $id; + $this->name = $name; + } + + public function __serialize(): array + { + return ['id' => -1, 'name' => 'wrong']; + } + + public function __unserialize(array $data): void + { + $this->id = -2; + $this->name = 'wrong'; + } + + public function info(): string + { + return $this->id . ':' . $this->name; + } +} + +$cache = new Opcache\UserCache('default'); +$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']; + + $cache->clear(); + var_dump($cache->store('complex', $payload)); + var_dump($cache->store('shared_pair', [$shared, $shared])); + var_dump($cache->store('refs', $refs)); + echo "seed\n"; + return; +} + +$complex = $cache->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 = $cache->fetch('shared_pair'); +var_dump(spl_object_id($pair[0]) === spl_object_id($pair[1])); + +$refs = $cache->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.user_cache_shm_size' => '32M', +]); +$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/user_cache_fpm_decode_failure_delete_001.phpt b/ext/opcache/tests/fpm/user_cache_fpm_decode_failure_delete_001.phpt new file mode 100644 index 000000000000..25c938f634bd --- /dev/null +++ b/ext/opcache/tests/fpm/user_cache_fpm_decode_failure_delete_001.phpt @@ -0,0 +1,117 @@ +--TEST-- +FPM: OPcache User Cache deletes entries that fail to decode +--EXTENSIONS-- +opcache +--SKIPIF-- + +--FILE-- +clear(); + var_dump($cache->store('single', new UserCacheGone())); + var_dump($cache->store('multiple', new UserCacheGone())); + var_dump($cache->store('autoload-single', new UserCacheAutoloadThrows())); + var_dump($cache->store('autoload-multiple', new UserCacheAutoloadThrows())); + var_dump($cache->has('single')); + var_dump($cache->has('multiple')); + var_dump($cache->has('autoload-single')); + var_dump($cache->has('autoload-multiple')); + return; +} + +if ($action === 'fetch') { + spl_autoload_register(function (string $class): void { + if ($class === 'UserCacheAutoloadThrows') { + throw new Exception('autoload failed'); + } + }); + + var_dump($cache->fetch('single', 'DEFAULT')); + var_dump($cache->has('single')); + var_dump($cache->fetch('autoload-single', 'DEFAULT')); + var_dump($cache->has('autoload-single')); + var_dump($cache->fetchMultiple(['multiple', 'missing', 'autoload-multiple'], 'DEFAULT')); + var_dump($cache->has('multiple')); + var_dump($cache->has('autoload-multiple')); + return; +} +PHP; + +$tester = new FPM\Tester($cfg, $code); +$tester->start(iniEntries: [ + 'opcache.enable' => '1', + 'opcache.user_cache_shm_size' => '32M', +]); +$tester->expectLogStartNotices(); + +$tester->request(query: 'action=seed')->expectBody( + "bool(true)\n" . + "bool(true)\n" . + "bool(true)\n" . + "bool(true)\n" . + "bool(true)\n" . + "bool(true)\n" . + "bool(true)\n" . + "bool(true)" +); + +$tester->request(query: 'action=fetch')->expectBody( + "string(7) \"DEFAULT\"\n" . + "bool(false)\n" . + "string(7) \"DEFAULT\"\n" . + "bool(false)\n" . + "array(3) {\n" . + " [\"multiple\"]=>\n" . + " string(7) \"DEFAULT\"\n" . + " [\"missing\"]=>\n" . + " string(7) \"DEFAULT\"\n" . + " [\"autoload-multiple\"]=>\n" . + " string(7) \"DEFAULT\"\n" . + "}\n" . + "bool(false)\n" . + "bool(false)" +); + +$tester->terminate(); +$tester->expectLogTerminatingNotices(); +$tester->close(); + +echo "Done\n"; + +?> +--EXPECT-- +Done +--CLEAN-- + diff --git a/ext/opcache/tests/fpm/user_cache_fpm_lookup_cache_001.phpt b/ext/opcache/tests/fpm/user_cache_fpm_lookup_cache_001.phpt new file mode 100644 index 000000000000..1d7700a0c1d0 --- /dev/null +++ b/ext/opcache/tests/fpm/user_cache_fpm_lookup_cache_001.phpt @@ -0,0 +1,94 @@ +--TEST-- +FPM: OPcache User Cache request-local lookup cache sees same-request updates +--EXTENSIONS-- +opcache +--SKIPIF-- + +--FILE-- +fetch($key, 'MISS'); +} + +$cache = new Opcache\UserCache('default'); +$scenario = $_GET['scenario'] ?? 'hit_store'; +$key = 'fpm_lookup_cache_' . $scenario . '_key'; +$cache->clear(); + +if ($scenario === 'hit_store') { + $cache->store($key, 'old'); + $first = fetch_or_miss($cache, $key); + $cache->store($key, 'new'); + echo $first, "\n", fetch_or_miss($cache, $key); + return; +} + +if ($scenario === 'miss_store') { + $first = fetch_or_miss($cache, $key); + $cache->store($key, 'created'); + echo $first, "\n", fetch_or_miss($cache, $key); + return; +} + +if ($scenario === 'hit_delete') { + $cache->store($key, 'old'); + $first = fetch_or_miss($cache, $key); + $cache->delete($key); + echo $first, "\n", fetch_or_miss($cache, $key); + return; +} + +if ($scenario === 'hit_clear') { + $cache->store($key, 'old'); + $first = fetch_or_miss($cache, $key); + $cache->clear(); + echo $first, "\n", fetch_or_miss($cache, $key); + return; +} + +throw new RuntimeException('unknown scenario ' . $scenario); +PHP; + +$tester = new FPM\Tester($cfg, $code); +$tester->start(iniEntries: [ + 'opcache.enable' => '1', + 'opcache.user_cache_shm_size' => '32M', +]); +$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/user_cache_fpm_opcache_reset_001.phpt b/ext/opcache/tests/fpm/user_cache_fpm_opcache_reset_001.phpt new file mode 100644 index 000000000000..55288c20e8f0 --- /dev/null +++ b/ext/opcache/tests/fpm/user_cache_fpm_opcache_reset_001.phpt @@ -0,0 +1,104 @@ +--TEST-- +FPM: opcache_reset and opcache_user_cache_reset clear OPcache User Cache partitions +--EXTENSIONS-- +opcache +--SKIPIF-- + +--FILE-- +store($key, $pool . '-value'); +} elseif ($action === 'reset') { + var_dump(opcache_reset()); +} elseif ($action === 'user-reset') { + var_dump(opcache_user_cache_reset()); +} + +printf("%s:%s\n", $pool, $cache->fetch($key, 'MISS')); +PHP; + +function requestBody(FPM\Tester $tester, string $pool, string $action): string +{ + return trim((string) $tester->request( + query: 'action=' . $action . '&pool=' . $pool, + address: '{{ADDR[' . $pool . ']}}', + )->getBody()); +} + +$tester = new FPM\Tester($cfg, $code); +$tester->start(iniEntries: [ + 'opcache.enable' => '1', + 'opcache.user_cache_shm_size' => '32M', + 'opcache.file_update_protection' => '0', +]); +$tester->expectLogStartNotices(); + +var_dump(requestBody($tester, 'alpha', 'seed')); +var_dump(requestBody($tester, 'beta', 'seed')); +var_dump(requestBody($tester, 'alpha', 'fetch')); +var_dump(requestBody($tester, 'beta', 'fetch')); +var_dump(requestBody($tester, 'alpha', 'reset')); +var_dump(requestBody($tester, 'alpha', 'fetch')); +var_dump(requestBody($tester, 'beta', 'fetch')); +var_dump(requestBody($tester, 'alpha', 'seed')); +var_dump(requestBody($tester, 'beta', 'seed')); +var_dump(requestBody($tester, 'alpha', 'fetch')); +var_dump(requestBody($tester, 'beta', 'fetch')); +var_dump(requestBody($tester, 'beta', 'user-reset')); +var_dump(requestBody($tester, 'alpha', 'fetch')); +var_dump(requestBody($tester, 'beta', 'fetch')); + +$tester->terminate(); +$tester->expectLogTerminatingNotices(); +$tester->close(); + +?> +--EXPECT-- +string(17) "alpha:alpha-value" +string(15) "beta:beta-value" +string(17) "alpha:alpha-value" +string(15) "beta:beta-value" +string(21) "bool(true) +alpha:MISS" +string(10) "alpha:MISS" +string(9) "beta:MISS" +string(17) "alpha:alpha-value" +string(15) "beta:beta-value" +string(17) "alpha:alpha-value" +string(15) "beta:beta-value" +string(20) "bool(true) +beta:MISS" +string(10) "alpha:MISS" +string(9) "beta:MISS" +--CLEAN-- + diff --git a/ext/opcache/tests/fpm/user_cache_fpm_persistence_001.phpt b/ext/opcache/tests/fpm/user_cache_fpm_persistence_001.phpt new file mode 100644 index 000000000000..bf8f0022fe4a --- /dev/null +++ b/ext/opcache/tests/fpm/user_cache_fpm_persistence_001.phpt @@ -0,0 +1,106 @@ +--TEST-- +FPM: OPcache User Cache persists across requests +--EXTENSIONS-- +opcache +--SKIPIF-- + +--FILE-- +clear(); + var_dump($cache->store('counter', 41)); + var_dump($cache->storeMultiple([ + 'message' => 'hello from fpm', + 'payload' => ['a' => 1, 'b' => [2, 3]], + ])); + var_dump($cache->store('persistent_key', 'long-lived')); + echo "seed\n"; + return; +} + +if ($action === 'fetch') { + var_dump($cache->fetch('counter')); + var_dump($cache->fetch('message')); + var_dump($cache->fetch('payload')); + $cache->deleteMultiple(['message']); + echo $cache->fetch('message', 'MISS'), "\n"; + echo "fetch\n"; + return; +} + +echo $cache->fetch('persistent_key', 'MISS'), "\n"; +echo "persist\n"; +PHP; + +$tester = new FPM\Tester($cfg, $code); +$tester->start(iniEntries: [ + 'opcache.enable' => '1', + 'opcache.user_cache_shm_size' => '32M', +]); +$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(1); + +$tester->request(query: 'action=persist')->expectBody( + "long-lived\n" . + "persist" +); + +$tester->terminate(); +$tester->expectLogTerminatingNotices(); +$tester->close(); + +echo "Done\n"; + +?> +--EXPECT-- +Done +--CLEAN-- + diff --git a/ext/opcache/tests/fpm/user_cache_fpm_pool_separate_001.phpt b/ext/opcache/tests/fpm/user_cache_fpm_pool_separate_001.phpt new file mode 100644 index 000000000000..4d75266afa4c --- /dev/null +++ b/ext/opcache/tests/fpm/user_cache_fpm_pool_separate_001.phpt @@ -0,0 +1,111 @@ +--TEST-- +FPM: OPcache User Cache is separated between pools +--EXTENSIONS-- +opcache +--SKIPIF-- + +--FILE-- +store($key, $pool . '-value'); +} + +printf( + "%s:%s:%s\n", + $pool, + $cache->fetch($key, 'MISS'), + $cache->info()->scope +); +PHP; + +function expectPoolState(FPM\Tester $tester, string $pool, string $expected): void +{ + $response = $tester->request( + query: 'action=fetch&pool=' . $pool, + address: '{{ADDR[' . $pool . ']}}', + ); + $body = trim((string) $response->getBody()); + if ($body !== $expected) { + throw new RuntimeException(sprintf( + 'Unexpected state for pool %s: expected %s, got %s', + $pool, + $expected, + $body + )); + } +} + +function seedPool(FPM\Tester $tester, string $pool, string $expected): void +{ + $response = $tester->request( + query: 'action=seed&pool=' . $pool, + address: '{{ADDR[' . $pool . ']}}', + ); + $body = trim((string) $response->getBody()); + if ($body !== $expected) { + throw new RuntimeException(sprintf( + 'Unexpected seed state for pool %s: expected %s, got %s', + $pool, + $expected, + $body + )); + } +} + +$tester = new FPM\Tester($cfg, $code); +$tester->start(iniEntries: [ + 'opcache.enable' => '1', + 'opcache.user_cache_shm_size' => '32M', + 'opcache.file_update_protection' => '0', +]); +$tester->expectLogStartNotices(); + +seedPool($tester, 'alpha', 'alpha:alpha-value:default'); +expectPoolState($tester, 'alpha', 'alpha:alpha-value:default'); +expectPoolState($tester, 'beta', 'beta:MISS:default'); + +seedPool($tester, 'beta', 'beta:beta-value:default'); +expectPoolState($tester, 'alpha', 'alpha:alpha-value:default'); +expectPoolState($tester, 'beta', 'beta:beta-value:default'); + +$tester->terminate(); +$tester->expectLogTerminatingNotices(); +$tester->close(); + +echo "Done\n"; + +?> +--EXPECT-- +Done +--CLEAN-- + diff --git a/ext/opcache/tests/fpm/user_cache_fpm_remember_callable_lifetime_001.phpt b/ext/opcache/tests/fpm/user_cache_fpm_remember_callable_lifetime_001.phpt new file mode 100644 index 000000000000..f1829144a4e5 --- /dev/null +++ b/ext/opcache/tests/fpm/user_cache_fpm_remember_callable_lifetime_001.phpt @@ -0,0 +1,120 @@ +--TEST-- +FPM: OPcache User Cache remember does not retain callables across requests +--EXTENSIONS-- +opcache +--SKIPIF-- + +--FILE-- +clear(); + + $token = 'seed-token'; + $carrier = new RememberCarrier('captured-object'); + $resource = tmpfile(); + fwrite($resource, 'request-local-resource'); + + $value = $cache->remember('captured', function () use (&$token, $carrier, $resource) { + return [ + 'token' => $token, + 'carrier' => $carrier->name, + 'position' => ftell($resource), + ]; + }); + + var_dump($value); + echo "seed\n"; + return; +} + +if ($action === 'hit') { + $poison = 'second-request'; + $value = $cache->remember('captured', function () use (&$poison) { + echo "CALLBACK_RAN:$poison\n"; + throw new RuntimeException('remember callback must not run on cache hit'); + }); + + var_dump($value); + echo "hit\n"; + return; +} + +$cache->delete('captured'); +$value = $cache->remember('captured', function () { + return 'fresh-after-delete'; +}); + +var_dump($value); +echo "miss\n"; +PHP; + +$tester = new FPM\Tester($cfg, $code); +$tester->start(iniEntries: [ + 'opcache.enable' => '1', + 'opcache.user_cache_shm_size' => '32M', +]); +$tester->expectLogStartNotices(); + +$expectedValue = + "array(3) {\n" . + " [\"token\"]=>\n" . + " string(10) \"seed-token\"\n" . + " [\"carrier\"]=>\n" . + " string(15) \"captured-object\"\n" . + " [\"position\"]=>\n" . + " int(22)\n" . + "}"; + +$tester->request(query: 'action=seed')->expectBody( + $expectedValue . "\n" . + "seed" +); + +$tester->request(query: 'action=hit')->expectBody( + $expectedValue . "\n" . + "hit" +); + +$tester->request(query: 'action=miss')->expectBody( + "string(18) \"fresh-after-delete\"\n" . + "miss" +); + +$tester->terminate(); +$tester->expectLogTerminatingNotices(); +$tester->close(); + +echo "Done\n"; + +?> +--EXPECT-- +Done +--CLEAN-- + diff --git a/ext/opcache/tests/fpm/user_cache_fpm_safe_direct_001.phpt b/ext/opcache/tests/fpm/user_cache_fpm_safe_direct_001.phpt new file mode 100644 index 000000000000..8900861554e0 --- /dev/null +++ b/ext/opcache/tests/fpm/user_cache_fpm_safe_direct_001.phpt @@ -0,0 +1,141 @@ +--TEST-- +FPM: OPcache User Cache safe-direct DateTime and SPL state survives requests +--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; + } +} + +$cache = new Opcache\UserCache('default'); +$action = $_GET['action'] ?? 'seed'; + +if ($action === 'seed') { + $cache->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); + $fixed = SplFixedArray::fromArray(['zero', 'one', ['two']], false); + $queue = new SplQueue(); + $queue->enqueue('q1'); + $queue->enqueue('q2'); + $priorityQueue = new SplPriorityQueue(); + $priorityQueue->setExtractFlags(SplPriorityQueue::EXTR_BOTH); + $priorityQueue->insert('low', 1); + $priorityQueue->insert('high', 10); + + var_dump($cache->store('safe_direct_payload', [ + 'event' => $event, + 'collection' => $collection, + 'fixed' => $fixed, + 'queue' => $queue, + 'priorityQueue' => $priorityQueue, + ])); + echo "seed\n"; + return; +} + +$payload = $cache->fetch('safe_direct_payload'); +$event = $payload['event']; +$collection = $payload['collection']; +$iterator = $collection->getIterator(); +$fixed = $payload['fixed']; +$queue = $payload['queue']; +$priorityQueue = $payload['priorityQueue']; + +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 $fixed->getSize(), ',', $fixed[2][0], "\n"; +echo $queue->dequeue(), ',', $queue->dequeue(), "\n"; +$top = $priorityQueue->extract(); +echo $top['data'], ',', $top['priority'], "\n"; +PHP; + +$tester = new FPM\Tester($cfg, $code); +$tester->start(iniEntries: [ + 'opcache.enable' => '1', + 'opcache.user_cache_shm_size' => '32M', +]); +$tester->expectLogStartNotices(); + +$tester->request(query: 'action=seed')->expectBody( + "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" . + "3,two\n" . + "q1,q2\n" . + "high,10" +); + +$tester->terminate(); +$tester->expectLogTerminatingNotices(); +$tester->close(); + +echo "Done\n"; + +?> +--EXPECT-- +Done +--CLEAN-- + diff --git a/ext/opcache/tests/user_cache_apache2handler_boundary_001.phpt b/ext/opcache/tests/user_cache_apache2handler_boundary_001.phpt new file mode 100644 index 000000000000..b533891b9a27 --- /dev/null +++ b/ext/opcache/tests/user_cache_apache2handler_boundary_001.phpt @@ -0,0 +1,209 @@ +--TEST-- +OPcache User Cache: apache2handler partitions cache data by virtual host +--EXTENSIONS-- +opcache +--SKIPIF-- + +--FILE-- +store($key, $host . '-value'); +} + +echo $host, ':', $cache->fetch($key, 'MISS'), ':', $cache->info()->scope, "\n"; +PHP; + +file_put_contents($alphaRoot . '/index.php', $script); +file_put_contents($betaRoot . '/index.php', $script); +file_put_contents($root . '/php.ini', implode("\n", [ + 'opcache.enable=1', + 'opcache.user_cache_shm_size=32M', + 'opcache.file_update_protection=0', +])); + +$port = user_cache_apache_free_port(); +$httpd = getenv('TEST_PHP_APACHE2HANDLER_HTTPD'); +$module = getenv('TEST_PHP_APACHE2HANDLER_MODULE'); +$moduleName = getenv('TEST_PHP_APACHE2HANDLER_MODULE_NAME') ?: 'php_module'; +$extraConfig = getenv('TEST_PHP_APACHE2HANDLER_EXTRA_CONFIG') ?: ''; +$conf = $root . '/httpd.conf'; + +file_put_contents($conf, << + ServerName alpha.local + DocumentRoot "$alphaRoot" + + Require all granted + AllowOverride None + + + SetHandler application/x-httpd-php + + + + + ServerName beta.local + DocumentRoot "$betaRoot" + + Require all granted + AllowOverride None + + + SetHandler application/x-httpd-php + + +CONF); + +try { + $process = proc_open( + [$httpd, '-X', '-f', $conf], + [['pipe', 'r'], ['pipe', 'w'], ['pipe', 'w']], + $pipes, + $root + ); + if (!is_resource($process)) { + throw new RuntimeException('Unable to start httpd'); + } + stream_set_blocking($pipes[1], false); + stream_set_blocking($pipes[2], false); + + user_cache_apache_wait($process, $pipes, $port); + + $checks = [ + ['alpha.local', '/index.php?action=seed', 'alpha.local:alpha.local-value:default'], + ['alpha.local', '/index.php?action=fetch', 'alpha.local:alpha.local-value:default'], + ['beta.local', '/index.php?action=fetch', 'beta.local:MISS:default'], + ['beta.local', '/index.php?action=seed', 'beta.local:beta.local-value:default'], + ['alpha.local', '/index.php?action=fetch', 'alpha.local:alpha.local-value:default'], + ['beta.local', '/index.php?action=fetch', 'beta.local:beta.local-value:default'], + ]; + + foreach ($checks as [$host, $path, $expected]) { + $actual = user_cache_apache_request($port, $host, $path); + if ($actual !== $expected) { + throw new RuntimeException("Expected $expected, got $actual"); + } + } + + echo "Done\n"; +} finally { + if (is_resource($process)) { + proc_terminate($process); + proc_close($process); + } + user_cache_apache_rm_rf($root); +} + +?> +--EXPECT-- +Done diff --git a/ext/opcache/tests/user_cache_atomic.phpt b/ext/opcache/tests/user_cache_atomic.phpt new file mode 100644 index 000000000000..632b6f809150 --- /dev/null +++ b/ext/opcache/tests/user_cache_atomic.phpt @@ -0,0 +1,64 @@ +--TEST-- +OPcache User Cache: atomic increment and decrement +--EXTENSIONS-- +opcache +--INI-- +opcache.enable=1 +opcache.enable_cli=1 +opcache.file_cache_only=0 +opcache.user_cache_shm_size=16M +--FILE-- +clear(); + +var_dump($cache->increment('counter')); +var_dump($cache->increment('counter', 4)); +var_dump($cache->decrement('counter', 2)); +var_dump($cache->fetch('counter')); + +var_dump($cache->decrement('missing', 3)); +var_dump($cache->fetch('missing')); + +var_dump($cache->store('string', 'x')); +var_dump($cache->increment('string')); +var_dump($cache->fetch('string')); + +var_dump($cache->store('max', PHP_INT_MAX)); +var_dump($cache->increment('max')); +var_dump($cache->fetch('max') === PHP_INT_MAX); + +var_dump($cache->store('min', PHP_INT_MIN)); +var_dump($cache->decrement('min')); +var_dump($cache->fetch('min') === PHP_INT_MIN); + +try { + $cache->increment('counter', -1); +} catch (ValueError $e) { + echo $e->getMessage(), "\n"; +} + +try { + $cache->decrement('counter', -1); +} catch (ValueError $e) { + echo $e->getMessage(), "\n"; +} +?> +--EXPECTF-- +int(1) +int(5) +int(3) +int(3) +int(-3) +int(-3) +bool(true) +bool(false) +string(1) "x" +bool(true) +bool(false) +bool(true) +bool(true) +bool(false) +bool(true) +Opcache\UserCache::increment(): Argument #2 ($step) must be greater than or equal to 0 +Opcache\UserCache::decrement(): Argument #2 ($step) must be greater than or equal to 0 diff --git a/ext/opcache/tests/user_cache_basic.phpt b/ext/opcache/tests/user_cache_basic.phpt new file mode 100644 index 000000000000..cb2017f75eda --- /dev/null +++ b/ext/opcache/tests/user_cache_basic.phpt @@ -0,0 +1,86 @@ +--TEST-- +OPcache User Cache: basic API +--EXTENSIONS-- +opcache +--INI-- +opcache.enable=1 +opcache.enable_cli=1 +opcache.file_cache_only=0 +opcache.user_cache_shm_size=16M +--FILE-- +info(); +var_dump($info->scope, $info->available, $info->unavailableReason); + +var_dump($cache->store('array', ['x' => 1])); +var_dump($cache->fetch('array')); +var_dump($cache->has('array')); + +$fetched = $cache->fetch('array'); +$fetched['x'] = 2; +var_dump($cache->fetch('array')); + +var_dump($cache->storeMultiple(['one' => 1, 'two' => 'x'], 60)); +var_dump($cache->fetch('one')); +var_dump($cache->fetchMultiple(['one', 'missing'], 'fallback')); +var_dump($cache->fetch('missing', 'fallback')); + +var_dump($cache->delete('one')); +var_dump($cache->has('one')); + +var_dump($cache->lock('lock')); +var_dump($cache->unlock('lock')); + +var_dump($cache->lock('locked-store')); +var_dump($cache->store('locked-store', 'value')); +var_dump($cache->unlock('locked-store')); + +var_dump($cache->lock('locked-delete')); +var_dump($cache->store('locked-delete', 'value')); +var_dump($cache->delete('locked-delete')); +var_dump($cache->unlock('locked-delete')); + +var_dump($cache->lock('locked-clear')); +var_dump($cache->clear()); +var_dump($cache->unlock('locked-clear')); +var_dump($cache->has('two')); +?> +--EXPECT-- +string(5) "basic" +bool(true) +NULL +bool(true) +array(1) { + ["x"]=> + int(1) +} +bool(true) +array(1) { + ["x"]=> + int(1) +} +bool(true) +int(1) +array(2) { + ["one"]=> + int(1) + ["missing"]=> + string(8) "fallback" +} +string(8) "fallback" +bool(true) +bool(false) +bool(true) +bool(true) +bool(true) +bool(true) +bool(true) +bool(true) +bool(true) +bool(true) +bool(true) +bool(true) +bool(true) +bool(true) +bool(false) diff --git a/ext/opcache/tests/user_cache_cgi_001.phpt b/ext/opcache/tests/user_cache_cgi_001.phpt new file mode 100644 index 000000000000..f5e82c097cc3 --- /dev/null +++ b/ext/opcache/tests/user_cache_cgi_001.phpt @@ -0,0 +1,28 @@ +--TEST-- +CGI: OPcache User Cache is available +--CGI-- +--EXTENSIONS-- +opcache +--INI-- +opcache.enable=1 +opcache.file_cache_only=0 +opcache.user_cache_shm_size=16M +--ENV-- +DOCUMENT_ROOT=/tmp/php-user-cache-cgi +SERVER_NAME=php-user-cache.local +--FILE-- +info(); + +var_dump(php_sapi_name()); +var_dump($info->available, $info->unavailableReason); +var_dump($cache->store('key', 'value')); +var_dump($cache->fetch('key', 'MISS')); +?> +--EXPECT-- +string(8) "cgi-fcgi" +bool(true) +NULL +bool(true) +string(5) "value" diff --git a/ext/opcache/tests/user_cache_cgi_boundary_001.phpt b/ext/opcache/tests/user_cache_cgi_boundary_001.phpt new file mode 100644 index 000000000000..e5808942afda --- /dev/null +++ b/ext/opcache/tests/user_cache_cgi_boundary_001.phpt @@ -0,0 +1,302 @@ +--TEST-- +CGI/FastCGI: OPcache User Cache partitions cache data by boundary +--EXTENSIONS-- +opcache +--CONFLICTS-- +all +--SKIPIF-- + +--FILE-- + $value) { + $body .= user_cache_cgi_name_value($name, $value); + } + + return $body; +} + +function user_cache_cgi_read_exact($fp, int $length): string +{ + $buffer = ''; + + while (strlen($buffer) < $length && !feof($fp)) { + $chunk = fread($fp, $length - strlen($buffer)); + if ($chunk === false) { + throw new RuntimeException('Failed to read FastCGI response'); + } + if ($chunk === '') { + if (stream_get_meta_data($fp)['timed_out']) { + throw new RuntimeException('Timed out reading FastCGI response'); + } + usleep(10000); + continue; + } + $buffer .= $chunk; + } + + if (strlen($buffer) !== $length) { + throw new RuntimeException('Truncated FastCGI response'); + } + + return $buffer; +} + +function user_cache_cgi_request(int $port, string $script, string $docRoot, string $serverName, string $query): string +{ + $fp = @stream_socket_client("tcp://127.0.0.1:$port", $errno, $errstr, 2); + if ($fp === false) { + throw new RuntimeException($errstr); + } + stream_set_timeout($fp, 5); + + $params = [ + 'SCRIPT_FILENAME' => $script, + 'SCRIPT_NAME' => '/index.php', + 'QUERY_STRING' => $query, + 'REQUEST_METHOD' => 'GET', + 'SERVER_NAME' => $serverName, + 'DOCUMENT_ROOT' => $docRoot, + 'REQUEST_URI' => '/index.php' . ($query !== '' ? '?' . $query : ''), + 'SERVER_PROTOCOL' => 'HTTP/1.1', + 'REMOTE_ADDR' => '127.0.0.1', + 'REDIRECT_STATUS' => '1', + ]; + + fwrite($fp, user_cache_cgi_record(FCGI_BEGIN_REQUEST, pack('nC6', FCGI_RESPONDER, 0, 0, 0, 0, 0, 0))); + fwrite($fp, user_cache_cgi_record(FCGI_PARAMS, user_cache_cgi_params($params))); + fwrite($fp, user_cache_cgi_record(FCGI_PARAMS, '')); + fwrite($fp, user_cache_cgi_record(FCGI_STDIN, '')); + + $stdout = ''; + $stderr = ''; + + while (!feof($fp)) { + $header = user_cache_cgi_read_exact($fp, 8); + $type = ord($header[1]); + $contentLength = unpack('n', substr($header, 4, 2))[1]; + $paddingLength = ord($header[6]); + $content = $contentLength > 0 ? user_cache_cgi_read_exact($fp, $contentLength) : ''; + if ($paddingLength > 0) { + user_cache_cgi_read_exact($fp, $paddingLength); + } + + if ($type === FCGI_STDOUT) { + $stdout .= $content; + } elseif ($type === FCGI_STDERR) { + $stderr .= $content; + } elseif ($type === FCGI_END_REQUEST) { + break; + } + } + + fclose($fp); + + if ($stderr !== '') { + throw new RuntimeException($stderr); + } + + $parts = preg_split("/\r?\n\r?\n/", $stdout, 2); + + return trim($parts[1] ?? $stdout); +} + +function user_cache_cgi_wait($process, array $pipes, int $port, string $script, string $docRoot, string $serverName): void +{ + for ($i = 0; $i < 50; $i++) { + $status = proc_get_status($process); + if (!$status['running']) { + throw new RuntimeException(stream_get_contents($pipes[2])); + } + + try { + user_cache_cgi_request($port, $script, $docRoot, $serverName, 'action=fetch'); + return; + } catch (Throwable) { + usleep(100000); + } + } + + throw new RuntimeException(stream_get_contents($pipes[2]) ?: 'php-cgi did not become ready'); +} + +function user_cache_cgi_rm_rf(string $path): void +{ + if (!file_exists($path)) { + return; + } + + if (!is_dir($path) || is_link($path)) { + unlink($path); + return; + } + + foreach (scandir($path) as $entry) { + if ($entry === '.' || $entry === '..') { + continue; + } + user_cache_cgi_rm_rf($path . DIRECTORY_SEPARATOR . $entry); + } + + rmdir($path); +} + +$root = sys_get_temp_dir() . '/php-user-cache-cgi-boundary-' . getmypid(); +$alphaRoot = $root . '/alpha'; +$betaRoot = $root . '/beta'; +$process = null; +$pipes = []; + +user_cache_cgi_rm_rf($root); +mkdir($alphaRoot, 0777, true); +mkdir($betaRoot, 0777, true); + +$script = <<<'PHP' +store($key, $host . '-value'); +} + +$info = $cache->info(); +echo $host, ':', $cache->fetch($key, 'MISS'), ':', ($info->available ? 'available' : $info->unavailableReason), "\n"; +PHP; + +file_put_contents($alphaRoot . '/index.php', $script); +file_put_contents($betaRoot . '/index.php', $script); +file_put_contents($root . '/php.ini', implode("\n", [ + 'opcache.enable=1', + 'opcache.user_cache_shm_size=32M', + 'opcache.file_update_protection=0', +])); + +try { + $phpCgi = user_cache_cgi_binary(); + $port = user_cache_cgi_free_port(); + $alphaScript = $alphaRoot . '/index.php'; + $betaScript = $betaRoot . '/index.php'; + + $process = proc_open( + [$phpCgi, '-c', $root, '-b', "127.0.0.1:$port"], + [['pipe', 'r'], ['pipe', 'w'], ['pipe', 'w']], + $pipes, + $root + ); + if (!is_resource($process)) { + throw new RuntimeException('Unable to start php-cgi'); + } + stream_set_blocking($pipes[1], false); + stream_set_blocking($pipes[2], false); + + user_cache_cgi_wait($process, $pipes, $port, $alphaScript, $alphaRoot, 'alpha.local'); + + $checks = [ + [$alphaScript, $alphaRoot, 'alpha.local', 'action=seed', 'alpha.local:alpha.local-value:available'], + [$alphaScript, $alphaRoot, 'alpha.local', 'action=fetch', 'alpha.local:alpha.local-value:available'], + [$betaScript, $betaRoot, 'beta.local', 'action=fetch', 'beta.local:MISS:available'], + [$betaScript, $betaRoot, 'beta.local', 'action=seed', 'beta.local:beta.local-value:available'], + [$alphaScript, $alphaRoot, 'alpha.local', 'action=fetch', 'alpha.local:alpha.local-value:available'], + [$betaScript, $betaRoot, 'beta.local', 'action=fetch', 'beta.local:beta.local-value:available'], + ]; + + foreach ($checks as [$file, $docRoot, $serverName, $query, $expected]) { + $actual = user_cache_cgi_request($port, $file, $docRoot, $serverName, $query); + if ($actual !== $expected) { + $stderr = stream_get_contents($pipes[2]); + throw new RuntimeException("Expected $expected, got $actual" . ($stderr !== '' ? "\n$stderr" : '')); + } + } + + echo "Done\n"; +} finally { + if (is_resource($process)) { + proc_terminate($process); + proc_close($process); + } + user_cache_cgi_rm_rf($root); +} + +?> +--EXPECT-- +Done diff --git a/ext/opcache/tests/user_cache_cgi_no_boundary_001.phpt b/ext/opcache/tests/user_cache_cgi_no_boundary_001.phpt new file mode 100644 index 000000000000..7dfd59227396 --- /dev/null +++ b/ext/opcache/tests/user_cache_cgi_no_boundary_001.phpt @@ -0,0 +1,27 @@ +--TEST-- +CGI: OPcache User Cache is unavailable without a cache boundary +--CGI-- +--EXTENSIONS-- +opcache +--INI-- +opcache.enable=1 +opcache.file_cache_only=0 +opcache.user_cache_shm_size=16M +--ENV-- +DOCUMENT_ROOT= +SERVER_NAME= +--FILE-- +info(); + +var_dump($info->available); +var_dump($cache->store('key', 'value')); +var_dump($cache->fetch('key', 'MISS')); +var_dump($cache->delete('key')); +?> +--EXPECT-- +bool(false) +bool(false) +string(4) "MISS" +bool(true) diff --git a/ext/opcache/tests/user_cache_datetime_safe_direct.phpt b/ext/opcache/tests/user_cache_datetime_safe_direct.phpt new file mode 100644 index 000000000000..c3e66c6b385a --- /dev/null +++ b/ext/opcache/tests/user_cache_datetime_safe_direct.phpt @@ -0,0 +1,250 @@ +--TEST-- +OPcache User Cache: DateTime safe-direct state is restored for subclasses +--EXTENSIONS-- +opcache +--INI-- +opcache.enable=1 +opcache.enable_cli=1 +opcache.file_cache_only=0 +opcache.user_cache_shm_size=16M +date.timezone=UTC +--FILE-- +cachedSelfHash = spl_object_hash($this); + $this->publicSelfHash = spl_object_hash($this); + } + + public function cachedSelfHash(): string + { + return $this->cachedSelfHash; + } +} + +class UserCacheMagicDateTime 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' => 'serialized-' . $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 UserCacheWakefulDateTime extends DateTime +{ + public static int $sleepCount = 0; + public static int $wakeupCount = 0; + + private string $label; + + public function __construct(string $time, DateTimeZone $timezone, string $label) + { + parent::__construct($time, $timezone); + $this->label = $label; + } + + public function __sleep(): array + { + self::$sleepCount++; + + return ['label']; + } + + public function __wakeup(): void + { + self::$wakeupCount++; + } + + public function label(): string + { + return $this->label; + } +} + +class UserCacheTaggedTimeZone 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; + } +} + +class UserCacheTaggedInterval 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; + } +} + +$cache = new Opcache\UserCache('datetime-safe-direct'); + +$date = new UserCacheCarbonLikeDateTime('2024-01-02 03:04:05.123456', new DateTimeZone('Asia/Tokyo')); +$date->label = 'tokyo'; + +$model = new UserCacheDateModel( + new UserCacheCarbonLikeDateTime('2026-06-29 09:00:00.000001', new DateTimeZone('UTC')), + new UserCacheCarbonLikeDateTime('2026-06-29 09:30:00.000002', new DateTimeZone('Europe/Paris')), + new UserCacheCarbonLikeDateTime('2026-06-29 10:00:00.000003', new DateTimeZone('America/New_York')), +); + +$payload = [ + 'date' => $date, + 'model' => $model, + 'offset' => new DateTimeImmutable('2023-10-27 10:00:00.000001 +05:30'), + 'abbr' => new DateTimeImmutable('2023-10-27 10:00:00.000002 EST'), + 'timezone' => new DateTimeZone('Europe/Paris'), + 'interval' => new DateInterval('P1DT2H'), + 'taggedTimezone' => new UserCacheTaggedTimeZone('Europe/Paris', 'paris'), + 'taggedInterval' => new UserCacheTaggedInterval('P1Y2M3DT4H5M6S', 'window', 9), + 'relativeInterval' => DateInterval::createFromDateString('2 days 4 hours'), + 'selfHash' => new UserCacheSelfHashDateTime('2026-06-15 10:15:00.333333', new DateTimeZone('UTC')), + 'magicDate' => new UserCacheMagicDateTime('2026-06-15 10:45:00.654321', new DateTimeZone('UTC'), 'magic'), + 'wakefulDate' => new UserCacheWakefulDateTime('2026-06-15 12:15:00.987654', new DateTimeZone('UTC'), 'wakeful'), +]; + +var_dump($cache->store('payload', $payload)); + +$fetched = $cache->fetch('payload'); + +var_dump($fetched['date'] instanceof UserCacheCarbonLikeDateTime); +var_dump($fetched['date']->label); +var_dump($fetched['date']->format('Y-m-d H:i:s.u P e')); + +var_dump($fetched['model'] instanceof UserCacheDateModel); +var_dump($fetched['model']->createdAt->format('Y-m-d H:i:s.u e')); +var_dump($fetched['model']->updatedAt->format('Y-m-d H:i:s.u e')); +var_dump($fetched['model']->deletedAt->format('Y-m-d H:i:s.u e')); + +var_dump($fetched['offset']->format('Y-m-d H:i:s.u P e')); +var_dump($fetched['abbr']->format('Y-m-d H:i:s.u P e')); +var_dump($fetched['timezone']->getName()); +var_dump($fetched['interval']->format('%d %h')); +var_dump($fetched['taggedTimezone'] instanceof UserCacheTaggedTimeZone); +var_dump($fetched['taggedTimezone']->getName()); +var_dump($fetched['taggedTimezone']->label()); +var_dump($fetched['taggedInterval'] instanceof UserCacheTaggedInterval); +var_dump($fetched['taggedInterval']->format('%y-%m-%d %h:%i:%s')); +var_dump($fetched['taggedInterval']->describe()); +var_dump($fetched['relativeInterval'] instanceof DateInterval); +var_dump($fetched['relativeInterval']->format('%d %h')); + +var_dump($fetched['selfHash'] instanceof UserCacheSelfHashDateTime); +var_dump($fetched['selfHash']->cachedSelfHash() === spl_object_hash($fetched['selfHash'])); +var_dump($fetched['selfHash']->cachedSelfHash() !== spl_object_hash($payload['selfHash'])); +var_dump($fetched['selfHash']->publicSelfHash === spl_object_hash($payload['selfHash'])); +var_dump($fetched['selfHash']->publicSelfHash === spl_object_hash($fetched['selfHash'])); + +var_dump($fetched['magicDate'] instanceof UserCacheMagicDateTime); +var_dump($fetched['magicDate']->format('Y-m-d H:i:s.u e')); +var_dump($fetched['magicDate']->label()); +var_dump(UserCacheMagicDateTime::$serializeCount); +var_dump(UserCacheMagicDateTime::$unserializeCount); + +var_dump($fetched['wakefulDate'] instanceof UserCacheWakefulDateTime); +var_dump($fetched['wakefulDate']->format('Y-m-d H:i:s.u e')); +var_dump($fetched['wakefulDate']->label()); +var_dump(UserCacheWakefulDateTime::$sleepCount); +var_dump(UserCacheWakefulDateTime::$wakeupCount); +?> +--EXPECT-- +bool(true) +bool(true) +string(5) "tokyo" +string(44) "2024-01-02 03:04:05.123456 +09:00 Asia/Tokyo" +bool(true) +string(30) "2026-06-29 09:00:00.000001 UTC" +string(39) "2026-06-29 09:30:00.000002 Europe/Paris" +string(43) "2026-06-29 10:00:00.000003 America/New_York" +string(40) "2023-10-27 10:00:00.000001 +05:30 +05:30" +string(37) "2023-10-27 10:00:00.000002 -05:00 EST" +string(12) "Europe/Paris" +string(3) "1 2" +bool(true) +string(12) "Europe/Paris" +string(5) "paris" +bool(true) +string(11) "1-2-3 4:5:6" +string(8) "window:9" +bool(true) +string(3) "2 4" +bool(true) +bool(true) +bool(true) +bool(true) +bool(false) +bool(true) +string(30) "2026-06-15 10:45:00.654321 UTC" +string(5) "magic" +int(0) +int(0) +bool(true) +string(30) "2026-06-15 12:15:00.987654 UTC" +string(7) "wakeful" +int(0) +int(0) diff --git a/ext/opcache/tests/user_cache_disabled.phpt b/ext/opcache/tests/user_cache_disabled.phpt new file mode 100644 index 000000000000..462c4e5ddb3f --- /dev/null +++ b/ext/opcache/tests/user_cache_disabled.phpt @@ -0,0 +1,54 @@ +--TEST-- +OPcache User Cache: disabled by INI size +--EXTENSIONS-- +opcache +--INI-- +opcache.enable=1 +opcache.enable_cli=1 +opcache.file_cache_only=0 +opcache.user_cache_shm_size=0 +--FILE-- +info(); +var_dump($info->scope, $info->available, $info->unavailableReason); +var_dump($info->enabled, $info->backendInitialized, $info->configuredMemory); +var_dump($cache->store('key', 1)); +var_dump($cache->storeMultiple(['key' => 1])); +var_dump($cache->increment('key')); +var_dump($cache->decrement('key')); +var_dump($cache->has('key')); +var_dump($cache->fetch('key', 'default')); +var_dump($cache->fetchMultiple(['key', 'other'], 'default')); +var_dump($cache->delete('key')); +var_dump($cache->deleteMultiple(['key', 'other'])); +var_dump($cache->clear()); +var_dump($cache->lock('key')); +var_dump($cache->unlock('key')); +var_dump($cache->remember('key', fn() => 42)); +?> +--EXPECT-- +string(3) "off" +bool(false) +string(63) "OPcache User Cache is disabled by opcache.user_cache_shm_size=0" +bool(false) +bool(false) +int(0) +bool(false) +bool(false) +bool(false) +bool(false) +bool(false) +string(7) "default" +array(2) { + ["key"]=> + string(7) "default" + ["other"]=> + string(7) "default" +} +bool(true) +bool(true) +bool(true) +bool(false) +bool(false) +int(42) diff --git a/ext/opcache/tests/user_cache_fuzz_roundtrip.phpt b/ext/opcache/tests/user_cache_fuzz_roundtrip.phpt new file mode 100644 index 000000000000..046108419629 --- /dev/null +++ b/ext/opcache/tests/user_cache_fuzz_roundtrip.phpt @@ -0,0 +1,435 @@ +--TEST-- +OPcache User Cache: deterministic store/fetch round-trip fuzz cases +--EXTENSIONS-- +opcache +spl +--INI-- +opcache.enable=1 +opcache.enable_cli=1 +opcache.file_cache_only=0 +opcache.user_cache_shm_size=16M +date.timezone=UTC +error_reporting=E_ALL & ~E_DEPRECATED +--FILE-- + 'serialized-' . $this->name, 'values' => []]; + } + + public function __unserialize(array $data): void + { + self::$unserializeCalls++; + $this->name = $data['name']; + $this->values = $data['values']; + } +} + +class UCFuzzDateTime extends DateTimeImmutable +{ + public function __construct( + string $time, + DateTimeZone $timezone, + public string $label, + ) { + parent::__construct($time, $timezone); + } +} + +class UCFuzzDateModel +{ + public function __construct( + public int $id, + public UCFuzzDateTime $createdAt, + public UCFuzzDateTime $updatedAt, + public UCFuzzDateTime $publishedAt, + public array $attributes, + ) { + } +} + +class UCFuzzArrayObject extends ArrayObject +{ + public function __construct( + array $storage, + public string $name, + public int $revision, + ) { + parent::__construct($storage, ArrayObject::ARRAY_AS_PROPS); + } +} + +function uc_fuzz_scalar(int $seed): mixed +{ + return match ($seed % 6) { + 0 => null, + 1 => $seed % 2 === 0, + 2 => $seed * 17 - 500, + 3 => $seed / 10.0, + 4 => 'str-' . $seed . '-' . str_repeat(chr(65 + ($seed % 26)), ($seed % 5) + 1), + default => ['seed' => $seed, 'enabled' => $seed % 3 === 0], + }; +} + +function uc_fuzz_array(int $seed, int $depth): array +{ + $result = []; + $count = ($seed % 4) + 1; + + for ($i = 0; $i < $count; $i++) { + $key = $i % 2 === 0 ? 'k' . $seed . '_' . $i : $i; + if ($depth >= 3) { + $result[$key] = uc_fuzz_scalar($seed + $i); + continue; + } + + $result[$key] = match (($seed + $i) % 5) { + 0 => uc_fuzz_scalar($seed + $i), + 1 => uc_fuzz_array($seed + $i + 1, $depth + 1), + 2 => new UCFuzzLeaf('leaf-' . $seed . '-' . $i, $seed + $i, ['hot' => $i % 2 === 0]), + 3 => new DateTimeImmutable(sprintf('2026-06-%02d 10:%02d:00.%06d', ($seed % 20) + 1, $i, $seed), new DateTimeZone('UTC')), + default => UCFuzzSuit::Hearts, + }; + } + + return $result; +} + +function uc_fuzz_date(int $seed, string $timezone): UCFuzzDateTime +{ + return new UCFuzzDateTime( + sprintf('2026-06-%02d %02d:%02d:%02d.%06d', ($seed % 20) + 1, $seed % 24, $seed % 60, ($seed * 3) % 60, $seed), + new DateTimeZone($timezone), + 'label-' . $seed, + ); +} + +function uc_fuzz_build_payload(int $seed): mixed +{ + switch ($seed % 16) { + case 0: + return uc_fuzz_scalar($seed); + case 1: + return uc_fuzz_array($seed, 0); + case 2: + return (object) [ + 'name' => 'std-' . $seed, + 'items' => uc_fuzz_array($seed + 10, 0), + ]; + case 3: + return new UCFuzzLeaf('leaf-' . $seed, $seed * 3, ['a' => true, 'b' => $seed % 2 === 0]); + case 4: + $shared = new UCFuzzLeaf('shared-' . $seed, $seed, ['shared' => true]); + return ['left' => $shared, 'right' => $shared, 'list' => [$shared]]; + case 5: + return [ + 'created' => uc_fuzz_date($seed, 'Asia/Tokyo'), + 'updated' => new DateTime('2026-06-29 12:34:56.123456', new DateTimeZone('Europe/Paris')), + 'timezone' => new DateTimeZone($seed % 2 === 0 ? 'UTC' : 'America/New_York'), + 'interval' => new DateInterval('P1DT2H'), + ]; + case 6: + return new UCFuzzArrayObject([ + 'rows' => uc_fuzz_array($seed + 20, 0), + 'owner' => new UCFuzzLeaf('owner-' . $seed, $seed, []), + ], 'array-object-' . $seed, $seed); + case 7: + return new ArrayIterator(uc_fuzz_array($seed + 30, 0)); + case 8: + return new RecursiveArrayIterator(['branch' => ['leaf' => uc_fuzz_array($seed + 40, 1)]]); + case 9: + $fixed = new SplFixedArray(3); + $fixed[0] = 'fixed-' . $seed; + $fixed[1] = uc_fuzz_array($seed + 50, 1); + $fixed[2] = new UCFuzzLeaf('fixed-leaf-' . $seed, $seed, []); + return $fixed; + case 10: + $list = new SplDoublyLinkedList(); + $list->setIteratorMode(SplDoublyLinkedList::IT_MODE_FIFO); + $list->push('list-' . $seed); + $list->push(new UCFuzzLeaf('list-leaf-' . $seed, $seed, [])); + $queue = new SplQueue(); + $queue->enqueue('queue-' . $seed); + $queue->enqueue($seed); + $stack = new SplStack(); + $stack->push('stack-' . $seed); + $stack->push($seed); + return ['list' => $list, 'queue' => $queue, 'stack' => $stack]; + case 11: + $min = new SplMinHeap(); + $max = new SplMaxHeap(); + $pq = new SplPriorityQueue(); + $pq->setExtractFlags(SplPriorityQueue::EXTR_BOTH); + for ($i = 0; $i < 4; $i++) { + $min->insert($seed + $i); + $max->insert($seed + $i); + $pq->insert('pq-' . $seed . '-' . $i, $seed + $i); + } + return ['min' => $min, 'max' => $max, 'pq' => $pq]; + case 12: + return [ + 'enum' => $seed % 2 === 0 ? UCFuzzSuit::Hearts : UCFuzzSuit::Spades, + 'magic' => new UCFuzzMagicObject('magic-' . $seed, uc_fuzz_array($seed + 60, 0)), + ]; + case 13: + return new UCFuzzDateModel( + 1000 + $seed, + uc_fuzz_date($seed, 'UTC'), + uc_fuzz_date($seed + 1, 'Europe/Paris'), + uc_fuzz_date($seed + 2, 'America/Los_Angeles'), + ['status' => 'published', 'score' => $seed * 7], + ); + case 14: + $node = new UCFuzzNode('node-' . $seed, $seed); + $node->payload = uc_fuzz_array($seed + 70, 0); + $node->child = new UCFuzzNode('child-' . $seed, $seed + 1); + $node->child->payload = new UCFuzzLeaf('nested-' . $seed, $seed, []); + return $node; + default: + $root = new UCFuzzNode('cycle-root-' . $seed, $seed); + $peer = new UCFuzzNode('cycle-peer-' . $seed, $seed + 1); + $root->child = $peer; + $peer->child = $root; + $root->payload = ['peer' => $peer]; + $peer->payload = ['root' => $root]; + return $root; + } +} + +function uc_fuzz_object_properties(object $value): array +{ + $properties = []; + foreach ((array) $value as $name => $propertyValue) { + $properties[str_replace("\0", '\\0', (string) $name)] = $propertyValue; + } + + ksort($properties); + return $properties; +} + +function uc_fuzz_normalize_iterable(iterable $values, SplObjectStorage $seen, int $depth): array +{ + $result = []; + foreach ($values as $key => $value) { + $result[(string) $key] = uc_fuzz_normalize($value, $seen, $depth + 1); + } + + return $result; +} + +function uc_fuzz_normalize_heap(SplHeap $heap, SplObjectStorage $seen, int $depth): array +{ + $copy = clone $heap; + $values = []; + + while (!$copy->isEmpty()) { + $values[] = uc_fuzz_normalize($copy->extract(), $seen, $depth + 1); + } + + return $values; +} + +function uc_fuzz_normalize(mixed $value, SplObjectStorage $seen, int $depth = 0): mixed +{ + if ($depth > 32) { + return '*depth*'; + } + + if ($value === null || is_bool($value) || is_int($value) || is_float($value) || is_string($value)) { + return $value; + } + + if (is_array($value)) { + $result = []; + foreach ($value as $key => $arrayValue) { + $result[(string) $key] = uc_fuzz_normalize($arrayValue, $seen, $depth + 1); + } + + return ['array' => $result]; + } + + if ($value instanceof UnitEnum) { + return [ + 'enum' => get_class($value), + 'name' => $value->name, + 'value' => $value instanceof BackedEnum ? $value->value : null, + ]; + } + + if (!$value instanceof object) { + return ['unknown' => get_debug_type($value)]; + } + + if ($seen->contains($value)) { + return ['ref' => $seen[$value]]; + } + + $seen[$value] = $seen->count(); + + if ($value instanceof DateTimeInterface) { + return [ + 'class' => get_class($value), + 'datetime' => $value->format('Y-m-d H:i:s.u P e'), + 'properties' => uc_fuzz_normalize(uc_fuzz_object_properties($value), $seen, $depth + 1), + ]; + } + + if ($value instanceof DateTimeZone) { + return [ + 'class' => get_class($value), + 'timezone' => $value->getName(), + 'properties' => uc_fuzz_normalize(uc_fuzz_object_properties($value), $seen, $depth + 1), + ]; + } + + if ($value instanceof DateInterval) { + return [ + 'class' => get_class($value), + 'interval' => $value->format('%r%y-%m-%d %h:%i:%s.%f'), + 'properties' => uc_fuzz_normalize(uc_fuzz_object_properties($value), $seen, $depth + 1), + ]; + } + + if ($value instanceof ArrayObject) { + return [ + 'class' => get_class($value), + 'flags' => $value->getFlags(), + 'iterator' => $value->getIteratorClass(), + 'storage' => uc_fuzz_normalize($value->getArrayCopy(), $seen, $depth + 1), + 'properties' => uc_fuzz_normalize(uc_fuzz_object_properties($value), $seen, $depth + 1), + ]; + } + + if ($value instanceof ArrayIterator) { + return [ + 'class' => get_class($value), + 'storage' => uc_fuzz_normalize($value->getArrayCopy(), $seen, $depth + 1), + 'properties' => uc_fuzz_normalize(uc_fuzz_object_properties($value), $seen, $depth + 1), + ]; + } + + if ($value instanceof SplFixedArray) { + return [ + 'class' => get_class($value), + 'size' => $value->getSize(), + 'storage' => uc_fuzz_normalize($value->toArray(), $seen, $depth + 1), + 'properties' => uc_fuzz_normalize(uc_fuzz_object_properties($value), $seen, $depth + 1), + ]; + } + + if ($value instanceof SplPriorityQueue) { + return [ + 'class' => get_class($value), + 'flags' => $value->getExtractFlags(), + 'values' => uc_fuzz_normalize_heap($value, $seen, $depth + 1), + 'properties' => uc_fuzz_normalize(uc_fuzz_object_properties($value), $seen, $depth + 1), + ]; + } + + if ($value instanceof SplHeap) { + return [ + 'class' => get_class($value), + 'values' => uc_fuzz_normalize_heap($value, $seen, $depth + 1), + 'properties' => uc_fuzz_normalize(uc_fuzz_object_properties($value), $seen, $depth + 1), + ]; + } + + if ($value instanceof SplDoublyLinkedList) { + return [ + 'class' => get_class($value), + 'mode' => $value->getIteratorMode(), + 'values' => uc_fuzz_normalize_iterable($value, $seen, $depth + 1), + 'properties' => uc_fuzz_normalize(uc_fuzz_object_properties($value), $seen, $depth + 1), + ]; + } + + return [ + 'class' => get_class($value), + 'properties' => uc_fuzz_normalize(uc_fuzz_object_properties($value), $seen, $depth + 1), + ]; +} + +function uc_fuzz_digest(mixed $value): mixed +{ + return uc_fuzz_normalize($value, new SplObjectStorage()); +} + +$count = 128; +for ($i = 0; $i < $count; $i++) { + $key = 'case_' . $i; + $payload = uc_fuzz_build_payload($i); + $before = uc_fuzz_digest($payload); + + if (!$cache->store($key, $payload)) { + throw new RuntimeException('store failed for fuzz case ' . $i); + } + + $missing = new stdClass(); + $fetched = $cache->fetch($key, $missing); + if ($fetched === $missing) { + throw new RuntimeException('fetch missed fuzz case ' . $i); + } + + $after = uc_fuzz_digest($fetched); + if ($before !== $after) { + echo "mismatch case ", $i, "\n"; + var_dump($before); + var_dump($after); + exit(1); + } +} + +echo "fuzz cases: ", $count, "\n"; +echo "magic calls: "; +var_dump([UCFuzzMagicObject::$serializeCalls, UCFuzzMagicObject::$unserializeCalls]); +?> +--EXPECT-- +fuzz cases: 128 +magic calls: array(2) { + [0]=> + int(0) + [1]=> + int(0) +} diff --git a/ext/opcache/tests/user_cache_info_reset.phpt b/ext/opcache/tests/user_cache_info_reset.phpt new file mode 100644 index 000000000000..195a394d4cc6 --- /dev/null +++ b/ext/opcache/tests/user_cache_info_reset.phpt @@ -0,0 +1,65 @@ +--TEST-- +OPcache User Cache: info statistics and user cache reset +--EXTENSIONS-- +opcache +--INI-- +opcache.enable=1 +opcache.enable_cli=1 +opcache.file_cache_only=0 +opcache.user_cache_shm_size=16M +--FILE-- +clear(); +$other->clear(); + +$initial = $cache->info(); +var_dump($initial->scope); +var_dump($initial->available); +var_dump($initial->enabled); +var_dump($initial->startupFailed); +var_dump($initial->backendInitialized); +var_dump($initial->configuredMemory === 16 * 1024 * 1024); +var_dump($initial->sharedMemorySize > 0); +var_dump($initial->entryCount); + +var_dump($cache->store('key', ['value' => 1])); +var_dump($other->store('key', 'other')); + +$afterStore = $cache->info(); +var_dump($afterStore->entryCount >= 2); +var_dump($afterStore->entryCapacity > 0); +var_dump($afterStore->usedMemory > 0); +var_dump($afterStore->freeMemory > 0); +var_dump($afterStore->wastedMemory >= 0); +var_dump($afterStore->tombstoneCount >= 0); + +var_dump(opcache_user_cache_reset()); +var_dump($cache->fetch('key', 'missing')); +var_dump($other->fetch('key', 'missing')); + +$afterReset = $cache->info(); +var_dump($afterReset->entryCount); +?> +--EXPECT-- +string(12) "info-reset-a" +bool(true) +bool(true) +bool(false) +bool(true) +bool(true) +bool(true) +int(0) +bool(true) +bool(true) +bool(true) +bool(true) +bool(true) +bool(true) +bool(true) +bool(true) +bool(true) +string(7) "missing" +string(7) "missing" +int(0) diff --git a/ext/opcache/tests/user_cache_invalid_keys.phpt b/ext/opcache/tests/user_cache_invalid_keys.phpt new file mode 100644 index 000000000000..c8bb362458c7 --- /dev/null +++ b/ext/opcache/tests/user_cache_invalid_keys.phpt @@ -0,0 +1,44 @@ +--TEST-- +OPcache User Cache: invalid cache keys are rejected consistently +--EXTENSIONS-- +opcache +--INI-- +opcache.enable=1 +opcache.enable_cli=1 +opcache.file_cache_only=0 +opcache.user_cache_shm_size=16M +--FILE-- +getMessage()); + echo $label, ': ', $e::class, ': ', $message, "\n"; + } +} + +show_error('store empty', fn() => $cache->store('', 1)); +show_error('fetch delimiter', fn() => $cache->fetch("bad\x1fkey")); +show_error('has empty', fn() => $cache->has('')); +show_error('lock delimiter', fn() => $cache->lock("bad\x1fkey")); +show_error('fetchMultiple empty', fn() => $cache->fetchMultiple(['ok', ''])); +show_error('fetchMultiple type', fn() => $cache->fetchMultiple(['ok', 1.5])); +show_error('deleteMultiple type', fn() => $cache->deleteMultiple(['ok', new stdClass()])); +show_error('storeMultiple empty key', fn() => $cache->storeMultiple(['' => 1])); +show_error('storeMultiple delimiter key', fn() => $cache->storeMultiple(["bad\x1fkey" => 1])); +?> +--EXPECT-- +store empty: ValueError: Argument #1 ($key) must be a non-empty string +fetch delimiter: ValueError: Argument #1 ($key) must not contain the user-cache key delimiter 0x1F +has empty: ValueError: Argument #1 ($key) must be a non-empty string +lock delimiter: ValueError: Argument #1 ($key) must not contain the user-cache key delimiter 0x1F +fetchMultiple empty: ValueError: Argument #1 ($keys) must contain only non-empty string or int cache keys that do not contain 0x1F +fetchMultiple type: ValueError: Argument #1 ($keys) must contain only non-empty string or int cache keys that do not contain 0x1F +deleteMultiple type: ValueError: Argument #1 ($keys) must contain only non-empty string or int cache keys that do not contain 0x1F +storeMultiple empty key: ValueError: Argument #1 ($values) must be an array with non-empty string keys that do not contain 0x1F +storeMultiple delimiter key: ValueError: Argument #1 ($values) must be an array with non-empty string keys that do not contain 0x1F diff --git a/ext/opcache/tests/user_cache_litespeed_boundary_001.phpt b/ext/opcache/tests/user_cache_litespeed_boundary_001.phpt new file mode 100644 index 000000000000..3c3693e17936 --- /dev/null +++ b/ext/opcache/tests/user_cache_litespeed_boundary_001.phpt @@ -0,0 +1,345 @@ +--TEST-- +OPcache User Cache: litespeed partitions cache data by boundary +--EXTENSIONS-- +opcache +--CONFLICTS-- +all +--SKIPIF-- + +--FILE-- + $value) { + $keyString = $key . "\0"; + $valueString = $value . "\0"; + $offsets[$key] = $baseOffset + strlen($block) + 4 + strlen($keyString); + $block .= pack('n', strlen($keyString)); + $block .= pack('n', strlen($valueString)); + $block .= $keyString; + $block .= $valueString; + } + + return $block . "\0\0\0\0"; +} + +function user_cache_lsapi_request_packet(string $script, string $docRoot, string $serverName, string $query): string +{ + $offsets = []; + $specialBlock = "\0\0\0\0"; + $envBase = LSAPI_REQ_HEADER_LEN + strlen($specialBlock); + $env = [ + 'SCRIPT_FILENAME' => $script, + 'SCRIPT_NAME' => '/index.php', + 'QUERY_STRING' => $query, + 'REQUEST_METHOD' => 'GET', + 'SERVER_NAME' => $serverName, + 'DOCUMENT_ROOT' => $docRoot, + 'REQUEST_URI' => '/index.php' . ($query !== '' ? '?' . $query : ''), + 'SERVER_PROTOCOL' => 'HTTP/1.1', + 'REMOTE_ADDR' => '127.0.0.1', + ]; + $envBlock = user_cache_lsapi_env_block($env, $envBase, $offsets); + $body = $specialBlock . $envBlock; + $body .= str_repeat("\0", (8 - ((LSAPI_REQ_HEADER_LEN + strlen($body)) % 8)) & 7); + $body .= str_repeat("\0", LSAPI_HTTP_HEADER_INDEX_LEN); + $length = LSAPI_REQ_HEADER_LEN + strlen($body); + + return user_cache_lsapi_packet_header(LSAPI_BEGIN_REQUEST, $length) + . user_cache_lsapi_pack_int(0) + . user_cache_lsapi_pack_int(0) + . user_cache_lsapi_pack_int($offsets['SCRIPT_FILENAME']) + . user_cache_lsapi_pack_int($offsets['SCRIPT_NAME']) + . user_cache_lsapi_pack_int($offsets['QUERY_STRING']) + . user_cache_lsapi_pack_int($offsets['REQUEST_METHOD']) + . user_cache_lsapi_pack_int(0) + . user_cache_lsapi_pack_int(count($env)) + . user_cache_lsapi_pack_int(0) + . $body; +} + +function user_cache_lsapi_read_exact($fp, int $length): string +{ + $buffer = ''; + + while (strlen($buffer) < $length && !feof($fp)) { + $chunk = fread($fp, $length - strlen($buffer)); + if ($chunk === false) { + throw new RuntimeException('Failed to read LSAPI response'); + } + if ($chunk === '') { + if (stream_get_meta_data($fp)['timed_out']) { + throw new RuntimeException('Timed out reading LSAPI response'); + } + usleep(10000); + continue; + } + $buffer .= $chunk; + } + + if (strlen($buffer) !== $length) { + throw new RuntimeException('Truncated LSAPI response'); + } + + return $buffer; +} + +function user_cache_lsapi_request(int $port, string $script, string $docRoot, string $serverName, string $query): string +{ + $fp = @stream_socket_client("tcp://127.0.0.1:$port", $errno, $errstr, 2); + if ($fp === false) { + throw new RuntimeException($errstr); + } + stream_set_timeout($fp, 5); + + fwrite($fp, user_cache_lsapi_request_packet($script, $docRoot, $serverName, $query)); + $body = ''; + + while (!feof($fp)) { + try { + $header = user_cache_lsapi_read_exact($fp, 8); + } catch (RuntimeException $e) { + if ($body !== '' && $e->getMessage() === 'Truncated LSAPI response') { + break; + } + throw $e; + } + if (substr($header, 0, 2) !== 'LS') { + throw new RuntimeException('Invalid LSAPI response header'); + } + + $type = ord($header[2]); + $length = user_cache_lsapi_unpack_int(substr($header, 4, 4)); + $payload = $length > 8 ? user_cache_lsapi_read_exact($fp, $length - 8) : ''; + + if ($type === LSAPI_RESP_STREAM) { + $body .= $payload; + } elseif ($type === LSAPI_RESP_END || $type === LSAPI_CONN_CLOSE) { + break; + } + } + + fclose($fp); + + return trim($body); +} + +function user_cache_lsapi_free_port(): int +{ + $server = stream_socket_server('tcp://127.0.0.1:0', $errno, $errstr); + if ($server === false) { + throw new RuntimeException($errstr); + } + + $name = stream_socket_get_name($server, false); + fclose($server); + + return (int) substr(strrchr($name, ':'), 1); +} + +function user_cache_lsapi_rm_rf(string $path): void +{ + if (!file_exists($path)) { + return; + } + + if (!is_dir($path) || is_link($path)) { + unlink($path); + return; + } + + foreach (scandir($path) as $entry) { + if ($entry === '.' || $entry === '..') { + continue; + } + user_cache_lsapi_rm_rf($path . DIRECTORY_SEPARATOR . $entry); + } + + rmdir($path); +} + +function user_cache_lsapi_start(string $lsphp, string $iniDir, int $port): array +{ + $env = array_merge($_ENV, [ + 'PHP_LSAPI_CHILDREN' => '1', + 'LSAPI_PPID_NO_CHECK' => '1', + 'LSAPI_MAX_REQS' => '100', + 'LSAPI_CLEAN_SHUTDOWN' => '1', + ]); + $process = proc_open( + [$lsphp, '-c', $iniDir, '-b', "127.0.0.1:$port"], + [['pipe', 'r'], ['pipe', 'w'], ['pipe', 'w']], + $pipes, + $iniDir, + $env + ); + if (!is_resource($process)) { + throw new RuntimeException('Unable to start lsphp'); + } + stream_set_blocking($pipes[1], false); + stream_set_blocking($pipes[2], false); + + return [$process, $pipes]; +} + +function user_cache_lsapi_wait($process, array $pipes, int $port, string $script, string $docRoot, string $serverName): void +{ + for ($i = 0; $i < 50; $i++) { + $status = proc_get_status($process); + if (!$status['running']) { + throw new RuntimeException(stream_get_contents($pipes[2])); + } + + try { + user_cache_lsapi_request($port, $script, $docRoot, $serverName, 'action=fetch'); + return; + } catch (Throwable) { + usleep(100000); + } + } + + throw new RuntimeException(stream_get_contents($pipes[2]) ?: 'lsphp did not become ready'); +} + +$root = sys_get_temp_dir() . '/php-user-cache-lsapi-' . getmypid(); +$alphaRoot = $root . '/alpha'; +$betaRoot = $root . '/beta'; +$otherRoot = $root . '/other'; +$processes = []; + +user_cache_lsapi_rm_rf($root); +mkdir($alphaRoot, 0777, true); +mkdir($betaRoot, 0777, true); +mkdir($otherRoot, 0777, true); + +$script = <<<'PHP' +store($key, $host . '-value'); +} + +echo $host, ':', $cache->fetch($key, 'MISS'), ':', $cache->info()->scope, "\n"; +PHP; + +file_put_contents($alphaRoot . '/index.php', $script); +file_put_contents($betaRoot . '/index.php', $script); +file_put_contents($otherRoot . '/index.php', $script); +file_put_contents($root . '/php.ini', implode("\n", [ + 'opcache.enable=1', + 'opcache.user_cache_shm_size=32M', + 'opcache.file_update_protection=0', +])); + +try { + $lsphp = user_cache_lsapi_binary(); + $alphaPort = user_cache_lsapi_free_port(); + $betaPort = user_cache_lsapi_free_port(); + $alphaScript = $alphaRoot . '/index.php'; + $betaScript = $betaRoot . '/index.php'; + $otherScript = $otherRoot . '/index.php'; + + $processes[] = user_cache_lsapi_start($lsphp, $root, $alphaPort); + $processes[] = user_cache_lsapi_start($lsphp, $root, $betaPort); + + user_cache_lsapi_wait($processes[0][0], $processes[0][1], $alphaPort, $alphaScript, $alphaRoot, 'alpha.local'); + user_cache_lsapi_wait($processes[1][0], $processes[1][1], $betaPort, $betaScript, $betaRoot, 'beta.local'); + + $checks = [ + [$alphaPort, $alphaScript, $alphaRoot, 'alpha.local', 'action=seed', 'alpha.local:alpha.local-value:default'], + [$alphaPort, $alphaScript, $alphaRoot, 'alpha.local', 'action=fetch', 'alpha.local:alpha.local-value:default'], + [$alphaPort, $otherScript, $otherRoot, 'other.local', 'action=fetch', 'other.local:MISS:default'], + [$alphaPort, $otherScript, $otherRoot, 'other.local', 'action=seed', 'other.local:other.local-value:default'], + [$alphaPort, $otherScript, $otherRoot, 'other.local', 'action=fetch', 'other.local:other.local-value:default'], + [$alphaPort, $alphaScript, $alphaRoot, 'alpha.local', 'action=fetch', 'alpha.local:alpha.local-value:default'], + [$betaPort, $betaScript, $betaRoot, 'beta.local', 'action=fetch', 'beta.local:MISS:default'], + [$betaPort, $betaScript, $betaRoot, 'beta.local', 'action=seed', 'beta.local:beta.local-value:default'], + [$alphaPort, $alphaScript, $alphaRoot, 'alpha.local', 'action=fetch', 'alpha.local:alpha.local-value:default'], + [$betaPort, $betaScript, $betaRoot, 'beta.local', 'action=fetch', 'beta.local:beta.local-value:default'], + ]; + + foreach ($checks as [$port, $file, $docRoot, $serverName, $query, $expected]) { + $actual = user_cache_lsapi_request($port, $file, $docRoot, $serverName, $query); + if ($actual !== $expected) { + $stderr = stream_get_contents($port === $alphaPort ? $processes[0][1][2] : $processes[1][1][2]); + throw new RuntimeException("Expected $expected, got $actual" . ($stderr !== '' ? "\n$stderr" : '')); + } + } + + echo "Done\n"; +} finally { + foreach ($processes as [$process, $pipes]) { + if (is_resource($process)) { + proc_terminate($process); + proc_close($process); + } + } + user_cache_lsapi_rm_rf($root); +} + +?> +--EXPECT-- +Done diff --git a/ext/opcache/tests/user_cache_lock_key_isolation.phpt b/ext/opcache/tests/user_cache_lock_key_isolation.phpt new file mode 100644 index 000000000000..12229a25aa9f --- /dev/null +++ b/ext/opcache/tests/user_cache_lock_key_isolation.phpt @@ -0,0 +1,61 @@ +--TEST-- +OPcache User Cache: locks are isolated by exact key +--EXTENSIONS-- +opcache +--INI-- +opcache.enable=1 +opcache.enable_cli=1 +opcache.file_cache_only=0 +opcache.user_cache_shm_size=16M +--FILE-- +clear(); + +var_dump($cache->lock('alpha')); +var_dump($cache->store('beta', 'beta-value')); +var_dump($cache->fetch('beta')); +var_dump($cache->delete('beta')); +var_dump($cache->has('beta')); + +var_dump($cache->storeMultiple([ + 'gamma' => 'gamma-value', + 'delta' => 'delta-value', +])); +var_dump($cache->fetchMultiple(['gamma', 'delta'])); +var_dump($cache->deleteMultiple(['gamma', 'delta'])); +var_dump($cache->has('gamma')); +var_dump($cache->has('delta')); + +var_dump($cache->lock('epsilon')); +var_dump($cache->unlock('epsilon')); +var_dump($cache->unlock('alpha')); + +var_dump($cache->lock('prefix')); +var_dump($cache->store('prefix:child', 'child-value')); +var_dump($cache->fetch('prefix:child')); +var_dump($cache->unlock('prefix')); +?> +--EXPECT-- +bool(true) +bool(true) +string(10) "beta-value" +bool(true) +bool(false) +bool(true) +array(2) { + ["gamma"]=> + string(11) "gamma-value" + ["delta"]=> + string(11) "delta-value" +} +bool(true) +bool(false) +bool(false) +bool(true) +bool(true) +bool(true) +bool(true) +bool(true) +string(11) "child-value" +bool(true) diff --git a/ext/opcache/tests/user_cache_opcache_disabled.phpt b/ext/opcache/tests/user_cache_opcache_disabled.phpt new file mode 100644 index 000000000000..9f25043f364c --- /dev/null +++ b/ext/opcache/tests/user_cache_opcache_disabled.phpt @@ -0,0 +1,54 @@ +--TEST-- +OPcache User Cache: OPcache disabled no-op behavior +--EXTENSIONS-- +opcache +--INI-- +opcache.enable=1 +opcache.enable_cli=0 +opcache.file_cache_only=0 +opcache.user_cache_shm_size=16M +--FILE-- +info(); +var_dump($info->scope, $info->available, $info->unavailableReason); +var_dump($info->enabled, $info->backendInitialized, $info->configuredMemory === 16 * 1024 * 1024); +var_dump($cache->store('key', 1)); +var_dump($cache->storeMultiple(['key' => 1])); +var_dump($cache->increment('key')); +var_dump($cache->decrement('key')); +var_dump($cache->has('key')); +var_dump($cache->fetch('key', 'default')); +var_dump($cache->fetchMultiple(['key', 'other'], 'default')); +var_dump($cache->delete('key')); +var_dump($cache->deleteMultiple(['key', 'other'])); +var_dump($cache->clear()); +var_dump($cache->lock('key')); +var_dump($cache->unlock('key')); +var_dump($cache->remember('key', fn() => 42)); +?> +--EXPECT-- +string(3) "off" +bool(false) +string(19) "OPcache is disabled" +bool(true) +bool(false) +bool(true) +bool(false) +bool(false) +bool(false) +bool(false) +bool(false) +string(7) "default" +array(2) { + ["key"]=> + string(7) "default" + ["other"]=> + string(7) "default" +} +bool(true) +bool(true) +bool(true) +bool(false) +bool(false) +int(42) diff --git a/ext/opcache/tests/user_cache_opcache_reset.phpt b/ext/opcache/tests/user_cache_opcache_reset.phpt new file mode 100644 index 000000000000..59b1d306ec15 --- /dev/null +++ b/ext/opcache/tests/user_cache_opcache_reset.phpt @@ -0,0 +1,40 @@ +--TEST-- +OPcache User Cache: opcache_reset clears cached values +--EXTENSIONS-- +opcache +--INI-- +opcache.enable=1 +opcache.enable_cli=1 +opcache.file_cache_only=0 +opcache.user_cache_shm_size=16M +--FILE-- +store('key', ['value' => 1])); +var_dump($other->store('key', 'other')); + +var_dump($cache->fetch('key')); +var_dump($other->fetch('key')); + +var_dump(opcache_reset()); + +var_dump($cache->fetch('key', 'missing')); +var_dump($other->fetch('key', 'missing')); +var_dump($cache->has('key')); +var_dump($other->has('key')); +?> +--EXPECT-- +bool(true) +bool(true) +array(1) { + ["value"]=> + int(1) +} +string(5) "other" +bool(true) +string(7) "missing" +string(7) "missing" +bool(false) +bool(false) diff --git a/ext/opcache/tests/user_cache_overwrite_reuse.phpt b/ext/opcache/tests/user_cache_overwrite_reuse.phpt new file mode 100644 index 000000000000..c0ba56ceb663 --- /dev/null +++ b/ext/opcache/tests/user_cache_overwrite_reuse.phpt @@ -0,0 +1,42 @@ +--TEST-- +OPcache User Cache: repeated stores reuse existing entries +--EXTENSIONS-- +opcache +--INI-- +opcache.enable=1 +opcache.enable_cli=1 +opcache.file_cache_only=0 +opcache.user_cache_shm_size=16M +--FILE-- +store('same', ['i' => $i, 'payload' => str_repeat('x', 1024)])) { + echo "same failed at $i\n"; + exit; + } +} + +$same = $cache->fetch('same'); +var_dump($same['i']); +var_dump(strlen($same['payload'])); + +for ($i = 0; $i < 5000; $i++) { + if (!$cache->delete('same')) { + echo "delete failed at $i\n"; + exit; + } + if (!$cache->store('same', ['i' => $i])) { + echo "store failed at $i\n"; + exit; + } +} + +$churned = $cache->fetch('same'); +var_dump($churned['i']); +?> +--EXPECT-- +int(19999) +int(1024) +int(4999) diff --git a/ext/opcache/tests/user_cache_pressure_clear.phpt b/ext/opcache/tests/user_cache_pressure_clear.phpt new file mode 100644 index 000000000000..c9c0c32aa5a6 --- /dev/null +++ b/ext/opcache/tests/user_cache_pressure_clear.phpt @@ -0,0 +1,30 @@ +--TEST-- +OPcache User Cache: store retries can clear on memory pressure +--EXTENSIONS-- +opcache +--INI-- +opcache.enable=1 +opcache.enable_cli=1 +opcache.file_cache_only=0 +opcache.user_cache_shm_size=1M +--FILE-- +clear(); + +var_dump($cache->store('marker', 'kept-until-pressure')); + +for ($i = 0; $i < 220; $i++) { + $cache->store('fill-' . $i, str_repeat(chr(65 + ($i % 26)), 4096)); +} + +$large = str_repeat('L', 256 * 1024); +var_dump($cache->store('large', $large)); +var_dump($cache->fetch('large', '') === $large); +var_dump($cache->fetch('marker', 'missing')); +?> +--EXPECT-- +bool(true) +bool(true) +bool(true) +string(7) "missing" diff --git a/ext/opcache/tests/user_cache_reference_graphs.phpt b/ext/opcache/tests/user_cache_reference_graphs.phpt new file mode 100644 index 000000000000..51a30e6a4ec0 --- /dev/null +++ b/ext/opcache/tests/user_cache_reference_graphs.phpt @@ -0,0 +1,262 @@ +--TEST-- +OPcache User Cache: references and cyclic graphs survive fetch and access +--EXTENSIONS-- +opcache +--INI-- +opcache.enable=1 +opcache.enable_cli=1 +opcache.file_cache_only=0 +opcache.user_cache_shm_size=16M +error_reporting=E_ALL & ~E_DEPRECATED +--FILE-- +name . ':' . $this->revision . ':' . $this->peer?->name; + } +} + +class UCReferencedPayload +{ + public function __construct( + public string $label, + public int $revision, + ) { + } +} + +class UCReferenceAssignmentPayload +{ + public UCReferencedPayload $alias; + + public function __construct( + public string $name, + public UCReferencedPayload $child, + ) { + $this->alias = $child; + } +} + +function uc_build_cycle_payload(string $rootName, string $peerName, int $revision): UCCyclePayload +{ + $root = new UCCyclePayload($rootName, $revision); + $peer = new UCCyclePayload($peerName, $revision + 1); + $root->peer = $peer; + $peer->peer = $root; + + return $root; +} + +$a = ['value' => 1, 'tag' => 't']; +$a['alias'] = &$a['value']; +var_dump($cache->store('k_scalar', $a)); + +$g = $cache->fetch('k_scalar'); +$g['value'] = 99; +echo "scalar alias follows write: "; +var_dump($g['alias'] === 99); +echo "scalar other fetch is independent: "; +var_dump($cache->fetch('k_scalar')['value'] === 1); + +$x = 10; +$b = ['r' => &$x, 'tag' => 'u']; +var_dump($cache->store('k_single', $b)); +echo "single value: "; +var_dump($cache->fetch('k_single')['r'] === 10); + +$o = new UCReferenceHolder(); +$o->n = 7; +$c = []; +$c['p'] = &$o; +$c['q'] = &$o; +var_dump($cache->store('k_objref', $c)); + +$gc = $cache->fetch('k_objref'); +echo "objref identity: "; +var_dump($gc['p'] === $gc['q']); +$gc['p']->n = 123; +echo "objref shared mutation: "; +var_dump($gc['q']->n === 123); + +$pair = new UCReferencePair(); +$v = 1; +$pair->a = &$v; +$pair->b = &$v; +var_dump($cache->store('k_proptref', $pair)); +echo "proptref round-trips to object: "; +var_dump($cache->fetch('k_proptref') instanceof UCReferencePair); + +$gp = $cache->fetch('k_proptref'); +$gp->a = 55; +echo "proptref alias preserved: "; +var_dump($gp->b === 55); + +$typed = new UCTypedReferencePair(); +$typedValue = 11; +$typed->a = &$typedValue; +$typed->b = &$typedValue; +var_dump($cache->store('k_typed_proptref', $typed)); +$gt = $cache->fetch('k_typed_proptref'); +$gt->a = 77; +echo "typed proptref alias preserved: "; +var_dump($gt->b === 77); +try { + $gt->a = 'not an int'; +} catch (TypeError $e) { + echo "typed proptref type check: TypeError\n"; +} + +$dynamic = new stdClass(); +$dynamicValue = 1; +$dynamic->a = &$dynamicValue; +$dynamic->b = &$dynamicValue; +var_dump($cache->store('dpr_scalar', $dynamic)); +$gd = $cache->fetch('dpr_scalar'); +$gd->a = 99; +echo "dynamic scalar alias: "; +var_dump($gd->b === 99); +echo "dynamic independent fetch: "; +var_dump($cache->fetch('dpr_scalar')->a === 1); + +$shared = (object) ['n' => 0]; +$dynamicObject = new stdClass(); +$dynamicObject->x = &$shared; +$dynamicObject->y = &$shared; +var_dump($cache->store('dpr_obj', $dynamicObject)); +$gdo = $cache->fetch('dpr_obj'); +echo "dynamic object identity: "; +var_dump($gdo->x === $gdo->y); +$gdo->x->n = 7; +echo "dynamic object shared mutation: "; +var_dump($gdo->y->n === 7); + +$self = ['n' => 5, 'list' => [1, 2, 3]]; +$self['self'] = &$self; +var_dump($cache->store('k_self', $self)); +$gs = $cache->fetch('k_self'); +echo "self cycle resolves: "; +var_dump($gs['self'] === $gs); +echo "value through cycle: ", $gs['self']['n'], " ", $gs['self']['list'][1], "\n"; + +$p = ['name' => 'p']; +$q = ['name' => 'q']; +$p['other'] = &$q; +$q['other'] = &$p; +var_dump($cache->store('k_mutual', $p)); +$gm = $cache->fetch('k_mutual'); +echo "mutual cycle resolves: "; +var_dump($gm['other']['other'] === $gm); +echo "names: ", $gm['name'], " ", $gm['other']['name'], "\n"; + +$root = ['a' => ['b' => ['c' => 7]]]; +$root['a']['b']['back'] = &$root; +var_dump($cache->store('k_deep', $root)); +$deep = $cache->fetch('k_deep'); +echo "deep cycle resolves: "; +var_dump($deep['a']['b']['back'] === $deep); +echo "deep value: ", $deep['a']['b']['back']['a']['b']['c'], "\n"; + +$g1 = $cache->fetch('k_self'); +$g2 = $cache->fetch('k_self'); +$g1['n'] = 99; +echo "fetches independent: "; +var_dump($g2['n'] === 5); + +$serializedCycle = uc_build_cycle_payload('serialized-root', 'serialized-peer', 1); +var_dump($cache->store('serialized_cycle_object', $serializedCycle)); +$fetchedSerializedCycle = $cache->fetch('serialized_cycle_object'); +echo "serialized cycle access: ", $fetchedSerializedCycle->describe(), "\n"; +echo "serialized cycle back edge: "; +var_dump($fetchedSerializedCycle->peer->peer === $fetchedSerializedCycle); + +$referenceAssignment = new UCReferenceAssignmentPayload( + 'reference-root', + new UCReferencedPayload('reference-child', 1), +); +var_dump($cache->store('reference_assignment_object', $referenceAssignment)); +$fetchedReferenceAssignment = $cache->fetch('reference_assignment_object'); +echo "reference assignment access: "; +var_dump($fetchedReferenceAssignment->name . ':' . $fetchedReferenceAssignment->child->label . ':' . $fetchedReferenceAssignment->child->revision); +echo "reference assignment identity: "; +var_dump($fetchedReferenceAssignment->child === $fetchedReferenceAssignment->alias); +$fetchedReferenceAssignment->child->revision = 9; +echo "reference assignment shared mutation: "; +var_dump($fetchedReferenceAssignment->alias->revision === 9); + +$cycleAssignment = uc_build_cycle_payload('cycle-root', 'cycle-peer', 1); +var_dump($cache->store('cycle_assignment_object', $cycleAssignment)); +$fetchedCycleAssignment = $cache->fetch('cycle_assignment_object'); +echo "cycle assignment access: ", $fetchedCycleAssignment->describe(), "\n"; +echo "cycle assignment back edge: "; +var_dump($fetchedCycleAssignment->peer->peer === $fetchedCycleAssignment); +?> +--EXPECT-- +bool(true) +scalar alias follows write: bool(true) +scalar other fetch is independent: bool(true) +bool(true) +single value: bool(true) +bool(true) +objref identity: bool(true) +objref shared mutation: bool(true) +bool(true) +proptref round-trips to object: bool(true) +proptref alias preserved: bool(true) +bool(true) +typed proptref alias preserved: bool(true) +typed proptref type check: TypeError +bool(true) +dynamic scalar alias: bool(true) +dynamic independent fetch: bool(true) +bool(true) +dynamic object identity: bool(true) +dynamic object shared mutation: bool(true) +bool(true) +self cycle resolves: bool(true) +value through cycle: 5 2 +bool(true) +mutual cycle resolves: bool(true) +names: p q +bool(true) +deep cycle resolves: bool(true) +deep value: 7 +fetches independent: bool(true) +bool(true) +serialized cycle access: serialized-root:1:serialized-peer +serialized cycle back edge: bool(true) +bool(true) +reference assignment access: string(32) "reference-root:reference-child:1" +reference assignment identity: bool(true) +reference assignment shared mutation: bool(true) +bool(true) +cycle assignment access: cycle-root:1:cycle-peer +cycle assignment back edge: bool(true) diff --git a/ext/opcache/tests/user_cache_shared_graph.phpt b/ext/opcache/tests/user_cache_shared_graph.phpt new file mode 100644 index 000000000000..91d4ce503a59 --- /dev/null +++ b/ext/opcache/tests/user_cache_shared_graph.phpt @@ -0,0 +1,40 @@ +--TEST-- +OPcache User Cache: shared graph does not call magic serialization methods +--EXTENSIONS-- +opcache +--INI-- +opcache.enable=1 +opcache.enable_cli=1 +opcache.file_cache_only=0 +opcache.user_cache_shm_size=16M +--FILE-- + 99]; + } + + public function __unserialize(array $data): void { + self::$calls += 10; + $this->value = $data['value']; + } +} + +$object = new MagicUserCacheObject(); +var_dump($cache->store('object', $object)); +$fetched = $cache->fetch('object'); +var_dump($fetched instanceof MagicUserCacheObject); +var_dump($fetched->value); +var_dump(MagicUserCacheObject::$calls); +?> +--EXPECT-- +bool(true) +bool(true) +int(1) +int(0) diff --git a/ext/opcache/tests/user_cache_shm_size_units.phpt b/ext/opcache/tests/user_cache_shm_size_units.phpt new file mode 100644 index 000000000000..e408f08c7a12 --- /dev/null +++ b/ext/opcache/tests/user_cache_shm_size_units.phpt @@ -0,0 +1,16 @@ +--TEST-- +OPcache User Cache: shm size directive accepts PHP quantity syntax +--EXTENSIONS-- +opcache +--INI-- +opcache.enable=1 +opcache.enable_cli=1 +opcache.file_cache_only=0 +opcache.user_cache_shm_size=16 +--FILE-- + +--EXPECT-- +int(16) diff --git a/ext/opcache/tests/user_cache_spl_safe_direct.phpt b/ext/opcache/tests/user_cache_spl_safe_direct.phpt new file mode 100644 index 000000000000..dfe5c0a1e142 --- /dev/null +++ b/ext/opcache/tests/user_cache_spl_safe_direct.phpt @@ -0,0 +1,397 @@ +--TEST-- +OPcache User Cache: SPL safe-direct state is restored +--EXTENSIONS-- +opcache +--INI-- +opcache.enable=1 +opcache.enable_cli=1 +opcache.file_cache_only=0 +opcache.user_cache_shm_size=16M +--FILE-- + parent::__serialize()]; + } + + public function __unserialize(array $data): void + { + self::$unserializeCalls++; + parent::__unserialize($data['payload']); + } +} + +class UserCacheSerializedStack extends SplStack +{ + public static int $serializeCalls = 0; + public static int $unserializeCalls = 0; + + public function __serialize(): array + { + self::$serializeCalls++; + + return parent::__serialize(); + } + + public function __unserialize(array $data): void + { + self::$unserializeCalls++; + parent::__unserialize($data); + } +} + +class UserCacheTaggedFixedArray 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; + } +} + +class UserCacheLabelIterator extends ArrayIterator +{ +} + +class UserCacheTaggedCollection 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 UserCacheTaggedIterator 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; + } +} + +class UserCacheTaggedRecursiveIterator 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; + } +} + +class UserCacheCountingMaxHeap extends SplMaxHeap +{ + public static int $compareCalls = 0; + + protected function compare(mixed $a, mixed $b): int + { + self::$compareCalls++; + + return $a['priority'] <=> $b['priority']; + } +} + +$arrayObject = new ArrayObject(['a' => 1, 'b' => ['c' => 2]], ArrayObject::ARRAY_AS_PROPS); +$arrayObject->extra = 'prop'; + +$arrayIterator = new ArrayIterator(['x' => 10, 'y' => 20]); +$recursiveArrayIterator = new RecursiveArrayIterator(['nested' => ['leaf' => 30]]); + +$fixed = SplFixedArray::fromArray(['zero', 'one', ['two']], false); + +$taggedFixed = new UserCacheTaggedFixedArray(3, 'vec', 7); +$taggedFixed[0] = 'a'; +$taggedFixed[1] = ['nested' => 1]; +$taggedFixed[2] = 42; + +$taggedCollection = new UserCacheTaggedCollection(['alpha' => 10, 'beta' => 20], 'metric', UserCacheLabelIterator::class); +$taggedIterator = new UserCacheTaggedIterator([3, 5, 8], 'fib'); +$taggedRecursiveIterator = new UserCacheTaggedRecursiveIterator(['leaf' => ['value' => 99]], 'tree'); + +$dll = new SplDoublyLinkedList(); +$dll->setIteratorMode(SplDoublyLinkedList::IT_MODE_FIFO); +$dll->push('first'); +$dll->push('second'); + +$queue = new SplQueue(); +$queue->enqueue('q1'); +$queue->enqueue('q2'); + +$stack = new SplStack(); +$stack->push('s1'); +$stack->push('s2'); + +$min = new SplMinHeap(); +$min->insert(3); +$min->insert(1); +$min->insert(2); + +$max = new SplMaxHeap(); +$max->insert(3); +$max->insert(1); +$max->insert(2); + +$pq = new SplPriorityQueue(); +$pq->setExtractFlags(SplPriorityQueue::EXTR_BOTH); +$pq->insert('low', 1); +$pq->insert('high', 10); + +$countingMaxHeap = new UserCacheCountingMaxHeap(); +$countingMaxHeap->insert(['priority' => 1, 'node' => (object) ['score' => 61]]); +$countingMaxHeap->insert(['priority' => 2, 'node' => (object) ['score' => 67]]); +UserCacheCountingMaxHeap::$compareCalls = 0; + +$serializedArrayObject = new UserCacheSerializedArrayObject(['x' => 1]); + +$serializedStack = new UserCacheSerializedStack(); +$serializedStack->push('fallback'); + +$payload = compact( + 'arrayObject', + 'arrayIterator', + 'recursiveArrayIterator', + 'fixed', + 'taggedFixed', + 'taggedCollection', + 'taggedIterator', + 'taggedRecursiveIterator', + 'dll', + 'queue', + 'stack', + 'min', + 'max', + 'pq', + 'countingMaxHeap', + 'serializedArrayObject', + 'serializedStack' +); + +var_dump($cache->store('spl', $payload)); + +$dirty = $cache->fetch('spl'); +$dirty['arrayObject']['a'] = 999; +$clean = $cache->fetch('spl'); +var_dump($clean['arrayObject']['a']); + +$fetched = $cache->fetch('spl'); + +var_dump($fetched['arrayObject'] instanceof ArrayObject); +var_dump($fetched['arrayObject']['b']['c']); +var_dump($fetched['arrayObject']->extra); + +var_dump($fetched['arrayIterator'] instanceof ArrayIterator); +var_dump(iterator_to_array($fetched['arrayIterator'])); + +var_dump($fetched['recursiveArrayIterator'] instanceof RecursiveArrayIterator); +var_dump($fetched['recursiveArrayIterator']->hasChildren()); + +var_dump($fetched['fixed'] instanceof SplFixedArray); +var_dump($fetched['fixed']->getSize()); +var_dump($fetched['fixed'][2][0]); + +var_dump($fetched['taggedFixed'] instanceof UserCacheTaggedFixedArray); +var_dump($fetched['taggedFixed']->getSize()); +var_dump($fetched['taggedFixed'][0]); +var_dump($fetched['taggedFixed'][1]['nested']); +var_dump($fetched['taggedFixed'][2]); +var_dump($fetched['taggedFixed']->describe()); + +$taggedCollectionIterator = $fetched['taggedCollection']->getIterator(); +var_dump($fetched['taggedCollection'] instanceof UserCacheTaggedCollection); +var_dump($taggedCollectionIterator instanceof UserCacheLabelIterator); +var_dump($fetched['taggedCollection']['alpha']); +var_dump($fetched['taggedCollection']['beta']); +var_dump($fetched['taggedCollection']->type()); + +$fetched['taggedIterator']->rewind(); +var_dump($fetched['taggedIterator'] instanceof UserCacheTaggedIterator); +var_dump($fetched['taggedIterator']->count()); +var_dump($fetched['taggedIterator']->current()); +var_dump($fetched['taggedIterator']->label()); + +$fetched['taggedRecursiveIterator']->rewind(); +var_dump($fetched['taggedRecursiveIterator'] instanceof UserCacheTaggedRecursiveIterator); +var_dump($fetched['taggedRecursiveIterator']->count()); +var_dump($fetched['taggedRecursiveIterator']->hasChildren()); +var_dump($fetched['taggedRecursiveIterator']->name()); + +var_dump($fetched['dll'] instanceof SplDoublyLinkedList); +var_dump(iterator_to_array($fetched['dll'], false)); + +var_dump($fetched['queue'] instanceof SplQueue); +var_dump($fetched['queue']->dequeue()); +var_dump($fetched['queue']->dequeue()); + +var_dump($fetched['stack'] instanceof SplStack); +var_dump($fetched['stack']->pop()); +var_dump($fetched['stack']->pop()); + +var_dump($fetched['min'] instanceof SplMinHeap); +$minOut = []; +while (!$fetched['min']->isEmpty()) { + $minOut[] = $fetched['min']->extract(); +} +var_dump($minOut); + +var_dump($fetched['max'] instanceof SplMaxHeap); +$maxOut = []; +while (!$fetched['max']->isEmpty()) { + $maxOut[] = $fetched['max']->extract(); +} +var_dump($maxOut); + +var_dump($fetched['pq'] instanceof SplPriorityQueue); +var_dump($fetched['pq']->extract()); +var_dump($fetched['pq']->extract()); + +var_dump($fetched['countingMaxHeap'] instanceof UserCacheCountingMaxHeap); +var_dump($fetched['countingMaxHeap']->top()['node']->score); +var_dump(UserCacheCountingMaxHeap::$compareCalls); + +var_dump($fetched['serializedArrayObject'] instanceof UserCacheSerializedArrayObject); +var_dump($fetched['serializedArrayObject']['x']); +var_dump(UserCacheSerializedArrayObject::$serializeCalls); +var_dump(UserCacheSerializedArrayObject::$unserializeCalls); + +var_dump($fetched['serializedStack'] instanceof UserCacheSerializedStack); +var_dump($fetched['serializedStack'][0]); +var_dump(UserCacheSerializedStack::$serializeCalls); +var_dump(UserCacheSerializedStack::$unserializeCalls); +?> +--EXPECT-- +bool(true) +int(1) +bool(true) +int(2) +string(4) "prop" +bool(true) +array(2) { + ["x"]=> + int(10) + ["y"]=> + int(20) +} +bool(true) +bool(true) +bool(true) +int(3) +string(3) "two" +bool(true) +int(3) +string(1) "a" +int(1) +int(42) +string(5) "vec:7" +bool(true) +bool(true) +int(10) +int(20) +string(6) "metric" +bool(true) +int(3) +int(3) +string(3) "fib" +bool(true) +int(1) +bool(true) +string(4) "tree" +bool(true) +array(2) { + [0]=> + string(5) "first" + [1]=> + string(6) "second" +} +bool(true) +string(2) "q1" +string(2) "q2" +bool(true) +string(2) "s2" +string(2) "s1" +bool(true) +array(3) { + [0]=> + int(1) + [1]=> + int(2) + [2]=> + int(3) +} +bool(true) +array(3) { + [0]=> + int(3) + [1]=> + int(2) + [2]=> + int(1) +} +bool(true) +array(2) { + ["data"]=> + string(4) "high" + ["priority"]=> + int(10) +} +array(2) { + ["data"]=> + string(3) "low" + ["priority"]=> + int(1) +} +bool(true) +int(67) +int(0) +bool(true) +int(1) +int(0) +int(0) +bool(true) +string(8) "fallback" +int(0) +int(0) diff --git a/ext/opcache/tests/user_cache_storable_values.phpt b/ext/opcache/tests/user_cache_storable_values.phpt new file mode 100644 index 000000000000..99d537740610 --- /dev/null +++ b/ext/opcache/tests/user_cache_storable_values.phpt @@ -0,0 +1,146 @@ +--TEST-- +OPcache User Cache: storable values use shared graph and opaque values are refused +--EXTENSIONS-- +opcache +spl +--INI-- +opcache.enable=1 +opcache.enable_cli=1 +opcache.file_cache_only=0 +opcache.user_cache_shm_size=16M +date.timezone=UTC +error_reporting=E_ALL & ~E_DEPRECATED +--FILE-- +store('storable_' . $i, $value) ? 'stored' : 'REFUSED'; +} + +echo "array: ", user_cache_storable_tier($cache, ['a' => 1, 'b' => [2, 3]]), "\n"; +echo "stdClass: ", user_cache_storable_tier($cache, (object) ['x' => 1, 'y' => [2]]), "\n"; +echo "json object: ", user_cache_storable_tier($cache, json_decode('{"a":1,"b":{"c":2}}')), "\n"; +echo "plain object: ", user_cache_storable_tier($cache, new UserCacheStorablePlain()), "\n"; +echo "DateTime: ", user_cache_storable_tier($cache, new DateTime('2026-01-01')), "\n"; +echo "DateInterval: ", user_cache_storable_tier($cache, new DateInterval('P1D')), "\n"; +echo "ArrayObject: ", user_cache_storable_tier($cache, new ArrayObject([1, 2, 3])), "\n"; +echo "SplStack: ", user_cache_storable_tier($cache, (function () { $s = new SplStack(); $s->push(1); return $s; })()), "\n"; +echo "__sleep obj: ", user_cache_storable_tier($cache, new UserCacheStorableSleep()), "\n"; +echo "enum: ", user_cache_storable_tier($cache, UserCacheStorableSuit::Hearts), "\n"; +$ref = 1; +echo "array w/ ref: ", user_cache_storable_tier($cache, ['x' => &$ref, 'y' => &$ref]), "\n"; + +$sleepFetched = $cache->fetch('storable_9'); +echo "__sleep magic calls: "; +var_dump([UserCacheStorableSleep::$sleepCalls, UserCacheStorableSleep::$wakeupCalls]); +echo "__sleep object access: "; +var_dump($sleepFetched->a . ':' . $sleepFetched->b); + +$resource = fopen(__FILE__, 'r'); +$closure = static fn () => true; + +try { + $cache->store('resource-root', $resource); + echo "Resource: stored\n"; +} catch (TypeError $e) { + echo "Resource: TypeError\n"; +} + +try { + $cache->store('closure-root', $closure); + echo "Closure: stored\n"; +} catch (TypeError $e) { + echo "Closure: TypeError\n"; +} + +echo "nested resource: ", user_cache_storable_tier($cache, ['value' => $resource]), "\n"; +echo "nested closure: ", user_cache_storable_tier($cache, ['value' => $closure]), "\n"; +echo "object resource: ", user_cache_storable_tier($cache, new UserCacheUnsupportedBox($resource)), "\n"; +echo "object closure: ", user_cache_storable_tier($cache, new UserCacheUnsupportedBox($closure)), "\n"; +echo "fixed resource: ", user_cache_storable_tier($cache, SplFixedArray::fromArray([$resource], false)), "\n"; +echo "fixed closure: ", user_cache_storable_tier($cache, SplFixedArray::fromArray([$closure], false)), "\n"; +echo "array resource: ", user_cache_storable_tier($cache, new ArrayObject(['value' => $resource])), "\n"; +echo "array closure: ", user_cache_storable_tier($cache, new ArrayObject(['value' => $closure])), "\n"; + +$fiber = new Fiber(function () { Fiber::suspend(); }); +$fiber->start(); +echo "Fiber: ", user_cache_storable_tier($cache, $fiber), "\n"; +echo "Generator: ", user_cache_storable_tier($cache, (function () { yield 1; })()), "\n"; +echo "WeakMap: ", user_cache_storable_tier($cache, (function () { $m = new WeakMap(); $m[new stdClass()] = 1; return $m; })()), "\n"; + +fclose($resource); +?> +--EXPECT-- +array: stored +stdClass: stored +json object: stored +plain object: stored +DateTime: stored +DateInterval: stored +ArrayObject: stored +SplStack: stored +__sleep obj: stored +enum: stored +array w/ ref: stored +__sleep magic calls: array(2) { + [0]=> + int(0) + [1]=> + int(0) +} +__sleep object access: string(3) "1:2" +Resource: TypeError +Closure: TypeError +nested resource: REFUSED +nested closure: REFUSED +object resource: REFUSED +object closure: REFUSED +fixed resource: REFUSED +fixed closure: REFUSED +array resource: REFUSED +array closure: REFUSED +Fiber: REFUSED +Generator: REFUSED +WeakMap: REFUSED diff --git a/ext/opcache/tests/user_cache_store_multiple_rollback.phpt b/ext/opcache/tests/user_cache_store_multiple_rollback.phpt new file mode 100644 index 000000000000..1a94dc8a2a6d --- /dev/null +++ b/ext/opcache/tests/user_cache_store_multiple_rollback.phpt @@ -0,0 +1,46 @@ +--TEST-- +OPcache User Cache: storeMultiple rolls back partial writes on failure +--EXTENSIONS-- +opcache +--INI-- +opcache.enable=1 +opcache.enable_cli=1 +opcache.file_cache_only=0 +opcache.user_cache_shm_size=1M +--FILE-- +store('existing-a', 'old-a')); +var_dump($cache->store('existing-b', 'old-b')); + +var_dump($cache->storeMultiple([ + 'existing-a' => 'new-a', + 'new-key' => 'new-value', + 'existing-b' => str_repeat('x', 4 * 1024 * 1024), +])); + +var_dump($cache->fetch('existing-a')); +var_dump($cache->fetch('existing-b')); +var_dump($cache->has('new-key')); + +$fresh = new Opcache\UserCache('store-multiple-rollback-fresh'); +var_dump($fresh->storeMultiple([ + 'fresh-a' => 'new-a', + 'fresh-b' => str_repeat('x', 4 * 1024 * 1024), +])); +var_dump($fresh->has('fresh-a')); +var_dump($fresh->has('fresh-b')); +?> +--EXPECT-- +bool(true) +bool(true) +bool(false) +string(5) "old-a" +string(5) "old-b" +bool(false) +bool(false) +bool(false) +bool(false) diff --git a/ext/opcache/tests/user_cache_tombstone_rehash.phpt b/ext/opcache/tests/user_cache_tombstone_rehash.phpt new file mode 100644 index 000000000000..d4b6bcdd9fd5 --- /dev/null +++ b/ext/opcache/tests/user_cache_tombstone_rehash.phpt @@ -0,0 +1,47 @@ +--TEST-- +OPcache User Cache: rehashes the entry table after delete churn +--EXTENSIONS-- +opcache +--INI-- +opcache.enable=1 +opcache.enable_cli=1 +opcache.file_cache_only=0 +opcache.user_cache_shm_size=16M +--FILE-- +store("keep_$i", ['id' => $i, 'name' => "keeper_$i"]); +} + +for ($round = 0; $round < 10; $round++) { + for ($i = 0; $i < 100; $i++) { + $cache->store("churn_{$round}_{$i}", $i); + } + for ($i = 0; $i < 100; $i++) { + $cache->delete("churn_{$round}_{$i}"); + } +} + +$ok = true; +for ($i = 0; $i < 50; $i++) { + $value = $cache->fetch("keep_$i"); + if (!is_array($value) || $value['id'] !== $i || $value['name'] !== "keeper_$i") { + $ok = false; + echo "lost keep_$i\n"; + } +} + +var_dump($ok); +var_dump($cache->fetch('churn_0_0', 'gone')); +var_dump($cache->has('churn_9_99')); +var_dump($cache->store('after_rehash', 'value')); +var_dump($cache->fetch('after_rehash')); +?> +--EXPECT-- +bool(true) +string(4) "gone" +bool(false) +bool(true) +string(5) "value" diff --git a/ext/opcache/tests/user_cache_ttl_remember.phpt b/ext/opcache/tests/user_cache_ttl_remember.phpt new file mode 100644 index 000000000000..24bcb0dc75a8 --- /dev/null +++ b/ext/opcache/tests/user_cache_ttl_remember.phpt @@ -0,0 +1,37 @@ +--TEST-- +OPcache User Cache: TTL and remember() +--EXTENSIONS-- +opcache +--INI-- +opcache.enable=1 +opcache.enable_cli=1 +opcache.file_cache_only=0 +opcache.user_cache_shm_size=16M +--FILE-- +store('ttl', 'value', 1)); +var_dump($cache->has('ttl')); +sleep(2); +var_dump($cache->fetch('ttl', 'expired')); + +$calls = 0; +var_dump($cache->remember('remembered', function () use (&$calls) { + $calls++; + return 'computed'; +})); +var_dump($cache->remember('remembered', function () use (&$calls) { + $calls++; + return 'ignored'; +})); +var_dump($calls); +?> +--EXPECT-- +bool(true) +bool(true) +string(7) "expired" +string(8) "computed" +string(8) "computed" +int(1) diff --git a/ext/opcache/zend_accelerator_module.c b/ext/opcache/zend_accelerator_module.c index 465b15cd9576..3bd553a7f655 100644 --- a/ext/opcache/zend_accelerator_module.c +++ b/ext/opcache/zend_accelerator_module.c @@ -28,6 +28,7 @@ #include "zend_shared_alloc.h" #include "zend_accelerator_blacklist.h" #include "zend_file_cache.h" +#include "zend_user_cache.h" #include "php_ini.h" #include "SAPI.h" #include "zend_virtual_cwd.h" @@ -100,6 +101,34 @@ static ZEND_INI_MH(OnUpdateMemoryConsumption) return SUCCESS; } +static ZEND_INI_MH(OnUpdateUserCacheShmSize) +{ + zend_long *p, size; + + if (accel_startup_ok) { + if (sapi_module.name != NULL && strcmp(sapi_module.name, "fpm-fcgi") == 0) { + zend_accel_error(ACCEL_LOG_WARNING, "opcache.user_cache_shm_size cannot be changed when OPcache is already set up. Are you using php_admin_value[opcache.user_cache_shm_size] in an individual pool's configuration?\n"); + } else { + zend_accel_error(ACCEL_LOG_WARNING, "opcache.user_cache_shm_size cannot be changed when OPcache is already set up.\n"); + } + + return FAILURE; + } + + p = ZEND_INI_GET_ADDR(); + size = zend_ini_parse_quantity_warn(new_value, entry->name); + + if (size < 0) { + zend_accel_error(ACCEL_LOG_WARNING, "opcache.user_cache_shm_size must be greater than or equal to 0, " ZEND_LONG_FMT " given.\n", size); + + return FAILURE; + } + + *p = size; + + return SUCCESS; +} + static ZEND_INI_MH(OnUpdateInternedStringsBuffer) { zend_long *p = ZEND_INI_GET_ADDR(); @@ -292,6 +321,7 @@ 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.user_cache_shm_size", "16M" , PHP_INI_SYSTEM, OnUpdateUserCacheShmSize, accel_directives.user_cache_shm_size, 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) @@ -444,6 +474,7 @@ static ZEND_NAMED_FUNCTION(accel_is_readable) static ZEND_MINIT_FUNCTION(zend_accelerator) { start_accel_extension(); + zend_opcache_user_cache_minit(); return SUCCESS; } @@ -485,11 +516,17 @@ static ZEND_MSHUTDOWN_FUNCTION(zend_accelerator) (void)type; /* keep the compiler happy */ UNREGISTER_INI_ENTRIES(); + zend_opcache_user_cache_mshutdown(); accel_shutdown(); return SUCCESS; } +static ZEND_RSHUTDOWN_FUNCTION(zend_accelerator) +{ + return zend_opcache_user_cache_rshutdown(); +} + void zend_accel_info(ZEND_MODULE_INFO_FUNC_ARGS) { php_info_print_table_start(); @@ -606,7 +643,7 @@ zend_module_entry opcache_module_entry = { 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, @@ -822,6 +859,7 @@ 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.user_cache_shm_size", ZCG(accel_directives).user_cache_shm_size); 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); @@ -922,6 +960,20 @@ ZEND_FUNCTION(opcache_reset) zend_shared_alloc_lock(); zend_accel_schedule_restart(ACCEL_RESTART_USER); zend_shared_alloc_unlock(); + zend_opcache_user_cache_invalidate_all(); + RETURN_TRUE; +} + +ZEND_FUNCTION(opcache_user_cache_reset) +{ + ZEND_PARSE_PARAMETERS_NONE(); + + if (!validate_api_restriction()) { + RETURN_FALSE; + } + + zend_opcache_user_cache_invalidate_all(); + RETURN_TRUE; } diff --git a/ext/opcache/zend_user_cache.c b/ext/opcache/zend_user_cache.c new file mode 100644 index 000000000000..d20c4d26236b --- /dev/null +++ b/ext/opcache/zend_user_cache.c @@ -0,0 +1,2213 @@ +/* + +----------------------------------------------------------------------+ + | 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_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_user_cache.h" +#include "zend_smart_str.h" + +#include "ext/standard/php_var.h" + +#include "SAPI.h" + +#include "zend_user_cache_internal.h" + +#include "opcache_user_cache_arginfo.h" + +#define ZEND_OPCACHE_USER_CACHE_API_VALUE_TYPE "object|array|string|int|float|bool|null" +#define ZEND_OPCACHE_USER_CACHE_STORAGE_KEY_CACHE_MAX 4096U + +typedef struct _zend_opcache_user_cache_object { + zend_string *scope; + zend_string *scope_prefix; + zend_opcache_user_cache_context *context; + HashTable *storage_key_cache; + zend_object std; +} zend_opcache_user_cache_object; + +typedef struct { + zend_ulong hash; + zend_string *key; + uint32_t index; +} zend_opcache_user_cache_bulk_order; + +typedef struct { + zend_long configured_memory; + zend_long shared_memory_size; + zend_long used_memory; + zend_long free_memory; + zend_long wasted_memory; + zend_long entry_count; + zend_long entry_capacity; + zend_long tombstone_count; + bool backend_initialized; +} zend_opcache_user_cache_info_stats; + +zend_class_entry *zend_opcache_user_cache_exception_ce; +zend_class_entry *zend_opcache_user_cache_info_ce; +static zend_class_entry *zend_opcache_user_cache_cache_ce; +static zend_object_handlers zend_opcache_user_cache_object_handlers; +zend_opcache_user_cache_context zend_opcache_user_cache_context_state = { + {0}, "user cache", "opcache_user_cache_lock", +#ifndef ZEND_WIN32 + ZEND_OPCACHE_USER_CACHE_SEM_FILENAME_PREFIX, +#endif + true, false +}; +bool zend_opcache_user_cache_runtime_opted_in = false; +zend_opcache_user_cache_partition *zend_opcache_user_cache_partitions = NULL; +static HashTable zend_opcache_user_cache_safe_direct_handler_table; +static bool zend_opcache_user_cache_safe_direct_handlers_initialized = false; +ZEND_EXT_TLS const char *zend_opcache_user_cache_request_unavailable_reason = NULL; +ZEND_EXT_TLS zend_opcache_user_cache_partition *zend_opcache_user_cache_active_partition = NULL; +ZEND_EXT_TLS bool zend_opcache_user_cache_lock_held = false; +ZEND_EXT_TLS bool zend_opcache_user_cache_lock_held_is_write = false; +ZEND_EXT_TLS zend_opcache_user_cache_runtime zend_opcache_user_cache_runtime_state = {0}; +ZEND_EXT_TLS zend_opcache_user_cache_context *zend_opcache_user_cache_active_context_ptr = NULL; +ZEND_EXT_TLS zend_opcache_user_cache_shared_graph_ref *zend_opcache_user_cache_shared_graph_refs = NULL; +ZEND_EXT_TLS uint32_t zend_opcache_user_cache_shared_graph_ref_count = 0; +ZEND_EXT_TLS uint32_t zend_opcache_user_cache_shared_graph_ref_capacity = 0; +ZEND_EXT_TLS HashTable *zend_opcache_user_cache_shared_graph_ref_index = NULL; +ZEND_EXT_TLS zend_opcache_user_cache_shared_graph_ref *zend_opcache_user_cache_retired_shared_graphs = NULL; +ZEND_EXT_TLS uint32_t zend_opcache_user_cache_retired_shared_graph_count = 0; +ZEND_EXT_TLS uint32_t zend_opcache_user_cache_retired_shared_graph_capacity = 0; +ZEND_EXT_TLS zend_opcache_user_cache_lookup_entry zend_opcache_user_cache_lookup_entry_storage[ZEND_OPCACHE_USER_CACHE_LOOKUP_BUCKETS]; +ZEND_EXT_TLS HashTable *zend_opcache_user_cache_request_local_slot_table = NULL; +ZEND_EXT_TLS HashTable *zend_opcache_user_cache_entry_lock_table = NULL; + +static zend_string *zend_opcache_user_cache_storage_key( + zend_opcache_user_cache_object *cache, + zend_string *key +); +static void zend_opcache_user_cache_object_free(zend_object *object); +static zend_object *zend_opcache_user_cache_object_create(zend_class_entry *ce); +static bool zend_opcache_user_cache_store_storage_key_prevalidated(zend_string *key, zval *value, zend_long ttl); + +static bool zend_opcache_user_cache_key_is_valid_user_key(zend_string *key) +{ + return ZSTR_LEN(key) != 0 && + memchr(ZSTR_VAL(key), ZEND_OPCACHE_USER_CACHE_KEY_DELIMITER_CHAR, ZSTR_LEN(key)) == NULL + ; +} + +static bool zend_opcache_user_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; + } + + if (memchr(ZSTR_VAL(key), ZEND_OPCACHE_USER_CACHE_KEY_DELIMITER_CHAR, ZSTR_LEN(key)) != NULL) { + zend_argument_value_error(arg_num, "must not contain the user-cache key delimiter " ZEND_OPCACHE_USER_CACHE_KEY_DELIMITER_NAME); + + return false; + } + + return true; +} + +static bool zend_opcache_user_cache_validate_store_array_value(zval *value, uint32_t arg_num) +{ + ZVAL_DEREF(value); + + if (Z_TYPE_P(value) == IS_RESOURCE) { + zend_argument_type_error(arg_num, "must contain only values of type " ZEND_OPCACHE_USER_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 contain Closure objects"); + + return false; + } + + return true; +} + +static bool zend_opcache_user_cache_validate_store_array(HashTable *values, uint32_t arg_num) +{ + zend_string *key; + zval *value; + + ZEND_HASH_FOREACH_STR_KEY_VAL(values, key, value) { + if (key == NULL || !zend_opcache_user_cache_key_is_valid_user_key(key)) { + zend_argument_value_error(arg_num, "must be an array with non-empty string keys that do not contain " ZEND_OPCACHE_USER_CACHE_KEY_DELIMITER_NAME); + + return false; + } + + if (!zend_opcache_user_cache_validate_store_array_value(value, arg_num)) { + return false; + } + } ZEND_HASH_FOREACH_END(); + + return true; +} + +static void zend_opcache_user_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 bool zend_opcache_user_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 (!zend_opcache_user_cache_key_is_valid_user_key(Z_STR_P(value))) { + zend_argument_value_error(arg_num, "must contain only non-empty string or int cache keys that do not contain " ZEND_OPCACHE_USER_CACHE_KEY_DELIMITER_NAME); + zend_opcache_user_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 that do not contain " ZEND_OPCACHE_USER_CACHE_KEY_DELIMITER_NAME); + zend_opcache_user_cache_release_key_list(prepared, index); + + return false; + } + } ZEND_HASH_FOREACH_END(); + + *prepared_keys = prepared; + *prepared_key_count = index; + + return true; +} + +static bool zend_opcache_user_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_USER_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 zend_string *zend_opcache_user_cache_build_scope_prefix(zend_string *scope) +{ + const char *prefix = "user_cache" ZEND_OPCACHE_USER_CACHE_KEY_DELIMITER; + size_t prefix_len = sizeof("user_cache" ZEND_OPCACHE_USER_CACHE_KEY_DELIMITER) - 1; + + return zend_string_concat3( + prefix, + prefix_len, + ZSTR_VAL(scope), + ZSTR_LEN(scope), + ZEND_STRL(ZEND_OPCACHE_USER_CACHE_KEY_DELIMITER) + ); +} + +static zend_string *zend_opcache_user_cache_build_storage_key( + const zend_opcache_user_cache_object *cache, + zend_string *key) +{ + return zend_string_concat2( + ZSTR_VAL(cache->scope_prefix), + ZSTR_LEN(cache->scope_prefix), + ZSTR_VAL(key), + ZSTR_LEN(key) + ); +} + +static zend_opcache_user_cache_object *zend_opcache_user_cache_object_from_obj( + zend_object *object) +{ + return (zend_opcache_user_cache_object *) ((char *) object - offsetof(zend_opcache_user_cache_object, std)); +} + +static zend_opcache_user_cache_object *zend_opcache_user_cache_from_this(zval *this_ptr) +{ + zend_opcache_user_cache_object *cache = zend_opcache_user_cache_object_from_obj(Z_OBJ_P(this_ptr)); + + if (cache->scope_prefix == NULL) { + zend_throw_error(NULL, "Opcache\\UserCache instance was not initialized"); + + return NULL; + } + + return cache; +} + +static void zend_opcache_user_cache_register_classes(void) +{ + if (zend_opcache_user_cache_cache_ce != NULL) { + return; + } + + zend_opcache_user_cache_exception_ce = zend_ce_exception; + zend_opcache_user_cache_info_ce = register_class_Opcache_UserCacheInfo(); + zend_opcache_user_cache_cache_ce = register_class_Opcache_UserCache(); + + zend_opcache_user_cache_cache_ce->create_object = zend_opcache_user_cache_object_create; + + memcpy( + &zend_opcache_user_cache_object_handlers, + zend_get_std_object_handlers(), + sizeof(zend_object_handlers) + ); + + zend_opcache_user_cache_object_handlers.offset = offsetof(zend_opcache_user_cache_object, std); + zend_opcache_user_cache_object_handlers.free_obj = zend_opcache_user_cache_object_free; + zend_opcache_user_cache_object_handlers.clone_obj = NULL; +} + +static void zend_opcache_user_cache_reset_class_entries(void) +{ + zend_opcache_user_cache_exception_ce = NULL; + zend_opcache_user_cache_info_ce = NULL; + zend_opcache_user_cache_cache_ce = NULL; +} + +static void zend_opcache_user_cache_invalidate_all_context(zend_opcache_user_cache_context *context) +{ + zend_opcache_user_cache_context *previous_context; + + previous_context = zend_opcache_user_cache_activate_context(context); + + if (ZCG(accel_directives).user_cache_shm_size == 0 || !context->storage.initialized) { + zend_opcache_user_cache_restore_context(previous_context); + + return; + } + + if (!zend_opcache_user_cache_wlock()) { + zend_opcache_user_cache_restore_context(previous_context); + + return; + } + + if (zend_opcache_user_cache_header_is_initialized_locked()) { + zend_opcache_user_cache_clear_locked(); + } + + zend_opcache_user_cache_unlock(); + zend_opcache_user_cache_restore_context(previous_context); +} + +static bool zend_opcache_user_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 bool zend_opcache_user_cache_parse_step(zend_long step, uint32_t arg_num) +{ + if (step < 0) { + zend_argument_value_error(arg_num, "must be greater than or equal to 0"); + + return false; + } + + return true; +} + +static bool zend_opcache_user_cache_parse_lease(zend_long lease, uint32_t arg_num) +{ + if (lease < 0) { + zend_argument_value_error(arg_num, "must be greater than or equal to 0"); + + return false; + } + + return true; +} + +/* The optimistic path performs its own consistency validation. */ +static bool zend_opcache_user_cache_can_read(void) +{ + zend_opcache_user_cache_ensure_ready(); + + return zend_opcache_user_cache_active_runtime()->available; +} + +static bool zend_opcache_user_cache_begin_read(void) +{ + if (!zend_opcache_user_cache_can_read()) { + return false; + } + + if (!zend_opcache_user_cache_rlock()) { + return false; + } + + if (!zend_opcache_user_cache_header_is_initialized_locked()) { + zend_opcache_user_cache_unlock(); + + return false; + } + + return true; +} + +static bool zend_opcache_user_cache_can_write(void) +{ + zend_opcache_user_cache_ensure_ready(); + + if (!zend_opcache_user_cache_active_runtime()->available) { + return false; + } + + return true; +} + +static bool zend_opcache_user_cache_acquire_write_lock(void) +{ + if (!zend_opcache_user_cache_wlock()) { + return false; + } + + if (!zend_opcache_user_cache_header_init_locked()) { + zend_opcache_user_cache_unlock(); + + return false; + } + + return true; +} + +static bool zend_opcache_user_cache_begin_entry_mutation(zend_string *key, bool *release_entry_lock) +{ + ZEND_ASSERT(release_entry_lock != NULL); + *release_entry_lock = false; + + if (!zend_opcache_user_cache_has_entry_lock(key)) { + if (!zend_opcache_user_cache_acquire_entry_lock(key)) { + return false; + } + + *release_entry_lock = true; + } + + return true; +} + +static void zend_opcache_user_cache_finish_entry_mutation(zend_string *key, bool release_entry_lock) +{ + if (release_entry_lock) { + zend_opcache_user_cache_release_entry_lock(key); + } +} + +static bool zend_opcache_user_cache_delete_storage_key_prevalidated(zend_string *key) +{ + if (!zend_opcache_user_cache_wlock_for_entry_mutation(key)) { + return false; + } + + zend_opcache_user_cache_delete_locked(key); + + zend_opcache_user_cache_unlock(); + + return true; +} + +static void zend_opcache_user_cache_delete_corrupt_entry_prevalidated(zend_string *key) +{ + if (EG(exception)) { + return; + } + + (void) zend_opcache_user_cache_delete_storage_key_prevalidated(key); +} + +static bool zend_opcache_user_cache_clear_scope_prevalidated( + zend_string *scope_prefix) +{ + if (!zend_opcache_user_cache_acquire_write_lock()) { + return false; + } + + zend_opcache_user_cache_delete_entries_by_prefix_locked(scope_prefix); + + zend_opcache_user_cache_unlock(); + zend_opcache_user_cache_release_active_request_local_slots_by_prefix(scope_prefix); + + return true; +} + +static void zend_opcache_user_cache_fetch_api( + zend_opcache_user_cache_context *context, + zend_string *key, + zval *default_value, + zval *return_value) +{ + zend_opcache_user_cache_context *previous_context; + bool fetched, found = false; + + previous_context = zend_opcache_user_cache_activate_context(context); + + if (!zend_opcache_user_cache_can_read()) { + zend_opcache_user_cache_restore_context(previous_context); + ZVAL_COPY(return_value, default_value); + + return; + } + + switch (zend_opcache_user_cache_fetch_optimistic(key, return_value)) { + case ZEND_OPCACHE_USER_CACHE_OPTIMISTIC_FOUND: + zend_opcache_user_cache_restore_context(previous_context); + + return; + case ZEND_OPCACHE_USER_CACHE_OPTIMISTIC_MISS: + zend_opcache_user_cache_restore_context(previous_context); + ZVAL_COPY(return_value, default_value); + + return; + case ZEND_OPCACHE_USER_CACHE_OPTIMISTIC_FALLBACK: + break; + } + + if (EG(exception)) { + zend_clear_exception(); + zend_opcache_user_cache_delete_corrupt_entry_prevalidated(key); + zend_opcache_user_cache_restore_context(previous_context); + ZVAL_COPY(return_value, default_value); + + return; + } + + if (!zend_opcache_user_cache_begin_read()) { + zend_opcache_user_cache_restore_context(previous_context); + + ZVAL_COPY(return_value, default_value); + + return; + } + + fetched = zend_opcache_user_cache_fetch_locked(key, return_value, false, &found, true); + + zend_opcache_user_cache_unlock(); + + if (!fetched) { + if (!found) { + zend_opcache_user_cache_restore_context(previous_context); + ZVAL_COPY(return_value, default_value); + + return; + } + + if (EG(exception)) { + zend_clear_exception(); + } + + zend_opcache_user_cache_delete_corrupt_entry_prevalidated(key); + zend_opcache_user_cache_restore_context(previous_context); + ZVAL_COPY(return_value, default_value); + + return; + } + + zend_opcache_user_cache_restore_context(previous_context); +} + +static zend_result zend_opcache_user_cache_fetch_multiple_api( + zend_opcache_user_cache_object *cache, + HashTable *keys, + zval *default_value, + zval *return_value) +{ + zend_opcache_user_cache_context *previous_context; + zend_string **prepared_keys, **storage_keys, *key, *storage_key; + zval fetched_value, default_copy; + uint32_t key_count, index; + bool fetched, found, locked_fetch; + + if (!zend_opcache_user_cache_prepare_key_list(keys, &prepared_keys, &key_count, 1)) { + return FAILURE; + } + + storage_keys = NULL; + if (key_count != 0) { + storage_keys = safe_emalloc(key_count, sizeof(zend_string *), 0); + for (index = 0; index < key_count; index++) { + storage_keys[index] = zend_opcache_user_cache_storage_key(cache, prepared_keys[index]); + } + } + + array_init_size(return_value, key_count); + + previous_context = zend_opcache_user_cache_activate_context(cache->context); + if (!zend_opcache_user_cache_begin_read()) { + zend_opcache_user_cache_restore_context(previous_context); + for (index = 0; index < key_count; index++) { + ZVAL_COPY(&default_copy, default_value); + zend_hash_update(Z_ARRVAL_P(return_value), prepared_keys[index], &default_copy); + zend_string_release(storage_keys[index]); + } + + if (storage_keys != NULL) { + efree(storage_keys); + } + + zend_opcache_user_cache_release_key_list(prepared_keys, key_count); + + return SUCCESS; + } + + locked_fetch = true; + for (index = 0; index < key_count; index++) { + key = prepared_keys[index]; + storage_key = storage_keys[index]; + + if (locked_fetch) { + fetched = zend_opcache_user_cache_fetch_locked(storage_key, &fetched_value, false, &found, true); + + if (!fetched) { + if (!found) { + ZVAL_COPY(&fetched_value, default_value); + } else { + if (EG(exception)) { + zend_clear_exception(); + } + + zend_opcache_user_cache_unlock(); + locked_fetch = false; + zend_opcache_user_cache_delete_corrupt_entry_prevalidated(storage_key); + ZVAL_COPY(&fetched_value, default_value); + } + } + } else { + zend_opcache_user_cache_fetch_api(cache->context, storage_key, default_value, &fetched_value); + } + + zend_hash_update(Z_ARRVAL_P(return_value), key, &fetched_value); + + zend_string_release(storage_key); + } + + if (locked_fetch) { + zend_opcache_user_cache_unlock(); + } + zend_opcache_user_cache_restore_context(previous_context); + if (storage_keys != NULL) { + efree(storage_keys); + } + zend_opcache_user_cache_release_key_list(prepared_keys, key_count); + + return SUCCESS; +} + +static void zend_opcache_user_cache_atomic_update_api( + zend_opcache_user_cache_context *context, + zend_string *key, + zend_long step, + zend_long ttl, + bool decrement, + zend_long *new_value, + bool *updated) +{ + zend_opcache_user_cache_context *previous_context; + bool is_overflow = false; + + previous_context = zend_opcache_user_cache_activate_context(context); + + if (!zend_opcache_user_cache_can_write()) { + *updated = false; + zend_opcache_user_cache_restore_context(previous_context); + + return; + } + + if (!zend_opcache_user_cache_wlock_for_entry_mutation(key)) { + *updated = false; + zend_opcache_user_cache_restore_context(previous_context); + + return; + } + + *updated = zend_opcache_user_cache_atomic_update_locked(key, step, ttl, decrement, true, new_value, &is_overflow); + + zend_opcache_user_cache_unlock(); + zend_opcache_user_cache_restore_context(previous_context); + + if (is_overflow) { + *updated = false; + } +} + +static void zend_opcache_user_cache_exists_api(zend_opcache_user_cache_context *context, zend_string *key, bool *exists) +{ + zend_opcache_user_cache_context *previous_context; + + previous_context = zend_opcache_user_cache_activate_context(context); + + if (!zend_opcache_user_cache_can_read()) { + *exists = false; + zend_opcache_user_cache_restore_context(previous_context); + + return; + } + + switch (zend_opcache_user_cache_exists_optimistic(key)) { + case ZEND_OPCACHE_USER_CACHE_OPTIMISTIC_FOUND: + *exists = true; + zend_opcache_user_cache_restore_context(previous_context); + + return; + case ZEND_OPCACHE_USER_CACHE_OPTIMISTIC_MISS: + *exists = false; + zend_opcache_user_cache_restore_context(previous_context); + + return; + case ZEND_OPCACHE_USER_CACHE_OPTIMISTIC_FALLBACK: + break; + } + + if (!zend_opcache_user_cache_begin_read()) { + *exists = false; + zend_opcache_user_cache_restore_context(previous_context); + + return; + } + + *exists = zend_opcache_user_cache_exists_locked(key); + + zend_opcache_user_cache_unlock(); + zend_opcache_user_cache_restore_context(previous_context); +} + +static void zend_opcache_user_cache_lock_api( + zend_opcache_user_cache_context *context, + zend_string *key, + zend_long lease, + bool *locked) +{ + zend_opcache_user_cache_context *previous_context; + + previous_context = zend_opcache_user_cache_activate_context(context); + + if (!zend_opcache_user_cache_can_write()) { + *locked = false; + zend_opcache_user_cache_restore_context(previous_context); + + return; + } + + *locked = zend_opcache_user_cache_try_acquire_entry_lock(key, lease); + + zend_opcache_user_cache_restore_context(previous_context); +} + +static void zend_opcache_user_cache_unlock_api(zend_opcache_user_cache_context *context, zend_string *key, bool *unlocked) +{ + zend_opcache_user_cache_context *previous_context; + + previous_context = zend_opcache_user_cache_activate_context(context); + + if (!zend_opcache_user_cache_can_write()) { + *unlocked = false; + zend_opcache_user_cache_restore_context(previous_context); + + return; + } + + *unlocked = zend_opcache_user_cache_release_entry_lock(key); + + zend_opcache_user_cache_restore_context(previous_context); +} + +static bool zend_opcache_user_cache_instance_store( + zend_opcache_user_cache_object *cache, + zend_string *key, + zval *value, + zend_long ttl) +{ + zend_opcache_user_cache_context *previous_context; + zend_string *storage_key; + bool stored; + + previous_context = zend_opcache_user_cache_activate_context(cache->context); + if (!zend_opcache_user_cache_can_write()) { + zend_opcache_user_cache_restore_context(previous_context); + + return false; + } + + storage_key = zend_opcache_user_cache_storage_key(cache, key); + stored = zend_opcache_user_cache_store_storage_key_prevalidated(storage_key, value, ttl); + + zend_string_release(storage_key); + + zend_opcache_user_cache_restore_context(previous_context); + + return stored; +} + +static zend_long zend_opcache_user_cache_size_to_zend_long(size_t size) +{ + return size > (size_t) ZEND_LONG_MAX ? ZEND_LONG_MAX : (zend_long) size; +} + +static void zend_opcache_user_cache_collect_info_stats(zend_opcache_user_cache_info_stats *stats) +{ + zend_opcache_user_cache_runtime *runtime; + zend_opcache_user_cache_storage *storage; + zend_opcache_user_cache_header *header; + zend_opcache_user_cache_block *block; + size_t free_memory = 0, wasted_memory = 0, tail_memory = 0; + uint32_t block_offset, block_limit, next_offset, iteration_limit, iterations; + + memset(stats, 0, sizeof(*stats)); + + runtime = zend_opcache_user_cache_active_runtime(); + storage = &zend_opcache_user_cache_active_context()->storage; + + stats->configured_memory = zend_opcache_user_cache_size_to_zend_long(runtime->configured_memory); + stats->shared_memory_size = zend_opcache_user_cache_size_to_zend_long(storage->size); + stats->backend_initialized = storage->initialized; + + if (!storage->initialized || !storage->lock_initialized || !zend_opcache_user_cache_rlock()) { + return; + } + + header = zend_opcache_user_cache_header_ptr(); + if (header == NULL || !zend_opcache_user_cache_header_is_initialized_locked()) { + stats->free_memory = stats->shared_memory_size; + zend_opcache_user_cache_unlock(); + + return; + } + + if (header->next_free <= header->data_size) { + tail_memory = header->data_size - header->next_free; + } + + block_limit = header->data_offset + header->next_free; + iteration_limit = header->data_size / (uint32_t) sizeof(zend_opcache_user_cache_block); + for (block_offset = header->free_list, iterations = 0; + block_offset != 0 && iterations < iteration_limit; + iterations++) { + if (block_offset < header->data_offset || block_offset > block_limit - sizeof(zend_opcache_user_cache_block)) { + break; + } + + block = zend_opcache_user_cache_block_ptr(block_offset); + next_offset = block_offset + block->size; + if (block->size < sizeof(zend_opcache_user_cache_block) || next_offset > block_limit) { + break; + } + + wasted_memory += block->size; + block_offset = block->next_free; + } + + free_memory = tail_memory + wasted_memory; + + stats->entry_count = (zend_long) header->count; + stats->entry_capacity = (zend_long) header->capacity; + stats->tombstone_count = (zend_long) header->tombstone_count; + stats->free_memory = zend_opcache_user_cache_size_to_zend_long(free_memory); + stats->wasted_memory = zend_opcache_user_cache_size_to_zend_long(wasted_memory); + stats->used_memory = storage->size > free_memory + ? zend_opcache_user_cache_size_to_zend_long(storage->size - free_memory) + : 0; + + zend_opcache_user_cache_unlock(); +} + +static void zend_opcache_user_cache_return_user_info( + zval *return_value, + zend_opcache_user_cache_object *cache) +{ + zend_opcache_user_cache_context *previous_context; + zend_opcache_user_cache_runtime *runtime; + zend_opcache_user_cache_info_stats stats; + + previous_context = zend_opcache_user_cache_activate_context(cache->context); + zend_opcache_user_cache_ensure_ready(); + runtime = zend_opcache_user_cache_active_runtime(); + zend_opcache_user_cache_collect_info_stats(&stats); + + object_init_ex(return_value, zend_opcache_user_cache_info_ce); + zend_update_property_str(zend_opcache_user_cache_info_ce, Z_OBJ_P(return_value), ZEND_STRL("scope"), cache->scope); + zend_update_property_bool(zend_opcache_user_cache_info_ce, Z_OBJ_P(return_value), ZEND_STRL("available"), runtime->available); + if (runtime->failure_reason != NULL) { + zend_update_property_string(zend_opcache_user_cache_info_ce, Z_OBJ_P(return_value), ZEND_STRL("unavailableReason"), (char *) runtime->failure_reason); + } else { + zend_update_property_null(zend_opcache_user_cache_info_ce, Z_OBJ_P(return_value), ZEND_STRL("unavailableReason")); + } + zend_update_property_bool(zend_opcache_user_cache_info_ce, Z_OBJ_P(return_value), ZEND_STRL("enabled"), runtime->enabled); + zend_update_property_bool(zend_opcache_user_cache_info_ce, Z_OBJ_P(return_value), ZEND_STRL("startupFailed"), runtime->startup_failed); + zend_update_property_bool(zend_opcache_user_cache_info_ce, Z_OBJ_P(return_value), ZEND_STRL("backendInitialized"), stats.backend_initialized); + zend_update_property_long(zend_opcache_user_cache_info_ce, Z_OBJ_P(return_value), ZEND_STRL("configuredMemory"), stats.configured_memory); + zend_update_property_long(zend_opcache_user_cache_info_ce, Z_OBJ_P(return_value), ZEND_STRL("sharedMemorySize"), stats.shared_memory_size); + zend_update_property_long(zend_opcache_user_cache_info_ce, Z_OBJ_P(return_value), ZEND_STRL("usedMemory"), stats.used_memory); + zend_update_property_long(zend_opcache_user_cache_info_ce, Z_OBJ_P(return_value), ZEND_STRL("freeMemory"), stats.free_memory); + zend_update_property_long(zend_opcache_user_cache_info_ce, Z_OBJ_P(return_value), ZEND_STRL("wastedMemory"), stats.wasted_memory); + zend_update_property_long(zend_opcache_user_cache_info_ce, Z_OBJ_P(return_value), ZEND_STRL("entryCount"), stats.entry_count); + zend_update_property_long(zend_opcache_user_cache_info_ce, Z_OBJ_P(return_value), ZEND_STRL("entryCapacity"), stats.entry_capacity); + zend_update_property_long(zend_opcache_user_cache_info_ce, Z_OBJ_P(return_value), ZEND_STRL("tombstoneCount"), stats.tombstone_count); + + zend_opcache_user_cache_restore_context(previous_context); +} + +static void zend_opcache_user_cache_object_free(zend_object *object) +{ + zend_opcache_user_cache_object *cache = zend_opcache_user_cache_object_from_obj(object); + + if (cache->scope != NULL) { + zend_string_release(cache->scope); + } + + if (cache->scope_prefix != NULL) { + zend_string_release(cache->scope_prefix); + } + + if (cache->storage_key_cache != NULL) { + zend_hash_destroy(cache->storage_key_cache); + FREE_HASHTABLE(cache->storage_key_cache); + } + + zend_object_std_dtor(&cache->std); +} + +static zend_object *zend_opcache_user_cache_object_create(zend_class_entry *ce) +{ + zend_opcache_user_cache_object *cache; + + cache = zend_object_alloc(sizeof(zend_opcache_user_cache_object), ce); + + zend_object_std_init(&cache->std, ce); + object_properties_init(&cache->std, ce); + + cache->scope = NULL; + cache->scope_prefix = NULL; + cache->storage_key_cache = NULL; + cache->context = zend_opcache_user_cache_default_context(); + cache->std.handlers = &zend_opcache_user_cache_object_handlers; + + return &cache->std; +} + +static zend_string *zend_opcache_user_cache_storage_key( + zend_opcache_user_cache_object *cache, + zend_string *key) +{ + zend_string *storage_key; + zval *cached, cache_zv; + + if (cache->storage_key_cache != NULL) { + cached = zend_hash_find(cache->storage_key_cache, key); + if (cached != NULL) { + return zend_string_copy(Z_STR_P(cached)); + } + } + + storage_key = zend_opcache_user_cache_build_storage_key(cache, key); + zend_string_hash_val(storage_key); + + if (cache->storage_key_cache == NULL) { + ALLOC_HASHTABLE(cache->storage_key_cache); + zend_hash_init(cache->storage_key_cache, 8, NULL, ZVAL_PTR_DTOR, 0); + } + + if (zend_hash_num_elements(cache->storage_key_cache) < ZEND_OPCACHE_USER_CACHE_STORAGE_KEY_CACHE_MAX) { + ZVAL_STR_COPY(&cache_zv, storage_key); + zend_hash_add(cache->storage_key_cache, key, &cache_zv); + } + + return storage_key; +} + +static void zend_opcache_user_cache_safe_direct_handlers_dtor(zval *zv) +{ + pefree(Z_PTR_P(zv), true); +} + +static const zend_opcache_user_cache_safe_direct_handlers *zend_opcache_user_cache_safe_direct_find_handlers( + zend_class_entry *ce, + zend_class_entry **base_ce_ptr) +{ + const zend_opcache_user_cache_safe_direct_handlers *handlers; + + if (!zend_opcache_user_cache_safe_direct_handlers_initialized) { + return NULL; + } + + while (ce != NULL) { + handlers = zend_hash_index_find_ptr( + &zend_opcache_user_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_user_cache_store_storage_key_prevalidated(zend_string *key, zval *value, zend_long ttl) +{ + zend_opcache_user_cache_prepared_value prepared; + uint64_t generation = 0; + bool stored, seed_request_local_slot = false; + + if (!zend_opcache_user_cache_prepare_value(&prepared, key, value, false, false, false)) { + zend_opcache_user_cache_destroy_prepared_value(&prepared); + + return false; + } + + /* Prepare values before entering the write section. */ + if (!zend_opcache_user_cache_wlock_for_entry_mutation(key)) { + zend_opcache_user_cache_destroy_prepared_value(&prepared); + + return false; + } + + zend_try { + stored = zend_opcache_user_cache_store_prepared_locked( + key, + value, + &prepared, + ttl, + false, + false, + &generation, + &seed_request_local_slot, + NULL, + true + ); + } zend_catch { + zend_opcache_user_cache_unlock_if_held(); + zend_opcache_user_cache_destroy_prepared_value(&prepared); + zend_bailout(); + } zend_end_try(); + + zend_opcache_user_cache_unlock(); + zend_opcache_user_cache_destroy_prepared_value(&prepared); + + if (stored && seed_request_local_slot) { + zend_opcache_user_cache_store_request_local_value_slot(key, generation, value, true); + } + + return stored; +} + +static int zend_opcache_user_cache_bulk_order_compare(const void *lhs_ptr, const void *rhs_ptr) +{ + const zend_opcache_user_cache_bulk_order *lhs, *rhs; + size_t lhs_len, rhs_len, compare_len; + int result; + + lhs = lhs_ptr; + rhs = rhs_ptr; + + if (lhs->hash != rhs->hash) { + return lhs->hash < rhs->hash ? -1 : 1; + } + + lhs_len = ZSTR_LEN(lhs->key); + rhs_len = ZSTR_LEN(rhs->key); + compare_len = lhs_len < rhs_len ? lhs_len : rhs_len; + + result = memcmp(ZSTR_VAL(lhs->key), ZSTR_VAL(rhs->key), compare_len); + if (result != 0) { + return result < 0 ? -1 : 1; + } + + if (lhs_len != rhs_len) { + return lhs_len < rhs_len ? -1 : 1; + } + + return lhs->index < rhs->index ? -1 : (lhs->index > rhs->index ? 1 : 0); +} + +/* Prepare first, reserve keys in order, then publish under one write lock. */ +static bool zend_opcache_user_cache_instance_store_multiple( + zend_opcache_user_cache_object *cache, + HashTable *values, + zend_long ttl) +{ + zend_opcache_user_cache_context *previous_context; + zend_opcache_user_cache_prepared_value *prepared; + zend_opcache_user_cache_replaced_entry *replaced; + zend_opcache_user_cache_bulk_order *order; + zend_string *key, **storage_keys; + zval *value, **value_ptrs; + uint64_t *generations; + uint32_t count, index, rollback_index, prepared_count, stored_count; + bool *reserved, *seed_slots, ok; + + previous_context = zend_opcache_user_cache_activate_context(cache->context); + if (!zend_opcache_user_cache_can_write()) { + zend_opcache_user_cache_restore_context(previous_context); + + return false; + } + + count = zend_hash_num_elements(values); + if (count == 0) { + zend_opcache_user_cache_restore_context(previous_context); + + return true; + } + + index = 0; + prepared_count = 0; + stored_count = 0; + ok = true; + + storage_keys = safe_emalloc(count, sizeof(zend_string *), 0); + value_ptrs = safe_emalloc(count, sizeof(zval *), 0); + prepared = safe_emalloc(count, sizeof(zend_opcache_user_cache_prepared_value), 0); + order = safe_emalloc(count, sizeof(zend_opcache_user_cache_bulk_order), 0); + replaced = ecalloc(count, sizeof(zend_opcache_user_cache_replaced_entry)); + generations = safe_emalloc(count, sizeof(uint64_t), 0); + reserved = ecalloc(count, sizeof(bool)); + seed_slots = ecalloc(count, sizeof(bool)); + + ZEND_HASH_FOREACH_STR_KEY_VAL(values, key, value) { + ZEND_ASSERT(key != NULL); + + storage_keys[index] = zend_opcache_user_cache_storage_key(cache, key); + value_ptrs[index] = value; + + if (!zend_opcache_user_cache_prepare_value(&prepared[index], storage_keys[index], value, false, false, false)) { + zend_opcache_user_cache_destroy_prepared_value(&prepared[index]); + zend_string_release(storage_keys[index]); + ok = false; + + break; + } + + order[index].hash = zend_string_hash_val(storage_keys[index]); + order[index].key = storage_keys[index]; + order[index].index = index; + prepared_count = ++index; + } ZEND_HASH_FOREACH_END(); + + if (ok) { + qsort(order, prepared_count, sizeof(*order), zend_opcache_user_cache_bulk_order_compare); + + for (index = 0; index < prepared_count; index++) { + if (!zend_opcache_user_cache_begin_entry_mutation( + storage_keys[order[index].index], + &reserved[order[index].index] + ) + ) { + ok = false; + + break; + } + } + } + + if (ok && zend_opcache_user_cache_acquire_write_lock()) { + zend_try { + for (index = 0; index < prepared_count; index++) { + if (!zend_opcache_user_cache_store_prepared_locked( + storage_keys[index], + value_ptrs[index], + &prepared[index], + ttl, + false, + false, + &generations[index], + &seed_slots[index], + &replaced[index], + false) + ) { + ok = false; + + break; + } + + stored_count = index + 1; + } + + if (ok) { + for (index = 0; index < stored_count; index++) { + zend_opcache_user_cache_discard_replaced_entry_locked(storage_keys[index], &replaced[index]); + } + } else { + for (rollback_index = stored_count; rollback_index > 0; rollback_index--) { + zend_opcache_user_cache_rollback_replaced_entry_locked( + storage_keys[rollback_index - 1], + &replaced[rollback_index - 1] + ); + } + + stored_count = 0; + } + } zend_catch { + zend_opcache_user_cache_unlock_if_held(); + zend_bailout(); + } zend_end_try(); + + zend_opcache_user_cache_unlock(); + } else { + ok = false; + } + + for (index = 0; index < prepared_count; index++) { + zend_opcache_user_cache_finish_entry_mutation(storage_keys[index], reserved[index]); + } + + for (index = 0; index < stored_count; index++) { + if (seed_slots[index]) { + zend_opcache_user_cache_store_request_local_value_slot( + storage_keys[index], + generations[index], + value_ptrs[index], + true + ); + } + } + + for (index = 0; index < prepared_count; index++) { + zend_opcache_user_cache_destroy_prepared_value(&prepared[index]); + zend_string_release(storage_keys[index]); + } + + efree(seed_slots); + efree(reserved); + efree(generations); + efree(replaced); + efree(order); + efree(prepared); + efree(value_ptrs); + efree(storage_keys); + + zend_opcache_user_cache_restore_context(previous_context); + + return ok; +} + +static bool zend_opcache_user_cache_init_partition_contexts(zend_opcache_user_cache_partition *partition, const char *name) +{ + partition->context = zend_opcache_user_cache_context_state; + partition->context.storage.lock_file = -1; + if (name != NULL) { + partition->name = strdup(name); + if (partition->name == NULL) { + return false; + } + + partition->context.name = partition->name; + partition->context.lock_name = partition->name; + } + + return true; +} + +static bool zend_opcache_user_cache_startup_storage_for_context(zend_opcache_user_cache_context *context) +{ + zend_opcache_user_cache_context *previous_context; + + previous_context = zend_opcache_user_cache_activate_context(context); + + zend_opcache_user_cache_reset_runtime(); + if (zend_opcache_user_cache_active_runtime()->enabled && ZCG(enabled) && accel_startup_ok && !file_cache_only) { + if (!zend_opcache_user_cache_startup_storage_before_request()) { + zend_opcache_user_cache_restore_context(previous_context); + + return false; + } + } + + zend_opcache_user_cache_restore_context(previous_context); + + return true; +} + +static void zend_opcache_user_cache_partition_shutdown_all(void) +{ + zend_opcache_user_cache_partition *partition, *next; + zend_opcache_user_cache_context *previous_context; + + previous_context = zend_opcache_user_cache_active_context_ptr; + partition = zend_opcache_user_cache_partitions; + while (partition != NULL) { + next = partition->next; + + zend_opcache_user_cache_activate_context(&partition->context); + zend_opcache_user_cache_shutdown_storage(); + zend_opcache_user_cache_reset_runtime(); + + free(partition->name); + free(partition); + + partition = next; + } + + zend_opcache_user_cache_partitions = NULL; + + zend_opcache_user_cache_restore_context(previous_context); +} + +void zend_opcache_user_cache_safe_direct_handlers_init(void) +{ + if (zend_opcache_user_cache_safe_direct_handlers_initialized) { + return; + } + + zend_hash_init( + &zend_opcache_user_cache_safe_direct_handler_table, + 8, + NULL, + zend_opcache_user_cache_safe_direct_handlers_dtor, + true + ); + + zend_opcache_user_cache_safe_direct_handlers_initialized = true; +} + +void zend_opcache_user_cache_safe_direct_handlers_destroy(void) +{ + if (!zend_opcache_user_cache_safe_direct_handlers_initialized) { + return; + } + + zend_hash_destroy(&zend_opcache_user_cache_safe_direct_handler_table); + + zend_opcache_user_cache_safe_direct_handlers_initialized = false; +} + +void zend_opcache_user_cache_safe_direct_register_class( + zend_class_entry *ce, + const zend_opcache_user_cache_safe_direct_handlers *handlers) +{ + zend_opcache_user_cache_safe_direct_handlers handlers_copy; + + if (ce == NULL || + handlers == NULL || + handlers->copy == NULL || + handlers->state_serialize == NULL || + handlers->state_unserialize == NULL + ) { + return; + } + + zend_opcache_user_cache_safe_direct_handlers_init(); + + handlers_copy = *handlers; + + zend_hash_index_update_mem( + &zend_opcache_user_cache_safe_direct_handler_table, + (zend_ulong) (uintptr_t) ce, + &handlers_copy, + sizeof(handlers_copy) + ); +} + +zend_opcache_user_cache_safe_direct_state_copy_func_t zend_opcache_user_cache_safe_direct_copy_func( + zend_class_entry *ce, + zend_class_entry **base_ce_ptr) +{ + const zend_opcache_user_cache_safe_direct_handlers *handlers = + zend_opcache_user_cache_safe_direct_find_handlers(ce, base_ce_ptr) + ; + + return handlers != NULL ? handlers->copy : NULL; +} + +zend_class_entry *zend_opcache_user_cache_safe_direct_find_base(zend_class_entry *ce) +{ + zend_class_entry *base_ce = NULL; + + if (zend_opcache_user_cache_safe_direct_copy_func(ce, &base_ce) == NULL) { + return NULL; + } + + return base_ce; +} + +zend_opcache_user_cache_safe_direct_state_has_unstorable_func_t zend_opcache_user_cache_safe_direct_state_has_unstorable_func( + zend_class_entry *ce) +{ + const zend_opcache_user_cache_safe_direct_handlers *handlers = + zend_opcache_user_cache_safe_direct_find_handlers(ce, NULL) + ; + + return handlers != NULL ? handlers->state_has_unstorable : NULL; +} + +zend_opcache_user_cache_safe_direct_state_serialize_func_t zend_opcache_user_cache_safe_direct_state_serialize_func( + zend_class_entry *ce) +{ + const zend_opcache_user_cache_safe_direct_handlers *handlers = + zend_opcache_user_cache_safe_direct_find_handlers(ce, NULL) + ; + + return handlers != NULL ? handlers->state_serialize : NULL; +} + +zend_opcache_user_cache_safe_direct_state_unserialize_func_t zend_opcache_user_cache_safe_direct_state_unserialize_func( + zend_class_entry *ce) +{ + const zend_opcache_user_cache_safe_direct_handlers *handlers = + zend_opcache_user_cache_safe_direct_find_handlers(ce, NULL) + ; + + return handlers != NULL ? handlers->state_unserialize : NULL; +} + +bool zend_opcache_user_cache_safe_direct_prefers_request_local_prototype(zend_class_entry *ce) +{ + const zend_opcache_user_cache_safe_direct_handlers *handlers = + zend_opcache_user_cache_safe_direct_find_handlers(ce, NULL) + ; + + return handlers != NULL && handlers->prefer_request_local_prototype; +} + +ZEND_API zend_opcache_user_cache_partition *zend_opcache_user_cache_partition_create(const char *name) +{ + zend_opcache_user_cache_partition *partition; + + partition = calloc(1, sizeof(zend_opcache_user_cache_partition)); + if (partition == NULL) { + return NULL; + } + + if (!zend_opcache_user_cache_init_partition_contexts(partition, name)) { + free(partition); + + return NULL; + } + + partition->next = zend_opcache_user_cache_partitions; + zend_opcache_user_cache_partitions = partition; + + return partition; +} + +ZEND_API bool zend_opcache_user_cache_partition_startup_storage(zend_opcache_user_cache_partition *partition) +{ + zend_opcache_user_cache_partition *previous_partition; + bool ok; + + if (partition == NULL) { + return true; + } + + previous_partition = zend_opcache_user_cache_active_partition; + zend_opcache_user_cache_active_partition = partition; + ok = zend_opcache_user_cache_startup_storage_for_context(&partition->context); + zend_opcache_user_cache_active_partition = previous_partition; + + return ok; +} + +ZEND_API bool zend_opcache_user_cache_startup_default_context_storage(void) +{ + return zend_opcache_user_cache_startup_storage_for_context(&zend_opcache_user_cache_context_state); +} + +ZEND_API void zend_opcache_user_cache_partition_activate(zend_opcache_user_cache_partition *partition) +{ + zend_opcache_user_cache_active_partition = partition; + zend_opcache_user_cache_active_context_ptr = NULL; + zend_opcache_user_cache_request_unavailable_reason = NULL; +} + +ZEND_API void zend_opcache_user_cache_activate_request_unavailable(const char *failure_reason) +{ + zend_opcache_user_cache_active_partition = NULL; + zend_opcache_user_cache_active_context_ptr = NULL; + zend_opcache_user_cache_request_unavailable_reason = failure_reason; +} + +ZEND_API void zend_opcache_user_cache_opt_in(void) +{ + zend_opcache_user_cache_runtime_opted_in = true; +} + +void zend_opcache_user_cache_minit(void) +{ + zend_opcache_user_cache_context *previous_context; + + zend_opcache_user_cache_runtime_opted_in = false; + + zend_opcache_user_cache_register_classes(); + zend_opcache_user_cache_safe_direct_handlers_init(); + zend_opcache_user_cache_optimistic_fork_setup(); + + previous_context = zend_opcache_user_cache_activate_context(zend_opcache_user_cache_default_context()); + + zend_opcache_user_cache_reset_storage(); + + zend_opcache_user_cache_restore_context(previous_context); +} + +void zend_opcache_user_cache_mshutdown(void) +{ + zend_opcache_user_cache_context *previous_context; + + zend_opcache_user_cache_partition_shutdown_all(); + + zend_opcache_user_cache_active_partition = NULL; + previous_context = zend_opcache_user_cache_activate_context(zend_opcache_user_cache_default_context()); + + zend_opcache_user_cache_shutdown_storage(); + zend_opcache_user_cache_reset_runtime(); + zend_opcache_user_cache_restore_context(previous_context); + + zend_opcache_user_cache_active_partition = NULL; + zend_opcache_user_cache_runtime_opted_in = false; + + zend_opcache_user_cache_safe_direct_handlers_destroy(); + zend_opcache_user_cache_reset_class_entries(); +} + +void zend_opcache_user_cache_invalidate_all(void) +{ + zend_opcache_user_cache_partition *partition; + zend_opcache_user_cache_partition *previous_partition; + zend_opcache_user_cache_context *previous_context; + + previous_partition = zend_opcache_user_cache_active_partition; + previous_context = zend_opcache_user_cache_active_context_ptr; + + zend_opcache_user_cache_invalidate_all_context(&zend_opcache_user_cache_context_state); + for (partition = zend_opcache_user_cache_partitions; partition != NULL; partition = partition->next) { + zend_opcache_user_cache_invalidate_all_context(&partition->context); + } + + zend_opcache_user_cache_active_partition = previous_partition; + zend_opcache_user_cache_active_context_ptr = previous_context; + + zend_opcache_user_cache_clear_lookup_cache(); + zend_opcache_user_cache_release_active_request_local_slots(); +} + +zend_result zend_opcache_user_cache_rshutdown(void) +{ + zend_opcache_user_cache_request_unavailable_reason = NULL; + + zend_opcache_user_cache_clear_lookup_cache(); + + zend_opcache_user_cache_release_request_entry_locks(); + zend_opcache_user_cache_release_request_local_slots(); + zend_opcache_user_cache_decode_resolve_cache_release(); + + return SUCCESS; +} + +zend_result zend_opcache_user_cache_post_deactivate(void) +{ + zend_opcache_user_cache_release_request_shared_graph_refs(); + + return SUCCESS; +} + +ZEND_METHOD(Opcache_UserCacheInfo, __construct) +{ +} + +ZEND_METHOD(Opcache_UserCache, __construct) +{ + zend_opcache_user_cache_object *cache; + zend_string *scope; + + ZEND_PARSE_PARAMETERS_START(1, 1) + Z_PARAM_STR(scope) + ZEND_PARSE_PARAMETERS_END(); + + if (memchr(ZSTR_VAL(scope), ZEND_OPCACHE_USER_CACHE_KEY_DELIMITER_CHAR, ZSTR_LEN(scope)) != NULL) { + zend_argument_value_error(1, "must not contain the user-cache key delimiter " ZEND_OPCACHE_USER_CACHE_KEY_DELIMITER_NAME); + + RETURN_THROWS(); + } + + cache = zend_opcache_user_cache_object_from_obj(Z_OBJ_P(ZEND_THIS)); + cache->scope = zend_string_copy(scope); + cache->scope_prefix = zend_opcache_user_cache_build_scope_prefix(scope); + cache->context = zend_opcache_user_cache_default_context(); +} + +ZEND_METHOD(Opcache_UserCache, store) +{ + zend_opcache_user_cache_object *cache; + zend_long ttl = 0; + zend_string *key; + 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(); + + cache = zend_opcache_user_cache_from_this(ZEND_THIS); + if (cache == NULL) { + RETURN_THROWS(); + } + + if (!zend_opcache_user_cache_validate_key(key, 1)) { + RETURN_THROWS(); + } + + if (!zend_opcache_user_cache_validate_api_value(value, 2)) { + RETURN_THROWS(); + } + + if (!zend_opcache_user_cache_parse_ttl(ttl, 3)) { + RETURN_THROWS(); + } + + if (!zend_opcache_user_cache_instance_store(cache, key, value, ttl)) { + if (EG(exception)) { + RETURN_THROWS(); + } + + RETURN_FALSE; + } + + RETURN_TRUE; +} + +ZEND_METHOD(Opcache_UserCache, storeMultiple) +{ + zend_opcache_user_cache_object *cache; + zend_long ttl = 0; + HashTable *values; + + ZEND_PARSE_PARAMETERS_START(1, 2) + Z_PARAM_ARRAY_HT(values) + Z_PARAM_OPTIONAL + Z_PARAM_LONG(ttl) + ZEND_PARSE_PARAMETERS_END(); + + cache = zend_opcache_user_cache_from_this(ZEND_THIS); + if (cache == NULL) { + RETURN_THROWS(); + } + + if (!zend_opcache_user_cache_validate_store_array(values, 1)) { + RETURN_THROWS(); + } + + if (!zend_opcache_user_cache_parse_ttl(ttl, 2)) { + RETURN_THROWS(); + } + + if (!zend_opcache_user_cache_instance_store_multiple(cache, values, ttl)) { + if (EG(exception)) { + RETURN_THROWS(); + } + + RETURN_FALSE; + } + + RETURN_TRUE; +} + +ZEND_METHOD(Opcache_UserCache, increment) +{ + zend_opcache_user_cache_object *cache; + zend_string *key, *storage_key; + zend_long step = 1, ttl = 0, new_value = 0; + bool updated; + + ZEND_PARSE_PARAMETERS_START(1, 3) + Z_PARAM_STR(key) + Z_PARAM_OPTIONAL + Z_PARAM_LONG(step) + Z_PARAM_LONG(ttl) + ZEND_PARSE_PARAMETERS_END(); + + cache = zend_opcache_user_cache_from_this(ZEND_THIS); + if (cache == NULL) { + RETURN_THROWS(); + } + + if (!zend_opcache_user_cache_validate_key(key, 1)) { + RETURN_THROWS(); + } + + if (!zend_opcache_user_cache_parse_step(step, 2)) { + RETURN_THROWS(); + } + + if (!zend_opcache_user_cache_parse_ttl(ttl, 3)) { + RETURN_THROWS(); + } + + storage_key = zend_opcache_user_cache_storage_key(cache, key); + zend_opcache_user_cache_atomic_update_api(cache->context, storage_key, step, ttl, false, &new_value, &updated); + zend_string_release(storage_key); + + if (!updated) { + RETURN_FALSE; + } + + RETURN_LONG(new_value); +} + +ZEND_METHOD(Opcache_UserCache, decrement) +{ + zend_opcache_user_cache_object *cache; + zend_string *key, *storage_key; + zend_long step = 1, ttl = 0, new_value = 0; + bool updated; + + ZEND_PARSE_PARAMETERS_START(1, 3) + Z_PARAM_STR(key) + Z_PARAM_OPTIONAL + Z_PARAM_LONG(step) + Z_PARAM_LONG(ttl) + ZEND_PARSE_PARAMETERS_END(); + + cache = zend_opcache_user_cache_from_this(ZEND_THIS); + if (cache == NULL) { + RETURN_THROWS(); + } + + if (!zend_opcache_user_cache_validate_key(key, 1)) { + RETURN_THROWS(); + } + + if (!zend_opcache_user_cache_parse_step(step, 2)) { + RETURN_THROWS(); + } + + if (!zend_opcache_user_cache_parse_ttl(ttl, 3)) { + RETURN_THROWS(); + } + + storage_key = zend_opcache_user_cache_storage_key(cache, key); + zend_opcache_user_cache_atomic_update_api(cache->context, storage_key, step, ttl, true, &new_value, &updated); + zend_string_release(storage_key); + + if (!updated) { + RETURN_FALSE; + } + + RETURN_LONG(new_value); +} + +ZEND_METHOD(Opcache_UserCache, fetch) +{ + zend_opcache_user_cache_object *cache; + zend_string *key, *storage_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(); + + cache = zend_opcache_user_cache_from_this(ZEND_THIS); + if (cache == NULL) { + RETURN_THROWS(); + } + + if (!zend_opcache_user_cache_validate_key(key, 1)) { + RETURN_THROWS(); + } + + if (default_value == NULL) { + ZVAL_NULL(&default_null); + default_value = &default_null; + } + + storage_key = zend_opcache_user_cache_storage_key(cache, key); + zend_opcache_user_cache_fetch_api(cache->context, storage_key, default_value, return_value); + zend_string_release(storage_key); +} + +ZEND_METHOD(Opcache_UserCache, fetchMultiple) +{ + zend_opcache_user_cache_object *cache; + zval *default_value = NULL, default_null; + HashTable *keys; + + ZEND_PARSE_PARAMETERS_START(1, 2) + Z_PARAM_ARRAY_HT(keys) + Z_PARAM_OPTIONAL + Z_PARAM_ZVAL(default_value) + ZEND_PARSE_PARAMETERS_END(); + + cache = zend_opcache_user_cache_from_this(ZEND_THIS); + if (cache == NULL) { + RETURN_THROWS(); + } + + if (default_value == NULL) { + ZVAL_NULL(&default_null); + default_value = &default_null; + } + + if (zend_opcache_user_cache_fetch_multiple_api(cache, keys, default_value, return_value) == FAILURE) { + if (EG(exception)) { + RETURN_THROWS(); + } + + RETURN_EMPTY_ARRAY(); + } +} + +ZEND_METHOD(Opcache_UserCache, has) +{ + zend_opcache_user_cache_object *cache; + zend_string *key, *storage_key; + bool exists; + + ZEND_PARSE_PARAMETERS_START(1, 1) + Z_PARAM_STR(key) + ZEND_PARSE_PARAMETERS_END(); + + cache = zend_opcache_user_cache_from_this(ZEND_THIS); + if (cache == NULL) { + RETURN_THROWS(); + } + + if (!zend_opcache_user_cache_validate_key(key, 1)) { + RETURN_THROWS(); + } + + storage_key = zend_opcache_user_cache_storage_key(cache, key); + zend_opcache_user_cache_exists_api(cache->context, storage_key, &exists); + zend_string_release(storage_key); + + RETURN_BOOL(exists); +} + +ZEND_METHOD(Opcache_UserCache, delete) +{ + zend_opcache_user_cache_context *previous_context; + zend_opcache_user_cache_object *cache; + zend_string *key, *storage_key; + + ZEND_PARSE_PARAMETERS_START(1, 1) + Z_PARAM_STR(key) + ZEND_PARSE_PARAMETERS_END(); + + cache = zend_opcache_user_cache_from_this(ZEND_THIS); + if (cache == NULL) { + RETURN_THROWS(); + } + + if (!zend_opcache_user_cache_validate_key(key, 1)) { + RETURN_THROWS(); + } + + previous_context = zend_opcache_user_cache_activate_context(cache->context); + if (!zend_opcache_user_cache_can_write()) { + zend_opcache_user_cache_restore_context(previous_context); + + if (EG(exception)) { + RETURN_THROWS(); + } + + RETURN_TRUE; + } + + storage_key = zend_opcache_user_cache_storage_key(cache, key); + if (!zend_opcache_user_cache_delete_storage_key_prevalidated(storage_key)) { + zend_string_release(storage_key); + zend_opcache_user_cache_restore_context(previous_context); + + if (EG(exception)) { + RETURN_THROWS(); + } + + RETURN_FALSE; + } + + zend_string_release(storage_key); + + zend_opcache_user_cache_restore_context(previous_context); + + RETURN_TRUE; +} + +ZEND_METHOD(Opcache_UserCache, deleteMultiple) +{ + zend_opcache_user_cache_context *previous_context; + zend_opcache_user_cache_object *cache; + zend_opcache_user_cache_bulk_order *order; + zend_string **prepared_keys, **storage_keys; + HashTable *keys; + uint32_t key_count, index; + bool *reserved, ok = true; + + ZEND_PARSE_PARAMETERS_START(1, 1) + Z_PARAM_ARRAY_HT(keys) + ZEND_PARSE_PARAMETERS_END(); + + cache = zend_opcache_user_cache_from_this(ZEND_THIS); + if (cache == NULL) { + RETURN_THROWS(); + } + + if (!zend_opcache_user_cache_prepare_key_list(keys, &prepared_keys, &key_count, 1)) { + RETURN_THROWS(); + } + + previous_context = zend_opcache_user_cache_activate_context(cache->context); + if (!zend_opcache_user_cache_can_write()) { + zend_opcache_user_cache_restore_context(previous_context); + zend_opcache_user_cache_release_key_list(prepared_keys, key_count); + + if (EG(exception)) { + RETURN_THROWS(); + } + + RETURN_TRUE; + } + + if (key_count == 0) { + zend_opcache_user_cache_restore_context(previous_context); + zend_opcache_user_cache_release_key_list(prepared_keys, key_count); + + RETURN_TRUE; + } + + storage_keys = safe_emalloc(key_count, sizeof(zend_string *), 0); + order = safe_emalloc(key_count, sizeof(zend_opcache_user_cache_bulk_order), 0); + reserved = ecalloc(key_count, sizeof(bool)); + + for (index = 0; index < key_count; index++) { + storage_keys[index] = zend_opcache_user_cache_storage_key(cache, prepared_keys[index]); + order[index].hash = zend_string_hash_val(storage_keys[index]); + order[index].key = storage_keys[index]; + order[index].index = index; + } + + qsort(order, key_count, sizeof(*order), zend_opcache_user_cache_bulk_order_compare); + + for (index = 0; index < key_count; index++) { + if (!zend_opcache_user_cache_begin_entry_mutation( + storage_keys[order[index].index], + &reserved[order[index].index]) + ) { + ok = false; + + break; + } + } + + if (ok && zend_opcache_user_cache_acquire_write_lock()) { + zend_try { + for (index = 0; index < key_count; index++) { + if (!zend_opcache_user_cache_delete_locked(storage_keys[index])) { + ok = false; + + break; + } + } + } zend_catch { + zend_opcache_user_cache_unlock_if_held(); + zend_bailout(); + } zend_end_try(); + + zend_opcache_user_cache_unlock(); + } else { + ok = false; + } + + for (index = 0; index < key_count; index++) { + zend_opcache_user_cache_finish_entry_mutation(storage_keys[index], reserved[index]); + zend_string_release(storage_keys[index]); + } + + efree(reserved); + efree(order); + efree(storage_keys); + + zend_opcache_user_cache_restore_context(previous_context); + zend_opcache_user_cache_release_key_list(prepared_keys, key_count); + + if (!ok) { + if (EG(exception)) { + RETURN_THROWS(); + } + + RETURN_FALSE; + } + + RETURN_TRUE; +} + +ZEND_METHOD(Opcache_UserCache, clear) +{ + zend_opcache_user_cache_context *previous_context; + zend_opcache_user_cache_object *cache; + + ZEND_PARSE_PARAMETERS_NONE(); + + cache = zend_opcache_user_cache_from_this(ZEND_THIS); + if (cache == NULL) { + RETURN_THROWS(); + } + + previous_context = zend_opcache_user_cache_activate_context(cache->context); + if (!zend_opcache_user_cache_can_write()) { + zend_opcache_user_cache_restore_context(previous_context); + + if (EG(exception)) { + RETURN_THROWS(); + } + + RETURN_TRUE; + } + + if (!zend_opcache_user_cache_clear_scope_prevalidated(cache->scope_prefix)) { + zend_opcache_user_cache_restore_context(previous_context); + + if (EG(exception)) { + RETURN_THROWS(); + } + + RETURN_FALSE; + } + + zend_opcache_user_cache_restore_context(previous_context); + + RETURN_TRUE; +} + +ZEND_METHOD(Opcache_UserCache, lock) +{ + zend_opcache_user_cache_object *cache; + zend_long lease = 0; + zend_string *key, *storage_key; + bool locked; + + ZEND_PARSE_PARAMETERS_START(1, 2) + Z_PARAM_STR(key) + Z_PARAM_OPTIONAL + Z_PARAM_LONG(lease) + ZEND_PARSE_PARAMETERS_END(); + + cache = zend_opcache_user_cache_from_this(ZEND_THIS); + if (cache == NULL) { + RETURN_THROWS(); + } + + if (!zend_opcache_user_cache_validate_key(key, 1)) { + RETURN_THROWS(); + } + + if (!zend_opcache_user_cache_parse_lease(lease, 2)) { + RETURN_THROWS(); + } + + storage_key = zend_opcache_user_cache_storage_key(cache, key); + zend_opcache_user_cache_lock_api(cache->context, storage_key, lease, &locked); + zend_string_release(storage_key); + + RETURN_BOOL(locked); +} + +ZEND_METHOD(Opcache_UserCache, unlock) +{ + zend_opcache_user_cache_object *cache; + zend_string *key, *storage_key; + bool unlocked; + + ZEND_PARSE_PARAMETERS_START(1, 1) + Z_PARAM_STR(key) + ZEND_PARSE_PARAMETERS_END(); + + cache = zend_opcache_user_cache_from_this(ZEND_THIS); + if (cache == NULL) { + RETURN_THROWS(); + } + + if (!zend_opcache_user_cache_validate_key(key, 1)) { + RETURN_THROWS(); + } + + storage_key = zend_opcache_user_cache_storage_key(cache, key); + zend_opcache_user_cache_unlock_api(cache->context, storage_key, &unlocked); + zend_string_release(storage_key); + + RETURN_BOOL(unlocked); +} + +ZEND_METHOD(Opcache_UserCache, remember) +{ + zend_fcall_info fci; + zend_fcall_info_cache fcc; + zend_opcache_user_cache_object *cache; + zend_long ttl = 0; + zend_string *key, *storage_key; + zval default_null = {0}, callback_result; + bool exists = false, locked = false; + + ZEND_PARSE_PARAMETERS_START(2, 3) + Z_PARAM_STR(key) + Z_PARAM_FUNC(fci, fcc) + Z_PARAM_OPTIONAL + Z_PARAM_LONG(ttl) + ZEND_PARSE_PARAMETERS_END(); + + cache = zend_opcache_user_cache_from_this(ZEND_THIS); + if (cache == NULL) { + RETURN_THROWS(); + } + + if (!zend_opcache_user_cache_validate_key(key, 1)) { + RETURN_THROWS(); + } + + if (!zend_opcache_user_cache_parse_ttl(ttl, 3)) { + RETURN_THROWS(); + } + + ZVAL_NULL(&default_null); + storage_key = zend_opcache_user_cache_storage_key(cache, key); + zend_opcache_user_cache_exists_api(cache->context, storage_key, &exists); + if (exists) { + zend_opcache_user_cache_fetch_api(cache->context, storage_key, &default_null, return_value); + zend_string_release(storage_key); + + return; + } + + zend_opcache_user_cache_lock_api(cache->context, storage_key, 0, &locked); + + zend_opcache_user_cache_exists_api(cache->context, storage_key, &exists); + if (locked && exists) { + zend_opcache_user_cache_fetch_api(cache->context, storage_key, &default_null, return_value); + zend_opcache_user_cache_unlock_api(cache->context, storage_key, &locked); + zend_string_release(storage_key); + + return; + } + + ZVAL_UNDEF(&callback_result); + fci.retval = &callback_result; + + zend_try { + if (zend_call_function(&fci, &fcc) == SUCCESS && !EG(exception)) { + if (Z_TYPE(callback_result) != IS_UNDEF) { + if (!zend_opcache_user_cache_instance_store(cache, key, &callback_result, ttl) && EG(exception)) { + zval_ptr_dtor(&callback_result); + zend_opcache_user_cache_unlock_api(cache->context, storage_key, &locked); + zend_string_release(storage_key); + RETURN_THROWS(); + } + } + } + } zend_catch { + if (locked) { + zend_opcache_user_cache_unlock_api(cache->context, storage_key, &locked); + } + zend_string_release(storage_key); + zend_bailout(); + } zend_end_try(); + + if (locked) { + zend_opcache_user_cache_unlock_api(cache->context, storage_key, &locked); + } + zend_string_release(storage_key); + + if (EG(exception)) { + if (Z_TYPE(callback_result) != IS_UNDEF) { + zval_ptr_dtor(&callback_result); + } + RETURN_THROWS(); + } + + if (Z_TYPE(callback_result) == IS_UNDEF) { + RETURN_NULL(); + } + + RETURN_COPY_VALUE(&callback_result); +} + +ZEND_METHOD(Opcache_UserCache, info) +{ + zend_opcache_user_cache_object *cache; + + ZEND_PARSE_PARAMETERS_NONE(); + + cache = zend_opcache_user_cache_from_this(ZEND_THIS); + if (cache == NULL) { + RETURN_THROWS(); + } + + zend_opcache_user_cache_return_user_info(return_value, cache); + + if (EG(exception)) { + RETURN_THROWS(); + } +} diff --git a/ext/opcache/zend_user_cache.h b/ext/opcache/zend_user_cache.h new file mode 100644 index 000000000000..93941cb0f0b0 --- /dev/null +++ b/ext/opcache/zend_user_cache.h @@ -0,0 +1,87 @@ +/* + +----------------------------------------------------------------------+ + | 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_USER_CACHE_H +#define ZEND_USER_CACHE_H + +#include "php.h" + +typedef bool (*zend_opcache_user_cache_safe_direct_clone_value_func_t)( + void *context, + zval *dst, + zval *src +); + +typedef bool (*zend_opcache_user_cache_safe_direct_value_has_unstorable_func_t)( + void *context, + const zval *value +); + +typedef bool (*zend_opcache_user_cache_safe_direct_state_copy_func_t)( + void *context, + zend_object *old_object, + zend_object *new_object, + zend_opcache_user_cache_safe_direct_clone_value_func_t clone_value +); + +typedef bool (*zend_opcache_user_cache_safe_direct_state_has_unstorable_func_t)( + void *context, + const zval *value, + zend_opcache_user_cache_safe_direct_value_has_unstorable_func_t value_has_unstorable +); + +typedef bool (*zend_opcache_user_cache_safe_direct_state_serialize_func_t)( + const zval *object, + zval *state +); + +typedef bool (*zend_opcache_user_cache_safe_direct_state_unserialize_func_t)( + zval *object, + zval *state +); + +typedef struct _zend_opcache_user_cache_partition zend_opcache_user_cache_partition; + +typedef struct { + bool prefer_request_local_prototype; + zend_opcache_user_cache_safe_direct_state_copy_func_t copy; + zend_opcache_user_cache_safe_direct_state_has_unstorable_func_t state_has_unstorable; + zend_opcache_user_cache_safe_direct_state_serialize_func_t state_serialize; + zend_opcache_user_cache_safe_direct_state_unserialize_func_t state_unserialize; +} zend_opcache_user_cache_safe_direct_handlers; + +BEGIN_EXTERN_C() + +/* Exported for SAPIs built outside the OPcache module on Windows. */ +ZEND_API void zend_opcache_user_cache_opt_in(void); +ZEND_API bool zend_opcache_user_cache_startup_default_context_storage(void); +ZEND_API zend_opcache_user_cache_partition *zend_opcache_user_cache_partition_create(const char *name); +ZEND_API bool zend_opcache_user_cache_partition_startup_storage(zend_opcache_user_cache_partition *partition); +ZEND_API void zend_opcache_user_cache_activate_request_unavailable(const char *failure_reason); +ZEND_API void zend_opcache_user_cache_partition_activate(zend_opcache_user_cache_partition *partition); +void zend_opcache_user_cache_minit(void); +void zend_opcache_user_cache_mshutdown(void); +zend_result zend_opcache_user_cache_rshutdown(void); +zend_result zend_opcache_user_cache_post_deactivate(void); +void zend_opcache_user_cache_invalidate_all(void); +void zend_opcache_user_cache_safe_direct_register_class( + zend_class_entry *ce, + const zend_opcache_user_cache_safe_direct_handlers *handlers +); + +END_EXTERN_C() + +#endif /* ZEND_USER_CACHE_H */ diff --git a/ext/opcache/zend_user_cache_entries.c b/ext/opcache/zend_user_cache_entries.c new file mode 100644 index 000000000000..d488592f5a1a --- /dev/null +++ b/ext/opcache/zend_user_cache_entries.c @@ -0,0 +1,3478 @@ +/* + +----------------------------------------------------------------------+ + | 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/zend_closures.h" +#include "Zend/zend_objects.h" +#include "ext/spl/php_spl.h" + +#include "zend_user_cache_internal.h" + +#define ZEND_OPCACHE_USER_CACHE_REQUEST_LOCAL_ARRAY_NO_CLONE ((void *) 1) +#define ZEND_OPCACHE_USER_CACHE_REQUEST_LOCAL_ARRAY_NEEDS_CLONE ((void *) 2) + +typedef struct { + HashTable arrays; + HashTable objects; + HashTable references; + HashTable *array_clone_flags; + /* False when identity maps are not needed. */ + bool track_identity; +} zend_opcache_user_cache_request_local_clone_context; + +typedef enum { + ZEND_OPCACHE_USER_CACHE_REQUEST_LOCAL_SLOT_MISS, + ZEND_OPCACHE_USER_CACHE_REQUEST_LOCAL_SLOT_HIT +} zend_opcache_user_cache_request_local_slot_result; + +typedef struct _zend_opcache_user_cache_unstorable_context { + HashTable *seen_arrays; + HashTable *seen_objects; + const char **failure_message; +} zend_opcache_user_cache_unstorable_context; + +static bool zend_opcache_user_cache_clone_request_local_value( + zend_opcache_user_cache_request_local_clone_context *context, + zval *dst, + zval *src +); + +static bool zend_opcache_user_cache_value_needs_request_local_clone_cached( + zend_opcache_user_cache_request_local_clone_context *context, + zval *value +); + +static bool zend_opcache_user_cache_find_slot_for_read_locked( + zend_string *key, + zend_ulong hash, + zend_opcache_user_cache_header **header_ptr, + uint32_t *slot_index, + bool *found +); + +static bool zend_opcache_user_cache_find_unstorable_value( + zval *value, + HashTable *seen_arrays, + HashTable *seen_objects, + const char **failure_message +); + +static void zend_opcache_user_cache_rehash_locked(zend_opcache_user_cache_header *header); + +static zend_always_inline void zend_opcache_user_cache_release_value_storage_locked(uint8_t value_type, uint32_t value_offset) +{ + bool graph_quiescent; + + if (value_offset == 0) { + return; + } + + if (value_type == ZEND_OPCACHE_USER_CACHE_VALUE_SHARED_GRAPH) { + /* An optimistic reader may still be about to pin this graph. */ + graph_quiescent = zend_opcache_user_cache_graph_payloads_quiescent_locked(); + + if (zend_opcache_user_cache_shared_graph_retire_payload_locked(value_offset)) { + if (graph_quiescent) { + zend_opcache_user_cache_free_locked(value_offset); + } else { + zend_opcache_user_cache_orphan_graph_payload_locked(value_offset); + } + } + } else if (zend_opcache_user_cache_value_uses_offset(value_type)) { + zend_opcache_user_cache_free_locked(value_offset); + } +} + +static zend_always_inline void zend_opcache_user_cache_release_entry_storage_locked(zend_opcache_user_cache_entry *entry) +{ + bool uses_combined_value_key; + + uses_combined_value_key = (entry->reserved & ZEND_OPCACHE_USER_CACHE_ENTRY_RESERVED_COMBINED_VALUE_KEY) != 0; + if (entry->key_offset != 0 && !uses_combined_value_key) { + zend_opcache_user_cache_free_locked(entry->key_offset); + } + + zend_opcache_user_cache_release_value_storage_locked(entry->value_type, entry->value_offset); +} + +static zend_always_inline void zend_opcache_user_cache_release_entry_storage_except_locked( + zend_opcache_user_cache_entry *entry, + const zend_opcache_user_cache_entry *kept_entry) +{ + bool uses_combined_value_key, kept_uses_combined_value_key; + + uses_combined_value_key = (entry->reserved & ZEND_OPCACHE_USER_CACHE_ENTRY_RESERVED_COMBINED_VALUE_KEY) != 0; + kept_uses_combined_value_key = kept_entry != NULL && + (kept_entry->reserved & ZEND_OPCACHE_USER_CACHE_ENTRY_RESERVED_COMBINED_VALUE_KEY) != 0 + ; + if (entry->key_offset != 0 && !uses_combined_value_key && + (kept_entry == NULL || kept_uses_combined_value_key || entry->key_offset != kept_entry->key_offset) + ) { + zend_opcache_user_cache_free_locked(entry->key_offset); + } + + if (entry->value_offset != 0 && + (kept_entry == NULL || + entry->value_offset != kept_entry->value_offset || + entry->value_type != kept_entry->value_type) + ) { + zend_opcache_user_cache_release_value_storage_locked(entry->value_type, entry->value_offset); + } +} + +static zend_always_inline void zend_opcache_user_cache_delete_entry_locked(zend_opcache_user_cache_entry *entry, zend_opcache_user_cache_header *header) +{ + if (entry->state == ZEND_OPCACHE_USER_CACHE_ENTRY_USED && header->count != 0) { + header->count--; + } + + if (entry->state != ZEND_OPCACHE_USER_CACHE_ENTRY_TOMBSTONE) { + header->tombstone_count++; + } + + zend_opcache_user_cache_release_entry_storage_locked(entry); + entry->hash = 0; + entry->key_offset = 0; + entry->key_len = 0; + entry->state = ZEND_OPCACHE_USER_CACHE_ENTRY_TOMBSTONE; + entry->value_type = ZEND_OPCACHE_USER_CACHE_VALUE_NULL; + entry->value_offset = 0; + entry->value_len = 0; + entry->reserved = 0; + entry->expires_at = 0; + entry->generation = 0; + entry->long_value = 0; + entry->double_value = 0; + zend_opcache_user_cache_bump_mutation_epoch_locked(header); +} + +static zend_always_inline void zend_opcache_user_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 zend_always_inline bool zend_opcache_user_cache_is_expired(const zend_opcache_user_cache_entry *entry, uint64_t now) +{ + return entry->state == ZEND_OPCACHE_USER_CACHE_ENTRY_USED && entry->expires_at != 0 && entry->expires_at <= now; +} + +static zend_always_inline bool zend_opcache_user_cache_maybe_expired(const zend_opcache_user_cache_entry *entry, uint64_t *now) +{ + if (entry->state != ZEND_OPCACHE_USER_CACHE_ENTRY_USED || entry->expires_at == 0) { + return false; + } + + if (*now == 0) { + *now = (uint64_t) time(NULL); + } + + return zend_opcache_user_cache_is_expired(entry, *now); +} + +static zend_always_inline bool zend_opcache_user_cache_payload_can_fit_locked(size_t size) +{ + zend_opcache_user_cache_header *header = zend_opcache_user_cache_header_ptr(); + size_t total_size; + + if (!header || size == 0 || size > UINT32_MAX - sizeof(zend_opcache_user_cache_block)) { + return false; + } + + total_size = ZEND_ALIGNED_SIZE(sizeof(zend_opcache_user_cache_block) + size); + + return total_size <= UINT32_MAX && total_size <= header->data_size; +} + +static zend_always_inline void zend_opcache_user_cache_init_prepared_value(zend_opcache_user_cache_prepared_value *prepared) +{ + memset(prepared, 0, sizeof(*prepared)); + prepared->value_type = ZEND_OPCACHE_USER_CACHE_VALUE_NULL; +} + +static zend_always_inline bool zend_opcache_user_cache_prepared_value_can_seed_request_local_slot( + const zend_opcache_user_cache_prepared_value *prepared) +{ + return prepared->value_type == ZEND_OPCACHE_USER_CACHE_VALUE_STRING && + prepared->value_len >= ZEND_OPCACHE_USER_CACHE_REQUEST_LOCAL_STRING_MIN_LEN + ; +} + +static zend_always_inline bool zend_opcache_user_cache_long_add_wrapped( + zend_long lhs, + zend_long rhs, + zend_long *result) +{ + *result = (zend_long) ((zend_ulong) lhs + (zend_ulong) rhs); + + return (rhs > 0 && lhs > ZEND_LONG_MAX - rhs) || + (rhs < 0 && lhs < ZEND_LONG_MIN - rhs) + ; +} + +static zend_always_inline bool zend_opcache_user_cache_long_sub_wrapped( + zend_long lhs, + zend_long rhs, + zend_long *result) +{ + *result = (zend_long) ((zend_ulong) lhs - (zend_ulong) rhs); + + return (rhs > 0 && lhs < ZEND_LONG_MIN + rhs) || + (rhs < 0 && lhs > ZEND_LONG_MAX + rhs) + ; +} + +static zend_always_inline void zend_opcache_user_cache_maybe_rehash_locked(void) +{ + zend_opcache_user_cache_header *header = zend_opcache_user_cache_header_ptr(); + + if (header != NULL && + zend_opcache_user_cache_header_is_initialized_locked() && + header->tombstone_count > header->capacity / 4 + ) { + zend_opcache_user_cache_rehash_locked(header); + } +} + +#ifdef ZEND_OPCACHE_USER_CACHE_HAVE_OPTIMISTIC +static zend_always_inline bool zend_opcache_user_cache_optimistic_payload_in_bounds( + const zend_opcache_user_cache_header *header, + uint32_t offset, + uint64_t len) +{ + uint64_t limit = (uint64_t) header->data_offset + header->data_size; + + return offset >= header->data_offset + sizeof(zend_opcache_user_cache_block) && + (uint64_t) offset + len <= limit + ; +} + +static zend_always_inline bool zend_opcache_user_cache_optimistic_header( + zend_opcache_user_cache_header **header_ptr, + uint64_t *seq_ptr) +{ + zend_opcache_user_cache_header *header = zend_opcache_user_cache_header_ptr(); + uint64_t seq; + + if (header == NULL) { + return false; + } + + seq = zend_opcache_user_cache_seq_load(&header->write_seq); + if (seq < 2 || (seq & 1) != 0) { + return false; + } + + if (header->magic != ZEND_OPCACHE_USER_CACHE_MAGIC || + header->version != ZEND_OPCACHE_USER_CACHE_VERSION || + header->capacity < ZEND_OPCACHE_USER_CACHE_MIN_CAPACITY || + header->capacity > ZEND_OPCACHE_USER_CACHE_MAX_CAPACITY + ) { + return false; + } + + if (zend_opcache_user_cache_seq_reload(&header->write_seq) != seq) { + return false; + } + + *header_ptr = header; + *seq_ptr = seq; + + return true; +} + +/* Lock-free probe; the caller revalidates the sequence before using snapshot. */ +static bool zend_opcache_user_cache_optimistic_probe( + const zend_opcache_user_cache_header *header, + const zend_opcache_user_cache_entry *entries, + zend_string *key, + zend_ulong hash, + zend_opcache_user_cache_entry *snapshot, + uint32_t *slot_index_ptr, + bool *found) +{ + const zend_opcache_user_cache_entry *entry; + uint64_t now = 0; + uint32_t index, step; + + index = (uint32_t) (hash % header->capacity); + + for (step = 0; step < header->capacity; step++) { + entry = &entries[index]; + + switch (entry->state) { + case ZEND_OPCACHE_USER_CACHE_ENTRY_EMPTY: + *found = false; + + return true; + case ZEND_OPCACHE_USER_CACHE_ENTRY_USED: + if (entry->hash == hash && entry->key_len == ZSTR_LEN(key)) { + if (!zend_opcache_user_cache_optimistic_payload_in_bounds(header, entry->key_offset, entry->key_len)) { + return false; + } + + if (memcmp(zend_opcache_user_cache_ptr(entry->key_offset), ZSTR_VAL(key), ZSTR_LEN(key)) == 0) { + if (entry->expires_at != 0) { + if (now == 0) { + now = (uint64_t) time(NULL); + } + + if (entry->expires_at <= now) { + *found = false; + + return true; + } + } + + *snapshot = *entry; + *slot_index_ptr = index; + *found = true; + + return true; + } + } + break; + case ZEND_OPCACHE_USER_CACHE_ENTRY_TOMBSTONE: + break; + default: + /* Torn read: resolved by the caller's sequence revalidation. */ + return false; + } + + ++index; + if (index == header->capacity) { + index = 0; + } + } + + *found = false; + + return true; +} +#endif + +static void zend_opcache_user_cache_request_local_slot_dtor(zval *slot_zv) +{ + zend_opcache_user_cache_request_local_slot *slot = Z_PTR_P(slot_zv); + + if (slot->has_array_clone_flags) { + zend_hash_destroy(&slot->array_clone_flags); + } + + if (!Z_ISUNDEF(slot->value)) { + zval_ptr_dtor(&slot->value); + } + + efree(slot); +} + +static bool zend_opcache_user_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_user_cache_value_needs_request_local_clone_inner(element, visited_arrays)) { + zend_hash_index_del(visited_arrays, array_key); + + return true; + } + } ZEND_HASH_FOREACH_END(); + + zend_hash_index_del(visited_arrays, array_key); + + return false; + default: + return false; + } +} + +static bool zend_opcache_user_cache_value_needs_request_local_clone(zval *value) +{ + HashTable visited_arrays; + bool result; + + if (Z_ISREF_P(value)) { + return true; + } else if (Z_TYPE_P(value) == IS_OBJECT) { + return true; + } else if (Z_TYPE_P(value) != IS_ARRAY) { + return false; + } + + zend_hash_init(&visited_arrays, 8, NULL, NULL, 0); + + result = zend_opcache_user_cache_value_needs_request_local_clone_inner(value, &visited_arrays); + zend_hash_destroy(&visited_arrays); + + return result; +} + +static bool zend_opcache_user_cache_collect_request_local_array_clone_flags_inner( + zval *value, + HashTable *visited_arrays, + HashTable *visited_objects, + HashTable *array_clone_flags, + bool record_array_result); + +static bool zend_opcache_user_cache_collect_request_local_object_clone_flags( + zend_object *object, + HashTable *visited_arrays, + HashTable *visited_objects, + HashTable *array_clone_flags) +{ + zend_ulong object_key; + zval *prop, *src, *end; + void *flag; + bool members_need_clone = false; + + if (object == NULL) { + return true; + } + + object_key = (zend_ulong) (uintptr_t) object; + + flag = zend_hash_index_find_ptr(array_clone_flags, object_key); + if (flag != NULL) { + return flag == ZEND_OPCACHE_USER_CACHE_REQUEST_LOCAL_ARRAY_NEEDS_CLONE; + } + + if (zend_hash_index_exists(visited_objects, object_key)) { + return true; + } + + zend_hash_index_add_empty_element(visited_objects, object_key); + + if (object->ce->default_properties_count) { + src = object->properties_table; + end = src + object->ce->default_properties_count; + do { + if (zend_opcache_user_cache_collect_request_local_array_clone_flags_inner( + src, + visited_arrays, + visited_objects, + array_clone_flags, + true + ) + ) { + members_need_clone = true; + } + src++; + } while (src != end); + } + + if (object->properties != NULL && zend_hash_num_elements(object->properties) != 0) { + /* Dynamic properties require the deep path. */ + members_need_clone = true; + + ZEND_HASH_MAP_FOREACH_VAL(object->properties, prop) { + if (Z_TYPE_P(prop) != IS_INDIRECT) { + zend_opcache_user_cache_collect_request_local_array_clone_flags_inner( + prop, + visited_arrays, + visited_objects, + array_clone_flags, + true + ); + } + } ZEND_HASH_FOREACH_END(); + } + + /* NO_CLONE allows a property-table memcpy. */ + zend_hash_index_update_ptr( + array_clone_flags, + object_key, + members_need_clone ? + ZEND_OPCACHE_USER_CACHE_REQUEST_LOCAL_ARRAY_NEEDS_CLONE : + ZEND_OPCACHE_USER_CACHE_REQUEST_LOCAL_ARRAY_NO_CLONE + ); + + return members_need_clone; +} + +static bool zend_opcache_user_cache_collect_request_local_array_clone_flags_inner( + zval *value, + HashTable *visited_arrays, + HashTable *visited_objects, + HashTable *array_clone_flags, + bool record_array_result) +{ + zend_ulong array_key; + zval *element; + bool needs_clone = false; + void *flag; + + if (Z_ISREF_P(value)) { + zend_opcache_user_cache_collect_request_local_array_clone_flags_inner( + &Z_REF_P(value)->val, + visited_arrays, + visited_objects, + array_clone_flags, + true + ); + + return true; + } + + switch (Z_TYPE_P(value)) { + case IS_OBJECT: + zend_opcache_user_cache_collect_request_local_object_clone_flags( + Z_OBJ_P(value), + visited_arrays, + visited_objects, + array_clone_flags + ); + + return true; + case IS_ARRAY: + array_key = (zend_ulong) (uintptr_t) Z_ARRVAL_P(value); + flag = zend_hash_index_find_ptr(array_clone_flags, array_key); + if (flag != NULL) { + return flag == ZEND_OPCACHE_USER_CACHE_REQUEST_LOCAL_ARRAY_NEEDS_CLONE; + } + + 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_user_cache_collect_request_local_array_clone_flags_inner( + element, + visited_arrays, + visited_objects, + array_clone_flags, + false + ) + ) { + needs_clone = true; + } + } ZEND_HASH_FOREACH_END(); + + zend_hash_index_del(visited_arrays, array_key); + + if (needs_clone || record_array_result) { + zend_hash_index_update_ptr( + array_clone_flags, + array_key, + needs_clone ? + ZEND_OPCACHE_USER_CACHE_REQUEST_LOCAL_ARRAY_NEEDS_CLONE : + ZEND_OPCACHE_USER_CACHE_REQUEST_LOCAL_ARRAY_NO_CLONE + ); + } + + return needs_clone; + default: + return false; + } +} + +static bool zend_opcache_user_cache_collect_request_local_array_clone_flags( + zval *value, + HashTable *array_clone_flags) +{ + HashTable visited_arrays, visited_objects; + bool result; + + zend_hash_init(&visited_arrays, 8, NULL, NULL, 0); + zend_hash_init(&visited_objects, 8, NULL, NULL, 0); + + result = zend_opcache_user_cache_collect_request_local_array_clone_flags_inner( + value, + &visited_arrays, + &visited_objects, + array_clone_flags, + true + ); + + zend_hash_destroy(&visited_objects); + zend_hash_destroy(&visited_arrays); + + return result; +} + +static void zend_opcache_user_cache_request_local_clone_object_dtor(zval *object_zv) +{ + zend_object *object = Z_PTR_P(object_zv); + + OBJ_RELEASE(object); +} + +static void zend_opcache_user_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_user_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_user_cache_request_local_clone_context_init( + zend_opcache_user_cache_request_local_clone_context *context, + HashTable *array_clone_flags, + bool track_identity) +{ + zend_hash_init(&context->arrays, 8, NULL, zend_opcache_user_cache_request_local_clone_array_dtor, 0); + zend_hash_init(&context->objects, 8, NULL, zend_opcache_user_cache_request_local_clone_object_dtor, 0); + zend_hash_init(&context->references, 8, NULL, zend_opcache_user_cache_request_local_clone_reference_dtor, 0); + context->array_clone_flags = array_clone_flags; + context->track_identity = track_identity; +} + +static void zend_opcache_user_cache_request_local_clone_context_destroy( + zend_opcache_user_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_user_cache_value_needs_request_local_clone_cached( + zend_opcache_user_cache_request_local_clone_context *context, + zval *value) +{ + void *flag; + + 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; + } + + if (context->array_clone_flags != NULL) { + flag = zend_hash_index_find_ptr( + context->array_clone_flags, + (zend_ulong) (uintptr_t) Z_ARRVAL_P(value) + ); + if (flag != NULL) { + return flag == ZEND_OPCACHE_USER_CACHE_REQUEST_LOCAL_ARRAY_NEEDS_CLONE; + } + } + + return zend_opcache_user_cache_value_needs_request_local_clone(value); +} + +static bool zend_opcache_user_cache_clone_request_local_array_ex( + zend_opcache_user_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; + zval *element, cloned_element; + void *flag; + + if (!known_needs_clone) { + if (context->array_clone_flags != NULL) { + flag = zend_hash_index_find_ptr(context->array_clone_flags, key); + if (flag == ZEND_OPCACHE_USER_CACHE_REQUEST_LOCAL_ARRAY_NO_CLONE) { + ZVAL_COPY(dst, src); + + return true; + } else if (flag == ZEND_OPCACHE_USER_CACHE_REQUEST_LOCAL_ARRAY_NEEDS_CLONE) { + known_needs_clone = true; + } + } + + if (!known_needs_clone && !zend_opcache_user_cache_value_needs_request_local_clone(src)) { + ZVAL_COPY(dst, src); + + return true; + } + } + + if (context->track_identity) { + array = zend_hash_index_find_ptr(&context->arrays, key); + if (array != NULL) { + GC_ADDREF(array); + ZVAL_ARR(dst, array); + + return true; + } + } + + array = zend_array_dup(Z_ARRVAL_P(src)); + if (context->track_identity) { + zend_hash_index_update_ptr(&context->arrays, key, array); + } + + ZEND_HASH_FOREACH_VAL(array, element) { + if (Z_TYPE_P(element) == IS_INDIRECT || + !zend_opcache_user_cache_value_needs_request_local_clone_cached(context, element) + ) { + continue; + } + + if (!zend_opcache_user_cache_clone_request_local_value(context, &cloned_element, element)) { + if (!context->track_identity) { + zend_array_release(array); + } + + ZVAL_UNDEF(dst); + + return false; + } + + zval_ptr_dtor(element); + ZVAL_COPY_VALUE(element, &cloned_element); + } ZEND_HASH_FOREACH_END(); + + if (context->track_identity) { + GC_ADDREF(array); + } + + ZVAL_ARR(dst, array); + + return true; +} + +static bool zend_opcache_user_cache_clone_request_local_reference( + zend_opcache_user_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; + zval inner; + + if (context->track_identity) { + new_ref = zend_hash_index_find_ptr(&context->references, key); + 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); + + if (context->track_identity) { + zend_hash_index_update_ptr(&context->references, key, new_ref); + } + + if (!zend_opcache_user_cache_clone_request_local_value(context, &inner, &src_ref->val)) { + if (!context->track_identity) { + zval_ptr_dtor(dst); + } + + ZVAL_UNDEF(dst); + + return false; + } + + ZVAL_COPY_VALUE(&new_ref->val, &inner); + if (context->track_identity) { + GC_ADDREF(new_ref); + ZVAL_REF(dst, new_ref); + } + + return true; +} + +static bool zend_opcache_user_cache_clone_request_local_object_members( + zend_opcache_user_cache_request_local_clone_context *context, + zend_object *old_object, + zend_object *new_object, + bool rebind_self_hash_properties) +{ + zend_ulong num_key; + zend_string *key, *old_object_hash = NULL, *new_object_hash = NULL; + zend_property_info *property_info; + zval *src, *dst, *end, *prop, new_prop; + uint32_t property_index = 0; + + 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 (rebind_self_hash_properties && + Z_TYPE_P(src) == IS_STRING && + zend_opcache_user_cache_is_userland_declared_non_public_property(old_object, property_index) + ) { + if (old_object_hash == NULL) { + old_object_hash = php_spl_object_hash(old_object); + } + + if (zend_string_equals(Z_STR_P(src), old_object_hash)) { + if (new_object_hash == NULL) { + new_object_hash = php_spl_object_hash(new_object); + } + + ZVAL_STR_COPY(&new_prop, new_object_hash); + } else { + if (!zend_opcache_user_cache_clone_request_local_value(context, &new_prop, src)) { + if (old_object_hash != NULL) { + zend_string_release(old_object_hash); + } + + if (new_object_hash != NULL) { + zend_string_release(new_object_hash); + } + + return false; + } + } + } else if (!zend_opcache_user_cache_clone_request_local_value(context, &new_prop, src)) { + if (old_object_hash != NULL) { + zend_string_release(old_object_hash); + } + + if (new_object_hash != NULL) { + zend_string_release(new_object_hash); + } + + return false; + } + + zval_ptr_dtor(dst); + + ZVAL_COPY_VALUE(dst, &new_prop); + Z_PROP_FLAG_P(dst) = Z_PROP_FLAG_P(src); + + if (Z_ISREF_P(dst) && new_object->ce->properties_info_table != NULL) { + property_info = new_object->ce->properties_info_table[property_index]; + if (property_info != NULL && + property_info != ZEND_WRONG_PROPERTY_INFO && + ZEND_TYPE_IS_SET(property_info->type) + ) { + ZEND_REF_ADD_TYPE_SOURCE(Z_REF_P(dst), property_info); + } + } + + src++; + dst++; + property_index++; + } 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_user_cache_clone_request_local_value(context, &new_prop, prop)) { + if (old_object_hash != NULL) { + zend_string_release(old_object_hash); + } + + if (new_object_hash != NULL) { + zend_string_release(new_object_hash); + } + + 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(); + } + + if (old_object_hash != NULL) { + zend_string_release(old_object_hash); + } + + if (new_object_hash != NULL) { + zend_string_release(new_object_hash); + } + + return true; +} + +static bool zend_opcache_user_cache_clone_request_local_std_object( + zend_opcache_user_cache_request_local_clone_context *context, + zend_object *old_object, + zend_object **new_object_ptr) +{ + zend_object *new_object; + zval *src, *dst, *end; + void *member_flag = NULL; + + new_object = zend_objects_new(old_object->ce); + + if (context->track_identity) { + zend_hash_index_update_ptr( + &context->objects, + (zend_ulong) (uintptr_t) old_object, + new_object + ); + } + + if (context->array_clone_flags != NULL) { + member_flag = zend_hash_index_find_ptr( + context->array_clone_flags, + (zend_ulong) (uintptr_t) old_object + ); + } + + if (member_flag == ZEND_OPCACHE_USER_CACHE_REQUEST_LOCAL_ARRAY_NO_CLONE && + old_object->properties == NULL + ) { + /* All declared properties are shareable by plain zval copy. */ + 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; + memcpy(dst, src, sizeof(zval) * old_object->ce->default_properties_count); + do { + if (Z_REFCOUNTED_P(src)) { + Z_ADDREF_P(src); + } + + src++; + } while (src != end); + } + } else { + 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); + } + + if (!zend_opcache_user_cache_clone_request_local_object_members(context, old_object, new_object, false)) { + /* Without identity tracking the caller does not own new_object. */ + if (!context->track_identity) { + OBJ_RELEASE(new_object); + } + + return false; + } + } + + if (context->track_identity) { + GC_ADDREF(new_object); + } + + *new_object_ptr = new_object; + + return true; +} + +static bool zend_opcache_user_cache_clone_request_local_value_callback( + void *context, + zval *dst, + zval *src) +{ + return zend_opcache_user_cache_clone_request_local_value( + (zend_opcache_user_cache_request_local_clone_context *) context, + dst, + src + ); +} + +static bool zend_opcache_user_cache_clone_request_local_safe_direct_object( + zend_opcache_user_cache_request_local_clone_context *context, + zend_object *old_object, + zend_object **new_object_ptr) +{ + zend_opcache_user_cache_safe_direct_state_copy_func_t copy_func; + zend_ulong key; + zend_class_entry *ce; + zend_object *new_object; + zval new_zv; + + ce = old_object->ce; + + copy_func = zend_opcache_user_cache_safe_direct_copy_func(ce, NULL); + if (copy_func == NULL) { + 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; + if (context->track_identity) { + zend_hash_index_update_ptr(&context->objects, key, new_object); + } + + if (!copy_func( + context, + old_object, + new_object, + zend_opcache_user_cache_clone_request_local_value_callback + )) { + if (!context->track_identity) { + OBJ_RELEASE(new_object); + } + + return false; + } + + if (!zend_opcache_user_cache_clone_request_local_object_members(context, old_object, new_object, true)) { + if (!context->track_identity) { + OBJ_RELEASE(new_object); + } + + return false; + } + + if (context->track_identity) { + GC_ADDREF(new_object); + } + + *new_object_ptr = new_object; + + return true; +} + +static bool zend_opcache_user_cache_clone_request_local_object( + zend_opcache_user_cache_request_local_clone_context *context, + zend_object *old_object, + zend_object **new_object_ptr) +{ + zend_ulong key; + zend_object *new_object; + + if (old_object == NULL || zend_object_is_lazy(old_object)) { + return false; + } + + if (context->track_identity) { + key = (zend_ulong) (uintptr_t) old_object; + new_object = zend_hash_index_find_ptr(&context->objects, key); + + if (new_object != NULL) { + GC_ADDREF(new_object); + *new_object_ptr = new_object; + + return true; + } + } + + if (old_object->handlers == zend_get_std_object_handlers()) { + return zend_opcache_user_cache_clone_request_local_std_object(context, old_object, new_object_ptr); + } + + return zend_opcache_user_cache_clone_request_local_safe_direct_object(context, old_object, new_object_ptr); +} + +static bool zend_opcache_user_cache_clone_request_local_value( + zend_opcache_user_cache_request_local_clone_context *context, + zval *dst, + zval *src) +{ + zend_object *object; + + if (Z_ISREF_P(src)) { + return zend_opcache_user_cache_clone_request_local_reference(context, dst, Z_REF_P(src)); + } + + switch (Z_TYPE_P(src)) { + case IS_ARRAY: + return zend_opcache_user_cache_clone_request_local_array_ex(context, dst, src, false); + case IS_OBJECT: + /* The clone helper returns a reference owned by the caller, so the + * destination takes it without a further addref. */ + if (!zend_opcache_user_cache_clone_request_local_object(context, Z_OBJ_P(src), &object)) { + return false; + } + + ZVAL_OBJ(dst, object); + + return true; + default: + ZVAL_COPY(dst, src); + + return true; + } +} + +static bool zend_opcache_user_cache_clone_request_local_slot_value(zval *dst, zval *src, bool no_aliases) +{ + zend_opcache_user_cache_request_local_clone_context context; + bool result; + + if (Z_TYPE_P(src) == IS_ARRAY) { + if (!zend_opcache_user_cache_value_needs_request_local_clone(src)) { + ZVAL_COPY(dst, src); + + return true; + } + + zend_opcache_user_cache_request_local_clone_context_init(&context, NULL, !no_aliases); + result = zend_opcache_user_cache_clone_request_local_array_ex(&context, dst, src, true); + zend_opcache_user_cache_request_local_clone_context_destroy(&context); + + return result; + } + + if (!zend_opcache_user_cache_value_needs_request_local_clone(src)) { + ZVAL_COPY(dst, src); + + return true; + } + + zend_opcache_user_cache_request_local_clone_context_init(&context, NULL, !no_aliases); + result = zend_opcache_user_cache_clone_request_local_value(&context, dst, src); + zend_opcache_user_cache_request_local_clone_context_destroy(&context); + + return result; +} + +static bool zend_opcache_user_cache_clone_request_local_slot_value_known( + zval *dst, + zval *src, + bool needs_clone, + HashTable *array_clone_flags, + bool no_aliases) +{ + zend_opcache_user_cache_request_local_clone_context context; + bool result; + + if (!needs_clone) { + ZVAL_COPY(dst, src); + + return true; + } + + zend_opcache_user_cache_request_local_clone_context_init(&context, array_clone_flags, !no_aliases); + + if (Z_TYPE_P(src) == IS_ARRAY) { + result = zend_opcache_user_cache_clone_request_local_array_ex(&context, dst, src, true); + } else { + result = zend_opcache_user_cache_clone_request_local_value(&context, dst, src); + } + + zend_opcache_user_cache_request_local_clone_context_destroy(&context); + + return result; +} + +static HashTable *zend_opcache_user_cache_request_local_slots(void) +{ + HashTable **slots_ptr = zend_opcache_user_cache_active_request_local_slots_ptr(); + + if (*slots_ptr == NULL) { + ALLOC_HASHTABLE(*slots_ptr); + zend_hash_init(*slots_ptr, 0, NULL, zend_opcache_user_cache_request_local_slot_dtor, 0); + } + + return *slots_ptr; +} + +static zend_never_inline void zend_opcache_user_cache_reacquire_read_lock_or_fail(const char *cache_name) +{ + if (!zend_opcache_user_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_user_cache_reacquire_write_lock_or_fail(const char *cache_name) +{ + if (!zend_opcache_user_cache_wlock()) { + zend_error_noreturn(E_ERROR, "Unable to reacquire the %s write lock after userland execution", cache_name); + } + + if (!zend_opcache_user_cache_header_init_locked()) { + zend_try { + zend_throw_exception_ex(zend_opcache_user_cache_exception_ce, 0, "Unable to initialize the %s header", cache_name); + } zend_catch { + zend_opcache_user_cache_unlock_if_held(); + zend_bailout(); + } zend_end_try(); + + return false; + } + + return true; +} + +static bool zend_opcache_user_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) +{ + bool ref_registered, lock_safe, defer_retired = false; + int result; + + switch (value_type) { + case ZEND_OPCACHE_USER_CACHE_VALUE_SHARED_GRAPH: + ref_registered = zend_opcache_user_cache_has_request_shared_graph_ref(value_offset); + if (!ref_registered && !zend_opcache_user_cache_shared_graph_acquire_locked(value_offset)) { + if (throw_if_missing) { + zend_try { + zend_throw_exception_ex(zend_opcache_user_cache_exception_ce, 0, "Stored %s value for key \"%s\" is corrupted", cache_name, ZSTR_VAL(key)); + } zend_catch { + zend_opcache_user_cache_unlock_if_held(); + zend_bailout(); + } zend_end_try(); + } + + return false; + } + + lock_safe = zend_opcache_user_cache_shared_graph_decode_is_lock_safe(value_offset); + if (!lock_safe) { + zend_opcache_user_cache_unlock(); + } + + ZVAL_UNDEF(return_value); + + if (lock_safe) { + zend_try { + result = zend_opcache_user_cache_fetch_shared_graph( + (const unsigned char *) zend_opcache_user_cache_ptr(value_offset), + value_len, + return_value + ); + } zend_catch { + zend_opcache_user_cache_unlock_if_held(); + zend_bailout(); + } zend_end_try(); + } else { + result = zend_opcache_user_cache_fetch_shared_graph( + (const unsigned char *) zend_opcache_user_cache_ptr(value_offset), + value_len, + return_value + ); + + if (result && !ref_registered) { + zend_opcache_user_cache_register_shared_graph_ref(value_offset); + } + } + if (!lock_safe && !result && Z_TYPE_P(return_value) != IS_UNDEF) { + zval_ptr_dtor(return_value); + ZVAL_UNDEF(return_value); + } + + if (!lock_safe) { + zend_opcache_user_cache_reacquire_read_lock_or_fail(cache_name); + } + + if (!result) { + if (lock_safe && Z_TYPE_P(return_value) != IS_UNDEF) { + zval_ptr_dtor(return_value); + ZVAL_UNDEF(return_value); + } + + if (!ref_registered && zend_opcache_user_cache_shared_graph_release_ref_locked(value_offset)) { + defer_retired = true; + } + + if (defer_retired) { + zend_opcache_user_cache_unlock(); + zend_opcache_user_cache_defer_retired_shared_graph_free(value_offset); + zend_opcache_user_cache_reacquire_read_lock_or_fail(cache_name); + } + + if (!EG(exception) && throw_if_missing) { + zend_try { + zend_throw_exception_ex(zend_opcache_user_cache_exception_ce, 0, "Stored %s value for key \"%s\" is corrupted", cache_name, ZSTR_VAL(key)); + } zend_catch { + zend_opcache_user_cache_unlock_if_held(); + zend_bailout(); + } zend_end_try(); + } + + return false; + } + + if (lock_safe && !ref_registered) { + /* Registration is request-local bookkeeping; it may allocate, so + * a bailout must still release the held read lock. Registering + * under the lock avoids the unlock/relock round-trip the + * off-lock decode path pays anyway. */ + zend_try { + zend_opcache_user_cache_register_shared_graph_ref(value_offset); + } zend_catch { + zend_opcache_user_cache_unlock_if_held(); + zend_bailout(); + } zend_end_try(); + } + + return true; + default: + return false; + } +} + +static zend_opcache_user_cache_request_local_slot_result zend_opcache_user_cache_fetch_request_local_slot( + zend_string *key, + uint64_t generation, + zval *return_value) +{ + zend_opcache_user_cache_request_local_slot *slot; + HashTable **slots_ptr = zend_opcache_user_cache_active_request_local_slots_ptr(); + + if (*slots_ptr == NULL) { + return ZEND_OPCACHE_USER_CACHE_REQUEST_LOCAL_SLOT_MISS; + } + + slot = zend_hash_find_ptr(*slots_ptr, key); + if (slot == NULL) { + return ZEND_OPCACHE_USER_CACHE_REQUEST_LOCAL_SLOT_MISS; + } + + if (slot->generation != generation) { + zend_hash_del(*slots_ptr, key); + + return ZEND_OPCACHE_USER_CACHE_REQUEST_LOCAL_SLOT_MISS; + } + + if (!zend_opcache_user_cache_clone_request_local_slot_value_known( + return_value, + &slot->value, + slot->needs_clone, + slot->has_array_clone_flags ? &slot->array_clone_flags : NULL, + slot->no_aliases + ) + ) { + zend_hash_del(*slots_ptr, key); + + return ZEND_OPCACHE_USER_CACHE_REQUEST_LOCAL_SLOT_MISS; + } + + return ZEND_OPCACHE_USER_CACHE_REQUEST_LOCAL_SLOT_HIT; +} + +static zend_opcache_user_cache_request_local_slot_result zend_opcache_user_cache_fetch_request_local_slot_locked( + zend_string *key, + uint64_t generation, + zval *return_value) +{ + zend_opcache_user_cache_request_local_slot_result result = ZEND_OPCACHE_USER_CACHE_REQUEST_LOCAL_SLOT_MISS; + + zend_try { + result = zend_opcache_user_cache_fetch_request_local_slot(key, generation, return_value); + } zend_catch { + zend_opcache_user_cache_unlock_if_held(); + zend_bailout(); + } zend_end_try(); + + return result; +} + +static bool zend_opcache_user_cache_fetch_finish( + zend_string *key, + uint64_t generation, + zval *return_value, + bool use_request_local_slot, + bool no_aliases) +{ + if (use_request_local_slot) { + zend_opcache_user_cache_store_request_local_value_slot(key, generation, return_value, no_aliases); + } + + return true; +} + +static bool zend_opcache_user_cache_fetch_finish_locked( + zend_string *key, + uint64_t generation, + zval *return_value, + bool use_request_local_slot, + bool no_aliases) +{ + bool result = false; + + zend_try { + result = zend_opcache_user_cache_fetch_finish(key, generation, return_value, use_request_local_slot, no_aliases); + } zend_catch { + zend_opcache_user_cache_unlock_if_held(); + zend_bailout(); + } zend_end_try(); + + return result; +} + +static void zend_opcache_user_cache_zval_stringl_locked(zval *return_value, const char *value, size_t value_len) +{ + zend_try { + ZVAL_STRINGL(return_value, value, value_len); + } zend_catch { + zend_opcache_user_cache_unlock_if_held(); + zend_bailout(); + } zend_end_try(); +} + +static void zend_opcache_user_cache_throw_missing_key_locked(zend_string *key) +{ + zend_try { + zend_throw_exception_ex(zend_opcache_user_cache_exception_ce, 0, "Cache key \"%s\" was not found", ZSTR_VAL(key)); + } zend_catch { + zend_opcache_user_cache_unlock_if_held(); + zend_bailout(); + } zend_end_try(); +} + +static void zend_opcache_user_cache_throw_unknown_type_locked(const char *cache_name, zend_string *key) +{ + zend_try { + zend_throw_exception_ex(zend_opcache_user_cache_exception_ce, 0, "Stored %s value for key \"%s\" has an unknown type", cache_name, ZSTR_VAL(key)); + } zend_catch { + zend_opcache_user_cache_unlock_if_held(); + zend_bailout(); + } zend_end_try(); +} + +static bool zend_opcache_user_cache_find_slot_in_header_locked( + zend_opcache_user_cache_header *header, + zend_string *key, + zend_ulong hash, + uint32_t *slot_index, + bool *found, + bool delete_expired) +{ + zend_opcache_user_cache_entry *entries, *entry; + uint64_t now = 0; + uint32_t index, first_tombstone = UINT32_MAX, step; + + if (header == NULL) { + return false; + } + + entries = zend_opcache_user_cache_entries(header); + index = (uint32_t) (hash % header->capacity); + + for (step = 0; step < header->capacity; step++) { + entry = &entries[index]; + + if (entry->state == ZEND_OPCACHE_USER_CACHE_ENTRY_EMPTY) { + *slot_index = first_tombstone != UINT32_MAX ? first_tombstone : index; + *found = false; + + return true; + } + + if (entry->state == ZEND_OPCACHE_USER_CACHE_ENTRY_TOMBSTONE) { + if (first_tombstone == UINT32_MAX) { + first_tombstone = index; + } + } else if (zend_opcache_user_cache_maybe_expired(entry, &now)) { + if (delete_expired) { + zend_opcache_user_cache_delete_entry_locked(entry, header); + } + + if (first_tombstone == UINT32_MAX) { + first_tombstone = index; + } + } else if (zend_opcache_user_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_user_cache_find_slot_exact_in_header_locked( + zend_opcache_user_cache_header *header, + zend_string *key, + zend_ulong hash, + uint32_t *slot_index, + bool *found) +{ + zend_opcache_user_cache_entry *entries, *entry; + uint32_t index, first_tombstone = UINT32_MAX, step; + + if (header == NULL) { + return false; + } + + entries = zend_opcache_user_cache_entries(header); + index = (uint32_t) (hash % header->capacity); + + for (step = 0; step < header->capacity; step++) { + entry = &entries[index]; + + if (entry->state == ZEND_OPCACHE_USER_CACHE_ENTRY_EMPTY) { + *slot_index = first_tombstone != UINT32_MAX ? first_tombstone : index; + *found = false; + + return true; + } + + if (entry->state == ZEND_OPCACHE_USER_CACHE_ENTRY_TOMBSTONE) { + if (first_tombstone == UINT32_MAX) { + first_tombstone = index; + } + } else if (zend_opcache_user_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_user_cache_find_slot_locked( + zend_string *key, + zend_ulong hash, + zend_opcache_user_cache_header **header_ptr, + uint32_t *slot_index, + bool *found, + bool delete_expired) +{ + zend_opcache_user_cache_header *header = zend_opcache_user_cache_header_ptr(); + + if (!header || !zend_opcache_user_cache_header_init_locked()) { + return false; + } + + if (header_ptr != NULL) { + *header_ptr = header; + } + + return zend_opcache_user_cache_find_slot_in_header_locked(header, key, hash, slot_index, found, delete_expired); +} + +static bool zend_opcache_user_cache_find_slot_exact_locked( + zend_string *key, + zend_ulong hash, + zend_opcache_user_cache_header **header_ptr, + uint32_t *slot_index, + bool *found) +{ + zend_opcache_user_cache_header *header = zend_opcache_user_cache_header_ptr(); + + if (!header || !zend_opcache_user_cache_header_init_locked()) { + return false; + } + + if (header_ptr != NULL) { + *header_ptr = header; + } + + return zend_opcache_user_cache_find_slot_exact_in_header_locked(header, key, hash, slot_index, found); +} + +static bool zend_opcache_user_cache_find_slot_for_read_locked( + zend_string *key, + zend_ulong hash, + zend_opcache_user_cache_header **header_ptr, + uint32_t *slot_index, + bool *found) +{ + return zend_opcache_user_cache_find_slot_locked(key, hash, header_ptr, slot_index, found, false); +} + +static bool zend_opcache_user_cache_find_slot_for_write_locked( + zend_string *key, + zend_ulong hash, + zend_opcache_user_cache_header **header_ptr, + uint32_t *slot_index, + bool *found) +{ + return zend_opcache_user_cache_find_slot_locked(key, hash, header_ptr, slot_index, found, true); +} + +/* Rebuild the entry table after tombstone churn. */ +static void zend_opcache_user_cache_rehash_locked(zend_opcache_user_cache_header *header) +{ + zend_opcache_user_cache_entry *entries, *snapshot, *entry, *target; + uint32_t index, slot, step; + + entries = zend_opcache_user_cache_entries(header); + snapshot = emalloc((size_t) header->capacity * sizeof(*snapshot)); + memcpy(snapshot, entries, (size_t) header->capacity * sizeof(*snapshot)); + memset(entries, 0, (size_t) header->capacity * sizeof(*entries)); + + header->count = 0; + header->tombstone_count = 0; + + for (index = 0; index < header->capacity; index++) { + entry = &snapshot[index]; + + if (entry->state != ZEND_OPCACHE_USER_CACHE_ENTRY_USED) { + continue; + } + + slot = (uint32_t) (entry->hash % header->capacity); + for (step = 0; step < header->capacity; step++) { + target = &entries[slot]; + + if (target->state == ZEND_OPCACHE_USER_CACHE_ENTRY_EMPTY) { + *target = *entry; + header->count++; + + break; + } + + ++slot; + if (slot == header->capacity) { + slot = 0; + } + } + } + + efree(snapshot); + + zend_opcache_user_cache_bump_mutation_epoch_locked(header); +} + +static bool zend_opcache_user_cache_expunge_expired_locked(void) +{ + zend_opcache_user_cache_header *header = zend_opcache_user_cache_header_ptr(); + zend_opcache_user_cache_entry *entries; + uint64_t now; + uint32_t index; + bool removed = false; + + if (!header || !zend_opcache_user_cache_header_init_locked()) { + return false; + } + + now = (uint64_t) time(NULL); + entries = zend_opcache_user_cache_entries(header); + for (index = 0; index < header->capacity; index++) { + if (zend_opcache_user_cache_is_expired(&entries[index], now)) { + zend_opcache_user_cache_delete_entry_locked(&entries[index], header); + removed = true; + } + } + + /* Memory pressure is also the moment to collect payloads whose free was + * deferred past a reader-drain timeout. */ + zend_opcache_user_cache_reclaim_orphaned_graphs_locked(); + + return removed; +} + +static void zend_opcache_user_cache_handle_store_failure(const char *failure_message, bool throw_on_failure, bool honor_strict_store_failure) +{ + zend_opcache_user_cache_context *context = zend_opcache_user_cache_active_context(); + + if (honor_strict_store_failure && context->strict_store_failure && !throw_on_failure) { + zend_throw_exception_ex(zend_opcache_user_cache_exception_ce, 0, "%s", failure_message); + + return; + } + + if (throw_on_failure) { + zend_throw_exception_ex(zend_opcache_user_cache_exception_ce, 0, "%s", failure_message); + } +} + +static bool zend_opcache_user_cache_safe_direct_value_has_unstorable( + void *context_ptr, + const zval *value) +{ + zend_opcache_user_cache_unstorable_context *context = context_ptr; + + return zend_opcache_user_cache_find_unstorable_value( + (zval *) value, + context->seen_arrays, + context->seen_objects, + context->failure_message + ); +} + +static bool zend_opcache_user_cache_find_unstorable_value( + zval *value, + HashTable *seen_arrays, + HashTable *seen_objects, + const char **failure_message) +{ + zend_ulong key; + zend_object *object; + zend_opcache_user_cache_safe_direct_state_has_unstorable_func_t state_has_unstorable; + zend_opcache_user_cache_unstorable_context unstorable_context; + zval *element, *property, *end; + + ZVAL_DEREF(value); + + if (Z_TYPE_P(value) == IS_RESOURCE) { + *failure_message = "resources cannot be stored in the user 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 user 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_user_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_object_is_lazy(object)) { + *failure_message = "lazy objects cannot be stored in the user cache"; + + return true; + } + + if (zend_hash_index_exists(seen_objects, key)) { + return false; + } + + zend_hash_index_add_empty_element(seen_objects, key); + + state_has_unstorable = zend_opcache_user_cache_safe_direct_state_has_unstorable_func(object->ce); + if (state_has_unstorable != NULL) { + unstorable_context.seen_arrays = seen_arrays; + unstorable_context.seen_objects = seen_objects; + unstorable_context.failure_message = failure_message; + + if (state_has_unstorable( + &unstorable_context, + value, + zend_opcache_user_cache_safe_direct_value_has_unstorable) + ) { + if (*failure_message == NULL) { + *failure_message = "objects of this class contain values that cannot be stored in the user cache"; + } + + 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_user_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_user_cache_find_unstorable_value(element, seen_arrays, seen_objects, failure_message)) { + return true; + } + } ZEND_HASH_FOREACH_END(); + } + } + + return false; +} + +static bool zend_opcache_user_cache_validate_storable_value(zval *value, bool throw_on_failure, bool honor_strict_store_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_user_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_user_cache_handle_store_failure(failure_message, throw_on_failure, honor_strict_store_failure); + } + + return false; +} + +static unsigned char *zend_opcache_user_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_user_cache_block_payload_capacity(reusable_offset) >= total_size + ) { + base_offset = reusable_offset; + } else { + base_offset = zend_opcache_user_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_user_cache_ptr(base_offset); +} + +static bool zend_opcache_user_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_user_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_user_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_user_cache_expunge_expired_locked()) { + return true; + } + } + + if (required_payload_size != 0 && !*retried_compact) { + if (zend_opcache_user_cache_compact_to_fit_locked(required_payload_size)) { + *retried_compact = true; + + return true; + } + } + + if (allow_clear && zend_opcache_user_cache_active_context()->clear_on_pressure && !*retried_clear) { + *retried_clear = true; + + return zend_opcache_user_cache_entry_locks_allow_clear_locked() && zend_opcache_user_cache_clear_locked(); + } + + return false; +} + +static bool zend_opcache_user_cache_store_prepared_locked_impl( + zend_string *key, + zval *value, + const zend_opcache_user_cache_prepared_value *prepared, + zend_long ttl, + bool throw_on_failure, + bool honor_strict_store_failure, + uint64_t *generation_ptr, + bool *seed_request_local_slot_ptr, + zend_opcache_user_cache_replaced_entry *replaced_entry_ptr, + bool allow_pressure_retries) +{ + const char *failure_message; + zend_opcache_user_cache_header *header; + zend_opcache_user_cache_entry *entries, *entry, old_entry; + zend_long new_long_value = 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_USER_CACHE_VALUE_NULL, new_value_type; + size_t key_payload_size, failed_payload_size; + bool found, retried_expired = false, retried_compact = false, retried_clear = false, allow_clear, old_combined, use_combined_publish, preserve_old_entry = false; + unsigned char *combined_payload; + double new_double_value = 0; + + ZVAL_DEREF(value); + + if (generation_ptr != NULL) { + *generation_ptr = 0; + } + + if (seed_request_local_slot_ptr != NULL) { + *seed_request_local_slot_ptr = false; + } + + if (replaced_entry_ptr != NULL) { + replaced_entry_ptr->found = false; + memset(&replaced_entry_ptr->entry, 0, sizeof(replaced_entry_ptr->entry)); + } + + if (prepared == NULL) { + return false; + } + + key_payload_size = ZSTR_LEN(key) + 1; + + zend_opcache_user_cache_maybe_rehash_locked(); + +retry_store: + old_key_offset = 0; + old_value_type = ZEND_OPCACHE_USER_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_user_cache_find_slot_for_write_locked(key, prepared->hash, &header, &slot_index, &found)) { + if (allow_pressure_retries && zend_opcache_user_cache_retry_store_after_pressure_locked(&retried_expired, &retried_compact, &retried_clear, 0, true)) { + goto retry_store; + } + + zend_opcache_user_cache_handle_store_failure("cache hash table is full", throw_on_failure, honor_strict_store_failure); + + return false; + } + + entries = zend_opcache_user_cache_entries(header); + entry = &entries[slot_index]; + + if (found) { + old_entry = *entry; + preserve_old_entry = replaced_entry_ptr != NULL; + 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_USER_CACHE_ENTRY_RESERVED_COMBINED_VALUE_KEY) != 0; + reusable_offset = found && !preserve_old_entry && old_value_type != ZEND_OPCACHE_USER_CACHE_VALUE_SHARED_GRAPH && !old_combined ? old_value_offset : 0; + new_key_offset = found ? old_key_offset : 0; + + use_combined_publish = zend_opcache_user_cache_value_uses_offset(prepared->value_type) && (!found || old_combined); + + /* Prepared shared graphs may overwrite a reusable combined block in place. */ + if (old_combined && old_value_offset != 0 && !preserve_old_entry) { + if (old_value_type == ZEND_OPCACHE_USER_CACHE_VALUE_SHARED_GRAPH) { + if (prepared->payload_source != NULL && + zend_opcache_user_cache_shared_graph_can_overwrite_payload_locked(old_value_offset) && + (prepared->value_type != ZEND_OPCACHE_USER_CACHE_VALUE_SHARED_GRAPH || + (prepared->payload_source != NULL && zend_opcache_user_cache_shared_graph_copy_fits_buffer( + prepared->payload_source, + prepared->payload_size, + prepared->payload_used_size, + (const unsigned char *) zend_opcache_user_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_user_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_user_cache_payload_can_fit_locked(key_payload_size); + + goto store_failed; + } + } + + switch (prepared->value_type) { + case ZEND_OPCACHE_USER_CACHE_VALUE_NULL: + new_value_type = ZEND_OPCACHE_USER_CACHE_VALUE_NULL; + + break; + case ZEND_OPCACHE_USER_CACHE_VALUE_TRUE: + new_value_type = ZEND_OPCACHE_USER_CACHE_VALUE_TRUE; + + break; + case ZEND_OPCACHE_USER_CACHE_VALUE_FALSE: + new_value_type = ZEND_OPCACHE_USER_CACHE_VALUE_FALSE; + + break; + case ZEND_OPCACHE_USER_CACHE_VALUE_LONG: + new_value_type = ZEND_OPCACHE_USER_CACHE_VALUE_LONG; + new_long_value = prepared->long_value; + + break; + case ZEND_OPCACHE_USER_CACHE_VALUE_DOUBLE: + new_value_type = ZEND_OPCACHE_USER_CACHE_VALUE_DOUBLE; + new_double_value = prepared->double_value; + + break; + case ZEND_OPCACHE_USER_CACHE_VALUE_STRING: + if (use_combined_publish) { + if (!zend_opcache_user_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_user_cache_payload_can_fit_locked(prepared->payload_size + key_payload_size); + + goto store_failed; + } + + new_reserved = ZEND_OPCACHE_USER_CACHE_ENTRY_RESERVED_COMBINED_VALUE_KEY; + } else { + offset = zend_opcache_user_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_user_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_USER_CACHE_VALUE_SHARED_GRAPH: + if (use_combined_publish) { + /* value_offset must point at the graph payload base. */ + combined_payload = zend_opcache_user_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) { + if (zend_opcache_user_cache_build_shared_graph_in_place( + value, + combined_payload, + prepared->payload_size, + NULL, + prepared->state_memo) + ) { + memcpy(combined_payload + prepared->payload_size, ZSTR_VAL(key), key_payload_size); + new_reserved = ZEND_OPCACHE_USER_CACHE_ENTRY_RESERVED_COMBINED_VALUE_KEY; + new_value_type = ZEND_OPCACHE_USER_CACHE_VALUE_SHARED_GRAPH; + new_value_offset = graph_offset; + new_value_len = prepared->value_len; + + break; + } + + if (graph_offset != combined_reusable_offset) { + zend_opcache_user_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 (allow_pressure_retries && + zend_opcache_user_cache_payload_can_fit_locked(prepared->payload_size + key_payload_size) && + zend_opcache_user_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_user_cache_alloc_locked(prepared->payload_size, NULL); + + if (graph_offset != 0) { + if (zend_opcache_user_cache_build_shared_graph_in_place( + value, + (unsigned char *) zend_opcache_user_cache_ptr(graph_offset), + prepared->payload_size, + NULL, + prepared->state_memo) + ) { + new_value_type = ZEND_OPCACHE_USER_CACHE_VALUE_SHARED_GRAPH; + new_value_offset = graph_offset; + new_value_len = prepared->value_len; + + break; + } + + zend_opcache_user_cache_free_locked(graph_offset); + + if (EG(exception)) { + return false; + } + } else if (allow_pressure_retries && + zend_opcache_user_cache_payload_can_fit_locked(prepared->payload_size) && + zend_opcache_user_cache_retry_store_after_pressure_locked(&retried_expired, &retried_compact, &retried_clear, prepared->payload_size, true) + ) { + goto retry_store; + } + } + + /* The shared-graph payload did not fit in shared memory. The build was + * already validated off-lock at prepare time, so a store-time failure is + * purely a memory shortfall, reached only after the expiry, compaction, + * and clear retries above. */ + failure_message = "not enough shared memory left"; + failed_payload_size = prepared->payload_size; + allow_clear = zend_opcache_user_cache_payload_can_fit_locked(prepared->payload_size); + + goto store_failed; + default: + ZEND_UNREACHABLE(); + } + + if (!found && entry->state == ZEND_OPCACHE_USER_CACHE_ENTRY_TOMBSTONE && header->tombstone_count != 0) { + header->tombstone_count--; + } + + 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_USER_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 (preserve_old_entry) { + replaced_entry_ptr->found = true; + replaced_entry_ptr->entry = old_entry; + } else { + if (found && old_key_offset != 0 && old_key_offset != new_key_offset && !old_combined) { + zend_opcache_user_cache_free_locked(old_key_offset); + } + + if (found && old_value_offset != 0 && old_value_offset != new_value_offset) { + zend_opcache_user_cache_release_value_storage_locked(old_value_type, old_value_offset); + } + } + + if (!found) { + header->count++; + } + + zend_opcache_user_cache_bump_mutation_epoch_locked(header); + + entry->generation = header->mutation_epoch; + if (generation_ptr != NULL) { + *generation_ptr = entry->generation; + } + + if (seed_request_local_slot_ptr != NULL) { + *seed_request_local_slot_ptr = zend_opcache_user_cache_prepared_value_can_seed_request_local_slot(prepared); + } + + return true; + +store_failed: + if (new_value_offset != 0 && new_value_offset != old_value_offset) { + zend_opcache_user_cache_free_locked(new_value_offset); + } + + if (new_key_offset != 0 && new_key_offset != old_key_offset && + (new_reserved & ZEND_OPCACHE_USER_CACHE_ENTRY_RESERVED_COMBINED_VALUE_KEY) == 0 + ) { + zend_opcache_user_cache_free_locked(new_key_offset); + } + + if (allow_pressure_retries && zend_opcache_user_cache_retry_store_after_pressure_locked(&retried_expired, &retried_compact, &retried_clear, failed_payload_size, allow_clear)) { + goto retry_store; + } + + zend_opcache_user_cache_handle_store_failure(failure_message, throw_on_failure, honor_strict_store_failure); + + return false; +} + +bool zend_opcache_user_cache_clear_locked(void) +{ + zend_opcache_user_cache_header *header = zend_opcache_user_cache_header_ptr(); + zend_opcache_user_cache_entry *entries; + uint64_t mutation_epoch; + uint32_t index; + + if (!header || !zend_opcache_user_cache_header_init_locked()) { + return false; + } + + mutation_epoch = header->mutation_epoch; + entries = zend_opcache_user_cache_entries(header); + for (index = 0; index < header->capacity; index++) { + if (entries[index].state == ZEND_OPCACHE_USER_CACHE_ENTRY_USED) { + zend_opcache_user_cache_release_entry_storage_locked(&entries[index]); + } + } + + memset(entries, 0, sizeof(zend_opcache_user_cache_entry) * header->capacity); + + zend_opcache_user_cache_reclaim_orphaned_graphs_locked(); + + header->count = 0; + header->tombstone_count = 0; + header->mutation_epoch = mutation_epoch + 1; + + /* Lookup-cache slots use 0 as their empty mutation epoch. */ + if (header->mutation_epoch == 0) { + header->mutation_epoch = 1; + } + + return true; +} + +bool zend_opcache_user_cache_prepare_value( + zend_opcache_user_cache_prepared_value *prepared, + zend_string *key, + zval *value, + bool throw_on_failure, + bool honor_strict_store_failure, + bool lock_held) +{ + size_t shared_graph_len; + + if (prepared == NULL) { + return false; + } + + zend_opcache_user_cache_init_prepared_value(prepared); + ZVAL_DEREF(value); + prepared->hash = zend_string_hash_val(key); + + if (Z_TYPE_P(value) == IS_RESOURCE) { + zend_opcache_user_cache_handle_store_failure( + "resources cannot be stored in the user cache", + throw_on_failure, + honor_strict_store_failure + ); + + return false; + } + + if (Z_TYPE_P(value) == IS_OBJECT && Z_OBJCE_P(value) == zend_ce_closure) { + zend_opcache_user_cache_handle_store_failure( + "Closure objects cannot be stored in the user cache", + throw_on_failure, + honor_strict_store_failure + ); + + return false; + } + + if (!zend_opcache_user_cache_validate_storable_value(value, throw_on_failure, honor_strict_store_failure)) { + return false; + } + + switch (Z_TYPE_P(value)) { + case IS_NULL: + prepared->value_type = ZEND_OPCACHE_USER_CACHE_VALUE_NULL; + + return true; + case IS_TRUE: + prepared->value_type = ZEND_OPCACHE_USER_CACHE_VALUE_TRUE; + + return true; + case IS_FALSE: + prepared->value_type = ZEND_OPCACHE_USER_CACHE_VALUE_FALSE; + + return true; + case IS_LONG: + prepared->value_type = ZEND_OPCACHE_USER_CACHE_VALUE_LONG; + prepared->long_value = Z_LVAL_P(value); + + return true; + case IS_DOUBLE: + prepared->value_type = ZEND_OPCACHE_USER_CACHE_VALUE_DOUBLE; + prepared->double_value = Z_DVAL_P(value); + + return true; + case IS_STRING: + if (Z_STRLEN_P(value) < ZEND_OPCACHE_USER_CACHE_DIRECT_STRING_MIN_LEN) { + prepared->value_type = ZEND_OPCACHE_USER_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; + } + + ZEND_FALLTHROUGH; + default: + shared_graph_len = 0; + + if (!lock_held && prepared->state_memo == NULL) { + prepared->state_memo = emalloc(sizeof(HashTable)); + zend_hash_init(prepared->state_memo, 8, NULL, ZVAL_PTR_DTOR, 0); + } + + if (zend_opcache_user_cache_calculate_shared_graph_size(value, &shared_graph_len, prepared->state_memo)) { + prepared->value_type = ZEND_OPCACHE_USER_CACHE_VALUE_SHARED_GRAPH; + prepared->value_len = (uint32_t) shared_graph_len; + prepared->payload_size = shared_graph_len; + + if (lock_held) { + return true; + } + + prepared->owned_buffer = emalloc(shared_graph_len); + if (zend_opcache_user_cache_build_shared_graph_in_place( + value, + prepared->owned_buffer, + shared_graph_len, + &prepared->payload_used_size, + prepared->state_memo + ) + ) { + prepared->payload_source = prepared->owned_buffer; + + return true; + } + + if (EG(exception)) { + return false; + } + + efree(prepared->owned_buffer); + prepared->owned_buffer = NULL; + } + + if (EG(exception)) { + return false; + } + + zend_opcache_user_cache_handle_store_failure( + "value cannot be stored in the user cache: it contains a resource, a closure, or an object with opaque internal state (e.g. Fiber, Generator, PDO)", + throw_on_failure, + honor_strict_store_failure + ); + + return false; + } +} + +void zend_opcache_user_cache_destroy_prepared_value(zend_opcache_user_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); + } + + if (prepared->state_memo != NULL) { + zend_hash_destroy(prepared->state_memo); + efree(prepared->state_memo); + } + + zend_opcache_user_cache_init_prepared_value(prepared); +} + +void zend_opcache_user_cache_store_request_local_value_slot(zend_string *key, uint64_t generation, zval *value, bool no_aliases) +{ + zend_opcache_user_cache_request_local_slot *slot; + zval slot_zv; + + ZVAL_DEREF(value); + + slot = emalloc(sizeof(zend_opcache_user_cache_request_local_slot)); + slot->generation = generation; + slot->needs_clone = false; + slot->has_array_clone_flags = false; + slot->no_aliases = no_aliases; + + ZVAL_UNDEF(&slot->value); + + if (!zend_opcache_user_cache_clone_request_local_slot_value(&slot->value, value, no_aliases)) { + ZVAL_PTR(&slot_zv, slot); + + zend_opcache_user_cache_request_local_slot_dtor(&slot_zv); + + return; + } + + if (Z_TYPE_P(&slot->value) == IS_ARRAY || Z_TYPE_P(&slot->value) == IS_OBJECT || Z_ISREF_P(&slot->value)) { + zend_hash_init(&slot->array_clone_flags, 8, NULL, NULL, 0); + slot->has_array_clone_flags = true; + slot->needs_clone = zend_opcache_user_cache_collect_request_local_array_clone_flags( + &slot->value, + &slot->array_clone_flags + ); + } else { + slot->needs_clone = false; + } + + zend_hash_update_ptr(zend_opcache_user_cache_request_local_slots(), key, slot); +} + +bool zend_opcache_user_cache_store_prepared_locked( + zend_string *key, + zval *value, + const zend_opcache_user_cache_prepared_value *prepared, + zend_long ttl, + bool throw_on_failure, + bool honor_strict_store_failure, + uint64_t *generation_ptr, + bool *seed_request_local_slot_ptr, + zend_opcache_user_cache_replaced_entry *replaced_entry_ptr, + bool allow_pressure_retries) +{ + bool stored = false; + + zend_try { + stored = zend_opcache_user_cache_store_prepared_locked_impl( + key, + value, + prepared, + ttl, + throw_on_failure, + honor_strict_store_failure, + generation_ptr, + seed_request_local_slot_ptr, + replaced_entry_ptr, + allow_pressure_retries + ); + } zend_catch { + zend_opcache_user_cache_unlock_if_held(); + zend_bailout(); + } zend_end_try(); + + return stored; +} + +bool zend_opcache_user_cache_store_locked(zend_string *key, zval *value, zend_long ttl, bool throw_on_failure, bool honor_strict_store_failure) +{ + const char *cache_name = zend_opcache_user_cache_active_context()->name; + zend_opcache_user_cache_prepared_value prepared; + bool stored; + + /* Preserve the caller's lock contract around preparation. */ + zend_opcache_user_cache_unlock(); + if (!zend_opcache_user_cache_prepare_value(&prepared, key, value, throw_on_failure, honor_strict_store_failure, false)) { + zend_opcache_user_cache_destroy_prepared_value(&prepared); + zend_opcache_user_cache_reacquire_write_lock_or_fail(cache_name); + + return false; + } + + if (!zend_opcache_user_cache_reacquire_write_lock_or_fail(cache_name)) { + zend_opcache_user_cache_destroy_prepared_value(&prepared); + + return false; + } + + stored = zend_opcache_user_cache_store_prepared_locked( + key, + value, + &prepared, + ttl, + throw_on_failure, + honor_strict_store_failure, + NULL, + NULL, + NULL, + true + ); + + zend_opcache_user_cache_destroy_prepared_value(&prepared); + + return stored; +} + +bool zend_opcache_user_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_user_cache_active_context()->name; + zend_opcache_user_cache_header *header; + zend_opcache_user_cache_entry *entries, *entry; + zend_opcache_user_cache_lookup_entry *lookup_entries, *lookup_entry; + zend_opcache_user_cache_request_local_slot_result slot_result; + zend_ulong hash; + uint64_t mutation_epoch, generation, now; + uint32_t way, slot_index; + bool found, use_proto, no_aliases; + + if (found_ptr != NULL) { + *found_ptr = false; + } + + hash = zend_string_hash_val(key); + + header = zend_opcache_user_cache_header_ptr(); + if (!header || !zend_opcache_user_cache_header_is_initialized_locked()) { + if (throw_if_missing) { + zend_opcache_user_cache_throw_missing_key_locked(key); + } + + return false; + } + + entries = zend_opcache_user_cache_entries(header); + mutation_epoch = header->mutation_epoch; + lookup_entries = zend_opcache_user_cache_lookup_cache_set(hash); + + for (way = 0; way < ZEND_OPCACHE_USER_CACHE_LOOKUP_WAYS; way++) { + lookup_entry = &lookup_entries[way]; + if (lookup_entry->state == ZEND_OPCACHE_USER_CACHE_LOOKUP_EMPTY || lookup_entry->hash != hash) { + continue; + } + + if (lookup_entry->mutation_epoch != mutation_epoch) { + /* Stale hits are revalidated below; stale misses are dropped. */ + if (lookup_entry->state != ZEND_OPCACHE_USER_CACHE_LOOKUP_HIT) { + zend_opcache_user_cache_lookup_cache_reset_entry(lookup_entry); + continue; + } + } else { + if (lookup_entry->state == ZEND_OPCACHE_USER_CACHE_LOOKUP_MISS) { + if (throw_if_missing) { + zend_opcache_user_cache_throw_missing_key_locked(key); + } + + return false; + } + + if (lookup_entry->key == key && + lookup_entry->value_type != ZEND_OPCACHE_USER_CACHE_LOOKUP_VALUE_NONE + ) { + switch (lookup_entry->value_type) { + case ZEND_OPCACHE_USER_CACHE_VALUE_NULL: + ZVAL_NULL(return_value); + break; + case ZEND_OPCACHE_USER_CACHE_VALUE_TRUE: + ZVAL_TRUE(return_value); + break; + case ZEND_OPCACHE_USER_CACHE_VALUE_FALSE: + ZVAL_FALSE(return_value); + break; + case ZEND_OPCACHE_USER_CACHE_VALUE_LONG: + ZVAL_LONG(return_value, lookup_entry->long_value); + break; + default: + ZVAL_DOUBLE(return_value, lookup_entry->double_value); + break; + } + + if (found_ptr != NULL) { + *found_ptr = true; + } + + return true; + } + } + + if (lookup_entry->slot_index >= header->capacity) { + zend_opcache_user_cache_lookup_cache_reset_entry(lookup_entry); + continue; + } + + entry = &entries[lookup_entry->slot_index]; + if (!zend_opcache_user_cache_key_equals(entry, key, hash)) { + zend_opcache_user_cache_lookup_cache_reset_entry(lookup_entry); + continue; + } + + now = 0; + if (zend_opcache_user_cache_maybe_expired(entry, &now)) { + zend_opcache_user_cache_lookup_cache_reset_entry(lookup_entry); + zend_opcache_user_cache_lookup_cache_store_miss(lookup_entries, hash, mutation_epoch); + if (throw_if_missing) { + zend_opcache_user_cache_throw_missing_key_locked(key); + } + + return false; + } + + slot_index = lookup_entry->slot_index; + + /* Revived stale hit (or a hit recorded without this key instance): + * refresh the epoch and refresh the inline value. */ + if (lookup_entry->mutation_epoch != mutation_epoch || lookup_entry->key != key) { + zend_opcache_user_cache_lookup_cache_store_hit(lookup_entry, hash, mutation_epoch, slot_index); + zend_opcache_user_cache_lookup_cache_fill_hit_value(lookup_entry, key, entry); + } + + found = true; + + goto value_found; + } + + if (!zend_opcache_user_cache_find_slot_in_header_locked(header, key, hash, &slot_index, &found, false) || !found) { + zend_opcache_user_cache_lookup_cache_store_miss(lookup_entries, hash, mutation_epoch); + if (throw_if_missing) { + zend_opcache_user_cache_throw_missing_key_locked(key); + } + + return false; + } + + lookup_entry = zend_opcache_user_cache_lookup_cache_select_slot(lookup_entries, hash, mutation_epoch, true); + if (lookup_entry != NULL) { + zend_opcache_user_cache_lookup_cache_store_hit(lookup_entry, hash, mutation_epoch, slot_index); + zend_opcache_user_cache_lookup_cache_fill_hit_value(lookup_entry, key, &entries[slot_index]); + } + +value_found: + entry = &entries[slot_index]; + generation = entry->generation; + + if (found_ptr != NULL) { + *found_ptr = true; + } + + use_proto = use_request_local_slot; + no_aliases = true; + if (entry->value_type == ZEND_OPCACHE_USER_CACHE_VALUE_SHARED_GRAPH) { + if (use_proto) { + use_proto = zend_opcache_user_cache_shared_graph_requires_prototype(entry->value_offset); + } + if (use_proto) { + no_aliases = !zend_opcache_user_cache_shared_graph_payload_has_aliases(entry->value_offset); + } + } + + if (use_proto) { + slot_result = zend_opcache_user_cache_fetch_request_local_slot_locked(key, generation, return_value); + if (slot_result == ZEND_OPCACHE_USER_CACHE_REQUEST_LOCAL_SLOT_HIT) { + return true; + } + + if (EG(exception)) { + return false; + } + } + + switch (entry->value_type) { + case ZEND_OPCACHE_USER_CACHE_VALUE_NULL: + ZVAL_NULL(return_value); + return zend_opcache_user_cache_fetch_finish_locked(key, generation, return_value, use_proto, no_aliases); + case ZEND_OPCACHE_USER_CACHE_VALUE_TRUE: + ZVAL_TRUE(return_value); + return zend_opcache_user_cache_fetch_finish_locked(key, generation, return_value, use_proto, no_aliases); + case ZEND_OPCACHE_USER_CACHE_VALUE_FALSE: + ZVAL_FALSE(return_value); + return zend_opcache_user_cache_fetch_finish_locked(key, generation, return_value, use_proto, no_aliases); + case ZEND_OPCACHE_USER_CACHE_VALUE_LONG: + ZVAL_LONG(return_value, entry->long_value); + return zend_opcache_user_cache_fetch_finish_locked(key, generation, return_value, use_proto, no_aliases); + case ZEND_OPCACHE_USER_CACHE_VALUE_DOUBLE: + ZVAL_DOUBLE(return_value, entry->double_value); + return zend_opcache_user_cache_fetch_finish_locked(key, generation, return_value, use_proto, no_aliases); + case ZEND_OPCACHE_USER_CACHE_VALUE_STRING: + zend_opcache_user_cache_zval_stringl_locked(return_value, zend_opcache_user_cache_ptr(entry->value_offset), entry->value_len); + return zend_opcache_user_cache_fetch_finish_locked(key, generation, return_value, use_proto, no_aliases); + case ZEND_OPCACHE_USER_CACHE_VALUE_SHARED_GRAPH: + if (!zend_opcache_user_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_user_cache_fetch_finish_locked(key, generation, return_value, use_proto, no_aliases); + default: + if (throw_if_missing) { + zend_opcache_user_cache_throw_unknown_type_locked(cache_name, key); + } + + return false; + } +} + +bool zend_opcache_user_cache_exists_locked(zend_string *key) +{ + zend_ulong hash = zend_string_hash_val(key); + uint32_t slot_index; + bool found; + + if (!zend_opcache_user_cache_find_slot_for_read_locked(key, hash, NULL, &slot_index, &found)) { + return false; + } + + return found; +} + +void zend_opcache_user_cache_discard_replaced_entry_locked( + zend_string *key, + zend_opcache_user_cache_replaced_entry *replaced_entry) +{ + zend_opcache_user_cache_header *header; + zend_opcache_user_cache_entry *entries, *current_entry = NULL; + uint32_t slot_index; + bool found; + + if (replaced_entry == NULL || !replaced_entry->found) { + return; + } + + if (zend_opcache_user_cache_find_slot_exact_locked( + key, + replaced_entry->entry.hash, + &header, + &slot_index, + &found + ) && + found + ) { + entries = zend_opcache_user_cache_entries(header); + current_entry = &entries[slot_index]; + } + + zend_opcache_user_cache_release_entry_storage_except_locked(&replaced_entry->entry, current_entry); + replaced_entry->found = false; + memset(&replaced_entry->entry, 0, sizeof(replaced_entry->entry)); +} + +void zend_opcache_user_cache_rollback_replaced_entry_locked( + zend_string *key, + zend_opcache_user_cache_replaced_entry *replaced_entry) +{ + zend_opcache_user_cache_header *header; + zend_opcache_user_cache_entry *entries, *entry; + uint32_t slot_index; + bool found; + + if (!zend_opcache_user_cache_find_slot_exact_locked( + key, + zend_string_hash_val(key), + &header, + &slot_index, + &found) + ) { + return; + } + + entries = zend_opcache_user_cache_entries(header); + entry = &entries[slot_index]; + + if (replaced_entry != NULL && replaced_entry->found) { + if (found) { + zend_opcache_user_cache_release_entry_storage_except_locked(entry, &replaced_entry->entry); + } else { + if (entry->state == ZEND_OPCACHE_USER_CACHE_ENTRY_TOMBSTONE && header->tombstone_count != 0) { + header->tombstone_count--; + } + header->count++; + } + + *entry = replaced_entry->entry; + replaced_entry->found = false; + memset(&replaced_entry->entry, 0, sizeof(replaced_entry->entry)); + } else if (found) { + zend_opcache_user_cache_delete_entry_locked(entry, header); + } +} + +bool zend_opcache_user_cache_delete_locked(zend_string *key) +{ + zend_opcache_user_cache_header *header; + zend_opcache_user_cache_entry *entries; + zend_ulong hash = zend_string_hash_val(key); + uint32_t slot_index; + bool found; + + if (!zend_opcache_user_cache_find_slot_for_write_locked(key, hash, &header, &slot_index, &found) || !found) { + return true; + } + + entries = zend_opcache_user_cache_entries(header); + zend_opcache_user_cache_delete_entry_locked(&entries[slot_index], header); + + return true; +} + +void zend_opcache_user_cache_delete_entries_by_prefix_locked(zend_string *prefix) +{ + zend_opcache_user_cache_header *header; + zend_opcache_user_cache_entry *entries, *entry; + uint32_t index; + + header = zend_opcache_user_cache_header_ptr(); + if (!header || !zend_opcache_user_cache_header_is_initialized_locked()) { + return; + } + + entries = zend_opcache_user_cache_entries(header); + for (index = 0; index < header->capacity; index++) { + entry = &entries[index]; + if (entry->state != ZEND_OPCACHE_USER_CACHE_ENTRY_USED || + entry->key_len < ZSTR_LEN(prefix) || + memcmp(zend_opcache_user_cache_ptr(entry->key_offset), ZSTR_VAL(prefix), ZSTR_LEN(prefix)) != 0 + ) { + continue; + } + + zend_opcache_user_cache_delete_entry_locked(entry, header); + } +} + +bool zend_opcache_user_cache_atomic_update_locked( + zend_string *key, + zend_long step, + zend_long ttl, + bool decrement, + bool insert_if_missing, + zend_long *new_value, + bool *is_overflow) +{ + zend_opcache_user_cache_header *header; + zend_opcache_user_cache_entry *entries, *entry; + zend_opcache_user_cache_prepared_value prepared; + zend_ulong hash = zend_string_hash_val(key); + zend_long result; + zval initial_value = {0}; + uint32_t slot_index; + bool found, result_is_overflow, stored; + + *is_overflow = false; + + if (!zend_opcache_user_cache_find_slot_for_write_locked(key, hash, &header, &slot_index, &found) || !found) { + if (insert_if_missing) { + result_is_overflow = decrement + ? zend_opcache_user_cache_long_sub_wrapped(0, step, &result) + : zend_opcache_user_cache_long_add_wrapped(0, step, &result) + ; + if (result_is_overflow) { + *is_overflow = true; + + return false; + } + + ZVAL_LONG(&initial_value, result); + if (!zend_opcache_user_cache_prepare_value(&prepared, key, &initial_value, false, false, true)) { + zend_opcache_user_cache_destroy_prepared_value(&prepared); + + return false; + } + + stored = zend_opcache_user_cache_store_prepared_locked( + key, + &initial_value, + &prepared, + ttl, + false, + false, + NULL, + NULL, + NULL, + true + ); + zend_opcache_user_cache_destroy_prepared_value(&prepared); + if (stored) { + *new_value = Z_LVAL(initial_value); + + return true; + } + + return false; + } + + return false; + } + + entries = zend_opcache_user_cache_entries(header); + entry = &entries[slot_index]; + if (entry->value_type != ZEND_OPCACHE_USER_CACHE_VALUE_LONG) { + return false; + } + + result_is_overflow = decrement + ? zend_opcache_user_cache_long_sub_wrapped(entry->long_value, step, &result) + : zend_opcache_user_cache_long_add_wrapped(entry->long_value, step, &result) + ; + if (result_is_overflow) { + *is_overflow = true; + + return false; + } + + entry->long_value = result; + + zend_opcache_user_cache_bump_mutation_epoch_locked(header); + entry->generation = header->mutation_epoch; + + *new_value = entry->long_value; + + return true; +} + +void zend_opcache_user_cache_release_request_local_slots(void) +{ + zend_opcache_user_cache_release_request_local_slot_context(&zend_opcache_user_cache_request_local_slot_table); +} + +void zend_opcache_user_cache_release_active_request_local_slots(void) +{ + zend_opcache_user_cache_release_request_local_slot_context(zend_opcache_user_cache_active_request_local_slots_ptr()); +} + +void zend_opcache_user_cache_release_active_request_local_slots_by_prefix(zend_string *prefix) +{ + HashTable **slots_ptr = zend_opcache_user_cache_active_request_local_slots_ptr(); + zend_string **keys; + zend_string *key; + uint32_t slot_count, count = 0, index; + + if (*slots_ptr == NULL) { + return; + } + + slot_count = zend_hash_num_elements(*slots_ptr); + if (slot_count == 0) { + zend_hash_destroy(*slots_ptr); + FREE_HASHTABLE(*slots_ptr); + + *slots_ptr = NULL; + + return; + } + + keys = safe_emalloc(slot_count, sizeof(zend_string *), 0); + ZEND_HASH_FOREACH_STR_KEY(*slots_ptr, key) { + if (key != NULL && + ZSTR_LEN(key) >= ZSTR_LEN(prefix) && + memcmp(ZSTR_VAL(key), ZSTR_VAL(prefix), ZSTR_LEN(prefix)) == 0 + ) { + keys[count++] = zend_string_copy(key); + } + } ZEND_HASH_FOREACH_END(); + + for (index = 0; index < count; index++) { + zend_hash_del(*slots_ptr, keys[index]); + zend_string_release(keys[index]); + } + efree(keys); + + if (zend_hash_num_elements(*slots_ptr) == 0) { + zend_hash_destroy(*slots_ptr); + FREE_HASHTABLE(*slots_ptr); + + *slots_ptr = NULL; + } +} + +#ifdef ZEND_OPCACHE_USER_CACHE_HAVE_OPTIMISTIC +/* Optimistic reads are bracketed by write_seq. Shared graphs are pinned inside + * a reader window before decoding. */ + +zend_opcache_user_cache_optimistic_result zend_opcache_user_cache_fetch_optimistic( + zend_string *key, + zval *return_value) +{ + zend_opcache_user_cache_header *header; + zend_opcache_user_cache_entry snapshot; + zend_opcache_user_cache_lookup_entry *lookup_entries, *lookup_entry; + zend_string *string_value; + zend_ulong hash; + uint64_t seq, mutation_epoch; + uint32_t way, slot_index = 0, hint_slot = UINT32_MAX, reader_slot = 0; + bool found = false, have_snapshot = false, use_proto, no_aliases, ref_registered; + + if (!zend_opcache_user_cache_optimistic_header(&header, &seq)) { + return ZEND_OPCACHE_USER_CACHE_OPTIMISTIC_FALLBACK; + } + + mutation_epoch = header->mutation_epoch; + hash = zend_string_hash_val(key); + lookup_entries = zend_opcache_user_cache_lookup_cache_set(hash); + + /* Request-local lookup cache: only epoch-current records are used here; + * stale-hint revival is left to the locked path. The epoch itself is part + * of the snapshot and is validated by the sequence reloads below. */ + for (way = 0; way < ZEND_OPCACHE_USER_CACHE_LOOKUP_WAYS; way++) { + lookup_entry = &lookup_entries[way]; + + if (lookup_entry->state == ZEND_OPCACHE_USER_CACHE_LOOKUP_EMPTY || + lookup_entry->hash != hash || + lookup_entry->mutation_epoch != mutation_epoch + ) { + continue; + } + + if (lookup_entry->state == ZEND_OPCACHE_USER_CACHE_LOOKUP_MISS) { + if (zend_opcache_user_cache_seq_reload(&header->write_seq) != seq) { + return ZEND_OPCACHE_USER_CACHE_OPTIMISTIC_FALLBACK; + } + + return ZEND_OPCACHE_USER_CACHE_OPTIMISTIC_MISS; + } + + if (lookup_entry->key == key && + lookup_entry->value_type != ZEND_OPCACHE_USER_CACHE_LOOKUP_VALUE_NONE + ) { + switch (lookup_entry->value_type) { + case ZEND_OPCACHE_USER_CACHE_VALUE_NULL: + ZVAL_NULL(return_value); + break; + case ZEND_OPCACHE_USER_CACHE_VALUE_TRUE: + ZVAL_TRUE(return_value); + break; + case ZEND_OPCACHE_USER_CACHE_VALUE_FALSE: + ZVAL_FALSE(return_value); + break; + case ZEND_OPCACHE_USER_CACHE_VALUE_LONG: + ZVAL_LONG(return_value, lookup_entry->long_value); + break; + default: + ZVAL_DOUBLE(return_value, lookup_entry->double_value); + break; + } + + if (zend_opcache_user_cache_seq_reload(&header->write_seq) != seq) { + return ZEND_OPCACHE_USER_CACHE_OPTIMISTIC_FALLBACK; + } + + return ZEND_OPCACHE_USER_CACHE_OPTIMISTIC_FOUND; + } + + hint_slot = lookup_entry->slot_index; + + break; + } + + /* A current-epoch hit names the exact slot; validate it directly before + * resorting to the full probe. */ + if (hint_slot != UINT32_MAX && hint_slot < header->capacity) { + const zend_opcache_user_cache_entry *hint_entry = &zend_opcache_user_cache_entries(header)[hint_slot]; + + if (hint_entry->state == ZEND_OPCACHE_USER_CACHE_ENTRY_USED && + hint_entry->hash == hash && + hint_entry->key_len == ZSTR_LEN(key) && + zend_opcache_user_cache_optimistic_payload_in_bounds(header, hint_entry->key_offset, hint_entry->key_len) && + memcmp(zend_opcache_user_cache_ptr(hint_entry->key_offset), ZSTR_VAL(key), ZSTR_LEN(key)) == 0 && + (hint_entry->expires_at == 0 || hint_entry->expires_at > (uint64_t) time(NULL)) + ) { + snapshot = *hint_entry; + slot_index = hint_slot; + found = true; + have_snapshot = true; + } + } + + if (!have_snapshot && + !zend_opcache_user_cache_optimistic_probe( + header, + zend_opcache_user_cache_entries(header), + key, + hash, + &snapshot, + &slot_index, + &found) + ) { + return ZEND_OPCACHE_USER_CACHE_OPTIMISTIC_FALLBACK; + } + + if (zend_opcache_user_cache_seq_reload(&header->write_seq) != seq) { + return ZEND_OPCACHE_USER_CACHE_OPTIMISTIC_FALLBACK; + } + + /* The probe outcome is now a consistent snapshot. */ + + if (!found) { + zend_opcache_user_cache_lookup_cache_store_miss(lookup_entries, hash, mutation_epoch); + + return ZEND_OPCACHE_USER_CACHE_OPTIMISTIC_MISS; + } + + lookup_entry = zend_opcache_user_cache_lookup_cache_select_slot(lookup_entries, hash, mutation_epoch, true); + if (lookup_entry != NULL) { + zend_opcache_user_cache_lookup_cache_store_hit(lookup_entry, hash, mutation_epoch, slot_index); + zend_opcache_user_cache_lookup_cache_fill_hit_value(lookup_entry, key, &snapshot); + } + + switch (snapshot.value_type) { + case ZEND_OPCACHE_USER_CACHE_VALUE_NULL: + ZVAL_NULL(return_value); + + return ZEND_OPCACHE_USER_CACHE_OPTIMISTIC_FOUND; + case ZEND_OPCACHE_USER_CACHE_VALUE_TRUE: + ZVAL_TRUE(return_value); + + return ZEND_OPCACHE_USER_CACHE_OPTIMISTIC_FOUND; + case ZEND_OPCACHE_USER_CACHE_VALUE_FALSE: + ZVAL_FALSE(return_value); + + return ZEND_OPCACHE_USER_CACHE_OPTIMISTIC_FOUND; + case ZEND_OPCACHE_USER_CACHE_VALUE_LONG: + ZVAL_LONG(return_value, snapshot.long_value); + + return ZEND_OPCACHE_USER_CACHE_OPTIMISTIC_FOUND; + case ZEND_OPCACHE_USER_CACHE_VALUE_DOUBLE: + ZVAL_DOUBLE(return_value, snapshot.double_value); + + return ZEND_OPCACHE_USER_CACHE_OPTIMISTIC_FOUND; + case ZEND_OPCACHE_USER_CACHE_VALUE_STRING: + if (zend_opcache_user_cache_fetch_request_local_slot(key, snapshot.generation, return_value) == + ZEND_OPCACHE_USER_CACHE_REQUEST_LOCAL_SLOT_HIT + ) { + return ZEND_OPCACHE_USER_CACHE_OPTIMISTIC_FOUND; + } + + if (!zend_opcache_user_cache_optimistic_payload_in_bounds(header, snapshot.value_offset, snapshot.value_len)) { + return ZEND_OPCACHE_USER_CACHE_OPTIMISTIC_FALLBACK; + } + + string_value = zend_string_init( + zend_opcache_user_cache_ptr(snapshot.value_offset), + snapshot.value_len, + 0 + ); + + /* The payload may have been moved or freed mid-copy; the copy is + * private memory, so a moved sequence just discards it. */ + if (zend_opcache_user_cache_seq_reload(&header->write_seq) != seq) { + zend_string_release(string_value); + + return ZEND_OPCACHE_USER_CACHE_OPTIMISTIC_FALLBACK; + } + + ZVAL_STR(return_value, string_value); + zend_opcache_user_cache_fetch_finish(key, snapshot.generation, return_value, true, true); + + return ZEND_OPCACHE_USER_CACHE_OPTIMISTIC_FOUND; + case ZEND_OPCACHE_USER_CACHE_VALUE_SHARED_GRAPH: + break; + default: + return ZEND_OPCACHE_USER_CACHE_OPTIMISTIC_FALLBACK; + } + + /* Shared graph. Read the graph header flags tentatively, then revalidate. */ + if (!zend_opcache_user_cache_optimistic_payload_in_bounds( + header, + snapshot.value_offset, + sizeof(zend_opcache_user_cache_shared_graph_header) + ZEND_MM_ALIGNMENT) || + !zend_opcache_user_cache_optimistic_payload_in_bounds(header, snapshot.value_offset, snapshot.value_len) + ) { + return ZEND_OPCACHE_USER_CACHE_OPTIMISTIC_FALLBACK; + } + + use_proto = zend_opcache_user_cache_shared_graph_requires_prototype(snapshot.value_offset); + no_aliases = !zend_opcache_user_cache_shared_graph_payload_has_aliases(snapshot.value_offset); + + if (zend_opcache_user_cache_seq_reload(&header->write_seq) != seq) { + return ZEND_OPCACHE_USER_CACHE_OPTIMISTIC_FALLBACK; + } + + if (use_proto && + zend_opcache_user_cache_fetch_request_local_slot(key, snapshot.generation, return_value) == + ZEND_OPCACHE_USER_CACHE_REQUEST_LOCAL_SLOT_HIT + ) { + return ZEND_OPCACHE_USER_CACHE_OPTIMISTIC_FOUND; + } + + ref_registered = zend_opcache_user_cache_has_request_shared_graph_ref(snapshot.value_offset); + + if (!ref_registered) { + /* Pin registration must not allocate inside the reader window. */ + zend_opcache_user_cache_shared_graph_ref_reserve(); + + if (!zend_opcache_user_cache_optimistic_reader_begin(header, &reader_slot)) { + return ZEND_OPCACHE_USER_CACHE_OPTIMISTIC_FALLBACK; + } + + if (zend_opcache_user_cache_seq_reload(&header->write_seq) != seq) { + zend_opcache_user_cache_optimistic_reader_end(header, reader_slot); + + return ZEND_OPCACHE_USER_CACHE_OPTIMISTIC_FALLBACK; + } + + if (!zend_opcache_user_cache_shared_graph_acquire_locked(snapshot.value_offset)) { + zend_opcache_user_cache_optimistic_reader_end(header, reader_slot); + + return ZEND_OPCACHE_USER_CACHE_OPTIMISTIC_FALLBACK; + } + + zend_opcache_user_cache_register_shared_graph_ref(snapshot.value_offset); + zend_opcache_user_cache_optimistic_reader_end(header, reader_slot); + } + + ZVAL_UNDEF(return_value); + + if (!zend_opcache_user_cache_fetch_shared_graph( + (const unsigned char *) zend_opcache_user_cache_ptr(snapshot.value_offset), + snapshot.value_len, + return_value) + ) { + if (Z_TYPE_P(return_value) != IS_UNDEF) { + zval_ptr_dtor(return_value); + ZVAL_UNDEF(return_value); + } + + /* Canonical error handling (corruption exceptions) is the locked + * path's job; the caller skips the retry when an exception is already + * pending. */ + return ZEND_OPCACHE_USER_CACHE_OPTIMISTIC_FALLBACK; + } + + zend_opcache_user_cache_fetch_finish(key, snapshot.generation, return_value, use_proto, no_aliases); + + return ZEND_OPCACHE_USER_CACHE_OPTIMISTIC_FOUND; +} + +zend_opcache_user_cache_optimistic_result zend_opcache_user_cache_exists_optimistic(zend_string *key) +{ + zend_opcache_user_cache_header *header; + zend_opcache_user_cache_entry snapshot; + zend_opcache_user_cache_lookup_entry *lookup_entries, *lookup_entry; + zend_ulong hash; + uint64_t seq, mutation_epoch; + uint32_t way, slot_index = 0; + bool found = false; + + if (!zend_opcache_user_cache_optimistic_header(&header, &seq)) { + return ZEND_OPCACHE_USER_CACHE_OPTIMISTIC_FALLBACK; + } + + mutation_epoch = header->mutation_epoch; + hash = zend_string_hash_val(key); + lookup_entries = zend_opcache_user_cache_lookup_cache_set(hash); + + for (way = 0; way < ZEND_OPCACHE_USER_CACHE_LOOKUP_WAYS; way++) { + lookup_entry = &lookup_entries[way]; + + if (lookup_entry->state == ZEND_OPCACHE_USER_CACHE_LOOKUP_EMPTY || + lookup_entry->hash != hash || + lookup_entry->mutation_epoch != mutation_epoch + ) { + continue; + } + + /* Hits may have expired without an epoch bump. */ + if (lookup_entry->state == ZEND_OPCACHE_USER_CACHE_LOOKUP_MISS) { + if (zend_opcache_user_cache_seq_reload(&header->write_seq) != seq) { + return ZEND_OPCACHE_USER_CACHE_OPTIMISTIC_FALLBACK; + } + + return ZEND_OPCACHE_USER_CACHE_OPTIMISTIC_MISS; + } + + break; + } + + if (!zend_opcache_user_cache_optimistic_probe( + header, + zend_opcache_user_cache_entries(header), + key, + hash, + &snapshot, + &slot_index, + &found) + ) { + return ZEND_OPCACHE_USER_CACHE_OPTIMISTIC_FALLBACK; + } + + if (zend_opcache_user_cache_seq_reload(&header->write_seq) != seq) { + return ZEND_OPCACHE_USER_CACHE_OPTIMISTIC_FALLBACK; + } + + if (!found) { + zend_opcache_user_cache_lookup_cache_store_miss(lookup_entries, hash, mutation_epoch); + + return ZEND_OPCACHE_USER_CACHE_OPTIMISTIC_MISS; + } + + lookup_entry = zend_opcache_user_cache_lookup_cache_select_slot(lookup_entries, hash, mutation_epoch, true); + if (lookup_entry != NULL) { + zend_opcache_user_cache_lookup_cache_store_hit(lookup_entry, hash, mutation_epoch, slot_index); + zend_opcache_user_cache_lookup_cache_fill_hit_value(lookup_entry, key, &snapshot); + } + + return ZEND_OPCACHE_USER_CACHE_OPTIMISTIC_FOUND; +} +#else +zend_opcache_user_cache_optimistic_result zend_opcache_user_cache_fetch_optimistic( + zend_string *key, + zval *return_value) +{ + (void) key; + (void) return_value; + + return ZEND_OPCACHE_USER_CACHE_OPTIMISTIC_FALLBACK; +} + +zend_opcache_user_cache_optimistic_result zend_opcache_user_cache_exists_optimistic(zend_string *key) +{ + (void) key; + + return ZEND_OPCACHE_USER_CACHE_OPTIMISTIC_FALLBACK; +} +#endif diff --git a/ext/opcache/zend_user_cache_internal.h b/ext/opcache/zend_user_cache_internal.h new file mode 100644 index 000000000000..1f4de7d40631 --- /dev/null +++ b/ext/opcache/zend_user_cache_internal.h @@ -0,0 +1,1074 @@ +/* + +----------------------------------------------------------------------+ + | 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_USER_CACHE_INTERNAL_H +#define ZEND_USER_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_user_cache.h" + +#include "ext/standard/php_var.h" + +#include "SAPI.h" + +#define ZEND_OPCACHE_USER_CACHE_MAGIC 0xCAC17E01U +#define ZEND_OPCACHE_USER_CACHE_VERSION 1U +#define ZEND_OPCACHE_USER_CACHE_MIN_CAPACITY 127U +#define ZEND_OPCACHE_USER_CACHE_MAX_CAPACITY 65521U +#define ZEND_OPCACHE_USER_CACHE_SLOT_BYTES 256U +/* Storage-key component separator. */ +#define ZEND_OPCACHE_USER_CACHE_KEY_DELIMITER "\x1f" +#define ZEND_OPCACHE_USER_CACHE_KEY_DELIMITER_CHAR '\x1f' +#define ZEND_OPCACHE_USER_CACHE_KEY_DELIMITER_NAME "0x1F" + +#define ZEND_OPCACHE_USER_CACHE_ENTRY_EMPTY 0 +#define ZEND_OPCACHE_USER_CACHE_ENTRY_USED 1 +#define ZEND_OPCACHE_USER_CACHE_ENTRY_TOMBSTONE 2 + +#define ZEND_OPCACHE_USER_CACHE_VALUE_NULL 0 +#define ZEND_OPCACHE_USER_CACHE_VALUE_TRUE 1 +#define ZEND_OPCACHE_USER_CACHE_VALUE_FALSE 2 +#define ZEND_OPCACHE_USER_CACHE_VALUE_LONG 3 +#define ZEND_OPCACHE_USER_CACHE_VALUE_DOUBLE 4 +#define ZEND_OPCACHE_USER_CACHE_VALUE_STRING 5 +#define ZEND_OPCACHE_USER_CACHE_VALUE_SHARED_GRAPH 8 + +#define ZEND_OPCACHE_USER_CACHE_SHARED_GRAPH_MAGIC 0xCAC17E02U +#define ZEND_OPCACHE_USER_CACHE_SHARED_GRAPH_VERSION 9U +/* Graph needs per-decode identity maps. */ +#define ZEND_OPCACHE_USER_CACHE_SHARED_GRAPH_FLAG_HAS_SHARED_IDENTITY 0x2U +/* Decode may resolve a class and must run off-lock. */ +#define ZEND_OPCACHE_USER_CACHE_SHARED_GRAPH_FLAG_HAS_OBJECT 0x4U +/* Prefer request-local prototype cloning on repeated fetches. */ +#define ZEND_OPCACHE_USER_CACHE_SHARED_GRAPH_FLAG_PREFERS_PROTOTYPE 0x8U +#define ZEND_OPCACHE_USER_CACHE_SHARED_GRAPH_RETIRED (1 << 30) +#define ZEND_OPCACHE_USER_CACHE_SHARED_GRAPH_REFCOUNT_MASK (ZEND_OPCACHE_USER_CACHE_SHARED_GRAPH_RETIRED - 1) + +#define ZEND_OPCACHE_USER_CACHE_SHARED_GRAPH_VALUE_UNDEF 0 +#define ZEND_OPCACHE_USER_CACHE_SHARED_GRAPH_VALUE_NULL 1 +#define ZEND_OPCACHE_USER_CACHE_SHARED_GRAPH_VALUE_TRUE 2 +#define ZEND_OPCACHE_USER_CACHE_SHARED_GRAPH_VALUE_FALSE 3 +#define ZEND_OPCACHE_USER_CACHE_SHARED_GRAPH_VALUE_LONG 4 +#define ZEND_OPCACHE_USER_CACHE_SHARED_GRAPH_VALUE_DOUBLE 5 +#define ZEND_OPCACHE_USER_CACHE_SHARED_GRAPH_VALUE_STRING 6 +#define ZEND_OPCACHE_USER_CACHE_SHARED_GRAPH_VALUE_ARRAY 7 +#define ZEND_OPCACHE_USER_CACHE_SHARED_GRAPH_VALUE_OBJECT 8 +#define ZEND_OPCACHE_USER_CACHE_SHARED_GRAPH_VALUE_DYNAMIC_ARRAY 10 +/* Back-reference to an OBJECT node. */ +#define ZEND_OPCACHE_USER_CACHE_SHARED_GRAPH_VALUE_OBJECT_REF 11 +/* PHP reference and back-reference to one. */ +#define ZEND_OPCACHE_USER_CACHE_SHARED_GRAPH_VALUE_REFERENCE 13 +#define ZEND_OPCACHE_USER_CACHE_SHARED_GRAPH_VALUE_REFERENCE_REF 14 + +/* Back-reference to a DYNAMIC_ARRAY node. */ +#define ZEND_OPCACHE_USER_CACHE_SHARED_GRAPH_VALUE_ARRAY_REF 15 + +/* Enum case encoded as class name + case name. */ +#define ZEND_OPCACHE_USER_CACHE_SHARED_GRAPH_VALUE_ENUM 16 + +/* Internal object restored through a registered C state handler. */ +#define ZEND_OPCACHE_USER_CACHE_SHARED_GRAPH_VALUE_SAFE_DIRECT_OBJECT 18 + +/* Node may be targeted by a back-reference. */ +#define ZEND_OPCACHE_USER_CACHE_SHARED_GRAPH_OBJECT_FLAG_SHARED 0x1U + +/* Rebind selected spl_object_hash() property values on fetch. */ +#define ZEND_OPCACHE_USER_CACHE_SHARED_GRAPH_PROPERTY_FLAG_SELF_OBJECT_HASH 0x1U + +#define ZEND_OPCACHE_USER_CACHE_LOOKUP_BUCKETS 256U +#define ZEND_OPCACHE_USER_CACHE_LOOKUP_WAYS 2U +#define ZEND_OPCACHE_USER_CACHE_LOOKUP_SETS (ZEND_OPCACHE_USER_CACHE_LOOKUP_BUCKETS / ZEND_OPCACHE_USER_CACHE_LOOKUP_WAYS) +#define ZEND_OPCACHE_USER_CACHE_LOOKUP_INVALID_SLOT UINT32_MAX +#define ZEND_OPCACHE_USER_CACHE_REQUEST_LOCAL_STRING_MIN_LEN 256U +/* Large strings are stored as STRING-rooted shared graphs. */ +#define ZEND_OPCACHE_USER_CACHE_DIRECT_STRING_MIN_LEN 4096U + +#define ZEND_OPCACHE_USER_CACHE_ENTRY_LOCK_TABLE_SIZE 1024U +#define ZEND_OPCACHE_USER_CACHE_ENTRY_LOCK_EMPTY 0 +#define ZEND_OPCACHE_USER_CACHE_ENTRY_LOCK_USED 1 +#define ZEND_OPCACHE_USER_CACHE_ENTRY_LOCK_TOMBSTONE 2 +#define ZEND_OPCACHE_USER_CACHE_LOW_MEMORY_COMPACT_THRESHOLD (3U * 1024U * 1024U) + +/* Optimistic fetch needs 64-bit atomics and explicit fences. */ +#if defined(__GNUC__) || defined(__clang__) +# define ZEND_OPCACHE_USER_CACHE_HAVE_OPTIMISTIC 1 +#elif defined(ZEND_WIN32) && defined(_MSC_VER) +# include +# define ZEND_OPCACHE_USER_CACHE_HAVE_OPTIMISTIC 1 +# define ZEND_OPCACHE_USER_CACHE_OPTIMISTIC_MSVC 1 +#endif + +/* Other platforms use fcntl locks. */ +#if !defined(ZEND_WIN32) && defined(__linux__) +# include +# define ZEND_OPCACHE_USER_CACHE_HAVE_SHARED_MUTEX 1 +#endif + +#define ZEND_OPCACHE_USER_CACHE_LOCK_MODEL_FCNTL 0U +#define ZEND_OPCACHE_USER_CACHE_LOCK_MODEL_MUTEX 1U + +#define ZEND_OPCACHE_USER_CACHE_READER_SLOTS 64U +#define ZEND_OPCACHE_USER_CACHE_READER_DRAIN_SPIN 1024U +#define ZEND_OPCACHE_USER_CACHE_READER_DRAIN_TIMEOUT_US 10000U +/* Force-retired graph payloads pending a later free. */ +#define ZEND_OPCACHE_USER_CACHE_ORPHANED_GRAPH_SLOTS 32U + +#define ZEND_OPCACHE_USER_CACHE_LOOKUP_EMPTY 0 +#define ZEND_OPCACHE_USER_CACHE_LOOKUP_HIT 1 +#define ZEND_OPCACHE_USER_CACHE_LOOKUP_MISS 2 + +#define ZEND_OPCACHE_USER_CACHE_ENTRY_RESERVED_COMBINED_VALUE_KEY 0x0001U + +#define ZEND_OPCACHE_USER_CACHE_BLOCK_FREE 1U +#define ZEND_OPCACHE_USER_CACHE_LOOKUP_VALUE_NONE 0xFFU + +#ifndef ZEND_WIN32 +# define ZEND_OPCACHE_USER_CACHE_SEM_FILENAME_PREFIX ".ZendUserCacheSem." +#endif + +#ifdef ZEND_OPCACHE_USER_CACHE_HAVE_SHARED_MUTEX +typedef union { + pthread_mutex_t mutex; + char padding[64]; +} zend_opcache_user_cache_shared_mutex; +#endif + +typedef struct { + bool enabled; + bool available; + bool startup_failed; + bool backend_initialized; + size_t configured_memory; + const char *failure_reason; +} zend_opcache_user_cache_runtime; + +typedef struct { + 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; + /* Resolved from header->lock_model at storage startup. */ + bool use_shared_mutex; + int lock_file; + char lockfile_name[MAXPATHLEN]; +#ifdef ZTS + MUTEX_T zts_lock; +#endif +} zend_opcache_user_cache_storage; + +typedef struct { + zend_opcache_user_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_user_cache_context; + +struct _zend_opcache_user_cache_partition { + zend_opcache_user_cache_context context; + char *name; + struct _zend_opcache_user_cache_partition *next; +}; + +typedef struct { + zend_ulong hash; + uint64_t owner_pid; + uint64_t owner_start_time; + uint64_t expires_at; + uint32_t key_offset; + uint32_t key_len; + uint8_t state; + uint8_t reserved[7]; +} zend_opcache_user_cache_entry_lock_record; + +/* Identifies an optimistic reader window; start time distinguishes recycled PIDs. */ +typedef struct { + uint64_t owner_pid; + uint64_t owner_incarnation; + uint64_t owner_start_time; + uint32_t active; + uint32_t reserved; +} zend_opcache_user_cache_reader_slot; + +/* Active SHM context for storage helpers. */ +typedef struct { + 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; + /* Odd while a writer is active, even otherwise. */ + uint64_t write_seq; + /* Optimistic readers currently inside pin windows. */ + uint32_t active_optimistic_readers; + uint32_t tombstone_count; + /* Selected once at header initialization. */ + uint32_t lock_model; + uint32_t lock_model_reserved; + uint32_t orphaned_graphs_saturated; + uint32_t orphaned_graphs_reserved; + uint32_t orphaned_graphs[ZEND_OPCACHE_USER_CACHE_ORPHANED_GRAPH_SLOTS]; + zend_opcache_user_cache_reader_slot reader_slots[ZEND_OPCACHE_USER_CACHE_READER_SLOTS]; +#ifdef ZEND_OPCACHE_USER_CACHE_HAVE_SHARED_MUTEX + /* Protects exact-key entry lock records in mutex mode. */ + zend_opcache_user_cache_shared_mutex global_shared_mutex; +#endif + zend_opcache_user_cache_entry_lock_record entry_lock_records[ZEND_OPCACHE_USER_CACHE_ENTRY_LOCK_TABLE_SIZE]; +#ifndef ZEND_WIN32 + uint32_t reserved_lock; +#endif +} zend_opcache_user_cache_header; + +typedef struct { + uint32_t size; + uint32_t prev_size; + uint32_t next_free; + uint32_t prev_free; + uint32_t flags; +} zend_opcache_user_cache_block; + +typedef struct { + zend_ulong hash; + uint32_t key_offset; + uint32_t key_len; + uint32_t value_offset; + uint32_t value_len; + uint64_t expires_at; + uint64_t generation; + uint8_t state; + uint8_t value_type; + uint16_t reserved; + zend_long long_value; + double double_value; +} zend_opcache_user_cache_entry; + +typedef struct { + bool found; + zend_opcache_user_cache_entry entry; +} zend_opcache_user_cache_replaced_entry; + +typedef struct { + zend_ulong hash; + uint64_t mutation_epoch; + uint32_t slot_index; + uint8_t state; + /* Inline scalar for exact-key, same-epoch hits. */ + uint8_t value_type; + uint8_t reserved[2]; + zend_string *key; + zend_long long_value; + double double_value; +} zend_opcache_user_cache_lookup_entry; + +typedef struct { + uint64_t generation; + bool needs_clone; + bool has_array_clone_flags; + /* Allows clone without identity maps. */ + bool no_aliases; + zval value; + HashTable array_clone_flags; +} zend_opcache_user_cache_request_local_slot; + +typedef struct { + 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; + /* Safe-direct state arrays computed during prepare. */ + HashTable *state_memo; +} zend_opcache_user_cache_prepared_value; + +typedef struct { + uint32_t magic; + uint32_t version; + uint32_t root_offset; + uint32_t root_type; + uint32_t flags; + zend_atomic_int ref_state; +} zend_opcache_user_cache_shared_graph_header; + +typedef struct { + zend_opcache_user_cache_context *context; + uint32_t payload_offset; +} zend_opcache_user_cache_shared_graph_ref; + +typedef struct { + zend_opcache_user_cache_context *context; + uint64_t owner_pid; + zend_long lease; + bool preserve_lease; +} zend_opcache_user_cache_entry_lock; + +typedef struct { + uint8_t type; + uint8_t reserved[7]; + union { + zend_long long_value; + double double_value; + uint64_t offset; + } payload; +} zend_opcache_user_cache_shared_graph_value; + +typedef struct { + uint32_t name_offset; + uint32_t reserved; + zend_opcache_user_cache_shared_graph_value value; +} zend_opcache_user_cache_shared_graph_property; + +typedef struct { + zend_ulong h; + uint32_t key_offset; + uint32_t reserved; + zend_opcache_user_cache_shared_graph_value value; +} zend_opcache_user_cache_shared_graph_array_element; + +typedef struct { + uint32_t count; + uint32_t next_free; + uint32_t elements_offset; + uint32_t reserved; +} zend_opcache_user_cache_shared_graph_array; + +typedef struct { + uint32_t class_name_offset; + uint32_t property_count; + uint32_t properties_offset; + uint32_t reserved; +} zend_opcache_user_cache_shared_graph_object; + +typedef struct { + uint32_t class_name_offset; + uint32_t property_count; + uint32_t properties_offset; + uint32_t reserved; + zend_opcache_user_cache_shared_graph_value state; +} zend_opcache_user_cache_shared_graph_safe_direct_object; + +typedef struct { + uint32_t flags; /* OBJECT_FLAG_SHARED if the reference is shared/cyclic */ + uint32_t reserved; + zend_opcache_user_cache_shared_graph_value inner; +} zend_opcache_user_cache_shared_graph_reference; + +typedef struct _zend_opcache_user_cache_shared_graph_enum { + uint32_t class_name_offset; + uint32_t case_name_offset; +} zend_opcache_user_cache_shared_graph_enum; + +typedef struct { + size_t size; + HashTable seen_arrays; + HashTable seen_objects; + HashTable seen_references; + /* Matches the copy pass's string deduplication. */ + HashTable string_dedup; + /* Safe-direct state arrays shared with the copy pass. */ + HashTable *state_memo; +} zend_opcache_user_cache_shared_graph_calc_context; + +typedef struct { + unsigned char *buffer; + size_t size; + size_t position; + HashTable seen_arrays; + HashTable seen_objects; + HashTable seen_references; + HashTable string_dedup; + bool has_shared_identity; + bool has_object; + bool prefers_prototype; + /* Safe-direct state arrays shared with the calc pass. */ + HashTable *state_memo; +} zend_opcache_user_cache_shared_graph_copy_context; + +typedef enum { + ZEND_OPCACHE_USER_CACHE_OPTIMISTIC_FALLBACK = 0, + ZEND_OPCACHE_USER_CACHE_OPTIMISTIC_FOUND, + ZEND_OPCACHE_USER_CACHE_OPTIMISTIC_MISS +} zend_opcache_user_cache_optimistic_result; + +extern zend_class_entry *zend_opcache_user_cache_exception_ce; +extern zend_class_entry *zend_opcache_user_cache_info_ce; +extern zend_opcache_user_cache_context zend_opcache_user_cache_context_state; +extern bool zend_opcache_user_cache_runtime_opted_in; +extern zend_opcache_user_cache_partition *zend_opcache_user_cache_partitions; +extern ZEND_EXT_TLS zend_opcache_user_cache_partition *zend_opcache_user_cache_active_partition; +extern ZEND_EXT_TLS zend_opcache_user_cache_runtime zend_opcache_user_cache_runtime_state; +extern ZEND_EXT_TLS zend_opcache_user_cache_context *zend_opcache_user_cache_active_context_ptr; +extern ZEND_EXT_TLS const char *zend_opcache_user_cache_request_unavailable_reason; +extern ZEND_EXT_TLS bool zend_opcache_user_cache_lock_held; +extern ZEND_EXT_TLS bool zend_opcache_user_cache_lock_held_is_write; +extern ZEND_EXT_TLS zend_opcache_user_cache_shared_graph_ref *zend_opcache_user_cache_shared_graph_refs; +extern ZEND_EXT_TLS uint32_t zend_opcache_user_cache_shared_graph_ref_count; +extern ZEND_EXT_TLS uint32_t zend_opcache_user_cache_shared_graph_ref_capacity; +extern ZEND_EXT_TLS HashTable *zend_opcache_user_cache_shared_graph_ref_index; +extern ZEND_EXT_TLS HashTable *zend_opcache_user_cache_decode_resolve_cache; +extern ZEND_EXT_TLS zend_opcache_user_cache_shared_graph_ref *zend_opcache_user_cache_retired_shared_graphs; +extern ZEND_EXT_TLS uint32_t zend_opcache_user_cache_retired_shared_graph_count; +extern ZEND_EXT_TLS uint32_t zend_opcache_user_cache_retired_shared_graph_capacity; +extern ZEND_EXT_TLS zend_opcache_user_cache_lookup_entry zend_opcache_user_cache_lookup_entry_storage[ZEND_OPCACHE_USER_CACHE_LOOKUP_BUCKETS]; +extern ZEND_EXT_TLS HashTable *zend_opcache_user_cache_request_local_slot_table; +extern ZEND_EXT_TLS HashTable *zend_opcache_user_cache_entry_lock_table; + +void zend_opcache_user_cache_reset_runtime(void); +void zend_opcache_user_cache_reset_storage(void); +bool zend_opcache_user_cache_header_init_locked(void); +void zend_opcache_user_cache_free_locked(uint32_t payload_offset); +uint32_t zend_opcache_user_cache_alloc_locked(size_t size, const void *source); +bool zend_opcache_user_cache_compact_to_fit_locked(size_t size); +bool zend_opcache_user_cache_startup_storage_before_request(void); +void zend_opcache_user_cache_shutdown_storage(void); +void zend_opcache_user_cache_ensure_ready(void); +bool zend_opcache_user_cache_rlock(void); +bool zend_opcache_user_cache_wlock(void); +bool zend_opcache_user_cache_wlock_for_entry_mutation(zend_string *key); +void zend_opcache_user_cache_unlock(void); +void zend_opcache_user_cache_unlock_if_held(void); +bool zend_opcache_user_cache_acquire_entry_lock(zend_string *key); +bool zend_opcache_user_cache_try_acquire_entry_lock(zend_string *key, zend_long lease); +bool zend_opcache_user_cache_has_entry_lock(zend_string *key); +bool zend_opcache_user_cache_release_entry_lock(zend_string *key); +bool zend_opcache_user_cache_entry_locks_allow_clear_locked(void); +void zend_opcache_user_cache_release_active_entry_locks(void); +void zend_opcache_user_cache_release_request_entry_locks(void); +void zend_opcache_user_cache_safe_direct_handlers_init(void); +void zend_opcache_user_cache_safe_direct_handlers_destroy(void); +zend_opcache_user_cache_safe_direct_state_copy_func_t zend_opcache_user_cache_safe_direct_copy_func( + zend_class_entry *ce, + zend_class_entry **base_ce_ptr +); +zend_class_entry *zend_opcache_user_cache_safe_direct_find_base(zend_class_entry *ce); +zend_opcache_user_cache_safe_direct_state_has_unstorable_func_t zend_opcache_user_cache_safe_direct_state_has_unstorable_func( + zend_class_entry *ce +); +zend_opcache_user_cache_safe_direct_state_serialize_func_t zend_opcache_user_cache_safe_direct_state_serialize_func( + zend_class_entry *ce +); +zend_opcache_user_cache_safe_direct_state_unserialize_func_t zend_opcache_user_cache_safe_direct_state_unserialize_func( + zend_class_entry *ce +); +bool zend_opcache_user_cache_safe_direct_prefers_request_local_prototype(zend_class_entry *ce); +bool zend_opcache_user_cache_calculate_shared_graph_size(const zval *value, size_t *buffer_len, HashTable *state_memo); +bool zend_opcache_user_cache_build_shared_graph_in_place(const zval *value, unsigned char *buffer, size_t buffer_len, size_t *graph_len, HashTable *state_memo); +bool zend_opcache_user_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_user_cache_fetch_shared_graph(const unsigned char *buffer, size_t buffer_len, zval *destination); +bool zend_opcache_user_cache_shared_graph_requires_prototype(uint32_t payload_offset); +bool zend_opcache_user_cache_shared_graph_decode_is_lock_safe(uint32_t payload_offset); +bool zend_opcache_user_cache_shared_graph_payload_has_aliases(uint32_t payload_offset); +void zend_opcache_user_cache_decode_resolve_cache_release(void); +bool zend_opcache_user_cache_shared_graph_can_overwrite_payload_locked(uint32_t payload_offset); +bool zend_opcache_user_cache_shared_graph_can_move_payload_locked(uint32_t payload_offset); +bool zend_opcache_user_cache_shared_graph_rebase_moved_payload_locked(uint32_t payload_offset, ptrdiff_t delta); +bool zend_opcache_user_cache_shared_graph_acquire_locked(uint32_t payload_offset); +bool zend_opcache_user_cache_shared_graph_retire_payload_locked(uint32_t payload_offset); +bool zend_opcache_user_cache_shared_graph_release_ref_locked(uint32_t payload_offset); +bool zend_opcache_user_cache_has_request_shared_graph_ref(uint32_t payload_offset); +void zend_opcache_user_cache_register_shared_graph_ref(uint32_t payload_offset); +void zend_opcache_user_cache_defer_retired_shared_graph_free(uint32_t payload_offset); +bool zend_opcache_user_cache_release_request_shared_graph_refs(void); +bool zend_opcache_user_cache_clear_locked(void); +bool zend_opcache_user_cache_prepare_value( + zend_opcache_user_cache_prepared_value *prepared, + zend_string *key, + zval *value, + bool throw_on_failure, + bool honor_strict_store_failure, + bool lock_held +); +void zend_opcache_user_cache_destroy_prepared_value(zend_opcache_user_cache_prepared_value *prepared); +bool zend_opcache_user_cache_store_prepared_locked( + zend_string *key, + zval *value, + const zend_opcache_user_cache_prepared_value *prepared, + zend_long ttl, + bool throw_on_failure, + bool honor_strict_store_failure, + uint64_t *generation_ptr, + bool *seed_request_local_slot_ptr, + zend_opcache_user_cache_replaced_entry *replaced_entry_ptr, + bool allow_pressure_retries +); +bool zend_opcache_user_cache_store_locked(zend_string *key, zval *value, zend_long ttl, bool throw_on_failure, bool honor_strict_store_failure); +bool zend_opcache_user_cache_fetch_locked(zend_string *key, zval *return_value, bool throw_if_missing, bool *found_ptr, bool use_request_local_slot); +bool zend_opcache_user_cache_exists_locked(zend_string *key); +bool zend_opcache_user_cache_delete_locked(zend_string *key); +void zend_opcache_user_cache_discard_replaced_entry_locked(zend_string *key, zend_opcache_user_cache_replaced_entry *replaced_entry); +void zend_opcache_user_cache_rollback_replaced_entry_locked(zend_string *key, zend_opcache_user_cache_replaced_entry *replaced_entry); +void zend_opcache_user_cache_delete_entries_by_prefix_locked(zend_string *prefix); +bool zend_opcache_user_cache_atomic_update_locked( + zend_string *key, + zend_long step, + zend_long ttl, + bool decrement, + bool insert_if_missing, + zend_long *new_value, + bool *is_overflow +); +void zend_opcache_user_cache_release_request_local_slots(void); +void zend_opcache_user_cache_release_active_request_local_slots(void); +void zend_opcache_user_cache_release_active_request_local_slots_by_prefix(zend_string *prefix); +void zend_opcache_user_cache_store_request_local_value_slot(zend_string *key, uint64_t generation, zval *value, bool no_aliases); +zend_opcache_user_cache_optimistic_result zend_opcache_user_cache_fetch_optimistic( + zend_string *key, + zval *return_value +); +zend_opcache_user_cache_optimistic_result zend_opcache_user_cache_exists_optimistic(zend_string *key); +bool zend_opcache_user_cache_optimistic_reader_begin(zend_opcache_user_cache_header *header, uint32_t *slot_index_ptr); +void zend_opcache_user_cache_optimistic_reader_end(zend_opcache_user_cache_header *header, uint32_t slot_index); +void zend_opcache_user_cache_optimistic_fork_setup(void); +bool zend_opcache_user_cache_graph_payloads_quiescent_locked(void); +void zend_opcache_user_cache_shared_graph_ref_reserve(void); +void zend_opcache_user_cache_orphan_graph_payload_locked(uint32_t payload_offset); +void zend_opcache_user_cache_reclaim_orphaned_graphs_locked(void); + +static zend_always_inline bool zend_opcache_user_cache_is_userland_declared_non_public_property( + zend_object *object, + uint32_t property_index) +{ + zend_property_info *property_info; + + if (object->ce->properties_info_table == NULL || + property_index >= object->ce->default_properties_count + ) { + return false; + } + + property_info = object->ce->properties_info_table[property_index]; + + return property_info != NULL && + property_info != ZEND_WRONG_PROPERTY_INFO && + property_info->ce != NULL && + property_info->ce->type == ZEND_USER_CLASS && + (property_info->flags & (ZEND_ACC_STATIC|ZEND_ACC_VIRTUAL)) == 0 && + property_info->offset != ZEND_VIRTUAL_PROPERTY_OFFSET && + (property_info->flags & ZEND_ACC_PPP_MASK) != ZEND_ACC_PUBLIC + ; +} + +#ifdef ZEND_OPCACHE_USER_CACHE_HAVE_OPTIMISTIC +/* MSVC Interlocked operations provide full barriers. */ +#ifdef ZEND_OPCACHE_USER_CACHE_OPTIMISTIC_MSVC +static zend_always_inline uint64_t zend_opcache_user_cache_atomic_load_64(const uint64_t *target) +{ + return (uint64_t) _InterlockedOr64((volatile __int64 *) target, 0); +} + +static zend_always_inline void zend_opcache_user_cache_atomic_store_64(uint64_t *target, uint64_t value) +{ + (void) _InterlockedExchange64((volatile __int64 *) target, (__int64) value); +} + +static zend_always_inline bool zend_opcache_user_cache_atomic_cas_64(uint64_t *target, uint64_t expected, uint64_t desired) +{ + return (uint64_t) _InterlockedCompareExchange64( + (volatile __int64 *) target, + (__int64) desired, + (__int64) expected + ) == expected; +} + +static zend_always_inline uint32_t zend_opcache_user_cache_atomic_load_32(const uint32_t *target) +{ + return (uint32_t) _InterlockedOr((volatile long *) target, 0); +} + +static zend_always_inline void zend_opcache_user_cache_atomic_store_32(uint32_t *target, uint32_t value) +{ + (void) _InterlockedExchange((volatile long *) target, (long) value); +} + +static zend_always_inline void zend_opcache_user_cache_atomic_inc_32(uint32_t *target) +{ + (void) _InterlockedIncrement((volatile long *) target); +} + +static zend_always_inline void zend_opcache_user_cache_atomic_dec_32(uint32_t *target) +{ + (void) _InterlockedDecrement((volatile long *) target); +} + +static zend_always_inline bool zend_opcache_user_cache_atomic_cas_32(uint32_t *target, uint32_t expected, uint32_t desired) +{ + return (uint32_t) _InterlockedCompareExchange((volatile long *) target, (long) desired, (long) expected) == expected; +} + +static zend_always_inline void zend_opcache_user_cache_atomic_fence_seq_cst(void) +{ + MemoryBarrier(); +} + +static zend_always_inline void zend_opcache_user_cache_atomic_fence_acquire(void) +{ + MemoryBarrier(); +} +#else +static zend_always_inline uint64_t zend_opcache_user_cache_atomic_load_64(const uint64_t *target) +{ + return __atomic_load_n(target, __ATOMIC_ACQUIRE); +} + +static zend_always_inline void zend_opcache_user_cache_atomic_store_64(uint64_t *target, uint64_t value) +{ + __atomic_store_n(target, value, __ATOMIC_RELEASE); +} + +static zend_always_inline bool zend_opcache_user_cache_atomic_cas_64(uint64_t *target, uint64_t expected, uint64_t desired) +{ + return __atomic_compare_exchange_n(target, &expected, desired, false, __ATOMIC_ACQ_REL, __ATOMIC_ACQUIRE); +} + +static zend_always_inline uint32_t zend_opcache_user_cache_atomic_load_32(const uint32_t *target) +{ + return __atomic_load_n(target, __ATOMIC_ACQUIRE); +} + +static zend_always_inline void zend_opcache_user_cache_atomic_store_32(uint32_t *target, uint32_t value) +{ + __atomic_store_n(target, value, __ATOMIC_RELEASE); +} + +static zend_always_inline void zend_opcache_user_cache_atomic_inc_32(uint32_t *target) +{ + (void) __atomic_fetch_add(target, 1, __ATOMIC_SEQ_CST); +} + +static zend_always_inline void zend_opcache_user_cache_atomic_dec_32(uint32_t *target) +{ + (void) __atomic_fetch_sub(target, 1, __ATOMIC_SEQ_CST); +} + +static zend_always_inline bool zend_opcache_user_cache_atomic_cas_32(uint32_t *target, uint32_t expected, uint32_t desired) +{ + return __atomic_compare_exchange_n(target, &expected, desired, false, __ATOMIC_ACQ_REL, __ATOMIC_ACQUIRE); +} + +static zend_always_inline void zend_opcache_user_cache_atomic_fence_seq_cst(void) +{ + __atomic_thread_fence(__ATOMIC_SEQ_CST); +} + +static zend_always_inline void zend_opcache_user_cache_atomic_fence_acquire(void) +{ + __atomic_thread_fence(__ATOMIC_ACQUIRE); +} +#endif +#endif + +static zend_always_inline zend_opcache_user_cache_context *zend_opcache_user_cache_active_context(void) +{ + if (zend_opcache_user_cache_active_context_ptr != NULL) { + return zend_opcache_user_cache_active_context_ptr; + } + + return zend_opcache_user_cache_active_partition != NULL + ? &zend_opcache_user_cache_active_partition->context + : &zend_opcache_user_cache_context_state + ; +} + +static zend_always_inline zend_opcache_user_cache_context *zend_opcache_user_cache_default_context(void) +{ + return zend_opcache_user_cache_active_partition != NULL + ? &zend_opcache_user_cache_active_partition->context + : &zend_opcache_user_cache_context_state + ; +} + +static zend_always_inline void *zend_opcache_user_cache_base(void) +{ + zend_opcache_user_cache_storage *storage = &zend_opcache_user_cache_active_context()->storage; + + if (!storage->initialized || + storage->segment_count != 1 + ) { + return NULL; + } + + return storage->segments[0]->p; +} + +static zend_always_inline zend_opcache_user_cache_header *zend_opcache_user_cache_header_ptr(void) +{ + return (zend_opcache_user_cache_header *) zend_opcache_user_cache_base(); +} + +static zend_always_inline char *zend_opcache_user_cache_ptr(uint32_t offset) +{ + return (char *) zend_opcache_user_cache_base() + offset; +} + +static zend_always_inline zend_opcache_user_cache_block *zend_opcache_user_cache_block_ptr(uint32_t offset) +{ + return (zend_opcache_user_cache_block *) zend_opcache_user_cache_ptr(offset); +} + +static zend_always_inline uint32_t zend_opcache_user_cache_block_payload_capacity(uint32_t payload_offset) +{ + zend_opcache_user_cache_block *block; + + if (payload_offset < sizeof(zend_opcache_user_cache_block)) { + return 0; + } + + block = zend_opcache_user_cache_block_ptr(payload_offset - sizeof(zend_opcache_user_cache_block)); + if (block->size < sizeof(zend_opcache_user_cache_block)) { + return 0; + } + + return block->size - (uint32_t) sizeof(zend_opcache_user_cache_block); +} + +static zend_always_inline uint32_t zend_opcache_user_cache_write_payload_locked(uint32_t reusable_offset, size_t size, const void *source) +{ + if (reusable_offset != 0 && zend_opcache_user_cache_block_payload_capacity(reusable_offset) >= size) { + memcpy(zend_opcache_user_cache_ptr(reusable_offset), source, size); + + return reusable_offset; + } + + return zend_opcache_user_cache_alloc_locked(size, source); +} + +static zend_always_inline zend_opcache_user_cache_context *zend_opcache_user_cache_activate_context(zend_opcache_user_cache_context *context) +{ + zend_opcache_user_cache_context *previous = zend_opcache_user_cache_active_context_ptr; + + zend_opcache_user_cache_active_context_ptr = context; + + return previous; +} + +static zend_always_inline void zend_opcache_user_cache_restore_context(zend_opcache_user_cache_context *context) +{ + zend_opcache_user_cache_active_context_ptr = context; +} + +static zend_always_inline zend_opcache_user_cache_runtime *zend_opcache_user_cache_active_runtime(void) +{ + return &zend_opcache_user_cache_runtime_state; +} + +static zend_always_inline HashTable **zend_opcache_user_cache_active_request_local_slots_ptr(void) +{ + return &zend_opcache_user_cache_request_local_slot_table; +} + +static zend_always_inline zend_opcache_user_cache_lookup_entry *zend_opcache_user_cache_lookup_cache_set(zend_ulong hash) +{ + uint32_t set_index = (uint32_t) (hash & (ZEND_OPCACHE_USER_CACHE_LOOKUP_SETS - 1)); + + return &zend_opcache_user_cache_lookup_entry_storage[set_index * ZEND_OPCACHE_USER_CACHE_LOOKUP_WAYS]; +} + +static zend_always_inline void zend_opcache_user_cache_lookup_entry_release_key( + zend_opcache_user_cache_lookup_entry *lookup_entry) +{ + if (lookup_entry->key != NULL) { + zend_string_release(lookup_entry->key); + lookup_entry->key = NULL; + } +} + +static zend_always_inline void zend_opcache_user_cache_lookup_cache_store( + zend_opcache_user_cache_lookup_entry *lookup_entry, + zend_ulong hash, + uint64_t mutation_epoch, + uint32_t slot_index, + uint8_t state) +{ + if (lookup_entry == NULL) { + return; + } + + zend_opcache_user_cache_lookup_entry_release_key(lookup_entry); + + lookup_entry->hash = hash; + lookup_entry->mutation_epoch = mutation_epoch; + lookup_entry->slot_index = slot_index; + lookup_entry->state = state; + lookup_entry->value_type = ZEND_OPCACHE_USER_CACHE_LOOKUP_VALUE_NONE; +} + +/* Store exact-key fast-path data for non-expiring scalars. */ +static zend_always_inline void zend_opcache_user_cache_lookup_cache_fill_hit_value( + zend_opcache_user_cache_lookup_entry *lookup_entry, + zend_string *key, + const zend_opcache_user_cache_entry *entry) +{ + if (lookup_entry == NULL) { + return; + } + + lookup_entry->key = zend_string_copy(key); + + if (entry->expires_at != 0) { + return; + } + + switch (entry->value_type) { + case ZEND_OPCACHE_USER_CACHE_VALUE_NULL: + case ZEND_OPCACHE_USER_CACHE_VALUE_TRUE: + case ZEND_OPCACHE_USER_CACHE_VALUE_FALSE: + case ZEND_OPCACHE_USER_CACHE_VALUE_LONG: + case ZEND_OPCACHE_USER_CACHE_VALUE_DOUBLE: + lookup_entry->value_type = entry->value_type; + lookup_entry->long_value = entry->long_value; + lookup_entry->double_value = entry->double_value; + break; + default: + break; + } +} + +static zend_always_inline zend_opcache_user_cache_lookup_entry *zend_opcache_user_cache_lookup_cache_preferred_way( + zend_opcache_user_cache_lookup_entry *lookup_entries, + zend_ulong hash) +{ + uint32_t way = (uint32_t) ((((uint64_t) hash >> 32) ^ hash) & (ZEND_OPCACHE_USER_CACHE_LOOKUP_WAYS - 1)); + + return &lookup_entries[way]; +} + +static zend_always_inline zend_opcache_user_cache_lookup_entry *zend_opcache_user_cache_lookup_cache_select_slot( + zend_opcache_user_cache_lookup_entry *lookup_entries, + zend_ulong hash, + uint64_t mutation_epoch, + bool allow_hit_eviction) +{ + zend_opcache_user_cache_lookup_entry *preferred, *alternate; + + if (lookup_entries == NULL) { + return NULL; + } + + preferred = zend_opcache_user_cache_lookup_cache_preferred_way(lookup_entries, hash); + alternate = preferred == &lookup_entries[0] ? &lookup_entries[1] : &lookup_entries[0]; + + /* Two-way set associative lookup cache, invalidated by mutation_epoch. */ + if (preferred->state != ZEND_OPCACHE_USER_CACHE_LOOKUP_EMPTY && preferred->hash == hash && preferred->mutation_epoch == mutation_epoch) { + return preferred; + } + if (alternate->state != ZEND_OPCACHE_USER_CACHE_LOOKUP_EMPTY && alternate->hash == hash && alternate->mutation_epoch == mutation_epoch) { + return alternate; + } + + if (preferred->state == ZEND_OPCACHE_USER_CACHE_LOOKUP_EMPTY || preferred->mutation_epoch != mutation_epoch) { + return preferred; + } + if (alternate->state == ZEND_OPCACHE_USER_CACHE_LOOKUP_EMPTY || alternate->mutation_epoch != mutation_epoch) { + return alternate; + } + + if (preferred->state == ZEND_OPCACHE_USER_CACHE_LOOKUP_MISS) { + return preferred; + } + if (alternate->state == ZEND_OPCACHE_USER_CACHE_LOOKUP_MISS) { + return alternate; + } + + return allow_hit_eviction ? preferred : NULL; +} + +static zend_always_inline void zend_opcache_user_cache_lookup_cache_store_miss( + zend_opcache_user_cache_lookup_entry *lookup_entries, + zend_ulong hash, + uint64_t mutation_epoch) +{ + zend_opcache_user_cache_lookup_entry *victim = zend_opcache_user_cache_lookup_cache_select_slot(lookup_entries, hash, mutation_epoch, false); + + if (victim == NULL) { + return; + } + + zend_opcache_user_cache_lookup_cache_store( + victim, + hash, + mutation_epoch, + ZEND_OPCACHE_USER_CACHE_LOOKUP_INVALID_SLOT, + ZEND_OPCACHE_USER_CACHE_LOOKUP_MISS + ); +} + +static zend_always_inline void zend_opcache_user_cache_lookup_cache_store_hit( + zend_opcache_user_cache_lookup_entry *lookup_entry, + zend_ulong hash, + uint64_t mutation_epoch, + uint32_t slot_index) +{ + zend_opcache_user_cache_lookup_cache_store( + lookup_entry, + hash, + mutation_epoch, + slot_index, + ZEND_OPCACHE_USER_CACHE_LOOKUP_HIT + ); +} + +#ifdef ZEND_OPCACHE_USER_CACHE_HAVE_OPTIMISTIC +/* Odd sequence values denote an active writer. */ +static zend_always_inline uint64_t zend_opcache_user_cache_seq_load(const uint64_t *seq) +{ + return zend_opcache_user_cache_atomic_load_64(seq); +} + +static zend_always_inline uint64_t zend_opcache_user_cache_seq_reload(const uint64_t *seq) +{ + zend_opcache_user_cache_atomic_fence_acquire(); + + return zend_opcache_user_cache_atomic_load_64(seq); +} + +static zend_always_inline void zend_opcache_user_cache_seq_announce(uint64_t *seq, uint64_t value) +{ + zend_opcache_user_cache_atomic_store_64(seq, value); + zend_opcache_user_cache_atomic_fence_seq_cst(); +} + +static zend_always_inline void zend_opcache_user_cache_seq_publish(uint64_t *seq, uint64_t value) +{ + zend_opcache_user_cache_atomic_store_64(seq, value); +} +#endif + +static zend_always_inline bool zend_opcache_user_cache_header_is_initialized_locked(void) +{ + zend_opcache_user_cache_header *header = zend_opcache_user_cache_header_ptr(); + + return header != NULL && + header->magic == ZEND_OPCACHE_USER_CACHE_MAGIC && + header->version == ZEND_OPCACHE_USER_CACHE_VERSION + ; +} + +static zend_always_inline void zend_opcache_user_cache_bump_mutation_epoch_locked(zend_opcache_user_cache_header *header) +{ + if (header == NULL) { + return; + } + + header->mutation_epoch++; + if (header->mutation_epoch == 0) { + header->mutation_epoch = 1; + } +} + +static zend_always_inline zend_opcache_user_cache_entry *zend_opcache_user_cache_entries(zend_opcache_user_cache_header *header) +{ + return (zend_opcache_user_cache_entry *) ((char *) header + sizeof(zend_opcache_user_cache_header)); +} + +static zend_always_inline bool zend_opcache_user_cache_key_equals( + const zend_opcache_user_cache_entry *entry, + zend_string *key, + zend_ulong hash) +{ + if (entry->state != ZEND_OPCACHE_USER_CACHE_ENTRY_USED || entry->hash != hash || entry->key_len != ZSTR_LEN(key)) { + return false; + } + + return memcmp(zend_opcache_user_cache_ptr(entry->key_offset), ZSTR_VAL(key), ZSTR_LEN(key)) == 0; +} + +static zend_always_inline bool zend_opcache_user_cache_value_uses_offset(uint8_t value_type) +{ + return + value_type == ZEND_OPCACHE_USER_CACHE_VALUE_STRING || + value_type == ZEND_OPCACHE_USER_CACHE_VALUE_SHARED_GRAPH + ; +} + +static zend_always_inline bool zend_opcache_user_cache_block_is_free(const zend_opcache_user_cache_block *block) +{ + return (block->flags & ZEND_OPCACHE_USER_CACHE_BLOCK_FREE) != 0; +} + +static zend_always_inline void zend_opcache_user_cache_block_mark_free(zend_opcache_user_cache_block *block) +{ + block->flags |= ZEND_OPCACHE_USER_CACHE_BLOCK_FREE; +} + +static zend_always_inline void zend_opcache_user_cache_block_mark_used(zend_opcache_user_cache_block *block) +{ + block->flags &= ~ZEND_OPCACHE_USER_CACHE_BLOCK_FREE; + block->next_free = 0; + block->prev_free = 0; +} + +static zend_always_inline void zend_opcache_user_cache_clear_lookup_cache(void) +{ + uint32_t index; + + for (index = 0; index < ZEND_OPCACHE_USER_CACHE_LOOKUP_BUCKETS; index++) { + zend_opcache_user_cache_lookup_entry_release_key(&zend_opcache_user_cache_lookup_entry_storage[index]); + } + + memset(zend_opcache_user_cache_lookup_entry_storage, 0, sizeof(zend_opcache_user_cache_lookup_entry) * ZEND_OPCACHE_USER_CACHE_LOOKUP_BUCKETS); +} + +static zend_always_inline void zend_opcache_user_cache_lookup_cache_reset_entry(zend_opcache_user_cache_lookup_entry *lookup_entry) +{ + if (lookup_entry == NULL) { + return; + } + + zend_opcache_user_cache_lookup_entry_release_key(lookup_entry); + + memset(lookup_entry, 0, sizeof(*lookup_entry)); +} + +#endif /* ZEND_USER_CACHE_INTERNAL_H */ diff --git a/ext/opcache/zend_user_cache_shared_graph.c b/ext/opcache/zend_user_cache_shared_graph.c new file mode 100644 index 000000000000..05f3eb5cc92d --- /dev/null +++ b/ext/opcache/zend_user_cache_shared_graph.c @@ -0,0 +1,3474 @@ +/* + +----------------------------------------------------------------------+ + | 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_interfaces.h" +#include "zend_enum.h" +#include "zend_operators.h" +#include "zend_user_cache_internal.h" +#include "ext/spl/php_spl.h" + +/* Per-decode maps, allocated lazily only when the graph has aliases. */ +static ZEND_EXT_TLS HashTable *zend_opcache_user_cache_decode_identity_map = NULL; +static ZEND_EXT_TLS HashTable *zend_opcache_user_cache_decode_reference_map = NULL; +static ZEND_EXT_TLS HashTable *zend_opcache_user_cache_decode_array_map = NULL; + +/* Request-lifetime class/enum lookup cache keyed by pinned payload addresses. */ +ZEND_EXT_TLS HashTable *zend_opcache_user_cache_decode_resolve_cache = NULL; + +static void zend_opcache_user_cache_decode_identity_object_dtor(zval *zv); +static void zend_opcache_user_cache_decode_reference_dtor(zval *zv); +static void zend_opcache_user_cache_decode_array_dtor(zval *zv); +static bool zend_opcache_user_cache_shared_graph_rebase_direct_array( + zend_array *array, + const unsigned char *old_base, + const unsigned char *new_base, + size_t len, + ptrdiff_t delta, + HashTable *seen_arrays +); + +static zend_always_inline void *zend_opcache_user_cache_decode_resolve_cache_find(const void *address) +{ + if (zend_opcache_user_cache_decode_resolve_cache == NULL) { + return NULL; + } + + return zend_hash_index_find_ptr( + zend_opcache_user_cache_decode_resolve_cache, + (zend_ulong) (uintptr_t) address + ); +} + +static zend_always_inline void zend_opcache_user_cache_decode_resolve_cache_store(const void *address, void *value) +{ + if (zend_opcache_user_cache_decode_resolve_cache == NULL) { + zend_opcache_user_cache_decode_resolve_cache = emalloc(sizeof(HashTable)); + zend_hash_init(zend_opcache_user_cache_decode_resolve_cache, 8, NULL, NULL, 0); + } + + zend_hash_index_add_ptr( + zend_opcache_user_cache_decode_resolve_cache, + (zend_ulong) (uintptr_t) address, + value + ); +} + +static zend_always_inline void zend_opcache_user_cache_decode_identity_map_teardown(void) +{ + if (zend_opcache_user_cache_decode_identity_map != NULL) { + zend_hash_destroy(zend_opcache_user_cache_decode_identity_map); + efree(zend_opcache_user_cache_decode_identity_map); + zend_opcache_user_cache_decode_identity_map = NULL; + } +} + +static zend_always_inline bool zend_opcache_user_cache_decode_identity_map_insert(uint32_t offset, zend_object *object) +{ + if (zend_opcache_user_cache_decode_identity_map == NULL) { + zend_opcache_user_cache_decode_identity_map = emalloc(sizeof(HashTable)); + zend_hash_init(zend_opcache_user_cache_decode_identity_map, 8, NULL, + zend_opcache_user_cache_decode_identity_object_dtor, 0 + ); + } + + GC_ADDREF(object); + if (zend_hash_index_add_ptr(zend_opcache_user_cache_decode_identity_map, offset, object) == NULL) { + OBJ_RELEASE(object); + + return false; + } + + return true; +} + +static zend_always_inline zend_object *zend_opcache_user_cache_decode_identity_map_find(uint32_t offset) +{ + if (zend_opcache_user_cache_decode_identity_map == NULL) { + return NULL; + } + + return zend_hash_index_find_ptr(zend_opcache_user_cache_decode_identity_map, offset); +} + +static zend_always_inline void zend_opcache_user_cache_decode_reference_map_teardown(void) +{ + if (zend_opcache_user_cache_decode_reference_map != NULL) { + zend_hash_destroy(zend_opcache_user_cache_decode_reference_map); + efree(zend_opcache_user_cache_decode_reference_map); + zend_opcache_user_cache_decode_reference_map = NULL; + } +} + +static zend_always_inline bool zend_opcache_user_cache_decode_reference_map_insert(uint32_t offset, zend_reference *reference) +{ + if (zend_opcache_user_cache_decode_reference_map == NULL) { + zend_opcache_user_cache_decode_reference_map = emalloc(sizeof(HashTable)); + zend_hash_init(zend_opcache_user_cache_decode_reference_map, + 8, + NULL, + zend_opcache_user_cache_decode_reference_dtor, 0 + ); + } + + GC_ADDREF(reference); + + if (zend_hash_index_add_ptr(zend_opcache_user_cache_decode_reference_map, offset, reference) == NULL) { + if (GC_DELREF(reference) == 0) { + efree_size(reference, sizeof(zend_reference)); + } + + return false; + } + + return true; +} + +static zend_always_inline zend_reference *zend_opcache_user_cache_decode_reference_map_find(uint32_t offset) +{ + if (zend_opcache_user_cache_decode_reference_map == NULL) { + return NULL; + } + + return zend_hash_index_find_ptr(zend_opcache_user_cache_decode_reference_map, offset); +} + +static zend_always_inline void zend_opcache_user_cache_decode_array_map_teardown(void) +{ + if (zend_opcache_user_cache_decode_array_map != NULL) { + zend_hash_destroy(zend_opcache_user_cache_decode_array_map); + + efree(zend_opcache_user_cache_decode_array_map); + + zend_opcache_user_cache_decode_array_map = NULL; + } +} + +static zend_always_inline bool zend_opcache_user_cache_decode_array_map_insert(uint32_t offset, zend_array *array) +{ + if (zend_opcache_user_cache_decode_array_map == NULL) { + zend_opcache_user_cache_decode_array_map = emalloc(sizeof(HashTable)); + + zend_hash_init(zend_opcache_user_cache_decode_array_map, 8, NULL, + zend_opcache_user_cache_decode_array_dtor, 0 + ); + } + + GC_ADDREF(array); + if (zend_hash_index_add_ptr(zend_opcache_user_cache_decode_array_map, offset, array) == NULL) { + if (GC_DELREF(array) == 0) { + zend_array_destroy(array); + } + + return false; + } + + return true; +} + +static zend_always_inline zend_array *zend_opcache_user_cache_decode_array_map_find(uint32_t offset) +{ + if (zend_opcache_user_cache_decode_array_map == NULL) { + return NULL; + } + + return zend_hash_index_find_ptr(zend_opcache_user_cache_decode_array_map, offset); +} + +/* Graphs are aligned within generic SHM blocks. */ +static zend_always_inline size_t zend_opcache_user_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 bool zend_opcache_user_cache_shared_graph_offset_in_block( + uint32_t offset, + uint32_t block_offset, + uint32_t block_size) +{ + return offset >= block_offset + sizeof(zend_opcache_user_cache_block) && offset < block_offset + block_size; +} + +static zend_always_inline const unsigned char *zend_opcache_user_cache_shared_graph_locate_buffer( + const unsigned char *buffer, + size_t buffer_len, + size_t *graph_len) +{ + const zend_opcache_user_cache_shared_graph_header *header; + size_t padding; + + padding = zend_opcache_user_cache_shared_graph_alignment_padding(buffer); + if (padding > buffer_len || buffer_len - padding < sizeof(zend_opcache_user_cache_shared_graph_header)) { + return NULL; + } + + buffer += padding; + buffer_len -= padding; + header = (const zend_opcache_user_cache_shared_graph_header *) buffer; + if (header->magic != ZEND_OPCACHE_USER_CACHE_SHARED_GRAPH_MAGIC || + header->version != ZEND_OPCACHE_USER_CACHE_SHARED_GRAPH_VERSION + ) { + return NULL; + } + + if (graph_len != NULL) { + *graph_len = buffer_len; + } + + return buffer; +} + +static zend_always_inline void zend_opcache_user_cache_shared_graph_calc_init(zend_opcache_user_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); + zend_hash_init(&context->seen_references, 8, NULL, NULL, 0); + zend_hash_init(&context->string_dedup, 8, NULL, NULL, 0); +} + +static zend_always_inline void zend_opcache_user_cache_shared_graph_calc_destroy(zend_opcache_user_cache_shared_graph_calc_context *context) +{ + zend_hash_destroy(&context->string_dedup); + zend_hash_destroy(&context->seen_references); + zend_hash_destroy(&context->seen_objects); + zend_hash_destroy(&context->seen_arrays); +} + +static zend_always_inline bool zend_opcache_user_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; +} + +/* Match the copy pass's string deduplication. */ +static zend_always_inline bool zend_opcache_user_cache_shared_graph_calc_reserve_string( + zend_opcache_user_cache_shared_graph_calc_context *context, + const zend_string *string) +{ + if (zend_hash_exists(&context->string_dedup, (zend_string *) string)) { + return true; + } + + if (!zend_opcache_user_cache_shared_graph_reserve_size(&context->size, _ZSTR_STRUCT_SIZE(ZSTR_LEN(string)))) { + return false; + } + + return zend_hash_add_empty_element(&context->string_dedup, (zend_string *) string) != NULL; +} + +static zend_always_inline bool zend_opcache_user_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_user_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_user_cache_shared_graph_can_restore_direct(zend_class_entry *ce) +{ + /* Non-serializable classes expose no usable value state. */ + if (ce->ce_flags & ZEND_ACC_NOT_SERIALIZABLE) { + return false; + } + + /* Custom internal objects need a registered safe-direct handler. */ + if (ce->type != ZEND_USER_CLASS && ce->create_object != NULL) { + return false; + } + + return true; +} + +static zend_always_inline bool zend_opcache_user_cache_shared_graph_property_is_default( + zend_object *object, + uint32_t property_index, + const zval *property_value) +{ + zend_property_info *property_info; + zval *default_properties, *default_value; + + if (object->ce->type != ZEND_USER_CLASS || + object->ce->properties_info_table == NULL || + property_index >= object->ce->default_properties_count + ) { + return false; + } + + property_info = object->ce->properties_info_table[property_index]; + default_properties = CE_DEFAULT_PROPERTIES_TABLE(object->ce); + default_value = &default_properties[OBJ_PROP_TO_NUM(property_info->offset)]; + + return Z_TYPE_P(default_value) != IS_UNDEF && zend_is_identical(property_value, default_value); +} + +static zend_always_inline bool zend_opcache_user_cache_shared_graph_is_unmangled_property_name(zend_string *property_name) +{ + return ZSTR_LEN(property_name) != 0 && ZSTR_VAL(property_name)[0] != '\0'; +} + +static zend_always_inline bool zend_opcache_user_cache_shared_graph_safe_direct_property_shadows_state( + zval *state, zend_string *property_name) +{ + return zend_opcache_user_cache_shared_graph_is_unmangled_property_name(property_name) && + Z_TYPE_P(state) == IS_ARRAY && + zend_hash_exists(Z_ARRVAL_P(state), property_name) + ; +} + +static zend_always_inline bool zend_opcache_user_cache_shared_graph_should_skip_safe_direct_property( + zend_object *object, + zval *state, + zend_string *property_name, + uint32_t property_index, + const zval *property_value) +{ + return zend_opcache_user_cache_shared_graph_safe_direct_property_shadows_state(state, property_name) || + zend_opcache_user_cache_shared_graph_property_is_default(object, property_index, property_value) + ; +} + +static zend_always_inline bool zend_opcache_user_cache_shared_graph_can_use_safe_direct(zend_class_entry *ce) +{ + zend_class_entry *base_ce = zend_opcache_user_cache_safe_direct_find_base(ce); + + return base_ce != NULL && + zend_opcache_user_cache_safe_direct_state_serialize_func(ce) != NULL && + zend_opcache_user_cache_safe_direct_state_unserialize_func(ce) != NULL + ; +} + +static zend_always_inline bool zend_opcache_user_cache_shared_graph_pointer_in_range( + const void *pointer, + const unsigned char *base, + size_t len) +{ + uintptr_t address, start; + + if (pointer == NULL || base == NULL || len == 0) { + return false; + } + + address = (uintptr_t) pointer; + start = (uintptr_t) base; + + return address >= start && address - start < len; +} + +static zend_always_inline void *zend_opcache_user_cache_shared_graph_rebase_pointer( + void *pointer, + const unsigned char *old_base, + size_t len, + ptrdiff_t delta) +{ + if (!zend_opcache_user_cache_shared_graph_pointer_in_range(pointer, old_base, len)) { + return pointer; + } + + return (void *) ((char *) pointer - delta); +} + +static bool zend_opcache_user_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_user_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_user_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_user_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_user_cache_shared_graph_calc_direct_value( + zend_opcache_user_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_user_cache_shared_graph_calc_reserve_string(context, Z_STR_P(value)); + case IS_ARRAY: + array = Z_ARRVAL_P(value); + if (array->nNumOfElements == 0) { + return true; + } + + if (!zend_opcache_user_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_user_cache_shared_graph_reserve_size(&context->size, sizeof(zend_array)) || + !zend_opcache_user_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_user_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_user_cache_shared_graph_calc_reserve_string(context, bucket[index].key) + ) { + result = false; + break; + } + + if (Z_TYPE(bucket[index].val) != IS_UNDEF && + !zend_opcache_user_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_user_cache_shared_graph_property_is_self_object_hash( + zend_object *object, + uint32_t property_index, + const zval *property_value, + zend_string **object_hash_ptr) +{ + if (Z_TYPE_P(property_value) != IS_STRING || + !zend_opcache_user_cache_is_userland_declared_non_public_property(object, property_index) + ) { + return false; + } + + if (*object_hash_ptr == NULL) { + *object_hash_ptr = php_spl_object_hash(object); + } + + return zend_string_equals(Z_STR_P(property_value), *object_hash_ptr); +} + +static zend_class_entry *zend_opcache_user_cache_decode_lookup_class(zend_string *class_name) +{ + zend_class_entry *ce; + + ce = zend_opcache_user_cache_decode_resolve_cache_find(class_name); + if (ce != NULL) { + return ce; + } + + ce = zend_lookup_class(class_name); + if (ce != NULL) { + zend_opcache_user_cache_decode_resolve_cache_store(class_name, ce); + } + + return ce; +} + +static void zend_opcache_user_cache_decode_identity_object_dtor(zval *zv) +{ + OBJ_RELEASE((zend_object *) Z_PTR_P(zv)); +} + +static void zend_opcache_user_cache_decode_reference_dtor(zval *zv) +{ + zval tmp; + + /* Release the transient ref taken on insert (frees the reference at rc 0). */ + ZVAL_REF(&tmp, (zend_reference *) Z_PTR_P(zv)); + zval_ptr_dtor(&tmp); +} + +static void zend_opcache_user_cache_decode_array_dtor(zval *zv) +{ + zval tmp; + + /* Release the transient ref taken on insert (frees the array at rc 0). */ + ZVAL_ARR(&tmp, (zend_array *) Z_PTR_P(zv)); + + zval_ptr_dtor(&tmp); +} + +static bool zend_opcache_user_cache_shared_graph_get_safe_direct_state( + HashTable *state_memo, + const zval *value, + zval **state_ptr, + zval *owned_state) +{ + zend_object *object = Z_OBJ_P(value); + zend_opcache_user_cache_safe_direct_state_serialize_func_t serialize_func; + zend_ulong memo_key; + zval *memo_state; + + ZVAL_UNDEF(owned_state); + *state_ptr = NULL; + + memo_key = (zend_ulong) (uintptr_t) object; + if (state_memo != NULL) { + memo_state = zend_hash_index_find(state_memo, memo_key); + if (memo_state != NULL) { + if (Z_TYPE_P(memo_state) != IS_ARRAY) { + return false; + } + + *state_ptr = memo_state; + + return true; + } + } + + serialize_func = zend_opcache_user_cache_safe_direct_state_serialize_func(object->ce); + if (serialize_func == NULL || + !serialize_func(value, owned_state) || + Z_TYPE_P(owned_state) != IS_ARRAY + ) { + if (!Z_ISUNDEF_P(owned_state)) { + zval_ptr_dtor(owned_state); + ZVAL_UNDEF(owned_state); + } + + return false; + } + + if (state_memo != NULL) { + memo_state = zend_hash_index_add(state_memo, memo_key, owned_state); + if (memo_state == NULL) { + zval_ptr_dtor(owned_state); + ZVAL_UNDEF(owned_state); + + return false; + } + + ZVAL_UNDEF(owned_state); + *state_ptr = memo_state; + + return true; + } + + *state_ptr = owned_state; + + return true; +} + +static bool zend_opcache_user_cache_shared_graph_calc_value( + zend_opcache_user_cache_shared_graph_calc_context *context, + const zval *value +) +{ + const HashTable *array; + zend_ulong array_key; + zend_string *key, *property_name, *case_name; + zend_class_entry *ce; + zend_reference *ref; + zend_object *object; + zval *element, *property_value, *source_value, *state_ptr, state; + HashTable *properties; + uint32_t property_count, property_index; + bool result; + + 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_user_cache_shared_graph_calc_reserve_string(context, Z_STR_P(value)); + case IS_ARRAY: + array = Z_ARRVAL_P(value); + + if (array->nNumOfElements == 0) { + /* Preserve nNextFreeElement for empty dynamic arrays. */ + if (array->nNextFreeElement != 0) { + return zend_opcache_user_cache_shared_graph_reserve_size(&context->size, + sizeof(zend_opcache_user_cache_shared_graph_array)) + ; + } + + return true; + } + + /* Immutable arrays cannot contain objects or references. */ + if ((GC_FLAGS(Z_ARRVAL_P(value)) & IS_ARRAY_IMMUTABLE) || + zend_opcache_user_cache_shared_graph_can_copy_direct_value(&context->seen_arrays, value) + ) { + return zend_opcache_user_cache_shared_graph_calc_direct_value(context, value); + } + + if (!zend_opcache_user_cache_shared_graph_mark_seen_array(&context->seen_arrays, array)) { + /* Cyclic arrays are emitted as back-references. */ + return true; + } + + array_key = (zend_ulong) (uintptr_t) array; + result = true; + + if (!zend_opcache_user_cache_shared_graph_reserve_size(&context->size, sizeof(zend_opcache_user_cache_shared_graph_array)) || + !zend_opcache_user_cache_shared_graph_reserve_size( + &context->size, + (size_t) array->nNumOfElements * sizeof(zend_opcache_user_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_user_cache_shared_graph_calc_reserve_string(context, key)) { + result = false; + + break; + } + + if (!zend_opcache_user_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; + + if (ce->ce_flags & ZEND_ACC_ENUM) { + /* Enum cases are restored by class name + case name. */ + case_name = Z_STR_P(zend_enum_fetch_case_name(object)); + + return zend_opcache_user_cache_shared_graph_reserve_size( + &context->size, + sizeof(zend_opcache_user_cache_shared_graph_enum) + ) && + zend_opcache_user_cache_shared_graph_calc_reserve_string(context, ce->name) && + zend_opcache_user_cache_shared_graph_calc_reserve_string(context, case_name) + ; + } + + if (zend_opcache_user_cache_shared_graph_can_use_safe_direct(ce) && + zend_opcache_user_cache_shared_graph_get_safe_direct_state( + context->state_memo, + value, + &state_ptr, + &state + ) + ) { + if (!zend_opcache_user_cache_shared_graph_reserve_size(&context->size, + sizeof(zend_opcache_user_cache_shared_graph_safe_direct_object)) || + !zend_opcache_user_cache_shared_graph_calc_reserve_string(context, ce->name) || + !zend_opcache_user_cache_shared_graph_calc_value(context, state_ptr) + ) { + if (!Z_ISUNDEF(state)) { + zval_ptr_dtor(&state); + } + + return false; + } + + properties = zend_std_get_properties(object); + property_count = properties != NULL ? properties->nNumOfElements : 0; + if (property_count != 0 && + !zend_opcache_user_cache_shared_graph_reserve_size( + &context->size, + (size_t) property_count * sizeof(zend_opcache_user_cache_shared_graph_property) + ) + ) { + if (!Z_ISUNDEF(state)) { + zval_ptr_dtor(&state); + } + + return false; + } + + property_index = 0; + result = true; + if (properties != NULL) { + ZEND_HASH_FOREACH_STR_KEY_VAL(properties, property_name, property_value) { + if (property_name == NULL) { + result = false; + + break; + } + + source_value = Z_TYPE_P(property_value) == IS_INDIRECT + ? Z_INDIRECT_P(property_value) + : property_value + ; + + if (zend_opcache_user_cache_shared_graph_should_skip_safe_direct_property( + object, + state_ptr, + property_name, + property_index, + source_value + ) + ) { + ++property_index; + + continue; + } + + if (!zend_opcache_user_cache_shared_graph_calc_reserve_string(context, property_name) || + !zend_opcache_user_cache_shared_graph_calc_value(context, source_value) + ) { + result = false; + + break; + } + + ++property_index; + } ZEND_HASH_FOREACH_END(); + } + + if (!Z_ISUNDEF(state)) { + zval_ptr_dtor(&state); + } + + return result; + } + + if (!zend_opcache_user_cache_shared_graph_can_restore_direct(ce)) { + return false; + } + + if (!zend_opcache_user_cache_shared_graph_mark_seen(&context->seen_objects, object)) { + /* Repeat or cyclic objects are emitted as back-references. */ + return true; + } + + if (!zend_opcache_user_cache_shared_graph_reserve_size(&context->size, sizeof(zend_opcache_user_cache_shared_graph_object))) { + return false; + } + + if (!zend_opcache_user_cache_shared_graph_calc_reserve_string(context, 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_user_cache_shared_graph_reserve_size( + &context->size, + (size_t) property_count * sizeof(zend_opcache_user_cache_shared_graph_property) + ) + ) { + if (properties != NULL) { + zend_release_properties(properties); + } + + return false; + } + + if (properties != NULL) { + property_index = 0; + ZEND_HASH_FOREACH_STR_KEY_VAL(properties, property_name, property_value) { + if (property_name == NULL) { + 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_user_cache_shared_graph_property_is_default( + object, + property_index, + source_value + ) + ) { + ++property_index; + continue; + } + + if (!zend_opcache_user_cache_shared_graph_calc_reserve_string(context, property_name)) { + zend_release_properties(properties); + + return false; + } + + if (!zend_opcache_user_cache_shared_graph_calc_value(context, source_value)) { + zend_release_properties(properties); + + return false; + } + + ++property_index; + } ZEND_HASH_FOREACH_END(); + + zend_release_properties(properties); + } + + return true; + case IS_REFERENCE: { + ref = Z_REF_P(value); + + /* Repeat (shared or cyclic) reference -> back-ref node, no extra payload. */ + if (!zend_opcache_user_cache_shared_graph_mark_seen(&context->seen_references, (zend_object *) ref)) { + return true; + } + + if (!zend_opcache_user_cache_shared_graph_reserve_size(&context->size, + sizeof(zend_opcache_user_cache_shared_graph_reference)) + ) { + return false; + } + + return zend_opcache_user_cache_shared_graph_calc_value(context, &ref->val); + } + default: + return false; + } + +array_done: + zend_hash_index_del(&context->seen_arrays, array_key); + + return result; +} + +static void zend_opcache_user_cache_shared_graph_copy_init( + zend_opcache_user_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); + zend_hash_init(&context->seen_references, 8, NULL, NULL, 0); + zend_hash_init(&context->string_dedup, 8, NULL, NULL, 0); + + context->has_shared_identity = false; + context->has_object = false; + context->prefers_prototype = false; +} + +static void zend_opcache_user_cache_shared_graph_copy_destroy(zend_opcache_user_cache_shared_graph_copy_context *context) +{ + zend_hash_destroy(&context->string_dedup); + zend_hash_destroy(&context->seen_references); + zend_hash_destroy(&context->seen_objects); + zend_hash_destroy(&context->seen_arrays); +} + +static bool zend_opcache_user_cache_shared_graph_copy_alloc( + zend_opcache_user_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_user_cache_shared_graph_copy_string( + zend_opcache_user_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; + zval *cached, cached_offset; + + /* Payload-local string deduplication. */ + cached = zend_hash_find(&context->string_dedup, (zend_string *) string); + if (cached != NULL) { + *offset = (uint32_t) Z_LVAL_P(cached); + + return true; + } + + string_size = _ZSTR_STRUCT_SIZE(ZSTR_LEN(string)); + if (!zend_opcache_user_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; + + ZVAL_LONG(&cached_offset, (zend_long) string_offset); + zend_hash_add(&context->string_dedup, (zend_string *) string, &cached_offset); + + return true; +} + +static bool zend_opcache_user_cache_shared_graph_copy_direct_value( + zend_opcache_user_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_user_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_user_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_user_cache_shared_graph_copy_alloc(context, sizeof(zend_array), &array_offset) || + !zend_opcache_user_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_user_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_user_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_user_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_user_cache_shared_graph_copy_property_value( + zend_opcache_user_cache_shared_graph_copy_context *context, + const zval *source, + zend_opcache_user_cache_shared_graph_value *destination +) +{ + zend_opcache_user_cache_shared_graph_object *graph_object; + zend_opcache_user_cache_shared_graph_safe_direct_object *graph_safe_direct; + zend_opcache_user_cache_shared_graph_property *graph_properties; + zend_opcache_user_cache_shared_graph_array *graph_array; + zend_opcache_user_cache_shared_graph_array_element *graph_elements, *graph_element; + zend_opcache_user_cache_shared_graph_enum *graph_enum; + zend_opcache_user_cache_shared_graph_reference *graph_reference; + zend_reference *ref; + zend_object *object; + zend_class_entry *ce; + zend_string *property_name, *key, *case_name, *self_hash; + zend_ulong array_key, h; + zval *property_value, *source_value, *element, array_value, *safe_direct_state_ptr, safe_direct_state; + HashTable *properties; + uint32_t string_offset, object_offset, class_name_offset, properties_offset, + property_index, property_count, + array_offset, elements_offset, key_offset, + shared_offset, + enum_offset, enum_class_offset, enum_case_offset, + reference_offset, + safe_direct_offset, safe_direct_class_offset, safe_direct_properties_offset + ; + bool result; + void *seen_offset; + + memset(destination, 0, sizeof(*destination)); + + switch (Z_TYPE_P(source)) { + case IS_UNDEF: + destination->type = ZEND_OPCACHE_USER_CACHE_SHARED_GRAPH_VALUE_UNDEF; + + return true; + case IS_NULL: + destination->type = ZEND_OPCACHE_USER_CACHE_SHARED_GRAPH_VALUE_NULL; + + return true; + case IS_TRUE: + destination->type = ZEND_OPCACHE_USER_CACHE_SHARED_GRAPH_VALUE_TRUE; + + return true; + case IS_FALSE: + destination->type = ZEND_OPCACHE_USER_CACHE_SHARED_GRAPH_VALUE_FALSE; + + return true; + case IS_LONG: + destination->type = ZEND_OPCACHE_USER_CACHE_SHARED_GRAPH_VALUE_LONG; + destination->payload.long_value = Z_LVAL_P(source); + + return true; + case IS_DOUBLE: + destination->type = ZEND_OPCACHE_USER_CACHE_SHARED_GRAPH_VALUE_DOUBLE; + destination->payload.double_value = Z_DVAL_P(source); + + return true; + case IS_STRING: + if (!zend_opcache_user_cache_shared_graph_copy_string(context, Z_STR_P(source), &string_offset)) { + return false; + } + + destination->type = ZEND_OPCACHE_USER_CACHE_SHARED_GRAPH_VALUE_STRING; + destination->payload.offset = string_offset; + + return true; + case IS_ARRAY: { + result = true; + + if (Z_ARRVAL_P(source)->nNumOfElements == 0) { + if (Z_ARRVAL_P(source)->nNextFreeElement == 0) { + destination->type = ZEND_OPCACHE_USER_CACHE_SHARED_GRAPH_VALUE_ARRAY; + destination->payload.offset = 0; + + return true; + } + + /* Preserve nNextFreeElement for empty dynamic arrays. */ + if (!zend_opcache_user_cache_shared_graph_copy_alloc(context, sizeof(*graph_array), &array_offset)) { + return false; + } + + graph_array = (zend_opcache_user_cache_shared_graph_array *) (context->buffer + array_offset); + graph_array->count = 0; + graph_array->next_free = (uint32_t) Z_ARRVAL_P(source)->nNextFreeElement; + graph_array->elements_offset = 0; + graph_array->reserved = 0; + destination->type = ZEND_OPCACHE_USER_CACHE_SHARED_GRAPH_VALUE_DYNAMIC_ARRAY; + destination->payload.offset = array_offset; + + return true; + } + + /* calc_value() already accepted immutable arrays. */ + if ((GC_FLAGS(Z_ARRVAL_P(source)) & IS_ARRAY_IMMUTABLE) || + zend_opcache_user_cache_shared_graph_can_copy_direct_value(&context->seen_arrays, source) + ) { + if (!zend_opcache_user_cache_shared_graph_copy_direct_value(context, source, &array_value)) { + return false; + } + + destination->type = ZEND_OPCACHE_USER_CACHE_SHARED_GRAPH_VALUE_ARRAY; + destination->payload.offset = (uint32_t) ((unsigned char *) Z_ARRVAL(array_value) - context->buffer); + + return true; + } + + array_key = (zend_ulong) (uintptr_t) Z_ARRVAL_P(source); + + seen_offset = zend_hash_index_find_ptr(&context->seen_arrays, array_key); + if (seen_offset != NULL) { + /* Back-reference to an array ancestor. */ + shared_offset = (uint32_t) (uintptr_t) seen_offset; + ((zend_opcache_user_cache_shared_graph_array *) (context->buffer + shared_offset))->reserved |= + ZEND_OPCACHE_USER_CACHE_SHARED_GRAPH_OBJECT_FLAG_SHARED + ; + + destination->type = ZEND_OPCACHE_USER_CACHE_SHARED_GRAPH_VALUE_ARRAY_REF; + destination->payload.offset = shared_offset; + context->has_shared_identity = true; + + return true; + } + + if (!zend_opcache_user_cache_shared_graph_copy_alloc(context, sizeof(*graph_array), &array_offset) || + !zend_opcache_user_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_user_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_array->reserved = 0; + + /* Record the offset before recursively encoding elements. */ + if (zend_hash_index_add_ptr(&context->seen_arrays, array_key, (void *) (uintptr_t) array_offset) == NULL) { + result = false; + + goto array_done; + } + + graph_elements = (zend_opcache_user_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_user_cache_shared_graph_copy_string(context, key, &key_offset)) { + result = false; + break; + } + + graph_element->key_offset = key_offset; + } + + if (!zend_opcache_user_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_USER_CACHE_SHARED_GRAPH_VALUE_DYNAMIC_ARRAY; + destination->payload.offset = array_offset; + } + + goto array_done; + } + case IS_OBJECT: + /* Objects and enums may resolve classes during decode. */ + context->has_object = true; + ce = Z_OBJCE_P(source); + + if (ce->ce_flags & ZEND_ACC_ENUM) { + case_name = Z_STR_P(zend_enum_fetch_case_name(Z_OBJ_P(source))); + + if (!zend_opcache_user_cache_shared_graph_copy_alloc(context, + sizeof(zend_opcache_user_cache_shared_graph_enum), &enum_offset) || + !zend_opcache_user_cache_shared_graph_copy_string(context, ce->name, &enum_class_offset) || + !zend_opcache_user_cache_shared_graph_copy_string(context, case_name, &enum_case_offset) + ) { + return false; + } + + graph_enum = (zend_opcache_user_cache_shared_graph_enum *) (context->buffer + enum_offset); + graph_enum->class_name_offset = enum_class_offset; + graph_enum->case_name_offset = enum_case_offset; + + destination->type = ZEND_OPCACHE_USER_CACHE_SHARED_GRAPH_VALUE_ENUM; + destination->payload.offset = enum_offset; + + return true; + } + + object = Z_OBJ_P(source); + if (zend_opcache_user_cache_shared_graph_can_use_safe_direct(object->ce)) { + if (zend_opcache_user_cache_safe_direct_prefers_request_local_prototype(object->ce)) { + context->prefers_prototype = true; + } + + seen_offset = zend_hash_index_find_ptr(&context->seen_objects, (zend_ulong) (uintptr_t) object); + if (seen_offset != NULL) { + shared_offset = (uint32_t) (uintptr_t) seen_offset; + ((zend_opcache_user_cache_shared_graph_safe_direct_object *) (context->buffer + shared_offset))->reserved |= + ZEND_OPCACHE_USER_CACHE_SHARED_GRAPH_OBJECT_FLAG_SHARED + ; + + destination->type = ZEND_OPCACHE_USER_CACHE_SHARED_GRAPH_VALUE_OBJECT_REF; + destination->payload.offset = shared_offset; + context->has_shared_identity = true; + + return true; + } + + if (zend_opcache_user_cache_shared_graph_get_safe_direct_state( + context->state_memo, + source, + &safe_direct_state_ptr, + &safe_direct_state + ) + ) { + if (!zend_opcache_user_cache_shared_graph_copy_alloc(context, + sizeof(zend_opcache_user_cache_shared_graph_safe_direct_object), &safe_direct_offset) || + !zend_opcache_user_cache_shared_graph_copy_string(context, object->ce->name, &safe_direct_class_offset) + ) { + if (!Z_ISUNDEF(safe_direct_state)) { + zval_ptr_dtor(&safe_direct_state); + } + + return false; + } + + if (zend_hash_index_add_ptr(&context->seen_objects, (zend_ulong) (uintptr_t) object, + (void *) (uintptr_t) safe_direct_offset) == NULL + ) { + if (!Z_ISUNDEF(safe_direct_state)) { + zval_ptr_dtor(&safe_direct_state); + } + + return false; + } + + graph_safe_direct = (zend_opcache_user_cache_shared_graph_safe_direct_object *) (context->buffer + safe_direct_offset); + graph_safe_direct->class_name_offset = safe_direct_class_offset; + graph_safe_direct->reserved = 0; + + if (!zend_opcache_user_cache_shared_graph_copy_property_value(context, safe_direct_state_ptr, &graph_safe_direct->state)) { + if (!Z_ISUNDEF(safe_direct_state)) { + zval_ptr_dtor(&safe_direct_state); + } + + return false; + } + + properties = zend_std_get_properties(object); + property_count = properties != NULL ? properties->nNumOfElements : 0; + graph_safe_direct->property_count = property_count; + graph_safe_direct->properties_offset = 0; + + if (property_count != 0) { + if (!zend_opcache_user_cache_shared_graph_copy_alloc( + context, + (size_t) property_count * sizeof(zend_opcache_user_cache_shared_graph_property), + &safe_direct_properties_offset + ) + ) { + if (!Z_ISUNDEF(safe_direct_state)) { + zval_ptr_dtor(&safe_direct_state); + } + + return false; + } + + graph_safe_direct->properties_offset = safe_direct_properties_offset; + graph_properties = (zend_opcache_user_cache_shared_graph_property *) (context->buffer + safe_direct_properties_offset); + property_index = 0; + result = true; + + self_hash = NULL; + ZEND_HASH_FOREACH_STR_KEY_VAL(properties, property_name, property_value) { + graph_properties[property_index].name_offset = 0; + graph_properties[property_index].reserved = 0; + graph_properties[property_index].value.type = ZEND_OPCACHE_USER_CACHE_SHARED_GRAPH_VALUE_UNDEF; + + if (property_name == NULL) { + result = false; + + break; + } + + source_value = Z_TYPE_P(property_value) == IS_INDIRECT + ? Z_INDIRECT_P(property_value) + : property_value + ; + + if (zend_opcache_user_cache_shared_graph_should_skip_safe_direct_property( + object, + safe_direct_state_ptr, + property_name, + property_index, + source_value + ) + ) { + ++property_index; + continue; + } + + if (!zend_opcache_user_cache_shared_graph_copy_string(context, property_name, &string_offset)) { + result = false; + + break; + } + graph_properties[property_index].name_offset = string_offset; + + if (zend_opcache_user_cache_shared_graph_property_is_self_object_hash( + object, + property_index, + source_value, + &self_hash + ) + ) { + graph_properties[property_index].reserved |= + ZEND_OPCACHE_USER_CACHE_SHARED_GRAPH_PROPERTY_FLAG_SELF_OBJECT_HASH + ; + } + + if (!zend_opcache_user_cache_shared_graph_copy_property_value( + context, + source_value, + &graph_properties[property_index].value + ) + ) { + result = false; + + break; + } + + ++property_index; + } ZEND_HASH_FOREACH_END(); + + if (self_hash != NULL) { + zend_string_release(self_hash); + } + + if (!result) { + if (!Z_ISUNDEF(safe_direct_state)) { + zval_ptr_dtor(&safe_direct_state); + } + + return false; + } + } + + if (!Z_ISUNDEF(safe_direct_state)) { + zval_ptr_dtor(&safe_direct_state); + } + + destination->type = ZEND_OPCACHE_USER_CACHE_SHARED_GRAPH_VALUE_SAFE_DIRECT_OBJECT; + destination->payload.offset = safe_direct_offset; + + return true; + } + } + + if (!zend_opcache_user_cache_shared_graph_can_restore_direct(object->ce)) { + return false; + } + + context->prefers_prototype = true; + + seen_offset = zend_hash_index_find_ptr(&context->seen_objects, (zend_ulong) (uintptr_t) object); + if (seen_offset != NULL) { + /* Back-reference to an object already emitted. */ + shared_offset = (uint32_t) (uintptr_t) seen_offset; + ((zend_opcache_user_cache_shared_graph_object *) (context->buffer + shared_offset))->reserved |= + ZEND_OPCACHE_USER_CACHE_SHARED_GRAPH_OBJECT_FLAG_SHARED + ; + + destination->type = ZEND_OPCACHE_USER_CACHE_SHARED_GRAPH_VALUE_OBJECT_REF; + destination->payload.offset = shared_offset; + context->has_shared_identity = true; + + return true; + } + + if (!zend_opcache_user_cache_shared_graph_copy_alloc(context, sizeof(*graph_object), &object_offset)) { + return false; + } + + /* Record the node offset before encoding properties so a cyclic or + * shared reference reached while encoding them resolves to a back-ref. */ + if (zend_hash_index_add_ptr(&context->seen_objects, (zend_ulong) (uintptr_t) object, + (void *) (uintptr_t) object_offset) == NULL + ) { + return false; + } + + if (!zend_opcache_user_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_user_cache_shared_graph_copy_alloc( + context, + ((size_t) property_count * sizeof(zend_opcache_user_cache_shared_graph_property)), + &properties_offset + ) + ) { + if (properties != NULL) { + zend_release_properties(properties); + } + + return false; + } + + graph_object = (zend_opcache_user_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; + + /* A later back-reference sets FLAG_SHARED. */ + graph_object->reserved = 0; + + if (property_count == 0) { + if (properties != NULL) { + zend_release_properties(properties); + } + + destination->type = ZEND_OPCACHE_USER_CACHE_SHARED_GRAPH_VALUE_OBJECT; + destination->payload.offset = object_offset; + + return true; + } + + graph_properties = (zend_opcache_user_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_release_properties(properties); + + return false; + } + + graph_properties[property_index].name_offset = 0; + graph_properties[property_index].reserved = 0; + graph_properties[property_index].value.type = ZEND_OPCACHE_USER_CACHE_SHARED_GRAPH_VALUE_UNDEF; + + source_value = Z_TYPE_P(property_value) == IS_INDIRECT + ? Z_INDIRECT_P(property_value) + : property_value + ; + + if (zend_opcache_user_cache_shared_graph_property_is_default( + object, + property_index, + source_value + ) + ) { + ++property_index; + continue; + } + + if (!zend_opcache_user_cache_shared_graph_copy_string(context, property_name, &graph_properties[property_index].name_offset)) { + zend_release_properties(properties); + + return false; + } + + if (!zend_opcache_user_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_USER_CACHE_SHARED_GRAPH_VALUE_OBJECT; + destination->payload.offset = object_offset; + + return true; + case IS_REFERENCE: { + ref = Z_REF_P(source); + seen_offset = zend_hash_index_find_ptr(&context->seen_references, (zend_ulong) (uintptr_t) ref); + + if (seen_offset != NULL) { + /* Shared/cyclic reference already emitted: flag it, write a back-ref. */ + shared_offset = (uint32_t) (uintptr_t) seen_offset; + ((zend_opcache_user_cache_shared_graph_reference *) (context->buffer + shared_offset))->flags |= + ZEND_OPCACHE_USER_CACHE_SHARED_GRAPH_OBJECT_FLAG_SHARED + ; + + destination->type = ZEND_OPCACHE_USER_CACHE_SHARED_GRAPH_VALUE_REFERENCE_REF; + destination->payload.offset = shared_offset; + context->has_shared_identity = true; + + return true; + } + + if (!zend_opcache_user_cache_shared_graph_copy_alloc(context, + sizeof(zend_opcache_user_cache_shared_graph_reference), &reference_offset) + ) { + return false; + } + + /* Record the offset before encoding the inner value so a cycle through + * this reference resolves to a back-ref. */ + if (zend_hash_index_add_ptr(&context->seen_references, (zend_ulong) (uintptr_t) ref, + (void *) (uintptr_t) reference_offset) == NULL + ) { + return false; + } + + graph_reference = (zend_opcache_user_cache_shared_graph_reference *) (context->buffer + reference_offset); + graph_reference->flags = 0; + graph_reference->reserved = 0; + + if (!zend_opcache_user_cache_shared_graph_copy_property_value(context, &ref->val, &graph_reference->inner)) { + return false; + } + + destination->type = ZEND_OPCACHE_USER_CACHE_SHARED_GRAPH_VALUE_REFERENCE; + destination->payload.offset = reference_offset; + context->has_shared_identity = true; + + return true; + } + default: + return false; + } + +array_done: + zend_hash_index_del(&context->seen_arrays, array_key); + + return result; +} + +static bool zend_opcache_user_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_VIRTUAL)) != 0 || + property_info->offset == ZEND_VIRTUAL_PROPERTY_OFFSET + ) { + return false; + } + + slot = OBJ_PROP(object, property_info->offset); + + if (Z_ISREF_P(property_value)) { + /* Preserve aliased properties by installing the reference itself. */ + if (ZEND_TYPE_IS_SET(property_info->type)) { + if (!zend_verify_prop_assignable_by_ref(property_info, property_value, true)) { + *failed = true; + + return false; + } + + ZEND_REF_ADD_TYPE_SOURCE(Z_REF_P(property_value), property_info); + } + + zval_ptr_dtor(slot); + ZVAL_COPY(slot, property_value); + + if (object->properties != NULL) { + ZVAL_INDIRECT(&indirect, slot); + zend_hash_update(object->properties, property_name, &indirect); + } + + return true; + } + + /* Other property kinds use the standard update path. */ + if ((property_info->flags & (ZEND_ACC_READONLY|ZEND_ACC_PPP_SET_MASK)) != 0 || + (property_info->flags & ZEND_ACC_PPP_MASK) != ZEND_ACC_PUBLIC + ) { + return false; + } + + 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_user_cache_shared_graph_update_object_property( + zval *object_zv, + zend_string *property_name, + zval *property_value +) +{ + const char *class_name, *prop_name; + zend_string *cname; + zend_class_entry *scope; + zend_object *object; + zend_property_info *property_info; + HashTable *properties; + size_t prop_name_len; + 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_user_cache_shared_graph_try_update_declared_object_property( + object, + property_name, + property_info, + property_value, + &failed) + ) { + return true; + } + + if (failed) { + return false; + } + + /* Preserve aliased dynamic properties. */ + if (Z_ISREF_P(property_value)) { + properties = zend_std_get_properties(object); + + if (properties != NULL) { + Z_TRY_ADDREF_P(property_value); + + zend_hash_update(properties, property_name, property_value); + + return true; + } + } + + zend_update_property(object->ce, object, ZSTR_VAL(property_name), ZSTR_LEN(property_name), property_value); + + return !EG(exception); + } + + /* Private/protected property name. */ + if (zend_unmangle_property_name_ex(property_name, &class_name, &prop_name, &prop_name_len) == SUCCESS) { + if (class_name[0] != '*') { + cname = zend_string_init(class_name, strlen(class_name), 0); + scope = zend_lookup_class(cname); + + if (scope != NULL) { + zend_update_property(scope, object, prop_name, prop_name_len, property_value); + } + + zend_string_release_ex(cname, 0); + } else { + zend_update_property(object->ce, object, prop_name, prop_name_len, property_value); + } + } + + return !EG(exception); +} + +static bool zend_opcache_user_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_user_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_user_cache_shared_graph_update_object_property(object_zv, property_name, property_value); +} + +static bool zend_opcache_user_cache_fetch_shared_graph_value( + const unsigned char *buffer, + const zend_opcache_user_cache_shared_graph_value *value, + zval *destination +) +{ + const zend_opcache_user_cache_shared_graph_object *graph_object; + const zend_opcache_user_cache_shared_graph_array *graph_array; + const zend_opcache_user_cache_shared_graph_array_element *graph_elements, *graph_element; + const zend_opcache_user_cache_shared_graph_property *properties, *property; + const zend_opcache_user_cache_shared_graph_safe_direct_object *graph_safe_direct; + const zend_opcache_user_cache_shared_graph_enum *graph_enum; + const zend_opcache_user_cache_shared_graph_reference *graph_reference; + zend_class_entry *ce, *enum_ce; + zend_string *class_name, *property_name, *enum_class_name, *enum_case_name; + zend_reference *ref, *shared_reference; + zend_array *shared_array; + zend_object *shared_object, *case_obj; + zval property_value, safe_direct_state; + uint32_t index; + + switch (value->type) { + case ZEND_OPCACHE_USER_CACHE_SHARED_GRAPH_VALUE_UNDEF: + ZVAL_UNDEF(destination); + return true; + case ZEND_OPCACHE_USER_CACHE_SHARED_GRAPH_VALUE_NULL: + ZVAL_NULL(destination); + return true; + case ZEND_OPCACHE_USER_CACHE_SHARED_GRAPH_VALUE_TRUE: + ZVAL_TRUE(destination); + return true; + case ZEND_OPCACHE_USER_CACHE_SHARED_GRAPH_VALUE_FALSE: + ZVAL_FALSE(destination); + return true; + case ZEND_OPCACHE_USER_CACHE_SHARED_GRAPH_VALUE_LONG: + ZVAL_LONG(destination, value->payload.long_value); + return true; + case ZEND_OPCACHE_USER_CACHE_SHARED_GRAPH_VALUE_DOUBLE: + ZVAL_DOUBLE(destination, value->payload.double_value); + return true; + case ZEND_OPCACHE_USER_CACHE_SHARED_GRAPH_VALUE_STRING: + ZVAL_INTERNED_STR(destination, (zend_string *) (void *) (buffer + (uint32_t) value->payload.offset)); + return true; + case ZEND_OPCACHE_USER_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; + } + + return true; + case ZEND_OPCACHE_USER_CACHE_SHARED_GRAPH_VALUE_DYNAMIC_ARRAY: + graph_array = (const zend_opcache_user_cache_shared_graph_array *) (buffer + (uint32_t) value->payload.offset); + array_init_size(destination, graph_array->count); + + /* Register before filling to resolve cycles. */ + if (graph_array->reserved & ZEND_OPCACHE_USER_CACHE_SHARED_GRAPH_OBJECT_FLAG_SHARED) { + if (!zend_opcache_user_cache_decode_array_map_insert((uint32_t) value->payload.offset, Z_ARRVAL_P(destination))) { + zval_ptr_dtor(destination); + + ZVAL_UNDEF(destination); + + return false; + } + + HT_ALLOW_COW_VIOLATION(Z_ARRVAL_P(destination)); + } + + graph_elements = (const zend_opcache_user_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_user_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); + + HT_ALLOW_COW_VIOLATION(Z_ARRVAL_P(destination)); + + 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 { + HT_ALLOW_COW_VIOLATION(Z_ARRVAL_P(destination)); + + 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; +#if ZEND_DEBUG + HT_FLAGS(Z_ARRVAL_P(destination)) &= ~HASH_FLAG_ALLOW_COW_VIOLATION; +#endif + return true; + case ZEND_OPCACHE_USER_CACHE_SHARED_GRAPH_VALUE_ARRAY_REF: { + shared_array = zend_opcache_user_cache_decode_array_map_find((uint32_t) value->payload.offset); + if (shared_array == NULL) { + return false; + } + + ZVAL_ARR(destination, shared_array); + GC_ADDREF(shared_array); + + return true; + } + case ZEND_OPCACHE_USER_CACHE_SHARED_GRAPH_VALUE_OBJECT: + graph_object = (const zend_opcache_user_cache_shared_graph_object *) (buffer + (uint32_t) value->payload.offset); + class_name = (zend_string *) (void *) (buffer + graph_object->class_name_offset); + ce = zend_opcache_user_cache_decode_lookup_class(class_name); + + if (ce == NULL || object_init_ex(destination, ce) != SUCCESS) { + return false; + } + + /* Register before decoding properties to resolve back-references. */ + if ((graph_object->reserved & ZEND_OPCACHE_USER_CACHE_SHARED_GRAPH_OBJECT_FLAG_SHARED) && + !zend_opcache_user_cache_decode_identity_map_insert((uint32_t) value->payload.offset, Z_OBJ_P(destination)) + ) { + zval_ptr_dtor(destination); + + ZVAL_UNDEF(destination); + + return false; + } + + if (graph_object->property_count == 0) { + return true; + } + + properties = (const zend_opcache_user_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_USER_CACHE_SHARED_GRAPH_VALUE_UNDEF) { + continue; + } + + property_name = (zend_string *) (void *) (buffer + property->name_offset); + + ZVAL_UNDEF(&property_value); + + if (!zend_opcache_user_cache_fetch_shared_graph_value(buffer, &property->value, &property_value) || + !zend_opcache_user_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); + } + + return true; + case ZEND_OPCACHE_USER_CACHE_SHARED_GRAPH_VALUE_OBJECT_REF: { + shared_object = zend_opcache_user_cache_decode_identity_map_find((uint32_t) value->payload.offset); + if (shared_object == NULL) { + return false; + } + + ZVAL_OBJ(destination, shared_object); + GC_ADDREF(shared_object); + + return true; + } + case ZEND_OPCACHE_USER_CACHE_SHARED_GRAPH_VALUE_SAFE_DIRECT_OBJECT: + graph_safe_direct = (const zend_opcache_user_cache_shared_graph_safe_direct_object *) (buffer + (uint32_t) value->payload.offset); + + class_name = (zend_string *) (void *) (buffer + graph_safe_direct->class_name_offset); + ce = zend_opcache_user_cache_decode_lookup_class(class_name); + if (ce == NULL || + zend_opcache_user_cache_safe_direct_state_unserialize_func(ce) == NULL || + object_init_ex(destination, ce) != SUCCESS + ) { + return false; + } + + if ((graph_safe_direct->reserved & ZEND_OPCACHE_USER_CACHE_SHARED_GRAPH_OBJECT_FLAG_SHARED) && + !zend_opcache_user_cache_decode_identity_map_insert((uint32_t) value->payload.offset, Z_OBJ_P(destination)) + ) { + zval_ptr_dtor(destination); + + ZVAL_UNDEF(destination); + + return false; + } + + ZVAL_UNDEF(&safe_direct_state); + if (!zend_opcache_user_cache_fetch_shared_graph_value(buffer, &graph_safe_direct->state, &safe_direct_state) || + Z_TYPE(safe_direct_state) != IS_ARRAY || + !zend_opcache_user_cache_safe_direct_state_unserialize_func(ce)(destination, &safe_direct_state) + ) { + zval_ptr_dtor(&safe_direct_state); + zval_ptr_dtor(destination); + + ZVAL_UNDEF(destination); + + return false; + } + zval_ptr_dtor(&safe_direct_state); + + if (graph_safe_direct->property_count != 0) { + properties = (const zend_opcache_user_cache_shared_graph_property *) (buffer + graph_safe_direct->properties_offset); + for (index = 0; index < graph_safe_direct->property_count; index++) { + property = &properties[index]; + if (property->value.type == ZEND_OPCACHE_USER_CACHE_SHARED_GRAPH_VALUE_UNDEF) { + continue; + } + + property_name = (zend_string *) (void *) (buffer + property->name_offset); + ZVAL_UNDEF(&property_value); + + if (property->reserved & ZEND_OPCACHE_USER_CACHE_SHARED_GRAPH_PROPERTY_FLAG_SELF_OBJECT_HASH) { + ZVAL_STR(&property_value, php_spl_object_hash(Z_OBJ_P(destination))); + } else if (!zend_opcache_user_cache_fetch_shared_graph_value(buffer, &property->value, &property_value)) { + zval_ptr_dtor(destination); + + ZVAL_UNDEF(destination); + + return false; + } + + if (!zend_opcache_user_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); + } + } + + return true; + case ZEND_OPCACHE_USER_CACHE_SHARED_GRAPH_VALUE_ENUM: { + graph_enum = (const zend_opcache_user_cache_shared_graph_enum *) (buffer + (uint32_t) value->payload.offset); + + /* Cache by pinned enum node address. */ + case_obj = zend_opcache_user_cache_decode_resolve_cache_find(graph_enum); + if (case_obj == NULL) { + enum_class_name = (zend_string *) (void *) (buffer + graph_enum->class_name_offset); + enum_case_name = (zend_string *) (void *) (buffer + graph_enum->case_name_offset); + enum_ce = zend_opcache_user_cache_decode_lookup_class(enum_class_name); + + if (enum_ce == NULL || !(enum_ce->ce_flags & ZEND_ACC_ENUM)) { + return false; + } + + case_obj = zend_enum_get_case(enum_ce, enum_case_name); + if (case_obj == NULL) { + return false; + } + + zend_opcache_user_cache_decode_resolve_cache_store(graph_enum, case_obj); + } + + ZVAL_OBJ(destination, case_obj); + GC_ADDREF(case_obj); + + return true; + } + case ZEND_OPCACHE_USER_CACHE_SHARED_GRAPH_VALUE_REFERENCE: { + graph_reference = (const zend_opcache_user_cache_shared_graph_reference *) (buffer + (uint32_t) value->payload.offset); + + ZVAL_NEW_EMPTY_REF(destination); + ref = Z_REF_P(destination); + + ZVAL_UNDEF(&ref->val); + + /* Register before decoding the inner value so a cycle or sibling that + * aliases this reference resolves to the same zend_reference. Only + * shared/cyclic references carry back-refs, so others need no entry. */ + if ((graph_reference->flags & ZEND_OPCACHE_USER_CACHE_SHARED_GRAPH_OBJECT_FLAG_SHARED) && + !zend_opcache_user_cache_decode_reference_map_insert((uint32_t) value->payload.offset, ref) + ) { + zval_ptr_dtor(destination); + + ZVAL_UNDEF(destination); + + return false; + } + + if (!zend_opcache_user_cache_fetch_shared_graph_value(buffer, &graph_reference->inner, &ref->val)) { + zval_ptr_dtor(destination); + + ZVAL_UNDEF(destination); + + return false; + } + + return true; + } + case ZEND_OPCACHE_USER_CACHE_SHARED_GRAPH_VALUE_REFERENCE_REF: { + shared_reference = zend_opcache_user_cache_decode_reference_map_find((uint32_t) value->payload.offset); + + if (shared_reference == NULL) { + return false; + } + + ZVAL_REF(destination, shared_reference); + GC_ADDREF(shared_reference); + + return true; + } + default: + return false; + } +} + +static zend_opcache_user_cache_shared_graph_header *zend_opcache_user_cache_shared_graph_payload_header(uint32_t payload_offset) +{ + const unsigned char *graph_buffer; + zend_opcache_user_cache_shared_graph_header *header; + size_t buffer_len; + + if (payload_offset == 0) { + return NULL; + } + + buffer_len = zend_opcache_user_cache_block_payload_capacity(payload_offset); + if (buffer_len == 0) { + return NULL; + } + + graph_buffer = zend_opcache_user_cache_shared_graph_locate_buffer( + (const unsigned char *) zend_opcache_user_cache_ptr(payload_offset), + buffer_len, + NULL + ); + if (graph_buffer == NULL) { + return NULL; + } + + header = (zend_opcache_user_cache_shared_graph_header *) graph_buffer; + if (header->magic != ZEND_OPCACHE_USER_CACHE_SHARED_GRAPH_MAGIC || + header->version != ZEND_OPCACHE_USER_CACHE_SHARED_GRAPH_VERSION + ) { + return NULL; + } + + return header; +} + +/* Caller holds the context write lock with an initialized header. */ +static bool zend_opcache_user_cache_free_retired_for_context_locked(zend_opcache_user_cache_context *context) +{ + zend_opcache_user_cache_shared_graph_ref *ref; + uint32_t index; + bool freed = false; + bool graph_quiescent = zend_opcache_user_cache_graph_payloads_quiescent_locked(); + + for (index = 0; index < zend_opcache_user_cache_retired_shared_graph_count; index++) { + ref = &zend_opcache_user_cache_retired_shared_graphs[index]; + + if (ref->context != context) { + continue; + } + + if (graph_quiescent) { + zend_opcache_user_cache_free_locked(ref->payload_offset); + freed = true; + } else { + zend_opcache_user_cache_orphan_graph_payload_locked(ref->payload_offset); + } + + ref->context = NULL; + } + + zend_opcache_user_cache_reclaim_orphaned_graphs_locked(); + + return freed; +} + +static bool zend_opcache_user_cache_free_retired_shared_graphs(void) +{ + zend_opcache_user_cache_context *context, *previous_context; + uint32_t index; + bool freed = false; + + if (zend_opcache_user_cache_retired_shared_graph_count == 0) { + return false; + } + + /* Group pending frees by context. */ + for (index = 0; index < zend_opcache_user_cache_retired_shared_graph_count; index++) { + context = zend_opcache_user_cache_retired_shared_graphs[index].context; + + if (context == NULL) { + continue; + } + + previous_context = zend_opcache_user_cache_activate_context(context); + + if (zend_opcache_user_cache_wlock()) { + if (zend_opcache_user_cache_header_is_initialized_locked()) { + freed = zend_opcache_user_cache_free_retired_for_context_locked(context) || freed; + } + + zend_opcache_user_cache_unlock(); + } + + zend_opcache_user_cache_restore_context(previous_context); + } + + efree(zend_opcache_user_cache_retired_shared_graphs); + + zend_opcache_user_cache_retired_shared_graphs = NULL; + zend_opcache_user_cache_retired_shared_graph_count = 0; + zend_opcache_user_cache_retired_shared_graph_capacity = 0; + + return freed; +} + +static bool zend_opcache_user_cache_shared_graph_rebase_direct_zval( + zval *value, + const unsigned char *old_base, + const unsigned char *new_base, + size_t len, + ptrdiff_t delta, + HashTable *seen_arrays) +{ + zend_array *array; + + switch (Z_TYPE_P(value)) { + case IS_STRING: + Z_STR_P(value) = (zend_string *) zend_opcache_user_cache_shared_graph_rebase_pointer( + Z_STR_P(value), + old_base, + len, + delta + ); + return true; + case IS_ARRAY: + array = (zend_array *) zend_opcache_user_cache_shared_graph_rebase_pointer( + Z_ARR_P(value), + old_base, + len, + delta + ); + + Z_ARR_P(value) = array; + + if (!zend_opcache_user_cache_shared_graph_pointer_in_range(array, new_base, len)) { + return true; + } + + return zend_opcache_user_cache_shared_graph_rebase_direct_array( + array, + old_base, + new_base, + len, + delta, + seen_arrays + ); + default: + return true; + } +} + +static bool zend_opcache_user_cache_shared_graph_rebase_direct_array( + zend_array *array, + const unsigned char *old_base, + const unsigned char *new_base, + size_t len, + ptrdiff_t delta, + HashTable *seen_arrays) +{ + zend_ulong key; + zval *packed; + Bucket *bucket; + uint32_t index; + void *data; + + if (!zend_opcache_user_cache_shared_graph_pointer_in_range(array, new_base, len)) { + return true; + } + + key = (zend_ulong) (uintptr_t) array; + if (zend_hash_index_exists(seen_arrays, key)) { + return true; + } + + if (zend_hash_index_add_empty_element(seen_arrays, key) == NULL) { + return false; + } + + data = HT_GET_DATA_ADDR(array); + data = zend_opcache_user_cache_shared_graph_rebase_pointer(data, old_base, len, delta); + + HT_SET_DATA_ADDR(array, data); + + if (!zend_opcache_user_cache_shared_graph_pointer_in_range(data, new_base, len)) { + return false; + } + + if (HT_IS_PACKED(array)) { + packed = array->arPacked; + for (index = 0; index < array->nNumUsed; index++) { + if (!zend_opcache_user_cache_shared_graph_rebase_direct_zval( + &packed[index], + old_base, + new_base, + len, + delta, + seen_arrays + ) + ) { + return false; + } + } + } else { + bucket = array->arData; + for (index = 0; index < array->nNumUsed; index++) { + if (bucket[index].key != NULL) { + bucket[index].key = (zend_string *) zend_opcache_user_cache_shared_graph_rebase_pointer( + bucket[index].key, + old_base, + len, + delta + ); + + if (!zend_opcache_user_cache_shared_graph_pointer_in_range(bucket[index].key, new_base, len)) { + return false; + } + } + + if (!zend_opcache_user_cache_shared_graph_rebase_direct_zval( + &bucket[index].val, + old_base, + new_base, + len, + delta, + seen_arrays + ) + ) { + return false; + } + } + } + + return true; +} + +static bool zend_opcache_user_cache_shared_graph_rebase_graph_value( + const unsigned char *buffer, + const zend_opcache_user_cache_shared_graph_value *value, + const unsigned char *old_base, + const unsigned char *new_base, + size_t len, + ptrdiff_t delta, + HashTable *seen_arrays) +{ + const zend_opcache_user_cache_shared_graph_array *graph_array; + const zend_opcache_user_cache_shared_graph_array_element *graph_elements; + const zend_opcache_user_cache_shared_graph_object *graph_object; + const zend_opcache_user_cache_shared_graph_property *properties; + const zend_opcache_user_cache_shared_graph_safe_direct_object *graph_safe_direct; + zend_array *array; + uint32_t index; + + switch (value->type) { + case ZEND_OPCACHE_USER_CACHE_SHARED_GRAPH_VALUE_ARRAY: + if ((uint32_t) value->payload.offset == 0) { + return true; + } + + array = (zend_array *) (void *) (buffer + (uint32_t) value->payload.offset); + + return zend_opcache_user_cache_shared_graph_rebase_direct_array( + array, + old_base, + new_base, + len, + delta, + seen_arrays + ); + case ZEND_OPCACHE_USER_CACHE_SHARED_GRAPH_VALUE_DYNAMIC_ARRAY: + graph_array = (const zend_opcache_user_cache_shared_graph_array *) (buffer + (uint32_t) value->payload.offset); + graph_elements = (const zend_opcache_user_cache_shared_graph_array_element *) (buffer + graph_array->elements_offset); + + for (index = 0; index < graph_array->count; index++) { + if (!zend_opcache_user_cache_shared_graph_rebase_graph_value( + buffer, + &graph_elements[index].value, + old_base, + new_base, + len, + delta, + seen_arrays + ) + ) { + return false; + } + } + return true; + case ZEND_OPCACHE_USER_CACHE_SHARED_GRAPH_VALUE_OBJECT: + graph_object = (const zend_opcache_user_cache_shared_graph_object *) (buffer + (uint32_t) value->payload.offset); + properties = (const zend_opcache_user_cache_shared_graph_property *) (buffer + graph_object->properties_offset); + for (index = 0; index < graph_object->property_count; index++) { + if (!zend_opcache_user_cache_shared_graph_rebase_graph_value( + buffer, + &properties[index].value, + old_base, + new_base, + len, + delta, + seen_arrays + ) + ) { + return false; + } + } + + return true; + case ZEND_OPCACHE_USER_CACHE_SHARED_GRAPH_VALUE_REFERENCE: + /* References can hold direct arrays with in-buffer pointers. */ + return zend_opcache_user_cache_shared_graph_rebase_graph_value( + buffer, + &((const zend_opcache_user_cache_shared_graph_reference *) (buffer + (uint32_t) value->payload.offset))->inner, + old_base, + new_base, + len, + delta, + seen_arrays + ); + case ZEND_OPCACHE_USER_CACHE_SHARED_GRAPH_VALUE_SAFE_DIRECT_OBJECT: { + graph_safe_direct = + (const zend_opcache_user_cache_shared_graph_safe_direct_object *) (buffer + (uint32_t) value->payload.offset) + ; + + if (!zend_opcache_user_cache_shared_graph_rebase_graph_value( + buffer, + &graph_safe_direct->state, + old_base, + new_base, + len, + delta, + seen_arrays + ) + ) { + return false; + } + + properties = (const zend_opcache_user_cache_shared_graph_property *) (buffer + graph_safe_direct->properties_offset); + for (index = 0; index < graph_safe_direct->property_count; index++) { + if (!zend_opcache_user_cache_shared_graph_rebase_graph_value( + buffer, + &properties[index].value, + old_base, + new_base, + len, + delta, + seen_arrays + ) + ) { + return false; + } + } + + return true; + } + default: + return true; + } +} + +/* Prevent new pins after a reader-drain timeout. */ +static void zend_opcache_user_cache_shared_graph_force_retire_locked(uint32_t payload_offset) +{ + zend_opcache_user_cache_shared_graph_header *header = zend_opcache_user_cache_shared_graph_payload_header(payload_offset); + int state, expected; + + if (header == NULL) { + return; + } + + for (;;) { + state = zend_atomic_int_load_ex(&header->ref_state); + + if ((state & ZEND_OPCACHE_USER_CACHE_SHARED_GRAPH_RETIRED) != 0) { + return; + } + + expected = state; + if (zend_atomic_int_compare_exchange_ex(&header->ref_state, &expected, state | ZEND_OPCACHE_USER_CACHE_SHARED_GRAPH_RETIRED)) { + return; + } + } +} + +static bool zend_opcache_user_cache_orphaned_graph_block_is_referenced_locked( + zend_opcache_user_cache_header *header, + uint32_t block_offset, + uint32_t block_size +) +{ + zend_opcache_user_cache_entry *entries, *entry; + uint32_t index; + + entries = zend_opcache_user_cache_entries(header); + for (index = 0; index < header->capacity; index++) { + entry = &entries[index]; + if (entry->state != ZEND_OPCACHE_USER_CACHE_ENTRY_USED) { + continue; + } + + if (entry->key_offset != 0 && + zend_opcache_user_cache_shared_graph_offset_in_block(entry->key_offset, block_offset, block_size) + ) { + return true; + } + + if (entry->value_offset != 0 && + zend_opcache_user_cache_shared_graph_offset_in_block(entry->value_offset, block_offset, block_size) + ) { + return true; + } + } + + return false; +} + +static bool zend_opcache_user_cache_reclaim_saturated_orphaned_graphs_locked(zend_opcache_user_cache_header *header) +{ + zend_opcache_user_cache_block *block; + zend_opcache_user_cache_shared_graph_header *graph_header; + uint32_t used_end, offset, block_size, payload_offset; + bool reclaimed = false, restart; + + do { + restart = false; + used_end = header->data_offset + header->next_free; + offset = header->data_offset; + + while (offset < used_end) { + block = zend_opcache_user_cache_block_ptr(offset); + block_size = block->size; + + if (block_size < ZEND_ALIGNED_SIZE(sizeof(zend_opcache_user_cache_block) + 1) || + block_size > used_end - offset + ) { + return reclaimed; + } + + if (!zend_opcache_user_cache_block_is_free(block) && + !zend_opcache_user_cache_orphaned_graph_block_is_referenced_locked(header, offset, block_size) + ) { + payload_offset = offset + (uint32_t) sizeof(zend_opcache_user_cache_block); + graph_header = zend_opcache_user_cache_shared_graph_payload_header(payload_offset); + + if (graph_header != NULL && + zend_atomic_int_load_ex(&graph_header->ref_state) == (int) ZEND_OPCACHE_USER_CACHE_SHARED_GRAPH_RETIRED + ) { + zend_opcache_user_cache_free_locked(payload_offset); + reclaimed = true; + restart = true; + + break; + } + } + + offset += block_size; + } + } while (restart); + + return reclaimed; +} + +void zend_opcache_user_cache_decode_resolve_cache_release(void) +{ + if (zend_opcache_user_cache_decode_resolve_cache == NULL) { + return; + } + + zend_hash_destroy(zend_opcache_user_cache_decode_resolve_cache); + efree(zend_opcache_user_cache_decode_resolve_cache); + + zend_opcache_user_cache_decode_resolve_cache = NULL; +} + +bool zend_opcache_user_cache_shared_graph_requires_prototype(uint32_t payload_offset) +{ + zend_opcache_user_cache_shared_graph_header *header = + zend_opcache_user_cache_shared_graph_payload_header(payload_offset) + ; + + if (header == NULL) { + return true; + } + + return (header->flags & ZEND_OPCACHE_USER_CACHE_SHARED_GRAPH_FLAG_PREFERS_PROTOTYPE) != 0; +} + +bool zend_opcache_user_cache_shared_graph_payload_has_aliases(uint32_t payload_offset) +{ + zend_opcache_user_cache_shared_graph_header *header = + zend_opcache_user_cache_shared_graph_payload_header(payload_offset) + ; + + if (header == NULL) { + return true; + } + + return (header->flags & ZEND_OPCACHE_USER_CACHE_SHARED_GRAPH_FLAG_HAS_SHARED_IDENTITY) != 0; +} + +bool zend_opcache_user_cache_shared_graph_decode_is_lock_safe(uint32_t payload_offset) +{ + zend_opcache_user_cache_shared_graph_header *header = + zend_opcache_user_cache_shared_graph_payload_header(payload_offset) + ; + + return header != NULL && + (header->flags & ZEND_OPCACHE_USER_CACHE_SHARED_GRAPH_FLAG_HAS_OBJECT) == 0 + ; +} + +bool zend_opcache_user_cache_calculate_shared_graph_size( + const zval *value, + size_t *buffer_len, + HashTable *state_memo +) +{ + zend_opcache_user_cache_shared_graph_calc_context calc_context; + zend_class_entry *root_ce; + bool result; + + if (!buffer_len) { + return false; + } + + *buffer_len = 0; + + if (Z_TYPE_P(value) == IS_OBJECT) { + root_ce = Z_OBJCE_P(value); + /* A top-level enum case becomes an ENUM root; otherwise only a directly + * restorable object is a valid graph root. */ + if (!zend_opcache_user_cache_shared_graph_can_restore_direct(root_ce) && + !zend_opcache_user_cache_shared_graph_can_use_safe_direct(root_ce) && + !(root_ce->ce_flags & ZEND_ACC_ENUM) + ) { + return false; + } + } else if (Z_TYPE_P(value) != IS_ARRAY && Z_TYPE_P(value) != IS_STRING) { + return false; + } + + zend_opcache_user_cache_shared_graph_calc_init(&calc_context); + + calc_context.state_memo = state_memo; + result = zend_opcache_user_cache_shared_graph_reserve_size(&calc_context.size, sizeof(zend_opcache_user_cache_shared_graph_header)); + + if (result) { + result = zend_opcache_user_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_user_cache_shared_graph_calc_destroy(&calc_context); + + return result; +} + +bool zend_opcache_user_cache_build_shared_graph_in_place( + const zval *value, + unsigned char *buffer, + size_t buffer_len, + size_t *graph_len, + HashTable *state_memo +) +{ + zend_opcache_user_cache_shared_graph_copy_context copy_context; + zend_opcache_user_cache_shared_graph_header *header; + zend_opcache_user_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_user_cache_shared_graph_alignment_padding(buffer); + if (padding > buffer_len || buffer_len - padding < sizeof(zend_opcache_user_cache_shared_graph_header)) { + return false; + } + + if (padding != 0) { + memset(buffer, 0, padding); + } + + buffer += padding; + buffer_len -= padding; + + zend_opcache_user_cache_shared_graph_copy_init(©_context, buffer, buffer_len); + + copy_context.state_memo = state_memo; + root_offset = 0; + root_type = 0; + + result = zend_opcache_user_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_user_cache_shared_graph_copy_property_value(©_context, value, &root_value) && + (root_value.type == ZEND_OPCACHE_USER_CACHE_SHARED_GRAPH_VALUE_OBJECT || + root_value.type == ZEND_OPCACHE_USER_CACHE_SHARED_GRAPH_VALUE_SAFE_DIRECT_OBJECT || + root_value.type == ZEND_OPCACHE_USER_CACHE_SHARED_GRAPH_VALUE_ENUM) + ; + } else if (Z_TYPE_P(value) == IS_ARRAY) { + result = zend_opcache_user_cache_shared_graph_copy_property_value(©_context, value, &root_value) && + (root_value.type == ZEND_OPCACHE_USER_CACHE_SHARED_GRAPH_VALUE_ARRAY || + root_value.type == ZEND_OPCACHE_USER_CACHE_SHARED_GRAPH_VALUE_DYNAMIC_ARRAY) + ; + } else if (Z_TYPE_P(value) == IS_STRING) { + result = zend_opcache_user_cache_shared_graph_copy_property_value(©_context, value, &root_value) && + root_value.type == ZEND_OPCACHE_USER_CACHE_SHARED_GRAPH_VALUE_STRING + ; + } else { + result = false; + } + + if (result) { + root_type = root_value.type; + root_offset = (uint32_t) root_value.payload.offset; + } + } + + if (!result) { + zend_opcache_user_cache_shared_graph_copy_destroy(©_context); + + return false; + } + + header = (zend_opcache_user_cache_shared_graph_header *) buffer; + header->magic = ZEND_OPCACHE_USER_CACHE_SHARED_GRAPH_MAGIC; + header->version = ZEND_OPCACHE_USER_CACHE_SHARED_GRAPH_VERSION; + header->root_offset = root_offset; + header->root_type = root_type; + header->flags = + (copy_context.has_shared_identity ? ZEND_OPCACHE_USER_CACHE_SHARED_GRAPH_FLAG_HAS_SHARED_IDENTITY : 0) | + (copy_context.has_object ? ZEND_OPCACHE_USER_CACHE_SHARED_GRAPH_FLAG_HAS_OBJECT : 0) | + (copy_context.prefers_prototype ? ZEND_OPCACHE_USER_CACHE_SHARED_GRAPH_FLAG_PREFERS_PROTOTYPE : 0) + ; + + ZEND_ATOMIC_INT_INIT(&header->ref_state, 0); + + if (graph_len != NULL) { + *graph_len = copy_context.position; + } + + zend_opcache_user_cache_shared_graph_copy_destroy(©_context); + + return true; +} + +bool zend_opcache_user_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_user_cache_shared_graph_alignment_padding(target_buffer); + + return target_padding <= target_buffer_len && source_graph_len <= target_buffer_len - target_padding; +} + +bool zend_opcache_user_cache_fetch_shared_graph( + const unsigned char *buffer, + size_t buffer_len, + zval *destination +) +{ + const zend_opcache_user_cache_shared_graph_header *header; + const unsigned char *graph_buffer; + zend_opcache_user_cache_shared_graph_value root_value; + HashTable *saved_identity_map, *saved_reference_map, *saved_array_map; + uint32_t root_type; + bool result; + + graph_buffer = zend_opcache_user_cache_shared_graph_locate_buffer(buffer, buffer_len, &buffer_len); + if (graph_buffer == NULL) { + return false; + } + + buffer = graph_buffer; + + header = (const zend_opcache_user_cache_shared_graph_header *) buffer; + if (header->magic != ZEND_OPCACHE_USER_CACHE_SHARED_GRAPH_MAGIC || + header->version != ZEND_OPCACHE_USER_CACHE_SHARED_GRAPH_VERSION || + (header->root_offset != 0 && header->root_offset >= buffer_len) + ) { + return false; + } + + /* Decoding can re-enter through autoload. */ + saved_identity_map = zend_opcache_user_cache_decode_identity_map; + zend_opcache_user_cache_decode_identity_map = NULL; + saved_reference_map = zend_opcache_user_cache_decode_reference_map; + zend_opcache_user_cache_decode_reference_map = NULL; + saved_array_map = zend_opcache_user_cache_decode_array_map; + zend_opcache_user_cache_decode_array_map = NULL; + + root_type = header->root_type != 0 ? header->root_type : ZEND_OPCACHE_USER_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; + + switch (root_type) { + case ZEND_OPCACHE_USER_CACHE_SHARED_GRAPH_VALUE_SAFE_DIRECT_OBJECT: + case ZEND_OPCACHE_USER_CACHE_SHARED_GRAPH_VALUE_OBJECT: + if (header->root_offset == 0) { + zend_opcache_user_cache_decode_identity_map = saved_identity_map; + zend_opcache_user_cache_decode_reference_map = saved_reference_map; + zend_opcache_user_cache_decode_array_map = saved_array_map; + + return false; + } + + result = zend_opcache_user_cache_fetch_shared_graph_value(buffer, &root_value, destination); + + break; + case ZEND_OPCACHE_USER_CACHE_SHARED_GRAPH_VALUE_ARRAY: + case ZEND_OPCACHE_USER_CACHE_SHARED_GRAPH_VALUE_DYNAMIC_ARRAY: + case ZEND_OPCACHE_USER_CACHE_SHARED_GRAPH_VALUE_ENUM: + case ZEND_OPCACHE_USER_CACHE_SHARED_GRAPH_VALUE_STRING: + result = zend_opcache_user_cache_fetch_shared_graph_value(buffer, &root_value, destination); + + break; + default: + zend_opcache_user_cache_decode_identity_map = saved_identity_map; + zend_opcache_user_cache_decode_reference_map = saved_reference_map; + zend_opcache_user_cache_decode_array_map = saved_array_map; + + return false; + } + + zend_opcache_user_cache_decode_identity_map_teardown(); + zend_opcache_user_cache_decode_reference_map_teardown(); + zend_opcache_user_cache_decode_array_map_teardown(); + + zend_opcache_user_cache_decode_identity_map = saved_identity_map; + zend_opcache_user_cache_decode_reference_map = saved_reference_map; + zend_opcache_user_cache_decode_array_map = saved_array_map; + + return result; +} + +bool zend_opcache_user_cache_shared_graph_can_overwrite_payload_locked(uint32_t payload_offset) +{ + return zend_opcache_user_cache_shared_graph_can_move_payload_locked(payload_offset); +} + +bool zend_opcache_user_cache_shared_graph_can_move_payload_locked(uint32_t payload_offset) +{ + zend_opcache_user_cache_shared_graph_header *header = zend_opcache_user_cache_shared_graph_payload_header(payload_offset); + + if (header == NULL) { + return false; + } + + /* Refcounts are reliable only after optimistic readers have drained. */ + if (!zend_opcache_user_cache_graph_payloads_quiescent_locked()) { + return false; + } + + return zend_atomic_int_load_ex(&header->ref_state) == 0; +} + +bool zend_opcache_user_cache_shared_graph_rebase_moved_payload_locked(uint32_t payload_offset, ptrdiff_t delta) +{ + const unsigned char *buffer, *old_buffer, *new_base, *old_base; + zend_opcache_user_cache_shared_graph_header *header; + zend_opcache_user_cache_shared_graph_value root_value; + HashTable seen_arrays; + uint32_t root_type; + size_t buffer_len, graph_len, old_padding, new_padding; + bool result; + + if (payload_offset == 0 || delta == 0) { + return true; + } + + buffer_len = zend_opcache_user_cache_block_payload_capacity(payload_offset); + if (buffer_len == 0) { + return false; + } + + buffer = (const unsigned char *) zend_opcache_user_cache_ptr(payload_offset); + old_buffer = buffer + delta; + old_padding = zend_opcache_user_cache_shared_graph_alignment_padding(old_buffer); + new_padding = zend_opcache_user_cache_shared_graph_alignment_padding(buffer); + + if (old_padding != new_padding) { + return false; + } + + buffer = zend_opcache_user_cache_shared_graph_locate_buffer(buffer, buffer_len, &graph_len); + if (buffer == NULL) { + return false; + } + + header = (zend_opcache_user_cache_shared_graph_header *) buffer; + if (header->magic != ZEND_OPCACHE_USER_CACHE_SHARED_GRAPH_MAGIC || + header->version != ZEND_OPCACHE_USER_CACHE_SHARED_GRAPH_VERSION || + (header->root_offset != 0 && header->root_offset >= graph_len) + ) { + return false; + } + + new_base = buffer; + old_base = old_buffer + old_padding; + root_type = header->root_type != 0 ? header->root_type : ZEND_OPCACHE_USER_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; + + zend_hash_init(&seen_arrays, 8, NULL, NULL, 0); + switch (root_type) { + case ZEND_OPCACHE_USER_CACHE_SHARED_GRAPH_VALUE_ARRAY: + case ZEND_OPCACHE_USER_CACHE_SHARED_GRAPH_VALUE_DYNAMIC_ARRAY: + case ZEND_OPCACHE_USER_CACHE_SHARED_GRAPH_VALUE_ENUM: + case ZEND_OPCACHE_USER_CACHE_SHARED_GRAPH_VALUE_STRING: + /* ENUM and STRING hold only buffer-relative offsets, so + * rebase_graph_value is a no-op for them (its default), but route + * them here so a moved payload does not hit the failing default. */ + result = zend_opcache_user_cache_shared_graph_rebase_graph_value( + buffer, + &root_value, + old_base, + new_base, + graph_len, + delta, + &seen_arrays + ); + break; + case ZEND_OPCACHE_USER_CACHE_SHARED_GRAPH_VALUE_OBJECT: + case ZEND_OPCACHE_USER_CACHE_SHARED_GRAPH_VALUE_SAFE_DIRECT_OBJECT: + result = header->root_offset != 0 && + zend_opcache_user_cache_shared_graph_rebase_graph_value( + buffer, + &root_value, + old_base, + new_base, + graph_len, + delta, + &seen_arrays + ); + break; + default: + result = false; + break; + } + + zend_hash_destroy(&seen_arrays); + + return result; +} + +void zend_opcache_user_cache_orphan_graph_payload_locked(uint32_t payload_offset) +{ + zend_opcache_user_cache_header *header = zend_opcache_user_cache_header_ptr(); + uint32_t index; + + if (header == NULL || payload_offset == 0) { + return; + } + + zend_opcache_user_cache_shared_graph_force_retire_locked(payload_offset); + + for (index = 0; index < ZEND_OPCACHE_USER_CACHE_ORPHANED_GRAPH_SLOTS; index++) { + if (header->orphaned_graphs[index] == payload_offset) { + return; + } + + if (header->orphaned_graphs[index] == 0) { + header->orphaned_graphs[index] = payload_offset; + + return; + } + } + + header->orphaned_graphs_saturated = 1; +} + +void zend_opcache_user_cache_reclaim_orphaned_graphs_locked(void) +{ + zend_opcache_user_cache_header *header = zend_opcache_user_cache_header_ptr(); + zend_opcache_user_cache_shared_graph_header *graph_header; + uint32_t index, payload_offset; + bool checked_quiescent = false; + + if (header == NULL) { + return; + } + + for (index = 0; index < ZEND_OPCACHE_USER_CACHE_ORPHANED_GRAPH_SLOTS; index++) { + payload_offset = header->orphaned_graphs[index]; + if (payload_offset == 0) { + continue; + } + + if (!checked_quiescent) { + if (!zend_opcache_user_cache_graph_payloads_quiescent_locked()) { + return; + } + + checked_quiescent = true; + } + + graph_header = zend_opcache_user_cache_shared_graph_payload_header(payload_offset); + + if (graph_header == NULL) { + header->orphaned_graphs[index] = 0; + + continue; + } + + if (zend_atomic_int_load_ex(&graph_header->ref_state) != + (int) ZEND_OPCACHE_USER_CACHE_SHARED_GRAPH_RETIRED + ) { + header->orphaned_graphs[index] = 0; + + continue; + } + + header->orphaned_graphs[index] = 0; + zend_opcache_user_cache_free_locked(payload_offset); + } + + if (header->orphaned_graphs_saturated != 0) { + if (!checked_quiescent) { + if (!zend_opcache_user_cache_graph_payloads_quiescent_locked()) { + return; + } + + checked_quiescent = true; + } + + zend_opcache_user_cache_reclaim_saturated_orphaned_graphs_locked(header); + header->orphaned_graphs_saturated = 0; + } +} + +bool zend_opcache_user_cache_shared_graph_acquire_locked(uint32_t payload_offset) +{ + zend_opcache_user_cache_shared_graph_header *header = zend_opcache_user_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_USER_CACHE_SHARED_GRAPH_REFCOUNT_MASK; + expected = state; + + if ((state & ZEND_OPCACHE_USER_CACHE_SHARED_GRAPH_RETIRED) != 0 || + refcount == ZEND_OPCACHE_USER_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_user_cache_shared_graph_retire_payload_locked(uint32_t payload_offset) +{ + zend_opcache_user_cache_shared_graph_header *header = zend_opcache_user_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_USER_CACHE_SHARED_GRAPH_REFCOUNT_MASK; + expected = state; + + if (refcount == 0) { + return true; + } + + if ((state & ZEND_OPCACHE_USER_CACHE_SHARED_GRAPH_RETIRED) != 0) { + return false; + } + + if (zend_atomic_int_compare_exchange_ex(&header->ref_state, &expected, state | ZEND_OPCACHE_USER_CACHE_SHARED_GRAPH_RETIRED)) { + return false; + } + } +} + +bool zend_opcache_user_cache_shared_graph_release_ref_locked(uint32_t payload_offset) +{ + zend_opcache_user_cache_shared_graph_header *header = zend_opcache_user_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_USER_CACHE_SHARED_GRAPH_REFCOUNT_MASK; + expected = state; + + if (refcount == 0) { + return false; + } + + desired = (state & ZEND_OPCACHE_USER_CACHE_SHARED_GRAPH_RETIRED) | (refcount - 1); + if (zend_atomic_int_compare_exchange_ex(&header->ref_state, &expected, desired)) { + return (desired & ZEND_OPCACHE_USER_CACHE_SHARED_GRAPH_RETIRED) != 0 && + (desired & ZEND_OPCACHE_USER_CACHE_SHARED_GRAPH_REFCOUNT_MASK) == 0 + ; + } + } +} + +bool zend_opcache_user_cache_has_request_shared_graph_ref(uint32_t payload_offset) +{ + zend_opcache_user_cache_context *context; + zval *index_zv; + uint32_t index; + + if (zend_opcache_user_cache_shared_graph_ref_count == 0) { + return false; + } + + context = zend_opcache_user_cache_active_context(); + + /* The index maps payload offsets to the first matching request pin. */ + if (zend_opcache_user_cache_shared_graph_ref_index != NULL) { + index_zv = zend_hash_index_find(zend_opcache_user_cache_shared_graph_ref_index, (zend_ulong) payload_offset); + if (index_zv == NULL) { + return false; + } + + index = (uint32_t) Z_LVAL_P(index_zv); + if (index < zend_opcache_user_cache_shared_graph_ref_count && + zend_opcache_user_cache_shared_graph_refs[index].context == context && + zend_opcache_user_cache_shared_graph_refs[index].payload_offset == payload_offset + ) { + return true; + } + } + + for (index = 0; index < zend_opcache_user_cache_shared_graph_ref_count; index++) { + if (zend_opcache_user_cache_shared_graph_refs[index].context == context && + zend_opcache_user_cache_shared_graph_refs[index].payload_offset == payload_offset + ) { + return true; + } + } + + return false; +} + +/* Avoid allocation inside the optimistic reader window. */ +void zend_opcache_user_cache_shared_graph_ref_reserve(void) +{ + if (zend_opcache_user_cache_shared_graph_ref_count == zend_opcache_user_cache_shared_graph_ref_capacity) { + zend_opcache_user_cache_shared_graph_ref_capacity = zend_opcache_user_cache_shared_graph_ref_capacity == 0 + ? 8 + : zend_opcache_user_cache_shared_graph_ref_capacity * 2 + ; + + zend_opcache_user_cache_shared_graph_refs = erealloc( + zend_opcache_user_cache_shared_graph_refs, + sizeof(zend_opcache_user_cache_shared_graph_ref) * zend_opcache_user_cache_shared_graph_ref_capacity + ); + } + + if (zend_opcache_user_cache_shared_graph_ref_index == NULL) { + zend_opcache_user_cache_shared_graph_ref_index = emalloc(sizeof(HashTable)); + zend_hash_init(zend_opcache_user_cache_shared_graph_ref_index, 8, NULL, NULL, 0); + } + + zend_hash_extend( + zend_opcache_user_cache_shared_graph_ref_index, + zend_hash_num_elements(zend_opcache_user_cache_shared_graph_ref_index) + 1, + 0 + ); +} + +void zend_opcache_user_cache_register_shared_graph_ref(uint32_t payload_offset) +{ + zval index_zv; + + if (zend_opcache_user_cache_shared_graph_ref_count == zend_opcache_user_cache_shared_graph_ref_capacity) { + zend_opcache_user_cache_shared_graph_ref_capacity = zend_opcache_user_cache_shared_graph_ref_capacity == 0 + ? 8 + : zend_opcache_user_cache_shared_graph_ref_capacity * 2 + ; + + zend_opcache_user_cache_shared_graph_refs = erealloc( + zend_opcache_user_cache_shared_graph_refs, + sizeof(zend_opcache_user_cache_shared_graph_ref) * zend_opcache_user_cache_shared_graph_ref_capacity + ); + } + + zend_opcache_user_cache_shared_graph_refs[zend_opcache_user_cache_shared_graph_ref_count].context = zend_opcache_user_cache_active_context(); + zend_opcache_user_cache_shared_graph_refs[zend_opcache_user_cache_shared_graph_ref_count].payload_offset = payload_offset; + + if (zend_opcache_user_cache_shared_graph_ref_index == NULL) { + zend_opcache_user_cache_shared_graph_ref_index = emalloc(sizeof(HashTable)); + zend_hash_init(zend_opcache_user_cache_shared_graph_ref_index, 8, NULL, NULL, 0); + } + + ZVAL_LONG(&index_zv, (zend_long) zend_opcache_user_cache_shared_graph_ref_count); + zend_hash_index_add( + zend_opcache_user_cache_shared_graph_ref_index, + (zend_ulong) payload_offset, + &index_zv + ); + + zend_opcache_user_cache_shared_graph_ref_count++; +} + +void zend_opcache_user_cache_defer_retired_shared_graph_free(uint32_t payload_offset) +{ + zend_opcache_user_cache_shared_graph_ref *ref; + + if (zend_opcache_user_cache_retired_shared_graph_count == zend_opcache_user_cache_retired_shared_graph_capacity) { + zend_opcache_user_cache_retired_shared_graph_capacity = zend_opcache_user_cache_retired_shared_graph_capacity == 0 + ? 4 + : zend_opcache_user_cache_retired_shared_graph_capacity * 2 + ; + + zend_opcache_user_cache_retired_shared_graphs = erealloc( + zend_opcache_user_cache_retired_shared_graphs, + sizeof(zend_opcache_user_cache_shared_graph_ref) * zend_opcache_user_cache_retired_shared_graph_capacity + ); + } + + ref = &zend_opcache_user_cache_retired_shared_graphs[zend_opcache_user_cache_retired_shared_graph_count++]; + ref->context = zend_opcache_user_cache_active_context(); + ref->payload_offset = payload_offset; +} + +bool zend_opcache_user_cache_release_request_shared_graph_refs(void) +{ + zend_opcache_user_cache_shared_graph_ref *ref; + zend_opcache_user_cache_context *context, *previous_context; + uint32_t index, inner; + bool released = false; + + if (zend_opcache_user_cache_shared_graph_ref_count == 0) { + if (zend_opcache_user_cache_shared_graph_ref_index != NULL) { + zend_hash_destroy(zend_opcache_user_cache_shared_graph_ref_index); + efree(zend_opcache_user_cache_shared_graph_ref_index); + zend_opcache_user_cache_shared_graph_ref_index = NULL; + } + + return zend_opcache_user_cache_free_retired_shared_graphs(); + } + + /* Release request pins grouped by context. */ + for (index = 0; index < zend_opcache_user_cache_shared_graph_ref_count; index++) { + context = zend_opcache_user_cache_shared_graph_refs[index].context; + + if (context == NULL) { + continue; + } + + previous_context = zend_opcache_user_cache_activate_context(context); + + if (zend_opcache_user_cache_wlock()) { + if (zend_opcache_user_cache_header_is_initialized_locked()) { + for (inner = index; inner < zend_opcache_user_cache_shared_graph_ref_count; inner++) { + ref = &zend_opcache_user_cache_shared_graph_refs[inner]; + + if (ref->context != context) { + continue; + } + + if (ref->payload_offset == 0) { + ref->context = NULL; + continue; + } + + released = true; + if (zend_opcache_user_cache_shared_graph_release_ref_locked(ref->payload_offset)) { + if (zend_opcache_user_cache_graph_payloads_quiescent_locked()) { + zend_opcache_user_cache_free_locked(ref->payload_offset); + } else { + zend_opcache_user_cache_orphan_graph_payload_locked(ref->payload_offset); + } + } + + ref->context = NULL; + } + + zend_opcache_user_cache_free_retired_for_context_locked(context); + } + + zend_opcache_user_cache_unlock(); + } + + zend_opcache_user_cache_restore_context(previous_context); + } + + efree(zend_opcache_user_cache_shared_graph_refs); + + zend_opcache_user_cache_shared_graph_refs = NULL; + zend_opcache_user_cache_shared_graph_ref_count = 0; + zend_opcache_user_cache_shared_graph_ref_capacity = 0; + + if (zend_opcache_user_cache_shared_graph_ref_index != NULL) { + zend_hash_destroy(zend_opcache_user_cache_shared_graph_ref_index); + efree(zend_opcache_user_cache_shared_graph_ref_index); + zend_opcache_user_cache_shared_graph_ref_index = NULL; + } + + return zend_opcache_user_cache_free_retired_shared_graphs() || released; +} diff --git a/ext/opcache/zend_user_cache_storage.c b/ext/opcache/zend_user_cache_storage.c new file mode 100644 index 000000000000..ff6e6a657686 --- /dev/null +++ b/ext/opcache/zend_user_cache_storage.c @@ -0,0 +1,3090 @@ +/* + +----------------------------------------------------------------------+ + | 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_user_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 +# include +# ifdef HAVE_UNISTD_H +# include +# endif +# if defined(USE_MMAP) || (defined(__linux__) && defined(HAVE_MEMFD_CREATE)) +# include +# endif +#endif + +#if defined(USE_MMAP) && !defined(ZEND_WIN32) +# if defined(MAP_ANON) && !defined(MAP_ANONYMOUS) +# define MAP_ANONYMOUS MAP_ANON +# endif +#endif + +#ifdef ZEND_WIN32 +# define ZEND_OPCACHE_USER_CACHE_WIN32_MAPPING_PREFIX_SIZE (2 * sizeof(void *)) +# define ZEND_OPCACHE_USER_CACHE_WIN32_MAPPING_NAME "ZendOPcache.UserCache.SharedMemoryArea" +# define ZEND_OPCACHE_USER_CACHE_WIN32_MAPPING_MUTEX_NAME "ZendOPcache.UserCache.SharedMemoryMutex" +# define ZEND_OPCACHE_USER_CACHE_WIN32_LOCK_FILE_NAME "ZendOPcache.UserCache.LockFile" +#endif + +#ifdef ZEND_OPCACHE_USER_CACHE_HAVE_OPTIMISTIC +# define ZEND_OPCACHE_USER_CACHE_READER_CLAIM_MAX 4U +#endif + +#ifdef ZEND_WIN32 +typedef struct _zend_opcache_user_cache_win32_segment { + zend_shared_segment segment; + HANDLE memfile; + void *mapping_base; + size_t mapping_size; +} zend_opcache_user_cache_win32_segment; +#endif + +#ifdef ZEND_OPCACHE_USER_CACHE_HAVE_OPTIMISTIC +typedef struct { + zend_opcache_user_cache_header *header; + uint32_t slot_index; +} zend_opcache_user_cache_reader_claim; +#endif + +typedef enum _zend_opcache_user_cache_entry_lock_release_mode { + ZEND_OPCACHE_USER_CACHE_ENTRY_LOCK_RELEASE_DROP, + ZEND_OPCACHE_USER_CACHE_ENTRY_LOCK_RELEASE_CLEAR, + ZEND_OPCACHE_USER_CACHE_ENTRY_LOCK_RELEASE_PRESERVE_LEASES +} zend_opcache_user_cache_entry_lock_release_mode; + +#if defined(USE_MMAP) && !defined(ZEND_WIN32) +static int zend_opcache_user_cache_mmap_create_segments( + size_t requested_size, + zend_shared_segment ***shared_segments_p, + int *shared_segments_count, + const char **error_in +); +static int zend_opcache_user_cache_mmap_detach_segment(zend_shared_segment *shared_segment); +static size_t zend_opcache_user_cache_mmap_segment_type_size(void); +#endif + +#ifdef ZEND_WIN32 +static int zend_opcache_user_cache_win32_create_segments( + size_t requested_size, + zend_shared_segment ***shared_segments_p, + int *shared_segments_count, + const char **error_in +); +static int zend_opcache_user_cache_win32_detach_segment(zend_shared_segment *shared_segment); +static size_t zend_opcache_user_cache_win32_segment_type_size(void); +#endif + +#if defined(USE_MMAP) && !defined(ZEND_WIN32) +static const zend_shared_memory_handlers zend_opcache_user_cache_mmap_handlers = { + zend_opcache_user_cache_mmap_create_segments, + zend_opcache_user_cache_mmap_detach_segment, + zend_opcache_user_cache_mmap_segment_type_size +}; +#endif + +#ifdef ZEND_WIN32 +static const zend_shared_memory_handlers zend_opcache_user_cache_win32_handlers = { + zend_opcache_user_cache_win32_create_segments, + zend_opcache_user_cache_win32_detach_segment, + zend_opcache_user_cache_win32_segment_type_size +}; +#endif + +static const zend_shared_memory_handler_entry zend_opcache_user_cache_handler_table[] = { +#if defined(USE_MMAP) && !defined(ZEND_WIN32) + { "mmap", &zend_opcache_user_cache_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_user_cache_win32_handlers }, +#endif + { NULL, NULL } +}; + +#ifndef ZEND_WIN32 +static ZEND_EXT_TLS zend_ulong zend_opcache_user_cache_entry_lock_owner_pid = 0; +#endif + +#ifdef ZEND_OPCACHE_USER_CACHE_HAVE_OPTIMISTIC +/* Current write-lock section has announced itself in write_seq. */ +static ZEND_EXT_TLS bool zend_opcache_user_cache_write_seq_bumped = false; +/* 0 = drain not attempted in this write section, 1 = drained, -1 = timed out + * (graph payloads must not be freed, moved, or overwritten in place). */ +static ZEND_EXT_TLS int8_t zend_opcache_user_cache_reader_drain_state = 0; + +static ZEND_EXT_TLS zend_opcache_user_cache_reader_claim zend_opcache_user_cache_reader_claims[ZEND_OPCACHE_USER_CACHE_READER_CLAIM_MAX]; +static ZEND_EXT_TLS uint32_t zend_opcache_user_cache_reader_claim_count = 0; +static uint64_t zend_opcache_user_cache_reader_incarnation = 0; +#endif + +static zend_always_inline bool zend_opcache_user_cache_force_startup_failure(void) +{ + const char *value = getenv("OPCACHE_USER_CACHE_FORCE_STARTUP_FAILURE"); + + return value != NULL && value[0] != '\0' && value[0] != '0'; +} + +static zend_always_inline bool zend_opcache_user_cache_requires_pre_request_storage(void) +{ + if (sapi_module.name == NULL) { + return false; + } + + return strcmp(sapi_module.name, "fpm-fcgi") == 0 || + strcmp(sapi_module.name, "apache2handler") == 0 || + strcmp(sapi_module.name, "cli-server") == 0; +} + +static zend_always_inline bool zend_opcache_user_cache_environment_is_allowed(void) +{ + return zend_opcache_user_cache_runtime_opted_in; +} + +static zend_always_inline bool zend_opcache_user_cache_opcache_is_disabled_for_sapi(void) +{ + if (!ZCG(enabled)) { + return true; + } + + return !ZCG(accel_directives).enable_cli && + sapi_module.name != NULL && + (strcmp(sapi_module.name, "cli") == 0 || strcmp(sapi_module.name, "phpdbg") == 0); +} + +static zend_always_inline void zend_opcache_user_cache_set_unavailable(const char *failure_reason, bool startup_failed) +{ + zend_opcache_user_cache_context *context = zend_opcache_user_cache_active_context(); + zend_opcache_user_cache_runtime *runtime = zend_opcache_user_cache_active_runtime(); + + 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_user_cache_set_available(void) +{ + zend_opcache_user_cache_context *context = zend_opcache_user_cache_active_context(); + zend_opcache_user_cache_runtime *runtime = zend_opcache_user_cache_active_runtime(); + + runtime->available = true; + runtime->startup_failed = false; + runtime->backend_initialized = context->storage.initialized; + runtime->failure_reason = NULL; +} + +static zend_always_inline HashTable **zend_opcache_user_cache_entry_lock_table_ptr(void) +{ + return &zend_opcache_user_cache_entry_lock_table; +} + +static zend_always_inline uint32_t zend_opcache_user_cache_entry_lock_table_index(zend_ulong hash) +{ + return (uint32_t) (hash % ZEND_OPCACHE_USER_CACHE_ENTRY_LOCK_TABLE_SIZE); +} + +#ifdef ZEND_OPCACHE_USER_CACHE_HAVE_OPTIMISTIC +static zend_always_inline uint64_t zend_opcache_user_cache_current_pid(void) +{ +#ifdef ZEND_WIN32 + return (uint64_t) GetCurrentProcessId(); +#else + return (uint64_t) getpid(); +#endif +} +#endif + +static zend_always_inline uint32_t zend_opcache_user_cache_used_end_offset_locked(const zend_opcache_user_cache_header *header) +{ + return header->data_offset + header->next_free; +} + +static zend_always_inline bool zend_opcache_user_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_user_cache_block)) { + return false; + } + + aligned_size = ZEND_ALIGNED_SIZE(sizeof(zend_opcache_user_cache_block) + size); + if (aligned_size > UINT32_MAX) { + return false; + } + + *block_size = (uint32_t) aligned_size; + + return true; +} + +static zend_always_inline bool zend_opcache_user_cache_offset_in_block(uint32_t offset, uint32_t block_offset, uint32_t block_size) +{ + return offset >= block_offset + sizeof(zend_opcache_user_cache_block) && offset < block_offset + block_size; +} + +static zend_always_inline bool zend_opcache_user_cache_entry_lock_record_key_matches( + const zend_opcache_user_cache_entry_lock_record *record, + zend_string *key, + zend_ulong hash) +{ + return record->state == ZEND_OPCACHE_USER_CACHE_ENTRY_LOCK_USED && + record->hash == hash && + record->key_len == ZSTR_LEN(key) && + memcmp(zend_opcache_user_cache_ptr(record->key_offset), ZSTR_VAL(key), ZSTR_LEN(key)) == 0 + ; +} + +#ifdef ZEND_WIN32 +static inline bool zend_opcache_user_cache_win32_set_segment( + zend_opcache_user_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_USER_CACHE_WIN32_MAPPING_PREFIX_SIZE; + segment->segment.pos = 0; + segment->segment.size = requested_size; + + return true; +} +#endif + +#if defined(USE_MMAP) && !defined(ZEND_WIN32) +static int zend_opcache_user_cache_mmap_create_segments( + size_t requested_size, + zend_shared_segment ***shared_segments_p, + int *shared_segments_count, + const char **error_in +) +{ + zend_shared_segment *segment; + void *mapping; + + mapping = mmap(NULL, requested_size, PROT_READ | PROT_WRITE, MAP_SHARED | MAP_ANONYMOUS, -1, 0); + if (mapping == MAP_FAILED) { + *error_in = "mmap"; + return ALLOC_FAILURE; + } + + *shared_segments_count = 1; + *shared_segments_p = (zend_shared_segment **) calloc(1, sizeof(zend_shared_segment *) + sizeof(zend_shared_segment)); + if (*shared_segments_p == NULL) { + munmap(mapping, requested_size); + *error_in = "calloc"; + return ALLOC_FAILURE; + } + + segment = (zend_shared_segment *) ((char *) *shared_segments_p + sizeof(zend_shared_segment *)); + (*shared_segments_p)[0] = segment; + + segment->p = mapping; + segment->pos = 0; + segment->size = requested_size; + segment->end = requested_size; + + return ALLOC_SUCCESS; +} + +static int zend_opcache_user_cache_mmap_detach_segment(zend_shared_segment *shared_segment) +{ + munmap(shared_segment->p, shared_segment->size); + + return 0; +} + +static size_t zend_opcache_user_cache_mmap_segment_type_size(void) +{ + return sizeof(zend_shared_segment); +} +#endif + +#ifdef ZEND_WIN32 +static void zend_opcache_user_cache_win32_create_name(char *buffer, size_t buffer_size, const char *name, size_t unique_id) +{ + zend_opcache_user_cache_context *context = zend_opcache_user_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 int zend_opcache_user_cache_win32_reattach_segment( + zend_opcache_user_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_USER_CACHE_WIN32_MAPPING_PREFIX_SIZE) { + *error_in = "size overflow"; + return ALLOC_FAILURE; + } + + mapping_size = requested_size + ZEND_OPCACHE_USER_CACHE_WIN32_MAPPING_PREFIX_SIZE; + metadata_view = MapViewOfFileEx(memfile, FILE_MAP_READ, 0, 0, ZEND_OPCACHE_USER_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_user_cache_win32_set_segment(segment, memfile, mapping_base, mapping_size, requested_size) + ? ALLOC_SUCCESS + : ALLOC_FAILURE; +} + +static int zend_opcache_user_cache_win32_create_segment( + zend_opcache_user_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_USER_CACHE_WIN32_MAPPING_PREFIX_SIZE) { + *error_in = "size overflow"; + return ALLOC_FAILURE; + } + + mapping_size = requested_size + ZEND_OPCACHE_USER_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_user_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_user_cache_win32_set_segment(segment, memfile, mapping_base, mapping_size, requested_size) + ? ALLOC_SUCCESS + : ALLOC_FAILURE; +} + +static int zend_opcache_user_cache_win32_create_segments( + size_t requested_size, + zend_shared_segment ***shared_segments_p, + int *shared_segments_count, + const char **error_in +) +{ + zend_opcache_user_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_user_cache_win32_segment) + ); + if (*shared_segments_p == NULL) { + *error_in = "calloc"; + return ALLOC_FAILURE; + } + + segment = (zend_opcache_user_cache_win32_segment *) ((char *) *shared_segments_p + sizeof(zend_shared_segment *)); + (*shared_segments_p)[0] = (zend_shared_segment *) segment; + + zend_opcache_user_cache_win32_create_name( + mapping_name, + sizeof(mapping_name), + ZEND_OPCACHE_USER_CACHE_WIN32_MAPPING_NAME, + requested_size + ); + zend_opcache_user_cache_win32_create_name( + mutex_name, + sizeof(mutex_name), + ZEND_OPCACHE_USER_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_user_cache_win32_reattach_segment(segment, memfile, requested_size, error_in); + if (result != ALLOC_SUCCESS) { + CloseHandle(memfile); + } + } else { + result = zend_opcache_user_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_user_cache_win32_detach_segment(zend_shared_segment *shared_segment) +{ + zend_opcache_user_cache_win32_segment *segment = (zend_opcache_user_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_user_cache_win32_segment_type_size(void) +{ + return sizeof(zend_opcache_user_cache_win32_segment); +} +#endif + +static void zend_opcache_user_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 ZEND_OPCACHE_USER_CACHE_HAVE_SHARED_MUTEX +static bool zend_opcache_user_cache_shared_mutexes_init(zend_opcache_user_cache_header *header) +{ + pthread_mutexattr_t attr; + bool ok; + + if (pthread_mutexattr_init(&attr) != 0) { + return false; + } + + ok = pthread_mutexattr_setpshared(&attr, PTHREAD_PROCESS_SHARED) == 0 && + pthread_mutexattr_setrobust(&attr, PTHREAD_MUTEX_ROBUST) == 0 && + pthread_mutex_init(&header->global_shared_mutex.mutex, &attr) == 0 + ; + + pthread_mutexattr_destroy(&attr); + + return ok; +} + +/* Discard mutable contents if a mutex owner died during a write section. */ +static void zend_opcache_user_cache_recover_after_owner_death(void) +{ + zend_opcache_user_cache_header *header = zend_opcache_user_cache_header_ptr(); + + if (header == NULL || !zend_opcache_user_cache_header_is_initialized_locked()) { + return; + } + + if ((header->write_seq & 1) == 0) { + return; + } + + memset( + zend_opcache_user_cache_entries(header), + 0, + (size_t) header->capacity * sizeof(zend_opcache_user_cache_entry) + ); + memset(header->entry_lock_records, 0, sizeof(header->entry_lock_records)); + memset(header->reader_slots, 0, sizeof(header->reader_slots)); + memset(header->orphaned_graphs, 0, sizeof(header->orphaned_graphs)); + + header->active_optimistic_readers = 0; + header->count = 0; + header->tombstone_count = 0; + header->next_free = 0; + header->free_list = 0; + header->last_block_offset = 0; + + zend_opcache_user_cache_bump_mutation_epoch_locked(header); + +#ifdef ZEND_OPCACHE_USER_CACHE_HAVE_OPTIMISTIC + zend_opcache_user_cache_seq_publish(&header->write_seq, header->write_seq + 1); +#else + header->write_seq++; +#endif +} + +static bool zend_opcache_user_cache_shared_mutex_lock(void) +{ + zend_opcache_user_cache_header *header = zend_opcache_user_cache_header_ptr(); + int result; + + if (header == NULL) { + return false; + } + + result = pthread_mutex_lock(&header->global_shared_mutex.mutex); + if (result == EOWNERDEAD) { + pthread_mutex_consistent(&header->global_shared_mutex.mutex); + zend_opcache_user_cache_recover_after_owner_death(); + result = 0; + } + + if (result != 0) { + return false; + } + + zend_opcache_user_cache_lock_held = true; + + return true; +} +#endif + +#ifndef ZEND_WIN32 +static bool zend_opcache_user_cache_lock_internal(short lock_type) +{ + zend_opcache_user_cache_storage *storage = &zend_opcache_user_cache_active_context()->storage; + struct flock mem_lock; + + if (!storage->lock_initialized) { + return false; + } + +#ifdef ZEND_OPCACHE_USER_CACHE_HAVE_SHARED_MUTEX + /* Use the lock backend selected by the segment header. */ + if (!storage->use_shared_mutex && + zend_opcache_user_cache_header_ptr() != NULL && + zend_opcache_user_cache_header_is_initialized_locked() && + zend_opcache_user_cache_header_ptr()->lock_model == ZEND_OPCACHE_USER_CACHE_LOCK_MODEL_MUTEX + ) { + storage->use_shared_mutex = true; + } + + if (storage->use_shared_mutex) { + return zend_opcache_user_cache_shared_mutex_lock(); + } +#endif + +#ifdef ZTS + if (tsrm_mutex_lock(storage->zts_lock) != 0) { + 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 + tsrm_mutex_unlock(storage->zts_lock); +#endif + return false; + } + } + + zend_opcache_user_cache_lock_held = true; + + return true; +} + +static bool zend_opcache_user_cache_lock_startup(void) +{ + zend_opcache_user_cache_context *context = zend_opcache_user_cache_active_context(); + zend_opcache_user_cache_storage *storage = &context->storage; + int val; + + if (storage->lock_initialized) { + return true; + } + +#ifdef ZTS + storage->zts_lock = tsrm_mutex_alloc(); + if (storage->zts_lock == NULL) { + 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 + tsrm_mutex_free(storage->zts_lock); + storage->zts_lock = NULL; +#endif + + return false; + } + + if (fchmod(storage->lock_file, 0666) == -1) { + close(storage->lock_file); + storage->lock_file = -1; +#ifdef ZTS + tsrm_mutex_free(storage->zts_lock); + storage->zts_lock = NULL; +#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_user_cache_lock_shutdown(void) +{ + zend_opcache_user_cache_storage *storage = &zend_opcache_user_cache_active_context()->storage; + + if (!storage->lock_initialized) { + return; + } + + if (storage->lock_file >= 0) { + close(storage->lock_file); + storage->lock_file = -1; + } +#ifdef ZTS + tsrm_mutex_free(storage->zts_lock); + storage->zts_lock = NULL; +#endif + storage->lock_initialized = false; +} + +static bool zend_opcache_user_cache_rlock_impl(void) +{ + return zend_opcache_user_cache_lock_internal(F_RDLCK); +} + +static bool zend_opcache_user_cache_wlock_impl(void) +{ + return zend_opcache_user_cache_lock_internal(F_WRLCK); +} + +static void zend_opcache_user_cache_unlock_impl(void) +{ + zend_opcache_user_cache_storage *storage = &zend_opcache_user_cache_active_context()->storage; + struct flock mem_unlock; + + if (!storage->lock_initialized) { + return; + } + +#ifdef ZEND_OPCACHE_USER_CACHE_HAVE_SHARED_MUTEX + if (storage->use_shared_mutex) { + zend_opcache_user_cache_header *header = zend_opcache_user_cache_header_ptr(); + + if (header != NULL) { + pthread_mutex_unlock(&header->global_shared_mutex.mutex); + } + + zend_opcache_user_cache_lock_held = false; + + return; + } +#endif + + 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 + tsrm_mutex_unlock(storage->zts_lock); +#endif + zend_opcache_user_cache_lock_held = false; +} +#else +static bool zend_opcache_user_cache_win32_open_lock_file_at(zend_opcache_user_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_user_cache_win32_open_lock_file(zend_opcache_user_cache_context *context) +{ + zend_opcache_user_cache_storage *storage = &context->storage; + char base_name[MAXPATHLEN], temp_path[MAXPATHLEN], *cursor; + DWORD temp_path_len; + + zend_opcache_user_cache_win32_create_name( + base_name, + sizeof(base_name), + ZEND_OPCACHE_USER_CACHE_WIN32_LOCK_FILE_NAME, + storage->size + ); + + /* Partition names may contain SAPI-specific delimiters that are invalid in + * Win32 file names. The mapping and mutex names are kernel object names, + * but the lock backend stores this name on disk. */ + for (cursor = base_name; *cursor != '\0'; cursor++) { + if ((unsigned char) *cursor < 32 || + *cursor == '<' || + *cursor == '>' || + *cursor == ':' || + *cursor == '"' || + *cursor == '/' || + *cursor == '\\' || + *cursor == '|' || + *cursor == '?' || + *cursor == '*' + ) { + *cursor = '_'; + } + } + + 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_user_cache_win32_open_lock_file_at(storage, temp_path, base_name); +} + +static bool zend_opcache_user_cache_win32_lock_range(zend_opcache_user_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_user_cache_win32_unlock_range(zend_opcache_user_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_user_cache_lock_internal(bool exclusive) +{ + zend_opcache_user_cache_storage *storage = &zend_opcache_user_cache_active_context()->storage; + + if (!storage->lock_initialized) { + return false; + } + +#ifdef ZTS + if (tsrm_mutex_lock(storage->zts_lock) != 0) { + return false; + } +#endif + + if (!zend_opcache_user_cache_win32_lock_range(storage, 0, exclusive, true)) { +#ifdef ZTS + tsrm_mutex_unlock(storage->zts_lock); +#endif + return false; + } + + zend_opcache_user_cache_lock_held = true; + + return true; +} + +static bool zend_opcache_user_cache_lock_startup(void) +{ + zend_opcache_user_cache_context *context = zend_opcache_user_cache_active_context(); + zend_opcache_user_cache_storage *storage = &context->storage; + + if (storage->lock_initialized) { + return true; + } + +#ifdef ZTS + storage->zts_lock = tsrm_mutex_alloc(); + if (storage->zts_lock == NULL) { + return false; + } +#endif + + if (!zend_opcache_user_cache_win32_open_lock_file(context)) { +#ifdef ZTS + tsrm_mutex_free(storage->zts_lock); + storage->zts_lock = NULL; +#endif + return false; + } + + storage->lock_initialized = true; + + return true; +} + +static void zend_opcache_user_cache_lock_shutdown(void) +{ + zend_opcache_user_cache_storage *storage = &zend_opcache_user_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 + tsrm_mutex_free(storage->zts_lock); + storage->zts_lock = NULL; +#endif + storage->lock_initialized = false; +} + +static bool zend_opcache_user_cache_rlock_impl(void) +{ + return zend_opcache_user_cache_lock_internal(false); +} + +static bool zend_opcache_user_cache_wlock_impl(void) +{ + return zend_opcache_user_cache_lock_internal(true); +} + +static void zend_opcache_user_cache_unlock_impl(void) +{ + zend_opcache_user_cache_storage *storage = &zend_opcache_user_cache_active_context()->storage; + + if (!storage->lock_initialized) { + return; + } + + zend_opcache_user_cache_win32_unlock_range(storage, 0); + +#ifdef ZTS + tsrm_mutex_unlock(storage->zts_lock); +#endif + zend_opcache_user_cache_lock_held = false; +} +#endif + +static void zend_opcache_user_cache_entry_lock_lease_wait(void) +{ +#ifdef ZEND_WIN32 + Sleep(10); +#elif defined(HAVE_UNISTD_H) + usleep(10000); +#endif +} + +static uint64_t zend_opcache_user_cache_entry_lock_expires_at(zend_long lease) +{ + uint64_t now, lease_seconds; + + ZEND_ASSERT(lease > 0); + + now = (uint64_t) time(NULL); + lease_seconds = (uint64_t) lease; + if (lease_seconds > UINT64_MAX - now) { + return UINT64_MAX; + } + + return now + lease_seconds; +} + +static uint64_t zend_opcache_user_cache_entry_lock_current_pid(void) +{ +#ifdef ZEND_WIN32 + return (uint64_t) GetCurrentProcessId(); +#else + return (uint64_t) getpid(); +#endif +} + +/* Best-effort process start time, used to tell a reader-slot owner apart from + * an unrelated process that recycled its PID. 0 means unknown. */ +static uint64_t zend_opcache_user_cache_process_start_time(uint64_t pid) +{ +#ifdef ZEND_WIN32 + FILETIME creation, exit_time, kernel_time, user_time; + HANDLE process = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, FALSE, (DWORD) pid); + uint64_t start_time = 0; + + if (process == NULL) { + return 0; + } + + if (GetProcessTimes(process, &creation, &exit_time, &kernel_time, &user_time)) { + start_time = ((uint64_t) creation.dwHighDateTime << 32) | creation.dwLowDateTime; + } + + CloseHandle(process); + + return start_time; +#elif defined(__linux__) + char path[64], stat_buffer[1024]; + const char *cursor; + unsigned long long start_time = 0; + ssize_t stat_len; + int fd, field; + + snprintf(path, sizeof(path), "/proc/%llu/stat", (unsigned long long) pid); + fd = open(path, O_RDONLY); + if (fd < 0) { + return 0; + } + + stat_len = read(fd, stat_buffer, sizeof(stat_buffer) - 1); + close(fd); + if (stat_len <= 0) { + return 0; + } + + stat_buffer[stat_len] = '\0'; + + /* The comm field may contain spaces and parentheses; fields are counted + * from after its closing parenthesis. starttime is overall field 22, the + * 20th space-separated token after ") ". */ + cursor = strrchr(stat_buffer, ')'); + if (cursor == NULL) { + return 0; + } + + cursor++; + for (field = 0; field < 19 && *cursor != '\0'; field++) { + while (*cursor == ' ') { + cursor++; + } + while (*cursor != '\0' && *cursor != ' ') { + cursor++; + } + } + + if (sscanf(cursor, " %llu", &start_time) != 1) { + return 0; + } + + return (uint64_t) start_time; +#else + (void) pid; + + return 0; +#endif +} + +/* Unknown owner state is treated as alive. */ +static bool zend_opcache_user_cache_process_owner_is_dead(uint64_t owner_pid, uint64_t owner_start_time) +{ + uint64_t start_time; + +#ifdef ZEND_WIN32 + HANDLE process = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, FALSE, (DWORD) owner_pid); + DWORD exit_code = 0; + + if (process == NULL) { + return GetLastError() == ERROR_INVALID_PARAMETER; + } + + if (GetExitCodeProcess(process, &exit_code) && exit_code != STILL_ACTIVE) { + CloseHandle(process); + + return true; + } + + CloseHandle(process); +#else + if (kill((pid_t) owner_pid, 0) == -1 && errno == ESRCH) { + return true; + } +#endif + + if (owner_start_time != 0) { + start_time = zend_opcache_user_cache_process_start_time(owner_pid); + if (start_time != 0 && start_time != owner_start_time) { + return true; + } + } + + return false; +} + +static void zend_opcache_user_cache_remove_entry_lock_record_locked( + zend_opcache_user_cache_entry_lock_record *record) +{ + if (record->state == ZEND_OPCACHE_USER_CACHE_ENTRY_LOCK_USED && record->key_offset != 0) { + zend_opcache_user_cache_free_locked(record->key_offset); + } + + memset(record, 0, sizeof(*record)); + record->state = ZEND_OPCACHE_USER_CACHE_ENTRY_LOCK_TOMBSTONE; +} + +static bool zend_opcache_user_cache_entry_lock_record_is_active_locked( + zend_opcache_user_cache_entry_lock_record *record, + uint64_t now) +{ + if (record->state != ZEND_OPCACHE_USER_CACHE_ENTRY_LOCK_USED) { + return false; + } + + if (record->expires_at != 0 && record->expires_at <= now) { + return false; + } + + if (record->owner_pid == 0) { + return record->expires_at != 0; + } + + if (zend_opcache_user_cache_process_owner_is_dead(record->owner_pid, record->owner_start_time)) { + record->owner_pid = 0; + record->owner_start_time = 0; + + return record->expires_at != 0; + } + + return true; +} + +static bool zend_opcache_user_cache_find_entry_lock_record_slot_locked( + zend_opcache_user_cache_header *header, + zend_string *key, + zend_ulong hash, + uint32_t *slot_index, + bool *found) +{ + uint32_t first_available = UINT32_MAX, index, probe; + uint64_t now = (uint64_t) time(NULL); + zend_opcache_user_cache_entry_lock_record *record; + + *found = false; + + for (probe = 0; probe < ZEND_OPCACHE_USER_CACHE_ENTRY_LOCK_TABLE_SIZE; probe++) { + index = (zend_opcache_user_cache_entry_lock_table_index(hash) + probe) % ZEND_OPCACHE_USER_CACHE_ENTRY_LOCK_TABLE_SIZE; + record = &header->entry_lock_records[index]; + + if (record->state == ZEND_OPCACHE_USER_CACHE_ENTRY_LOCK_EMPTY) { + *slot_index = first_available != UINT32_MAX ? first_available : index; + + return true; + } + + if (record->state == ZEND_OPCACHE_USER_CACHE_ENTRY_LOCK_TOMBSTONE) { + if (first_available == UINT32_MAX) { + first_available = index; + } + continue; + } + + if (!zend_opcache_user_cache_entry_lock_record_is_active_locked(record, now)) { + zend_opcache_user_cache_remove_entry_lock_record_locked(record); + if (first_available == UINT32_MAX) { + first_available = index; + } + continue; + } + + if (zend_opcache_user_cache_entry_lock_record_key_matches(record, key, hash)) { + *slot_index = index; + *found = true; + + return true; + } + } + + if (first_available != UINT32_MAX) { + *slot_index = first_available; + + return true; + } + + return false; +} + +static bool zend_opcache_user_cache_insert_entry_lock_record_locked( + zend_opcache_user_cache_header *header, + uint32_t slot_index, + zend_string *key, + zend_ulong hash, + zend_long lease) +{ + zend_opcache_user_cache_entry_lock_record *record = &header->entry_lock_records[slot_index]; + uint32_t key_offset; + uint64_t owner_pid; + + if (ZSTR_LEN(key) > UINT32_MAX) { + return false; + } + + key_offset = zend_opcache_user_cache_alloc_locked(ZSTR_LEN(key), ZSTR_VAL(key)); + if (key_offset == 0) { + return false; + } + + owner_pid = zend_opcache_user_cache_entry_lock_current_pid(); + + memset(record, 0, sizeof(*record)); + record->hash = hash; + record->owner_pid = owner_pid; + record->owner_start_time = zend_opcache_user_cache_process_start_time(owner_pid); + record->expires_at = lease > 0 ? zend_opcache_user_cache_entry_lock_expires_at(lease) : 0; + record->key_offset = key_offset; + record->key_len = (uint32_t) ZSTR_LEN(key); + record->state = ZEND_OPCACHE_USER_CACHE_ENTRY_LOCK_USED; + + return true; +} + +static bool zend_opcache_user_cache_update_entry_lock_record_lease_locked( + zend_opcache_user_cache_header *header, + zend_string *key, + zend_long lease) +{ + zend_opcache_user_cache_entry_lock_record *record; + zend_ulong hash; + uint32_t slot_index; + uint64_t expires_at; + bool found; + + if (lease <= 0) { + return true; + } + + hash = zend_string_hash_val(key); + if (!zend_opcache_user_cache_find_entry_lock_record_slot_locked(header, key, hash, &slot_index, &found) || !found) { + return false; + } + + record = &header->entry_lock_records[slot_index]; + expires_at = zend_opcache_user_cache_entry_lock_expires_at(lease); + if (record->expires_at < expires_at) { + record->expires_at = expires_at; + } + + return true; +} + +static zend_opcache_user_cache_entry_lock *zend_opcache_user_cache_create_local_entry_lock( + zend_opcache_user_cache_context *context, + zend_long lease, + bool preserve_lease) +{ + zend_opcache_user_cache_entry_lock *lock; + + lock = emalloc(sizeof(zend_opcache_user_cache_entry_lock)); + lock->context = context; + lock->owner_pid = zend_opcache_user_cache_entry_lock_current_pid(); + lock->lease = lease; + lock->preserve_lease = preserve_lease; + + return lock; +} + +static bool zend_opcache_user_cache_add_local_entry_lock( + HashTable *locks, + zend_string *key, + zend_opcache_user_cache_entry_lock *lock) +{ + bool added = false; + + zend_try { + added = zend_hash_add_ptr(locks, key, lock) != NULL; + } zend_catch { + efree(lock); + zend_bailout(); + } zend_end_try(); + + return added; +} + +static void zend_opcache_user_cache_release_entry_lock_context( + zend_opcache_user_cache_context *context, + HashTable **locks_ptr, + zend_opcache_user_cache_entry_lock_release_mode mode) +{ + zend_opcache_user_cache_context *previous_context; + zend_opcache_user_cache_entry_lock *lock; + zend_opcache_user_cache_header *header; + zend_string *key, **released_keys; + zend_ulong hash; + uint32_t lock_count, released_key_count = 0, index, slot_index; + bool found; + + if (*locks_ptr == NULL) { + return; + } + + lock_count = zend_hash_num_elements(*locks_ptr); + if (lock_count == 0 || mode == ZEND_OPCACHE_USER_CACHE_ENTRY_LOCK_RELEASE_DROP) { + zend_hash_destroy(*locks_ptr); + FREE_HASHTABLE(*locks_ptr); + *locks_ptr = NULL; + + return; + } + + released_keys = safe_emalloc(lock_count, sizeof(zend_string *), 0); + ZEND_HASH_FOREACH_STR_KEY_PTR(*locks_ptr, key, lock) { + if (key == NULL || lock == NULL || lock->context != context) { + continue; + } + + released_keys[released_key_count++] = zend_string_copy(key); + } ZEND_HASH_FOREACH_END(); + + if (released_key_count == 0) { + efree(released_keys); + + return; + } + + previous_context = zend_opcache_user_cache_activate_context(context); + if (zend_opcache_user_cache_wlock()) { + header = zend_opcache_user_cache_header_ptr(); + if (zend_opcache_user_cache_header_init_locked()) { + for (index = 0; index < released_key_count; index++) { + key = released_keys[index]; + lock = zend_hash_find_ptr(*locks_ptr, key); + if (lock == NULL) { + continue; + } + + hash = zend_string_hash_val(key); + if (!zend_opcache_user_cache_find_entry_lock_record_slot_locked(header, key, hash, &slot_index, &found) || !found) { + continue; + } + + if (header->entry_lock_records[slot_index].owner_pid != lock->owner_pid) { + continue; + } + + if (mode == ZEND_OPCACHE_USER_CACHE_ENTRY_LOCK_RELEASE_PRESERVE_LEASES && + lock->preserve_lease && + lock->lease > 0) { + header->entry_lock_records[slot_index].owner_pid = 0; + header->entry_lock_records[slot_index].owner_start_time = 0; + header->entry_lock_records[slot_index].expires_at = + zend_opcache_user_cache_entry_lock_expires_at(lock->lease); + } else { + zend_opcache_user_cache_remove_entry_lock_record_locked(&header->entry_lock_records[slot_index]); + } + } + } + zend_opcache_user_cache_unlock(); + } + zend_opcache_user_cache_restore_context(previous_context); + + for (index = 0; index < released_key_count; index++) { + zend_hash_del(*locks_ptr, released_keys[index]); + zend_string_release(released_keys[index]); + } + efree(released_keys); + + if (zend_hash_num_elements(*locks_ptr) == 0) { + zend_hash_destroy(*locks_ptr); + FREE_HASHTABLE(*locks_ptr); + *locks_ptr = NULL; + } +} + +static void zend_opcache_user_cache_release_entry_lock_all_contexts( + HashTable **locks_ptr, + zend_opcache_user_cache_entry_lock_release_mode mode) +{ + zend_opcache_user_cache_context *context = NULL; + zend_opcache_user_cache_entry_lock *lock; + + while (*locks_ptr != NULL) { + context = NULL; + + ZEND_HASH_FOREACH_PTR(*locks_ptr, lock) { + if (lock != NULL && lock->context != NULL) { + context = lock->context; + + break; + } + } ZEND_HASH_FOREACH_END(); + + if (context == NULL) { + zend_opcache_user_cache_release_entry_lock_context( + zend_opcache_user_cache_default_context(), + locks_ptr, + ZEND_OPCACHE_USER_CACHE_ENTRY_LOCK_RELEASE_DROP + ); + + return; + } + + zend_opcache_user_cache_release_entry_lock_context(context, locks_ptr, mode); + } +} + +static void zend_opcache_user_cache_ensure_entry_lock_process(void) +{ +#ifndef ZEND_WIN32 + zend_ulong current_pid = (zend_ulong) getpid(); + + if (zend_opcache_user_cache_entry_lock_owner_pid == 0) { + zend_opcache_user_cache_entry_lock_owner_pid = current_pid; + + return; + } + + if (zend_opcache_user_cache_entry_lock_owner_pid == current_pid) { + return; + } + + /* Drop inherited lock tables without releasing parent-owned records. */ + zend_opcache_user_cache_release_entry_lock_context( + zend_opcache_user_cache_default_context(), + &zend_opcache_user_cache_entry_lock_table, + ZEND_OPCACHE_USER_CACHE_ENTRY_LOCK_RELEASE_DROP + ); + zend_opcache_user_cache_entry_lock_owner_pid = current_pid; +#endif +} + +static void zend_opcache_user_cache_entry_lock_dtor(zval *lock_zv) +{ + zend_opcache_user_cache_entry_lock *lock = Z_PTR_P(lock_zv); + + if (lock != NULL) { + efree(lock); + } +} + +static HashTable *zend_opcache_user_cache_entry_locks(void) +{ + HashTable **locks_ptr = zend_opcache_user_cache_entry_lock_table_ptr(); + + if (*locks_ptr == NULL) { + ALLOC_HASHTABLE(*locks_ptr); + zend_hash_init(*locks_ptr, 0, NULL, zend_opcache_user_cache_entry_lock_dtor, 0); + } + + return *locks_ptr; +} + +static HashTable *zend_opcache_user_cache_prepare_entry_locks_for_insert(void) +{ + HashTable *locks = zend_opcache_user_cache_entry_locks(); + + zend_hash_extend(locks, zend_hash_num_elements(locks) + 1, 0); + + return locks; +} + +static void zend_opcache_user_cache_destroy_empty_entry_locks(HashTable **locks_ptr) +{ + if (*locks_ptr != NULL && zend_hash_num_elements(*locks_ptr) == 0) { + zend_hash_destroy(*locks_ptr); + FREE_HASHTABLE(*locks_ptr); + *locks_ptr = NULL; + } +} + +static bool zend_opcache_user_cache_acquire_entry_lock_record( + zend_opcache_user_cache_context *context, + zend_string *key, + zend_long lease, + bool preserve_lease, + bool blocking) +{ + zend_opcache_user_cache_context *previous_context; + zend_opcache_user_cache_entry_lock *lock; + HashTable **locks_ptr = zend_opcache_user_cache_entry_lock_table_ptr(); + HashTable *locks; + zend_opcache_user_cache_header *header; + zend_ulong hash = zend_string_hash_val(key); + uint32_t slot_index; + bool found, inserted, insert_failed; + + zend_opcache_user_cache_ensure_entry_lock_process(); + + if (*locks_ptr != NULL && (lock = zend_hash_find_ptr(*locks_ptr, key)) != NULL) { + if (lease > lock->lease) { + previous_context = zend_opcache_user_cache_activate_context(context); + if (!zend_opcache_user_cache_wlock()) { + zend_opcache_user_cache_restore_context(previous_context); + + return false; + } + + header = zend_opcache_user_cache_header_ptr(); + inserted = zend_opcache_user_cache_header_init_locked() && + zend_opcache_user_cache_update_entry_lock_record_lease_locked(header, key, lease) + ; + zend_opcache_user_cache_unlock(); + zend_opcache_user_cache_restore_context(previous_context); + + if (!inserted) { + return false; + } + + lock->lease = lease; + } + lock->preserve_lease = lock->preserve_lease || preserve_lease; + + return true; + } + + locks = zend_opcache_user_cache_prepare_entry_locks_for_insert(); + lock = zend_opcache_user_cache_create_local_entry_lock(context, lease, preserve_lease); + + for (;;) { + inserted = false; + found = false; + insert_failed = false; + + previous_context = zend_opcache_user_cache_activate_context(context); + if (!zend_opcache_user_cache_wlock()) { + zend_opcache_user_cache_restore_context(previous_context); + efree(lock); + zend_opcache_user_cache_destroy_empty_entry_locks(locks_ptr); + + return false; + } + + header = zend_opcache_user_cache_header_ptr(); + if (!zend_opcache_user_cache_header_init_locked()) { + zend_opcache_user_cache_unlock(); + zend_opcache_user_cache_restore_context(previous_context); + efree(lock); + zend_opcache_user_cache_destroy_empty_entry_locks(locks_ptr); + + return false; + } + if (zend_opcache_user_cache_find_entry_lock_record_slot_locked(header, key, hash, &slot_index, &found)) { + if (!found && + !(inserted = zend_opcache_user_cache_insert_entry_lock_record_locked(header, slot_index, key, hash, lease)) + ) { + insert_failed = true; + } + } + zend_opcache_user_cache_unlock(); + zend_opcache_user_cache_restore_context(previous_context); + + if (inserted) { + if (zend_opcache_user_cache_add_local_entry_lock(locks, key, lock)) { + return true; + } + + previous_context = zend_opcache_user_cache_activate_context(context); + if (zend_opcache_user_cache_wlock()) { + header = zend_opcache_user_cache_header_ptr(); + if (zend_opcache_user_cache_header_is_initialized_locked() && + zend_opcache_user_cache_find_entry_lock_record_slot_locked(header, key, hash, &slot_index, &found) && + found && + header->entry_lock_records[slot_index].owner_pid == lock->owner_pid + ) { + zend_opcache_user_cache_remove_entry_lock_record_locked(&header->entry_lock_records[slot_index]); + } + zend_opcache_user_cache_unlock(); + } + zend_opcache_user_cache_restore_context(previous_context); + efree(lock); + zend_opcache_user_cache_destroy_empty_entry_locks(locks_ptr); + + return false; + } + + if (insert_failed) { + efree(lock); + zend_opcache_user_cache_destroy_empty_entry_locks(locks_ptr); + + return false; + } + + if (!blocking) { + efree(lock); + zend_opcache_user_cache_destroy_empty_entry_locks(locks_ptr); + + return false; + } + + zend_opcache_user_cache_entry_lock_lease_wait(); + } +} + +static uint32_t zend_opcache_user_cache_calculate_capacity(size_t size) +{ + size_t capacity = size / ZEND_OPCACHE_USER_CACHE_SLOT_BYTES, data_offset; + + if (capacity < ZEND_OPCACHE_USER_CACHE_MIN_CAPACITY) { + capacity = ZEND_OPCACHE_USER_CACHE_MIN_CAPACITY; + } else if (capacity > ZEND_OPCACHE_USER_CACHE_MAX_CAPACITY) { + capacity = ZEND_OPCACHE_USER_CACHE_MAX_CAPACITY; + } + + if ((capacity & 1) == 0) { + capacity--; + } + + for (;;) { + data_offset = ZEND_ALIGNED_SIZE(sizeof(zend_opcache_user_cache_header) + capacity * sizeof(zend_opcache_user_cache_entry)); + if (data_offset < size || capacity == ZEND_OPCACHE_USER_CACHE_MIN_CAPACITY) { + break; + } + + capacity >>= 1; + if (capacity < ZEND_OPCACHE_USER_CACHE_MIN_CAPACITY) { + capacity = ZEND_OPCACHE_USER_CACHE_MIN_CAPACITY; + } + + if ((capacity & 1) == 0) { + capacity--; + } + } + + return (uint32_t) capacity; +} + +static void zend_opcache_user_cache_free_list_remove_locked(zend_opcache_user_cache_header *header, uint32_t block_offset) +{ + zend_opcache_user_cache_block *block = zend_opcache_user_cache_block_ptr(block_offset); + + if (block->prev_free != 0) { + zend_opcache_user_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_user_cache_block_ptr(block->next_free)->prev_free = block->prev_free; + } + + block->next_free = 0; + block->prev_free = 0; + block->flags &= ~ZEND_OPCACHE_USER_CACHE_BLOCK_FREE; +} + +static void zend_opcache_user_cache_free_list_insert_locked(zend_opcache_user_cache_header *header, uint32_t block_offset) +{ + zend_opcache_user_cache_block *block = zend_opcache_user_cache_block_ptr(block_offset); + + block->prev_free = 0; + block->next_free = header->free_list; + + if (header->free_list != 0) { + zend_opcache_user_cache_block_ptr(header->free_list)->prev_free = block_offset; + } + + zend_opcache_user_cache_block_mark_free(block); + header->free_list = block_offset; +} + +static void zend_opcache_user_cache_update_following_prev_size_locked( + zend_opcache_user_cache_header *header, + uint32_t block_offset, + const zend_opcache_user_cache_block *block +) +{ + uint32_t next_offset = block_offset + block->size; + + if (next_offset < zend_opcache_user_cache_used_end_offset_locked(header)) { + zend_opcache_user_cache_block_ptr(next_offset)->prev_size = block->size; + } +} + +static void zend_opcache_user_cache_trim_tail_free_blocks_locked( + zend_opcache_user_cache_header *header, + uint32_t block_offset +) +{ + zend_opcache_user_cache_block *block = zend_opcache_user_cache_block_ptr(block_offset); + uint32_t prev_offset; + + while (block_offset >= header->data_offset && + header->last_block_offset == block_offset && + zend_opcache_user_cache_block_is_free(block) && + block_offset + block->size == zend_opcache_user_cache_used_end_offset_locked(header) + ) { + prev_offset = 0; + zend_opcache_user_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_user_cache_block_ptr(block_offset); + } +} + +static bool zend_opcache_user_cache_try_handler(const zend_shared_memory_handler_entry *handler_entry) +{ + const char *error_in = NULL; + zend_opcache_user_cache_context *context = zend_opcache_user_cache_active_context(); + zend_opcache_user_cache_runtime *runtime = zend_opcache_user_cache_active_runtime(); + zend_opcache_user_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_user_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_user_cache_startup_storage(void) +{ + const zend_shared_memory_handler_entry *handler_entry; + const char *requested_model = ZCG(accel_directives).memory_model; + zend_opcache_user_cache_storage *storage = &zend_opcache_user_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_user_cache_handler_table; handler_entry->name; handler_entry++) { + if (strcmp(requested_model, handler_entry->name) == 0 && zend_opcache_user_cache_try_handler(handler_entry)) { + goto storage_ready; + } + } + } + + for (handler_entry = zend_opcache_user_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_user_cache_try_handler(handler_entry)) { + goto storage_ready; + } + } + + return false; + +storage_ready: + if (storage->segment_count != 1) { + zend_opcache_user_cache_cleanup_segments( + storage->handler, + storage->segments, + storage->segment_count + ); + zend_opcache_user_cache_reset_storage(); + + return false; + } + + if (!zend_opcache_user_cache_lock_startup()) { + zend_opcache_user_cache_cleanup_segments( + storage->handler, + storage->segments, + storage->segment_count + ); + zend_opcache_user_cache_reset_storage(); + + return false; + } + + if (!zend_opcache_user_cache_wlock()) { + zend_opcache_user_cache_lock_shutdown(); + zend_opcache_user_cache_cleanup_segments( + storage->handler, + storage->segments, + storage->segment_count + ); + zend_opcache_user_cache_reset_storage(); + + return false; + } + + if (!zend_opcache_user_cache_header_init_locked()) { + zend_opcache_user_cache_unlock(); + zend_opcache_user_cache_lock_shutdown(); + zend_opcache_user_cache_cleanup_segments( + storage->handler, + storage->segments, + storage->segment_count + ); + zend_opcache_user_cache_reset_storage(); + + return false; + } + + zend_opcache_user_cache_unlock(); + +#ifdef ZEND_OPCACHE_USER_CACHE_HAVE_SHARED_MUTEX + storage->use_shared_mutex = + zend_opcache_user_cache_header_ptr() != NULL && + zend_opcache_user_cache_header_ptr()->lock_model == ZEND_OPCACHE_USER_CACHE_LOCK_MODEL_MUTEX + ; +#endif + + return true; +} + +static bool zend_opcache_user_cache_block_is_movable_locked( + zend_opcache_user_cache_header *header, + uint32_t block_offset, + uint32_t block_size +) +{ + zend_opcache_user_cache_entry *entries, *entry; + zend_opcache_user_cache_entry_lock_record *record; + uint32_t index; + bool referenced = false; + + entries = zend_opcache_user_cache_entries(header); + for (index = 0; index < header->capacity; index++) { + entry = &entries[index]; + if (entry->state != ZEND_OPCACHE_USER_CACHE_ENTRY_USED) { + continue; + } + + if (entry->key_offset != 0 && + zend_opcache_user_cache_offset_in_block(entry->key_offset, block_offset, block_size) + ) { + referenced = true; + } + + if (entry->value_offset != 0 && + zend_opcache_user_cache_offset_in_block(entry->value_offset, block_offset, block_size) + ) { + referenced = true; + if (entry->value_type == ZEND_OPCACHE_USER_CACHE_VALUE_SHARED_GRAPH) { + if (!zend_opcache_user_cache_shared_graph_can_move_payload_locked(entry->value_offset)) { + return false; + } + } + } + } + + for (index = 0; index < ZEND_OPCACHE_USER_CACHE_ENTRY_LOCK_TABLE_SIZE; index++) { + record = &header->entry_lock_records[index]; + if (record->state == ZEND_OPCACHE_USER_CACHE_ENTRY_LOCK_USED && + record->key_offset != 0 && + zend_opcache_user_cache_offset_in_block(record->key_offset, block_offset, block_size) + ) { + referenced = true; + } + } + + return referenced; +} + +static void zend_opcache_user_cache_update_moved_block_entries_locked( + zend_opcache_user_cache_header *header, + uint32_t old_block_offset, + uint32_t new_block_offset, + uint32_t block_size +) +{ + zend_opcache_user_cache_entry *entries, *entry; + zend_opcache_user_cache_entry_lock_record *record; + uint32_t index, delta, new_value_offset; + + ZEND_ASSERT(new_block_offset <= old_block_offset); + delta = old_block_offset - new_block_offset; + entries = zend_opcache_user_cache_entries(header); + for (index = 0; index < header->capacity; index++) { + entry = &entries[index]; + if (entry->state != ZEND_OPCACHE_USER_CACHE_ENTRY_USED) { + continue; + } + + if (entry->key_offset != 0 && + zend_opcache_user_cache_offset_in_block(entry->key_offset, old_block_offset, block_size) + ) { + entry->key_offset -= delta; + } + + if (entry->value_offset != 0 && + zend_opcache_user_cache_offset_in_block(entry->value_offset, old_block_offset, block_size) + ) { + new_value_offset = entry->value_offset - delta; + + if (entry->value_type == ZEND_OPCACHE_USER_CACHE_VALUE_SHARED_GRAPH) { + if (!zend_opcache_user_cache_shared_graph_rebase_moved_payload_locked(new_value_offset, delta)) { + ZEND_ASSERT(0); + } + } + + entry->value_offset = new_value_offset; + } + } + + for (index = 0; index < ZEND_OPCACHE_USER_CACHE_ENTRY_LOCK_TABLE_SIZE; index++) { + record = &header->entry_lock_records[index]; + if (record->state == ZEND_OPCACHE_USER_CACHE_ENTRY_LOCK_USED && + record->key_offset != 0 && + zend_opcache_user_cache_offset_in_block(record->key_offset, old_block_offset, block_size) + ) { + record->key_offset -= delta; + } + } +} + +static bool zend_opcache_user_cache_compaction_can_fit_locked( + zend_opcache_user_cache_header *header, + uint32_t required_block_size +) +{ + zend_opcache_user_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_user_cache_block_ptr(offset); + block_size = block->size; + if (block_size < ZEND_ALIGNED_SIZE(sizeof(zend_opcache_user_cache_block) + 1) || + block_size > used_end - offset + ) { + return false; + } + + next_offset = offset + block_size; + if (!zend_opcache_user_cache_block_is_free(block)) { + movable = zend_opcache_user_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; +} + +static bool zend_opcache_user_cache_compact_movable_blocks_locked(zend_opcache_user_cache_header *header) +{ + zend_opcache_user_cache_block *block, *free_block; + uint32_t used_end, offset, next_offset, block_size, write_offset, + previous_block_size = 0, last_block_offset = 0, free_size; + bool moved = false, movable; + + 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_user_cache_block_ptr(offset); + block_size = block->size; + if (block_size < ZEND_ALIGNED_SIZE(sizeof(zend_opcache_user_cache_block) + 1) || + block_size > used_end - offset + ) { + return false; + } + + next_offset = offset + block_size; + if (zend_opcache_user_cache_block_is_free(block)) { + offset = next_offset; + continue; + } + + movable = zend_opcache_user_cache_block_is_movable_locked(header, offset, block_size); + if (!movable) { + if (write_offset < offset) { + free_block = zend_opcache_user_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_USER_CACHE_BLOCK_FREE; + zend_opcache_user_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_user_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_user_cache_ptr(write_offset), block, block_size); + zend_opcache_user_cache_update_moved_block_entries_locked(header, offset, write_offset, block_size); + block = zend_opcache_user_cache_block_ptr(write_offset); + moved = true; + } + + block->prev_size = previous_block_size; + block->next_free = 0; + block->prev_free = 0; + zend_opcache_user_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_user_cache_bump_mutation_epoch_locked(header); + } + + return moved; +} + +static bool zend_opcache_user_cache_compact_if_low_memory_locked( + zend_opcache_user_cache_header *header, + uint32_t allocating_block_size +) +{ + uint32_t tail_remaining; + + if (!zend_opcache_user_cache_active_context()->clear_on_pressure || + header->free_list == 0 || + header->next_free > header->data_size + ) { + return false; + } + + tail_remaining = header->data_size - header->next_free; + if (tail_remaining >= ZEND_OPCACHE_USER_CACHE_LOW_MEMORY_COMPACT_THRESHOLD && + allocating_block_size <= tail_remaining - ZEND_OPCACHE_USER_CACHE_LOW_MEMORY_COMPACT_THRESHOLD + ) { + return false; + } + + if (!zend_opcache_user_cache_compaction_can_fit_locked(header, 0)) { + return false; + } + + return zend_opcache_user_cache_compact_movable_blocks_locked(header); +} + +#ifdef ZEND_OPCACHE_USER_CACHE_HAVE_OPTIMISTIC +static void zend_opcache_user_cache_write_section_enter(void) +{ + zend_opcache_user_cache_header *header = zend_opcache_user_cache_header_ptr(); + + zend_opcache_user_cache_write_seq_bumped = false; + zend_opcache_user_cache_reader_drain_state = 0; + + if (header == NULL || !zend_opcache_user_cache_header_is_initialized_locked()) { + return; + } + + zend_opcache_user_cache_seq_announce(&header->write_seq, header->write_seq + 1); + zend_opcache_user_cache_write_seq_bumped = true; +} + +static void zend_opcache_user_cache_write_section_leave(void) +{ + zend_opcache_user_cache_header *header; + + if (!zend_opcache_user_cache_write_seq_bumped) { + return; + } + + zend_opcache_user_cache_write_seq_bumped = false; + zend_opcache_user_cache_reader_drain_state = 0; + + header = zend_opcache_user_cache_header_ptr(); + if (header == NULL) { + return; + } + + zend_opcache_user_cache_seq_publish(&header->write_seq, header->write_seq + 1); +} + +/* A freshly initialized header becomes visible before the write section ends. */ +static void zend_opcache_user_cache_write_section_note_header_initialized(zend_opcache_user_cache_header *header) +{ + if (!zend_opcache_user_cache_lock_held_is_write || zend_opcache_user_cache_write_seq_bumped) { + return; + } + + zend_opcache_user_cache_seq_announce(&header->write_seq, header->write_seq + 1); + zend_opcache_user_cache_write_seq_bumped = true; + zend_opcache_user_cache_reader_drain_state = 0; +} + +static bool zend_opcache_user_cache_reader_slot_owner_is_dead(const zend_opcache_user_cache_reader_slot *slot) +{ + return zend_opcache_user_cache_process_owner_is_dead(slot->owner_pid, slot->owner_start_time); +} + +#ifndef ZEND_WIN32 +static void zend_opcache_user_cache_optimistic_atfork_child(void) +{ + zend_opcache_user_cache_reader_claim_count = 0; +} +#endif + +static int32_t zend_opcache_user_cache_claim_reader_slot(zend_opcache_user_cache_header *header) +{ + zend_opcache_user_cache_reader_slot *slot; + uint64_t my_pid, expected; + uint32_t index; + + for (index = 0; index < zend_opcache_user_cache_reader_claim_count; index++) { + if (zend_opcache_user_cache_reader_claims[index].header == header) { + return (int32_t) zend_opcache_user_cache_reader_claims[index].slot_index; + } + } + + if (zend_opcache_user_cache_reader_claim_count == ZEND_OPCACHE_USER_CACHE_READER_CLAIM_MAX || + zend_opcache_user_cache_reader_incarnation == 0 + ) { + return -1; + } + + my_pid = zend_opcache_user_cache_current_pid(); + + for (index = 0; index < ZEND_OPCACHE_USER_CACHE_READER_SLOTS; index++) { + slot = &header->reader_slots[index]; + + expected = zend_opcache_user_cache_atomic_load_64(&slot->owner_pid); + + if (expected == my_pid && + slot->owner_incarnation != zend_opcache_user_cache_reader_incarnation + ) { + /* Stale claim from a previous incarnation of this PID. */ + zend_opcache_user_cache_atomic_store_32(&slot->active, 0); + slot->owner_incarnation = zend_opcache_user_cache_reader_incarnation; + slot->owner_start_time = zend_opcache_user_cache_process_start_time(my_pid); + + goto claimed; + } + + if (expected != 0) { + if (zend_opcache_user_cache_reader_slot_owner_is_dead(slot) && + zend_opcache_user_cache_atomic_load_32(&slot->active) == 0 && + zend_opcache_user_cache_atomic_cas_64(&slot->owner_pid, expected, my_pid) + ) { + slot->owner_incarnation = zend_opcache_user_cache_reader_incarnation; + slot->owner_start_time = zend_opcache_user_cache_process_start_time(my_pid); + + goto claimed; + } + + continue; + } + + if (zend_opcache_user_cache_atomic_cas_64(&slot->owner_pid, 0, my_pid)) { + slot->owner_incarnation = zend_opcache_user_cache_reader_incarnation; + slot->owner_start_time = zend_opcache_user_cache_process_start_time(my_pid); + + goto claimed; + } + } + + return -1; + +claimed: + zend_opcache_user_cache_reader_claims[zend_opcache_user_cache_reader_claim_count].header = header; + zend_opcache_user_cache_reader_claims[zend_opcache_user_cache_reader_claim_count].slot_index = index; + zend_opcache_user_cache_reader_claim_count++; + + return (int32_t) index; +} + +bool zend_opcache_user_cache_graph_payloads_quiescent_locked(void) +{ + zend_opcache_user_cache_header *header; + zend_opcache_user_cache_reader_slot *slot; + uint64_t waited_us = 0; + uint32_t count, index, spin = 0; + + if (!zend_opcache_user_cache_write_seq_bumped) { + return true; + } + + if (zend_opcache_user_cache_reader_drain_state != 0) { + return zend_opcache_user_cache_reader_drain_state > 0; + } + + header = zend_opcache_user_cache_header_ptr(); + if (header == NULL) { + return true; + } + + /* Pair with the reader's publish fence: after this fence, either the + * reader's counter increment is visible here, or the reader observes the + * odd sequence and backs off without pinning. */ + zend_opcache_user_cache_atomic_fence_seq_cst(); + + for (;;) { + count = zend_opcache_user_cache_atomic_load_32(&header->active_optimistic_readers); + if (count == 0) { + zend_opcache_user_cache_reader_drain_state = 1; + + return true; + } + + spin++; + + if ((spin & 0xFFU) == 0) { + /* Recover readers whose owner is known dead. */ + for (index = 0; index < ZEND_OPCACHE_USER_CACHE_READER_SLOTS; index++) { + slot = &header->reader_slots[index]; + + if (zend_opcache_user_cache_atomic_load_32(&slot->active) == 0 || slot->owner_pid == 0) { + continue; + } + + if (zend_opcache_user_cache_reader_slot_owner_is_dead(slot) && + zend_opcache_user_cache_atomic_cas_32(&slot->active, 1, 0) + ) { + zend_opcache_user_cache_atomic_dec_32(&header->active_optimistic_readers); + } + } + } + + if (spin > ZEND_OPCACHE_USER_CACHE_READER_DRAIN_SPIN) { +#ifdef ZEND_WIN32 + Sleep(1); + waited_us += 1000; +#else + usleep(50); + waited_us += 50; +#endif + + if (waited_us > ZEND_OPCACHE_USER_CACHE_READER_DRAIN_TIMEOUT_US) { + zend_opcache_user_cache_reader_drain_state = -1; + + return false; + } + } + } +} + +void zend_opcache_user_cache_optimistic_fork_setup(void) +{ + static bool registered = false; + + if (zend_opcache_user_cache_reader_incarnation == 0) { + zend_opcache_user_cache_reader_incarnation = + ((uint64_t) time(NULL) << 20) ^ + (uint64_t) (uintptr_t) &zend_opcache_user_cache_reader_incarnation ^ + zend_opcache_user_cache_current_pid() + ; + if (zend_opcache_user_cache_reader_incarnation == 0) { + zend_opcache_user_cache_reader_incarnation = 1; + } + } + + if (registered) { + return; + } + + registered = true; + +#ifndef ZEND_WIN32 + pthread_atfork(NULL, NULL, zend_opcache_user_cache_optimistic_atfork_child); +#endif +} + +bool zend_opcache_user_cache_optimistic_reader_begin(zend_opcache_user_cache_header *header, uint32_t *slot_index_ptr) +{ + int32_t slot_index = zend_opcache_user_cache_claim_reader_slot(header); + + if (slot_index < 0) { + return false; + } + + *slot_index_ptr = (uint32_t) slot_index; + + zend_opcache_user_cache_atomic_store_32(&header->reader_slots[slot_index].active, 1); + /* Pairs with the draining writer's fence. */ + zend_opcache_user_cache_atomic_inc_32(&header->active_optimistic_readers); + + return true; +} + +void zend_opcache_user_cache_optimistic_reader_end(zend_opcache_user_cache_header *header, uint32_t slot_index) +{ + zend_opcache_user_cache_atomic_dec_32(&header->active_optimistic_readers); + zend_opcache_user_cache_atomic_store_32(&header->reader_slots[slot_index].active, 0); +} +#else +bool zend_opcache_user_cache_graph_payloads_quiescent_locked(void) +{ + return true; +} + +void zend_opcache_user_cache_optimistic_fork_setup(void) +{ +} + +bool zend_opcache_user_cache_optimistic_reader_begin(zend_opcache_user_cache_header *header, uint32_t *slot_index_ptr) +{ + (void) header; + (void) slot_index_ptr; + + return false; +} + +void zend_opcache_user_cache_optimistic_reader_end(zend_opcache_user_cache_header *header, uint32_t slot_index) +{ + (void) header; + (void) slot_index; +} +#endif + +void zend_opcache_user_cache_reset_runtime(void) +{ + zend_opcache_user_cache_context *context = zend_opcache_user_cache_active_context(); + zend_opcache_user_cache_runtime *runtime = zend_opcache_user_cache_active_runtime(); + + memset(runtime, 0, sizeof(*runtime)); + + runtime->configured_memory = ZCG(accel_directives).user_cache_shm_size; + + runtime->enabled = runtime->configured_memory != 0; + if (!runtime->enabled) { + runtime->available = false; + runtime->startup_failed = false; + runtime->backend_initialized = context->storage.initialized; + runtime->failure_reason = "OPcache User Cache is disabled by opcache.user_cache_shm_size=0"; + } + +} + +void zend_opcache_user_cache_reset_storage(void) +{ + zend_opcache_user_cache_storage *storage = &zend_opcache_user_cache_active_context()->storage; + + memset(storage, 0, sizeof(*storage)); + storage->lock_file = -1; +} + +bool zend_opcache_user_cache_header_init_locked(void) +{ + zend_opcache_user_cache_storage *storage = &zend_opcache_user_cache_active_context()->storage; + zend_opcache_user_cache_header *header = zend_opcache_user_cache_header_ptr(); + uint32_t capacity, data_offset; + + if (!header) { + return false; + } + + if (header->magic == ZEND_OPCACHE_USER_CACHE_MAGIC && header->version == ZEND_OPCACHE_USER_CACHE_VERSION) { + return true; + } + + capacity = zend_opcache_user_cache_calculate_capacity(storage->size); + data_offset = (uint32_t) ZEND_ALIGNED_SIZE(sizeof(zend_opcache_user_cache_header) + capacity * sizeof(zend_opcache_user_cache_entry)); + if (data_offset >= storage->size) { + return false; + } + + 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->write_seq = 2; +#ifdef ZEND_OPCACHE_USER_CACHE_HAVE_SHARED_MUTEX + header->lock_model = zend_opcache_user_cache_shared_mutexes_init(header) + ? ZEND_OPCACHE_USER_CACHE_LOCK_MODEL_MUTEX + : ZEND_OPCACHE_USER_CACHE_LOCK_MODEL_FCNTL + ; +#else + header->lock_model = ZEND_OPCACHE_USER_CACHE_LOCK_MODEL_FCNTL; +#endif + header->magic = ZEND_OPCACHE_USER_CACHE_MAGIC; + header->version = ZEND_OPCACHE_USER_CACHE_VERSION; + +#ifdef ZEND_OPCACHE_USER_CACHE_HAVE_OPTIMISTIC + zend_opcache_user_cache_write_section_note_header_initialized(header); +#endif + + return true; +} + +void zend_opcache_user_cache_free_locked(uint32_t payload_offset) +{ + zend_opcache_user_cache_header *header = zend_opcache_user_cache_header_ptr(); + zend_opcache_user_cache_block *block, *adjacent; + uint32_t block_offset, original_block_offset, next_offset, prev_offset; + + if (!header || payload_offset < sizeof(zend_opcache_user_cache_block)) { + return; + } + + block_offset = payload_offset - (uint32_t) sizeof(zend_opcache_user_cache_block); + original_block_offset = block_offset; + block = zend_opcache_user_cache_block_ptr(block_offset); + if (zend_opcache_user_cache_block_is_free(block)) { + return; + } + + zend_opcache_user_cache_block_mark_free(block); + + next_offset = block_offset + block->size; + if (next_offset < zend_opcache_user_cache_used_end_offset_locked(header)) { + adjacent = zend_opcache_user_cache_block_ptr(next_offset); + + if (zend_opcache_user_cache_block_is_free(adjacent)) { + zend_opcache_user_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_user_cache_block_ptr(prev_offset); + if (zend_opcache_user_cache_block_is_free(adjacent)) { + zend_opcache_user_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_user_cache_update_following_prev_size_locked(header, block_offset, block); + zend_opcache_user_cache_free_list_insert_locked(header, block_offset); + zend_opcache_user_cache_trim_tail_free_blocks_locked(header, block_offset); +} + +uint32_t zend_opcache_user_cache_alloc_locked(size_t size, const void *source) +{ + zend_opcache_user_cache_header *header = zend_opcache_user_cache_header_ptr(); + zend_opcache_user_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_user_cache_block) + 1); + + if (!header || size == 0 || size > UINT32_MAX - sizeof(zend_opcache_user_cache_block)) { + return 0; + } + + aligned_size = ZEND_ALIGNED_SIZE(sizeof(zend_opcache_user_cache_block) + size); + if (aligned_size > UINT32_MAX) { + return 0; + } + + total_size = (uint32_t) aligned_size; + zend_opcache_user_cache_compact_if_low_memory_locked(header, total_size); + + free_offset_ptr = &header->free_list; + while (*free_offset_ptr != 0) { + block = zend_opcache_user_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_user_cache_block_ptr(best_offset); + zend_opcache_user_cache_free_list_remove_locked(header, best_offset); + if (block->size >= total_size + min_split_size) { + remainder = zend_opcache_user_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_USER_CACHE_BLOCK_FREE; + block->size = total_size; + zend_opcache_user_cache_update_following_prev_size_locked(header, best_offset + total_size, remainder); + zend_opcache_user_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_user_cache_update_following_prev_size_locked(header, best_offset, block); + } + zend_opcache_user_cache_block_mark_used(block); + if (source != NULL) { + memcpy(zend_opcache_user_cache_ptr(best_offset + sizeof(zend_opcache_user_cache_block)), source, size); + } + + return best_offset + (uint32_t) sizeof(zend_opcache_user_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_user_cache_block_ptr(block_offset); + block->size = total_size; + block->prev_size = header->last_block_offset != 0 ? zend_opcache_user_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_user_cache_ptr(block_offset + sizeof(zend_opcache_user_cache_block)), source, size); + } + header->next_free += total_size; + header->last_block_offset = block_offset; + + return block_offset + (uint32_t) sizeof(zend_opcache_user_cache_block); +} + +bool zend_opcache_user_cache_compact_to_fit_locked(size_t size) +{ + zend_opcache_user_cache_header *header = zend_opcache_user_cache_header_ptr(); + uint32_t required_block_size; + + if (!header || !zend_opcache_user_cache_header_init_locked()) { + return false; + } + + if (!zend_opcache_user_cache_payload_size_to_block_size(size, &required_block_size) || + required_block_size > header->data_size || + !zend_opcache_user_cache_compaction_can_fit_locked(header, required_block_size) + ) { + return false; + } + + return zend_opcache_user_cache_compact_movable_blocks_locked(header); +} + +bool zend_opcache_user_cache_startup_storage_before_request(void) +{ + zend_opcache_user_cache_storage *storage = &zend_opcache_user_cache_active_context()->storage; + + if (zend_opcache_user_cache_force_startup_failure()) { + zend_opcache_user_cache_set_unavailable("Unable to initialize shared memory backend", true); + + return false; + } + + if (!zend_opcache_user_cache_environment_is_allowed()) { + zend_opcache_user_cache_set_unavailable("OPcache User Cache is not enabled for this SAPI", false); + + return true; + } + + if (!zend_opcache_user_cache_startup_storage()) { + zend_opcache_user_cache_set_unavailable("Unable to initialize shared memory backend", true); + + return false; + } + + storage->initialized_before_request = true; + + return true; +} + +void zend_opcache_user_cache_shutdown_storage(void) +{ + zend_opcache_user_cache_storage *storage = &zend_opcache_user_cache_active_context()->storage; + + zend_opcache_user_cache_lock_shutdown(); + zend_opcache_user_cache_cleanup_segments( + storage->handler, + storage->segments, + storage->segment_count + ); + zend_opcache_user_cache_reset_storage(); +} + +void zend_opcache_user_cache_ensure_ready(void) +{ + zend_opcache_user_cache_context *context = zend_opcache_user_cache_active_context(); + zend_opcache_user_cache_runtime *runtime; + zend_opcache_user_cache_storage *storage = &context->storage; + + zend_opcache_user_cache_reset_runtime(); + runtime = zend_opcache_user_cache_active_runtime(); + if (!runtime->enabled) { + return; + } + + if (zend_opcache_user_cache_request_unavailable_reason != NULL) { + zend_opcache_user_cache_set_unavailable(zend_opcache_user_cache_request_unavailable_reason, false); + + return; + } + + if (!zend_opcache_user_cache_environment_is_allowed()) { + zend_opcache_user_cache_set_unavailable("OPcache User Cache is not enabled for this SAPI", false); + + return; + } + + if (zend_opcache_user_cache_opcache_is_disabled_for_sapi()) { + zend_opcache_user_cache_set_unavailable("OPcache is disabled", false); + + return; + } + + if (!accel_startup_ok) { + zend_opcache_user_cache_set_unavailable("OPcache startup failed", true); + + return; + } + + if (file_cache_only) { + zend_opcache_user_cache_set_unavailable("Cache is unavailable in file_cache_only mode", false); + + return; + } + + if (!storage->initialized && + zend_opcache_user_cache_requires_pre_request_storage() + ) { + zend_opcache_user_cache_set_unavailable("Shared memory backend was not initialized before worker startup", true); + + return; + } + + if (!zend_opcache_user_cache_startup_storage()) { + zend_opcache_user_cache_set_unavailable("Unable to initialize shared memory backend", true); + + return; + } + + if (zend_opcache_user_cache_requires_pre_request_storage() && + !storage->initialized_before_request + ) { + zend_opcache_user_cache_set_unavailable("Shared memory backend was initialized after worker startup", true); + + return; + } + + zend_opcache_user_cache_set_available(); +} + +bool zend_opcache_user_cache_rlock(void) +{ + if (!zend_opcache_user_cache_rlock_impl()) { + return false; + } + + zend_opcache_user_cache_lock_held_is_write = false; + + return true; +} + +bool zend_opcache_user_cache_wlock(void) +{ + if (!zend_opcache_user_cache_wlock_impl()) { + return false; + } + + zend_opcache_user_cache_lock_held_is_write = true; +#ifdef ZEND_OPCACHE_USER_CACHE_HAVE_OPTIMISTIC + zend_opcache_user_cache_write_section_enter(); +#endif + + return true; +} + +bool zend_opcache_user_cache_wlock_for_entry_mutation(zend_string *key) +{ + HashTable **locks_ptr = zend_opcache_user_cache_entry_lock_table_ptr(); + zend_opcache_user_cache_entry_lock *local_lock; + zend_opcache_user_cache_header *header; + zend_ulong hash = zend_string_hash_val(key); + uint32_t slot_index; + bool found; + + zend_opcache_user_cache_ensure_entry_lock_process(); + + for (;;) { + if (!zend_opcache_user_cache_wlock()) { + return false; + } + + if (!zend_opcache_user_cache_header_init_locked()) { + zend_opcache_user_cache_unlock(); + + return false; + } + + header = zend_opcache_user_cache_header_ptr(); + if (!zend_opcache_user_cache_find_entry_lock_record_slot_locked(header, key, hash, &slot_index, &found) || !found) { + return true; + } + + if (*locks_ptr != NULL && + (local_lock = zend_hash_find_ptr(*locks_ptr, key)) != NULL && + header->entry_lock_records[slot_index].owner_pid == local_lock->owner_pid + ) { + return true; + } + + zend_opcache_user_cache_unlock(); + zend_opcache_user_cache_entry_lock_lease_wait(); + } +} + +void zend_opcache_user_cache_unlock(void) +{ +#ifdef ZEND_OPCACHE_USER_CACHE_HAVE_OPTIMISTIC + if (zend_opcache_user_cache_lock_held_is_write) { + zend_opcache_user_cache_write_section_leave(); + } +#endif + zend_opcache_user_cache_lock_held_is_write = false; + zend_opcache_user_cache_unlock_impl(); +} + +void zend_opcache_user_cache_unlock_if_held(void) +{ + if (zend_opcache_user_cache_lock_held) { + zend_opcache_user_cache_unlock(); + } +} + +bool zend_opcache_user_cache_acquire_entry_lock(zend_string *key) +{ + return zend_opcache_user_cache_acquire_entry_lock_record( + zend_opcache_user_cache_active_context(), + key, + 0, + false, + true + ); +} + +bool zend_opcache_user_cache_try_acquire_entry_lock(zend_string *key, zend_long lease) +{ + return zend_opcache_user_cache_acquire_entry_lock_record( + zend_opcache_user_cache_active_context(), + key, + lease, + true, + false + ); +} + +bool zend_opcache_user_cache_has_entry_lock(zend_string *key) +{ + HashTable **locks_ptr = zend_opcache_user_cache_entry_lock_table_ptr(); + + zend_opcache_user_cache_ensure_entry_lock_process(); + + return *locks_ptr != NULL && zend_hash_exists(*locks_ptr, key); +} + +bool zend_opcache_user_cache_release_entry_lock(zend_string *key) +{ + zend_opcache_user_cache_context *context = zend_opcache_user_cache_active_context(); + zend_opcache_user_cache_context *previous_context; + zend_opcache_user_cache_entry_lock *lock; + HashTable **locks_ptr = zend_opcache_user_cache_entry_lock_table_ptr(); + zend_opcache_user_cache_header *header; + zend_ulong hash; + uint32_t slot_index; + bool found; + + zend_opcache_user_cache_ensure_entry_lock_process(); + + if (*locks_ptr == NULL) { + return false; + } + + lock = zend_hash_find_ptr(*locks_ptr, key); + if (lock == NULL) { + return false; + } + + hash = zend_string_hash_val(key); + previous_context = zend_opcache_user_cache_activate_context(context); + if (!zend_opcache_user_cache_wlock()) { + zend_opcache_user_cache_restore_context(previous_context); + + return false; + } + + header = zend_opcache_user_cache_header_ptr(); + if (zend_opcache_user_cache_header_is_initialized_locked() && + zend_opcache_user_cache_find_entry_lock_record_slot_locked(header, key, hash, &slot_index, &found) && + found && + header->entry_lock_records[slot_index].owner_pid == lock->owner_pid + ) { + zend_opcache_user_cache_remove_entry_lock_record_locked(&header->entry_lock_records[slot_index]); + } + zend_opcache_user_cache_unlock(); + zend_opcache_user_cache_restore_context(previous_context); + + zend_hash_del(*locks_ptr, key); + if (zend_hash_num_elements(*locks_ptr) == 0) { + zend_hash_destroy(*locks_ptr); + FREE_HASHTABLE(*locks_ptr); + *locks_ptr = NULL; + } + + return true; +} + +bool zend_opcache_user_cache_entry_locks_allow_clear_locked(void) +{ + zend_opcache_user_cache_header *header = zend_opcache_user_cache_header_ptr(); + zend_opcache_user_cache_entry_lock_record *record; + uint64_t owner_pid; + uint64_t now; + uint32_t index; + + zend_opcache_user_cache_ensure_entry_lock_process(); + + if (header == NULL || !zend_opcache_user_cache_header_is_initialized_locked()) { + return true; + } + + owner_pid = zend_opcache_user_cache_entry_lock_current_pid(); + now = (uint64_t) time(NULL); + + for (index = 0; index < ZEND_OPCACHE_USER_CACHE_ENTRY_LOCK_TABLE_SIZE; index++) { + record = &header->entry_lock_records[index]; + if (record->state != ZEND_OPCACHE_USER_CACHE_ENTRY_LOCK_USED) { + continue; + } + + if (!zend_opcache_user_cache_entry_lock_record_is_active_locked(record, now)) { + zend_opcache_user_cache_remove_entry_lock_record_locked(record); + continue; + } + + if (record->owner_pid != owner_pid) { + return false; + } + } + + return true; +} + +void zend_opcache_user_cache_release_active_entry_locks(void) +{ + zend_opcache_user_cache_context *context = zend_opcache_user_cache_active_context(); + + zend_opcache_user_cache_release_entry_lock_context( + context, + zend_opcache_user_cache_entry_lock_table_ptr(), + ZEND_OPCACHE_USER_CACHE_ENTRY_LOCK_RELEASE_CLEAR + ); +} + +void zend_opcache_user_cache_release_request_entry_locks(void) +{ + zend_opcache_user_cache_release_entry_lock_all_contexts( + &zend_opcache_user_cache_entry_lock_table, + ZEND_OPCACHE_USER_CACHE_ENTRY_LOCK_RELEASE_PRESERVE_LEASES + ); +#ifndef ZEND_WIN32 + zend_opcache_user_cache_entry_lock_owner_pid = 0; +#endif +} diff --git a/ext/spl/spl_array.c b/ext/spl/spl_array.c index 1976192e7b06..c9a2cdd33258 100644 --- a/ext/spl/spl_array.c +++ b/ext/spl/spl_array.c @@ -18,6 +18,7 @@ #include "php.h" #include "ext/standard/php_var.h" +#include "ext/opcache/zend_user_cache.h" #include "zend_smart_str.h" #include "zend_interfaces.h" #include "zend_exceptions.h" @@ -1506,6 +1507,205 @@ PHP_METHOD(ArrayObject, __unserialize) } /* }}} */ +static void spl_array_object_build_user_cache_state(zval *object, zval *return_value) +{ + spl_array_object *intern; + zval tmp; + + intern = Z_SPLARRAY_P(object); + array_init_size(return_value, 3); + + ZVAL_LONG(&tmp, (intern->ar_flags & SPL_ARRAY_CLONE_MASK)); + zend_hash_next_index_insert(Z_ARRVAL_P(return_value), &tmp); + + if (intern->ar_flags & SPL_ARRAY_IS_SELF) { + ZVAL_NULL(&tmp); + } else { + ZVAL_COPY(&tmp, &intern->array); + } + zend_hash_next_index_insert(Z_ARRVAL_P(return_value), &tmp); + + if (intern->ce_get_iterator == spl_ce_ArrayIterator) { + ZVAL_NULL(&tmp); + } else { + ZVAL_STR_COPY(&tmp, intern->ce_get_iterator->name); + } + zend_hash_next_index_insert(Z_ARRVAL_P(return_value), &tmp); +} + +static void spl_array_object_unserialize_user_cache_state_ex(zval *object, HashTable *data) +{ + spl_array_object *intern; + zend_long flags; + zend_class_entry *ce; + zval *flags_zv, *storage_zv, *iterator_class_zv; + + intern = Z_SPLARRAY_P(object); + flags_zv = zend_hash_index_find(data, 0); + storage_zv = zend_hash_index_find(data, 1); + iterator_class_zv = zend_hash_index_find(data, 2); + + if (!flags_zv || !storage_zv || + Z_TYPE_P(flags_zv) != IS_LONG || + (iterator_class_zv && (Z_TYPE_P(iterator_class_zv) != IS_NULL && + Z_TYPE_P(iterator_class_zv) != IS_STRING)) + ) { + zend_throw_exception(spl_ce_UnexpectedValueException, "Incomplete or ill-typed serialization data", 0); + + return; + } + + flags = Z_LVAL_P(flags_zv); + intern->ar_flags &= ~SPL_ARRAY_CLONE_MASK; + intern->ar_flags |= flags & SPL_ARRAY_CLONE_MASK; + + if (flags & SPL_ARRAY_IS_SELF) { + zval_ptr_dtor(&intern->array); + ZVAL_UNDEF(&intern->array); + } else { + if (Z_TYPE_P(storage_zv) != IS_OBJECT && Z_TYPE_P(storage_zv) != IS_ARRAY) { + zend_throw_exception(spl_ce_InvalidArgumentException, "Passed variable is not an array or object", 0); + + return; + } + + spl_array_set_array(object, intern, storage_zv, flags & SPL_ARRAY_CLONE_MASK, true); + } + + if (iterator_class_zv && Z_TYPE_P(iterator_class_zv) == IS_STRING) { + ce = zend_lookup_class(Z_STR_P(iterator_class_zv)); + + if (!ce) { + 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; + } + + 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; + } + + intern->ce_get_iterator = ce; + } +} + +static bool spl_array_object_copy_user_cache_state( + void *context, + zend_object *old_object, + zend_object *new_object, + zend_opcache_user_cache_safe_direct_clone_value_func_t clone_value) +{ + spl_array_object *old_intern, *new_intern; + zval new_zv, cloned_storage_zv; + bool result; + + result = false; + old_intern = spl_array_from_obj(old_object); + new_intern = spl_array_from_obj(new_object); + + if (clone_value == NULL) { + return false; + } + + ZVAL_OBJ(&new_zv, new_object); + ZVAL_UNDEF(&cloned_storage_zv); + + new_intern->ar_flags &= ~SPL_ARRAY_CLONE_MASK; + new_intern->ar_flags |= old_intern->ar_flags & SPL_ARRAY_CLONE_MASK; + new_intern->ce_get_iterator = old_intern->ce_get_iterator; + + if (old_intern->ar_flags & SPL_ARRAY_IS_SELF) { + zval_ptr_dtor(&new_intern->array); + ZVAL_UNDEF(&new_intern->array); + result = true; + + goto cleanup; + } + + if (!clone_value(context, &cloned_storage_zv, &old_intern->array) || + (Z_TYPE(cloned_storage_zv) != IS_OBJECT && Z_TYPE(cloned_storage_zv) != IS_ARRAY) + ) { + goto cleanup; + } + + spl_array_set_array(&new_zv, new_intern, &cloned_storage_zv, old_intern->ar_flags & SPL_ARRAY_CLONE_MASK, true); + result = !EG(exception); + +cleanup: + if (Z_TYPE(cloned_storage_zv) != IS_UNDEF) { + zval_ptr_dtor(&cloned_storage_zv); + } + + return result; +} + +static bool spl_array_object_user_cache_state_has_unstorable( + void *context, + const zval *object, + zend_opcache_user_cache_safe_direct_value_has_unstorable_func_t value_has_unstorable) +{ + spl_array_object *intern; + + if (value_has_unstorable == NULL) { + return false; + } + + intern = Z_SPLARRAY_P(object); + if (intern->ar_flags & SPL_ARRAY_IS_SELF) { + return false; + } + + return value_has_unstorable(context, &intern->array); +} + +static bool spl_array_object_serialize_user_cache_state(const zval *object, zval *state) +{ + ZVAL_UNDEF(state); + spl_array_object_build_user_cache_state((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; + } + + return true; +} + +static bool spl_array_object_unserialize_user_cache_state(zval *object, zval *state) +{ + if (Z_TYPE_P(state) != IS_ARRAY) { + return false; + } + + spl_array_object_unserialize_user_cache_state_ex(object, Z_ARRVAL_P(state)); + + return !EG(exception); +} + +static const zend_opcache_user_cache_safe_direct_handlers *spl_array_object_get_user_cache_handlers(void) +{ + static const zend_opcache_user_cache_safe_direct_handlers handlers = { + false, + spl_array_object_copy_user_cache_state, + spl_array_object_user_cache_state_has_unstorable, + spl_array_object_serialize_user_cache_state, + spl_array_object_unserialize_user_cache_state + }; + + return &handlers; +} + /* {{{ */ PHP_METHOD(ArrayObject, __debugInfo) { @@ -1853,6 +2053,8 @@ PHP_METHOD(RecursiveArrayIterator, getChildren) /* {{{ PHP_MINIT_FUNCTION(spl_array) */ PHP_MINIT_FUNCTION(spl_array) { + const zend_opcache_user_cache_safe_direct_handlers *handlers; + spl_ce_ArrayObject = register_class_ArrayObject(zend_ce_aggregate, zend_ce_arrayaccess, zend_ce_serializable, zend_ce_countable); spl_ce_ArrayObject->create_object = spl_array_object_new; spl_ce_ArrayObject->default_object_handlers = &spl_handler_ArrayObject; @@ -1888,6 +2090,12 @@ PHP_MINIT_FUNCTION(spl_array) spl_ce_RecursiveArrayIterator->create_object = spl_array_object_new; spl_ce_RecursiveArrayIterator->get_iterator = spl_array_get_iterator; + handlers = spl_array_object_get_user_cache_handlers(); + + zend_opcache_user_cache_safe_direct_register_class(spl_ce_ArrayObject, handlers); + zend_opcache_user_cache_safe_direct_register_class(spl_ce_ArrayIterator, handlers); + zend_opcache_user_cache_safe_direct_register_class(spl_ce_RecursiveArrayIterator, handlers); + return SUCCESS; } /* }}} */ diff --git a/ext/spl/spl_dllist.c b/ext/spl/spl_dllist.c index 77e8b0d4e7da..3340f17d9dd4 100644 --- a/ext/spl/spl_dllist.c +++ b/ext/spl/spl_dllist.c @@ -17,6 +17,7 @@ #endif #include "php.h" +#include "ext/opcache/zend_user_cache.h" #include "zend_interfaces.h" #include "zend_exceptions.h" #include "zend_hash.h" @@ -1101,6 +1102,161 @@ PHP_METHOD(SplDoublyLinkedList, __unserialize) { object_properties_load(&intern->std, Z_ARRVAL_P(members_zv)); } /* }}} */ +static bool spl_dllist_object_allows_user_cache_state(zend_class_entry *ce) +{ + return instanceof_function(ce, spl_ce_SplDoublyLinkedList); +} + +static bool spl_dllist_object_copy_user_cache_state( + void *context, + zend_object *old_object, + zend_object *new_object, + zend_opcache_user_cache_safe_direct_clone_value_func_t clone_value) +{ + spl_dllist_object *old_intern, *new_intern; + spl_ptr_llist_element *current; + zval cloned_elem; + + if (clone_value == NULL || + !spl_dllist_object_allows_user_cache_state(old_object->ce) || + !spl_dllist_object_allows_user_cache_state(new_object->ce) + ) { + return false; + } + + old_intern = spl_dllist_from_obj(old_object); + new_intern = spl_dllist_from_obj(new_object); + if (new_intern->llist->count != 0) { + return false; + } + + new_intern->flags = old_intern->flags; + current = old_intern->llist->head; + while (current) { + ZVAL_UNDEF(&cloned_elem); + if (!clone_value(context, &cloned_elem, ¤t->data)) { + return false; + } + + spl_ptr_llist_push(new_intern->llist, &cloned_elem); + zval_ptr_dtor(&cloned_elem); + current = current->next; + } + + return !EG(exception); +} + +static bool spl_dllist_object_user_cache_state_has_unstorable( + void *context, + const zval *object, + zend_opcache_user_cache_safe_direct_value_has_unstorable_func_t value_has_unstorable) +{ + spl_dllist_object *intern; + spl_ptr_llist_element *current; + + if (value_has_unstorable == NULL) { + return false; + } + + if (!spl_dllist_object_allows_user_cache_state(Z_OBJCE_P(object))) { + return true; + } + + intern = Z_SPLDLLIST_P((zval *) object); + current = intern->llist->head; + while (current) { + if (value_has_unstorable(context, ¤t->data)) { + return true; + } + + current = current->next; + } + + return false; +} + +static bool spl_dllist_object_serialize_user_cache_state(const zval *object, zval *state) +{ + spl_dllist_object *intern; + spl_ptr_llist_element *current; + zval tmp; + + if (!spl_dllist_object_allows_user_cache_state(Z_OBJCE_P(object))) { + return false; + } + + intern = Z_SPLDLLIST_P((zval *) object); + current = intern->llist->head; + + array_init(state); + + ZVAL_LONG(&tmp, intern->flags); + zend_hash_next_index_insert(Z_ARRVAL_P(state), &tmp); + + array_init_size(&tmp, intern->llist->count); + while (current) { + zend_hash_next_index_insert(Z_ARRVAL(tmp), ¤t->data); + Z_TRY_ADDREF(current->data); + current = current->next; + } + zend_hash_next_index_insert(Z_ARRVAL_P(state), &tmp); + + return true; +} + +static bool spl_dllist_object_unserialize_user_cache_state(zval *object, zval *state) +{ + spl_dllist_object *intern; + zend_long flags; + zval *flags_zv, *storage_zv, *elem; + HashTable *data; + + if (!spl_dllist_object_allows_user_cache_state(Z_OBJCE_P(object)) || + Z_TYPE_P(state) != IS_ARRAY + ) { + return false; + } + + data = Z_ARRVAL_P(state); + flags_zv = zend_hash_index_find(data, 0); + storage_zv = zend_hash_index_find(data, 1); + if (!flags_zv || !storage_zv || + Z_TYPE_P(flags_zv) != IS_LONG || Z_TYPE_P(storage_zv) != IS_ARRAY + ) { + return false; + } + + flags = Z_LVAL_P(flags_zv); + if ((flags & ~(SPL_DLLIST_IT_MASK | SPL_DLLIST_IT_FIX)) != 0) { + return false; + } + + intern = Z_SPLDLLIST_P(object); + if (intern->llist->count != 0) { + return false; + } + + intern->flags = (int) flags; + ZEND_HASH_FOREACH_VAL(Z_ARRVAL_P(storage_zv), elem) { + spl_ptr_llist_push(intern->llist, elem); + } ZEND_HASH_FOREACH_END(); + + return !EG(exception); +} + +static const zend_opcache_user_cache_safe_direct_handlers *spl_dllist_object_get_user_cache_handlers(void) +{ + static const zend_opcache_user_cache_safe_direct_handlers handlers = { + false, + spl_dllist_object_copy_user_cache_state, + spl_dllist_object_user_cache_state_has_unstorable, + spl_dllist_object_serialize_user_cache_state, + spl_dllist_object_unserialize_user_cache_state + }; + + return &handlers; +} + /* {{{ Inserts a new entry before the specified $index consisting of $newval. */ PHP_METHOD(SplDoublyLinkedList, add) { @@ -1219,6 +1375,21 @@ PHP_MINIT_FUNCTION(spl_dllist) /* {{{ */ spl_ce_SplStack->create_object = spl_dllist_object_new; spl_ce_SplStack->get_iterator = spl_dllist_get_iterator; + zend_opcache_user_cache_safe_direct_register_class( + spl_ce_SplDoublyLinkedList, + spl_dllist_object_get_user_cache_handlers() + ); + + zend_opcache_user_cache_safe_direct_register_class( + spl_ce_SplQueue, + spl_dllist_object_get_user_cache_handlers() + ); + + zend_opcache_user_cache_safe_direct_register_class( + spl_ce_SplStack, + spl_dllist_object_get_user_cache_handlers() + ); + return SUCCESS; } /* }}} */ diff --git a/ext/spl/spl_fixedarray.c b/ext/spl/spl_fixedarray.c index d2eae52e3c0c..90c8ba763201 100644 --- a/ext/spl/spl_fixedarray.c +++ b/ext/spl/spl_fixedarray.c @@ -26,6 +26,7 @@ #include "spl_fixedarray.h" #include "spl_exceptions.h" #include "ext/json/php_json.h" /* For php_json_serializable_ce */ +#include "ext/opcache/zend_user_cache.h" static zend_object_handlers spl_handler_SplFixedArray; PHPAPI zend_class_entry *spl_ce_SplFixedArray; @@ -576,20 +577,21 @@ PHP_METHOD(SplFixedArray, __wakeup) } } -PHP_METHOD(SplFixedArray, __serialize) +static void spl_fixedarray_object_serialize(zval *object, zval *return_value) { - spl_fixedarray_object *intern = Z_SPLFIXEDARRAY_P(ZEND_THIS); + spl_fixedarray_object *intern = Z_SPLFIXEDARRAY_P(object); + HashTable *ht; + uint32_t num_properties; zval *current; zend_string *key; + zend_long i; - ZEND_PARSE_PARAMETERS_NONE(); - - HashTable *ht = zend_std_get_properties(&intern->std); - uint32_t num_properties = zend_hash_num_elements(ht); + ht = zend_std_get_properties(&intern->std); + num_properties = zend_hash_num_elements(ht); array_init_size(return_value, intern->array.size + num_properties); /* elements */ - for (zend_long i = 0; i < intern->array.size; i++) { + for (i = 0; i < intern->array.size; i++) { current = &intern->array.elements[i]; zend_hash_next_index_insert(Z_ARRVAL_P(return_value), current); Z_TRY_ADDREF_P(current); @@ -607,18 +609,13 @@ PHP_METHOD(SplFixedArray, __serialize) } ZEND_HASH_FOREACH_END(); } -PHP_METHOD(SplFixedArray, __unserialize) +static void spl_fixedarray_object_unserialize(zval *object, HashTable *data) { - spl_fixedarray_object *intern = Z_SPLFIXEDARRAY_P(ZEND_THIS); - HashTable *data; + spl_fixedarray_object *intern = Z_SPLFIXEDARRAY_P(object); zval members_zv, *elem; zend_string *key; zend_long size; - if (zend_parse_parameters(ZEND_NUM_ARGS(), "h", &data) == FAILURE) { - RETURN_THROWS(); - } - if (intern->array.size == 0) { size = zend_hash_num_elements(data); spl_fixedarray_init_non_empty_struct(&intern->array, size); @@ -652,6 +649,164 @@ PHP_METHOD(SplFixedArray, __unserialize) } } +PHP_METHOD(SplFixedArray, __serialize) +{ + ZEND_PARSE_PARAMETERS_NONE(); + + spl_fixedarray_object_serialize(ZEND_THIS, return_value); +} + +PHP_METHOD(SplFixedArray, __unserialize) +{ + HashTable *data; + + 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 bool spl_fixedarray_object_copy_user_cache_state( + void *context, + zend_object *old_object, + zend_object *new_object, + zend_opcache_user_cache_safe_direct_clone_value_func_t clone_value) +{ + zval old_zv, new_zv, state_zv, cloned_state_zv, elements_zv, copied_elem, *elem; + zend_string *key; + bool result; + + if (clone_value == NULL) { + return false; + } + + result = 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_user_cache_state_has_unstorable( + void *context, + const zval *object, + zend_opcache_user_cache_safe_direct_value_has_unstorable_func_t value_has_unstorable) +{ + zval state_zv; + bool result; + + if (value_has_unstorable == NULL) { + return false; + } + + result = 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_user_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_user_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); +} + +static const zend_opcache_user_cache_safe_direct_handlers *spl_fixedarray_object_get_user_cache_handlers(void) +{ + static const zend_opcache_user_cache_safe_direct_handlers handlers = { + false, + spl_fixedarray_object_copy_user_cache_state, + spl_fixedarray_object_user_cache_state_has_unstorable, + spl_fixedarray_object_serialize_user_cache_state, + spl_fixedarray_object_unserialize_user_cache_state + }; + + return &handlers; +} + PHP_METHOD(SplFixedArray, count) { zval *object = ZEND_THIS; @@ -960,5 +1115,10 @@ PHP_MINIT_FUNCTION(spl_fixedarray) spl_handler_SplFixedArray.get_gc = spl_fixedarray_object_get_gc; spl_handler_SplFixedArray.free_obj = spl_fixedarray_object_free_storage; + zend_opcache_user_cache_safe_direct_register_class( + spl_ce_SplFixedArray, + spl_fixedarray_object_get_user_cache_handlers() + ); + return SUCCESS; } diff --git a/ext/spl/spl_heap.c b/ext/spl/spl_heap.c index 1073836aa53b..c25e86caa09e 100644 --- a/ext/spl/spl_heap.c +++ b/ext/spl/spl_heap.c @@ -24,6 +24,7 @@ #include "spl_heap_arginfo.h" #include "spl_exceptions.h" #include "spl_functions.h" /* For spl_set_private_debug_info_property() */ +#include "ext/opcache/zend_user_cache.h" #define PTR_HEAP_BLOCK_SIZE 64 @@ -1259,6 +1260,291 @@ PHP_METHOD(SplHeap, __unserialize) } } +static bool spl_heap_object_allows_user_cache_state(zend_class_entry *ce) +{ + return instanceof_function(ce, spl_ce_SplHeap) || + instanceof_function(ce, spl_ce_SplPriorityQueue) + ; +} + +static bool spl_heap_object_user_cache_is_pqueue(zend_class_entry *ce) +{ + return instanceof_function(ce, spl_ce_SplPriorityQueue); +} + +static void spl_ptr_heap_user_cache_ensure_capacity(spl_ptr_heap *heap, size_t count) +{ + size_t alloc_size; + + while (count > heap->max_size) { + alloc_size = heap->max_size * heap->elem_size; + heap->elements = safe_erealloc(heap->elements, 2, alloc_size, 0); + memset((char *) heap->elements + alloc_size, 0, alloc_size); + + heap->max_size *= 2; + } +} + +static void spl_heap_object_user_cache_append_zval(spl_heap_object *intern, zval *value) +{ + zval *target; + + spl_ptr_heap_user_cache_ensure_capacity(intern->heap, intern->heap->count + 1); + target = spl_heap_elem(intern->heap, intern->heap->count); + + intern->heap->count++; + + ZVAL_COPY(target, value); +} + +static void spl_heap_object_user_cache_append_pqueue_elem(spl_heap_object *intern, zval *data, zval *priority) +{ + spl_pqueue_elem *target; + + spl_ptr_heap_user_cache_ensure_capacity(intern->heap, intern->heap->count + 1); + target = spl_heap_elem(intern->heap, intern->heap->count); + + intern->heap->count++; + + ZVAL_COPY(&target->data, data); + ZVAL_COPY(&target->priority, priority); +} + +static bool spl_heap_object_copy_user_cache_state( + void *context, + zend_object *old_object, + zend_object *new_object, + zend_opcache_user_cache_safe_direct_clone_value_func_t clone_value) +{ + spl_heap_object *old_intern, *new_intern; + spl_pqueue_elem *old_elem; + zval cloned_data, cloned_priority, *zv_old_elem, cloned_elem; + size_t i; + bool is_pqueue; + + if (clone_value == NULL || + !spl_heap_object_allows_user_cache_state(old_object->ce) || + !spl_heap_object_allows_user_cache_state(new_object->ce) + ) { + return false; + } + + old_intern = spl_heap_from_obj(old_object); + new_intern = spl_heap_from_obj(new_object); + if (new_intern->heap->count != 0 || + (old_intern->heap->flags & (SPL_HEAP_CORRUPTED | SPL_HEAP_WRITE_LOCKED)) != 0 + ) { + return false; + } + + new_intern->flags = old_intern->flags; + is_pqueue = spl_heap_object_user_cache_is_pqueue(old_object->ce); + for (i = 0; i < old_intern->heap->count; i++) { + if (is_pqueue) { + old_elem = spl_heap_elem(old_intern->heap, i); + + ZVAL_UNDEF(&cloned_data); + ZVAL_UNDEF(&cloned_priority); + if (!clone_value(context, &cloned_data, &old_elem->data)) { + return false; + } + if (!clone_value(context, &cloned_priority, &old_elem->priority)) { + zval_ptr_dtor(&cloned_data); + + return false; + } + + spl_heap_object_user_cache_append_pqueue_elem(new_intern, &cloned_data, &cloned_priority); + + zval_ptr_dtor(&cloned_data); + zval_ptr_dtor(&cloned_priority); + } else { + zv_old_elem = spl_heap_elem(old_intern->heap, i); + ZVAL_UNDEF(&cloned_elem); + if (!clone_value(context, &cloned_elem, zv_old_elem)) { + return false; + } + + spl_heap_object_user_cache_append_zval(new_intern, &cloned_elem); + + zval_ptr_dtor(&cloned_elem); + } + } + + return !EG(exception); +} + +static bool spl_heap_object_user_cache_state_has_unstorable( + void *context, + const zval *object, + zend_opcache_user_cache_safe_direct_value_has_unstorable_func_t value_has_unstorable) +{ + spl_heap_object *intern; + spl_pqueue_elem *elem; + zval *zv_elem; + size_t i; + bool is_pqueue; + + if (value_has_unstorable == NULL) { + return false; + } + + if (!spl_heap_object_allows_user_cache_state(Z_OBJCE_P(object))) { + return true; + } + + intern = Z_SPLHEAP_P((zval *) object); + if ((intern->heap->flags & (SPL_HEAP_CORRUPTED | SPL_HEAP_WRITE_LOCKED)) != 0) { + return true; + } + + is_pqueue = spl_heap_object_user_cache_is_pqueue(Z_OBJCE_P(object)); + for (i = 0; i < intern->heap->count; i++) { + if (is_pqueue) { + elem = spl_heap_elem(intern->heap, i); + + if (value_has_unstorable(context, &elem->data) || + value_has_unstorable(context, &elem->priority)) { + return true; + } + } else { + zv_elem = spl_heap_elem(intern->heap, i); + + if (value_has_unstorable(context, zv_elem)) { + return true; + } + } + } + + return false; +} + +static bool spl_heap_object_serialize_user_cache_state(const zval *object, zval *state) +{ + spl_heap_object *intern; + spl_pqueue_elem *pqueue_elem; + zval *heap_elem; + zval elements, entry, tmp; + size_t i; + bool is_pqueue; + + if (!spl_heap_object_allows_user_cache_state(Z_OBJCE_P(object))) { + return false; + } + + intern = Z_SPLHEAP_P((zval *) object); + if ((intern->heap->flags & (SPL_HEAP_CORRUPTED | SPL_HEAP_WRITE_LOCKED)) != 0) { + return false; + } + + is_pqueue = spl_heap_object_user_cache_is_pqueue(Z_OBJCE_P(object)); + + array_init(state); + + ZVAL_LONG(&tmp, intern->flags); + zend_hash_next_index_insert(Z_ARRVAL_P(state), &tmp); + + array_init_size(&elements, intern->heap->count); + for (i = 0; i < intern->heap->count; i++) { + if (is_pqueue) { + pqueue_elem = spl_heap_elem(intern->heap, i); + + array_init_size(&entry, 2); + Z_TRY_ADDREF(pqueue_elem->data); + zend_hash_index_add_new(Z_ARRVAL(entry), 0, &pqueue_elem->data); + Z_TRY_ADDREF(pqueue_elem->priority); + zend_hash_index_add_new(Z_ARRVAL(entry), 1, &pqueue_elem->priority); + zend_hash_next_index_insert(Z_ARRVAL(elements), &entry); + } else { + heap_elem = spl_heap_elem(intern->heap, i); + + zend_hash_next_index_insert(Z_ARRVAL(elements), heap_elem); + Z_TRY_ADDREF_P(heap_elem); + } + } + + zend_hash_next_index_insert(Z_ARRVAL_P(state), &elements); + + return true; +} + +static bool spl_heap_object_unserialize_user_cache_state(zval *object, zval *state) +{ + spl_heap_object *intern; + zend_long flags; + zval *flags_zv, *elements_zv, *elem, *data_zv, *priority_zv; + HashTable *data; + bool is_pqueue; + + if (!spl_heap_object_allows_user_cache_state(Z_OBJCE_P(object)) || + Z_TYPE_P(state) != IS_ARRAY + ) { + return false; + } + + data = Z_ARRVAL_P(state); + flags_zv = zend_hash_index_find(data, 0); + elements_zv = zend_hash_index_find(data, 1); + if (!flags_zv || !elements_zv || + Z_TYPE_P(flags_zv) != IS_LONG || Z_TYPE_P(elements_zv) != IS_ARRAY + ) { + return false; + } + + is_pqueue = spl_heap_object_user_cache_is_pqueue(Z_OBJCE_P(object)); + flags = Z_LVAL_P(flags_zv); + if (is_pqueue) { + flags &= SPL_PQUEUE_EXTR_MASK; + if (!flags) { + return false; + } + } else if (flags != 0) { + return false; + } + + intern = Z_SPLHEAP_P(object); + if (intern->heap->count != 0 || + (intern->heap->flags & SPL_HEAP_WRITE_LOCKED) != 0 + ) { + return false; + } + + intern->flags = (int) flags; + + ZEND_HASH_FOREACH_VAL(Z_ARRVAL_P(elements_zv), elem) { + if (is_pqueue) { + if (Z_TYPE_P(elem) != IS_ARRAY) { + return false; + } + + data_zv = zend_hash_index_find(Z_ARRVAL_P(elem), 0); + priority_zv = zend_hash_index_find(Z_ARRVAL_P(elem), 1); + if (!data_zv || !priority_zv) { + return false; + } + + spl_heap_object_user_cache_append_pqueue_elem(intern, data_zv, priority_zv); + } else { + spl_heap_object_user_cache_append_zval(intern, elem); + } + } ZEND_HASH_FOREACH_END(); + + return !EG(exception); +} + +static const zend_opcache_user_cache_safe_direct_handlers *spl_heap_object_get_user_cache_handlers(void) +{ + static const zend_opcache_user_cache_safe_direct_handlers handlers = { + false, + spl_heap_object_copy_user_cache_state, + spl_heap_object_user_cache_state_has_unstorable, + spl_heap_object_serialize_user_cache_state, + spl_heap_object_unserialize_user_cache_state + }; + + return &handlers; +} + /* iterator handler table */ static const zend_object_iterator_funcs spl_heap_it_funcs = { spl_heap_it_dtor, @@ -1356,6 +1642,26 @@ PHP_MINIT_FUNCTION(spl_heap) /* {{{ */ spl_handler_SplPriorityQueue.get_gc = spl_pqueue_object_get_gc; spl_handler_SplPriorityQueue.free_obj = spl_heap_object_free_storage; + zend_opcache_user_cache_safe_direct_register_class( + spl_ce_SplHeap, + spl_heap_object_get_user_cache_handlers() + ); + + zend_opcache_user_cache_safe_direct_register_class( + spl_ce_SplMinHeap, + spl_heap_object_get_user_cache_handlers() + ); + + zend_opcache_user_cache_safe_direct_register_class( + spl_ce_SplMaxHeap, + spl_heap_object_get_user_cache_handlers() + ); + + zend_opcache_user_cache_safe_direct_register_class( + spl_ce_SplPriorityQueue, + spl_heap_object_get_user_cache_handlers() + ); + return SUCCESS; } /* }}} */ diff --git a/sapi/apache2handler/sapi_apache2.c b/sapi/apache2handler/sapi_apache2.c index 83b3f02fb743..dc5e8ec1e109 100644 --- a/sapi/apache2handler/sapi_apache2.c +++ b/sapi/apache2handler/sapi_apache2.c @@ -32,6 +32,7 @@ #include "zend_smart_str.h" #include "ext/standard/php_standard.h" +#include "ext/opcache/zend_user_cache.h" #include "apr_strings.h" #include "ap_config.h" @@ -64,6 +65,14 @@ char *apache2_php_ini_path_override = NULL; ZEND_TSRMLS_CACHE_DEFINE() #endif +typedef struct _php_apache_user_cache_partition_entry { + const server_rec *server; + zend_opcache_user_cache_partition *partition; + struct _php_apache_user_cache_partition_entry *next; +} php_apache_user_cache_partition_entry; + +static php_apache_user_cache_partition_entry *php_apache_user_cache_partitions = NULL; + static size_t php_apache_sapi_ub_write(const char *str, size_t str_length) { @@ -418,6 +427,7 @@ static sapi_module_struct apache2_sapi_module = { static apr_status_t php_apache_server_shutdown(void *tmp) { apache2_sapi_module.shutdown(&apache2_sapi_module); + php_apache_user_cache_partitions = NULL; sapi_shutdown(); #ifdef ZTS tsrm_shutdown(); @@ -458,6 +468,62 @@ static int php_pre_config(apr_pool_t *pconf, apr_pool_t *plog, apr_pool_t *ptemp return OK; } +static zend_opcache_user_cache_partition *php_apache_user_cache_partition_for_server(const server_rec *server) +{ + php_apache_user_cache_partition_entry *entry; + + for (entry = php_apache_user_cache_partitions; entry != NULL; entry = entry->next) { + if (entry->server == server) { + return entry->partition; + } + } + + return NULL; +} + +static void php_apache_user_cache_init_partitions(apr_pool_t *pconf, server_rec *server) +{ + const char *hostname, *partition_name; + php_apache_user_cache_partition_entry *entry; + server_rec *current; + unsigned int index; + + php_apache_user_cache_partitions = NULL; + zend_opcache_user_cache_opt_in(); + + index = 0; + for (current = server; current != NULL; current = current->next, index++) { + hostname = current->server_hostname != NULL ? current->server_hostname : "default"; + partition_name = apr_psprintf( + pconf, + "apache2handler:%u:%s:%u", + index, + hostname, + (unsigned int) current->port + ); + + entry = apr_pcalloc(pconf, sizeof(*entry)); + if (entry == NULL) { + ap_log_error(APLOG_MARK, APLOG_WARNING, 0, current, "Unable to allocate OPcache User Cache partition entry"); + continue; + } + + entry->server = current; + entry->partition = zend_opcache_user_cache_partition_create(partition_name); + if (entry->partition == NULL) { + ap_log_error(APLOG_MARK, APLOG_WARNING, 0, current, "Unable to allocate OPcache User Cache partition"); + continue; + } + + if (!zend_opcache_user_cache_partition_startup_storage(entry->partition)) { + ap_log_error(APLOG_MARK, APLOG_WARNING, 0, current, "OPcache User Cache partition startup failed; User Cache will be unavailable"); + } + + entry->next = php_apache_user_cache_partitions; + php_apache_user_cache_partitions = entry; + } +} + static int php_apache_server_startup(apr_pool_t *pconf, apr_pool_t *plog, apr_pool_t *ptemp, server_rec *s) { @@ -500,6 +566,7 @@ php_apache_server_startup(apr_pool_t *pconf, apr_pool_t *plog, apr_pool_t *ptemp if (apache2_sapi_module.startup(&apache2_sapi_module) != SUCCESS) { return DONE; } + php_apache_user_cache_init_partitions(pconf, s); apr_pool_cleanup_register(pconf, NULL, php_apache_server_shutdown, apr_pool_cleanup_null); php_apache_add_version(pconf); @@ -548,12 +615,21 @@ static int php_apache_request_ctor(request_rec *r, php_struct *ctx) ctx->r->user = apr_pstrdup(ctx->r->pool, SG(request_info).auth_user); - return php_request_startup(); + zend_opcache_user_cache_partition_activate(php_apache_user_cache_partition_for_server(r->server)); + + if (php_request_startup() == FAILURE) { + zend_opcache_user_cache_partition_activate(NULL); + + return FAILURE; + } + + return SUCCESS; } static void php_apache_request_dtor(request_rec *r) { php_request_shutdown(NULL); + zend_opcache_user_cache_partition_activate(NULL); } static void php_apache_ini_dtor(request_rec *r, request_rec *p) diff --git a/sapi/cgi/cgi_main.c b/sapi/cgi/cgi_main.c index f49507827e2d..b64fcc6bbc38 100644 --- a/sapi/cgi/cgi_main.c +++ b/sapi/cgi/cgi_main.c @@ -63,6 +63,7 @@ #include "ext/standard/php_standard.h" #include "ext/standard/dl_arginfo.h" #include "ext/standard/url.h" +#include "ext/opcache/zend_user_cache.h" #ifdef PHP_WIN32 # include @@ -131,10 +132,23 @@ static pid_t pgroup; #define PHP_MODE_HIGHLIGHT 2 #define PHP_MODE_LINT 4 #define PHP_MODE_STRIP 5 +#define PHP_CGI_USER_CACHE_MAX_BOUNDARY_PARTITIONS 32 + +typedef struct _cgi_user_cache_boundary_partition { + char *boundary; + size_t boundary_len; + zend_ulong boundary_hash; + zend_opcache_user_cache_partition *partition; + struct _cgi_user_cache_boundary_partition *next; +} cgi_user_cache_boundary_partition; static char *php_optarg = NULL; static int php_optind = 1; static zend_module_entry cgi_module_entry; +static cgi_user_cache_boundary_partition *cgi_user_cache_partitions = NULL; +static uint32_t cgi_user_cache_partition_count = 0; +static bool cgi_user_cache_partition_limit_logged = false; +static bool cgi_user_cache_partition_startup_failed_logged = false; static const opt_struct OPTIONS[] = { {'a', 0, "interactive"}, @@ -776,6 +790,228 @@ static void sapi_cgi_log_message(const char *message, int syslog_type_int) } } +static const char *cgi_user_cache_get_boundary_value(const char *name) +{ + const char *value; + fcgi_request *request; + int name_len; + + if (SG(server_context) != NULL && SG(server_context) != (void *) 1) { + request = (fcgi_request*) SG(server_context); + if (fcgi_has_env(request)) { + name_len = (int) strlen(name); + value = fcgi_getenv(request, name, name_len); + if (value != NULL) { + return value; + } + } + } + + return getenv(name); +} + +static cgi_user_cache_boundary_partition *cgi_user_cache_find_boundary_partition( + const char *boundary, + size_t boundary_len, + zend_ulong boundary_hash) +{ + cgi_user_cache_boundary_partition *entry; + + for (entry = cgi_user_cache_partitions; entry != NULL; entry = entry->next) { + if (entry->boundary_hash == boundary_hash && + entry->boundary_len == boundary_len && + memcmp(entry->boundary, boundary, boundary_len) == 0 + ) { + return entry; + } + } + + return NULL; +} + +static cgi_user_cache_boundary_partition *cgi_user_cache_create_boundary_partition( + const char *boundary, + size_t boundary_len, + zend_ulong boundary_hash) +{ + cgi_user_cache_boundary_partition *entry; + char partition_name[128]; + + if (cgi_user_cache_partition_count >= PHP_CGI_USER_CACHE_MAX_BOUNDARY_PARTITIONS) { + if (!cgi_user_cache_partition_limit_logged) { + sapi_cgi_log_message("OPcache User Cache disabled for this request because the CGI/FastCGI cache boundary partition limit was reached", 0); + cgi_user_cache_partition_limit_logged = true; + } + + return NULL; + } + + entry = calloc(1, sizeof(*entry)); + if (entry == NULL) { + return NULL; + } + + entry->boundary = malloc(boundary_len + 1); + if (entry->boundary == NULL) { + free(entry); + + return NULL; + } + + memcpy(entry->boundary, boundary, boundary_len); + entry->boundary[boundary_len] = '\0'; + entry->boundary_len = boundary_len; + entry->boundary_hash = boundary_hash; + + snprintf( + partition_name, + sizeof(partition_name), + "cgi-fcgi:%ld:%u:" ZEND_ULONG_FMT, + (long) getpid(), + cgi_user_cache_partition_count, + boundary_hash + ); + + entry->partition = zend_opcache_user_cache_partition_create(partition_name); + if (entry->partition == NULL) { + free(entry->boundary); + free(entry); + + return NULL; + } + + entry->next = cgi_user_cache_partitions; + cgi_user_cache_partitions = entry; + cgi_user_cache_partition_count++; + + return entry; +} + +static char *cgi_user_cache_build_boundary_key(const char *boundary, size_t *boundary_key_len) +{ + char *boundary_key; + size_t boundary_len, prefix_len = 0; +#if !defined(PHP_WIN32) && defined(HAVE_UNISTD_H) + char prefix[64]; + int prefix_result; +#endif + + boundary_len = strlen(boundary); + +#if !defined(PHP_WIN32) && defined(HAVE_UNISTD_H) + prefix_result = snprintf( + prefix, + sizeof(prefix), + "uid:%ld:gid:%ld:", + (long) geteuid(), + (long) getegid() + ); + if (prefix_result < 0 || (size_t) prefix_result >= sizeof(prefix)) { + return NULL; + } + prefix_len = (size_t) prefix_result; +#endif + + if (boundary_len > SIZE_MAX - prefix_len - 1) { + return NULL; + } + + boundary_key = malloc(prefix_len + boundary_len + 1); + if (boundary_key == NULL) { + return NULL; + } + +#if !defined(PHP_WIN32) && defined(HAVE_UNISTD_H) + memcpy(boundary_key, prefix, prefix_len); +#endif + memcpy(boundary_key + prefix_len, boundary, boundary_len + 1); + *boundary_key_len = prefix_len + boundary_len; + + return boundary_key; +} + +static zend_opcache_user_cache_partition *cgi_user_cache_get_partition_for_boundary(const char *boundary) +{ + cgi_user_cache_boundary_partition *entry; + char *boundary_key; + size_t boundary_len; + zend_ulong boundary_hash; + + boundary_key = cgi_user_cache_build_boundary_key(boundary, &boundary_len); + if (boundary_key == NULL) { + return NULL; + } + + boundary_hash = zend_inline_hash_func(boundary_key, boundary_len); + entry = cgi_user_cache_find_boundary_partition(boundary_key, boundary_len, boundary_hash); + if (entry == NULL) { + entry = cgi_user_cache_create_boundary_partition(boundary_key, boundary_len, boundary_hash); + if (entry == NULL) { + free(boundary_key); + + return NULL; + } + } + + free(boundary_key); + + if (!zend_opcache_user_cache_partition_startup_storage(entry->partition) && + !cgi_user_cache_partition_startup_failed_logged + ) { + sapi_cgi_log_message("OPcache User Cache partition startup failed; User Cache will be unavailable", 0); + cgi_user_cache_partition_startup_failed_logged = true; + } + + return entry->partition; +} + +static zend_opcache_user_cache_partition *cgi_user_cache_get_request_partition(void) +{ + const char *boundary; + + boundary = cgi_user_cache_get_boundary_value("DOCUMENT_ROOT"); + if (boundary == NULL || boundary[0] == '\0') { + boundary = cgi_user_cache_get_boundary_value("SERVER_NAME"); + } + if (boundary == NULL || boundary[0] == '\0') { + return NULL; + } + + return cgi_user_cache_get_partition_for_boundary(boundary); +} + +static void cgi_user_cache_activate_request_partition(void) +{ + zend_opcache_user_cache_partition *partition; + + partition = cgi_user_cache_get_request_partition(); + if (partition == NULL) { + zend_opcache_user_cache_activate_request_unavailable("CGI/FastCGI cache boundary is not available"); + + return; + } + + zend_opcache_user_cache_partition_activate(partition); +} + +static void cgi_user_cache_shutdown_boundary_partitions(void) +{ + cgi_user_cache_boundary_partition *entry, *next; + + entry = cgi_user_cache_partitions; + while (entry != NULL) { + next = entry->next; + free(entry->boundary); + free(entry); + entry = next; + } + + cgi_user_cache_partitions = NULL; + cgi_user_cache_partition_count = 0; + cgi_user_cache_partition_limit_logged = false; + cgi_user_cache_partition_startup_failed_logged = false; +} + /* {{{ php_cgi_ini_activate_user_config */ static void php_cgi_ini_activate_user_config(char *path, size_t path_len, const char *doc_root, size_t doc_root_len) { @@ -941,6 +1177,8 @@ static int sapi_cgi_activate(void) efree(path); } + cgi_user_cache_activate_request_partition(); + return SUCCESS; } @@ -961,12 +1199,20 @@ static int sapi_cgi_deactivate(void) sapi_cgi_flush(SG(server_context)); } } + zend_opcache_user_cache_partition_activate(NULL); + return SUCCESS; } static int php_cgi_startup(sapi_module_struct *sapi_module_ptr) { - return php_module_startup(sapi_module_ptr, &cgi_module_entry); + if (php_module_startup(sapi_module_ptr, &cgi_module_entry) == FAILURE) { + return FAILURE; + } + + zend_opcache_user_cache_opt_in(); + + return SUCCESS; } /* {{{ sapi_module_struct cgi_sapi_module */ @@ -1545,6 +1791,7 @@ static PHP_MINIT_FUNCTION(cgi) static PHP_MSHUTDOWN_FUNCTION(cgi) { zend_hash_destroy(&CGIG(user_config_cache)); + cgi_user_cache_shutdown_boundary_partitions(); UNREGISTER_INI_ENTRIES(); return SUCCESS; @@ -2285,6 +2532,7 @@ consult the installation file that came with this distribution, or visit \n\ while (!fastcgi || fcgi_accept_request(request) >= 0) { SG(server_context) = fastcgi ? (void *)request : (void *) 1; init_request_info(request); + cgi_user_cache_activate_request_partition(); if (!cgi && !fastcgi) { while ((c = php_getopt(argc, argv, OPTIONS, &php_optarg, &php_optind, 0, 2)) != -1) { diff --git a/sapi/cli/php_cli.c b/sapi/cli/php_cli.c index 9a05f8e68547..364b30c97745 100644 --- a/sapi/cli/php_cli.c +++ b/sapi/cli/php_cli.c @@ -52,6 +52,9 @@ #include "fopen_wrappers.h" #include "ext/standard/php_standard.h" #include "ext/standard/dl_arginfo.h" + +#include "ext/opcache/zend_user_cache.h" + #include "cli.h" #ifdef PHP_WIN32 #include @@ -390,7 +393,14 @@ static void sapi_cli_send_header(sapi_header_struct *sapi_header, void *server_c static int php_cli_startup(sapi_module_struct *sapi_module_ptr) /* {{{ */ { - return php_module_startup(sapi_module_ptr, NULL); + int result; + + result = php_module_startup(sapi_module_ptr, NULL); + if (result == SUCCESS) { + zend_opcache_user_cache_opt_in(); + } + + return result; } /* }}} */ diff --git a/sapi/cli/php_cli_server.c b/sapi/cli/php_cli_server.c index 797979b67305..a19b6588fc8b 100644 --- a/sapi/cli/php_cli_server.c +++ b/sapi/cli/php_cli_server.c @@ -44,6 +44,8 @@ #include +#include "ext/opcache/zend_user_cache.h" + #ifdef HAVE_DLFCN_H #include #endif @@ -499,7 +501,14 @@ const zend_function_entry server_additional_functions[] = { static int sapi_cli_server_startup(sapi_module_struct *sapi_module_ptr) /* {{{ */ { - return php_module_startup(sapi_module_ptr, &cli_server_module_entry); + int result; + + result = php_module_startup(sapi_module_ptr, &cli_server_module_entry); + if (result == SUCCESS) { + zend_opcache_user_cache_opt_in(); + } + + return result; } /* }}} */ static size_t sapi_cli_server_ub_write(const char *str, size_t str_length) /* {{{ */ @@ -2571,6 +2580,10 @@ static zend_result php_cli_server_ctor(php_cli_server *server, const char *addr, } server->server_sock = server_sock; + if (!zend_opcache_user_cache_startup_default_context_storage()) { + php_cli_server_logf(PHP_CLI_SERVER_LOG_ERROR, "OPcache User Cache startup failed; User Cache will be unavailable"); + } + php_cli_server_startup_workers(); php_cli_server_poller_ctor(&server->poller); diff --git a/sapi/fpm/fpm/fpm.c b/sapi/fpm/fpm/fpm.c index cb86f403b89c..ddc21883e66e 100644 --- a/sapi/fpm/fpm/fpm.c +++ b/sapi/fpm/fpm/fpm.c @@ -21,6 +21,8 @@ #include "fpm_log.h" #include "zlog.h" +#include "ext/opcache/zend_user_cache.h" + struct fpm_globals_s fpm_globals = { .parent_pid = 0, .argc = 0, @@ -41,6 +43,27 @@ struct fpm_globals_s fpm_globals = { .send_config_pipe = {0, 0}, }; +static int fpm_user_cache_init_main(void) +{ + struct fpm_worker_pool_s *wp; + + zend_opcache_user_cache_opt_in(); + + for (wp = fpm_worker_all_pools; wp; wp = wp->next) { + wp->user_cache_partition = zend_opcache_user_cache_partition_create(wp->config->name); + if (wp->user_cache_partition == NULL) { + zlog(ZLOG_ERROR, "[pool %s] unable to allocate OPcache User Cache partition", wp->config->name); + return -1; + } + + if (!zend_opcache_user_cache_partition_startup_storage(wp->user_cache_partition)) { + zlog(ZLOG_WARNING, "[pool %s] OPcache User Cache partition startup failed; User Cache will be unavailable", wp->config->name); + } + } + + return 0; +} + enum fpm_init_return_status fpm_init(int argc, char **argv, char *config, char *prefix, char *pid, int test_conf, int run_as_root, int force_daemon, int force_stderr) /* {{{ */ { fpm_globals.argc = argc; @@ -64,6 +87,7 @@ enum fpm_init_return_status fpm_init(int argc, char **argv, char *config, char * 0 > fpm_children_init_main() || 0 > fpm_sockets_init_main() || 0 > fpm_worker_pool_init_main() || + 0 > fpm_user_cache_init_main() || 0 > fpm_event_init_main()) { if (fpm_globals.test_successful) { diff --git a/sapi/fpm/fpm/fpm_children.c b/sapi/fpm/fpm/fpm_children.c index 285df91a9b69..7237be78e1af 100644 --- a/sapi/fpm/fpm/fpm_children.c +++ b/sapi/fpm/fpm/fpm_children.c @@ -29,6 +29,8 @@ #include "zlog.h" +#include "ext/opcache/zend_user_cache.h" + static time_t *last_faults; static int fault; @@ -189,6 +191,7 @@ static void fpm_child_init(struct fpm_worker_pool_s *wp) /* {{{ */ { fpm_globals.max_requests = wp->config->pm_max_requests; fpm_globals.listening_socket = dup(wp->listening_socket); + zend_opcache_user_cache_partition_activate(wp->user_cache_partition); if (0 > fpm_stdio_init_child(wp) || 0 > fpm_log_init_child(wp) || diff --git a/sapi/fpm/fpm/fpm_worker_pool.h b/sapi/fpm/fpm/fpm_worker_pool.h index efb8640cd32f..943616f9a602 100644 --- a/sapi/fpm/fpm/fpm_worker_pool.h +++ b/sapi/fpm/fpm/fpm_worker_pool.h @@ -5,6 +5,7 @@ #include "fpm_conf.h" #include "fpm_shm.h" +#include "ext/opcache/zend_user_cache.h" struct fpm_worker_pool_s; struct fpm_child_s; @@ -38,6 +39,7 @@ struct fpm_worker_pool_s { struct fpm_scoreboard_s *scoreboard; int log_fd; char **limit_extensions; + zend_opcache_user_cache_partition *user_cache_partition; /* for ondemand PM */ struct fpm_event_s *ondemand_event; diff --git a/sapi/litespeed/lsapi_main.c b/sapi/litespeed/lsapi_main.c index e7aec268489b..3b418b596659 100644 --- a/sapi/litespeed/lsapi_main.c +++ b/sapi/litespeed/lsapi_main.c @@ -26,6 +26,8 @@ #include "lsapilib.h" #include "lsapi_main_arginfo.h" +#include "ext/opcache/zend_user_cache.h" + #include #include @@ -51,6 +53,7 @@ #endif #define SAPI_LSAPI_MAX_HEADER_LENGTH LSAPI_RESP_HTTP_HEADER_MAX +#define LSAPI_USER_CACHE_MAX_BOUNDARY_PARTITIONS 32 /* Key for each cache entry is dirname(PATH_TRANSLATED). * @@ -63,6 +66,15 @@ typedef struct _user_config_cache_entry { time_t expires; HashTable user_config; } user_config_cache_entry; + +typedef struct _lsapi_user_cache_boundary_partition { + char *boundary; + size_t boundary_len; + zend_ulong boundary_hash; + zend_opcache_user_cache_partition *partition; + struct _lsapi_user_cache_boundary_partition *next; +} lsapi_user_cache_boundary_partition; + static HashTable user_config_cache; static int lsapi_mode = 0; @@ -73,6 +85,10 @@ static int ignore_php_ini = 0; static char * argv0 = NULL; static int engine = 1; static int parse_user_ini = 0; +static lsapi_user_cache_boundary_partition *lsapi_user_cache_partitions = NULL; +static uint32_t lsapi_user_cache_partition_count = 0; +static bool lsapi_user_cache_partition_limit_logged = false; +static bool lsapi_user_cache_partition_startup_failed_logged = false; #ifdef ZTS zend_compiler_globals *compiler_globals; @@ -97,6 +113,7 @@ static int php_lsapi_startup(sapi_module_struct *sapi_module) if (php_module_startup(sapi_module, NULL)==FAILURE) { return FAILURE; } + zend_opcache_user_cache_opt_in(); argv0 = sapi_module->executable_location; return SUCCESS; } @@ -176,6 +193,8 @@ static int sapi_lsapi_deactivate(void) SG(request_info).path_translated = NULL; } + zend_opcache_user_cache_partition_activate(NULL); + return SUCCESS; } /* }}} */ @@ -513,6 +532,208 @@ static void sapi_lsapi_log_message(const char *message, int syslog_type_int) } /* }}} */ +static lsapi_user_cache_boundary_partition *lsapi_user_cache_find_boundary_partition( + const char *boundary, + size_t boundary_len, + zend_ulong boundary_hash) +{ + lsapi_user_cache_boundary_partition *entry; + + for (entry = lsapi_user_cache_partitions; entry != NULL; entry = entry->next) { + if (entry->boundary_hash == boundary_hash && + entry->boundary_len == boundary_len && + memcmp(entry->boundary, boundary, boundary_len) == 0 + ) { + return entry; + } + } + + return NULL; +} + +static lsapi_user_cache_boundary_partition *lsapi_user_cache_create_boundary_partition( + const char *boundary, + size_t boundary_len, + zend_ulong boundary_hash) +{ + lsapi_user_cache_boundary_partition *entry; + char partition_name[128]; + + if (lsapi_user_cache_partition_count >= LSAPI_USER_CACHE_MAX_BOUNDARY_PARTITIONS) { + if (!lsapi_user_cache_partition_limit_logged) { + sapi_lsapi_log_message("OPcache User Cache disabled for this request because the LSAPI cache boundary partition limit was reached", 0); + lsapi_user_cache_partition_limit_logged = true; + } + + return NULL; + } + + entry = calloc(1, sizeof(*entry)); + if (entry == NULL) { + return NULL; + } + + entry->boundary = malloc(boundary_len + 1); + if (entry->boundary == NULL) { + free(entry); + + return NULL; + } + + memcpy(entry->boundary, boundary, boundary_len); + entry->boundary[boundary_len] = '\0'; + entry->boundary_len = boundary_len; + entry->boundary_hash = boundary_hash; + + snprintf( + partition_name, + sizeof(partition_name), + "litespeed:%ld:%u:" ZEND_ULONG_FMT, + (long) getpid(), + lsapi_user_cache_partition_count, + boundary_hash + ); + + entry->partition = zend_opcache_user_cache_partition_create(partition_name); + if (entry->partition == NULL) { + free(entry->boundary); + free(entry); + + return NULL; + } + + entry->next = lsapi_user_cache_partitions; + lsapi_user_cache_partitions = entry; + lsapi_user_cache_partition_count++; + + return entry; +} + +static char *lsapi_user_cache_build_boundary_key(const char *boundary, size_t *boundary_key_len) +{ + char *boundary_key; + size_t boundary_len, prefix_len = 0; +#if !defined(PHP_WIN32) && defined(HAVE_UNISTD_H) + char prefix[64]; + int prefix_result; +#endif + + boundary_len = strlen(boundary); + +#if !defined(PHP_WIN32) && defined(HAVE_UNISTD_H) + prefix_result = snprintf( + prefix, + sizeof(prefix), + "uid:%ld:gid:%ld:", + (long) geteuid(), + (long) getegid() + ); + if (prefix_result < 0 || (size_t) prefix_result >= sizeof(prefix)) { + return NULL; + } + prefix_len = (size_t) prefix_result; +#endif + + if (boundary_len > SIZE_MAX - prefix_len - 1) { + return NULL; + } + + boundary_key = malloc(prefix_len + boundary_len + 1); + if (boundary_key == NULL) { + return NULL; + } + +#if !defined(PHP_WIN32) && defined(HAVE_UNISTD_H) + memcpy(boundary_key, prefix, prefix_len); +#endif + memcpy(boundary_key + prefix_len, boundary, boundary_len + 1); + *boundary_key_len = prefix_len + boundary_len; + + return boundary_key; +} + +static zend_opcache_user_cache_partition *lsapi_user_cache_get_partition_for_boundary(const char *boundary) +{ + lsapi_user_cache_boundary_partition *entry; + char *boundary_key; + size_t boundary_len; + zend_ulong boundary_hash; + + boundary_key = lsapi_user_cache_build_boundary_key(boundary, &boundary_len); + if (boundary_key == NULL) { + return NULL; + } + + boundary_hash = zend_inline_hash_func(boundary_key, boundary_len); + entry = lsapi_user_cache_find_boundary_partition(boundary_key, boundary_len, boundary_hash); + if (entry == NULL) { + entry = lsapi_user_cache_create_boundary_partition(boundary_key, boundary_len, boundary_hash); + if (entry == NULL) { + free(boundary_key); + + return NULL; + } + } + + free(boundary_key); + + if (!zend_opcache_user_cache_partition_startup_storage(entry->partition) && + !lsapi_user_cache_partition_startup_failed_logged + ) { + sapi_lsapi_log_message("OPcache User Cache partition startup failed; User Cache will be unavailable", 0); + lsapi_user_cache_partition_startup_failed_logged = true; + } + + return entry->partition; +} + +static zend_opcache_user_cache_partition *lsapi_user_cache_get_request_partition(void) +{ + const char *boundary; + + boundary = sapi_lsapi_getenv("DOCUMENT_ROOT", 0); + if (boundary == NULL || boundary[0] == '\0') { + boundary = sapi_lsapi_getenv("SERVER_NAME", 0); + } + if (boundary == NULL || boundary[0] == '\0') { + return NULL; + } + + return lsapi_user_cache_get_partition_for_boundary(boundary); +} + +static void lsapi_user_cache_activate_request_partition(void) +{ + zend_opcache_user_cache_partition *partition; + + partition = lsapi_user_cache_get_request_partition(); + if (partition == NULL) { + zend_opcache_user_cache_activate_request_unavailable("LSAPI cache boundary is not available"); + + return; + } + + zend_opcache_user_cache_partition_activate(partition); +} + +static void lsapi_user_cache_shutdown_boundary_partitions(void) +{ + lsapi_user_cache_boundary_partition *entry, *next; + + entry = lsapi_user_cache_partitions; + while (entry != NULL) { + next = entry->next; + free(entry->boundary); + free(entry); + entry = next; + } + + lsapi_user_cache_partitions = NULL; + lsapi_user_cache_partition_count = 0; + lsapi_user_cache_partition_limit_logged = false; + lsapi_user_cache_partition_startup_failed_logged = false; +} + /* Set to 1 to turn on log messages useful during development: */ #if 0 @@ -583,6 +804,7 @@ static int sapi_lsapi_activate(void) if (parse_user_ini && lsapi_activate_user_ini() == FAILURE) { return FAILURE; } + lsapi_user_cache_activate_request_partition(); return SUCCESS; } /* {{{ sapi_module_struct cgi_sapi_module */ @@ -736,6 +958,7 @@ static void lsapi_atexit(void) static int lsapi_module_main(int show_source) { struct sigaction act; + lsapi_user_cache_activate_request_partition(); if (php_request_startup() == FAILURE ) { return -1; } @@ -1517,6 +1740,8 @@ int main( int argc, char * argv[] ) return FAILURE; } + zend_opcache_user_cache_opt_in(); + if ( climode ) { return cli_main(argc, argv); } @@ -1627,6 +1852,7 @@ static PHP_MINIT_FUNCTION(litespeed) static PHP_MSHUTDOWN_FUNCTION(litespeed) { zend_hash_destroy(&user_config_cache); + lsapi_user_cache_shutdown_boundary_partitions(); /* UNREGISTER_INI_ENTRIES(); */ return SUCCESS; diff --git a/sapi/phpdbg/phpdbg.c b/sapi/phpdbg/phpdbg.c index 3c0d5e836dd0..5e15bc12947a 100644 --- a/sapi/phpdbg/phpdbg.c +++ b/sapi/phpdbg/phpdbg.c @@ -32,6 +32,8 @@ #include "ext/standard/basic_functions.h" +#include "ext/opcache/zend_user_cache.h" + #if defined(PHP_WIN32) && defined(HAVE_OPENSSL_EXT) # include "openssl/applink.c" #endif @@ -690,6 +692,8 @@ static inline int php_sapi_phpdbg_module_startup(sapi_module_struct *module) /* return FAILURE; } + zend_opcache_user_cache_opt_in(); + phpdbg_booted = 1; return SUCCESS;