diff --git a/ext/date/php_date.c b/ext/date/php_date.c
index 2b0696305d2b..15332a1aac81 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/user_cache/php_user_cache.h" /* For user_cache safe direct path */
#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;
}
/* }}} */
@@ -5851,6 +5857,426 @@ static bool php_date_period_initialize_from_hash(php_period_obj *period_obj, con
return true;
} /* }}} */
+#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 *arr, const char *name, int64_t value)
+{
+#if PHP_DATE_SIZEOF_LONG == 8
+ add_assoc_long(arr, name, (zend_long) value);
+#else
+ add_assoc_str(arr, 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 *ctx,
+ zend_object *new_object,
+ zend_object *old_object,
+ php_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;
+ php_period_obj *new_periodobj, *old_periodobj;
+
+ (void) ctx;
+
+ if (clone_value == NULL) {
+ return false;
+ }
+
+ 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;
+ }
+
+ if (instanceof_function(old_object->ce, date_ce_period)) {
+ old_periodobj = php_period_obj_from_obj(old_object);
+ new_periodobj = php_period_obj_from_obj(new_object);
+
+ new_periodobj->start_ce = old_periodobj->start_ce;
+ new_periodobj->recurrences = old_periodobj->recurrences;
+ new_periodobj->initialized = old_periodobj->initialized;
+ new_periodobj->include_start_date = old_periodobj->include_start_date;
+ new_periodobj->include_end_date = old_periodobj->include_end_date;
+
+ if (old_periodobj->start != NULL) {
+ new_periodobj->start = timelib_time_clone(old_periodobj->start);
+ if (new_periodobj->start == NULL) {
+ return false;
+ }
+ }
+
+ if (old_periodobj->current != NULL) {
+ new_periodobj->current = timelib_time_clone(old_periodobj->current);
+ if (new_periodobj->current == NULL) {
+ return false;
+ }
+ }
+
+ if (old_periodobj->end != NULL) {
+ new_periodobj->end = timelib_time_clone(old_periodobj->end);
+ if (new_periodobj->end == NULL) {
+ return false;
+ }
+ }
+
+ if (old_periodobj->interval != NULL) {
+ new_periodobj->interval = timelib_rel_time_clone(old_periodobj->interval);
+ if (new_periodobj->interval == NULL) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ return false;
+}
+
+static bool php_date_user_cache_timezone_pair_to_hash(php_date_obj *dateobj, HashTable *props)
+{
+ zval zv;
+
+ ZVAL_LONG(&zv, dateobj->time->zone_type);
+ zend_hash_str_update(props, "timezone_type", sizeof("timezone_type")-1, &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:
+ return false;
+ }
+ zend_hash_str_update(props, "timezone", sizeof("timezone")-1, &zv);
+
+ return true;
+}
+
+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, 3);
+
+ php_date_user_cache_copy_time_snapshot(&snapshot, dateobj->time);
+
+ if (!php_date_user_cache_timezone_pair_to_hash(dateobj, Z_ARRVAL_P(state))) {
+ zval_ptr_dtor(state);
+ ZVAL_UNDEF(state);
+
+ return false;
+ }
+
+ 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(zval *state, const zval *object)
+{
+ 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)
+ ) {
+ return php_date_serialize_datetime_user_cache_state(Z_PHPDATE_P((zval *) object), 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 && intervalobj->date_string == NULL) {
+ return false;
+ }
+
+ array_init_size(state, intervalobj->from_string ? 2 : 18);
+ date_interval_object_to_hash(intervalobj, Z_ARRVAL_P(state));
+
+ if (!intervalobj->from_string) {
+ 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;
+ }
+
+ if (instanceof_function(Z_OBJCE_P(object), date_ce_period)) {
+ php_period_obj *periodobj = Z_PHPPERIOD_P((zval *) object);
+
+ if (!periodobj->initialized || periodobj->start == NULL) {
+ return false;
+ }
+
+ array_init_size(state, 7);
+ date_period_object_to_hash(periodobj, Z_ARRVAL_P(state));
+
+ 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_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)
+ ) {
+ return php_date_unserialize_datetime_user_cache_state(object, 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);
+ }
+
+ if (instanceof_function(Z_OBJCE_P(object), date_ce_period)) {
+ return php_date_period_initialize_from_hash(Z_PHPPERIOD_P(object), Z_ARRVAL_P(state))
+ && !EG(exception);
+ }
+
+ return false;
+}
+
+static const php_user_cache_safe_direct_handlers php_date_user_cache_handlers = {
+ .prefer_request_local_prototype = true,
+ .copy = php_date_copy_user_cache_state,
+ .state_serialize = php_date_serialize_user_cache_state,
+ .state_unserialize = php_date_unserialize_user_cache_state,
+};
+
+static void php_date_register_user_cache_handlers(void)
+{
+ php_user_cache_safe_direct_register_class(date_ce_date, &php_date_user_cache_handlers);
+ php_user_cache_safe_direct_register_class(date_ce_immutable, &php_date_user_cache_handlers);
+ php_user_cache_safe_direct_register_class(date_ce_timezone, &php_date_user_cache_handlers);
+ php_user_cache_safe_direct_register_class(date_ce_interval, &php_date_user_cache_handlers);
+ php_user_cache_safe_direct_register_class(date_ce_period, &php_date_user_cache_handlers);
+}
+
/* {{{ */
PHP_METHOD(DatePeriod, __set_state)
{
diff --git a/ext/spl/spl_array.c b/ext/spl/spl_array.c
index 1976192e7b06..7b104f3f3760 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/user_cache/php_user_cache.h" /* For user_cache safe direct path */
#include "zend_smart_str.h"
#include "zend_interfaces.h"
#include "zend_exceptions.h"
@@ -1506,6 +1507,219 @@ PHP_METHOD(ArrayObject, __unserialize)
}
/* }}} */
+/* Serializes ArrayObject state without dynamic members; used by the user-cache
+ * safe-direct path only (the regular __serialize includes members). */
+static void spl_array_object_user_cache_serialize_state(zval *object, zval *return_value)
+{
+ spl_array_object *intern = Z_SPLARRAY_P(object);
+ zval tmp;
+
+ array_init(return_value);
+
+ /* flags */
+ ZVAL_LONG(&tmp, (intern->ar_flags & SPL_ARRAY_CLONE_MASK));
+ zend_hash_next_index_insert(Z_ARRVAL_P(return_value), &tmp);
+
+ /* storage */
+ 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);
+
+ /* iterator class */
+ 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_user_cache_unserialize_state(zval *object, HashTable *data)
+{
+ spl_array_object *intern = Z_SPLARRAY_P(object);
+ zend_class_entry *ce;
+ zval *flags_zv, *storage_zv, *iterator_class_zv;
+ zend_long flags;
+
+ 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, 0L, 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, spl_ce_ArrayIterator)) {
+ zend_throw_exception_ex(
+ spl_ce_UnexpectedValueException,
+ 0,
+ "Cannot deserialize ArrayObject with iterator class '%s'; this class is not derived from ArrayIterator",
+ ZSTR_VAL(Z_STR_P(iterator_class_zv))
+ );
+
+ return;
+ }
+
+ intern->ce_get_iterator = ce;
+ }
+}
+
+static bool spl_array_object_copy_user_cache_state(
+ void *ctx,
+ zend_object *new_obj,
+ zend_object *old_obj,
+ php_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_obj);
+ new_intern = spl_array_from_obj(new_obj);
+
+ if (clone_value == NULL) {
+ return false;
+ }
+
+ ZVAL_OBJ(&new_zv, new_obj);
+ 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(ctx, &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 *ctx,
+ const zval *object,
+ php_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(ctx, &intern->array);
+}
+
+static bool spl_array_object_serialize_user_cache_state(zval *state, const zval *object)
+{
+ ZVAL_UNDEF(state);
+
+ spl_array_object_user_cache_serialize_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_user_cache_unserialize_state(object, Z_ARRVAL_P(state));
+
+ return !EG(exception);
+}
+
+static const php_user_cache_safe_direct_handlers spl_array_user_cache_handlers = {
+ .copy = spl_array_object_copy_user_cache_state,
+ .state_has_unstorable = spl_array_object_user_cache_state_has_unstorable,
+ .state_serialize = spl_array_object_serialize_user_cache_state,
+ .state_unserialize = spl_array_object_unserialize_user_cache_state,
+};
+
/* {{{ */
PHP_METHOD(ArrayObject, __debugInfo)
{
@@ -1888,6 +2102,10 @@ PHP_MINIT_FUNCTION(spl_array)
spl_ce_RecursiveArrayIterator->create_object = spl_array_object_new;
spl_ce_RecursiveArrayIterator->get_iterator = spl_array_get_iterator;
+ php_user_cache_safe_direct_register_class(spl_ce_ArrayObject, &spl_array_user_cache_handlers);
+ php_user_cache_safe_direct_register_class(spl_ce_ArrayIterator, &spl_array_user_cache_handlers);
+ php_user_cache_safe_direct_register_class(spl_ce_RecursiveArrayIterator, &spl_array_user_cache_handlers);
+
return SUCCESS;
}
/* }}} */
diff --git a/ext/spl/spl_dllist.c b/ext/spl/spl_dllist.c
index 77e8b0d4e7da..bf3d586fda42 100644
--- a/ext/spl/spl_dllist.c
+++ b/ext/spl/spl_dllist.c
@@ -17,6 +17,7 @@
#endif
#include "php.h"
+#include "ext/user_cache/php_user_cache.h" /* For user_cache safe direct support path */
#include "zend_interfaces.h"
#include "zend_exceptions.h"
#include "zend_hash.h"
@@ -1101,6 +1102,152 @@ PHP_METHOD(SplDoublyLinkedList, __unserialize) {
object_properties_load(&intern->std, Z_ARRVAL_P(members_zv));
} /* }}} */
+/* Serializes list state without dynamic members; used by the user-cache
+ * safe-direct path only. */
+static void spl_dllist_object_user_cache_serialize_state(zval *object, zval *return_value)
+{
+ spl_dllist_object *intern = Z_SPLDLLIST_P(object);
+ spl_ptr_llist_element *current = intern->llist->head;
+ zval tmp;
+
+ array_init(return_value);
+
+ ZVAL_LONG(&tmp, intern->flags);
+ zend_hash_next_index_insert(Z_ARRVAL_P(return_value), &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(return_value), &tmp);
+}
+
+/* Strict restore for the machine-written user-cache state: validates the flags
+ * value and requires an empty list. Returns false on malformed data. */
+static bool spl_dllist_object_user_cache_unserialize_state(zval *object, HashTable *data)
+{
+ spl_dllist_object *intern = Z_SPLDLLIST_P(object);
+ zend_long flags;
+ zval *flags_zv, *storage_zv, *elem;
+
+ 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;
+ }
+
+ 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 bool spl_dllist_object_copy_user_cache_state(
+ void *ctx,
+ zend_object *new_obj,
+ zend_object *old_obj,
+ php_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) {
+ return false;
+ }
+
+ old_intern = spl_dllist_from_obj(old_obj);
+ new_intern = spl_dllist_from_obj(new_obj);
+ 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(ctx, &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 *ctx,
+ const zval *object,
+ php_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;
+ }
+
+ intern = Z_SPLDLLIST_P((zval *) object);
+ current = intern->llist->head;
+ while (current) {
+ if (value_has_unstorable(ctx, ¤t->data)) {
+ return true;
+ }
+
+ current = current->next;
+ }
+
+ return false;
+}
+
+static bool spl_dllist_object_serialize_user_cache_state(zval *state, const zval *object)
+{
+ spl_dllist_object_user_cache_serialize_state((zval *) object, state);
+
+ return true;
+}
+
+static bool spl_dllist_object_unserialize_user_cache_state(zval *object, zval *state)
+{
+ if (Z_TYPE_P(state) != IS_ARRAY) {
+ return false;
+ }
+
+ return spl_dllist_object_user_cache_unserialize_state(object, Z_ARRVAL_P(state));
+}
+
+static const php_user_cache_safe_direct_handlers spl_dllist_user_cache_handlers = {
+ .copy = spl_dllist_object_copy_user_cache_state,
+ .state_has_unstorable = spl_dllist_object_user_cache_state_has_unstorable,
+ .state_serialize = spl_dllist_object_serialize_user_cache_state,
+ .state_unserialize = spl_dllist_object_unserialize_user_cache_state,
+};
+
/* {{{ Inserts a new entry before the specified $index consisting of $newval. */
PHP_METHOD(SplDoublyLinkedList, add)
{
@@ -1219,6 +1366,10 @@ PHP_MINIT_FUNCTION(spl_dllist) /* {{{ */
spl_ce_SplStack->create_object = spl_dllist_object_new;
spl_ce_SplStack->get_iterator = spl_dllist_get_iterator;
+ php_user_cache_safe_direct_register_class(spl_ce_SplDoublyLinkedList, &spl_dllist_user_cache_handlers);
+ php_user_cache_safe_direct_register_class(spl_ce_SplQueue, &spl_dllist_user_cache_handlers);
+ php_user_cache_safe_direct_register_class(spl_ce_SplStack, &spl_dllist_user_cache_handlers);
+
return SUCCESS;
}
/* }}} */
diff --git a/ext/spl/spl_fixedarray.c b/ext/spl/spl_fixedarray.c
index d2eae52e3c0c..d8d4ffa5c3c2 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/user_cache/php_user_cache.h" /* For user_cache safe direct support path */
static zend_object_handlers spl_handler_SplFixedArray;
PHPAPI zend_class_entry *spl_ce_SplFixedArray;
@@ -652,6 +653,136 @@ PHP_METHOD(SplFixedArray, __unserialize)
}
}
+static void spl_fixedarray_object_build_user_cache_elements(spl_fixedarray_object *intern, zval *return_value)
+{
+ zend_long i;
+ zval *current;
+
+ array_init_size(return_value, intern->array.size);
+
+ 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);
+ }
+}
+
+static bool spl_fixedarray_object_copy_user_cache_state(
+ void *ctx,
+ zend_object *new_obj,
+ zend_object *old_obj,
+ php_user_cache_safe_direct_clone_value_func_t clone_value)
+{
+ spl_fixedarray_object *old_intern, *new_intern;
+ zend_long size, i;
+ zval cloned_elem;
+
+ if (clone_value == NULL) {
+ return false;
+ }
+
+ old_intern = spl_fixed_array_from_obj(old_obj);
+ new_intern = spl_fixed_array_from_obj(new_obj);
+ if (new_intern->array.size != 0) {
+ return false;
+ }
+ size = old_intern->array.size;
+
+ /* spl_fixedarray_init() NULL-fills the newly allocated elements before we
+ * populate them one at a time below, so a clone failure partway through
+ * the loop leaves the not-yet-cloned tail as NULL for the destructor to
+ * walk, instead of the uninitialized memory init_non_empty_struct() alone
+ * would leave behind. */
+ spl_fixedarray_init(&new_intern->array, size);
+
+ for (i = 0; i < size; i++) {
+ ZVAL_UNDEF(&cloned_elem);
+
+ if (!clone_value(ctx, &cloned_elem, &old_intern->array.elements[i])) {
+ return false;
+ }
+
+ ZVAL_COPY_DEREF(&new_intern->array.elements[i], &cloned_elem);
+
+ zval_ptr_dtor(&cloned_elem);
+ }
+
+ return !EG(exception);
+}
+
+static bool spl_fixedarray_object_user_cache_state_has_unstorable(
+ void *ctx,
+ const zval *object,
+ php_user_cache_safe_direct_value_has_unstorable_func_t value_has_unstorable)
+{
+ zval state_zv;
+ bool result;
+
+ if (value_has_unstorable == NULL) {
+ return false;
+ }
+
+ spl_fixedarray_object_build_user_cache_elements(Z_SPLFIXEDARRAY_P((zval *) object), &state_zv);
+
+ result = value_has_unstorable(ctx, &state_zv);
+
+ zval_ptr_dtor(&state_zv);
+
+ return result;
+}
+
+static bool spl_fixedarray_object_serialize_user_cache_state(zval *state, const zval *object)
+{
+ spl_fixedarray_object_build_user_cache_elements(Z_SPLFIXEDARRAY_P((zval *) object), state);
+
+ return true;
+}
+
+static void spl_fixedarray_object_user_cache_unserialize_state(zval *object, HashTable *data)
+{
+ spl_fixedarray_object *intern = Z_SPLFIXEDARRAY_P(object);
+ zend_long size;
+ zval *elem;
+
+ if (intern->array.size != 0) {
+ return;
+ }
+
+ size = zend_hash_num_elements(data);
+ if (!size) {
+ return;
+ }
+
+ spl_fixedarray_init_non_empty_struct(&intern->array, size);
+
+ intern->array.size = 0;
+
+ ZEND_HASH_FOREACH_VAL(data, elem) {
+ ZVAL_COPY_DEREF(&intern->array.elements[intern->array.size], elem);
+ intern->array.size++;
+ } ZEND_HASH_FOREACH_END();
+}
+
+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_user_cache_unserialize_state(object, Z_ARRVAL_P(state));
+
+ return !EG(exception);
+}
+
+static const php_user_cache_safe_direct_handlers spl_fixedarray_user_cache_handlers = {
+ .copy = spl_fixedarray_object_copy_user_cache_state,
+ .state_has_unstorable = spl_fixedarray_object_user_cache_state_has_unstorable,
+ .state_serialize = spl_fixedarray_object_serialize_user_cache_state,
+ .state_unserialize = spl_fixedarray_object_unserialize_user_cache_state,
+};
+
PHP_METHOD(SplFixedArray, count)
{
zval *object = ZEND_THIS;
@@ -960,5 +1091,7 @@ 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;
+ php_user_cache_safe_direct_register_class(spl_ce_SplFixedArray, &spl_fixedarray_user_cache_handlers);
+
return SUCCESS;
}
diff --git a/ext/spl/spl_heap.c b/ext/spl/spl_heap.c
index 1073836aa53b..2d95caa4ef35 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/user_cache/php_user_cache.h"
#define PTR_HEAP_BLOCK_SIZE 64
@@ -1259,6 +1260,274 @@ PHP_METHOD(SplHeap, __unserialize)
}
}
+static bool spl_heap_object_is_pqueue(zend_class_entry *ce)
+{
+ return instanceof_function(ce, spl_ce_SplPriorityQueue);
+}
+
+static void spl_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_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_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 *ctx,
+ zend_object *new_obj,
+ zend_object *old_obj,
+ php_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) {
+ return false;
+ }
+
+ old_intern = spl_heap_from_obj(old_obj);
+ new_intern = spl_heap_from_obj(new_obj);
+ 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_is_pqueue(old_obj->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(ctx, &cloned_data, &old_elem->data)) {
+ return false;
+ }
+
+ if (!clone_value(ctx, &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(ctx, &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 *ctx,
+ const zval *object,
+ php_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;
+ }
+
+ 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_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(ctx, &elem->data) ||
+ value_has_unstorable(ctx, &elem->priority)
+ ) {
+ return true;
+ }
+ } else {
+ zv_elem = spl_heap_elem(intern->heap, i);
+
+ if (value_has_unstorable(ctx, zv_elem)) {
+ return true;
+ }
+ }
+ }
+
+ return false;
+}
+
+static bool spl_heap_object_serialize_user_cache_state(zval *state, const zval *object)
+{
+ spl_heap_object *intern;
+ spl_pqueue_elem *pqueue_elem;
+ zval *heap_elem, elems, entry, tmp;
+ size_t i;
+ bool is_pqueue;
+
+ 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_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(&elems, 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(elems), &entry);
+ } else {
+ heap_elem = spl_heap_elem(intern->heap, i);
+
+ zend_hash_next_index_insert(Z_ARRVAL(elems), heap_elem);
+ Z_TRY_ADDREF_P(heap_elem);
+ }
+ }
+
+ zend_hash_next_index_insert(Z_ARRVAL_P(state), &elems);
+
+ 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 (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_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 php_user_cache_safe_direct_handlers spl_heap_user_cache_handlers = {
+ .copy = spl_heap_object_copy_user_cache_state,
+ .state_has_unstorable = spl_heap_object_user_cache_state_has_unstorable,
+ .state_serialize = spl_heap_object_serialize_user_cache_state,
+ .state_unserialize = spl_heap_object_unserialize_user_cache_state,
+};
+
/* iterator handler table */
static const zend_object_iterator_funcs spl_heap_it_funcs = {
spl_heap_it_dtor,
@@ -1356,6 +1625,11 @@ 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;
+ php_user_cache_safe_direct_register_class(spl_ce_SplHeap, &spl_heap_user_cache_handlers);
+ php_user_cache_safe_direct_register_class(spl_ce_SplMinHeap, &spl_heap_user_cache_handlers);
+ php_user_cache_safe_direct_register_class(spl_ce_SplMaxHeap, &spl_heap_user_cache_handlers);
+ php_user_cache_safe_direct_register_class(spl_ce_SplPriorityQueue, &spl_heap_user_cache_handlers);
+
return SUCCESS;
}
/* }}} */
diff --git a/ext/user_cache/config.m4 b/ext/user_cache/config.m4
new file mode 100644
index 000000000000..0cae97ab8e52
--- /dev/null
+++ b/ext/user_cache/config.m4
@@ -0,0 +1,16 @@
+PHP_NEW_EXTENSION([user_cache], m4_normalize([
+ user_cache.c
+ user_cache_storage.c
+ user_cache_shared_graph.c
+ user_cache_entries.c
+ user_cache_serdes.c
+ user_cache_alloc_shm.c
+ user_cache_alloc_posix.c
+ ]),
+ [no])
+
+PHP_ADD_EXTENSION_DEP(user_cache, date)
+PHP_ADD_EXTENSION_DEP(user_cache, spl)
+PHP_ADD_EXTENSION_DEP(user_cache, hash)
+
+PHP_INSTALL_HEADERS([ext/user_cache], [php_user_cache.h])
diff --git a/ext/user_cache/config.w32 b/ext/user_cache/config.w32
new file mode 100644
index 000000000000..8ed2457d1d75
--- /dev/null
+++ b/ext/user_cache/config.w32
@@ -0,0 +1,16 @@
+EXTENSION("user_cache", "\
+ user_cache.c \
+ user_cache_storage.c \
+ user_cache_shared_graph.c \
+ user_cache_entries.c \
+ user_cache_serdes.c \
+ user_cache_alloc_shm.c \
+ user_cache_alloc_posix.c", false);
+
+ADD_EXTENSION_DEP('user_cache', 'date');
+ADD_EXTENSION_DEP('user_cache', 'spl');
+ADD_EXTENSION_DEP('user_cache', 'hash');
+
+ADD_FLAG('CFLAGS_USER_CACHE', "/I " + configure_module_dirname);
+
+PHP_INSTALL_HEADERS("ext/user_cache", "php_user_cache.h");
diff --git a/ext/user_cache/php_user_cache.h b/ext/user_cache/php_user_cache.h
new file mode 100644
index 000000000000..e770c7a244e5
--- /dev/null
+++ b/ext/user_cache/php_user_cache.h
@@ -0,0 +1,125 @@
+/*
+ +----------------------------------------------------------------------+
+ | 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 PHP_USER_CACHE_H
+#define PHP_USER_CACHE_H
+
+#include "php.h"
+
+/* Public API for extensions, SAPIs and embedders. */
+
+/* Keep in sync with availability_enum_case(). */
+typedef enum {
+ PHP_USER_CACHE_REASON_NONE = 0,
+ PHP_USER_CACHE_REASON_DISABLED_BY_INI,
+ PHP_USER_CACHE_REASON_SHM_INIT_FAILED,
+ PHP_USER_CACHE_REASON_SAPI_NOT_ENABLED,
+ PHP_USER_CACHE_REASON_BACKEND_NOT_INITIALIZED_BEFORE_WORKER,
+ PHP_USER_CACHE_REASON_BACKEND_INITIALIZED_AFTER_WORKER,
+ PHP_USER_CACHE_REASON_CGI_BOUNDARY_UNAVAILABLE,
+ PHP_USER_CACHE_REASON_LSAPI_BOUNDARY_UNAVAILABLE
+} php_user_cache_reason;
+
+/* Handlers for copying native object state without invoking user code. */
+typedef bool (*php_user_cache_safe_direct_clone_value_func_t)(
+ void *context,
+ zval *dst,
+ zval *src
+);
+
+typedef bool (*php_user_cache_safe_direct_value_has_unstorable_func_t)(
+ void *context,
+ const zval *value
+);
+
+typedef bool (*php_user_cache_safe_direct_state_copy_func_t)(
+ void *context,
+ zend_object *new_object,
+ zend_object *old_object,
+ php_user_cache_safe_direct_clone_value_func_t clone_value
+);
+
+typedef bool (*php_user_cache_safe_direct_state_has_unstorable_func_t)(
+ void *context,
+ const zval *value,
+ php_user_cache_safe_direct_value_has_unstorable_func_t value_has_unstorable
+);
+
+typedef bool (*php_user_cache_safe_direct_state_serialize_func_t)(
+ zval *state,
+ const zval *object
+);
+
+typedef bool (*php_user_cache_safe_direct_state_unserialize_func_t)(
+ zval *object,
+ zval *state
+);
+
+typedef struct {
+ bool prefer_request_local_prototype;
+ php_user_cache_safe_direct_state_copy_func_t copy;
+ php_user_cache_safe_direct_state_has_unstorable_func_t state_has_unstorable;
+ php_user_cache_safe_direct_state_serialize_func_t state_serialize;
+ php_user_cache_safe_direct_state_unserialize_func_t state_unserialize;
+} php_user_cache_safe_direct_handlers;
+
+typedef struct _php_user_cache_partition php_user_cache_partition;
+
+BEGIN_EXTERN_C()
+
+/* The handler structure is copied and may be temporary. */
+ZEND_API void php_user_cache_safe_direct_register_class(
+ zend_class_entry *ce,
+ const php_user_cache_safe_direct_handlers *handlers
+);
+/* SAPI and embedder integration. */
+ZEND_API void php_user_cache_opt_in(void);
+ZEND_API bool php_user_cache_startup_default_context_storage(void);
+ZEND_API php_user_cache_partition *php_user_cache_partition_create(const char *name);
+ZEND_API bool php_user_cache_partition_startup_storage(php_user_cache_partition *partition);
+ZEND_API void php_user_cache_activate_request_unavailable(php_user_cache_reason reason);
+ZEND_API void php_user_cache_partition_activate(php_user_cache_partition *partition);
+ZEND_API php_user_cache_partition *php_user_cache_boundary_partition_get(
+ const char *sapi_prefix,
+ const char *boundary,
+ void (*log_message)(const char *message)
+);
+/* Activate a request partition using DOCUMENT_ROOT or SERVER_NAME. */
+ZEND_API void php_user_cache_activate_boundary_partition(
+ const char *sapi_prefix,
+ const char *(*get_env)(const char *name),
+ void (*log_message)(const char *message),
+ php_user_cache_reason failure_reason
+);
+ZEND_API void php_user_cache_boundary_partitions_shutdown(void);
+
+#ifdef ZTS
+/* Called by php_tsrm_startup_ex() before module startup. */
+size_t php_user_cache_globals_size(void);
+void php_user_cache_globals_startup(void);
+#endif
+
+void php_user_cache_minit(void);
+void php_user_cache_mshutdown(void);
+zend_result php_user_cache_rshutdown(void);
+zend_result php_user_cache_post_deactivate(void);
+void php_user_cache_invalidate_active(void);
+
+extern zend_module_entry user_cache_module_entry;
+
+#define phpext_user_cache_ptr &user_cache_module_entry
+
+END_EXTERN_C()
+
+#endif /* PHP_USER_CACHE_H */
diff --git a/ext/user_cache/tests/CONFLICTS b/ext/user_cache/tests/CONFLICTS
new file mode 100644
index 000000000000..3b242bdd032d
--- /dev/null
+++ b/ext/user_cache/tests/CONFLICTS
@@ -0,0 +1 @@
+user_cache
diff --git a/ext/user_cache/tests/fpm/CONFLICTS b/ext/user_cache/tests/fpm/CONFLICTS
new file mode 100644
index 000000000000..0702cb5bfbb0
--- /dev/null
+++ b/ext/user_cache/tests/fpm/CONFLICTS
@@ -0,0 +1 @@
+all
diff --git a/ext/user_cache/tests/fpm/skipif.inc b/ext/user_cache/tests/fpm/skipif.inc
new file mode 100644
index 000000000000..bc6813f63979
--- /dev/null
+++ b/ext/user_cache/tests/fpm/skipif.inc
@@ -0,0 +1,13 @@
+
+--FILE--
+id = $id;
+ $this->name = $name;
+ }
+
+ public function __serialize(): array
+ {
+ return ['id' => $this->id, 'name' => $this->name];
+ }
+
+ public function __unserialize(array $data): void
+ {
+ $this->id = $data['id'];
+ $this->name = $data['name'];
+ }
+
+ public function info(): string
+ {
+ return $this->id . ':' . $this->name;
+ }
+}
+
+$cache = UserCache\Cache::getPool('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',
+ '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();
+unset($tester);
+gc_collect_cycles();
+
+echo "Done\n";
+
+?>
+--EXPECT--
+Done
+--CLEAN--
+
diff --git a/ext/user_cache/tests/fpm/user_cache_fpm_dateperiod.phpt b/ext/user_cache/tests/fpm/user_cache_fpm_dateperiod.phpt
new file mode 100644
index 000000000000..29a71418fbfa
--- /dev/null
+++ b/ext/user_cache/tests/fpm/user_cache_fpm_dateperiod.phpt
@@ -0,0 +1,112 @@
+--TEST--
+FPM: UserCache\Cache safe-direct DatePeriod state survives across requests
+--EXTENSIONS--
+user_cache
+--SKIPIF--
+
+--FILE--
+label = $label;
+ }
+}
+
+function period_dates(DatePeriod $period): string
+{
+ $dates = [];
+ foreach ($period as $date) {
+ $dates[] = $date->format('Y-m-d');
+ }
+ return implode(',', $dates);
+}
+
+$cache = UserCache\Cache::getPool('default');
+$action = $_GET['action'] ?? 'seed';
+
+if ($action === 'seed') {
+ $cache->clear();
+
+ $byRecurrences = new DatePeriod(new DateTimeImmutable('2026-01-01'), new DateInterval('P1D'), 3);
+ $byEnd = new DatePeriod(
+ new DateTimeImmutable('2026-03-01'),
+ new DateInterval('P1M'),
+ new DateTimeImmutable('2026-05-15'),
+ );
+ $tagged = new TaggedDatePeriod(new DateTimeImmutable('2026-02-01'), new DateInterval('P1D'), 2);
+ $tagged->tag('window');
+
+ var_dump($cache->store('period_payload', [
+ 'recurrences' => $byRecurrences,
+ 'end' => $byEnd,
+ 'tagged' => $tagged,
+ ]));
+ echo "seed\n";
+ return;
+}
+
+$payload = $cache->fetch('period_payload');
+$recurrences = $payload['recurrences'];
+$end = $payload['end'];
+$tagged = $payload['tagged'];
+
+echo ($recurrences instanceof DatePeriod ? 'DatePeriod' : get_debug_type($recurrences)), ',', period_dates($recurrences), "\n";
+echo period_dates($end), "\n";
+echo ($tagged instanceof TaggedDatePeriod ? 'TaggedDatePeriod' : get_debug_type($tagged)), ',', $tagged->label, ',', period_dates($tagged), "\n";
+PHP;
+
+$tester = new FPM\Tester($cfg, $code);
+$tester->start(iniEntries: [
+ 'opcache.enable' => '1',
+ 'user_cache.shm_size' => '32M',
+ 'date.timezone' => 'UTC',
+]);
+$tester->expectLogStartNotices();
+
+$tester->request(query: 'action=seed')->expectBody(
+ "bool(true)\n" .
+ "seed"
+);
+
+$tester->request(query: 'action=fetch')->expectBody(
+ "DatePeriod,2026-01-01,2026-01-02,2026-01-03,2026-01-04\n" .
+ "2026-03-01,2026-04-01,2026-05-01\n" .
+ "TaggedDatePeriod,window,2026-02-01,2026-02-02,2026-02-03"
+);
+
+$tester->terminate();
+$tester->expectLogTerminatingNotices();
+$tester->close();
+unset($tester);
+gc_collect_cycles();
+
+echo "Done\n";
+
+?>
+--EXPECT--
+Done
+--CLEAN--
+
diff --git a/ext/user_cache/tests/fpm/user_cache_fpm_decode_failure_delete.phpt b/ext/user_cache/tests/fpm/user_cache_fpm_decode_failure_delete.phpt
new file mode 100644
index 000000000000..e834e35f5fd2
--- /dev/null
+++ b/ext/user_cache/tests/fpm/user_cache_fpm_decode_failure_delete.phpt
@@ -0,0 +1,130 @@
+--TEST--
+FPM: UserCache\Cache drops silently-corrupt entries but propagates decode exceptions
+--EXTENSIONS--
+user_cache
+--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');
+ }
+ });
+
+ /* A missing class with no autoloader hit fails to decode without an
+ * exception, so the entry is corrupt and dropped. */
+ var_dump($cache->fetch('single', 'DEFAULT'));
+ var_dump($cache->has('single'));
+
+ /* A throwing autoloader is a genuine userland exception: it propagates and
+ * the entry is kept, matching native unserialize(). */
+ try {
+ $cache->fetch('autoload-single', 'DEFAULT');
+ echo "no exception\n";
+ } catch (Exception $e) {
+ echo "caught: ", $e->getMessage(), "\n";
+ }
+ var_dump($cache->has('autoload-single'));
+
+ /* In a batch, the corrupt entry is dropped and the miss yields the default
+ * before the throwing autoloader aborts the whole fetch. */
+ try {
+ $cache->fetchMultiple(['multiple', 'missing', 'autoload-multiple'], 'DEFAULT');
+ echo "no exception\n";
+ } catch (Exception $e) {
+ echo "caught: ", $e->getMessage(), "\n";
+ }
+ 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',
+ '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" .
+ "caught: autoload failed\n" .
+ "bool(true)\n" .
+ "caught: autoload failed\n" .
+ "bool(false)\n" .
+ "bool(true)"
+);
+
+$tester->terminate();
+$tester->expectLogTerminatingNotices();
+$tester->close();
+unset($tester);
+gc_collect_cycles();
+
+echo "Done\n";
+
+?>
+--EXPECT--
+Done
+--CLEAN--
+
diff --git a/ext/user_cache/tests/fpm/user_cache_fpm_lookup_cache.phpt b/ext/user_cache/tests/fpm/user_cache_fpm_lookup_cache.phpt
new file mode 100644
index 000000000000..c8acc43ad3d1
--- /dev/null
+++ b/ext/user_cache/tests/fpm/user_cache_fpm_lookup_cache.phpt
@@ -0,0 +1,96 @@
+--TEST--
+FPM: UserCache\Cache request-local lookup cache sees same-request updates
+--EXTENSIONS--
+user_cache
+--SKIPIF--
+
+--FILE--
+fetch($key, 'MISS');
+}
+
+$cache = UserCache\Cache::getPool('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',
+ '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();
+unset($tester);
+gc_collect_cycles();
+
+echo "Done\n";
+
+?>
+--EXPECT--
+Done
+--CLEAN--
+
diff --git a/ext/user_cache/tests/fpm/user_cache_fpm_persistence.phpt b/ext/user_cache/tests/fpm/user_cache_fpm_persistence.phpt
new file mode 100644
index 000000000000..9744eff7018a
--- /dev/null
+++ b/ext/user_cache/tests/fpm/user_cache_fpm_persistence.phpt
@@ -0,0 +1,108 @@
+--TEST--
+FPM: UserCache\Cache persists across requests
+--EXTENSIONS--
+user_cache
+--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',
+ '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();
+unset($tester);
+gc_collect_cycles();
+
+echo "Done\n";
+
+?>
+--EXPECT--
+Done
+--CLEAN--
+
diff --git a/ext/user_cache/tests/fpm/user_cache_fpm_pool_separate.phpt b/ext/user_cache/tests/fpm/user_cache_fpm_pool_separate.phpt
new file mode 100644
index 000000000000..126aa3e97cf5
--- /dev/null
+++ b/ext/user_cache/tests/fpm/user_cache_fpm_pool_separate.phpt
@@ -0,0 +1,113 @@
+--TEST--
+FPM: UserCache\Cache is separated between pools
+--EXTENSIONS--
+user_cache
+--SKIPIF--
+
+--FILE--
+store($key, $pool . '-value');
+}
+
+printf(
+ "%s:%s:%s\n",
+ $pool,
+ $cache->fetch($key, 'MISS'),
+ $cache->getPoolStatus()->getPoolName()
+);
+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',
+ '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();
+unset($tester);
+gc_collect_cycles();
+
+echo "Done\n";
+
+?>
+--EXPECT--
+Done
+--CLEAN--
+
diff --git a/ext/user_cache/tests/fpm/user_cache_fpm_remember_callable_lifetime.phpt b/ext/user_cache/tests/fpm/user_cache_fpm_remember_callable_lifetime.phpt
new file mode 100644
index 000000000000..488b83439efb
--- /dev/null
+++ b/ext/user_cache/tests/fpm/user_cache_fpm_remember_callable_lifetime.phpt
@@ -0,0 +1,122 @@
+--TEST--
+FPM: UserCache\Cache remember does not retain callables across requests
+--EXTENSIONS--
+user_cache
+--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',
+ '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();
+unset($tester);
+gc_collect_cycles();
+
+echo "Done\n";
+
+?>
+--EXPECT--
+Done
+--CLEAN--
+
diff --git a/ext/user_cache/tests/fpm/user_cache_fpm_request_local_slot.phpt b/ext/user_cache/tests/fpm/user_cache_fpm_request_local_slot.phpt
new file mode 100644
index 000000000000..3fdca8abd087
--- /dev/null
+++ b/ext/user_cache/tests/fpm/user_cache_fpm_request_local_slot.phpt
@@ -0,0 +1,95 @@
+--TEST--
+FPM: UserCache\Cache request-local slots amortize __unserialize per request only
+--EXTENSIONS--
+user_cache
+--SKIPIF--
+
+--FILE--
+ $this->value];
+ }
+
+ public function __unserialize(array $data): void
+ {
+ self::$unserializeCalls++;
+ $this->value = $data['value'];
+ }
+}
+
+$cache = UserCache\Cache::getPool('default');
+$scenario = $_GET['scenario'] ?? 'seed';
+$key = 'fpm_request_local_slot_key';
+
+if ($scenario === 'seed') {
+ $cache->clear();
+ echo $cache->store($key, new FpmSlotMagicValue(41)) ? 'stored' : 'store-failed';
+ return;
+}
+
+if ($scenario === 'measure') {
+ $last = null;
+ for ($i = 0; $i < 5; $i++) {
+ $last = $cache->fetch($key);
+ }
+ echo FpmSlotMagicValue::$unserializeCalls, ':', $last->value;
+ return;
+}
+
+throw new RuntimeException('unknown scenario ' . $scenario);
+PHP;
+
+$tester = new FPM\Tester($cfg, $code);
+$tester->start(iniEntries: [
+ 'opcache.enable' => '1',
+ 'user_cache.shm_size' => '32M',
+]);
+$tester->expectLogStartNotices();
+
+$tester->request(query: 'scenario=seed')->expectBody('stored');
+
+/* Each fetch must run __unserialize(), including repeated fetches in one request. */
+$tester->request(query: 'scenario=measure')->expectBody('5:41');
+$tester->request(query: 'scenario=measure')->expectBody('5:41');
+
+$tester->terminate();
+
+$tester->close();
+
+gc_collect_cycles();
+
+echo "Done\n";
+
+?>
+--EXPECT--
+Done
+--CLEAN--
+
diff --git a/ext/user_cache/tests/fpm/user_cache_fpm_reset.phpt b/ext/user_cache/tests/fpm/user_cache_fpm_reset.phpt
new file mode 100644
index 000000000000..509d04745cc7
--- /dev/null
+++ b/ext/user_cache/tests/fpm/user_cache_fpm_reset.phpt
@@ -0,0 +1,106 @@
+--TEST--
+FPM: user_cache_reset clears only the active pool partition
+--EXTENSIONS--
+user_cache
+--SKIPIF--
+
+--FILE--
+store($key, $pool . '-value');
+} elseif ($action === 'reset') {
+ var_dump(user_cache_reset());
+} elseif ($action === 'user-reset') {
+ var_dump(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',
+ '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();
+unset($tester);
+gc_collect_cycles();
+
+?>
+--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(15) "beta:beta-value"
+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(17) "alpha:alpha-value"
+string(9) "beta:MISS"
+--CLEAN--
+
diff --git a/ext/user_cache/tests/fpm/user_cache_fpm_safe_direct.phpt b/ext/user_cache/tests/fpm/user_cache_fpm_safe_direct.phpt
new file mode 100644
index 000000000000..babf5dade31d
--- /dev/null
+++ b/ext/user_cache/tests/fpm/user_cache_fpm_safe_direct.phpt
@@ -0,0 +1,143 @@
+--TEST--
+FPM: UserCache\Cache safe-direct DateTime and SPL state survives requests
+--EXTENSIONS--
+user_cache
+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 = UserCache\Cache::getPool('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',
+ '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();
+unset($tester);
+gc_collect_cycles();
+
+echo "Done\n";
+
+?>
+--EXPECT--
+Done
+--CLEAN--
+
diff --git a/ext/user_cache/tests/user_cache_add.phpt b/ext/user_cache/tests/user_cache_add.phpt
new file mode 100644
index 000000000000..e516e57c82df
--- /dev/null
+++ b/ext/user_cache/tests/user_cache_add.phpt
@@ -0,0 +1,39 @@
+--TEST--
+UserCache\Cache: add() stores only when the key is absent
+--EXTENSIONS--
+user_cache
+--INI--
+user_cache.enable=1
+user_cache.enable_cli=1
+opcache.file_cache_only=0
+user_cache.shm_size=16M
+--FILE--
+clear();
+
+var_dump($cache->add('key', 'first'));
+var_dump($cache->add('key', 'second'));
+var_dump($cache->fetch('key'));
+
+var_dump($cache->store('key', 'overwritten'));
+var_dump($cache->fetch('key'));
+
+var_dump($cache->delete('key'));
+var_dump($cache->add('key', 'after-delete'));
+var_dump($cache->fetch('key'));
+
+var_dump($cache->add('ttl-key', 'v', 60));
+var_dump($cache->fetch('ttl-key'));
+?>
+--EXPECT--
+bool(true)
+bool(false)
+string(5) "first"
+bool(true)
+string(11) "overwritten"
+bool(true)
+bool(true)
+string(12) "after-delete"
+bool(true)
+string(1) "v"
diff --git a/ext/user_cache/tests/user_cache_apache2handler_boundary_001.phpt b/ext/user_cache/tests/user_cache_apache2handler_boundary_001.phpt
new file mode 100644
index 000000000000..783f4b04d672
--- /dev/null
+++ b/ext/user_cache/tests/user_cache_apache2handler_boundary_001.phpt
@@ -0,0 +1,218 @@
+--TEST--
+UserCache\Cache: apache2handler partitions cache data by virtual host
+--EXTENSIONS--
+user_cache
+--SKIPIF--
+
+--FILE--
+store($key, $host . '-value');
+}
+
+echo $host, ':', $cache->fetch($key, 'MISS'), ':', $cache->getPoolStatus()->getPoolName(), "\n";
+PHP;
+
+file_put_contents($alphaRoot . '/index.php', $script);
+file_put_contents($betaRoot . '/index.php', $script);
+file_put_contents($root . '/php.ini', implode("\n", [
+ 'user_cache.enable=1',
+ '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') ?: '';
+$ldPreload = getenv('TEST_PHP_APACHE2HANDLER_LD_PRELOAD');
+$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 {
+ $env = null;
+ if ($ldPreload !== false && $ldPreload !== '') {
+ /* Sanitizer builds need the runtime preloaded by httpd, but not by the PHP test runner. */
+ $env = getenv();
+ $env['LD_PRELOAD'] = $ldPreload;
+ }
+
+ $process = proc_open(
+ [$httpd, '-X', '-f', $conf],
+ [['pipe', 'r'], ['pipe', 'w'], ['pipe', 'w']],
+ $pipes,
+ $root,
+ $env
+ );
+ 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/user_cache/tests/user_cache_array_int_keys.phpt b/ext/user_cache/tests/user_cache_array_int_keys.phpt
new file mode 100644
index 000000000000..c505af5c11c7
--- /dev/null
+++ b/ext/user_cache/tests/user_cache_array_int_keys.phpt
@@ -0,0 +1,94 @@
+--TEST--
+UserCache\Cache: negative, sparse, and post-unset integer array keys round-trip exactly
+--EXTENSIONS--
+user_cache
+--INI--
+user_cache.enable=1
+user_cache.enable_cli=1
+opcache.file_cache_only=0
+user_cache.shm_size=16M
+--FILE--
+ 'a', -1 => 'b', 0 => 'c', -100 => 'd', 7 => 'e'];
+$cache->store('negative', $negative);
+$fetched = $cache->fetch('negative');
+ok('negative keys value', $fetched === $negative);
+ok('negative keys order', array_keys($fetched) === [-5, -1, 0, -100, 7]);
+
+$sparse = [0 => 'x', 5 => 'y', 10 => 'z'];
+$cache->store('sparse', $sparse);
+$fetched = $cache->fetch('sparse');
+ok('sparse keys value', $fetched === $sparse);
+ok('sparse not list', !array_is_list($fetched));
+
+$packed = [10, 20, 30, 40];
+unset($packed[1]);
+$cache->store('unset', $packed);
+$fetched = $cache->fetch('unset');
+ok('post-unset keys', array_keys($fetched) === [0, 2, 3]);
+ok('post-unset value', $fetched === $packed);
+$fetched[] = 99;
+ok('append after unset uses key 4', array_key_last($fetched) === 4);
+
+$list = ['p', 'q', 'r'];
+$cache->store('list', $list);
+$fetched = $cache->fetch('list');
+ok('list is list', array_is_list($fetched));
+$fetched[] = 's';
+ok('list append index 3', array_key_last($fetched) === 3);
+
+$numeric = ['123' => 'a', '007' => 'b', '-3' => 'c', 'x' => 'd'];
+$cache->store('numeric', $numeric);
+$fetched = $cache->fetch('numeric');
+ok('numeric string key value', $fetched === $numeric);
+ok('numeric string key coerced', array_key_first($fetched) === 123 && array_key_exists(-3, $fetched));
+
+$limits = [PHP_INT_MAX => 'max', PHP_INT_MIN => 'min', 0 => 'zero'];
+$cache->store('limits', $limits);
+$fetched = $cache->fetch('limits');
+ok('int limit keys', $fetched[PHP_INT_MAX] === 'max' && $fetched[PHP_INT_MIN] === 'min' && $fetched[0] === 'zero');
+
+$transition = [0 => 'a', 1 => 'b'];
+$transition[100] = 'c';
+$cache->store('transition', $transition);
+$fetched = $cache->fetch('transition');
+ok('packed to hashed value', $fetched === $transition);
+ok('packed to hashed keys', array_keys($fetched) === [0, 1, 100]);
+
+$nested = [
+ 'neg' => $negative,
+ 'sparse' => $sparse,
+ 'list' => ['deep' => [-2 => 'q', 4 => 'r']],
+ 9 => [PHP_INT_MAX => 'z'],
+];
+$cache->store('nested', $nested);
+ok('nested mixed keys', $cache->fetch('nested') === $nested);
+
+$all = [$negative, $sparse, $packed, $list, $numeric, $limits, $transition, $nested];
+$cache->store('all', $all);
+ok('serialize parity', serialize($cache->fetch('all')) === serialize($all));
+?>
+--EXPECT--
+negative keys value: OK
+negative keys order: OK
+sparse keys value: OK
+sparse not list: OK
+post-unset keys: OK
+post-unset value: OK
+append after unset uses key 4: OK
+list is list: OK
+list append index 3: OK
+numeric string key value: OK
+numeric string key coerced: OK
+int limit keys: OK
+packed to hashed value: OK
+packed to hashed keys: OK
+nested mixed keys: OK
+serialize parity: OK
diff --git a/ext/user_cache/tests/user_cache_atomic.phpt b/ext/user_cache/tests/user_cache_atomic.phpt
new file mode 100644
index 000000000000..fa5320121497
--- /dev/null
+++ b/ext/user_cache/tests/user_cache_atomic.phpt
@@ -0,0 +1,82 @@
+--TEST--
+UserCache\Cache: atomic increment and decrement
+--EXTENSIONS--
+user_cache
+--INI--
+user_cache.enable=1
+user_cache.enable_cli=1
+opcache.file_cache_only=0
+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'));
+try {
+ $cache->increment('string');
+} catch (\ValueError $e) {
+ echo $e->getMessage(), "\n";
+}
+try {
+ $cache->decrement('string');
+} catch (\ValueError $e) {
+ echo $e->getMessage(), "\n";
+}
+var_dump($cache->fetch('string'));
+
+var_dump($cache->store('max', PHP_INT_MAX));
+try {
+ $cache->increment('max');
+} catch (\ArithmeticError $e) {
+ echo $e->getMessage(), "\n";
+}
+var_dump($cache->fetch('max') === PHP_INT_MAX);
+
+var_dump($cache->store('min', PHP_INT_MIN));
+try {
+ $cache->decrement('min');
+} catch (\ArithmeticError $e) {
+ echo $e->getMessage(), "\n";
+}
+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)
+Increment of user cache key "string" requires the stored value to be an integer
+Decrement of user cache key "string" requires the stored value to be an integer
+string(1) "x"
+bool(true)
+Increment of user cache key "max" would exceed the range of a PHP integer
+bool(true)
+bool(true)
+Decrement of user cache key "min" would exceed the range of a PHP integer
+bool(true)
+UserCache\Cache::increment(): Argument #2 ($step) must be greater than or equal to 0
+UserCache\Cache::decrement(): Argument #2 ($step) must be greater than or equal to 0
diff --git a/ext/user_cache/tests/user_cache_basic.phpt b/ext/user_cache/tests/user_cache_basic.phpt
new file mode 100644
index 000000000000..925e59ae8347
--- /dev/null
+++ b/ext/user_cache/tests/user_cache_basic.phpt
@@ -0,0 +1,92 @@
+--TEST--
+UserCache\Cache: basic API
+--EXTENSIONS--
+user_cache
+--INI--
+user_cache.enable=1
+user_cache.enable_cli=1
+opcache.file_cache_only=0
+user_cache.shm_size=16M
+--FILE--
+getPoolStatus();
+var_dump($poolStatus->getPoolName(), $status->getAvailability()->name, $poolStatus->getEntryCount());
+
+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->delete('never-stored'));
+var_dump($cache->deleteMultiple(['never-stored', 'also-missing']));
+
+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"
+string(9) "Available"
+int(0)
+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(true)
+bool(true)
+bool(false)
diff --git a/ext/user_cache/tests/user_cache_cgi.phpt b/ext/user_cache/tests/user_cache_cgi.phpt
new file mode 100644
index 000000000000..907b756c3209
--- /dev/null
+++ b/ext/user_cache/tests/user_cache_cgi.phpt
@@ -0,0 +1,31 @@
+--TEST--
+CGI: UserCache\Cache is available
+--CGI--
+--EXTENSIONS--
+user_cache
+--INI--
+user_cache.enable=1
+opcache.file_cache_only=0
+user_cache.shm_size=16M
+--ENV--
+DOCUMENT_ROOT=/tmp/php-user-cache-cgi
+SERVER_NAME=php-user-user_cache.local
+--FILE--
+getPoolStatus();
+
+var_dump(php_sapi_name());
+var_dump($status->getAvailability()->name, $poolStatus->getEntryCount());
+var_dump($cache->store('key', 'value'));
+var_dump($cache->fetch('key', 'MISS'));
+?>
+--EXPECT--
+string(8) "cgi-fcgi"
+string(9) "Available"
+int(0)
+bool(true)
+string(5) "value"
diff --git a/ext/user_cache/tests/user_cache_cgi_boundary.phpt b/ext/user_cache/tests/user_cache_cgi_boundary.phpt
new file mode 100644
index 000000000000..ceb43e238374
--- /dev/null
+++ b/ext/user_cache/tests/user_cache_cgi_boundary.phpt
@@ -0,0 +1,309 @@
+--TEST--
+CGI/FastCGI: UserCache\Cache partitions cache data by boundary
+--EXTENSIONS--
+user_cache
+--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'
+clear();
+} elseif ($action === 'seed') {
+ $cache->store($key, $host . '-value');
+ $cache->store($complexKey, ['host' => $host, 'nested' => ['value' => 42]]);
+}
+
+$status = UserCache\Cache::getStatus();
+$complex = $cache->fetch($complexKey, ['host' => 'MISS', 'nested' => ['value' => 'MISS']]);
+echo $host, ':', $cache->fetch($key, 'MISS'), ':', $complex['host'], ':', $complex['nested']['value'], ':', ($status->getAvailability() === UserCache\CacheAvailability::Available ? 'available' : $status->getAvailability()->name), "\n";
+PHP;
+
+file_put_contents($alphaRoot . '/index.php', $script);
+file_put_contents($betaRoot . '/index.php', $script);
+file_put_contents($root . '/php.ini', implode("\n", [
+ 'user_cache.enable=1',
+ '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');
+ user_cache_cgi_request($port, $alphaScript, $alphaRoot, 'alpha.local', 'action=clear');
+ user_cache_cgi_request($port, $betaScript, $betaRoot, 'beta.local', 'action=clear');
+
+ $checks = [
+ [$alphaScript, $alphaRoot, 'alpha.local', 'action=seed', 'alpha.local:alpha.local-value:alpha.local:42:available'],
+ [$alphaScript, $alphaRoot, 'alpha.local', 'action=fetch', 'alpha.local:alpha.local-value:alpha.local:42:available'],
+ [$betaScript, $betaRoot, 'beta.local', 'action=fetch', 'beta.local:MISS:MISS:MISS:available'],
+ [$betaScript, $betaRoot, 'beta.local', 'action=seed', 'beta.local:beta.local-value:beta.local:42:available'],
+ [$alphaScript, $alphaRoot, 'alpha.local', 'action=fetch', 'alpha.local:alpha.local-value:alpha.local:42:available'],
+ [$betaScript, $betaRoot, 'beta.local', 'action=fetch', 'beta.local:beta.local-value:beta.local:42: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/user_cache/tests/user_cache_cgi_no_boundary.phpt b/ext/user_cache/tests/user_cache_cgi_no_boundary.phpt
new file mode 100644
index 000000000000..557000863f30
--- /dev/null
+++ b/ext/user_cache/tests/user_cache_cgi_no_boundary.phpt
@@ -0,0 +1,32 @@
+--TEST--
+CGI: UserCache\Cache is unavailable without a cache boundary
+--CGI--
+--EXTENSIONS--
+user_cache
+--INI--
+user_cache.enable=1
+opcache.file_cache_only=0
+user_cache.shm_size=16M
+--ENV--
+DOCUMENT_ROOT=
+SERVER_NAME=
+--FILE--
+getPoolStatus();
+
+var_dump($status->getAvailability()->name, $poolStatus->getEntryCount(), $poolStatus->getEntryKeys(), $poolStatus->getUsedMemory());
+var_dump($cache->store('key', 'value'));
+var_dump($cache->fetch('key', 'MISS'));
+var_dump($cache->delete('key'));
+?>
+--EXPECT--
+string(31) "UnavailableByCgiFastCgiBoundary"
+int(0)
+array(0) {
+}
+int(0)
+bool(false)
+string(4) "MISS"
+bool(true)
diff --git a/ext/user_cache/tests/user_cache_cgi_value_matrix.phpt b/ext/user_cache/tests/user_cache_cgi_value_matrix.phpt
new file mode 100644
index 000000000000..6508be52c907
--- /dev/null
+++ b/ext/user_cache/tests/user_cache_cgi_value_matrix.phpt
@@ -0,0 +1,424 @@
+--TEST--
+CGI/FastCGI: UserCache\Cache round-trips a rich value matrix across the cross-process named segment
+--EXTENSIONS--
+user_cache
+--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=warmup');
+ 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-matrix-' . getmypid();
+$docRoot = $root . '/site';
+$process = null;
+$pipes = [];
+
+user_cache_cgi_rm_rf($root);
+mkdir($docRoot, 0777, true);
+
+/* Compare values across processes through native serialization. */
+$script = <<<'PHP'
+ $this->state];
+ }
+
+ public function __unserialize(array $data): void
+ {
+ $this->state = $data['state'];
+ }
+}
+
+class MatrixSleep
+{
+ public int $kept = 5;
+ public string $dropped = 'gone';
+
+ public function __sleep(): array
+ {
+ return ['kept'];
+ }
+
+ public function __wakeup(): void
+ {
+ }
+}
+
+class MatrixLegacy implements Serializable
+{
+ public function __construct(public int $v = 0)
+ {
+ }
+
+ public function serialize(): string
+ {
+ return (string) $this->v;
+ }
+
+ public function unserialize(string $data): void
+ {
+ $this->v = (int) $data;
+ }
+}
+
+function matrix_build(): array
+{
+ $shared = new stdClass();
+ $shared->tag = 'shared';
+
+ $cycleA = new stdClass();
+ $cycleB = new stdClass();
+ $cycleA->peer = $cycleB;
+ $cycleB->peer = $cycleA;
+
+ $ref = 1;
+ $withRef = ['a' => &$ref, 'b' => &$ref];
+
+ $storage = new SplObjectStorage();
+ $storageKey = new stdClass();
+ $storageKey->id = 1;
+ $storage[$storageKey] = ['data' => [1, 2, 3]];
+
+ $stack = new SplStack();
+ $stack->push('a');
+ $stack->push('b');
+
+ return [
+ 'scalars' => [null, true, PHP_INT_MIN, PHP_INT_MAX, 1.5, 'str', "bin\0\xff\xfe"],
+ 'arrays' => ['packed' => [1, 2, 3], 'hashed' => ['a' => 1, -5 => 'neg'], 'nested' => ['x' => ['y' => [1, 2]]]],
+ 'refs' => $withRef,
+ 'cycle' => $cycleA,
+ 'plain' => new MatrixPlain(),
+ 'enum' => MatrixSuit::Spades,
+ 'magic' => new MatrixMagic(['id' => 7, 'items' => [1, 2, 3]]),
+ 'sleep' => new MatrixSleep(),
+ 'legacy' => new MatrixLegacy(99),
+ 'date' => new DateTimeImmutable('2026-07-09 12:34:56.123456', new DateTimeZone('Asia/Tokyo')),
+ 'interval' => new DateInterval('P1Y2DT3H'),
+ 'period' => new DatePeriod(new DateTimeImmutable('2026-01-01'), new DateInterval('P1D'), 2),
+ 'stack' => $stack,
+ 'arrayobject' => new ArrayObject(['k' => 'v', 'n' => [1, 2]]),
+ 'storage' => $storage,
+ 'sharedid' => [$shared, $shared],
+ ];
+}
+
+if ($action === 'seed') {
+ foreach (matrix_build() as $key => $value) {
+ if (!$cache->store('m_' . $key, $value)) {
+ echo 'seed-fail:', $key, "\n";
+ exit;
+ }
+ }
+ echo "seeded:ok\n";
+ exit;
+}
+
+if ($action === 'warmup') {
+ echo "warmup:ok\n";
+ exit;
+}
+
+$mismatch = [];
+foreach (matrix_build() as $key => $expected) {
+ $missing = new stdClass();
+ $fetched = $cache->fetch('m_' . $key, $missing);
+ if ($fetched === $missing) {
+ $mismatch[] = $key . ':MISS';
+ continue;
+ }
+ if (serialize($fetched) !== serialize($expected)) {
+ $mismatch[] = $key . ':DIFF';
+ }
+}
+
+echo $mismatch === [] ? "verify:ok\n" : ('verify:FAIL ' . implode(',', $mismatch) . "\n");
+PHP;
+
+file_put_contents($docRoot . '/index.php', $script);
+file_put_contents($root . '/php.ini', implode("\n", [
+ 'user_cache.enable=1',
+ 'user_cache.shm_size=32M',
+ 'opcache.file_update_protection=0',
+ 'date.timezone=UTC',
+ 'error_reporting=E_ALL & ~E_DEPRECATED',
+ 'display_errors=0',
+]));
+
+try {
+ $phpCgi = user_cache_cgi_binary();
+ $port = user_cache_cgi_free_port();
+ $scriptFile = $docRoot . '/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, $scriptFile, $docRoot, 'matrix.local');
+
+ $checks = [
+ [$scriptFile, $docRoot, 'matrix.local', 'action=seed', 'seeded:ok'],
+ [$scriptFile, $docRoot, 'matrix.local', 'action=verify', 'verify:ok'],
+ ];
+
+ foreach ($checks as [$file, $root2, $serverName, $query, $expected]) {
+ $actual = user_cache_cgi_request($port, $file, $root2, $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/user_cache/tests/user_cache_clear_pool_scope.phpt b/ext/user_cache/tests/user_cache_clear_pool_scope.phpt
new file mode 100644
index 000000000000..9160b30112d4
--- /dev/null
+++ b/ext/user_cache/tests/user_cache_clear_pool_scope.phpt
@@ -0,0 +1,41 @@
+--TEST--
+UserCache\Cache: clear() only affects its own pool
+--EXTENSIONS--
+user_cache
+--INI--
+user_cache.enable=1
+user_cache.enable_cli=1
+opcache.file_cache_only=0
+user_cache.shm_size=16M
+--FILE--
+store('shared-key', 'from-a'));
+var_dump($a->store('a-only', 1));
+var_dump($b->store('shared-key', 'from-b'));
+var_dump($b->store('b-only', 2));
+
+var_dump($a->clear());
+
+var_dump($a->has('shared-key'));
+var_dump($a->has('a-only'));
+var_dump($a->getPoolStatus()->getEntryCount());
+
+var_dump($b->fetch('shared-key'));
+var_dump($b->fetch('b-only'));
+var_dump($b->getPoolStatus()->getEntryCount());
+?>
+--EXPECT--
+bool(true)
+bool(true)
+bool(true)
+bool(true)
+bool(true)
+bool(false)
+bool(false)
+int(0)
+string(6) "from-b"
+int(2)
+int(2)
diff --git a/ext/user_cache/tests/user_cache_cross_container_reference.phpt b/ext/user_cache/tests/user_cache_cross_container_reference.phpt
new file mode 100644
index 000000000000..9af1501875ea
--- /dev/null
+++ b/ext/user_cache/tests/user_cache_cross_container_reference.phpt
@@ -0,0 +1,91 @@
+--TEST--
+UserCache\Cache: a single reference shared across array and object containers survives
+--EXTENSIONS--
+user_cache
+--INI--
+user_cache.enable=1
+user_cache.enable_cli=1
+opcache.file_cache_only=0
+user_cache.shm_size=16M
+--FILE--
+slot = &$value;
+$graph = ['inArray' => &$value, 'holder' => $holder];
+
+$cache->store('scalar', $graph);
+$fetched = $cache->fetch('scalar');
+ok('initial value', $fetched['inArray'] === 5 && $fetched['holder']->slot === 5);
+$fetched['inArray'] = 42;
+ok('array write reaches property', $fetched['holder']->slot === 42);
+$fetched['holder']->slot = 99;
+ok('property write reaches array', $fetched['inArray'] === 99);
+
+$other = $cache->fetch('scalar');
+ok('other fetch independent', $other['inArray'] === 5);
+
+class RefPair
+{
+ public $left;
+ public $right;
+}
+
+$shared = 1;
+$pair = new RefPair();
+$pair->left = &$shared;
+$pair->right = &$shared;
+$wide = ['cell' => &$shared, 'pair' => $pair];
+$cache->store('wide', $wide);
+$fetchedWide = $cache->fetch('wide');
+$fetchedWide['cell'] = 7;
+ok('wide alias left', $fetchedWide['pair']->left === 7);
+ok('wide alias right', $fetchedWide['pair']->right === 7);
+
+$object = new stdClass();
+$object->n = 0;
+$objRef = $object;
+$objHolder = new RefHolder();
+$objHolder->slot = &$objRef;
+$objGraph = ['ref' => &$objRef, 'holder' => $objHolder];
+$cache->store('object', $objGraph);
+$fetchedObj = $cache->fetch('object');
+ok('object alias identity', $fetchedObj['ref'] === $fetchedObj['holder']->slot);
+$fetchedObj['ref']->n = 5;
+ok('object shared mutation', $fetchedObj['holder']->slot->n === 5);
+
+class ArrayPropHolder
+{
+ public array $data = [];
+}
+$inner = 3;
+$aph = new ArrayPropHolder();
+$aph->data['a'] = &$inner;
+$aph->data['b'] = &$inner;
+$cache->store('array-prop', $aph);
+$fetchedAph = $cache->fetch('array-prop');
+$fetchedAph->data['a'] = 88;
+ok('reference inside object-array property', $fetchedAph->data['b'] === 88);
+?>
+--EXPECT--
+initial value: OK
+array write reaches property: OK
+property write reaches array: OK
+other fetch independent: OK
+wide alias left: OK
+wide alias right: OK
+object alias identity: OK
+object shared mutation: OK
+reference inside object-array property: OK
diff --git a/ext/user_cache/tests/user_cache_dateinterval_safe_direct.phpt b/ext/user_cache/tests/user_cache_dateinterval_safe_direct.phpt
new file mode 100644
index 000000000000..20da01f9e1b9
--- /dev/null
+++ b/ext/user_cache/tests/user_cache_dateinterval_safe_direct.phpt
@@ -0,0 +1,87 @@
+--TEST--
+UserCache\Cache: DateInterval safe-direct state round-trips (incl. subclasses)
+--EXTENSIONS--
+user_cache
+--INI--
+user_cache.enable=1
+user_cache.enable_cli=1
+opcache.file_cache_only=0
+user_cache.shm_size=16M
+date.timezone=UTC
+--FILE--
+label = $label;
+ $this->revision = $revision;
+ }
+
+ public function describe(): string
+ {
+ return $this->label . ':' . $this->revision;
+ }
+}
+
+$cache = UserCache\Cache::getPool('dateinterval-safe-direct');
+$cache->clear();
+
+/* --- spec interval: non-from_string, full field set --- */
+$spec = new DateInterval('P1Y2M3DT4H5M6S');
+$cache->store('spec', $spec);
+$s = $cache->fetch('spec');
+var_dump($s instanceof DateInterval);
+var_dump(serialize($s) === serialize($spec));
+var_dump($s->format('%y-%m-%d %h:%i:%s'));
+
+/* --- diff interval: carries computed days + invert --- */
+$diff = (new DateTimeImmutable('2026-01-01 00:00:00'))->diff(new DateTimeImmutable('2026-03-15 10:30:00'));
+$cache->store('diff', $diff);
+$d = $cache->fetch('diff');
+var_dump(serialize($d) === serialize($diff));
+var_dump($d->days);
+var_dump($d->invert);
+var_dump($d->format('%a days %h:%i'));
+
+/* --- inverted diff --- */
+$inv = (new DateTimeImmutable('2026-03-15'))->diff(new DateTimeImmutable('2026-01-01'));
+$cache->store('inv', $inv);
+var_dump($cache->fetch('inv')->invert);
+
+/* --- from_string interval --- */
+$fs = DateInterval::createFromDateString('2 days 4 hours');
+$cache->store('fs', $fs);
+$f = $cache->fetch('fs');
+var_dump(serialize($f) === serialize($fs));
+var_dump($f->format('%d %h'));
+
+/* --- subclass with extra state, no __serialize override => safe-direct --- */
+$tagged = new TaggedInterval('P10DT12H');
+$tagged->tag('window', 9);
+$cache->store('tagged', $tagged);
+$t = $cache->fetch('tagged');
+var_dump($t instanceof TaggedInterval);
+var_dump($t->label);
+var_dump($t->describe());
+var_dump($t->format('%d %h'));
+?>
+--EXPECT--
+bool(true)
+bool(true)
+string(11) "1-2-3 4:5:6"
+bool(true)
+int(73)
+int(0)
+string(13) "73 days 10:30"
+int(1)
+bool(true)
+string(3) "2 4"
+bool(true)
+string(6) "window"
+string(8) "window:9"
+string(5) "10 12"
diff --git a/ext/user_cache/tests/user_cache_dateperiod.phpt b/ext/user_cache/tests/user_cache_dateperiod.phpt
new file mode 100644
index 000000000000..a10f7ec930a7
--- /dev/null
+++ b/ext/user_cache/tests/user_cache_dateperiod.phpt
@@ -0,0 +1,80 @@
+--TEST--
+UserCache\Cache: DatePeriod round-trips through its serialization contract
+--EXTENSIONS--
+user_cache
+--INI--
+user_cache.enable=1
+user_cache.enable_cli=1
+opcache.file_cache_only=0
+user_cache.shm_size=16M
+date.timezone=UTC
+--FILE--
+format('Y-m-d');
+ }
+ return $dates;
+}
+
+/* Compare before iteration mutates the DatePeriod cursor. */
+$byRecurrences = new DatePeriod(new DateTimeImmutable('2026-01-01'), new DateInterval('P1D'), 3);
+$cache->store('recurrences', $byRecurrences);
+$fetched = $cache->fetch('recurrences');
+ok('recurrences instanceof', $fetched instanceof DatePeriod);
+ok('recurrences parity', serialize($fetched) === serialize($byRecurrences));
+ok('recurrences dates', period_dates($fetched) === ['2026-01-01', '2026-01-02', '2026-01-03', '2026-01-04']);
+
+$byEnd = new DatePeriod(
+ new DateTimeImmutable('2026-03-01'),
+ new DateInterval('P1M'),
+ new DateTimeImmutable('2026-05-15'),
+);
+$cache->store('end', $byEnd);
+$fetched = $cache->fetch('end');
+ok('end parity', serialize($fetched) === serialize($byEnd));
+ok('end dates', period_dates($fetched) === ['2026-03-01', '2026-04-01', '2026-05-01']);
+
+$excludeStart = new DatePeriod(
+ new DateTimeImmutable('2026-01-01'),
+ new DateInterval('P1D'),
+ 2,
+ DatePeriod::EXCLUDE_START_DATE,
+);
+$cache->store('exclude', $excludeStart);
+$fetched = $cache->fetch('exclude');
+ok('exclude-start parity', serialize($fetched) === serialize($excludeStart));
+ok('exclude-start dates', period_dates($fetched) === ['2026-01-02', '2026-01-03']);
+
+$interval = new DateInterval('P2D');
+$graph = [
+ 'period' => new DatePeriod(new DateTimeImmutable('2026-06-01'), $interval, 2),
+ 'interval' => $interval,
+ 'zone' => new DateTimeZone('Asia/Tokyo'),
+];
+$cache->store('graph', $graph);
+$fetched = $cache->fetch('graph');
+ok('graph parity', serialize($fetched) === serialize($graph));
+ok('nested components', $fetched['interval'] instanceof DateInterval && $fetched['zone']->getName() === 'Asia/Tokyo');
+ok('nested period dates', period_dates($fetched['period']) === ['2026-06-01', '2026-06-03', '2026-06-05']);
+?>
+--EXPECT--
+recurrences instanceof: OK
+recurrences parity: OK
+recurrences dates: OK
+end parity: OK
+end dates: OK
+exclude-start parity: OK
+exclude-start dates: OK
+graph parity: OK
+nested components: OK
+nested period dates: OK
diff --git a/ext/user_cache/tests/user_cache_dateperiod_safe_direct.phpt b/ext/user_cache/tests/user_cache_dateperiod_safe_direct.phpt
new file mode 100644
index 000000000000..e2c341499c23
--- /dev/null
+++ b/ext/user_cache/tests/user_cache_dateperiod_safe_direct.phpt
@@ -0,0 +1,156 @@
+--TEST--
+UserCache\Cache: DatePeriod safe-direct state round-trips (incl. subclasses)
+--EXTENSIONS--
+user_cache
+--INI--
+user_cache.enable=1
+user_cache.enable_cli=1
+opcache.file_cache_only=0
+user_cache.shm_size=16M
+date.timezone=UTC
+--FILE--
+label = $label;
+ $this->revision = $revision;
+ }
+
+ public function describe(): string
+ {
+ return $this->label . ':' . $this->revision;
+ }
+}
+
+class MagicDatePeriod extends DatePeriod
+{
+ public static int $serializeCount = 0;
+ public static int $unserializeCount = 0;
+
+ public string $note = 'none';
+
+ public function __serialize(): array
+ {
+ self::$serializeCount++;
+
+ return parent::__serialize() + ['note' => 'serialized-' . $this->note];
+ }
+
+ public function __unserialize(array $data): void
+ {
+ self::$unserializeCount++;
+ parent::__unserialize($data);
+ $this->note = $data['note'];
+ }
+}
+
+function period_dates(DatePeriod $period): array
+{
+ $dates = [];
+ foreach ($period as $date) {
+ $dates[] = $date->format('Y-m-d');
+ }
+ return $dates;
+}
+
+$cache = UserCache\Cache::getPool('dateperiod-safe-direct');
+$cache->clear();
+
+/* --- recurrences-based --- */
+$byRecurrences = new DatePeriod(new DateTimeImmutable('2026-01-01'), new DateInterval('P1D'), 3);
+$cache->store('recurrences', $byRecurrences);
+$r = $cache->fetch('recurrences');
+var_dump($r instanceof DatePeriod);
+var_dump(serialize($r) === serialize($byRecurrences));
+var_dump(period_dates($r) === ['2026-01-01', '2026-01-02', '2026-01-03', '2026-01-04']);
+
+/* --- end-based --- */
+$byEnd = new DatePeriod(
+ new DateTimeImmutable('2026-03-01'),
+ new DateInterval('P1M'),
+ new DateTimeImmutable('2026-05-15'),
+);
+$cache->store('end', $byEnd);
+var_dump(serialize($cache->fetch('end')) === serialize($byEnd));
+
+/* --- EXCLUDE_START_DATE --- */
+$excludeStart = new DatePeriod(
+ new DateTimeImmutable('2026-01-01'),
+ new DateInterval('P1D'),
+ 2,
+ DatePeriod::EXCLUDE_START_DATE,
+);
+$cache->store('exclude-start', $excludeStart);
+var_dump(period_dates($cache->fetch('exclude-start')) === ['2026-01-02', '2026-01-03']);
+
+/* --- INCLUDE_END_DATE --- */
+$includeEnd = new DatePeriod(
+ new DateTimeImmutable('2026-01-01'),
+ new DateInterval('P1D'),
+ new DateTimeImmutable('2026-01-03'),
+ DatePeriod::INCLUDE_END_DATE,
+);
+$cache->store('include-end', $includeEnd);
+var_dump(serialize($cache->fetch('include-end')) === serialize($includeEnd));
+var_dump(period_dates($cache->fetch('include-end')) === ['2026-01-01', '2026-01-02', '2026-01-03']);
+
+/* --- subclass with extra state, no __serialize override => safe-direct --- */
+$tagged = new TaggedDatePeriod(new DateTimeImmutable('2026-02-01'), new DateInterval('P1D'), 2);
+$tagged->tag('window', 9);
+$cache->store('tagged', $tagged);
+$t = $cache->fetch('tagged');
+var_dump($t instanceof TaggedDatePeriod);
+var_dump($t->label);
+var_dump($t->describe());
+var_dump(period_dates($t) === ['2026-02-01', '2026-02-02', '2026-02-03']);
+
+/* --- subclass overriding __serialize => magic hooks route, still round-trips --- */
+$magic = new MagicDatePeriod(new DateTimeImmutable('2026-04-01'), new DateInterval('P1D'), 1);
+$magic->note = 'hello';
+$cache->store('magic', $magic);
+$m = $cache->fetch('magic');
+var_dump($m instanceof MagicDatePeriod);
+var_dump($m->note);
+var_dump(period_dates($m) === ['2026-04-01', '2026-04-02']);
+var_dump(MagicDatePeriod::$serializeCount > 0);
+var_dump(MagicDatePeriod::$unserializeCount > 0);
+
+/* --- shared identity: interval reused across the graph --- */
+$interval = new DateInterval('P2D');
+$graph = [
+ 'period' => new DatePeriod(new DateTimeImmutable('2026-06-01'), $interval, 2),
+ 'interval' => $interval,
+ 'zone' => new DateTimeZone('Asia/Tokyo'),
+];
+$cache->store('graph', $graph);
+$g = $cache->fetch('graph');
+var_dump(serialize($g) === serialize($graph));
+var_dump($g['interval'] instanceof DateInterval && $g['zone']->getName() === 'Asia/Tokyo');
+var_dump(period_dates($g['period']) === ['2026-06-01', '2026-06-03', '2026-06-05']);
+?>
+--EXPECT--
+bool(true)
+bool(true)
+bool(true)
+bool(true)
+bool(true)
+bool(true)
+bool(true)
+bool(true)
+string(6) "window"
+string(8) "window:9"
+bool(true)
+bool(true)
+string(5) "hello"
+bool(true)
+bool(true)
+bool(true)
+bool(true)
+bool(true)
+bool(true)
diff --git a/ext/user_cache/tests/user_cache_datetime_safe_direct.phpt b/ext/user_cache/tests/user_cache_datetime_safe_direct.phpt
new file mode 100644
index 000000000000..bcc2ad0bea07
--- /dev/null
+++ b/ext/user_cache/tests/user_cache_datetime_safe_direct.phpt
@@ -0,0 +1,353 @@
+--TEST--
+UserCache\Cache: DateTime safe-direct state is restored for subclasses
+--EXTENSIONS--
+user_cache
+--INI--
+user_cache.enable=1
+user_cache.enable_cli=1
+opcache.file_cache_only=0
+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 UserCacheMagicSelfHashDateTime extends DateTime
+{
+ public static int $serializeCount = 0;
+ public static int $unserializeCount = 0;
+
+ private string $constructedObjectId;
+
+ public function __construct(string $time, DateTimeZone $timezone)
+ {
+ parent::__construct($time, $timezone);
+ $this->constructedObjectId = spl_object_hash($this);
+ }
+
+ public function __serialize(): array
+ {
+ self::$serializeCount++;
+
+ return parent::__serialize() + ['constructedObjectId' => $this->constructedObjectId];
+ }
+
+ public function __unserialize(array $data): void
+ {
+ self::$unserializeCount++;
+ parent::__unserialize($data);
+ $this->constructedObjectId = spl_object_hash($this);
+ }
+
+ public function constructedObjectId(): string
+ {
+ return $this->constructedObjectId;
+ }
+}
+
+class UserCacheMagicFilteredDateTime extends DateTime
+{
+ public static int $serializeCount = 0;
+ public static int $unserializeCount = 0;
+
+ public Closure $hidden;
+ private string $label;
+
+ public function __construct(string $time, DateTimeZone $timezone, string $label)
+ {
+ parent::__construct($time, $timezone);
+ $this->hidden = static fn(): int => 1;
+ $this->label = $label;
+ }
+
+ public function __serialize(): array
+ {
+ self::$serializeCount++;
+ $timezone = $this->getTimezone();
+
+ return [
+ 'date' => $this->format('Y-m-d H:i:s.u'),
+ 'timezone_type' => 3,
+ 'timezone' => $timezone->getName(),
+ 'label' => $this->label,
+ ];
+ }
+
+ public function __unserialize(array $data): void
+ {
+ self::$unserializeCount++;
+ parent::__unserialize($data);
+ $this->hidden = static fn(): int => 2;
+ $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 = UserCache\Cache::getPool('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'),
+ 'magicSelfHash' => new UserCacheMagicSelfHashDateTime('2026-06-15 11:45:00.111111', new DateTimeZone('UTC')),
+ 'magicFiltered' => new UserCacheMagicFilteredDateTime('2026-06-15 12:00:00.222222', new DateTimeZone('UTC'), 'filtered'),
+ '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() === $payload['selfHash']->cachedSelfHash());
+var_dump($fetched['selfHash']->cachedSelfHash() !== spl_object_hash($fetched['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['magicSelfHash'] instanceof UserCacheMagicSelfHashDateTime);
+var_dump($fetched['magicSelfHash']->format('Y-m-d H:i:s.u e'));
+var_dump($fetched['magicSelfHash']->constructedObjectId() === spl_object_hash($fetched['magicSelfHash']));
+var_dump($fetched['magicSelfHash']->constructedObjectId() !== spl_object_hash($payload['magicSelfHash']));
+var_dump(UserCacheMagicSelfHashDateTime::$serializeCount);
+var_dump(UserCacheMagicSelfHashDateTime::$unserializeCount);
+
+var_dump($fetched['magicFiltered'] instanceof UserCacheMagicFilteredDateTime);
+var_dump($fetched['magicFiltered']->format('Y-m-d H:i:s.u e'));
+var_dump($fetched['magicFiltered']->label());
+var_dump(($fetched['magicFiltered']->hidden)());
+var_dump(UserCacheMagicFilteredDateTime::$serializeCount);
+var_dump(UserCacheMagicFilteredDateTime::$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(16) "serialized-magic"
+int(1)
+int(1)
+bool(true)
+string(30) "2026-06-15 11:45:00.111111 UTC"
+bool(true)
+bool(true)
+int(1)
+int(1)
+bool(true)
+string(30) "2026-06-15 12:00:00.222222 UTC"
+string(8) "filtered"
+int(2)
+int(1)
+int(1)
+bool(true)
+string(30) "2026-06-15 12:15:00.987654 UTC"
+string(7) "wakeful"
+int(0)
+int(0)
diff --git a/ext/user_cache/tests/user_cache_default_valued_property_alias.phpt b/ext/user_cache/tests/user_cache_default_valued_property_alias.phpt
new file mode 100644
index 000000000000..d790c43680e3
--- /dev/null
+++ b/ext/user_cache/tests/user_cache_default_valued_property_alias.phpt
@@ -0,0 +1,31 @@
+--TEST--
+UserCache\Cache: default-valued properties are stored when they carry aliases
+--EXTENSIONS--
+user_cache
+--INI--
+user_cache.enable=1
+user_cache.enable_cli=1
+opcache.file_cache_only=0
+user_cache.shm_size=16M
+--FILE--
+left =& $object->right;
+
+var_dump($cache->store('object', $object));
+
+$fetched = $cache->fetch('object');
+$fetched->left = 99;
+var_dump($fetched->left, $fetched->right);
+?>
+--EXPECT--
+bool(true)
+int(99)
+int(99)
diff --git a/ext/user_cache/tests/user_cache_disabled.phpt b/ext/user_cache/tests/user_cache_disabled.phpt
new file mode 100644
index 000000000000..6621e4762c6f
--- /dev/null
+++ b/ext/user_cache/tests/user_cache_disabled.phpt
@@ -0,0 +1,61 @@
+--TEST--
+UserCache\Cache: disabled by INI size
+--EXTENSIONS--
+user_cache
+--INI--
+user_cache.enable=1
+user_cache.enable_cli=1
+opcache.file_cache_only=0
+user_cache.shm_size=0
+--FILE--
+getPoolStatus();
+var_dump($poolStatus->getPoolName(), $status->getAvailability()->name, $poolStatus->getEntryCount(), $poolStatus->getEntryKeys(), $poolStatus->getUsedMemory());
+var_dump($status->getConfiguredMemory());
+var_dump($cache->store('key', 1));
+var_dump($cache->add('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));
+
+var_dump($status->getAvailability());
+?>
+--EXPECT--
+string(3) "off"
+string(13) "DisabledByIni"
+int(0)
+array(0) {
+}
+int(0)
+int(0)
+bool(false)
+bool(false)
+bool(false)
+NULL
+NULL
+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)
+enum(UserCache\CacheAvailability::DisabledByIni)
diff --git a/ext/user_cache/tests/user_cache_empty_array_roundtrip.phpt b/ext/user_cache/tests/user_cache_empty_array_roundtrip.phpt
new file mode 100644
index 000000000000..150427e449d0
--- /dev/null
+++ b/ext/user_cache/tests/user_cache_empty_array_roundtrip.phpt
@@ -0,0 +1,98 @@
+--TEST--
+UserCache\Cache: empty arrays round-trip from the lazy (unallocated) table
+--EXTENSIONS--
+user_cache
+--INI--
+user_cache.enable=1
+user_cache.enable_cli=1
+opcache.file_cache_only=0
+user_cache.shm_size=16M
+--FILE--
+store('root', []));
+var_dump($cache->fetch('root'));
+
+var_dump($cache->store('nested', ['a' => [], 'b' => [[], []], 'c' => ['x' => 1]]));
+var_dump($cache->fetch('nested'));
+
+$shared = [];
+$wrap = ['first' => &$shared, 'second' => &$shared];
+var_dump($cache->store('shared', $wrap));
+$fetched = $cache->fetch('shared');
+$fetched['first'][] = 'linked';
+var_dump($fetched['second']);
+
+$root = new UserCacheEmptyArrayNode('root');
+$leaf = new UserCacheEmptyArrayNode('leaf');
+$leaf->parent = $root;
+$root->children[] = $leaf;
+var_dump($cache->store('graph', $root));
+$graph = $cache->fetch('graph');
+var_dump($graph->children[0]->children);
+var_dump($graph->children[0]->parent === $graph);
+
+$empty = $cache->fetch('root');
+$empty[] = 42;
+var_dump($empty);
+
+unset($shared, $wrap, $fetched, $root, $leaf, $graph, $empty);
+gc_collect_cycles();
+?>
+--EXPECT--
+bool(true)
+array(0) {
+}
+bool(true)
+array(3) {
+ ["a"]=>
+ array(0) {
+ }
+ ["b"]=>
+ array(2) {
+ [0]=>
+ array(0) {
+ }
+ [1]=>
+ array(0) {
+ }
+ }
+ ["c"]=>
+ array(1) {
+ ["x"]=>
+ int(1)
+ }
+}
+bool(true)
+array(1) {
+ [0]=>
+ string(6) "linked"
+}
+bool(true)
+array(0) {
+}
+bool(true)
+array(1) {
+ [0]=>
+ int(42)
+}
diff --git a/ext/user_cache/tests/user_cache_expired_read_reclaim.phpt b/ext/user_cache/tests/user_cache_expired_read_reclaim.phpt
new file mode 100644
index 000000000000..568b3c851c55
--- /dev/null
+++ b/ext/user_cache/tests/user_cache_expired_read_reclaim.phpt
@@ -0,0 +1,44 @@
+--TEST--
+UserCache\Cache: expired entries observed by reads are reclaimed by a later mutation
+--EXTENSIONS--
+user_cache
+--INI--
+user_cache.enable=1
+user_cache.enable_cli=1
+opcache.file_cache_only=0
+; Capacity fits inside one bounded expunge window, so a single mutation
+; reclaims every observed expired entry.
+user_cache.shm_size=1M
+--FILE--
+store('expiring-' . $i, str_repeat('v', 64), 1);
+}
+$cache->store('persistent', 'kept');
+
+var_dump($cache->getPoolStatus()->getEntryCount());
+
+sleep(2);
+
+/* Cross the observation threshold before triggering expiry cleanup. */
+for ($i = 0; $i < 80; $i++) {
+ if ($cache->fetch('expiring-' . $i, null) !== null) {
+ exit('unexpected hit');
+ }
+}
+
+$cache->store('trigger', 'mutation');
+
+var_dump($cache->getPoolStatus()->getEntryCount());
+var_dump($cache->fetch('persistent'));
+var_dump($cache->fetch('trigger'));
+?>
+--EXPECT--
+int(81)
+int(2)
+string(4) "kept"
+string(8) "mutation"
diff --git a/ext/user_cache/tests/user_cache_fetch_multiple_int_keys.phpt b/ext/user_cache/tests/user_cache_fetch_multiple_int_keys.phpt
new file mode 100644
index 000000000000..2080f7142723
--- /dev/null
+++ b/ext/user_cache/tests/user_cache_fetch_multiple_int_keys.phpt
@@ -0,0 +1,39 @@
+--TEST--
+UserCache\Cache: fetchMultiple normalizes integer keys
+--EXTENSIONS--
+user_cache
+--INI--
+user_cache.enable=1
+user_cache.enable_cli=1
+opcache.file_cache_only=0
+user_cache.shm_size=16M
+--FILE--
+storeMultiple([
+ 123 => 'int-key',
+ '456' => 'numeric-string-key',
+]));
+
+$result = $cache->fetchMultiple([123, '456', 789], 'default');
+var_dump($result);
+var_dump(array_key_exists(123, $result));
+var_dump(array_key_exists(456, $result));
+var_dump($result[123], $result[456], $result[789]);
+?>
+--EXPECT--
+bool(true)
+array(3) {
+ [123]=>
+ string(7) "int-key"
+ [456]=>
+ string(18) "numeric-string-key"
+ [789]=>
+ string(7) "default"
+}
+bool(true)
+bool(true)
+string(7) "int-key"
+string(18) "numeric-string-key"
+string(7) "default"
diff --git a/ext/user_cache/tests/user_cache_fetch_restore_exception.phpt b/ext/user_cache/tests/user_cache_fetch_restore_exception.phpt
new file mode 100644
index 000000000000..5857ff684c39
--- /dev/null
+++ b/ext/user_cache/tests/user_cache_fetch_restore_exception.phpt
@@ -0,0 +1,77 @@
+--TEST--
+UserCache\Cache: fetch/remember/fetchMultiple propagate restore-hook exceptions and keep the entry
+--EXTENSIONS--
+user_cache
+--INI--
+user_cache.enable=1
+user_cache.enable_cli=1
+opcache.file_cache_only=0
+user_cache.shm_size=16M
+--FILE--
+clear();
+
+class RestoreWakeupThrows
+{
+ public static bool $failNext = false;
+ public int $value = 0;
+
+ public function __wakeup(): void
+ {
+ if (self::$failNext) {
+ self::$failNext = false;
+ throw new RuntimeException('wakeup failure');
+ }
+ }
+}
+
+RestoreWakeupThrows::$failNext = false;
+
+var_dump($cache->store('k', new RestoreWakeupThrows()));
+
+/* fetch(): a throwing __wakeup propagates like native unserialize() and the
+ * entry is retained because it is not corrupt. */
+RestoreWakeupThrows::$failNext = true;
+try {
+ $cache->fetch('k', 'DEFAULT');
+ echo "no exception\n";
+} catch (RuntimeException $e) {
+ echo "fetch caught: ", $e->getMessage(), "\n";
+}
+var_dump($cache->has('k'));
+
+/* remember(): the entry exists, so the exception propagates instead of
+ * recomputing via the callback. */
+RestoreWakeupThrows::$failNext = true;
+try {
+ $cache->remember('k', fn (): string => 'computed');
+ echo "no exception\n";
+} catch (RuntimeException $e) {
+ echo "remember caught: ", $e->getMessage(), "\n";
+}
+var_dump($cache->has('k'));
+
+/* fetchMultiple(): a throwing restore aborts the whole batch. */
+RestoreWakeupThrows::$failNext = true;
+try {
+ $cache->fetchMultiple(['k'], 'DEFAULT');
+ echo "no exception\n";
+} catch (RuntimeException $e) {
+ echo "fetchMultiple caught: ", $e->getMessage(), "\n";
+}
+var_dump($cache->has('k'));
+
+/* Once the hook no longer throws, the retained entry restores normally. */
+$restored = $cache->fetch('k', 'DEFAULT');
+var_dump($restored instanceof RestoreWakeupThrows);
+?>
+--EXPECT--
+bool(true)
+fetch caught: wakeup failure
+bool(true)
+remember caught: wakeup failure
+bool(true)
+fetchMultiple caught: wakeup failure
+bool(true)
+bool(true)
diff --git a/ext/user_cache/tests/user_cache_float_precision.phpt b/ext/user_cache/tests/user_cache_float_precision.phpt
new file mode 100644
index 000000000000..52ce6ebce393
--- /dev/null
+++ b/ext/user_cache/tests/user_cache_float_precision.phpt
@@ -0,0 +1,83 @@
+--TEST--
+UserCache\Cache: floats round-trip bit-exactly (negative zero, subnormals, precision)
+--EXTENSIONS--
+user_cache
+--INI--
+user_cache.enable=1
+user_cache.enable_cli=1
+opcache.file_cache_only=0
+user_cache.shm_size=16M
+--FILE--
+ 0.0,
+ 'negative zero' => -0.0,
+ 'epsilon' => PHP_FLOAT_EPSILON,
+ 'float min' => PHP_FLOAT_MIN,
+ 'float max' => PHP_FLOAT_MAX,
+ 'smallest subnormal' => 4.9e-324,
+ 'one third' => 1 / 3,
+ 'point one plus point two' => 0.1 + 0.2,
+ 'large' => 1.7e308,
+ 'small negative' => -2.2250738585072014e-308,
+];
+
+foreach ($floats as $label => $value) {
+ $cache->store('f', $value);
+ $fetched = $cache->fetch('f');
+ ok($label, pack('d', $fetched) === pack('d', $value));
+}
+
+$cache->store('negzero', -0.0);
+ok('negative zero sign bit', fdiv(1.0, $cache->fetch('negzero')) === -INF);
+
+foreach (['inf' => INF, '-inf' => -INF] as $label => $value) {
+ $cache->store('nf', $value);
+ ok($label, $cache->fetch('nf') === $value);
+}
+$cache->store('nan', NAN);
+ok('nan', is_nan($cache->fetch('nan')));
+
+$graph = [
+ 'array' => [-0.0, PHP_FLOAT_MIN, 4.9e-324],
+ 'object' => (object) ['ratio' => 1 / 3, 'neg' => -0.0],
+];
+$cache->store('graph', $graph);
+$fetched = $cache->fetch('graph');
+$arrayOk = true;
+foreach ($graph['array'] as $index => $value) {
+ $arrayOk = $arrayOk && pack('d', $fetched['array'][$index]) === pack('d', $value);
+}
+ok('floats in array', $arrayOk);
+ok('floats in object', pack('d', $fetched['object']->ratio) === pack('d', 1 / 3)
+ && pack('d', $fetched['object']->neg) === pack('d', -0.0));
+
+$cache->store('all', $floats);
+ok('serialize parity', serialize($cache->fetch('all')) === serialize($floats));
+?>
+--EXPECT--
+positive zero: OK
+negative zero: OK
+epsilon: OK
+float min: OK
+float max: OK
+smallest subnormal: OK
+one third: OK
+point one plus point two: OK
+large: OK
+small negative: OK
+negative zero sign bit: OK
+inf: OK
+-inf: OK
+nan: OK
+floats in array: OK
+floats in object: OK
+serialize parity: OK
diff --git a/ext/user_cache/tests/user_cache_fork_entry_lock.phpt b/ext/user_cache/tests/user_cache_fork_entry_lock.phpt
new file mode 100644
index 000000000000..91fbd6d1f405
--- /dev/null
+++ b/ext/user_cache/tests/user_cache_fork_entry_lock.phpt
@@ -0,0 +1,88 @@
+--TEST--
+UserCache\Cache: forked child does not inherit parent entry lock ownership
+--EXTENSIONS--
+user_cache
+pcntl
+--INI--
+user_cache.enable=1
+user_cache.enable_cli=1
+opcache.file_cache_only=0
+user_cache.shm_size=16M
+--FILE--
+clear();
+
+$shared = range(1, 4);
+$value = [$shared, $shared, ['nested' => $shared]];
+
+var_dump($cache->store('graph', $value));
+var_dump($cache->lock('lock'));
+
+$pid = pcntl_fork();
+if ($pid === 0) {
+ echo "child unlock\n";
+ var_dump($cache->unlock('lock'));
+ echo "child lock\n";
+ var_dump($cache->lock('lock', 1));
+ echo "child fetch\n";
+ var_dump($cache->fetch('graph'));
+ exit(0);
+}
+
+if ($pid > 0) {
+ pcntl_waitpid($pid, $status);
+ echo "parent unlock\n";
+ var_dump($cache->unlock('lock'));
+} else {
+ echo "pcntl_fork() failed\n";
+}
+?>
+--EXPECT--
+bool(true)
+bool(true)
+child unlock
+bool(false)
+child lock
+bool(false)
+child fetch
+array(3) {
+ [0]=>
+ array(4) {
+ [0]=>
+ int(1)
+ [1]=>
+ int(2)
+ [2]=>
+ int(3)
+ [3]=>
+ int(4)
+ }
+ [1]=>
+ array(4) {
+ [0]=>
+ int(1)
+ [1]=>
+ int(2)
+ [2]=>
+ int(3)
+ [3]=>
+ int(4)
+ }
+ [2]=>
+ array(1) {
+ ["nested"]=>
+ array(4) {
+ [0]=>
+ int(1)
+ [1]=>
+ int(2)
+ [2]=>
+ int(3)
+ [3]=>
+ int(4)
+ }
+ }
+}
+parent unlock
+bool(true)
diff --git a/ext/user_cache/tests/user_cache_fuzz_roundtrip.phpt b/ext/user_cache/tests/user_cache_fuzz_roundtrip.phpt
new file mode 100644
index 000000000000..99f379f9c062
--- /dev/null
+++ b/ext/user_cache/tests/user_cache_fuzz_roundtrip.phpt
@@ -0,0 +1,438 @@
+--TEST--
+UserCache\Cache: deterministic store/fetch round-trip fuzz cases
+--EXTENSIONS--
+user_cache
+spl
+--INI--
+user_cache.enable=1
+user_cache.enable_cli=1
+opcache.file_cache_only=0
+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]);
+
+unset($payload, $before, $missing, $fetched, $after);
+gc_collect_cycles();
+?>
+--EXPECT--
+fuzz cases: 128
+magic calls: array(2) {
+ [0]=>
+ int(8)
+ [1]=>
+ int(8)
+}
diff --git a/ext/user_cache/tests/user_cache_info_counters.phpt b/ext/user_cache/tests/user_cache_info_counters.phpt
new file mode 100644
index 000000000000..f448565d9957
--- /dev/null
+++ b/ext/user_cache/tests/user_cache_info_counters.phpt
@@ -0,0 +1,35 @@
+--TEST--
+UserCache\Cache: expunge and store-failure counters are exposed
+--EXTENSIONS--
+user_cache
+--INI--
+user_cache.enable=1
+user_cache.enable_cli=1
+opcache.file_cache_only=0
+user_cache.shm_size=2M
+--FILE--
+clear();
+
+$info = UserCache\Cache::getStatus();
+$expungeBase = $info->getExpungeCount();
+$storeFailureBase = $info->getStoreFailureCount();
+
+/* Fill until pressure resets the partition. */
+$payload = str_repeat('x', 64 * 1024);
+for ($i = 0; $i < 64; $i++) {
+ $cache->store('big-' . $i, $payload . $i);
+}
+
+$info = UserCache\Cache::getStatus();
+var_dump($info->getExpungeCount() > $expungeBase);
+
+var_dump($cache->store('too-big', str_repeat('y', 4 * 1024 * 1024)));
+$info = UserCache\Cache::getStatus();
+var_dump($info->getStoreFailureCount() > $storeFailureBase);
+?>
+--EXPECT--
+bool(true)
+bool(false)
+bool(true)
diff --git a/ext/user_cache/tests/user_cache_info_reset.phpt b/ext/user_cache/tests/user_cache_info_reset.phpt
new file mode 100644
index 000000000000..79729ff5e93f
--- /dev/null
+++ b/ext/user_cache/tests/user_cache_info_reset.phpt
@@ -0,0 +1,73 @@
+--TEST--
+UserCache\Cache: info statistics and user cache reset
+--EXTENSIONS--
+user_cache
+--INI--
+user_cache.enable=1
+user_cache.enable_cli=1
+opcache.file_cache_only=0
+user_cache.shm_size=16M
+--FILE--
+clear();
+$other->clear();
+
+$initial = UserCache\Cache::getStatus();
+$initialPool = $cache->getPoolStatus();
+var_dump($initialPool->getPoolName());
+var_dump($initial->getAvailability()->name);
+var_dump($initial->getConfiguredMemory() === 16 * 1024 * 1024);
+var_dump($initial->getSharedMemorySize() > 0);
+var_dump($initial->getEntryCount());
+var_dump($initialPool->getEntryCount());
+
+var_dump($cache->store('key', ['value' => 1]));
+var_dump($other->store('key', 'other'));
+
+$afterStore = UserCache\Cache::getStatus();
+$afterStorePool = $cache->getPoolStatus();
+$afterStoreOther = $other->getPoolStatus();
+var_dump($afterStore->getEntryCount() >= 2);
+var_dump($afterStorePool->getEntryCount());
+var_dump($afterStoreOther->getEntryCount());
+var_dump($afterStore->getEntryCapacity() > 0);
+var_dump($afterStore->getUsedMemory() > 0);
+var_dump($afterStore->getFreeMemory() > 0);
+var_dump($afterStore->getWastedMemory() >= 0);
+var_dump($afterStore->getTombstoneCount() >= 0);
+var_dump($afterStorePool->getUsedMemory() > 0);
+
+var_dump(user_cache_reset());
+var_dump($cache->fetch('key', 'missing'));
+var_dump($other->fetch('key', 'missing'));
+
+$afterReset = UserCache\Cache::getStatus();
+$afterResetPool = $cache->getPoolStatus();
+var_dump($afterReset->getEntryCount());
+var_dump($afterResetPool->getEntryCount());
+?>
+--EXPECT--
+string(12) "info-reset-a"
+string(9) "Available"
+bool(true)
+bool(true)
+int(0)
+int(0)
+bool(true)
+bool(true)
+bool(true)
+int(1)
+int(1)
+bool(true)
+bool(true)
+bool(true)
+bool(true)
+bool(true)
+bool(true)
+bool(true)
+string(7) "missing"
+string(7) "missing"
+int(0)
+int(0)
diff --git a/ext/user_cache/tests/user_cache_invalid_keys.phpt b/ext/user_cache/tests/user_cache_invalid_keys.phpt
new file mode 100644
index 000000000000..95b8ea0dac5a
--- /dev/null
+++ b/ext/user_cache/tests/user_cache_invalid_keys.phpt
@@ -0,0 +1,44 @@
+--TEST--
+UserCache\Cache: invalid cache keys are rejected consistently
+--EXTENSIONS--
+user_cache
+--INI--
+user_cache.enable=1
+user_cache.enable_cli=1
+opcache.file_cache_only=0
+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 or int keys that do not contain 0x1F
+storeMultiple delimiter key: ValueError: Argument #1 ($values) must be an array with non-empty string or int keys that do not contain 0x1F
diff --git a/ext/user_cache/tests/user_cache_litespeed_boundary.phpt b/ext/user_cache/tests/user_cache_litespeed_boundary.phpt
new file mode 100644
index 000000000000..c0dcd5a32882
--- /dev/null
+++ b/ext/user_cache/tests/user_cache_litespeed_boundary.phpt
@@ -0,0 +1,378 @@
+--TEST--
+UserCache\Cache: litespeed partitions cache data by boundary
+--EXTENSIONS--
+user_cache
+--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_stop($process, array $pipes): void
+{
+ if (is_resource($process)) {
+ proc_terminate($process);
+
+ for ($i = 0; $i < 20; $i++) {
+ $status = proc_get_status($process);
+ if (!$status['running']) {
+ break;
+ }
+ usleep(100000);
+ }
+
+ $status = proc_get_status($process);
+ if ($status['running']) {
+ proc_terminate($process, 9);
+ }
+ }
+
+ foreach ($pipes as $pipe) {
+ if (is_resource($pipe)) {
+ fclose($pipe);
+ }
+ }
+
+ if (is_resource($process)) {
+ proc_close($process);
+ }
+}
+
+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'
+clear();
+} elseif ($action === 'seed') {
+ $cache->store($key, $host . '-value');
+}
+
+echo $host, ':', $cache->fetch($key, 'MISS'), ':', $cache->getPoolStatus()->getPoolName(), "\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", [
+ 'user_cache.enable=1',
+ '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');
+
+ user_cache_lsapi_request($alphaPort, $alphaScript, $alphaRoot, 'alpha.local', 'action=clear');
+ user_cache_lsapi_request($alphaPort, $otherScript, $otherRoot, 'other.local', 'action=clear');
+ user_cache_lsapi_request($betaPort, $betaScript, $betaRoot, 'beta.local', 'action=clear');
+
+ $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]) {
+ user_cache_lsapi_stop($process, $pipes);
+ }
+ user_cache_lsapi_rm_rf($root);
+}
+
+?>
+--EXPECT--
+Done
diff --git a/ext/user_cache/tests/user_cache_lock_key_isolation.phpt b/ext/user_cache/tests/user_cache_lock_key_isolation.phpt
new file mode 100644
index 000000000000..300b22cea9d5
--- /dev/null
+++ b/ext/user_cache/tests/user_cache_lock_key_isolation.phpt
@@ -0,0 +1,61 @@
+--TEST--
+UserCache\Cache: locks are isolated by exact key
+--EXTENSIONS--
+user_cache
+--INI--
+user_cache.enable=1
+user_cache.enable_cli=1
+opcache.file_cache_only=0
+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/user_cache/tests/user_cache_lock_lease.phpt b/ext/user_cache/tests/user_cache_lock_lease.phpt
new file mode 100644
index 000000000000..004a026382c4
--- /dev/null
+++ b/ext/user_cache/tests/user_cache_lock_lease.phpt
@@ -0,0 +1,41 @@
+--TEST--
+UserCache\Cache: lock lease expires and can be taken over by another process
+--EXTENSIONS--
+user_cache
+pcntl
+--INI--
+user_cache.enable=1
+user_cache.enable_cli=1
+opcache.file_cache_only=0
+user_cache.shm_size=16M
+--FILE--
+clear();
+
+var_dump($cache->lock('leased', 1));
+
+$pid = pcntl_fork();
+if ($pid === 0) {
+ echo "child lock before expiry\n";
+ var_dump($cache->lock('leased'));
+ sleep(2);
+ echo "child lock after expiry\n";
+ var_dump($cache->lock('leased'));
+ var_dump($cache->unlock('leased'));
+ exit(0);
+}
+
+if ($pid > 0) {
+ pcntl_waitpid($pid, $status);
+} else {
+ echo "pcntl_fork() failed\n";
+}
+?>
+--EXPECT--
+bool(true)
+child lock before expiry
+bool(false)
+child lock after expiry
+bool(true)
+bool(true)
diff --git a/ext/user_cache/tests/user_cache_lock_reentrancy.phpt b/ext/user_cache/tests/user_cache_lock_reentrancy.phpt
new file mode 100644
index 000000000000..5960c4db296e
--- /dev/null
+++ b/ext/user_cache/tests/user_cache_lock_reentrancy.phpt
@@ -0,0 +1,33 @@
+--TEST--
+UserCache\Cache: lock re-entry within a request and unlock of unheld keys
+--EXTENSIONS--
+user_cache
+--INI--
+user_cache.enable=1
+user_cache.enable_cli=1
+opcache.file_cache_only=0
+user_cache.shm_size=16M
+--FILE--
+lock('key'));
+var_dump($cache->lock('key'));
+var_dump($cache->unlock('key'));
+var_dump($cache->unlock('key'));
+
+var_dump($cache->unlock('never-locked'));
+
+try {
+ $cache->lock('key', -1);
+} catch (ValueError $e) {
+ echo $e->getMessage(), "\n";
+}
+?>
+--EXPECT--
+bool(true)
+bool(true)
+bool(true)
+bool(false)
+bool(false)
+UserCache\Cache::lock(): Argument #2 ($lease) must be greater than or equal to 0
diff --git a/ext/user_cache/tests/user_cache_magic_serialize_contract.phpt b/ext/user_cache/tests/user_cache_magic_serialize_contract.phpt
new file mode 100644
index 000000000000..369db5a2f2fe
--- /dev/null
+++ b/ext/user_cache/tests/user_cache_magic_serialize_contract.phpt
@@ -0,0 +1,285 @@
+--TEST--
+UserCache\Cache: classes that cannot round-trip without __serialize()/__unserialize()
+--EXTENSIONS--
+user_cache
+--INI--
+user_cache.enable=1
+user_cache.enable_cli=1
+opcache.file_cache_only=0
+user_cache.shm_size=16M
+--FILE--
+formatter = fn (string $v): string => $this->prefix . ':' . $v;
+ }
+
+ public function __serialize(): array
+ {
+ return ['prefix' => $this->prefix];
+ }
+
+ public function __unserialize(array $data): void
+ {
+ $this->prefix = $data['prefix'];
+ $this->formatter = fn (string $v): string => $this->prefix . ':' . $v;
+ }
+
+ public function format(string $v): string
+ {
+ return ($this->formatter)($v);
+ }
+}
+
+$holder = new ContractClosureHolder('log');
+var_dump($cache->store('closure-holder', $holder));
+var_dump($cache->fetch('closure-holder')->format('entry'));
+
+class ContractIndexedCollection
+{
+ /** @var array */
+ private array $items;
+ /** @var array */
+ private array $indexByName = [];
+
+ public function __construct(array $items)
+ {
+ $this->items = $items;
+ $this->rebuildIndex();
+ }
+
+ public function __serialize(): array
+ {
+ return ['items' => $this->items];
+ }
+
+ public function __unserialize(array $data): void
+ {
+ $this->items = $data['items'];
+ $this->rebuildIndex();
+ }
+
+ private function rebuildIndex(): void
+ {
+ $this->indexByName = [];
+ foreach ($this->items as $position => $item) {
+ $this->indexByName[$item['name']] = $position;
+ }
+ }
+
+ public function findByName(string $name): ?array
+ {
+ $position = $this->indexByName[$name] ?? null;
+
+ return $position === null ? null : $this->items[$position];
+ }
+}
+
+$collection = new ContractIndexedCollection([
+ ['id' => 10, 'name' => 'alpha'],
+ ['id' => 20, 'name' => 'beta'],
+]);
+var_dump($cache->store('indexed', $collection));
+var_dump($cache->fetch('indexed')->findByName('beta')['id']);
+
+class ContractPackedMatrix
+{
+ /** @var array> */
+ private array $rows;
+
+ public function __construct(array $rows)
+ {
+ $this->rows = $rows;
+ }
+
+ public function __serialize(): array
+ {
+ return ['packed' => implode(';', array_map(static fn (array $r): string => implode(',', $r), $this->rows))];
+ }
+
+ public function __unserialize(array $data): void
+ {
+ $this->rows = array_map(
+ static fn (string $r): array => array_map('intval', explode(',', $r)),
+ explode(';', $data['packed'])
+ );
+ }
+
+ public function cell(int $row, int $column): int
+ {
+ return $this->rows[$row][$column];
+ }
+}
+
+var_dump($cache->store('matrix', new ContractPackedMatrix([[1, 2], [3, 4]])));
+var_dump($cache->fetch('matrix')->cell(1, 0));
+
+/* Snapshot hooks run once per store; restore hooks run on every fetch. */
+class ContractCounter
+{
+ public static int $serializeCalls = 0;
+ public static int $unserializeCalls = 0;
+
+ public function __construct(private int $value = 7)
+ {
+ }
+
+ public function __serialize(): array
+ {
+ self::$serializeCalls++;
+ return ['value' => $this->value];
+ }
+
+ public function __unserialize(array $data): void
+ {
+ self::$unserializeCalls++;
+ $this->value = $data['value'];
+ }
+}
+
+var_dump($cache->store('counter', new ContractCounter()));
+var_dump(ContractCounter::$serializeCalls);
+$cache->fetch('counter');
+$cache->fetch('counter');
+var_dump(ContractCounter::$unserializeCalls);
+
+class ContractParent
+{
+ private array $secret = [];
+
+ public function __serialize(): array
+ {
+ return ['secret' => $this->secret];
+ }
+
+ public function __unserialize(array $data): void
+ {
+ $this->secret = $data['secret'];
+ }
+
+ public function addSecret(string $v): void
+ {
+ $this->secret[] = $v;
+ }
+
+ public function secretCount(): int
+ {
+ return count($this->secret);
+ }
+}
+
+class ContractChild extends ContractParent
+{
+ public string $label = 'child';
+}
+
+$child = new ContractChild();
+$child->addSecret('a');
+$child->addSecret('b');
+var_dump($cache->store('child', $child));
+$fetchedChild = $cache->fetch('child');
+var_dump($fetchedChild instanceof ContractChild, $fetchedChild->secretCount(), $fetchedChild->label);
+
+class ContractReadonly
+{
+ public function __construct(public readonly string $token)
+ {
+ }
+
+ public function __serialize(): array
+ {
+ return ['token' => $this->token];
+ }
+
+ public function __unserialize(array $data): void
+ {
+ $this->token = $data['token'];
+ }
+}
+
+var_dump($cache->store('ro', new ContractReadonly('tok-1')));
+var_dump($cache->fetch('ro')->token);
+
+class ContractSelf
+{
+ public ?ContractSelf $self = null;
+
+ public function __construct(public int $v = 1)
+ {
+ }
+
+ public function __serialize(): array
+ {
+ return ['self' => $this, 'v' => $this->v];
+ }
+
+ public function __unserialize(array $data): void
+ {
+ $this->self = $data['self'];
+ $this->v = $data['v'];
+ }
+}
+
+var_dump($cache->store('self', new ContractSelf()));
+$fetchedSelf = $cache->fetch('self');
+var_dump($fetchedSelf->self === $fetchedSelf, $fetchedSelf->v);
+
+$nested = [
+ 'holders' => [new ContractClosureHolder('a'), new ContractClosureHolder('b')],
+ 'plain' => ['x' => 1],
+];
+var_dump($cache->store('nested', $nested));
+$fetchedNested = $cache->fetch('nested');
+var_dump($fetchedNested['holders'][1]->format('n'), $fetchedNested['plain']['x']);
+
+class ContractBadReturn
+{
+ public function __serialize()
+ {
+ return 'not-an-array';
+ }
+
+ public function __unserialize(array $data): void
+ {
+ }
+}
+
+try {
+ $cache->store('bad', new ContractBadReturn());
+} catch (TypeError $e) {
+ echo $e->getMessage(), "\n";
+}
+
+unset($holder, $collection, $child, $fetchedChild, $fetchedSelf, $nested, $fetchedNested);
+gc_collect_cycles();
+?>
+--EXPECT--
+bool(true)
+string(9) "log:entry"
+bool(true)
+int(20)
+bool(true)
+int(3)
+bool(true)
+int(1)
+int(2)
+bool(true)
+bool(true)
+int(2)
+string(5) "child"
+bool(true)
+string(5) "tok-1"
+bool(true)
+bool(true)
+int(1)
+bool(true)
+string(3) "b:n"
+int(1)
+ContractBadReturn::__serialize() must return an array
diff --git a/ext/user_cache/tests/user_cache_magic_state_unstorable.phpt b/ext/user_cache/tests/user_cache_magic_state_unstorable.phpt
new file mode 100644
index 000000000000..094a9a0a68f0
--- /dev/null
+++ b/ext/user_cache/tests/user_cache_magic_state_unstorable.phpt
@@ -0,0 +1,69 @@
+--TEST--
+UserCache\Cache: magic serialization state rejects unstorable values
+--EXTENSIONS--
+user_cache
+--INI--
+user_cache.enable=1
+user_cache.enable_cli=1
+opcache.file_cache_only=0
+user_cache.shm_size=16M
+--FILE--
+ $this->value];
+ }
+
+ public function __unserialize(array $data): void
+ {
+ $this->value = $data['value'] ?? null;
+ }
+}
+
+class UserCacheSleepResource
+{
+ public $value;
+
+ public function __sleep(): array
+ {
+ return ['value'];
+ }
+
+ public function __wakeup(): void
+ {
+ }
+}
+
+function show(string $label, callable $callback): void
+{
+ try {
+ var_dump($callback());
+ } catch (TypeError $e) {
+ echo $label, ": TypeError\n";
+ }
+}
+
+$cache = UserCache\Cache::getPool('magic-state-unstorable');
+
+$serializeResource = new UserCacheSerializeResource();
+$serializeResource->value = fopen(__FILE__, 'r');
+show('serialize-resource', fn() => $cache->store('serialize-resource', $serializeResource));
+fclose($serializeResource->value);
+
+$serializeClosure = new UserCacheSerializeResource();
+$serializeClosure->value = static fn() => null;
+show('serialize-closure', fn() => $cache->store('serialize-closure', $serializeClosure));
+
+$sleepResource = new UserCacheSleepResource();
+$sleepResource->value = fopen(__FILE__, 'r');
+show('sleep-resource', fn() => $cache->store('sleep-resource', $sleepResource));
+fclose($sleepResource->value);
+?>
+--EXPECT--
+serialize-resource: TypeError
+serialize-closure: TypeError
+sleep-resource: TypeError
diff --git a/ext/user_cache/tests/user_cache_negative_ttl.phpt b/ext/user_cache/tests/user_cache_negative_ttl.phpt
new file mode 100644
index 000000000000..323f5342a2d9
--- /dev/null
+++ b/ext/user_cache/tests/user_cache_negative_ttl.phpt
@@ -0,0 +1,40 @@
+--TEST--
+UserCache\Cache: negative TTL throws ValueError on every write API
+--EXTENSIONS--
+user_cache
+--INI--
+user_cache.enable=1
+user_cache.enable_cli=1
+opcache.file_cache_only=0
+user_cache.shm_size=16M
+--FILE--
+ fn() => $cache->store('key', 1, -1),
+ 'add' => fn() => $cache->add('key', 1, -1),
+ 'storeMultiple' => fn() => $cache->storeMultiple(['key' => 1], -1),
+ 'increment' => fn() => $cache->increment('key', 1, -1),
+ 'decrement' => fn() => $cache->decrement('key', 1, -1),
+ 'remember' => fn() => $cache->remember('key', fn() => 1, -1),
+];
+
+foreach ($calls as $fn) {
+ try {
+ $fn();
+ } catch (ValueError $e) {
+ echo $e->getMessage(), "\n";
+ }
+}
+
+var_dump($cache->has('key'));
+?>
+--EXPECT--
+UserCache\Cache::store(): Argument #3 ($ttl) must be greater than or equal to 0
+UserCache\Cache::add(): Argument #3 ($ttl) must be greater than or equal to 0
+UserCache\Cache::storeMultiple(): Argument #2 ($ttl) must be greater than or equal to 0
+UserCache\Cache::increment(): Argument #3 ($ttl) must be greater than or equal to 0
+UserCache\Cache::decrement(): Argument #3 ($ttl) must be greater than or equal to 0
+UserCache\Cache::remember(): Argument #3 ($ttl) must be greater than or equal to 0
+bool(false)
diff --git a/ext/user_cache/tests/user_cache_overwrite_reuse.phpt b/ext/user_cache/tests/user_cache_overwrite_reuse.phpt
new file mode 100644
index 000000000000..b77b87f02277
--- /dev/null
+++ b/ext/user_cache/tests/user_cache_overwrite_reuse.phpt
@@ -0,0 +1,42 @@
+--TEST--
+UserCache\Cache: repeated stores reuse existing entries
+--EXTENSIONS--
+user_cache
+--INI--
+user_cache.enable=1
+user_cache.enable_cli=1
+opcache.file_cache_only=0
+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/user_cache/tests/user_cache_packed_dynamic_array.phpt b/ext/user_cache/tests/user_cache_packed_dynamic_array.phpt
new file mode 100644
index 000000000000..cf045fd80390
--- /dev/null
+++ b/ext/user_cache/tests/user_cache_packed_dynamic_array.phpt
@@ -0,0 +1,58 @@
+--TEST--
+UserCache\Cache: packed dynamic arrays preserve append semantics
+--EXTENSIONS--
+user_cache
+--INI--
+user_cache.enable=1
+user_cache.enable_cli=1
+opcache.file_cache_only=0
+user_cache.shm_size=16M
+--FILE--
+ [
+ new PackedDynamicArrayNode(3),
+ new PackedDynamicArrayNode(4),
+ ],
+ ],
+];
+
+var_dump($cache->store('payload', $payload));
+
+$fetched = $cache->fetch('payload');
+var_dump(array_is_list($fetched));
+echo implode(',', array_keys($fetched)), "\n";
+
+$fetched[] = new PackedDynamicArrayNode(5);
+echo implode(',', array_keys($fetched)), "\n";
+echo $fetched[3]->id, "\n";
+
+var_dump(array_is_list($fetched[2]['nested']));
+$fetched[2]['nested'][] = new PackedDynamicArrayNode(6);
+echo implode(',', array_keys($fetched[2]['nested'])), "\n";
+echo $fetched[2]['nested'][2]->id, "\n";
+
+$again = $cache->fetch('payload');
+echo count($again), "\n";
+echo count($again[2]['nested']), "\n";
+?>
+--EXPECT--
+bool(true)
+bool(true)
+0,1,2
+0,1,2,3
+5
+bool(true)
+0,1,2
+6
+3
+2
diff --git a/ext/user_cache/tests/user_cache_pool_factory.phpt b/ext/user_cache/tests/user_cache_pool_factory.phpt
new file mode 100644
index 000000000000..e1d350309613
--- /dev/null
+++ b/ext/user_cache/tests/user_cache_pool_factory.phpt
@@ -0,0 +1,68 @@
+--TEST--
+UserCache\Cache: pool factory returns per-name singletons
+--EXTENSIONS--
+user_cache
+--INI--
+user_cache.enable=1
+user_cache.enable_cli=1
+opcache.file_cache_only=0
+user_cache.shm_size=16M
+--FILE--
+getMessage(), "\n";
+}
+
+try {
+ UserCache\Cache::getPool("bad\x1fpool");
+} catch (ValueError $e) {
+ echo $e->getMessage(), "\n";
+}
+
+var_dump($a->store('shared-key', 'from-factory'));
+var_dump($c->fetch('shared-key', 'MISS'));
+var_dump($a->fetch('shared-key'));
+?>
+--EXPECT--
+bool(false)
+bool(true)
+bool(false)
+bool(true)
+bool(false)
+array(2) {
+ [0]=>
+ string(7) "factory"
+ [1]=>
+ string(13) "factory-other"
+}
+bool(true)
+Error
+UserCache\Cache::getPool(): Argument #1 ($pool) must not be empty
+UserCache\Cache::getPool(): Argument #1 ($pool) must not contain the user-cache key delimiter 0x1F
+bool(true)
+string(4) "MISS"
+string(12) "from-factory"
diff --git a/ext/user_cache/tests/user_cache_pool_status_memory.phpt b/ext/user_cache/tests/user_cache_pool_status_memory.phpt
new file mode 100644
index 000000000000..68559b5907bf
--- /dev/null
+++ b/ext/user_cache/tests/user_cache_pool_status_memory.phpt
@@ -0,0 +1,99 @@
+--TEST--
+UserCache\Cache: pool status reports per-pool memory usage
+--EXTENSIONS--
+user_cache
+--INI--
+user_cache.enable=1
+user_cache.enable_cli=1
+opcache.file_cache_only=0
+user_cache.shm_size=16M
+--FILE--
+getPoolStatus();
+var_dump($empty->getEntryCount());
+var_dump($empty->getEntryKeys());
+var_dump($empty->getUsedMemory());
+
+var_dump($a->store('int', 123));
+$afterInt = $a->getPoolStatus();
+var_dump($afterInt->getEntryCount());
+var_dump($afterInt->getEntryKeys());
+var_dump($afterInt->getUsedMemory() > 0);
+
+$intMemory = $afterInt->getUsedMemory();
+var_dump($a->store('str', str_repeat('x', 100)));
+$afterString = $a->getPoolStatus();
+$keys = $afterString->getEntryKeys();
+sort($keys);
+var_dump($afterString->getEntryCount());
+var_dump($keys);
+var_dump($afterString->getUsedMemory() > $intMemory);
+$stringMemory = $afterString->getUsedMemory();
+
+var_dump($b->store('int', 123));
+var_dump($b->store('str', str_repeat('x', 100)));
+$bStatus = $b->getPoolStatus();
+var_dump($bStatus->getUsedMemory() === $afterString->getUsedMemory());
+
+var_dump($a->store('large', str_repeat('y', 4096)));
+$afterLarge = $a->getPoolStatus();
+var_dump($afterLarge->getEntryCount());
+var_dump($afterLarge->getUsedMemory() > $stringMemory);
+/* Status objects are snapshots, not live views. */
+var_dump($afterString->getUsedMemory() === $afterLarge->getUsedMemory());
+var_dump($b->getPoolStatus()->getUsedMemory() === $bStatus->getUsedMemory());
+
+var_dump($a->delete('large'));
+$afterDelete = $a->getPoolStatus();
+var_dump($afterDelete->getEntryCount());
+var_dump($afterDelete->getUsedMemory() === $stringMemory);
+
+var_dump($a->clear());
+$afterClear = $a->getPoolStatus();
+var_dump($afterClear->getEntryCount());
+var_dump($afterClear->getEntryKeys());
+var_dump($afterClear->getUsedMemory());
+?>
+--EXPECT--
+int(0)
+array(0) {
+}
+int(0)
+bool(true)
+int(1)
+array(1) {
+ [0]=>
+ string(3) "int"
+}
+bool(true)
+bool(true)
+int(2)
+array(2) {
+ [0]=>
+ string(3) "int"
+ [1]=>
+ string(3) "str"
+}
+bool(true)
+bool(true)
+bool(true)
+bool(true)
+bool(true)
+int(3)
+bool(true)
+bool(false)
+bool(true)
+bool(true)
+int(2)
+bool(true)
+bool(true)
+int(0)
+array(0) {
+}
+int(0)
diff --git a/ext/user_cache/tests/user_cache_pressure_clear.phpt b/ext/user_cache/tests/user_cache_pressure_clear.phpt
new file mode 100644
index 000000000000..564f8860f9ad
--- /dev/null
+++ b/ext/user_cache/tests/user_cache_pressure_clear.phpt
@@ -0,0 +1,30 @@
+--TEST--
+UserCache\Cache: store retries can clear on memory pressure
+--EXTENSIONS--
+user_cache
+--INI--
+user_cache.enable=1
+user_cache.enable_cli=1
+opcache.file_cache_only=0
+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/user_cache/tests/user_cache_private_property_missing_scope.phpt b/ext/user_cache/tests/user_cache_private_property_missing_scope.phpt
new file mode 100644
index 000000000000..9cda62d090e1
--- /dev/null
+++ b/ext/user_cache/tests/user_cache_private_property_missing_scope.phpt
@@ -0,0 +1,30 @@
+--TEST--
+UserCache\Cache: missing private property declaring class fails decode
+--EXTENSIONS--
+user_cache
+--INI--
+user_cache.enable=1
+user_cache.enable_cli=1
+opcache.file_cache_only=0
+user_cache.shm_size=16M
+--FILE--
+clear();
+
+var_dump($cache->store('value', $value));
+var_dump($cache->fetch('value', 'default'));
+var_dump($cache->has('value'));
+?>
+--EXPECT--
+bool(true)
+string(7) "default"
+bool(false)
diff --git a/ext/user_cache/tests/user_cache_pure_enum.phpt b/ext/user_cache/tests/user_cache_pure_enum.phpt
new file mode 100644
index 000000000000..7c5df08c01ec
--- /dev/null
+++ b/ext/user_cache/tests/user_cache_pure_enum.phpt
@@ -0,0 +1,35 @@
+--TEST--
+UserCache\Cache: pure (non-backed) enum cases round-trip as singletons
+--EXTENSIONS--
+user_cache
+--INI--
+user_cache.enable=1
+user_cache.enable_cli=1
+opcache.file_cache_only=0
+user_cache.shm_size=16M
+--FILE--
+store('single', PureSuit::Hearts));
+var_dump($cache->fetch('single') === PureSuit::Hearts);
+
+var_dump($cache->store('list', [PureSuit::Hearts, PureSuit::Spades, PureSuit::Hearts]));
+$fetched = $cache->fetch('list');
+var_dump($fetched[0] === PureSuit::Hearts);
+var_dump($fetched[1] === PureSuit::Spades);
+var_dump($fetched[2] === PureSuit::Hearts);
+?>
+--EXPECT--
+bool(true)
+bool(true)
+bool(true)
+bool(true)
+bool(true)
+bool(true)
diff --git a/ext/user_cache/tests/user_cache_readonly_properties.phpt b/ext/user_cache/tests/user_cache_readonly_properties.phpt
new file mode 100644
index 000000000000..2e5a65a0366b
--- /dev/null
+++ b/ext/user_cache/tests/user_cache_readonly_properties.phpt
@@ -0,0 +1,65 @@
+--TEST--
+UserCache\Cache: readonly and inherited readonly properties survive the round trip
+--EXTENSIONS--
+user_cache
+--INI--
+user_cache.enable=1
+user_cache.enable_cli=1
+opcache.file_cache_only=0
+user_cache.shm_size=16M
+--FILE--
+baseName . ':' . $this->baseCount;
+ }
+}
+
+class UserCacheReadonlyChild extends UserCacheReadonlyBase
+{
+ public function __construct(
+ string $baseName,
+ int $baseCount,
+ public readonly array $items,
+ ) {
+ parent::__construct($baseName, $baseCount);
+ }
+}
+
+$child = new UserCacheReadonlyChild('alpha', 7, ['a', 'b']);
+
+var_dump($cache->store('child', $child));
+
+$fetched = $cache->fetch('child');
+var_dump($fetched instanceof UserCacheReadonlyChild);
+var_dump($fetched->describe());
+var_dump($fetched->baseName);
+var_dump($fetched->items);
+
+try {
+ $fetched->baseName = 'changed';
+} catch (Error $e) {
+ echo get_class($e), "\n";
+}
+?>
+--EXPECT--
+bool(true)
+bool(true)
+string(7) "alpha:7"
+string(5) "alpha"
+array(2) {
+ [0]=>
+ string(1) "a"
+ [1]=>
+ string(1) "b"
+}
+Error
diff --git a/ext/user_cache/tests/user_cache_reference_cycle_gc.phpt b/ext/user_cache/tests/user_cache_reference_cycle_gc.phpt
new file mode 100644
index 000000000000..797bd6a0d38d
--- /dev/null
+++ b/ext/user_cache/tests/user_cache_reference_cycle_gc.phpt
@@ -0,0 +1,57 @@
+--TEST--
+UserCache\Cache: plain cyclic object graph round-trips and remains collectable
+--EXTENSIONS--
+user_cache
+--INI--
+user_cache.enable=1
+user_cache.enable_cli=1
+opcache.file_cache_only=0
+user_cache.shm_size=16M
+--FILE--
+parent = $root;
+$root->children[] = $leaf;
+
+var_dump($cache->store('graph', $root));
+
+$graph = $cache->fetch('graph');
+echo "decoded names: ", $graph->name, " ", $graph->children[0]->name, "\n";
+echo "back edge identity: ";
+var_dump($graph->children[0]->parent === $graph);
+echo "decoded graph is a copy: ";
+var_dump($graph !== $root);
+echo "leaf children: ";
+var_dump($graph->children[0]->children);
+
+$again = $cache->fetch('graph');
+echo "fetches independent: ";
+var_dump($again !== $graph);
+echo "fetched back edge identity: ";
+var_dump($again->children[0]->parent === $again);
+
+unset($root, $leaf, $graph, $again);
+gc_collect_cycles();
+?>
+--EXPECT--
+bool(true)
+decoded names: root leaf
+back edge identity: bool(true)
+decoded graph is a copy: bool(true)
+leaf children: array(0) {
+}
+fetches independent: bool(true)
+fetched back edge identity: bool(true)
diff --git a/ext/user_cache/tests/user_cache_reference_graphs.phpt b/ext/user_cache/tests/user_cache_reference_graphs.phpt
new file mode 100644
index 000000000000..aebd82b29a97
--- /dev/null
+++ b/ext/user_cache/tests/user_cache_reference_graphs.phpt
@@ -0,0 +1,272 @@
+--TEST--
+UserCache\Cache: references and cyclic graphs survive fetch and access
+--EXTENSIONS--
+user_cache
+--INI--
+user_cache.enable=1
+user_cache.enable_cli=1
+opcache.file_cache_only=0
+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);
+
+unset(
+ $a, $g, $x, $b, $o, $c, $gc, $pair, $v, $gp,
+ $typed, $typedValue, $gt, $dynamic, $dynamicValue, $gd,
+ $shared, $dynamicObject, $gdo, $self, $gs, $p, $q, $gm,
+ $root, $deep, $g1, $g2, $serializedCycle, $fetchedSerializedCycle,
+ $referenceAssignment, $fetchedReferenceAssignment, $cycleAssignment,
+ $fetchedCycleAssignment
+);
+gc_collect_cycles();
+?>
+--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/user_cache/tests/user_cache_remember_failure.phpt b/ext/user_cache/tests/user_cache_remember_failure.phpt
new file mode 100644
index 000000000000..f83978d3d990
--- /dev/null
+++ b/ext/user_cache/tests/user_cache_remember_failure.phpt
@@ -0,0 +1,49 @@
+--TEST--
+UserCache\Cache: remember() failure paths (callback throw, unstorable result)
+--EXTENSIONS--
+user_cache
+--INI--
+user_cache.enable=1
+user_cache.enable_cli=1
+opcache.file_cache_only=0
+user_cache.shm_size=16M
+--FILE--
+remember('boom', function () {
+ throw new RuntimeException('from callback');
+ });
+} catch (RuntimeException $e) {
+ echo $e->getMessage(), "\n";
+}
+var_dump($cache->has('boom'));
+var_dump($cache->lock('boom'));
+var_dump($cache->unlock('boom'));
+
+foreach ([
+ 'res' => fn() => fopen('php://memory', 'r'),
+ 'closure' => fn() => fn() => 1,
+ 'nested-res' => fn() => ['inner' => fopen('php://memory', 'r')],
+] as $key => $callback) {
+ try {
+ $cache->remember($key, $callback);
+ } catch (TypeError $e) {
+ echo $e->getMessage(), "\n";
+ }
+ var_dump($cache->has($key));
+}
+?>
+--EXPECT--
+from callback
+bool(false)
+bool(true)
+bool(true)
+resources cannot be stored in the user cache
+bool(false)
+Closure objects cannot be stored in the user cache
+bool(false)
+resources cannot be stored in the user cache
+bool(false)
diff --git a/ext/user_cache/tests/user_cache_remember_key_argument.phpt b/ext/user_cache/tests/user_cache_remember_key_argument.phpt
new file mode 100644
index 000000000000..5c4cde68973d
--- /dev/null
+++ b/ext/user_cache/tests/user_cache_remember_key_argument.phpt
@@ -0,0 +1,36 @@
+--TEST--
+UserCache\Cache: remember() passes the cache key to the callback
+--EXTENSIONS--
+user_cache
+--INI--
+user_cache.enable=1
+user_cache.enable_cli=1
+opcache.file_cache_only=0
+user_cache.shm_size=16M
+--FILE--
+remember('first', $loader));
+var_dump($cache->remember('second', $loader));
+
+$calls = 0;
+$counting = static function (string $key) use (&$calls): string {
+ $calls++;
+ return 'value:' . $key;
+};
+var_dump($cache->remember('first', $counting));
+var_dump($calls);
+
+var_dump($cache->remember('third', static fn (): int => 42));
+?>
+--EXPECT--
+string(12) "loaded:first"
+string(13) "loaded:second"
+string(12) "loaded:first"
+int(0)
+int(42)
diff --git a/ext/user_cache/tests/user_cache_request_local_slot_deferred_promotion.phpt b/ext/user_cache/tests/user_cache_request_local_slot_deferred_promotion.phpt
new file mode 100644
index 000000000000..fdc1528a6e53
--- /dev/null
+++ b/ext/user_cache/tests/user_cache_request_local_slot_deferred_promotion.phpt
@@ -0,0 +1,99 @@
+--TEST--
+UserCache\Cache: request-local slots are promoted after the first shared-graph fetch
+--EXTENSIONS--
+user_cache
+pcntl
+--INI--
+user_cache.enable=1
+user_cache.enable_cli=1
+opcache.file_cache_only=0
+user_cache.shm_size=16M
+date.timezone=UTC
+--FILE--
+clear();
+
+class UserCacheDeferredPromotionDate extends DateTime
+{
+ private string $cachedSelfHash;
+
+ public function __construct(string $time, DateTimeZone $timezone)
+ {
+ parent::__construct($time, $timezone);
+ $this->cachedSelfHash = spl_object_hash($this);
+ }
+
+ public function cachedSelfHash(): string
+ {
+ return $this->cachedSelfHash;
+ }
+}
+
+$pid = pcntl_fork();
+if ($pid === 0) {
+ $date = new UserCacheDeferredPromotionDate('2026-07-03 12:34:56.123456', new DateTimeZone('Asia/Tokyo'));
+ $payload = [
+ 'first' => $date,
+ 'second' => $date,
+ 'nested' => ['label' => 'original'],
+ 'storedHash' => $date->cachedSelfHash(),
+ ];
+
+ var_dump($cache->store('payload', $payload));
+ exit(0);
+}
+
+if ($pid < 0) {
+ echo "pcntl_fork() failed\n";
+ exit(1);
+}
+
+pcntl_waitpid($pid, $status);
+
+$first = $cache->fetch('payload');
+$first['first']->modify('+1 day');
+$first['nested']['label'] = 'mutated-first';
+
+$second = $cache->fetch('payload');
+$secondDateUnchanged = $second['first']->format('Y-m-d H:i:s.u e') === '2026-07-03 12:34:56.123456 Asia/Tokyo';
+$secondNestedUnchanged = $second['nested']['label'] === 'original';
+$second['first']->modify('+2 days');
+$second['nested']['label'] = 'mutated-second';
+
+$third = $cache->fetch('payload');
+
+echo "status: ", pcntl_wexitstatus($status), "\n";
+echo "first alias: ";
+var_dump($first['first'] === $first['second']);
+echo "second alias: ";
+var_dump($second['first'] === $second['second']);
+echo "third alias: ";
+var_dump($third['first'] === $third['second']);
+echo "fetches independent: ";
+var_dump($first['first'] !== $second['first'] && $second['first'] !== $third['first']);
+echo "second unchanged by first: ";
+var_dump($secondDateUnchanged);
+var_dump($secondNestedUnchanged);
+echo "third unchanged by second: ";
+var_dump($third['first']->format('Y-m-d H:i:s.u e') === '2026-07-03 12:34:56.123456 Asia/Tokyo');
+var_dump($third['nested']['label'] === 'original');
+echo "object hashes preserved as stored: ";
+var_dump($first['first']->cachedSelfHash() === $first['storedHash']);
+var_dump($second['first']->cachedSelfHash() === $second['storedHash']);
+var_dump($third['first']->cachedSelfHash() === $third['storedHash']);
+?>
+--EXPECTF--
+bool(true)
+status: 0
+first alias: bool(true)
+second alias: bool(true)
+third alias: bool(true)
+fetches independent: bool(true)
+second unchanged by first: bool(true)
+bool(true)
+third unchanged by second: bool(true)
+bool(true)
+object hashes preserved as stored: bool(true)
+bool(true)
+bool(true)
diff --git a/ext/user_cache/tests/user_cache_request_local_slot_magic_objects.phpt b/ext/user_cache/tests/user_cache_request_local_slot_magic_objects.phpt
new file mode 100644
index 000000000000..2643aea7d9f7
--- /dev/null
+++ b/ext/user_cache/tests/user_cache_request_local_slot_magic_objects.phpt
@@ -0,0 +1,118 @@
+--TEST--
+UserCache\Cache: graphs with magic restore methods re-run them on every fetch
+--EXTENSIONS--
+user_cache
+--INI--
+user_cache.enable=1
+user_cache.enable_cli=1
+opcache.file_cache_only=0
+user_cache.shm_size=16M
+--FILE--
+clear();
+
+/* Restore hooks must run on every fetch. */
+class SlotMagicPair
+{
+ public static int $serializeCalls = 0;
+ public static int $unserializeCalls = 0;
+
+ public function __construct(public int $value = 0)
+ {
+ }
+
+ public function __serialize(): array
+ {
+ self::$serializeCalls++;
+ return ['value' => $this->value];
+ }
+
+ public function __unserialize(array $data): void
+ {
+ self::$unserializeCalls++;
+ $this->value = $data['value'];
+ }
+}
+
+class SlotUnserializeOnly
+{
+ public static int $unserializeCalls = 0;
+
+ public function __construct(public int $value = 0)
+ {
+ }
+
+ public function __unserialize(array $data): void
+ {
+ self::$unserializeCalls++;
+ $this->value = $data['value'];
+ }
+}
+
+class SlotSleepWakeup
+{
+ public static int $sleepCalls = 0;
+ public static int $wakeupCalls = 0;
+
+ public function __construct(public int $value = 0)
+ {
+ }
+
+ public function __sleep(): array
+ {
+ self::$sleepCalls++;
+ return ['value'];
+ }
+
+ public function __wakeup(): void
+ {
+ self::$wakeupCalls++;
+ }
+}
+
+var_dump($cache->store('pair', new SlotMagicPair(11)));
+$first = $cache->fetch('pair');
+$second = $cache->fetch('pair');
+$third = $cache->fetch('pair');
+$fourth = $cache->fetch('pair');
+echo "pair serialize calls: ", SlotMagicPair::$serializeCalls, "\n";
+echo "pair unserialize calls: ", SlotMagicPair::$unserializeCalls, "\n";
+echo "pair fetches independent: ";
+var_dump($third !== $fourth && $third->value === 11 && $fourth->value === 11);
+$third->value = 99;
+echo "pair mutation isolated: ";
+var_dump($cache->fetch('pair')->value === 11);
+
+var_dump($cache->store('unserialize-only', new SlotUnserializeOnly(22)));
+$cache->fetch('unserialize-only');
+$cache->fetch('unserialize-only');
+$clone1 = $cache->fetch('unserialize-only');
+$clone2 = $cache->fetch('unserialize-only');
+echo "unserialize-only calls: ", SlotUnserializeOnly::$unserializeCalls, "\n";
+echo "unserialize-only fetches independent: ";
+var_dump($clone1 !== $clone2 && $clone1->value === 22 && $clone2->value === 22);
+
+var_dump($cache->store('sleeper', new SlotSleepWakeup(33)));
+$cache->fetch('sleeper');
+$cache->fetch('sleeper');
+$clone1 = $cache->fetch('sleeper');
+$clone2 = $cache->fetch('sleeper');
+echo "sleep calls: ", SlotSleepWakeup::$sleepCalls, "\n";
+echo "wakeup calls: ", SlotSleepWakeup::$wakeupCalls, "\n";
+echo "sleeper fetches independent: ";
+var_dump($clone1 !== $clone2 && $clone1->value === 33 && $clone2->value === 33);
+?>
+--EXPECT--
+bool(true)
+pair serialize calls: 1
+pair unserialize calls: 4
+pair fetches independent: bool(true)
+pair mutation isolated: bool(true)
+bool(true)
+unserialize-only calls: 4
+unserialize-only fetches independent: bool(true)
+bool(true)
+sleep calls: 1
+wakeup calls: 4
+sleeper fetches independent: bool(true)
diff --git a/ext/user_cache/tests/user_cache_request_local_slot_store_invalidate.phpt b/ext/user_cache/tests/user_cache_request_local_slot_store_invalidate.phpt
new file mode 100644
index 000000000000..a8259fc87b3a
--- /dev/null
+++ b/ext/user_cache/tests/user_cache_request_local_slot_store_invalidate.phpt
@@ -0,0 +1,62 @@
+--TEST--
+UserCache\Cache: same-request store()/delete() invalidate promoted request-local slots
+--EXTENSIONS--
+user_cache
+--INI--
+user_cache.enable=1
+user_cache.enable_cli=1
+opcache.file_cache_only=0
+user_cache.shm_size=16M
+--FILE--
+clear();
+
+class SlotInvalidateValue
+{
+ public static int $unserializeCalls = 0;
+
+ public function __construct(public int $value = 0)
+ {
+ }
+
+ public function __serialize(): array
+ {
+ return ['value' => $this->value];
+ }
+
+ public function __unserialize(array $data): void
+ {
+ self::$unserializeCalls++;
+ $this->value = $data['value'];
+ }
+}
+
+var_dump($cache->store('key', new SlotInvalidateValue(1)));
+$cache->fetch('key');
+$cache->fetch('key');
+$promoted = $cache->fetch('key');
+echo "calls after promotion: ", SlotInvalidateValue::$unserializeCalls, "\n";
+echo "promoted value: ", $promoted->value, "\n";
+
+var_dump($cache->store('key', new SlotInvalidateValue(2)));
+$fresh = $cache->fetch('key');
+echo "value after overwrite: ", $fresh->value, "\n";
+echo "calls after overwrite fetch: ", SlotInvalidateValue::$unserializeCalls, "\n";
+
+var_dump($cache->delete('key'));
+var_dump($cache->fetch('key'));
+var_dump($cache->store('key', new SlotInvalidateValue(3)));
+echo "value after delete and re-store: ", $cache->fetch('key')->value, "\n";
+?>
+--EXPECT--
+bool(true)
+calls after promotion: 3
+promoted value: 1
+bool(true)
+value after overwrite: 2
+calls after overwrite fetch: 4
+bool(true)
+NULL
+bool(true)
+value after delete and re-store: 3
diff --git a/ext/user_cache/tests/user_cache_request_local_slot_strict_parity.phpt b/ext/user_cache/tests/user_cache_request_local_slot_strict_parity.phpt
new file mode 100644
index 000000000000..47bc73403536
--- /dev/null
+++ b/ext/user_cache/tests/user_cache_request_local_slot_strict_parity.phpt
@@ -0,0 +1,145 @@
+--TEST--
+UserCache\Cache: request-local slots are strictly serialize-parity-safe
+--INI--
+user_cache.enable=1
+user_cache.enable_cli=1
+opcache.file_cache_only=0
+user_cache.shm_size=16M
+date.timezone=UTC
+--FILE--
+clear();
+
+/* Restore hooks must run on every fetch; structural cloning must not run __clone(). */
+class StrictParityMagicDate extends DateTime
+{
+ public static int $unserializeCalls = 0;
+ public static int $cloneCalls = 0;
+
+ public function __serialize(): array
+ {
+ return parent::__serialize();
+ }
+
+ public function __unserialize(array $data): void
+ {
+ self::$unserializeCalls++;
+ parent::__unserialize($data);
+ }
+
+ public function __clone(): void
+ {
+ self::$cloneCalls++;
+ }
+}
+
+/* Structural slot cloning must not invoke __clone(). */
+class StrictParityPlainClone
+{
+ public static int $cloneCalls = 0;
+
+ public function __construct(public int $value = 0, public array $tags = [])
+ {
+ }
+
+ public function __clone(): void
+ {
+ self::$cloneCalls++;
+ }
+}
+
+class StrictParitySleeper
+{
+ public static int $wakeupCalls = 0;
+
+ public int $value = 0;
+
+ public function __construct(int $value = 0)
+ {
+ $this->value = $value;
+ }
+
+ public function __sleep(): array
+ {
+ return ['value'];
+ }
+
+ public function __wakeup(): void
+ {
+ self::$wakeupCalls++;
+ }
+}
+
+class StrictParityWakeupOnly
+{
+ public static int $wakeupCalls = 0;
+
+ public int $value = 0;
+
+ public function __construct(int $value = 0)
+ {
+ $this->value = $value;
+ }
+
+ public function __wakeup(): void
+ {
+ self::$wakeupCalls++;
+ }
+}
+
+var_dump($cache->store('magic', new StrictParityMagicDate('2026-07-08 01:02:03.456789', new DateTimeZone('UTC'))));
+$magic = null;
+for ($i = 0; $i < 5; $i++) {
+ $magic = $cache->fetch('magic');
+}
+echo "magic __unserialize calls: ", StrictParityMagicDate::$unserializeCalls, "\n";
+echo "magic __clone calls: ", StrictParityMagicDate::$cloneCalls, "\n";
+echo "magic round-trips: ";
+var_dump($magic instanceof StrictParityMagicDate &&
+ $magic->format('Y-m-d H:i:s.u') === '2026-07-08 01:02:03.456789');
+
+var_dump($cache->store('plain', new StrictParityPlainClone(42, ['a', 'b'])));
+$plainOk = true;
+for ($i = 0; $i < 5; $i++) {
+ $plain = $cache->fetch('plain');
+ $plainOk = $plainOk && $plain->value === 42 && $plain->tags === ['a', 'b'];
+}
+echo "plain __clone calls: ", StrictParityPlainClone::$cloneCalls, "\n";
+echo "plain round-trips: ";
+var_dump($plainOk);
+
+var_dump($cache->store('sleeper', new StrictParitySleeper(7)));
+$sleeperOk = true;
+for ($i = 0; $i < 5; $i++) {
+ $sleeper = $cache->fetch('sleeper');
+ $sleeperOk = $sleeperOk && $sleeper->value === 7;
+}
+echo "sleeper __wakeup calls: ", StrictParitySleeper::$wakeupCalls, "\n";
+echo "sleeper round-trips: ";
+var_dump($sleeperOk);
+
+var_dump($cache->store('wakeup-only', new StrictParityWakeupOnly(9)));
+$wakeupOnlyOk = true;
+for ($i = 0; $i < 5; $i++) {
+ $wakeupOnly = $cache->fetch('wakeup-only');
+ $wakeupOnlyOk = $wakeupOnlyOk && $wakeupOnly->value === 9;
+}
+echo "wakeup-only __wakeup calls: ", StrictParityWakeupOnly::$wakeupCalls, "\n";
+echo "wakeup-only round-trips: ";
+var_dump($wakeupOnlyOk);
+?>
+--EXPECT--
+bool(true)
+magic __unserialize calls: 5
+magic __clone calls: 0
+magic round-trips: bool(true)
+bool(true)
+plain __clone calls: 0
+plain round-trips: bool(true)
+bool(true)
+sleeper __wakeup calls: 5
+sleeper round-trips: bool(true)
+bool(true)
+wakeup-only __wakeup calls: 5
+wakeup-only round-trips: bool(true)
diff --git a/ext/user_cache/tests/user_cache_request_local_slot_unserialize_exception.phpt b/ext/user_cache/tests/user_cache_request_local_slot_unserialize_exception.phpt
new file mode 100644
index 000000000000..05c71da30e4f
--- /dev/null
+++ b/ext/user_cache/tests/user_cache_request_local_slot_unserialize_exception.phpt
@@ -0,0 +1,69 @@
+--TEST--
+UserCache\Cache: throwing __unserialize propagates and keeps the entry
+--EXTENSIONS--
+user_cache
+--INI--
+user_cache.enable=1
+user_cache.enable_cli=1
+opcache.file_cache_only=0
+user_cache.shm_size=16M
+--FILE--
+clear();
+
+class SlotThrowingUnserialize
+{
+ public static bool $failNext = false;
+ public static int $unserializeCalls = 0;
+
+ public function __construct(public int $value = 0)
+ {
+ }
+
+ public function __serialize(): array
+ {
+ return ['value' => $this->value];
+ }
+
+ public function __unserialize(array $data): void
+ {
+ self::$unserializeCalls++;
+ if (self::$failNext) {
+ self::$failNext = false;
+ throw new RuntimeException('unserialize failure');
+ }
+
+ $this->value = $data['value'];
+ }
+}
+
+SlotThrowingUnserialize::$unserializeCalls = 0;
+
+var_dump($cache->store('key', new SlotThrowingUnserialize(7)));
+
+/* A throwing restore hook propagates like native unserialize(), and the entry
+ * is kept because it is not corrupt. No request-local slot is seeded. */
+SlotThrowingUnserialize::$failNext = true;
+try {
+ $cache->fetch('key');
+ echo "no exception\n";
+} catch (RuntimeException $e) {
+ echo "caught: ", $e->getMessage(), "\n";
+}
+echo "calls after throwing fetch: ", SlotThrowingUnserialize::$unserializeCalls, "\n";
+echo "entry kept: ";
+var_dump($cache->has('key'));
+
+/* The next fetch no longer throws and restores the value, seeding the slot. */
+$restored = $cache->fetch('key');
+echo "restored value: ", $restored->value, "\n";
+echo "calls after successful fetch: ", SlotThrowingUnserialize::$unserializeCalls, "\n";
+?>
+--EXPECT--
+bool(true)
+caught: unserialize failure
+calls after throwing fetch: 1
+entry kept: bool(true)
+restored value: 7
+calls after successful fetch: 2
diff --git a/ext/user_cache/tests/user_cache_reset.phpt b/ext/user_cache/tests/user_cache_reset.phpt
new file mode 100644
index 000000000000..2233061f3aa6
--- /dev/null
+++ b/ext/user_cache/tests/user_cache_reset.phpt
@@ -0,0 +1,40 @@
+--TEST--
+UserCache\Cache: user_cache_reset() clears cached values in the active partition
+--EXTENSIONS--
+user_cache
+--INI--
+user_cache.enable=1
+user_cache.enable_cli=1
+opcache.file_cache_only=0
+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(user_cache_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/user_cache/tests/user_cache_serdes_identity.phpt b/ext/user_cache/tests/user_cache_serdes_identity.phpt
new file mode 100644
index 000000000000..9d29e95c4162
--- /dev/null
+++ b/ext/user_cache/tests/user_cache_serdes_identity.phpt
@@ -0,0 +1,75 @@
+--TEST--
+UserCache\Cache: identity, cycles, and graph nesting of __sleep() objects
+--EXTENSIONS--
+user_cache
+--INI--
+user_cache.enable=1
+user_cache.enable_cli=1
+opcache.file_cache_only=0
+user_cache.shm_size=16M
+--FILE--
+link = $self;
+var_dump($cache->store('self-cycle', $self));
+$fetched = $cache->fetch('self-cycle');
+var_dump($fetched->link === $fetched);
+
+$shared = ['payload' => range(1, 4)];
+$a = new SerdesNode();
+$b = new SerdesNode();
+$a->items = $shared;
+$b->items = $shared;
+$a->link = $b;
+var_dump($cache->store('pair', $a));
+$pair = $cache->fetch('pair');
+var_dump($pair->link instanceof SerdesNode);
+var_dump($pair->items === $pair->link->items);
+
+$node = new SerdesNode();
+$node->items = ['x' => 1];
+var_dump($cache->store('nested', ['first' => $node, 'second' => $node, 'plain' => [1, 2, 3]]));
+$nested = $cache->fetch('nested');
+var_dump($nested['first'] instanceof SerdesNode);
+var_dump($nested['first'] === $nested['second']);
+var_dump($nested['first']->items['x']);
+var_dump($nested['plain']);
+
+unset($self, $fetched, $shared, $a, $b, $pair, $node, $nested);
+gc_collect_cycles();
+?>
+--EXPECT--
+bool(true)
+bool(true)
+bool(true)
+bool(true)
+bool(true)
+bool(true)
+bool(true)
+bool(true)
+int(1)
+array(3) {
+ [0]=>
+ int(1)
+ [1]=>
+ int(2)
+ [2]=>
+ int(3)
+}
diff --git a/ext/user_cache/tests/user_cache_serdes_mixed_keys.phpt b/ext/user_cache/tests/user_cache_serdes_mixed_keys.phpt
new file mode 100644
index 000000000000..6980b62dbf52
--- /dev/null
+++ b/ext/user_cache/tests/user_cache_serdes_mixed_keys.phpt
@@ -0,0 +1,51 @@
+--TEST--
+UserCache\Cache: serdes arrays keep mixed string/integer keys intact
+--EXTENSIONS--
+user_cache
+--INI--
+user_cache.enable=1
+user_cache.enable_cli=1
+opcache.file_cache_only=0
+user_cache.shm_size=16M
+--FILE--
+ ['name' => 'x', 5 => 'y'],
+ 'strings then int' => ['a' => 1, 'b' => 2, 10 => 3],
+ 'int then string' => [5 => 'y', 'name' => 'x'],
+ 'interleaved' => ['a' => 1, 3 => 'x', 'b' => 2, 7 => 'y'],
+ 'packed' => ['a', 'b'],
+ 'nested mixed' => ['outer' => [1 => 'one', 'two' => 2], 9 => ['k' => 'v']],
+];
+
+foreach ($cases as $label => $data) {
+ $holder = new MixedKeyHolder();
+ $holder->data = $data;
+
+ var_dump($cache->store($label, $holder));
+ $fetched = $cache->fetch($label);
+ var_dump($fetched->data === $data);
+}
+?>
+--EXPECT--
+bool(true)
+bool(true)
+bool(true)
+bool(true)
+bool(true)
+bool(true)
+bool(true)
+bool(true)
+bool(true)
+bool(true)
+bool(true)
+bool(true)
diff --git a/ext/user_cache/tests/user_cache_serdes_sleep_wakeup.phpt b/ext/user_cache/tests/user_cache_serdes_sleep_wakeup.phpt
new file mode 100644
index 000000000000..091034656a26
--- /dev/null
+++ b/ext/user_cache/tests/user_cache_serdes_sleep_wakeup.phpt
@@ -0,0 +1,110 @@
+--TEST--
+UserCache\Cache: __sleep()/__wakeup() classes round-trip through the serialization contract
+--EXTENSIONS--
+user_cache
+--INI--
+user_cache.enable=1
+user_cache.enable_cli=1
+opcache.file_cache_only=0
+user_cache.shm_size=16M
+--FILE--
+kept . '/' . $this->dropped . '/' . $this->protectedKept . '/' . count($this->privateKept);
+ }
+}
+
+$object = new SerdesSleeper();
+$object->kept = 'stored-kept';
+$object->dropped = 'stored-dropped';
+(function () {
+ $this->protectedKept = 7;
+ $this->privateKept = ['x', 'y'];
+})->call($object);
+
+var_dump($cache->store('sleeper', $object));
+var_dump(SerdesSleeper::$sleepCalls);
+
+$fetched = $cache->fetch('sleeper');
+var_dump($fetched instanceof SerdesSleeper);
+var_dump(SerdesSleeper::$wakeupCalls);
+var_dump($fetched->describe());
+
+/* Restore hooks run on every fetch. */
+$second = $cache->fetch('sleeper');
+var_dump(SerdesSleeper::$wakeupCalls);
+var_dump($second !== $fetched);
+
+class SerdesBadSleeper
+{
+ public int $real = 1;
+
+ public function __sleep(): array
+ {
+ return ['real', 'missing'];
+ }
+
+ public function __wakeup(): void
+ {
+ }
+}
+
+var_dump($cache->store('bad-sleeper', new SerdesBadSleeper()));
+var_dump($cache->fetch('bad-sleeper')->real);
+
+class SerdesWakerOnly
+{
+ public static int $wakeupCalls = 0;
+ public int $value = 5;
+
+ public function __wakeup(): void
+ {
+ self::$wakeupCalls++;
+ }
+}
+
+$waker = new SerdesWakerOnly();
+$waker->value = 11;
+var_dump($cache->store('waker', $waker));
+var_dump($cache->fetch('waker')->value);
+var_dump(SerdesWakerOnly::$wakeupCalls);
+?>
+--EXPECTF--
+bool(true)
+int(1)
+bool(true)
+int(1)
+string(31) "stored-kept/default-dropped/7/2"
+int(2)
+bool(true)
+
+Warning: UserCache\Cache::store(): "missing" returned as member variable from __sleep() but does not exist in %s on line %d
+bool(true)
+int(1)
+bool(true)
+int(11)
+int(1)
diff --git a/ext/user_cache/tests/user_cache_serializable_interface.phpt b/ext/user_cache/tests/user_cache_serializable_interface.phpt
new file mode 100644
index 000000000000..91dc4d677b22
--- /dev/null
+++ b/ext/user_cache/tests/user_cache_serializable_interface.phpt
@@ -0,0 +1,118 @@
+--TEST--
+UserCache\Cache: legacy Serializable interface honours its serialize()/unserialize() contract
+--EXTENSIONS--
+user_cache
+--INI--
+user_cache.enable=1
+user_cache.enable_cli=1
+opcache.file_cache_only=0
+user_cache.shm_size=16M
+date.timezone=UTC
+error_reporting=E_ALL & ~E_DEPRECATED
+--FILE--
+value;
+ }
+
+ public function unserialize(string $data): void
+ {
+ self::$unserializeCalls++;
+ $this->value = (int) $data;
+ }
+}
+
+$scalar = new LegacyScalar(42);
+$cache->store('scalar', $scalar);
+$fetched = $cache->fetch('scalar');
+ok('scalar instanceof', $fetched instanceof LegacyScalar);
+ok('scalar value', $fetched->value === 42);
+ok('scalar unserialize ran once per fetch', LegacyScalar::$unserializeCalls === 1);
+ok('scalar parity', serialize($fetched) === serialize($scalar));
+
+class Partial implements Serializable
+{
+ public int $kept = 0;
+ public int $notRestored = 0;
+
+ public function serialize(): string
+ {
+ return (string) $this->kept;
+ }
+
+ public function unserialize(string $data): void
+ {
+ $this->kept = (int) $data;
+ }
+}
+$partial = new Partial();
+$partial->kept = 5;
+$partial->notRestored = 99;
+$cache->store('partial', $partial);
+$fetchedPartial = $cache->fetch('partial');
+$nativePartial = unserialize(serialize($partial));
+ok('partial matches native', $fetchedPartial->kept === $nativePartial->kept
+ && $fetchedPartial->notRestored === $nativePartial->notRestored);
+
+class LegacyBag implements Serializable
+{
+ public array $items = [];
+
+ public function serialize(): string
+ {
+ return serialize($this->items);
+ }
+
+ public function unserialize(string $data): void
+ {
+ $this->items = unserialize($data);
+ }
+}
+
+$bag = new LegacyBag();
+$bag->items = ['a' => 1, 'b' => [2, 3], 'when' => new DateTimeImmutable('2026-01-01', new DateTimeZone('UTC'))];
+$cache->store('bag', $bag);
+$fetched = $cache->fetch('bag');
+ok('bag instanceof', $fetched instanceof LegacyBag);
+ok('bag array', $fetched->items['a'] === 1 && $fetched->items['b'] === [2, 3]);
+ok('bag nested object', $fetched->items['when'] instanceof DateTimeImmutable
+ && $fetched->items['when']->format('Y-m-d') === '2026-01-01');
+ok('bag parity', serialize($fetched) === serialize($bag));
+
+$graph = ['first' => new LegacyScalar(7), 'list' => [new LegacyScalar(8), new LegacyScalar(9)]];
+$cache->store('graph', $graph);
+$fetched = $cache->fetch('graph');
+ok('graph values', $fetched['first']->value === 7
+ && $fetched['list'][0]->value === 8
+ && $fetched['list'][1]->value === 9);
+ok('graph parity', serialize($fetched) === serialize($graph));
+?>
+--EXPECT--
+scalar instanceof: OK
+scalar value: OK
+scalar unserialize ran once per fetch: OK
+scalar parity: OK
+partial matches native: OK
+bag instanceof: OK
+bag array: OK
+bag nested object: OK
+bag parity: OK
+graph values: OK
+graph parity: OK
diff --git a/ext/user_cache/tests/user_cache_serializable_precedence.phpt b/ext/user_cache/tests/user_cache_serializable_precedence.phpt
new file mode 100644
index 000000000000..2dcf07bd963c
--- /dev/null
+++ b/ext/user_cache/tests/user_cache_serializable_precedence.phpt
@@ -0,0 +1,164 @@
+--TEST--
+UserCache\Cache: Serializable interface outranks __sleep/__wakeup/__unserialize (native precedence)
+--EXTENSIONS--
+user_cache
+--INI--
+user_cache.enable=1
+user_cache.enable_cli=1
+opcache.file_cache_only=0
+user_cache.shm_size=16M
+error_reporting=E_ALL & ~E_DEPRECATED
+--FILE--
+kept;
+ }
+
+ public function unserialize(string $data): void
+ {
+ $this->kept = (int) $data;
+ $this->via = 'iface';
+ }
+
+ public function __sleep(): array
+ {
+ return ['kept'];
+ }
+
+ public function __wakeup(): void
+ {
+ $this->via = 'sleep';
+ }
+}
+
+class PrecedenceSerVsWakeup implements Serializable
+{
+ public int $kept = 0;
+ public string $via = 'none';
+
+ public function serialize(): string
+ {
+ return (string) $this->kept;
+ }
+
+ public function unserialize(string $data): void
+ {
+ $this->kept = (int) $data;
+ $this->via = 'iface';
+ }
+
+ public function __wakeup(): void
+ {
+ $this->via = 'wakeup';
+ }
+}
+
+class PrecedenceSerVsMagicUnserialize implements Serializable
+{
+ public int $kept = 0;
+ public string $via = 'none';
+
+ public function serialize(): string
+ {
+ return (string) $this->kept;
+ }
+
+ public function unserialize(string $data): void
+ {
+ $this->kept = (int) $data;
+ $this->via = 'iface';
+ }
+
+ public function __unserialize(array $data): void
+ {
+ $this->via = 'magic';
+ }
+}
+
+function check_precedence(UserCache\Cache $cache, string $label, object $value): void
+{
+ $native = unserialize(serialize($value));
+ $cache->store($label, $value);
+ $fetched = $cache->fetch($label);
+
+ ok($label . ' instanceof', $fetched instanceof $value);
+ ok($label . ' interface won (native parity)',
+ $fetched->via === $native->via && $fetched->via === 'iface');
+ ok($label . ' value', $fetched->kept === $native->kept);
+}
+
+$a = new PrecedenceSerVsSleep();
+$a->kept = 5;
+check_precedence($cache, 'PrecedenceSerVsSleep', $a);
+
+$b = new PrecedenceSerVsWakeup();
+$b->kept = 6;
+check_precedence($cache, 'PrecedenceSerVsWakeup', $b);
+
+$c = new PrecedenceSerVsMagicUnserialize();
+$c->kept = 7;
+check_precedence($cache, 'PrecedenceSerVsMagicUnserialize', $c);
+
+/* __serialize()/__unserialize() take precedence over Serializable. */
+class PrecedenceMagicOverIface implements Serializable
+{
+ public int $kept = 0;
+ public string $via = 'none';
+
+ public function serialize(): string
+ {
+ return 'IFACE';
+ }
+
+ public function unserialize(string $data): void
+ {
+ $this->via = 'iface';
+ }
+
+ public function __serialize(): array
+ {
+ return ['kept' => $this->kept];
+ }
+
+ public function __unserialize(array $data): void
+ {
+ $this->kept = $data['kept'];
+ $this->via = 'magic';
+ }
+}
+
+$d = new PrecedenceMagicOverIface();
+$d->kept = 8;
+$native = unserialize(serialize($d));
+$cache->store('magic-over-iface', $d);
+$fetched = $cache->fetch('magic-over-iface');
+ok('magic outranks interface', $fetched->via === $native->via && $fetched->via === 'magic');
+ok('magic value', $fetched->kept === $native->kept && $fetched->kept === 8);
+?>
+--EXPECT--
+PrecedenceSerVsSleep instanceof: OK
+PrecedenceSerVsSleep interface won (native parity): OK
+PrecedenceSerVsSleep value: OK
+PrecedenceSerVsWakeup instanceof: OK
+PrecedenceSerVsWakeup interface won (native parity): OK
+PrecedenceSerVsWakeup value: OK
+PrecedenceSerVsMagicUnserialize instanceof: OK
+PrecedenceSerVsMagicUnserialize interface won (native parity): OK
+PrecedenceSerVsMagicUnserialize value: OK
+magic outranks interface: OK
+magic value: OK
diff --git a/ext/user_cache/tests/user_cache_serializable_value_coverage.phpt b/ext/user_cache/tests/user_cache_serializable_value_coverage.phpt
new file mode 100644
index 000000000000..aff5cb651f9c
--- /dev/null
+++ b/ext/user_cache/tests/user_cache_serializable_value_coverage.phpt
@@ -0,0 +1,230 @@
+--TEST--
+UserCache\Cache: every serializable value class round-trips through store/fetch
+--EXTENSIONS--
+user_cache
+--INI--
+user_cache.enable=1
+user_cache.enable_cli=1
+opcache.file_cache_only=0
+user_cache.shm_size=32M
+--FILE--
+store($key, $value)) {
+ echo $label, ": STORE FAILED\n";
+ return;
+ }
+
+ $fetched = $cache->fetch($key);
+ if ($verify !== null) {
+ $ok = $verify($value, $fetched);
+ echo $label, ': ', $ok ? 'OK' : 'MISMATCH', "\n";
+ unset($fetched);
+ gc_collect_cycles();
+ return;
+ }
+
+ $ok = serialize($fetched) === serialize($value);
+ echo $label, ': ', $ok ? 'OK' : 'MISMATCH', "\n";
+ unset($fetched);
+ gc_collect_cycles();
+}
+
+check($cache, 'null', null);
+check($cache, 'bool', true);
+check($cache, 'int', PHP_INT_MAX);
+check($cache, 'negative int', PHP_INT_MIN);
+check($cache, 'float', 1.5);
+check($cache, 'float INF', INF);
+check($cache, 'float NAN', NAN);
+check($cache, 'empty string', '');
+check($cache, 'binary string', "a\0b\xff\xfe");
+check($cache, 'unicode string', 'こんにちは🌏');
+
+check($cache, 'empty array', []);
+check($cache, 'packed array', range(1, 64));
+check($cache, 'hashed array', ['a' => 1, 'b' => ['c' => 2.5, 'd' => null], 10 => 'x']);
+check($cache, 'binary keys', ["k\0ey" => 1, '0' => 2]);
+$deep = 'leaf';
+for ($d = 0; $d < 32; $d++) {
+ $deep = ['level' => $d, 'inner' => $deep];
+}
+check($cache, 'deep array', $deep);
+$aliased = ['v' => 1];
+$aliased['alias'] = &$aliased['v'];
+check($cache, 'array with references', $aliased);
+$selfref = ['x' => 1];
+$selfref['self'] = &$selfref;
+check($cache, 'self-referencing array', $selfref, static function ($orig, $fetched) {
+ /* Argument copying detaches the outer reference; inspect the cycle directly. */
+ return $fetched['self']['self']['self']['x'] === 1 && is_array($fetched['self']['self']);
+});
+
+check($cache, 'stdClass', (object) ['a' => 1, 'nested' => (object) ['b' => 2]]);
+
+class CoveragePlain
+{
+ public int $pub = 1;
+ protected string $prot = 'p';
+ private array $priv = ['x'];
+}
+check($cache, 'plain class with visibility', new CoveragePlain());
+
+#[\AllowDynamicProperties]
+class CoverageDynamic
+{
+ public int $declared = 1;
+}
+$dynamic = new CoverageDynamic();
+$dynamic->added = 'dynamic';
+check($cache, 'dynamic properties', $dynamic);
+
+class CoverageReadonly
+{
+ public function __construct(public readonly string $name) {}
+}
+check($cache, 'readonly property', new CoverageReadonly('ro'));
+
+class CoverageMagicSerialize
+{
+ public function __construct(private array $state) {}
+ public function __serialize(): array { return ['state' => $this->state]; }
+ public function __unserialize(array $data): void { $this->state = $data['state']; }
+}
+check($cache, '__serialize/__unserialize', new CoverageMagicSerialize(['k' => [1, 2]]));
+
+class CoverageSleep
+{
+ public int $kept = 5;
+ public string $dropped = 'gone';
+ public function __sleep(): array { return ['kept']; }
+ public function __wakeup(): void {}
+}
+check($cache, '__sleep/__wakeup', new CoverageSleep());
+
+$shared = new stdClass();
+$shared->tag = 'shared';
+check($cache, 'shared object identity', [$shared, $shared], static function ($orig, $fetched) {
+ return $fetched[0] === $fetched[1] && $fetched[0]->tag === 'shared';
+});
+
+class CoverageNode
+{
+ public ?CoverageNode $parent = null;
+ /** @var CoverageNode[] */
+ public array $children = [];
+ public function __construct(public string $id) {}
+}
+$root = new CoverageNode('root');
+$child = new CoverageNode('child');
+$child->parent = $root;
+$root->children[] = $child;
+check($cache, 'cyclic object graph', $root, static function ($orig, $fetched) {
+ return $fetched->children[0]->parent === $fetched && $fetched->children[0]->id === 'child';
+});
+
+$selfobj = new CoverageNode('self');
+$selfobj->parent = $selfobj;
+check($cache, 'self-referencing object', $selfobj, static function ($orig, $fetched) {
+ return $fetched->parent === $fetched;
+});
+
+enum CoverageSuit: string
+{
+ case Hearts = 'h';
+ case Spades = 's';
+}
+check($cache, 'backed enum case', CoverageSuit::Spades, static function ($orig, $fetched) {
+ return $fetched === CoverageSuit::Spades;
+});
+check($cache, 'enum inside array', ['suit' => CoverageSuit::Hearts], static function ($orig, $fetched) {
+ return $fetched['suit'] === CoverageSuit::Hearts;
+});
+
+check($cache, 'DateTimeImmutable', new DateTimeImmutable('2026-07-02 12:00:00', new DateTimeZone('Asia/Tokyo')));
+check($cache, 'DateInterval', new DateInterval('P1DT2H'));
+check($cache, 'ArrayObject', new ArrayObject(['a' => 1, 'b' => 2]));
+check($cache, 'SplStack', (static function () {
+ $s = new SplStack();
+ $s->push('one');
+ $s->push('two');
+ return $s;
+})());
+check($cache, 'SplFixedArray', SplFixedArray::fromArray([1, 'two', 3.0]));
+check($cache, 'SplObjectStorage', (static function () {
+ $s = new SplObjectStorage();
+ $o = new stdClass();
+ $o->v = 1;
+ $s[$o] = 'data';
+ return $s;
+})(), static function ($orig, $fetched) {
+ if (!$fetched instanceof SplObjectStorage || count($fetched) !== 1) {
+ return false;
+ }
+ $fetched->rewind();
+ return $fetched->current()->v === 1 && $fetched->getInfo() === 'data';
+});
+check($cache, 'Randomizer engine object', new Random\Randomizer(new Random\Engine\Xoshiro256StarStar(1234)), static function ($orig, $fetched) {
+ return $fetched->getInt(0, PHP_INT_MAX) === $orig->getInt(0, PHP_INT_MAX);
+});
+
+check($cache, 'mixed payload', [
+ 'config' => ['ttl' => 300, 'flags' => [true, false]],
+ 'entity' => new CoverageMagicSerialize(['id' => 7]),
+ 'sleeper' => new CoverageSleep(),
+ 'when' => new DateTimeImmutable('2026-01-01', new DateTimeZone('UTC')),
+ 'suit' => CoverageSuit::Hearts,
+], static function ($orig, $fetched) {
+ return $fetched['config']['ttl'] === 300
+ && $fetched['entity'] instanceof CoverageMagicSerialize
+ && $fetched['sleeper'] instanceof CoverageSleep
+ && $fetched['when'] instanceof DateTimeImmutable
+ && $fetched['suit'] === CoverageSuit::Hearts;
+});
+
+unset($deep, $aliased, $selfref, $dynamic, $shared, $root, $child, $selfobj);
+gc_collect_cycles();
+?>
+--EXPECT--
+null: OK
+bool: OK
+int: OK
+negative int: OK
+float: OK
+float INF: OK
+float NAN: OK
+empty string: OK
+binary string: OK
+unicode string: OK
+empty array: OK
+packed array: OK
+hashed array: OK
+binary keys: OK
+deep array: OK
+array with references: OK
+self-referencing array: OK
+stdClass: OK
+plain class with visibility: OK
+dynamic properties: OK
+readonly property: OK
+__serialize/__unserialize: OK
+__sleep/__wakeup: OK
+shared object identity: OK
+cyclic object graph: OK
+self-referencing object: OK
+backed enum case: OK
+enum inside array: OK
+DateTimeImmutable: OK
+DateInterval: OK
+ArrayObject: OK
+SplStack: OK
+SplFixedArray: OK
+SplObjectStorage: OK
+Randomizer engine object: OK
+mixed payload: OK
diff --git a/ext/user_cache/tests/user_cache_serialize_only.phpt b/ext/user_cache/tests/user_cache_serialize_only.phpt
new file mode 100644
index 000000000000..f680cccfbeaf
--- /dev/null
+++ b/ext/user_cache/tests/user_cache_serialize_only.phpt
@@ -0,0 +1,169 @@
+--TEST--
+UserCache\Cache: __serialize() without __unserialize() restores by property assignment
+--EXTENSIONS--
+user_cache
+--INI--
+user_cache.enable=1
+user_cache.enable_cli=1
+opcache.file_cache_only=0
+user_cache.shm_size=16M
+--FILE--
+kept = $kept;
+ $this->dropped = 'set';
+ }
+
+ public function __serialize(): array
+ {
+ return ['kept' => $this->kept];
+ }
+}
+
+$value = new SerializeOnly(5);
+$cache->store('basic', $value);
+$fetched = $cache->fetch('basic');
+ok('basic instanceof', $fetched instanceof SerializeOnly);
+ok('basic kept restored', $fetched->kept === 5);
+ok('basic omitted reverts to default', $fetched->dropped === 'default');
+ok('basic parity', serialize($fetched) === serialize($value));
+
+class SerializeOnlyWakeful
+{
+ public static int $wakeupCalls = 0;
+ public int $v = 0;
+
+ public function __construct(int $v)
+ {
+ $this->v = $v;
+ }
+
+ public function __serialize(): array
+ {
+ return ['v' => $this->v];
+ }
+
+ public function __wakeup(): void
+ {
+ self::$wakeupCalls++;
+ }
+}
+
+$cache->store('wakeful', new SerializeOnlyWakeful(9));
+$first = $cache->fetch('wakeful');
+$second = $cache->fetch('wakeful');
+ok('wakeful value', $first->v === 9 && $second->v === 9);
+ok('wakeful __wakeup per fetch', SerializeOnlyWakeful::$wakeupCalls === 2);
+ok('wakeful distinct instances', $first !== $second);
+
+class SerializeOnlyRich
+{
+ public int $num;
+ private string $secret;
+ public ?object $child;
+
+ public function __construct()
+ {
+ $this->num = 1;
+ $this->secret = 'hidden';
+ $this->child = null;
+ }
+
+ public function __serialize(): array
+ {
+ return [
+ 'num' => 42,
+ "\0" . self::class . "\0secret" => 'restored',
+ 'child' => (object) ['deep' => [1, 2, 3]],
+ ];
+ }
+
+ public function reveal(): string
+ {
+ return $this->secret;
+ }
+}
+
+$rich = new SerializeOnlyRich();
+$cache->store('rich', $rich);
+$fetchedRich = $cache->fetch('rich');
+ok('rich typed', $fetchedRich->num === 42);
+ok('rich private', $fetchedRich->reveal() === 'restored');
+ok('rich nested object', $fetchedRich->child instanceof stdClass && $fetchedRich->child->deep === [1, 2, 3]);
+ok('rich parity', serialize($fetchedRich) === serialize($rich));
+
+$graph = ['a' => new SerializeOnly(1), 'list' => [new SerializeOnly(2), new SerializeOnly(3)]];
+$cache->store('graph', $graph);
+$fetchedGraph = $cache->fetch('graph');
+ok('graph values', $fetchedGraph['a']->kept === 1
+ && $fetchedGraph['list'][0]->kept === 2
+ && $fetchedGraph['list'][1]->kept === 3);
+ok('graph parity', serialize($fetchedGraph) === serialize($graph));
+
+#[\AllowDynamicProperties]
+class SerializeOnlyIntKeys
+{
+ public int $declared = 1;
+
+ public function __serialize(): array
+ {
+ return ['declared' => 7, 42 => 'answer', 'label' => 'x'];
+ }
+}
+$intKeys = new SerializeOnlyIntKeys();
+$cache->store('intkeys', $intKeys);
+$fetchedIntKeys = $cache->fetch('intkeys');
+ok('integer key parity', serialize($fetchedIntKeys) === serialize(unserialize(serialize($intKeys))));
+ok('integer key property', $fetchedIntKeys->declared === 7 && $fetchedIntKeys->{42} === 'answer');
+
+class SerializeOnlyResource
+{
+ public $handle;
+
+ public function __serialize(): array
+ {
+ return ['handle' => $this->handle];
+ }
+}
+
+$withResource = new SerializeOnlyResource();
+$withResource->handle = fopen(__FILE__, 'r');
+try {
+ $cache->store('resource', $withResource);
+ ok('resource rejected', false);
+} catch (TypeError $e) {
+ ok('resource rejected', true);
+}
+fclose($withResource->handle);
+?>
+--EXPECT--
+basic instanceof: OK
+basic kept restored: OK
+basic omitted reverts to default: OK
+basic parity: OK
+wakeful value: OK
+wakeful __wakeup per fetch: OK
+wakeful distinct instances: OK
+rich typed: OK
+rich private: OK
+rich nested object: OK
+rich parity: OK
+graph values: OK
+graph parity: OK
+integer key parity: OK
+integer key property: OK
+resource rejected: OK
diff --git a/ext/user_cache/tests/user_cache_shaped_array_state.phpt b/ext/user_cache/tests/user_cache_shaped_array_state.phpt
new file mode 100644
index 000000000000..296f5025fce1
--- /dev/null
+++ b/ext/user_cache/tests/user_cache_shaped_array_state.phpt
@@ -0,0 +1,127 @@
+--TEST--
+UserCache\Cache: shaped associative state arrays preserve unserialize semantics
+--EXTENSIONS--
+user_cache
+--INI--
+user_cache.enable=1
+user_cache.enable_cli=1
+opcache.file_cache_only=0
+user_cache.shm_size=16M
+--FILE--
+id = $id;
+ $this->label = $label;
+ }
+
+ public function __serialize(): array
+ {
+ return [
+ 'id' => $this->id,
+ 'label' => $this->label,
+ 'nested' => [
+ 'id' => $this->id,
+ 'label' => $this->label,
+ 'checksum' => $this->id + strlen($this->label),
+ ],
+ ];
+ }
+
+ public function __unserialize(array $data): void
+ {
+ $GLOBALS['shaped_array_state_unserialize_count']++;
+ self::$events[] = array_keys($data);
+ $this->id = $data['id'];
+ $this->label = $data['label'];
+ $this->checksum = $data['nested']['checksum'];
+ }
+}
+
+$GLOBALS['shaped_array_state_unserialize_count'] = 0;
+
+$cache = UserCache\Cache::getPool('shaped-array-state');
+$payload = [
+ new ShapedArrayStatePayload(10, 'first'),
+ new ShapedArrayStatePayload(20, 'second'),
+ new ShapedArrayStatePayload(30, 'third'),
+];
+
+var_dump($cache->store('payload', $payload));
+
+$first = $cache->fetch('payload');
+$second = $cache->fetch('payload');
+
+echo $GLOBALS['shaped_array_state_unserialize_count'], "\n";
+echo $first[0]->checksum, ",", $first[1]->checksum, ",", $first[2]->checksum, "\n";
+echo $second[0]->checksum, ",", $second[1]->checksum, ",", $second[2]->checksum, "\n";
+var_dump(ShapedArrayStatePayload::$events);
+?>
+--EXPECT--
+bool(true)
+6
+15,26,35
+15,26,35
+array(6) {
+ [0]=>
+ array(3) {
+ [0]=>
+ string(2) "id"
+ [1]=>
+ string(5) "label"
+ [2]=>
+ string(6) "nested"
+ }
+ [1]=>
+ array(3) {
+ [0]=>
+ string(2) "id"
+ [1]=>
+ string(5) "label"
+ [2]=>
+ string(6) "nested"
+ }
+ [2]=>
+ array(3) {
+ [0]=>
+ string(2) "id"
+ [1]=>
+ string(5) "label"
+ [2]=>
+ string(6) "nested"
+ }
+ [3]=>
+ array(3) {
+ [0]=>
+ string(2) "id"
+ [1]=>
+ string(5) "label"
+ [2]=>
+ string(6) "nested"
+ }
+ [4]=>
+ array(3) {
+ [0]=>
+ string(2) "id"
+ [1]=>
+ string(5) "label"
+ [2]=>
+ string(6) "nested"
+ }
+ [5]=>
+ array(3) {
+ [0]=>
+ string(2) "id"
+ [1]=>
+ string(5) "label"
+ [2]=>
+ string(6) "nested"
+ }
+}
diff --git a/ext/user_cache/tests/user_cache_shared_graph.phpt b/ext/user_cache/tests/user_cache_shared_graph.phpt
new file mode 100644
index 000000000000..43a77b8a5607
--- /dev/null
+++ b/ext/user_cache/tests/user_cache_shared_graph.phpt
@@ -0,0 +1,40 @@
+--TEST--
+UserCache\Cache: classes declaring both magic methods use their serialization contract
+--EXTENSIONS--
+user_cache
+--INI--
+user_cache.enable=1
+user_cache.enable_cli=1
+opcache.file_cache_only=0
+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(99)
+int(11)
diff --git a/ext/user_cache/tests/user_cache_shared_graph_dag_dedup.phpt b/ext/user_cache/tests/user_cache_shared_graph_dag_dedup.phpt
new file mode 100644
index 000000000000..e5320a6ee41d
--- /dev/null
+++ b/ext/user_cache/tests/user_cache_shared_graph_dag_dedup.phpt
@@ -0,0 +1,51 @@
+--TEST--
+UserCache\Cache: repeated arrays in a DAG round-trip with copy-on-write isolation
+--EXTENSIONS--
+user_cache
+--INI--
+user_cache.enable=1
+user_cache.enable_cli=1
+opcache.file_cache_only=0
+user_cache.shm_size=16M
+--FILE--
+ str_repeat('q', 64),
+ 'list' => range(1, 32),
+ 'nested' => ['deep' => 'value'],
+];
+
+$dag = [
+ 'first' => $shared,
+ 'second' => $shared,
+ 'third' => $shared,
+ 'wrapped' => [$shared, $shared],
+];
+
+var_dump($cache->store('dag', $dag));
+$fetched = $cache->fetch('dag');
+var_dump($fetched === $dag);
+
+$fetched['first']['nested']['deep'] = 'mutated';
+var_dump($fetched['second']['nested']['deep']);
+var_dump($fetched['wrapped'][1] === $shared);
+
+$pair = [['k' => 'v'], ['k' => 'v']];
+var_dump($cache->store('pair', $pair));
+var_dump($cache->fetch('pair') === $pair);
+
+$dag['third']['extra'] = 'tail';
+var_dump($cache->store('dag', $dag));
+var_dump($cache->fetch('dag') === $dag);
+?>
+--EXPECT--
+bool(true)
+bool(true)
+string(5) "value"
+bool(true)
+bool(true)
+bool(true)
+bool(true)
+bool(true)
diff --git a/ext/user_cache/tests/user_cache_shm_size_clamp_warning.phpt b/ext/user_cache/tests/user_cache_shm_size_clamp_warning.phpt
new file mode 100644
index 000000000000..a8741d1d39ae
--- /dev/null
+++ b/ext/user_cache/tests/user_cache_shm_size_clamp_warning.phpt
@@ -0,0 +1,21 @@
+--TEST--
+UserCache\Cache: shm size clamp emits a PHP warning
+--EXTENSIONS--
+user_cache
+--SKIPIF--
+
+--INI--
+user_cache.enable=1
+user_cache.enable_cli=1
+opcache.file_cache_only=0
+opcache.log_verbosity_level=1
+user_cache.shm_size=8192M
+--FILE--
+getConfiguredMemory() > 0);
+?>
+--EXPECTF--
+Warning: user_cache.shm_size is limited to slightly under 4096M; clamping in Unknown on line 0
+bool(true)
diff --git a/ext/user_cache/tests/user_cache_shm_size_runtime_immutable.phpt b/ext/user_cache/tests/user_cache_shm_size_runtime_immutable.phpt
new file mode 100644
index 000000000000..7d7f54619d05
--- /dev/null
+++ b/ext/user_cache/tests/user_cache_shm_size_runtime_immutable.phpt
@@ -0,0 +1,19 @@
+--TEST--
+UserCache\Cache: user_cache.shm_size cannot be changed at runtime
+--EXTENSIONS--
+user_cache
+--INI--
+user_cache.enable=1
+user_cache.enable_cli=1
+opcache.file_cache_only=0
+user_cache.shm_size=16M
+--FILE--
+getConfiguredMemory());
+?>
+--EXPECT--
+bool(false)
+string(3) "16M"
+int(16777216)
diff --git a/ext/user_cache/tests/user_cache_shm_size_units.phpt b/ext/user_cache/tests/user_cache_shm_size_units.phpt
new file mode 100644
index 000000000000..d81d219124a2
--- /dev/null
+++ b/ext/user_cache/tests/user_cache_shm_size_units.phpt
@@ -0,0 +1,15 @@
+--TEST--
+UserCache\Cache: shm size directive accepts PHP quantity syntax
+--EXTENSIONS--
+user_cache
+--INI--
+user_cache.enable=1
+user_cache.enable_cli=1
+opcache.file_cache_only=0
+user_cache.shm_size=16
+--FILE--
+getConfiguredMemory());
+?>
+--EXPECT--
+int(16)
diff --git a/ext/user_cache/tests/user_cache_sleep_shared_graph.phpt b/ext/user_cache/tests/user_cache_sleep_shared_graph.phpt
new file mode 100644
index 000000000000..df00f6f419cc
--- /dev/null
+++ b/ext/user_cache/tests/user_cache_sleep_shared_graph.phpt
@@ -0,0 +1,138 @@
+--TEST--
+UserCache\Cache: __sleep() objects restore from shared graph state
+--EXTENSIONS--
+user_cache
+--INI--
+user_cache.enable=1
+user_cache.enable_cli=1
+opcache.file_cache_only=0
+user_cache.shm_size=16M
+--FILE--
+protectedKept = $protectedKept;
+ $this->privateKept = $privateKept;
+ }
+
+ public function __sleep(): array
+ {
+ self::$sleepCalls++;
+ return ['kept', 'protectedKept', 'privateKept', 'self'];
+ }
+
+ public function __wakeup(): void
+ {
+ self::$wakeupCalls++;
+ $this->kept .= ':awake';
+ }
+
+ public function describe(): string
+ {
+ return $this->kept . '/' .
+ $this->dropped . '/' .
+ $this->protectedKept . '/' .
+ count($this->privateKept) . '/' .
+ ($this->self === $this ? 'self' : 'other');
+ }
+}
+
+$box = new UCSleepSharedGraphBox();
+$box->kept = 'stored-kept';
+$box->dropped = 'stored-dropped';
+$box->setHidden(7, ['x', 'y']);
+$box->self = $box;
+
+var_dump($cache->store('box', $box));
+var_dump(UCSleepSharedGraphBox::$sleepCalls);
+
+$first = $cache->fetch('box');
+var_dump($first instanceof UCSleepSharedGraphBox);
+var_dump(UCSleepSharedGraphBox::$wakeupCalls);
+var_dump($first->describe());
+
+$second = $cache->fetch('box');
+var_dump(UCSleepSharedGraphBox::$wakeupCalls);
+var_dump($second !== $first);
+var_dump($second->self === $second);
+
+class UCSleepSharedGraphReadonly
+{
+ public function __construct(public readonly string $token)
+ {
+ }
+
+ public function __sleep(): array
+ {
+ return ['token'];
+ }
+
+ public function __wakeup(): void
+ {
+ }
+}
+
+var_dump($cache->store('readonly', new UCSleepSharedGraphReadonly('tok-1')));
+var_dump($cache->fetch('readonly')->token);
+
+class UCSleepSharedGraphWakeupMutates
+{
+ public array $items = [1];
+
+ public function __sleep(): array
+ {
+ return ['items'];
+ }
+
+ public function __wakeup(): void
+ {
+ $this->items[] = 2;
+ }
+}
+
+var_dump($cache->store('wakeup-mutates', new UCSleepSharedGraphWakeupMutates()));
+$mutated = $cache->fetch('wakeup-mutates');
+var_dump($mutated->items);
+$mutated->items[] = 3;
+var_dump($cache->fetch('wakeup-mutates')->items);
+
+unset($box, $first, $second, $mutated);
+gc_collect_cycles();
+?>
+--EXPECT--
+bool(true)
+int(1)
+bool(true)
+int(1)
+string(42) "stored-kept:awake/default-dropped/7/2/self"
+int(2)
+bool(true)
+bool(true)
+bool(true)
+string(5) "tok-1"
+bool(true)
+array(2) {
+ [0]=>
+ int(1)
+ [1]=>
+ int(2)
+}
+array(2) {
+ [0]=>
+ int(1)
+ [1]=>
+ int(2)
+}
diff --git a/ext/user_cache/tests/user_cache_spl_object_storage.phpt b/ext/user_cache/tests/user_cache_spl_object_storage.phpt
new file mode 100644
index 000000000000..3ee7305d2f09
--- /dev/null
+++ b/ext/user_cache/tests/user_cache_spl_object_storage.phpt
@@ -0,0 +1,107 @@
+--TEST--
+UserCache\Cache: SplObjectStorage round-trips entries, data, and shared identity
+--EXTENSIONS--
+user_cache
+spl
+--INI--
+user_cache.enable=1
+user_cache.enable_cli=1
+opcache.file_cache_only=0
+user_cache.shm_size=16M
+--FILE--
+id = 1;
+$b = new stdClass();
+$b->id = 2;
+$c = new stdClass();
+$c->id = 3;
+
+$storage = new SplObjectStorage();
+$storage[$a] = 'scalar-data';
+$storage[$b] = ['nested' => [1, 2, 3], 'flag' => true];
+$storage[$c] = (object) ['tag' => 'object-data'];
+
+$cache->store('storage', $storage);
+$fetched = $cache->fetch('storage');
+
+ok('instanceof', $fetched instanceof SplObjectStorage);
+ok('count', count($fetched) === 3);
+
+$byId = [];
+foreach ($fetched as $object) {
+ $byId[$object->id] = $fetched->getInfo();
+}
+ok('data preserved', $byId[1] === 'scalar-data'
+ && $byId[2] === ['nested' => [1, 2, 3], 'flag' => true]
+ && $byId[3] instanceof stdClass && $byId[3]->tag === 'object-data');
+ok('parity', serialize($fetched) === serialize($storage));
+
+$shared = new stdClass();
+$shared->id = 100;
+$sharedStorage = new SplObjectStorage();
+$sharedStorage[$shared] = 'info';
+$graph = ['storage' => $sharedStorage, 'also' => $shared];
+
+$cache->store('graph', $graph);
+$fetchedGraph = $cache->fetch('graph');
+$fetchedGraph['storage']->rewind();
+$keyObject = $fetchedGraph['storage']->current();
+ok('shared key identity', $keyObject === $fetchedGraph['also']);
+$fetchedGraph['also']->id = 200;
+ok('shared mutation follows', $keyObject->id === 200);
+
+$empty = new SplObjectStorage();
+$cache->store('empty', $empty);
+ok('empty storage', count($cache->fetch('empty')) === 0);
+
+class TaggedStorage extends SplObjectStorage
+{
+ public string $label = 'default';
+}
+$tagged = new TaggedStorage();
+$tagged->label = 'tagged';
+$element = new stdClass();
+$element->id = 5;
+$tagged[$element] = 'x';
+$cache->store('tagged', $tagged);
+$fetchedTagged = $cache->fetch('tagged');
+ok('subclass instanceof', $fetchedTagged instanceof TaggedStorage);
+ok('subclass property', $fetchedTagged->label === 'tagged' && count($fetchedTagged) === 1);
+ok('subclass parity', serialize($fetchedTagged) === serialize($tagged));
+
+$inner = new SplObjectStorage();
+$innerKey = new stdClass();
+$innerKey->id = 7;
+$inner[$innerKey] = 'inner';
+$outer = new SplObjectStorage();
+$outerKey = new stdClass();
+$outerKey->id = 8;
+$outer[$outerKey] = $inner;
+$cache->store('nested', $outer);
+$fetchedNested = $cache->fetch('nested');
+$fetchedNested->rewind();
+$nestedInfo = $fetchedNested->getInfo();
+ok('nested storage', $nestedInfo instanceof SplObjectStorage && count($nestedInfo) === 1);
+ok('nested parity', serialize($fetchedNested) === serialize($outer));
+?>
+--EXPECT--
+instanceof: OK
+count: OK
+data preserved: OK
+parity: OK
+shared key identity: OK
+shared mutation follows: OK
+empty storage: OK
+subclass instanceof: OK
+subclass property: OK
+subclass parity: OK
+nested storage: OK
+nested parity: OK
diff --git a/ext/user_cache/tests/user_cache_spl_safe_direct.phpt b/ext/user_cache/tests/user_cache_spl_safe_direct.phpt
new file mode 100644
index 000000000000..3f9511aca537
--- /dev/null
+++ b/ext/user_cache/tests/user_cache_spl_safe_direct.phpt
@@ -0,0 +1,397 @@
+--TEST--
+UserCache\Cache: SPL safe-direct state is restored
+--EXTENSIONS--
+user_cache
+--INI--
+user_cache.enable=1
+user_cache.enable_cli=1
+opcache.file_cache_only=0
+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(1)
+int(3)
+bool(true)
+string(8) "fallback"
+int(1)
+int(3)
diff --git a/ext/user_cache/tests/user_cache_state_schema_prototype.phpt b/ext/user_cache/tests/user_cache_state_schema_prototype.phpt
new file mode 100644
index 000000000000..3a1f914118dd
--- /dev/null
+++ b/ext/user_cache/tests/user_cache_state_schema_prototype.phpt
@@ -0,0 +1,202 @@
+--TEST--
+UserCache\Cache: class-scoped state schema and shape prototypes preserve magic semantics
+--INI--
+user_cache.enable=1
+user_cache.enable_cli=1
+opcache.file_cache_only=0
+user_cache.shm_size=16M
+--FILE--
+ $this->id,
+ 'label' => $this->label,
+ 'nested' => [
+ 'label' => $this->label,
+ 'checksum' => $this->id + strlen($this->label),
+ ],
+ ];
+ }
+
+ public function __unserialize(array $data): void
+ {
+ $id = $data['id'];
+ $label = $data['label'];
+
+ self::$unserializeCalls++;
+ $data['id'] = -1;
+ $data[] = 'local-only';
+ self::$events[] = [array_keys($data), array_key_last($data)];
+
+ $this->id = $id;
+ $this->label = $label . ':' . $data['nested']['checksum'];
+ }
+
+ public function label(): string
+ {
+ return $this->id . ':' . $this->label;
+ }
+}
+
+class UserCacheSchemaA
+{
+ public function __construct(private int $value)
+ {
+ }
+
+ public function __serialize(): array
+ {
+ return ['value' => $this->value, 'tag' => 'a'];
+ }
+
+ public function __unserialize(array $data): void
+ {
+ $this->value = $data['value'] + 10;
+ }
+
+ public function value(): int
+ {
+ return $this->value;
+ }
+}
+
+class UserCacheSchemaB
+{
+ public function __construct(private int $value)
+ {
+ }
+
+ public function __serialize(): array
+ {
+ return ['value' => $this->value, 'tag' => 'b'];
+ }
+
+ public function __unserialize(array $data): void
+ {
+ $this->value = $data['value'] + 20;
+ }
+
+ public function value(): int
+ {
+ return $this->value;
+ }
+}
+
+class UserCacheSchemaSelfState
+{
+ public static int $selfMatches = 0;
+
+ public function __construct(private string $name)
+ {
+ }
+
+ public function __serialize(): array
+ {
+ return ['self' => $this, 'name' => $this->name];
+ }
+
+ public function __unserialize(array $data): void
+ {
+ if ($data['self'] === $this) {
+ self::$selfMatches++;
+ }
+
+ $this->name = $data['name'];
+ }
+
+ public function name(): string
+ {
+ return $this->name;
+ }
+}
+
+$cache = UserCache\Cache::getPool('state-schema-prototype');
+
+var_dump($cache->store('payload', [
+ new UserCacheSchemaPayload(1, 'one'),
+ new UserCacheSchemaPayload(2, 'two'),
+ new UserCacheSchemaPayload(3, 'three'),
+]));
+
+$first = $cache->fetch('payload');
+$second = $cache->fetch('payload');
+
+echo UserCacheSchemaPayload::$serializeCalls, ',', UserCacheSchemaPayload::$unserializeCalls, "\n";
+echo $first[0]->label(), '|', $first[1]->label(), '|', $first[2]->label(), "\n";
+echo $second[0]->label(), '|', $second[1]->label(), '|', $second[2]->label(), "\n";
+var_dump(UserCacheSchemaPayload::$events[0]);
+var_dump(UserCacheSchemaPayload::$events[5]);
+
+$sharedPayload = new UserCacheSchemaPayload(4, 'four');
+var_dump($cache->store('shared-payload', [$sharedPayload, $sharedPayload]));
+$sharedPayload = $cache->fetch('shared-payload');
+var_dump($sharedPayload[0] === $sharedPayload[1]);
+echo $sharedPayload[0]->label(), '|', $sharedPayload[1]->label(), "\n";
+
+var_dump($cache->store('classes', [new UserCacheSchemaA(5), new UserCacheSchemaB(5)]));
+$classes = $cache->fetch('classes');
+echo get_class($classes[0]), ':', $classes[0]->value(), "\n";
+echo get_class($classes[1]), ':', $classes[1]->value(), "\n";
+
+var_dump($cache->store('self', new UserCacheSchemaSelfState('kept')));
+$self = $cache->fetch('self');
+echo $self->name(), ',', UserCacheSchemaSelfState::$selfMatches, "\n";
+?>
+--EXPECT--
+bool(true)
+3,6
+1:one:4|2:two:5|3:three:8
+1:one:4|2:two:5|3:three:8
+array(2) {
+ [0]=>
+ array(4) {
+ [0]=>
+ string(2) "id"
+ [1]=>
+ string(5) "label"
+ [2]=>
+ string(6) "nested"
+ [3]=>
+ int(0)
+ }
+ [1]=>
+ int(0)
+}
+array(2) {
+ [0]=>
+ array(4) {
+ [0]=>
+ string(2) "id"
+ [1]=>
+ string(5) "label"
+ [2]=>
+ string(6) "nested"
+ [3]=>
+ int(0)
+ }
+ [1]=>
+ int(0)
+}
+bool(true)
+bool(true)
+4:four:8|4:four:8
+bool(true)
+UserCacheSchemaA:15
+UserCacheSchemaB:25
+bool(true)
+kept,1
diff --git a/ext/user_cache/tests/user_cache_status_objects.phpt b/ext/user_cache/tests/user_cache_status_objects.phpt
new file mode 100644
index 000000000000..97848b01490d
--- /dev/null
+++ b/ext/user_cache/tests/user_cache_status_objects.phpt
@@ -0,0 +1,83 @@
+--TEST--
+UserCache\Cache: status objects
+--EXTENSIONS--
+user_cache
+--INI--
+user_cache.enable=1
+user_cache.enable_cli=1
+opcache.file_cache_only=0
+user_cache.shm_size=16M
+--FILE--
+getPoolStatus();
+
+var_dump($status instanceof UserCache\CacheStatus);
+var_dump($poolStatus instanceof UserCache\CachePoolStatus);
+var_dump($status->getAvailability() === UserCache\CacheAvailability::Available);
+var_dump($poolStatus->getPoolName());
+var_dump($poolStatus->getEntryCount());
+var_dump($poolStatus->getEntryKeys());
+var_dump($poolStatus->getUsedMemory());
+
+var_dump($cache->store('a', 1));
+var_dump($cache->store('b', 2));
+
+$other = UserCache\Cache::getPool('status-objects-other');
+var_dump($other->store('a', 3));
+
+$status = UserCache\Cache::getStatus();
+$poolStatus = $cache->getPoolStatus();
+$otherStatus = $other->getPoolStatus();
+
+var_dump($status->getEntryCount() >= 3);
+var_dump($poolStatus->getEntryCount());
+var_dump($otherStatus->getEntryCount());
+$keys = $poolStatus->getEntryKeys();
+sort($keys);
+var_dump($keys);
+var_dump($poolStatus->getUsedMemory() > 0);
+
+foreach ([$status, $poolStatus] as $object) {
+ try {
+ serialize($object);
+ } catch (Throwable $e) {
+ echo $e::class, ': ', $e->getMessage(), "\n";
+ }
+}
+
+try {
+ $status->entryCount = 0;
+} catch (Throwable $e) {
+ echo $e::class, ': ', $e->getMessage(), "\n";
+}
+?>
+--EXPECTF--
+bool(true)
+bool(true)
+bool(true)
+string(14) "status-objects"
+int(0)
+array(0) {
+}
+int(0)
+bool(true)
+bool(true)
+bool(true)
+bool(true)
+int(2)
+int(1)
+array(2) {
+ [0]=>
+ string(1) "a"
+ [1]=>
+ string(1) "b"
+}
+bool(true)
+Exception: Serialization of 'UserCache\CacheStatus' is not allowed
+Exception: Serialization of 'UserCache\CachePoolStatus' is not allowed
+Error: Cannot create dynamic property UserCache\CacheStatus::$entryCount
diff --git a/ext/user_cache/tests/user_cache_storable_values.phpt b/ext/user_cache/tests/user_cache_storable_values.phpt
new file mode 100644
index 000000000000..e5b373d17ea4
--- /dev/null
+++ b/ext/user_cache/tests/user_cache_storable_values.phpt
@@ -0,0 +1,150 @@
+--TEST--
+UserCache\Cache: storable values use shared graph and opaque values are refused
+--EXTENSIONS--
+user_cache
+spl
+--INI--
+user_cache.enable=1
+user_cache.enable_cli=1
+opcache.file_cache_only=0
+user_cache.shm_size=16M
+date.timezone=UTC
+error_reporting=E_ALL & ~E_DEPRECATED
+--FILE--
+store('storable_' . $i, $value) ? 'stored' : 'REFUSED';
+ } catch (TypeError $e) {
+ return 'TypeError';
+ }
+}
+
+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(1)
+ [1]=>
+ int(1)
+}
+__sleep object access: string(3) "1:2"
+Resource: TypeError
+Closure: TypeError
+nested resource: TypeError
+nested closure: TypeError
+object resource: TypeError
+object closure: TypeError
+fixed resource: TypeError
+fixed closure: TypeError
+array resource: TypeError
+array closure: TypeError
+Fiber: TypeError
+Generator: TypeError
+WeakMap: TypeError
diff --git a/ext/user_cache/tests/user_cache_store_multiple_rollback.phpt b/ext/user_cache/tests/user_cache_store_multiple_rollback.phpt
new file mode 100644
index 000000000000..4060dfd68df8
--- /dev/null
+++ b/ext/user_cache/tests/user_cache_store_multiple_rollback.phpt
@@ -0,0 +1,46 @@
+--TEST--
+UserCache\Cache: storeMultiple rolls back partial writes on failure
+--EXTENSIONS--
+user_cache
+--INI--
+user_cache.enable=1
+user_cache.enable_cli=1
+opcache.file_cache_only=0
+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 = UserCache\Cache::getPool('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/user_cache/tests/user_cache_store_multiple_typeerror.phpt b/ext/user_cache/tests/user_cache_store_multiple_typeerror.phpt
new file mode 100644
index 000000000000..14c92992b973
--- /dev/null
+++ b/ext/user_cache/tests/user_cache_store_multiple_typeerror.phpt
@@ -0,0 +1,39 @@
+--TEST--
+UserCache\Cache: storeMultiple() rejects unstorable values and rolls back
+--EXTENSIONS--
+user_cache
+--INI--
+user_cache.enable=1
+user_cache.enable_cli=1
+opcache.file_cache_only=0
+user_cache.shm_size=16M
+--FILE--
+store('existing', 'previous'));
+
+try {
+ $cache->storeMultiple([
+ 'existing' => 'replaced',
+ 'fresh' => 1,
+ 'bad' => fn() => 1,
+ ]);
+} catch (TypeError $e) {
+ echo $e->getMessage(), "\n";
+}
+var_dump($cache->fetch('existing'));
+var_dump($cache->has('fresh'));
+
+try {
+ $cache->storeMultiple(['bad' => fopen('php://memory', 'r')]);
+} catch (TypeError $e) {
+ echo $e->getMessage(), "\n";
+}
+?>
+--EXPECT--
+bool(true)
+UserCache\Cache::storeMultiple(): Argument #1 ($values) must not contain Closure objects
+string(8) "previous"
+bool(false)
+UserCache\Cache::storeMultiple(): Argument #1 ($values) must contain only values of type object|array|string|int|float|bool|null, resource given
diff --git a/ext/user_cache/tests/user_cache_tombstone_rehash.phpt b/ext/user_cache/tests/user_cache_tombstone_rehash.phpt
new file mode 100644
index 000000000000..ebd9dbfd6e72
--- /dev/null
+++ b/ext/user_cache/tests/user_cache_tombstone_rehash.phpt
@@ -0,0 +1,47 @@
+--TEST--
+UserCache\Cache: rehashes the entry table after delete churn
+--EXTENSIONS--
+user_cache
+--INI--
+user_cache.enable=1
+user_cache.enable_cli=1
+opcache.file_cache_only=0
+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/user_cache/tests/user_cache_ttl_remember.phpt b/ext/user_cache/tests/user_cache_ttl_remember.phpt
new file mode 100644
index 000000000000..74ed1a1d22b8
--- /dev/null
+++ b/ext/user_cache/tests/user_cache_ttl_remember.phpt
@@ -0,0 +1,37 @@
+--TEST--
+UserCache\Cache: TTL and remember()
+--EXTENSIONS--
+user_cache
+--INI--
+user_cache.enable=1
+user_cache.enable_cli=1
+opcache.file_cache_only=0
+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/user_cache/tests/user_cache_ttl_semantics.phpt b/ext/user_cache/tests/user_cache_ttl_semantics.phpt
new file mode 100644
index 000000000000..88780b85d3c7
--- /dev/null
+++ b/ext/user_cache/tests/user_cache_ttl_semantics.phpt
@@ -0,0 +1,39 @@
+--TEST--
+UserCache\Cache: counter TTL expiry and TTL overwrite semantics
+--EXTENSIONS--
+user_cache
+--INI--
+user_cache.enable=1
+user_cache.enable_cli=1
+opcache.file_cache_only=0
+user_cache.shm_size=16M
+--FILE--
+clear();
+
+var_dump($cache->increment('counter', 1, 1));
+
+var_dump($cache->store('overwritten', 'v1', 1));
+var_dump($cache->store('overwritten', 'v2'));
+
+var_dump($cache->store('shortened', 'v1'));
+var_dump($cache->store('shortened', 'v2', 1));
+
+sleep(2);
+
+var_dump($cache->fetch('counter', 'MISS'));
+var_dump($cache->increment('counter'));
+var_dump($cache->fetch('overwritten', 'MISS'));
+var_dump($cache->fetch('shortened', 'MISS'));
+?>
+--EXPECT--
+int(1)
+bool(true)
+bool(true)
+bool(true)
+bool(true)
+string(4) "MISS"
+int(1)
+string(2) "v2"
+string(4) "MISS"
diff --git a/ext/user_cache/tests/user_cache_uninitialized_typed_prop.phpt b/ext/user_cache/tests/user_cache_uninitialized_typed_prop.phpt
new file mode 100644
index 000000000000..78fb711dd744
--- /dev/null
+++ b/ext/user_cache/tests/user_cache_uninitialized_typed_prop.phpt
@@ -0,0 +1,94 @@
+--TEST--
+UserCache\Cache: uninitialized typed properties round-trip as uninitialized
+--EXTENSIONS--
+user_cache
+--INI--
+user_cache.enable=1
+user_cache.enable_cli=1
+opcache.file_cache_only=0
+user_cache.shm_size=16M
+--FILE--
+assigned = 42;
+
+$cache->store('box', $object);
+$fetched = $cache->fetch('box');
+
+$assignedProp = new ReflectionProperty($fetched, 'assigned');
+$neverProp = new ReflectionProperty($fetched, 'neverAssigned');
+$nullableProp = new ReflectionProperty($fetched, 'nullableNeverAssigned');
+$defaultProp = new ReflectionProperty($fetched, 'withDefault');
+
+ok('assigned initialized', $assignedProp->isInitialized($fetched) && $fetched->assigned === 42);
+ok('never-assigned stays uninitialized', !$neverProp->isInitialized($fetched));
+ok('nullable-never stays uninitialized', !$nullableProp->isInitialized($fetched));
+ok('default initialized', $defaultProp->isInitialized($fetched) && $fetched->withDefault === 'default');
+
+$readError = false;
+try {
+ $fetched->neverAssigned;
+} catch (Error $e) {
+ $readError = str_contains($e->getMessage(), 'must not be accessed before initialization');
+}
+ok('uninitialized read still errors', $readError);
+
+$fetched->neverAssigned = 7;
+ok('can assign after fetch', $fetched->neverAssigned === 7);
+$typeError = false;
+try {
+ $fetched->assigned = 'not an int';
+} catch (TypeError $e) {
+ $typeError = true;
+}
+ok('type check preserved', $typeError);
+
+class UninitReadonly
+{
+ public readonly int $id;
+ public string $name;
+ public function __construct(string $name)
+ {
+ $this->name = $name;
+ }
+}
+$ro = new UninitReadonly('n');
+$cache->store('ro', $ro);
+$fetchedRo = $cache->fetch('ro');
+$idProp = new ReflectionProperty($fetchedRo, 'id');
+ok('readonly uninitialized survives', !$idProp->isInitialized($fetchedRo) && $fetchedRo->name === 'n');
+
+$graph = ['direct' => new UninitBox(), 'list' => [new UninitBox()]];
+$graph['direct']->assigned = 1;
+$cache->store('graph', $graph);
+$fetchedGraph = $cache->fetch('graph');
+$nestedProp = new ReflectionProperty($fetchedGraph['list'][0], 'neverAssigned');
+ok('nested uninitialized survives', !$nestedProp->isInitialized($fetchedGraph['list'][0]));
+ok('serialize parity', serialize($cache->fetch('box')) === serialize($object));
+?>
+--EXPECT--
+assigned initialized: OK
+never-assigned stays uninitialized: OK
+nullable-never stays uninitialized: OK
+default initialized: OK
+uninitialized read still errors: OK
+can assign after fetch: OK
+type check preserved: OK
+readonly uninitialized survives: OK
+nested uninitialized survives: OK
+serialize parity: OK
diff --git a/ext/user_cache/tests/user_cache_unserialize_only_contract.phpt b/ext/user_cache/tests/user_cache_unserialize_only_contract.phpt
new file mode 100644
index 000000000000..125ac9bf13e6
--- /dev/null
+++ b/ext/user_cache/tests/user_cache_unserialize_only_contract.phpt
@@ -0,0 +1,84 @@
+--TEST--
+UserCache\Cache: __unserialize() without __serialize() is honored
+--EXTENSIONS--
+user_cache
+--INI--
+user_cache.enable=1
+user_cache.enable_cli=1
+opcache.file_cache_only=0
+user_cache.shm_size=16M
+--FILE--
+value = $data['value'];
+ $this->derived = $this->value * 3;
+ }
+}
+
+class SleepAndUnserializeContract
+{
+ public int $keep = 5;
+ public int $drop = 99;
+ public bool $wakeup = false;
+ public array $keys = [];
+ public static array $events = [];
+
+ public function __sleep(): array
+ {
+ self::$events[] = 'sleep';
+
+ return ['keep'];
+ }
+
+ public function __wakeup(): void
+ {
+ self::$events[] = 'wakeup';
+ $this->wakeup = true;
+ }
+
+ public function __unserialize(array $data): void
+ {
+ self::$events[] = 'unserialize';
+ $this->keys = array_keys($data);
+ $this->keep = $data['keep'];
+ $this->drop = -1;
+ }
+}
+
+$cache = UserCache\Cache::getPool('unserialize-only-contract');
+
+var_dump($cache->store('one', new UnserializeOnlyContract()));
+$one = $cache->fetch('one');
+var_dump($one->value, $one->derived, UnserializeOnlyContract::$calls);
+
+var_dump($cache->store('two', new SleepAndUnserializeContract()));
+$two = $cache->fetch('two');
+var_dump($two->keep, $two->drop, $two->wakeup, $two->keys, SleepAndUnserializeContract::$events);
+?>
+--EXPECT--
+bool(true)
+int(7)
+int(21)
+int(1)
+bool(true)
+int(5)
+int(-1)
+bool(false)
+array(1) {
+ [0]=>
+ string(4) "keep"
+}
+array(2) {
+ [0]=>
+ string(5) "sleep"
+ [1]=>
+ string(11) "unserialize"
+}
diff --git a/ext/user_cache/tests/user_cache_unstorable_extended.phpt b/ext/user_cache/tests/user_cache_unstorable_extended.phpt
new file mode 100644
index 000000000000..560095d3a882
--- /dev/null
+++ b/ext/user_cache/tests/user_cache_unstorable_extended.phpt
@@ -0,0 +1,51 @@
+--TEST--
+UserCache\Cache: lazy objects and not-serializable internal objects are rejected
+--EXTENSIONS--
+user_cache
+--INI--
+user_cache.enable=1
+user_cache.enable_cli=1
+opcache.file_cache_only=0
+user_cache.shm_size=16M
+--FILE--
+ $reflector->newLazyGhost(function ($object) { $object->x = 2; }),
+ 'proxy' => $reflector->newLazyProxy(fn() => new LazyTarget()),
+ 'nested-ghost' => ['inner' => $reflector->newLazyGhost(function ($object) { $object->x = 2; })],
+ 'not-serializable' => UserCache\Cache::getStatus(),
+ 'weakref' => WeakReference::create(new LazyTarget()),
+ 'nested-weakref' => ['inner' => WeakReference::create(new LazyTarget())],
+];
+
+foreach ($values as $key => $value) {
+ try {
+ $cache->store($key, $value);
+ } catch (TypeError $e) {
+ echo $e->getMessage(), "\n";
+ }
+ var_dump($cache->has($key));
+}
+?>
+--EXPECT--
+lazy objects cannot be stored in the user cache
+bool(false)
+lazy objects cannot be stored in the user cache
+bool(false)
+lazy objects cannot be stored in the user cache
+bool(false)
+objects with opaque internal state (e.g. Fiber, Generator, PDO) cannot be stored in the user cache
+bool(false)
+objects with opaque internal state (e.g. Fiber, Generator, PDO) cannot be stored in the user cache
+bool(false)
+objects with opaque internal state (e.g. Fiber, Generator, PDO) cannot be stored in the user cache
+bool(false)
diff --git a/ext/user_cache/tests/user_cache_wakeup_only_identity.phpt b/ext/user_cache/tests/user_cache_wakeup_only_identity.phpt
new file mode 100644
index 000000000000..22867c21de63
--- /dev/null
+++ b/ext/user_cache/tests/user_cache_wakeup_only_identity.phpt
@@ -0,0 +1,63 @@
+--TEST--
+UserCache\Cache: __wakeup()-only objects keep outer shared graph identity
+--EXTENSIONS--
+user_cache
+--INI--
+user_cache.enable=1
+user_cache.enable_cli=1
+opcache.file_cache_only=0
+user_cache.shm_size=16M
+--FILE--
+clear();
+
+class WakeupOnlyIdentityHolder
+{
+ public static int $wakeupCalls = 0;
+ public object $child;
+
+ public function __wakeup(): void
+ {
+ self::$wakeupCalls++;
+ }
+}
+
+class WakeupOnlyIdentityChild
+{
+ public int $value = 1;
+}
+
+$child = new WakeupOnlyIdentityChild();
+$holder = new WakeupOnlyIdentityHolder();
+$holder->child = $child;
+
+$native = unserialize(serialize(['holder' => $holder, 'child' => $child]));
+var_dump($native['holder']->child === $native['child']);
+
+WakeupOnlyIdentityHolder::$wakeupCalls = 0;
+
+var_dump($cache->store('graph', ['holder' => $holder, 'child' => $child]));
+
+$first = $cache->fetch('graph');
+var_dump($first['holder']->child === $first['child']);
+$first['holder']->child->value = 42;
+var_dump($first['child']->value);
+var_dump(WakeupOnlyIdentityHolder::$wakeupCalls);
+
+$second = $cache->fetch('graph');
+var_dump($second['holder']->child === $second['child']);
+var_dump($second['child']->value);
+var_dump(WakeupOnlyIdentityHolder::$wakeupCalls);
+var_dump($first['holder'] !== $second['holder']);
+?>
+--EXPECT--
+bool(true)
+bool(true)
+bool(true)
+int(42)
+int(1)
+bool(true)
+int(1)
+int(2)
+bool(true)
diff --git a/ext/user_cache/user_cache.c b/ext/user_cache/user_cache.c
new file mode 100644
index 000000000000..1b93a3a7c5ae
--- /dev/null
+++ b/ext/user_cache/user_cache.c
@@ -0,0 +1,3418 @@
+/*
+ +----------------------------------------------------------------------+
+ | 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 |
+ +----------------------------------------------------------------------+
+ | Authors: Go Kudo |
+ +----------------------------------------------------------------------+
+ */
+
+#include "php.h"
+#include "php_user_cache.h"
+
+#include "user_cache_internal.h"
+#include "user_cache_arginfo.h"
+#include "user_cache_decl.h"
+#include "user_cache_shm.h"
+
+#include "Zend/zend_atomic.h"
+#include "Zend/zend_closures.h"
+#include "Zend/zend_exceptions.h"
+#include "Zend/zend_smart_str.h"
+
+#include "ext/standard/php_var.h"
+#include "ext/standard/info.h"
+
+#include "SAPI.h"
+
+#define PHP_USER_CACHE_API_VALUE_TYPE "object|array|string|int|float|bool|null"
+#define PHP_USER_CACHE_STORAGE_KEY_CACHE_MAX 4096U
+#define PHP_USER_CACHE_MAX_BOUNDARY_PARTITIONS 32U
+
+#define PHP_USER_CACHE_DEFINE_OBJ_FROM_STD(type) \
+ static type *type##_from_obj(zend_object *obj) \
+ { \
+ return (type *) ((char *) obj - offsetof(type, std)); \
+ }
+
+#define PHP_USER_CACHE_DEFINE_SAFE_DIRECT_HANDLER_GETTER(member) \
+ php_user_cache_safe_direct_##member##_func_t php_user_cache_safe_direct_##member##_func( \
+ zend_class_entry *ce) \
+ { \
+ const php_user_cache_safe_direct_handlers *handlers = \
+ php_user_cache_safe_direct_find_handlers(ce, NULL) \
+ ; \
+ return handlers != NULL ? handlers->member : NULL; \
+ }
+
+#define PHP_USER_CACHE_DEFINE_STATUS_LONG_GETTER(method, field) \
+ ZEND_METHOD(UserCache_CacheStatus, method) \
+ { \
+ php_user_cache_status_object *status; \
+ \
+ ZEND_PARSE_PARAMETERS_NONE(); \
+ \
+ status = user_cache_status_from_this(ZEND_THIS); \
+ if (status == NULL) { \
+ RETURN_THROWS(); \
+ } \
+ \
+ RETURN_LONG(status->stats.field); \
+ }
+
+typedef struct {
+ zend_string *scope;
+ zend_string *scope_prefix;
+ php_user_cache_context *context;
+ HashTable *storage_key_cache;
+ zend_object std;
+} php_user_cache_object;
+
+typedef struct {
+ zend_string *scope;
+ zend_string *scope_prefix;
+ php_user_cache_context *context;
+ /* Immutable snapshot. */
+ zend_long entry_count;
+ zend_long used_memory;
+ zval entry_keys;
+ zend_object std;
+} php_user_cache_pool_status_object;
+
+typedef struct {
+ zend_ulong hash;
+ zend_string *key;
+ uint32_t index;
+} php_user_cache_bulk_order;
+
+typedef struct {
+ zval *value;
+ php_user_cache_prepared_value prepared;
+ php_user_cache_store_result store_result;
+} php_user_cache_bulk_store_item;
+
+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;
+ zend_long expunge_count;
+ zend_long store_failure_count;
+} php_user_cache_info_stats;
+
+typedef struct {
+ /* Immutable snapshot. */
+ php_user_cache_info_stats stats;
+ php_user_cache_reason availability_reason;
+ bool initialized;
+ zend_object std;
+} php_user_cache_status_object;
+
+typedef struct _php_user_cache_boundary_partition {
+ char *boundary;
+ size_t boundary_len;
+ zend_ulong boundary_hash;
+ php_user_cache_partition *partition;
+ struct _php_user_cache_boundary_partition *next;
+} php_user_cache_boundary_partition;
+
+#ifndef ZTS
+php_user_cache_globals user_cache_globals;
+#else
+int user_cache_globals_id;
+size_t user_cache_globals_offset;
+#endif
+
+static HashTable php_user_cache_safe_direct_handler_table;
+static bool php_user_cache_safe_direct_handlers_initialized = false;
+static php_user_cache_boundary_partition *php_user_cache_boundary_partitions = NULL;
+static zend_class_entry *php_user_cache_availability_ce;
+static zend_class_entry *php_user_cache_ce;
+static zend_object_handlers php_user_cache_object_handlers;
+static zend_object_handlers php_user_cache_status_object_handlers;
+static zend_object_handlers php_user_cache_pool_status_object_handlers;
+static uint32_t php_user_cache_boundary_partition_count = 0;
+static bool php_user_cache_boundary_limit_logged = false;
+static bool php_user_cache_boundary_startup_failed_logged = false;
+static uint64_t php_user_cache_self_pid = 0;
+#ifndef ZEND_WIN32
+static bool php_user_cache_self_pid_uncached = false;
+static bool php_user_cache_pid_atfork_registered = false;
+#endif
+
+zend_class_entry *php_user_cache_exception_ce;
+zend_class_entry *php_user_cache_status_ce;
+zend_class_entry *php_user_cache_pool_status_ce;
+php_user_cache_context php_user_cache_context_state = {
+ .storage = { .lock_file = -1 },
+ .name = "user cache",
+ .lock_name = "php_user_cache_lock",
+#ifndef ZEND_WIN32
+ .sem_filename_prefix = PHP_USER_CACHE_SEM_FILENAME_PREFIX,
+#endif
+ .clear_on_pressure = true,
+ .strict_store_failure = false,
+};
+bool php_user_cache_runtime_opted_in = false;
+/* Read-only after module startup. */
+php_user_cache_partition *php_user_cache_partitions = NULL;
+
+static zend_string *user_cache_storage_key(
+ php_user_cache_object *cache,
+ zend_string *key
+);
+static void user_cache_object_free(zend_object *obj);
+static zend_object *user_cache_object_create(zend_class_entry *ce);
+static void user_cache_pool_status_object_free(zend_object *obj);
+static zend_object *user_cache_pool_status_object_create(zend_class_entry *ce);
+static zend_object *user_cache_status_object_create(zend_class_entry *ce);
+static bool user_cache_store_storage_key_prevalidated(zend_string *key, zval *value, zend_long ttl, bool add_only);
+
+#ifndef ZEND_WIN32
+static void user_cache_pid_atfork_child(void)
+{
+ php_user_cache_self_pid = 0;
+}
+#endif
+
+static bool user_cache_user_key_is_valid(zend_string *key)
+{
+ return ZSTR_LEN(key) != 0 &&
+ memchr(ZSTR_VAL(key), PHP_USER_CACHE_KEY_DELIMITER_CHAR, ZSTR_LEN(key)) == NULL
+ ;
+}
+
+static bool user_cache_validate_delimiter_free(zend_string *str, uint32_t arg_num, const char *empty_error)
+{
+ if (ZSTR_LEN(str) == 0) {
+ zend_argument_value_error(arg_num, "%s", empty_error);
+
+ return false;
+ }
+
+ if (memchr(ZSTR_VAL(str), PHP_USER_CACHE_KEY_DELIMITER_CHAR, ZSTR_LEN(str)) != NULL) {
+ zend_argument_value_error(arg_num, "must not contain the user-cache key delimiter " PHP_USER_CACHE_KEY_DELIMITER_NAME);
+
+ return false;
+ }
+
+ return true;
+}
+
+static bool user_cache_validate_key(zend_string *key, uint32_t arg_num)
+{
+ return user_cache_validate_delimiter_free(key, arg_num, "must be a non-empty string");
+}
+
+static bool user_cache_validate_arg_value_kind(
+ zval *value,
+ uint32_t arg_num,
+ const char *resource_error,
+ const char *closure_error)
+{
+ ZVAL_DEREF(value);
+
+ if (Z_TYPE_P(value) == IS_RESOURCE) {
+ zend_argument_type_error(arg_num, "%s", resource_error);
+
+ return false;
+ }
+
+ if (Z_TYPE_P(value) == IS_OBJECT && Z_OBJCE_P(value) == zend_ce_closure) {
+ zend_argument_type_error(arg_num, "%s", closure_error);
+
+ return false;
+ }
+
+ return true;
+}
+
+static bool user_cache_validate_store_array_value(zval *value, uint32_t arg_num)
+{
+ return user_cache_validate_arg_value_kind(
+ value,
+ arg_num,
+ "must contain only values of type " PHP_USER_CACHE_API_VALUE_TYPE ", resource given",
+ "must not contain Closure objects"
+ );
+}
+
+static bool 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 && !user_cache_user_key_is_valid(key)) {
+ zend_argument_value_error(arg_num, "must be an array with non-empty string or int keys that do not contain " PHP_USER_CACHE_KEY_DELIMITER_NAME);
+
+ return false;
+ }
+
+ if (!user_cache_validate_store_array_value(value, arg_num)) {
+ return false;
+ }
+ } ZEND_HASH_FOREACH_END();
+
+ return true;
+}
+
+static void user_cache_release_key_list(zend_string **keys, uint32_t count)
+{
+ uint32_t i;
+
+ if (keys == NULL) {
+ return;
+ }
+
+ for (i = 0; i < count; i++) {
+ zend_string_release(keys[i]);
+ }
+
+ efree(keys);
+}
+
+static bool user_cache_prepare_key_list(
+ HashTable *keys,
+ uint32_t arg_num,
+ zend_string ***prepared_keys,
+ uint32_t *prepared_count)
+{
+ zend_string **prepared;
+ zval *value;
+ uint32_t count, i = 0;
+
+ ZEND_ASSERT(prepared_keys != NULL);
+ ZEND_ASSERT(prepared_count != NULL);
+
+ count = zend_hash_num_elements(keys);
+
+ *prepared_keys = NULL;
+ *prepared_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 (!user_cache_user_key_is_valid(Z_STR_P(value))) {
+ zend_argument_value_error(arg_num, "must contain only non-empty string or int cache keys that do not contain " PHP_USER_CACHE_KEY_DELIMITER_NAME);
+ user_cache_release_key_list(prepared, i);
+
+ return false;
+ }
+
+ prepared[i++] = zend_string_copy(Z_STR_P(value));
+ } else if (Z_TYPE_P(value) == IS_LONG) {
+ prepared[i++] = 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 " PHP_USER_CACHE_KEY_DELIMITER_NAME);
+ user_cache_release_key_list(prepared, i);
+
+ return false;
+ }
+ } ZEND_HASH_FOREACH_END();
+
+ *prepared_keys = prepared;
+ *prepared_count = i;
+
+ return true;
+}
+
+static bool user_cache_validate_api_value(zval *value, uint32_t arg_num)
+{
+ return user_cache_validate_arg_value_kind(
+ value,
+ arg_num,
+ "must be of type " PHP_USER_CACHE_API_VALUE_TYPE ", resource given",
+ "must not be a Closure object"
+ );
+}
+
+static bool user_cache_validate_remember_value(zval *value)
+{
+ ZVAL_DEREF(value);
+
+ if (Z_TYPE_P(value) == IS_RESOURCE) {
+ zend_type_error(PHP_USER_CACHE_MSG_RESOURCE_UNSTORABLE);
+
+ return false;
+ }
+
+ if (Z_TYPE_P(value) == IS_OBJECT && Z_OBJCE_P(value) == zend_ce_closure) {
+ zend_type_error(PHP_USER_CACHE_MSG_CLOSURE_UNSTORABLE);
+
+ return false;
+ }
+
+ return true;
+}
+
+static zend_string *user_cache_build_scope_prefix(zend_string *scope)
+{
+ const char *prefix = "user_cache" PHP_USER_CACHE_KEY_DELIMITER;
+ size_t prefix_len = sizeof("user_cache" PHP_USER_CACHE_KEY_DELIMITER) - 1;
+
+ return zend_string_concat3(
+ prefix,
+ prefix_len,
+ ZSTR_VAL(scope),
+ ZSTR_LEN(scope),
+ ZEND_STRL(PHP_USER_CACHE_KEY_DELIMITER)
+ );
+}
+
+PHP_USER_CACHE_DEFINE_OBJ_FROM_STD(php_user_cache_object)
+
+PHP_USER_CACHE_DEFINE_OBJ_FROM_STD(php_user_cache_pool_status_object)
+
+PHP_USER_CACHE_DEFINE_OBJ_FROM_STD(php_user_cache_status_object)
+
+static php_user_cache_object *user_cache_object_from_this(zval *this_ptr)
+{
+ php_user_cache_object *cache = php_user_cache_object_from_obj(Z_OBJ_P(this_ptr));
+
+ ZEND_ASSERT(cache->context == php_user_cache_active_context());
+
+ if (cache->scope_prefix == NULL) {
+ zend_throw_error(NULL, "UserCache\\Cache instance was not initialized");
+
+ return NULL;
+ }
+
+ return cache;
+}
+
+static php_user_cache_pool_status_object *user_cache_pool_status_from_this(zval *this_ptr)
+{
+ php_user_cache_pool_status_object *status =
+ php_user_cache_pool_status_object_from_obj(Z_OBJ_P(this_ptr))
+ ;
+
+ if (status->scope == NULL || status->scope_prefix == NULL || status->context == NULL) {
+ zend_throw_error(NULL, "UserCache\\CachePoolStatus instance was not initialized");
+
+ return NULL;
+ }
+
+ return status;
+}
+
+static php_user_cache_status_object *user_cache_status_from_this(zval *this_ptr)
+{
+ php_user_cache_status_object *status =
+ php_user_cache_status_object_from_obj(Z_OBJ_P(this_ptr))
+ ;
+
+ if (!status->initialized) {
+ zend_throw_error(NULL, "UserCache\\CacheStatus instance was not initialized");
+
+ return NULL;
+ }
+
+ return status;
+}
+
+static void user_cache_register_classes(void)
+{
+ if (php_user_cache_ce != NULL) {
+ return;
+ }
+
+ php_user_cache_exception_ce = zend_ce_exception;
+ php_user_cache_availability_ce = register_class_UserCache_CacheAvailability();
+ php_user_cache_status_ce = register_class_UserCache_CacheStatus();
+ php_user_cache_pool_status_ce = register_class_UserCache_CachePoolStatus();
+ php_user_cache_ce = register_class_UserCache_Cache();
+
+ php_user_cache_ce->create_object = user_cache_object_create;
+ php_user_cache_status_ce->create_object = user_cache_status_object_create;
+ php_user_cache_pool_status_ce->create_object = user_cache_pool_status_object_create;
+
+ memcpy(
+ &php_user_cache_object_handlers,
+ zend_get_std_object_handlers(),
+ sizeof(zend_object_handlers)
+ );
+
+ php_user_cache_object_handlers.offset = offsetof(php_user_cache_object, std);
+ php_user_cache_object_handlers.free_obj = user_cache_object_free;
+ php_user_cache_object_handlers.clone_obj = NULL;
+
+ memcpy(
+ &php_user_cache_status_object_handlers,
+ zend_get_std_object_handlers(),
+ sizeof(zend_object_handlers)
+ );
+
+ php_user_cache_status_object_handlers.offset = offsetof(php_user_cache_status_object, std);
+ php_user_cache_status_object_handlers.clone_obj = NULL;
+
+ memcpy(
+ &php_user_cache_pool_status_object_handlers,
+ zend_get_std_object_handlers(),
+ sizeof(zend_object_handlers)
+ );
+
+ php_user_cache_pool_status_object_handlers.offset = offsetof(php_user_cache_pool_status_object, std);
+ php_user_cache_pool_status_object_handlers.free_obj = user_cache_pool_status_object_free;
+ php_user_cache_pool_status_object_handlers.clone_obj = NULL;
+}
+
+static void user_cache_reset_class_entries(void)
+{
+ php_user_cache_exception_ce = NULL;
+ php_user_cache_availability_ce = NULL;
+ php_user_cache_status_ce = NULL;
+ php_user_cache_pool_status_ce = NULL;
+ php_user_cache_ce = NULL;
+}
+
+static void user_cache_invalidate_context(php_user_cache_context *ctx)
+{
+ php_user_cache_context *prev_ctx;
+
+ prev_ctx = php_user_cache_activate_context(ctx);
+
+ if (UC_G(shm_size) == 0 || !ctx->storage.initialized) {
+ php_user_cache_restore_context(prev_ctx);
+
+ return;
+ }
+
+ if (!php_user_cache_wlock()) {
+ php_user_cache_restore_context(prev_ctx);
+
+ return;
+ }
+
+ if (php_user_cache_header_is_initialized_locked()) {
+ php_user_cache_clear_locked();
+ }
+
+ php_user_cache_unlock();
+ php_user_cache_restore_context(prev_ctx);
+}
+
+static bool user_cache_validate_non_negative(zend_long value, uint32_t arg_num)
+{
+ if (value < 0) {
+ zend_argument_value_error(arg_num, "must be greater than or equal to 0");
+
+ return false;
+ }
+
+ return true;
+}
+
+static bool user_cache_can_read(void)
+{
+ php_user_cache_ensure_ready();
+
+ return php_user_cache_active_runtime()->available;
+}
+
+static bool user_cache_begin_read(void)
+{
+ if (!user_cache_can_read()) {
+ return false;
+ }
+
+ if (!php_user_cache_rlock()) {
+ return false;
+ }
+
+ if (!php_user_cache_header_is_initialized_locked()) {
+ php_user_cache_unlock();
+
+ return false;
+ }
+
+ return true;
+}
+
+static bool user_cache_can_write(void)
+{
+ return user_cache_can_read();
+}
+
+static bool user_cache_begin_write(void)
+{
+ if (!php_user_cache_wlock()) {
+ return false;
+ }
+
+ if (!php_user_cache_header_init_locked()) {
+ php_user_cache_unlock();
+
+ return false;
+ }
+
+ return true;
+}
+
+static bool user_cache_delete_storage_key_prevalidated(zend_string *key)
+{
+ if (!php_user_cache_wlock_for_entry_mutation(key)) {
+ return false;
+ }
+
+ php_user_cache_delete_locked(key);
+
+ php_user_cache_unlock();
+
+ return true;
+}
+
+static void user_cache_delete_corrupt_entry_prevalidated(zend_string *key)
+{
+ if (EG(exception)) {
+ return;
+ }
+
+ (void) user_cache_delete_storage_key_prevalidated(key);
+}
+
+static bool user_cache_clear_scope_prevalidated(
+ zend_string *scope_prefix)
+{
+ if (!user_cache_begin_write()) {
+ return false;
+ }
+
+ php_user_cache_delete_by_prefix_locked(scope_prefix);
+
+ php_user_cache_unlock();
+ php_user_cache_release_active_request_local_slots_by_prefix(scope_prefix);
+
+ return true;
+}
+
+static bool user_cache_fetch_if_present_api(
+ zend_string *key,
+ zval *return_value)
+{
+ php_user_cache_fetch_pending_seed pending_seed;
+ bool fetched, entry_found = false;
+
+ if (!user_cache_can_read()) {
+ return false;
+ }
+
+ UC_G(stack_overflowed) = false;
+ switch (php_user_cache_fetch_optimistic(key, return_value)) {
+ case PHP_USER_CACHE_OPTIMISTIC_FOUND:
+ return true;
+ case PHP_USER_CACHE_OPTIMISTIC_MISS:
+ return false;
+ case PHP_USER_CACHE_OPTIMISTIC_FALLBACK:
+ break;
+ }
+
+ if (EG(exception)) {
+ /* A stack overflow in the lock-free path is not a real error and would
+ * recur under the read lock, so surface a miss and drop the exception. */
+ if (UC_G(stack_overflowed)) {
+ UC_G(stack_overflowed) = false;
+ zend_clear_exception();
+
+ return false;
+ }
+
+ return false;
+ }
+
+ if (!user_cache_begin_read()) {
+ return false;
+ }
+
+ UC_G(stack_overflowed) = false;
+ fetched = php_user_cache_fetch_locked(key, false, true, return_value, &entry_found, &pending_seed);
+
+ php_user_cache_unlock();
+
+ if (fetched) {
+ /* Clone outside the shared-memory lock. */
+ if (pending_seed.should_seed_request_local_slot) {
+ php_user_cache_fetch_finish(key, pending_seed.generation, return_value, pending_seed.flags);
+ }
+
+ return true;
+ }
+
+ if (!entry_found) {
+ return false;
+ }
+
+ if (UC_G(stack_overflowed)) {
+ UC_G(stack_overflowed) = false;
+ if (EG(exception)) {
+ zend_clear_exception();
+ }
+
+ return false;
+ }
+
+ if (EG(exception)) {
+ return false;
+ }
+
+ /* Decode failed without an exception: the entry is corrupt, drop it. */
+ user_cache_delete_corrupt_entry_prevalidated(key);
+
+ return false;
+}
+
+static void user_cache_fetch_api(
+ zend_string *key,
+ zval *default_value,
+ zval *return_value)
+{
+ if (!user_cache_fetch_if_present_api(key, return_value)) {
+ ZVAL_COPY(return_value, default_value);
+ }
+}
+
+static void user_cache_fetch_multiple_fill_defaults(
+ zend_string **prepared_keys,
+ zend_string **storage_keys,
+ uint32_t count,
+ zval *default_value,
+ zval *return_value)
+{
+ zval default_copy;
+ uint32_t i;
+
+ for (i = 0; i < count; i++) {
+ ZVAL_COPY(&default_copy, default_value);
+ zend_symtable_update(Z_ARRVAL_P(return_value), prepared_keys[i], &default_copy);
+ zend_string_release(storage_keys[i]);
+ }
+}
+
+/* A corrupt entry drops the batch lock and switches subsequent reads to the
+ * self-locking path. */
+static void user_cache_fetch_multiple_fetch_one(
+ zend_string *storage_key,
+ zval *default_value,
+ bool *rlock_held,
+ zval *out,
+ php_user_cache_fetch_pending_seed *pending_seed)
+{
+ bool fetched, found;
+
+ pending_seed->should_seed_request_local_slot = false;
+
+ if (!*rlock_held) {
+ user_cache_fetch_api(storage_key, default_value, out);
+
+ return;
+ }
+
+ UC_G(stack_overflowed) = false;
+ fetched = php_user_cache_fetch_locked(storage_key, false, true, out, &found, pending_seed);
+ if (fetched) {
+ return;
+ }
+
+ if (!found) {
+ ZVAL_COPY(out, default_value);
+ } else if (UC_G(stack_overflowed)) {
+ UC_G(stack_overflowed) = false;
+
+ if (EG(exception)) {
+ zend_clear_exception();
+ }
+
+ ZVAL_COPY(out, default_value);
+ } else if (EG(exception)) {
+ /* A restore hook or autoloader threw: propagate the userland exception
+ * and keep the entry. Drop the batch lock so the caller can unwind. */
+ php_user_cache_unlock();
+ *rlock_held = false;
+
+ ZVAL_UNDEF(out);
+ } else {
+ php_user_cache_unlock();
+ *rlock_held = false;
+ user_cache_delete_corrupt_entry_prevalidated(storage_key);
+
+ ZVAL_COPY(out, default_value);
+ }
+}
+
+static void user_cache_finish_fetch_multiple(
+ zend_string **prepared_keys,
+ zend_string **storage_keys,
+ const php_user_cache_fetch_pending_seed *pending_seeds,
+ uint32_t count,
+ zval *return_value)
+{
+ zval *val;
+ uint32_t i;
+
+ for (i = 0; i < count; i++) {
+ if (pending_seeds[i].should_seed_request_local_slot) {
+ val = zend_symtable_find(Z_ARRVAL_P(return_value), prepared_keys[i]);
+ if (val != NULL) {
+ php_user_cache_fetch_finish(
+ storage_keys[i],
+ pending_seeds[i].generation,
+ val,
+ pending_seeds[i].flags
+ );
+ }
+ }
+
+ zend_string_release(storage_keys[i]);
+ }
+}
+
+static zend_result user_cache_fetch_multiple_api(
+ php_user_cache_object *cache,
+ HashTable *keys,
+ zval *default_value,
+ zval *return_value)
+{
+ php_user_cache_fetch_pending_seed *pending_seeds;
+ zend_string **prepared_keys, **storage_keys, *key, *storage_key;
+ zval val;
+ uint32_t count, i;
+ bool rlock_held;
+
+ if (!user_cache_prepare_key_list(keys, 1, &prepared_keys, &count)) {
+ return FAILURE;
+ }
+
+ /* Do not allocate while holding the read lock. */
+ storage_keys = NULL;
+ pending_seeds = NULL;
+ if (count != 0) {
+ storage_keys = safe_emalloc(count, sizeof(zend_string *), 0);
+ pending_seeds = safe_emalloc(count, sizeof(php_user_cache_fetch_pending_seed), 0);
+
+ for (i = 0; i < count; i++) {
+ storage_keys[i] = user_cache_storage_key(cache, prepared_keys[i]);
+ }
+ }
+
+ array_init_size(return_value, count);
+
+ if (!user_cache_begin_read()) {
+ user_cache_fetch_multiple_fill_defaults(
+ prepared_keys,
+ storage_keys,
+ count,
+ default_value,
+ return_value
+ );
+
+ if (storage_keys != NULL) {
+ efree(storage_keys);
+ }
+
+ if (pending_seeds != NULL) {
+ efree(pending_seeds);
+ }
+
+ user_cache_release_key_list(prepared_keys, count);
+
+ return SUCCESS;
+ }
+
+ rlock_held = true;
+
+ zend_try {
+ for (i = 0; i < count; i++) {
+ key = prepared_keys[i];
+ storage_key = storage_keys[i];
+
+ user_cache_fetch_multiple_fetch_one(
+ storage_key,
+ default_value,
+ &rlock_held,
+ &val,
+ &pending_seeds[i]
+ );
+
+ /* A restore hook or autoloader threw: abort the batch and let the
+ * userland exception propagate, matching native unserialize(). */
+ if (EG(exception)) {
+ zval_ptr_dtor(&val);
+
+ break;
+ }
+
+ zend_symtable_update(Z_ARRVAL_P(return_value), key, &val);
+ }
+ } zend_catch {
+ php_user_cache_unlock_if_held();
+ zend_bailout();
+ } zend_end_try();
+
+ if (rlock_held) {
+ php_user_cache_unlock();
+ }
+
+ if (EG(exception)) {
+ zval_ptr_dtor(return_value);
+ ZVAL_UNDEF(return_value);
+
+ for (i = 0; i < count; i++) {
+ zend_string_release(storage_keys[i]);
+ }
+
+ if (storage_keys != NULL) {
+ efree(storage_keys);
+ }
+
+ if (pending_seeds != NULL) {
+ efree(pending_seeds);
+ }
+
+ user_cache_release_key_list(prepared_keys, count);
+
+ return FAILURE;
+ }
+
+ /* Clone outside the shared-memory lock. */
+ user_cache_finish_fetch_multiple(prepared_keys, storage_keys, pending_seeds, count, return_value);
+
+ if (storage_keys != NULL) {
+ efree(storage_keys);
+ }
+
+ if (pending_seeds != NULL) {
+ efree(pending_seeds);
+ }
+
+ user_cache_release_key_list(prepared_keys, count);
+
+ return SUCCESS;
+}
+
+static bool user_cache_atomic_update_api(
+ zend_string *key,
+ zend_long step,
+ zend_long ttl,
+ bool decrement,
+ php_user_cache_atomic_update_result *result)
+{
+ bool updated;
+
+ if (!user_cache_can_write() ||
+ !php_user_cache_wlock_for_entry_mutation(key)
+ ) {
+ memset(result, 0, sizeof(*result));
+
+ return false;
+ }
+
+ updated = php_user_cache_atomic_update_locked(key, step, ttl, decrement, true, result);
+
+ php_user_cache_unlock();
+
+ if (result->is_overflow || result->is_type_error) {
+ updated = false;
+ }
+
+ return updated;
+}
+
+static bool user_cache_exists_api(zend_string *key)
+{
+ bool exists;
+
+ if (!user_cache_can_read()) {
+ return false;
+ }
+
+ switch (php_user_cache_exists_optimistic(key)) {
+ case PHP_USER_CACHE_OPTIMISTIC_FOUND:
+ return true;
+ case PHP_USER_CACHE_OPTIMISTIC_MISS:
+ return false;
+ case PHP_USER_CACHE_OPTIMISTIC_FALLBACK:
+ break;
+ }
+
+ if (!user_cache_begin_read()) {
+ return false;
+ }
+
+ exists = php_user_cache_exists_locked(key);
+
+ php_user_cache_unlock();
+
+ return exists;
+}
+
+static bool user_cache_lock_api(
+ zend_string *key,
+ zend_long lease)
+{
+ if (!user_cache_can_write()) {
+ return false;
+ }
+
+ return php_user_cache_try_acquire_entry_lock(key, lease);
+}
+
+static bool user_cache_unlock_api(zend_string *key)
+{
+ if (!user_cache_can_write()) {
+ return false;
+ }
+
+ return php_user_cache_release_entry_lock(key);
+}
+
+static zend_long user_cache_count_to_zend_long(uint64_t count)
+{
+ return count > (uint64_t) ZEND_LONG_MAX ? ZEND_LONG_MAX : (zend_long) count;
+}
+
+static zend_long user_cache_size_to_zend_long(size_t size)
+{
+ return user_cache_count_to_zend_long(size);
+}
+
+/* Caller must hold the read lock. */
+static size_t user_cache_sum_wasted_memory_locked(php_user_cache_header *header)
+{
+ php_user_cache_block *block;
+ uint32_t block_offset, block_limit, next_offset, iter_limit, iters;
+ size_t wasted_memory = 0;
+
+ block_limit = header->data_offset + header->next_free;
+ iter_limit = (header->data_size / ((uint32_t) sizeof(php_user_cache_block)));
+ for (block_offset = header->free_list, iters = 0;
+ block_offset != 0 && iters < iter_limit;
+ iters++
+ ) {
+ if (block_offset < header->data_offset || block_offset > block_limit - sizeof(php_user_cache_block)) {
+ break;
+ }
+
+ block = php_user_cache_block_ptr(block_offset);
+ next_offset = block_offset + block->size;
+ if (block->size < sizeof(php_user_cache_block) || next_offset > block_limit) {
+ break;
+ }
+
+ wasted_memory += block->size;
+ block_offset = block->next_free;
+ }
+
+ return wasted_memory;
+}
+
+static void user_cache_collect_info_stats(php_user_cache_info_stats *stats)
+{
+ php_user_cache_runtime *runtime;
+ php_user_cache_storage *storage;
+ php_user_cache_header *header;
+ size_t free_memory = 0, wasted_memory = 0, tail_memory = 0;
+
+ memset(stats, 0, sizeof(*stats));
+
+ runtime = php_user_cache_active_runtime();
+ storage = &php_user_cache_active_context()->storage;
+
+ stats->configured_memory = user_cache_size_to_zend_long(runtime->configured_memory);
+ stats->shared_memory_size = user_cache_size_to_zend_long(storage->size);
+
+ if (!storage->initialized ||
+ !storage->lock_initialized ||
+ !php_user_cache_rlock()
+ ) {
+ return;
+ }
+
+ header = php_user_cache_header_ptr();
+ if (header == NULL || !php_user_cache_header_is_initialized_locked()) {
+ stats->free_memory = stats->shared_memory_size;
+ php_user_cache_unlock();
+
+ return;
+ }
+
+ if (header->next_free <= header->data_size) {
+ tail_memory = header->data_size - header->next_free;
+ }
+
+ wasted_memory = user_cache_sum_wasted_memory_locked(header);
+ 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->expunge_count = user_cache_count_to_zend_long(header->expunge_count);
+ stats->store_failure_count = user_cache_count_to_zend_long(header->store_failure_count);
+ stats->free_memory = user_cache_size_to_zend_long(free_memory);
+ stats->wasted_memory = user_cache_size_to_zend_long(wasted_memory);
+ stats->used_memory = storage->size > free_memory
+ ? user_cache_size_to_zend_long(storage->size - free_memory)
+ : 0
+ ;
+
+ php_user_cache_unlock();
+}
+
+/* No default: the compiler should diagnose a missing enum case. */
+static zend_object *user_cache_availability_enum_case(php_user_cache_reason reason)
+{
+ int case_id = ZEND_ENUM_UserCache_CacheAvailability_UnavailableByUnknownReason;
+
+ switch (reason) {
+ case PHP_USER_CACHE_REASON_NONE:
+ case_id = ZEND_ENUM_UserCache_CacheAvailability_Available;
+ break;
+ case PHP_USER_CACHE_REASON_DISABLED_BY_INI:
+ case_id = ZEND_ENUM_UserCache_CacheAvailability_DisabledByIni;
+ break;
+ case PHP_USER_CACHE_REASON_SHM_INIT_FAILED:
+ case_id = ZEND_ENUM_UserCache_CacheAvailability_UnavailableBySharedMemoryInitializationFailed;
+ break;
+ case PHP_USER_CACHE_REASON_SAPI_NOT_ENABLED:
+ case_id = ZEND_ENUM_UserCache_CacheAvailability_DisabledBySapi;
+ break;
+ case PHP_USER_CACHE_REASON_BACKEND_NOT_INITIALIZED_BEFORE_WORKER:
+ case_id = ZEND_ENUM_UserCache_CacheAvailability_UnavailableByBackendNotInitializedBeforeWorkerStartup;
+ break;
+ case PHP_USER_CACHE_REASON_BACKEND_INITIALIZED_AFTER_WORKER:
+ case_id = ZEND_ENUM_UserCache_CacheAvailability_UnavailableByBackendInitializedAfterWorkerStartup;
+ break;
+ case PHP_USER_CACHE_REASON_CGI_BOUNDARY_UNAVAILABLE:
+ case_id = ZEND_ENUM_UserCache_CacheAvailability_UnavailableByCgiFastCgiBoundary;
+ break;
+ case PHP_USER_CACHE_REASON_LSAPI_BOUNDARY_UNAVAILABLE:
+ case_id = ZEND_ENUM_UserCache_CacheAvailability_UnavailableByLsapiBoundary;
+ break;
+ }
+
+ return zend_enum_get_case_by_id(php_user_cache_availability_ce, case_id);
+}
+
+static void user_cache_return_status(zval *return_value)
+{
+ php_user_cache_status_object *status;
+
+ php_user_cache_ensure_ready();
+
+ object_init_ex(return_value, php_user_cache_status_ce);
+ status = php_user_cache_status_object_from_obj(Z_OBJ_P(return_value));
+
+ status->availability_reason = php_user_cache_active_runtime()->failure_reason;
+ user_cache_collect_info_stats(&status->stats);
+
+ status->initialized = true;
+}
+
+static void user_cache_add_pool_memory(size_t *used_memory, size_t size)
+{
+ if (*used_memory >= (size_t) ZEND_LONG_MAX ||
+ size > (size_t) ZEND_LONG_MAX - *used_memory
+ ) {
+ *used_memory = (size_t) ZEND_LONG_MAX;
+
+ return;
+ }
+
+ *used_memory += size;
+}
+
+static size_t user_cache_payload_block_size(uint32_t payload_offset)
+{
+ php_user_cache_block *block;
+
+ if (payload_offset < sizeof(php_user_cache_block)) {
+ return 0;
+ }
+
+ block = php_user_cache_block_ptr(payload_offset - sizeof(php_user_cache_block));
+ if (block->size < sizeof(php_user_cache_block)) {
+ return 0;
+ }
+
+ return block->size;
+}
+
+static void user_cache_account_pool_entry(
+ const php_user_cache_entry *entry,
+ zend_string *prefix,
+ zend_long *entry_count,
+ size_t *used_memory,
+ zval *entry_keys)
+{
+ const char *key;
+ size_t prefix_len;
+ bool combined_value_key;
+
+ combined_value_key = (entry->reserved & PHP_USER_CACHE_ENTRY_RESERVED_COMBINED_VALUE_KEY) != 0;
+ key = (const char *) php_user_cache_ptr(entry->key_offset);
+ prefix_len = ZSTR_LEN(prefix);
+
+ (*entry_count)++;
+
+ add_next_index_stringl(entry_keys, key + prefix_len, entry->key_len - prefix_len);
+
+ user_cache_add_pool_memory(used_memory, sizeof(php_user_cache_entry));
+ if (combined_value_key) {
+ user_cache_add_pool_memory(
+ used_memory,
+ user_cache_payload_block_size(entry->value_offset)
+ );
+ } else {
+ user_cache_add_pool_memory(
+ used_memory,
+ user_cache_payload_block_size(entry->key_offset)
+ );
+ user_cache_add_pool_memory(
+ used_memory,
+ user_cache_payload_block_size(entry->value_offset)
+ );
+ }
+}
+
+static void user_cache_collect_pool_status(
+ php_user_cache_context *ctx,
+ zend_string *prefix,
+ zend_long *entry_count,
+ zend_long *used_memory,
+ zval *entry_keys)
+{
+ php_user_cache_context *prev_ctx;
+ php_user_cache_storage *storage;
+ php_user_cache_header *header;
+ php_user_cache_entry *entries, *entry;
+ uint32_t i;
+ size_t used_size = 0;
+
+ *entry_count = 0;
+ *used_memory = 0;
+
+ prev_ctx = php_user_cache_activate_context(ctx);
+
+ storage = &php_user_cache_active_context()->storage;
+ if (!storage->initialized || !storage->lock_initialized || !php_user_cache_rlock()) {
+ php_user_cache_restore_context(prev_ctx);
+
+ return;
+ }
+
+ header = php_user_cache_header_ptr();
+ if (header == NULL ||
+ !php_user_cache_header_is_initialized_locked()
+ ) {
+ php_user_cache_unlock();
+ php_user_cache_restore_context(prev_ctx);
+
+ return;
+ }
+
+ entries = php_user_cache_entries_ptr(header);
+
+ /* Release the lock before propagating an allocation bailout. */
+ zend_try {
+ for (i = 0; i < header->capacity; i++) {
+ entry = &entries[i];
+ if (entry->state == PHP_USER_CACHE_ENTRY_USED &&
+ entry->key_len >= ZSTR_LEN(prefix) &&
+ memcmp(php_user_cache_ptr(entry->key_offset), ZSTR_VAL(prefix), ZSTR_LEN(prefix)) == 0
+ ) {
+ user_cache_account_pool_entry(
+ entry,
+ prefix,
+ entry_count,
+ &used_size,
+ entry_keys
+ );
+ }
+ }
+ } zend_catch {
+ php_user_cache_unlock_if_held();
+ php_user_cache_restore_context(prev_ctx);
+ zend_bailout();
+ } zend_end_try();
+
+ php_user_cache_unlock();
+ php_user_cache_restore_context(prev_ctx);
+
+ *used_memory = user_cache_size_to_zend_long(used_size);
+}
+
+static void user_cache_return_pool_status(
+ php_user_cache_object *cache,
+ zval *return_value)
+{
+ php_user_cache_pool_status_object *status;
+
+ php_user_cache_ensure_ready();
+
+ object_init_ex(return_value, php_user_cache_pool_status_ce);
+
+ status = php_user_cache_pool_status_object_from_obj(Z_OBJ_P(return_value));
+ status->scope = zend_string_copy(cache->scope);
+ status->scope_prefix = zend_string_copy(cache->scope_prefix);
+ status->context = cache->context;
+
+ array_init(&status->entry_keys);
+
+ user_cache_collect_pool_status(
+ status->context,
+ status->scope_prefix,
+ &status->entry_count,
+ &status->used_memory,
+ &status->entry_keys
+ );
+}
+
+static void user_cache_object_free(zend_object *obj)
+{
+ php_user_cache_object *cache = php_user_cache_object_from_obj(obj);
+
+ 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 void user_cache_pool_status_object_free(zend_object *obj)
+{
+ php_user_cache_pool_status_object *status =
+ php_user_cache_pool_status_object_from_obj(obj)
+ ;
+
+ if (status->scope != NULL) {
+ zend_string_release(status->scope);
+ }
+
+ if (status->scope_prefix != NULL) {
+ zend_string_release(status->scope_prefix);
+ }
+
+ zval_ptr_dtor(&status->entry_keys);
+
+ zend_object_std_dtor(&status->std);
+}
+
+static zend_object *user_cache_object_create(zend_class_entry *ce)
+{
+ php_user_cache_object *cache;
+
+ cache = zend_object_alloc(sizeof(php_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 = php_user_cache_owning_context();
+ cache->std.handlers = &php_user_cache_object_handlers;
+
+ return &cache->std;
+}
+
+static zend_object *user_cache_pool_status_object_create(zend_class_entry *ce)
+{
+ php_user_cache_pool_status_object *status;
+
+ status = zend_object_alloc(sizeof(php_user_cache_pool_status_object), ce);
+
+ zend_object_std_init(&status->std, ce);
+ object_properties_init(&status->std, ce);
+
+ status->scope = NULL;
+ status->scope_prefix = NULL;
+ status->context = NULL;
+ status->entry_count = 0;
+ status->used_memory = 0;
+
+ ZVAL_UNDEF(&status->entry_keys);
+
+ status->std.handlers = &php_user_cache_pool_status_object_handlers;
+
+ return &status->std;
+}
+
+static zend_object *user_cache_status_object_create(zend_class_entry *ce)
+{
+ php_user_cache_status_object *status;
+
+ status = zend_object_alloc(sizeof(php_user_cache_status_object), ce);
+
+ zend_object_std_init(&status->std, ce);
+ object_properties_init(&status->std, ce);
+
+ memset(&status->stats, 0, sizeof(status->stats));
+
+ status->availability_reason = PHP_USER_CACHE_REASON_NONE;
+ status->initialized = false;
+ status->std.handlers = &php_user_cache_status_object_handlers;
+
+ return &status->std;
+}
+
+static HashTable *user_cache_pools(void)
+{
+ if (UC_G(pool_table) == NULL) {
+ ALLOC_HASHTABLE(UC_G(pool_table));
+ zend_hash_init(UC_G(pool_table), 8, NULL, ZVAL_PTR_DTOR, 0);
+ }
+
+ return UC_G(pool_table);
+}
+
+static void user_cache_release_pools(void)
+{
+ if (UC_G(pool_table) == NULL) {
+ return;
+ }
+
+ zend_hash_destroy(UC_G(pool_table));
+ FREE_HASHTABLE(UC_G(pool_table));
+
+ UC_G(pool_table) = NULL;
+}
+
+static bool user_cache_validate_pool_name(zend_string *pool, uint32_t arg_num)
+{
+ return user_cache_validate_delimiter_free(pool, arg_num, "must not be empty");
+}
+
+static zend_object *user_cache_get_or_create_pool(zend_string *pool)
+{
+ php_user_cache_object *cache;
+ zend_object *obj;
+ zval *existing, pool_zv;
+ HashTable *pools = user_cache_pools();
+
+ existing = zend_hash_find(pools, pool);
+ if (existing != NULL) {
+ return Z_OBJ_P(existing);
+ }
+
+ obj = user_cache_object_create(php_user_cache_ce);
+
+ cache = php_user_cache_object_from_obj(obj);
+ cache->scope = zend_string_copy(pool);
+ cache->scope_prefix = user_cache_build_scope_prefix(pool);
+
+ ZVAL_OBJ(&pool_zv, obj);
+
+ if (zend_hash_add(pools, pool, &pool_zv) == NULL) {
+ OBJ_RELEASE(obj);
+
+ return NULL;
+ }
+
+ return obj;
+}
+
+static zend_string *user_cache_storage_key_cache_lookup(
+ php_user_cache_object *cache,
+ zend_string *key)
+{
+ zval *cached;
+
+ if (cache->storage_key_cache == NULL) {
+ return NULL;
+ }
+
+ cached = zend_hash_find(cache->storage_key_cache, key);
+ if (cached == NULL) {
+ return NULL;
+ }
+
+ return zend_string_copy(Z_STR_P(cached));
+}
+
+static zend_string *user_cache_storage_key_build(
+ php_user_cache_object *cache,
+ zend_string *key)
+{
+ zend_string *storage_key;
+ zval cache_zv;
+
+ storage_key = zend_string_concat2(
+ ZSTR_VAL(cache->scope_prefix),
+ ZSTR_LEN(cache->scope_prefix),
+ ZSTR_VAL(key),
+ ZSTR_LEN(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) < PHP_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 zend_string *user_cache_storage_key(
+ php_user_cache_object *cache,
+ zend_string *key)
+{
+ zend_string *storage_key = user_cache_storage_key_cache_lookup(cache, key);
+
+ if (storage_key != NULL) {
+ return storage_key;
+ }
+
+ return user_cache_storage_key_build(cache, key);
+}
+
+static zend_string *user_cache_validated_storage_key(
+ php_user_cache_object *cache,
+ zend_string *key,
+ uint32_t arg_num)
+{
+ zend_string *storage_key = user_cache_storage_key_cache_lookup(cache, key);
+
+ if (storage_key != NULL) {
+ return storage_key;
+ }
+
+ if (!user_cache_validate_key(key, arg_num)) {
+ return NULL;
+ }
+
+ return user_cache_storage_key_build(cache, key);
+}
+
+static void user_cache_safe_direct_handlers_dtor(zval *zv)
+{
+ pefree(Z_PTR_P(zv), true);
+}
+
+const php_user_cache_safe_direct_handlers *php_user_cache_safe_direct_find_handlers(
+ zend_class_entry *ce,
+ zend_class_entry **base_ce_ptr)
+{
+ const php_user_cache_safe_direct_handlers *handlers;
+
+ if (!php_user_cache_safe_direct_handlers_initialized) {
+ return NULL;
+ }
+
+ while (ce != NULL) {
+ handlers = zend_hash_index_find_ptr(
+ &php_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 user_cache_store_storage_key_prevalidated(zend_string *key, zval *value, zend_long ttl, bool add_only)
+{
+ php_user_cache_prepare_options prep_opts = {
+ .caller_holds_write_lock = false,
+ .throw_on_failure = false,
+ .honor_strict_store_failure = false,
+ };
+ php_user_cache_store_options store_opts = {
+ .retry_after_memory_pressure = true,
+ .throw_on_failure = false,
+ .honor_strict_store_failure = false,
+ .capture_replaced_entry = false,
+ };
+ php_user_cache_prepared_value prepared;
+ php_user_cache_store_result result;
+ bool stored;
+
+ if (!php_user_cache_prepare_value(key, value, &prep_opts, &prepared)) {
+ php_user_cache_destroy_prepared_value(&prepared);
+
+ return false;
+ }
+
+ if (!php_user_cache_wlock_for_entry_mutation(key)) {
+ php_user_cache_destroy_prepared_value(&prepared);
+
+ return false;
+ }
+
+ if (add_only && php_user_cache_exists_locked(key)) {
+ php_user_cache_unlock();
+ php_user_cache_destroy_prepared_value(&prepared);
+
+ return false;
+ }
+
+ zend_try {
+ stored = php_user_cache_store_prepared_locked(
+ key,
+ value,
+ &prepared,
+ ttl,
+ &store_opts,
+ &result
+ );
+ } zend_catch {
+ php_user_cache_unlock_if_held();
+ php_user_cache_destroy_prepared_value(&prepared);
+
+ zend_bailout();
+ } zend_end_try();
+
+ php_user_cache_unlock();
+ php_user_cache_destroy_prepared_value(&prepared);
+
+ if (stored && result.should_seed_request_local_slot) {
+ php_user_cache_store_request_local_slot(key, result.stored_generation, value, true);
+ }
+
+ return stored;
+}
+
+/* Entry locks must be acquired in this order. */
+static int user_cache_bulk_order_compare(const void *lhs_ptr, const void *rhs_ptr)
+{
+ const php_user_cache_bulk_order *lhs, *rhs;
+ size_t lhs_len, rhs_len, cmp_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);
+ cmp_len = lhs_len < rhs_len ? lhs_len : rhs_len;
+
+ result = memcmp(ZSTR_VAL(lhs->key), ZSTR_VAL(rhs->key), cmp_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);
+}
+
+/* Acquires locks in bulk order and records them by original index. */
+static bool user_cache_bulk_lock_keys_ordered(
+ zend_string **storage_keys,
+ php_user_cache_bulk_order *order,
+ bool *acquired,
+ uint32_t count)
+{
+ zend_string **sorted_keys;
+ uint32_t i;
+ bool *sorted_acquired, result;
+
+ for (i = 0; i < count; i++) {
+ order[i].hash = zend_string_hash_val(storage_keys[i]);
+ order[i].key = storage_keys[i];
+ order[i].index = i;
+ }
+
+ qsort(order, count, sizeof(*order), user_cache_bulk_order_compare);
+
+ sorted_keys = safe_emalloc(count, sizeof(*sorted_keys), 0);
+ sorted_acquired = safe_emalloc(count, sizeof(*sorted_acquired), 0);
+
+ for (i = 0; i < count; i++) {
+ sorted_keys[i] = storage_keys[order[i].index];
+ }
+
+ result = php_user_cache_acquire_entry_locks(sorted_keys, sorted_acquired, count);
+
+ for (i = 0; i < count; i++) {
+ acquired[order[i].index] = sorted_acquired[i];
+ }
+
+ efree(sorted_acquired);
+ efree(sorted_keys);
+
+ return result;
+}
+
+static uint32_t user_cache_prepare_bulk_store_items(
+ php_user_cache_object *cache,
+ HashTable *values,
+ php_user_cache_bulk_store_item *items,
+ zend_string **storage_keys,
+ const php_user_cache_prepare_options *prep_opts,
+ bool *result)
+{
+ zend_ulong num_key;
+ zend_string *key, *storage_key;
+ zval *value;
+ uint32_t i = 0;
+
+ *result = true;
+
+ ZEND_HASH_FOREACH_KEY_VAL(values, num_key, key, value) {
+ if (key != NULL) {
+ storage_key = user_cache_storage_key(cache, key);
+ } else {
+ key = zend_long_to_str(num_key);
+ storage_key = user_cache_storage_key(cache, key);
+
+ zend_string_release(key);
+ }
+
+ storage_keys[i] = storage_key;
+ items[i].value = value;
+
+ if (!php_user_cache_prepare_value(storage_key, value, prep_opts, &items[i].prepared)) {
+ php_user_cache_destroy_prepared_value(&items[i].prepared);
+
+ zend_string_release(storage_key);
+
+ *result = false;
+
+ break;
+ }
+
+ i++;
+ } ZEND_HASH_FOREACH_END();
+
+ return i;
+}
+
+/* Rolls back the entire batch if any store fails. */
+static uint32_t user_cache_commit_bulk_store_locked(
+ php_user_cache_bulk_store_item *items,
+ zend_string **storage_keys,
+ uint32_t prepared_count,
+ zend_long ttl,
+ const php_user_cache_store_options *store_opts,
+ bool *result)
+{
+ uint32_t i, j, stored_count = 0;
+
+ *result = true;
+
+ for (i = 0; i < prepared_count; i++) {
+ if (!php_user_cache_store_prepared_locked(
+ storage_keys[i],
+ items[i].value,
+ &items[i].prepared,
+ ttl,
+ store_opts,
+ &items[i].store_result)
+ ) {
+ *result = false;
+
+ break;
+ }
+
+ stored_count = i + 1;
+ }
+
+ if (*result) {
+ for (i = 0; i < stored_count; i++) {
+ php_user_cache_discard_replaced_entry_locked(
+ storage_keys[i],
+ &items[i].store_result.replaced_entry
+ );
+ }
+ } else {
+ for (j = stored_count; j > 0; j--) {
+ php_user_cache_rollback_replaced_entry_locked(
+ storage_keys[j - 1],
+ &items[j - 1].store_result.replaced_entry
+ );
+ }
+
+ stored_count = 0;
+ }
+
+ return stored_count;
+}
+
+static void user_cache_finish_bulk_store(
+ php_user_cache_bulk_store_item *items,
+ zend_string **storage_keys,
+ uint32_t stored_count)
+{
+ uint32_t i;
+
+ for (i = 0; i < stored_count; i++) {
+ if (items[i].store_result.should_seed_request_local_slot) {
+ php_user_cache_store_request_local_slot(
+ storage_keys[i],
+ items[i].store_result.stored_generation,
+ items[i].value,
+ true
+ );
+ }
+ }
+}
+
+static bool user_cache_instance_store_multiple(
+ php_user_cache_object *cache,
+ HashTable *values,
+ zend_long ttl)
+{
+ php_user_cache_bulk_store_item *items;
+ php_user_cache_bulk_order *order;
+ zend_string **storage_keys;
+ php_user_cache_prepare_options prep_opts = {
+ .caller_holds_write_lock = false,
+ .throw_on_failure = false,
+ .honor_strict_store_failure = false,
+ };
+ php_user_cache_store_options store_opts = {
+ .retry_after_memory_pressure = false,
+ .throw_on_failure = false,
+ .honor_strict_store_failure = false,
+ .capture_replaced_entry = true,
+ };
+ uint32_t i, count, prepared_count, stored_count = 0;
+ bool result, *acquired;
+
+ if (!user_cache_can_write()) {
+ return false;
+ }
+
+ count = zend_hash_num_elements(values);
+ if (count == 0) {
+ return true;
+ }
+
+ items = ecalloc(count, sizeof(*items));
+ order = safe_emalloc(count, sizeof(*order), 0);
+ storage_keys = safe_emalloc(count, sizeof(zend_string *), 0);
+ acquired = ecalloc(count, sizeof(bool));
+
+ prepared_count = user_cache_prepare_bulk_store_items(
+ cache,
+ values,
+ items,
+ storage_keys,
+ &prep_opts,
+ &result
+ );
+
+ if (result) {
+ result = user_cache_bulk_lock_keys_ordered(storage_keys, order, acquired, prepared_count);
+ }
+
+ if (result && user_cache_begin_write()) {
+ zend_try {
+ stored_count = user_cache_commit_bulk_store_locked(
+ items,
+ storage_keys,
+ prepared_count,
+ ttl,
+ &store_opts,
+ &result
+ );
+ } zend_catch {
+ php_user_cache_unlock_if_held();
+
+ zend_bailout();
+ } zend_end_try();
+
+ php_user_cache_unlock();
+ } else {
+ result = false;
+ }
+
+ php_user_cache_release_entry_locks(storage_keys, acquired, prepared_count);
+
+ user_cache_finish_bulk_store(items, storage_keys, stored_count);
+
+ for (i = 0; i < prepared_count; i++) {
+ php_user_cache_destroy_prepared_value(&items[i].prepared);
+
+ zend_string_release(storage_keys[i]);
+ }
+
+ efree(acquired);
+ efree(storage_keys);
+ efree(order);
+ efree(items);
+
+ return result;
+}
+
+static bool user_cache_instance_delete_multiple(
+ php_user_cache_object *cache,
+ HashTable *keys)
+{
+ php_user_cache_bulk_order *order;
+ zend_string **prepared_keys, **storage_keys;
+ uint32_t i, count;
+ bool *acquired, result = true;
+
+ if (!user_cache_prepare_key_list(keys, 1, &prepared_keys, &count)) {
+ return false;
+ }
+
+ if (!user_cache_can_write()) {
+ user_cache_release_key_list(prepared_keys, count);
+
+ return !EG(exception);
+ }
+
+ if (count == 0) {
+ user_cache_release_key_list(prepared_keys, count);
+
+ return true;
+ }
+
+ storage_keys = safe_emalloc(count, sizeof(zend_string *), 0);
+ order = safe_emalloc(count, sizeof(php_user_cache_bulk_order), 0);
+ acquired = ecalloc(count, sizeof(bool));
+
+ for (i = 0; i < count; i++) {
+ storage_keys[i] = user_cache_storage_key(cache, prepared_keys[i]);
+ }
+
+ result = user_cache_bulk_lock_keys_ordered(storage_keys, order, acquired, count);
+
+ if (result && user_cache_begin_write()) {
+ zend_try {
+ for (i = 0; i < count; i++) {
+ php_user_cache_delete_locked(storage_keys[i]);
+ }
+ } zend_catch {
+ php_user_cache_unlock_if_held();
+
+ zend_bailout();
+ } zend_end_try();
+
+ php_user_cache_unlock();
+ } else {
+ result = false;
+ }
+
+ php_user_cache_release_entry_locks(storage_keys, acquired, count);
+
+ for (i = 0; i < count; i++) {
+ zend_string_release(storage_keys[i]);
+ }
+
+ efree(acquired);
+ efree(order);
+ efree(storage_keys);
+
+ user_cache_release_key_list(prepared_keys, count);
+
+ return result;
+}
+
+static bool user_cache_init_partition_context(php_user_cache_partition *partition, const char *name)
+{
+ partition->context = php_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 user_cache_startup_storage_for_context(php_user_cache_context *ctx)
+{
+ php_user_cache_context *prev_ctx;
+
+ prev_ctx = php_user_cache_activate_context(ctx);
+
+ php_user_cache_reset_runtime();
+ if (php_user_cache_active_runtime()->enabled && UC_G(enable)) {
+ if (!php_user_cache_startup_storage_before_request()) {
+ php_user_cache_restore_context(prev_ctx);
+
+ return false;
+ }
+ }
+
+ php_user_cache_restore_context(prev_ctx);
+
+ return true;
+}
+
+static void user_cache_partitions_shutdown(void)
+{
+ php_user_cache_partition *partition, *next;
+ php_user_cache_context *prev_ctx;
+
+ prev_ctx = UC_G(active_context_ptr);
+
+ partition = php_user_cache_partitions;
+ while (partition != NULL) {
+ next = partition->next;
+
+ php_user_cache_activate_context(&partition->context);
+ php_user_cache_shutdown_storage();
+ php_user_cache_reset_runtime();
+
+ free(partition->name);
+ free(partition);
+
+ partition = next;
+ }
+
+ php_user_cache_partitions = NULL;
+
+ php_user_cache_restore_context(prev_ctx);
+}
+
+/* Separate identical boundary names owned by different Unix users. */
+static bool user_cache_format_boundary_key_prefix(char *prefix, size_t prefix_size, size_t *prefix_len)
+{
+#if !defined(ZEND_WIN32) && defined(HAVE_UNISTD_H)
+ int n;
+
+ n = snprintf(
+ prefix,
+ prefix_size,
+ "uid:%ld:gid:%ld:",
+ (long) geteuid(),
+ (long) getegid()
+ );
+
+ if (n < 0 || (size_t) n >= prefix_size) {
+ return false;
+ }
+
+ *prefix_len = (size_t) n;
+#else
+ prefix[0] = '\0';
+ *prefix_len = 0;
+ (void) prefix_size;
+#endif
+
+ return true;
+}
+
+static char *user_cache_build_boundary_key(const char *boundary, size_t *key_len)
+{
+ size_t boundary_len, prefix_len;
+ char *boundary_key, prefix[64];
+
+ boundary_len = strlen(boundary);
+
+ if (!user_cache_format_boundary_key_prefix(prefix, sizeof(prefix), &prefix_len)) {
+ return NULL;
+ }
+
+ if (boundary_len > SIZE_MAX - prefix_len - 1) {
+ return NULL;
+ }
+
+ boundary_key = malloc(prefix_len + boundary_len + 1);
+ if (boundary_key == NULL) {
+ return NULL;
+ }
+
+ memcpy(boundary_key, prefix, prefix_len);
+ memcpy(boundary_key + prefix_len, boundary, boundary_len + 1);
+
+ *key_len = prefix_len + boundary_len;
+
+ return boundary_key;
+}
+
+static php_user_cache_boundary_partition *user_cache_find_boundary_partition(
+ const char *boundary,
+ size_t boundary_len,
+ zend_ulong boundary_hash)
+{
+ php_user_cache_boundary_partition *entry;
+
+ for (entry = php_user_cache_boundary_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 php_user_cache_boundary_partition *user_cache_create_boundary_partition(
+ const char *sapi_prefix,
+ const char *boundary,
+ size_t boundary_len,
+ zend_ulong boundary_hash,
+ void (*log_message)(const char *message))
+{
+ php_user_cache_boundary_partition *entry;
+ char partition_name[128];
+
+ if (php_user_cache_boundary_partition_count >= PHP_USER_CACHE_MAX_BOUNDARY_PARTITIONS) {
+ if (!php_user_cache_boundary_limit_logged) {
+ log_message("User Cache disabled for this request because the cache boundary partition limit was reached");
+ php_user_cache_boundary_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),
+ "%s:boundary:" ZEND_ULONG_FMT ":" ZEND_ULONG_FMT ":%zx",
+ sapi_prefix,
+ boundary_hash,
+ boundary_len > 1 ? zend_inline_hash_func(boundary + 1, boundary_len - 1) : boundary_hash,
+ boundary_len
+ );
+
+ entry->partition = php_user_cache_partition_create(partition_name);
+ if (entry->partition == NULL) {
+ free(entry->boundary);
+ free(entry);
+
+ return NULL;
+ }
+
+ entry->partition->context.boundary_identity = entry->boundary;
+ entry->partition->context.boundary_identity_len = entry->boundary_len;
+ entry->partition->context.boundary_shared = true;
+
+ entry->next = php_user_cache_boundary_partitions;
+
+ php_user_cache_boundary_partitions = entry;
+ php_user_cache_boundary_partition_count++;
+
+ return entry;
+}
+
+uint64_t php_user_cache_cached_pid(void)
+{
+#ifndef ZEND_WIN32
+ if (UNEXPECTED(php_user_cache_self_pid_uncached)) {
+ return php_user_cache_current_pid();
+ }
+#endif
+
+ if (UNEXPECTED(php_user_cache_self_pid == 0)) {
+ php_user_cache_self_pid = php_user_cache_current_pid();
+ }
+
+ return php_user_cache_self_pid;
+}
+
+void php_user_cache_safe_direct_handlers_init(void)
+{
+ if (php_user_cache_safe_direct_handlers_initialized) {
+ return;
+ }
+
+ zend_hash_init(
+ &php_user_cache_safe_direct_handler_table,
+ 8,
+ NULL,
+ user_cache_safe_direct_handlers_dtor,
+ true
+ );
+
+ php_user_cache_safe_direct_handlers_initialized = true;
+}
+
+void php_user_cache_safe_direct_handlers_destroy(void)
+{
+ if (!php_user_cache_safe_direct_handlers_initialized) {
+ return;
+ }
+
+ zend_hash_destroy(&php_user_cache_safe_direct_handler_table);
+
+ php_user_cache_safe_direct_handlers_initialized = false;
+}
+
+ZEND_API void php_user_cache_safe_direct_register_class(
+ zend_class_entry *ce,
+ const php_user_cache_safe_direct_handlers *handlers)
+{
+ php_user_cache_safe_direct_handlers handlers_copy;
+
+ if (ce == NULL ||
+ handlers == NULL ||
+ handlers->copy == NULL ||
+ handlers->state_serialize == NULL ||
+ handlers->state_unserialize == NULL
+ ) {
+ return;
+ }
+
+ php_user_cache_safe_direct_handlers_init();
+
+ handlers_copy = *handlers;
+
+ zend_hash_index_update_mem(
+ &php_user_cache_safe_direct_handler_table,
+ (zend_ulong) (uintptr_t) ce,
+ &handlers_copy,
+ sizeof(handlers_copy)
+ );
+}
+
+php_user_cache_safe_direct_state_copy_func_t php_user_cache_safe_direct_state_copy_func(
+ zend_class_entry *ce,
+ zend_class_entry **base_ce_ptr)
+{
+ const php_user_cache_safe_direct_handlers *handlers =
+ php_user_cache_safe_direct_find_handlers(ce, base_ce_ptr)
+ ;
+
+ return handlers != NULL ? handlers->copy : NULL;
+}
+
+zend_class_entry *php_user_cache_safe_direct_find_base(zend_class_entry *ce)
+{
+ zend_class_entry *base_ce = NULL;
+
+ if (php_user_cache_safe_direct_state_copy_func(ce, &base_ce) == NULL) {
+ return NULL;
+ }
+
+ return base_ce;
+}
+
+PHP_USER_CACHE_DEFINE_SAFE_DIRECT_HANDLER_GETTER(state_has_unstorable)
+
+PHP_USER_CACHE_DEFINE_SAFE_DIRECT_HANDLER_GETTER(state_serialize)
+
+PHP_USER_CACHE_DEFINE_SAFE_DIRECT_HANDLER_GETTER(state_unserialize)
+
+bool php_user_cache_safe_direct_prefers_request_local_prototype(zend_class_entry *ce)
+{
+ const php_user_cache_safe_direct_handlers *handlers =
+ php_user_cache_safe_direct_find_handlers(ce, NULL)
+ ;
+
+ return handlers != NULL && handlers->prefer_request_local_prototype;
+}
+
+ZEND_API php_user_cache_partition *php_user_cache_partition_create(const char *name)
+{
+ php_user_cache_partition *partition;
+
+ partition = calloc(1, sizeof(php_user_cache_partition));
+ if (partition == NULL) {
+ return NULL;
+ }
+
+ if (!user_cache_init_partition_context(partition, name)) {
+ free(partition);
+
+ return NULL;
+ }
+
+ partition->next = php_user_cache_partitions;
+ php_user_cache_partitions = partition;
+
+ return partition;
+}
+
+ZEND_API bool php_user_cache_partition_startup_storage(php_user_cache_partition *partition)
+{
+ php_user_cache_partition *prev_partition;
+ bool result;
+
+ if (partition == NULL) {
+ return true;
+ }
+
+ prev_partition = UC_G(active_partition);
+ UC_G(active_partition) = partition;
+
+ result = user_cache_startup_storage_for_context(&partition->context);
+
+ UC_G(active_partition) = prev_partition;
+
+ return result;
+}
+
+ZEND_API bool php_user_cache_startup_default_context_storage(void)
+{
+ return user_cache_startup_storage_for_context(&php_user_cache_context_state);
+}
+
+ZEND_API void php_user_cache_partition_activate(php_user_cache_partition *partition)
+{
+ UC_G(active_partition) = partition;
+ UC_G(active_context_ptr) = NULL;
+ UC_G(request_unavailable_reason) = PHP_USER_CACHE_REASON_NONE;
+ UC_G(runtime_resolved) = false;
+}
+
+ZEND_API void php_user_cache_activate_request_unavailable(php_user_cache_reason reason)
+{
+ UC_G(active_partition) = NULL;
+ UC_G(active_context_ptr) = NULL;
+ UC_G(request_unavailable_reason) = reason;
+ UC_G(runtime_resolved) = false;
+}
+
+ZEND_API php_user_cache_partition *php_user_cache_boundary_partition_get(
+ const char *sapi_prefix,
+ const char *boundary,
+ void (*log_message)(const char *message))
+{
+ php_user_cache_boundary_partition *entry;
+ zend_ulong boundary_hash;
+ size_t boundary_len;
+ char *boundary_key;
+
+ boundary_key = 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 = user_cache_find_boundary_partition(boundary_key, boundary_len, boundary_hash);
+ if (entry == NULL) {
+ entry = user_cache_create_boundary_partition(sapi_prefix, boundary_key, boundary_len, boundary_hash, log_message);
+ if (entry == NULL) {
+ free(boundary_key);
+
+ return NULL;
+ }
+ }
+
+ free(boundary_key);
+
+ if (!php_user_cache_partition_startup_storage(entry->partition) &&
+ !php_user_cache_boundary_startup_failed_logged
+ ) {
+ log_message("User Cache partition startup failed; User Cache will be unavailable");
+ php_user_cache_boundary_startup_failed_logged = true;
+ }
+
+ return entry->partition;
+}
+
+ZEND_API void php_user_cache_activate_boundary_partition(
+ const char *sapi_prefix,
+ const char *(*get_env)(const char *name),
+ void (*log_message)(const char *message),
+ php_user_cache_reason failure_reason)
+{
+ const char *boundary;
+ php_user_cache_partition *partition = NULL;
+
+ boundary = get_env("DOCUMENT_ROOT");
+ if (boundary == NULL || boundary[0] == '\0') {
+ boundary = get_env("SERVER_NAME");
+ }
+
+ if (boundary != NULL && boundary[0] != '\0') {
+ partition = php_user_cache_boundary_partition_get(sapi_prefix, boundary, log_message);
+ }
+
+ if (partition == NULL) {
+ php_user_cache_activate_request_unavailable(failure_reason);
+
+ return;
+ }
+
+ php_user_cache_partition_activate(partition);
+}
+
+ZEND_API void php_user_cache_boundary_partitions_shutdown(void)
+{
+ php_user_cache_boundary_partition *entry, *next;
+
+ entry = php_user_cache_boundary_partitions;
+ while (entry != NULL) {
+ next = entry->next;
+ free(entry->boundary);
+ free(entry);
+ entry = next;
+ }
+
+ php_user_cache_boundary_partitions = NULL;
+ php_user_cache_boundary_partition_count = 0;
+ php_user_cache_boundary_limit_logged = false;
+ php_user_cache_boundary_startup_failed_logged = false;
+}
+
+ZEND_API void php_user_cache_opt_in(void)
+{
+ php_user_cache_runtime_opted_in = true;
+}
+
+#ifdef ZTS
+static void user_cache_globals_ctor(php_user_cache_globals *user_cache_globals)
+{
+ memset(user_cache_globals, 0, sizeof(php_user_cache_globals));
+}
+
+#ifdef PHP_USER_CACHE_HAVE_OPTIMISTIC
+/* Release reader slots owned by the terminating thread. */
+static void user_cache_globals_dtor(php_user_cache_globals *user_cache_globals)
+{
+ php_user_cache_release_thread_reader_claims(user_cache_globals);
+}
+#endif
+
+size_t php_user_cache_globals_size(void)
+{
+ return sizeof(php_user_cache_globals);
+}
+
+/* SAPI activation may access these globals before MINIT and after MSHUTDOWN. */
+void php_user_cache_globals_startup(void)
+{
+ user_cache_globals_id = ts_allocate_fast_id(
+ &user_cache_globals_id,
+ &user_cache_globals_offset,
+ sizeof(php_user_cache_globals),
+ (ts_allocate_ctor) user_cache_globals_ctor,
+#ifdef PHP_USER_CACHE_HAVE_OPTIMISTIC
+ (ts_allocate_dtor) user_cache_globals_dtor
+#else
+ NULL
+#endif
+ );
+}
+#endif
+
+void php_user_cache_minit(void)
+{
+ php_user_cache_context *prev_ctx;
+
+#ifndef ZEND_WIN32
+ php_user_cache_self_pid = php_user_cache_current_pid();
+ if (!php_user_cache_pid_atfork_registered &&
+ !php_user_cache_self_pid_uncached
+ ) {
+ if (pthread_atfork(NULL, NULL, user_cache_pid_atfork_child) == 0) {
+ php_user_cache_pid_atfork_registered = true;
+ } else {
+ php_user_cache_self_pid = 0;
+ php_user_cache_self_pid_uncached = true;
+ }
+ }
+#endif
+
+ php_user_cache_runtime_opted_in = false;
+
+ user_cache_register_classes();
+
+ php_user_cache_safe_direct_handlers_init();
+ php_user_cache_optimistic_fork_setup();
+
+ prev_ctx = php_user_cache_activate_context(php_user_cache_owning_context());
+
+ php_user_cache_reset_storage();
+
+ php_user_cache_restore_context(prev_ctx);
+}
+
+void php_user_cache_mshutdown(void)
+{
+ php_user_cache_context *prev_ctx;
+
+ user_cache_partitions_shutdown();
+
+ UC_G(active_partition) = NULL;
+
+ prev_ctx = php_user_cache_activate_context(php_user_cache_owning_context());
+
+ php_user_cache_shutdown_storage();
+ php_user_cache_reset_runtime();
+ php_user_cache_restore_context(prev_ctx);
+
+ UC_G(active_partition) = NULL;
+
+ php_user_cache_runtime_opted_in = false;
+
+ php_user_cache_safe_direct_handlers_destroy();
+ user_cache_reset_class_entries();
+}
+
+void php_user_cache_invalidate_active(void)
+{
+ user_cache_invalidate_context(php_user_cache_active_context());
+
+ php_user_cache_lookup_cache_clear();
+ php_user_cache_release_active_request_local_slots();
+}
+
+zend_result php_user_cache_rshutdown(void)
+{
+ php_user_cache_unlock_if_held();
+
+ UC_G(request_unavailable_reason) = PHP_USER_CACHE_REASON_NONE;
+ UC_G(runtime_resolved) = false;
+
+ user_cache_release_pools();
+ php_user_cache_lookup_cache_clear();
+
+ php_user_cache_release_request_entry_locks();
+ php_user_cache_release_request_local_slots();
+
+ if (UC_G(request_local_slot_may_cycle)) {
+ UC_G(request_local_slot_may_cycle) = false;
+
+ /* No later GC run is guaranteed during shutdown. */
+ if (!CG(unclean_shutdown)) {
+ gc_collect_cycles();
+ }
+ }
+
+ php_user_cache_decode_resolve_cache_release();
+ php_user_cache_decode_shape_prototype_cache_release();
+ php_user_cache_decode_maps_teardown();
+
+ return SUCCESS;
+}
+
+zend_result php_user_cache_post_deactivate(void)
+{
+ php_user_cache_release_request_shared_graph_refs();
+
+ return SUCCESS;
+}
+
+static void user_cache_store_method(INTERNAL_FUNCTION_PARAMETERS, bool add_only)
+{
+ php_user_cache_object *cache;
+ zend_long ttl = 0;
+ zend_string *key, *storage_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 = user_cache_object_from_this(ZEND_THIS);
+ if (cache == NULL) {
+ RETURN_THROWS();
+ }
+
+ storage_key = user_cache_validated_storage_key(cache, key, 1);
+ if (storage_key == NULL) {
+ RETURN_THROWS();
+ }
+
+ if (!user_cache_validate_api_value(value, 2)) {
+ zend_string_release(storage_key);
+ RETURN_THROWS();
+ }
+
+ if (!user_cache_validate_non_negative(ttl, 3)) {
+ zend_string_release(storage_key);
+ RETURN_THROWS();
+ }
+
+ if (!(user_cache_can_write() &&
+ user_cache_store_storage_key_prevalidated(storage_key, value, ttl, add_only))
+ ) {
+ zend_string_release(storage_key);
+
+ if (EG(exception)) {
+ RETURN_THROWS();
+ }
+
+ RETURN_FALSE;
+ }
+
+ zend_string_release(storage_key);
+
+ RETURN_TRUE;
+}
+
+static void user_cache_atomic_update_method(INTERNAL_FUNCTION_PARAMETERS, bool decrement)
+{
+ php_user_cache_object *cache;
+ php_user_cache_atomic_update_result result;
+ zend_long step = 1, ttl = 0;
+ zend_string *key, *storage_key;
+ 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 = user_cache_object_from_this(ZEND_THIS);
+ if (cache == NULL) {
+ RETURN_THROWS();
+ }
+
+ storage_key = user_cache_validated_storage_key(cache, key, 1);
+ if (storage_key == NULL) {
+ RETURN_THROWS();
+ }
+
+ if (!user_cache_validate_non_negative(step, 2)) {
+ zend_string_release(storage_key);
+
+ RETURN_THROWS();
+ }
+
+ if (!user_cache_validate_non_negative(ttl, 3)) {
+ zend_string_release(storage_key);
+
+ RETURN_THROWS();
+ }
+
+ updated = user_cache_atomic_update_api(storage_key, step, ttl, decrement, &result);
+
+ zend_string_release(storage_key);
+
+ if (result.is_type_error) {
+ zend_value_error(
+ "%s of user cache key \"%s\" requires the stored value to be an integer",
+ decrement ? "Decrement" : "Increment",
+ ZSTR_VAL(key)
+ );
+
+ RETURN_THROWS();
+ }
+
+ if (result.is_overflow) {
+ zend_throw_error(
+ zend_ce_arithmetic_error,
+ "%s of user cache key \"%s\" would exceed the range of a PHP integer",
+ decrement ? "Decrement" : "Increment",
+ ZSTR_VAL(key)
+ );
+
+ RETURN_THROWS();
+ }
+
+ if (!updated) {
+ RETURN_NULL();
+ }
+
+ RETURN_LONG(result.new_value);
+}
+
+ZEND_METHOD(UserCache_CacheStatus, __construct)
+{
+}
+
+ZEND_METHOD(UserCache_CacheStatus, getAvailability)
+{
+ php_user_cache_status_object *status;
+ zend_object *availability;
+
+ ZEND_PARSE_PARAMETERS_NONE();
+
+ status = user_cache_status_from_this(ZEND_THIS);
+ if (status == NULL) {
+ RETURN_THROWS();
+ }
+
+ availability = user_cache_availability_enum_case(status->availability_reason);
+
+ RETURN_OBJ_COPY(availability);
+}
+
+PHP_USER_CACHE_DEFINE_STATUS_LONG_GETTER(getConfiguredMemory, configured_memory)
+
+PHP_USER_CACHE_DEFINE_STATUS_LONG_GETTER(getSharedMemorySize, shared_memory_size)
+
+PHP_USER_CACHE_DEFINE_STATUS_LONG_GETTER(getUsedMemory, used_memory)
+
+PHP_USER_CACHE_DEFINE_STATUS_LONG_GETTER(getFreeMemory, free_memory)
+
+PHP_USER_CACHE_DEFINE_STATUS_LONG_GETTER(getWastedMemory, wasted_memory)
+
+PHP_USER_CACHE_DEFINE_STATUS_LONG_GETTER(getEntryCount, entry_count)
+
+PHP_USER_CACHE_DEFINE_STATUS_LONG_GETTER(getEntryCapacity, entry_capacity)
+
+PHP_USER_CACHE_DEFINE_STATUS_LONG_GETTER(getTombstoneCount, tombstone_count)
+
+PHP_USER_CACHE_DEFINE_STATUS_LONG_GETTER(getExpungeCount, expunge_count)
+
+PHP_USER_CACHE_DEFINE_STATUS_LONG_GETTER(getStoreFailureCount, store_failure_count)
+
+ZEND_METHOD(UserCache_CachePoolStatus, __construct)
+{
+}
+
+ZEND_METHOD(UserCache_CachePoolStatus, getPoolName)
+{
+ php_user_cache_pool_status_object *status;
+
+ ZEND_PARSE_PARAMETERS_NONE();
+
+ status = user_cache_pool_status_from_this(ZEND_THIS);
+ if (status == NULL) {
+ RETURN_THROWS();
+ }
+
+ RETURN_STR_COPY(status->scope);
+}
+
+ZEND_METHOD(UserCache_CachePoolStatus, getEntryCount)
+{
+ php_user_cache_pool_status_object *status;
+
+ ZEND_PARSE_PARAMETERS_NONE();
+
+ status = user_cache_pool_status_from_this(ZEND_THIS);
+ if (status == NULL) {
+ RETURN_THROWS();
+ }
+
+ RETURN_LONG(status->entry_count);
+}
+
+ZEND_METHOD(UserCache_CachePoolStatus, getEntryKeys)
+{
+ php_user_cache_pool_status_object *status;
+
+ ZEND_PARSE_PARAMETERS_NONE();
+
+ status = user_cache_pool_status_from_this(ZEND_THIS);
+ if (status == NULL) {
+ RETURN_THROWS();
+ }
+
+ if (Z_TYPE(status->entry_keys) != IS_ARRAY) {
+ RETURN_EMPTY_ARRAY();
+ }
+
+ RETURN_COPY(&status->entry_keys);
+}
+
+ZEND_METHOD(UserCache_CachePoolStatus, getUsedMemory)
+{
+ php_user_cache_pool_status_object *status;
+
+ ZEND_PARSE_PARAMETERS_NONE();
+
+ status = user_cache_pool_status_from_this(ZEND_THIS);
+ if (status == NULL) {
+ RETURN_THROWS();
+ }
+
+ RETURN_LONG(status->used_memory);
+}
+
+ZEND_METHOD(UserCache_Cache, __construct)
+{
+ ZEND_PARSE_PARAMETERS_NONE();
+
+ zend_throw_error(NULL, "UserCache\\Cache instances must be obtained via UserCache\\Cache::getPool()");
+}
+
+ZEND_METHOD(UserCache_Cache, hasPool)
+{
+ zend_string *pool;
+
+ ZEND_PARSE_PARAMETERS_START(1, 1)
+ Z_PARAM_STR(pool)
+ ZEND_PARSE_PARAMETERS_END();
+
+ if (!user_cache_validate_pool_name(pool, 1)) {
+ RETURN_THROWS();
+ }
+
+ RETURN_BOOL(UC_G(pool_table) != NULL &&
+ zend_hash_exists(UC_G(pool_table), pool)
+ );
+}
+
+ZEND_METHOD(UserCache_Cache, getPool)
+{
+ zend_string *pool;
+ zend_object *obj;
+
+ ZEND_PARSE_PARAMETERS_START(1, 1)
+ Z_PARAM_STR(pool)
+ ZEND_PARSE_PARAMETERS_END();
+
+ if (!user_cache_validate_pool_name(pool, 1)) {
+ RETURN_THROWS();
+ }
+
+ obj = user_cache_get_or_create_pool(pool);
+ if (obj == NULL) {
+ zend_throw_error(NULL, "Failed to register UserCache\\Cache pool");
+
+ RETURN_THROWS();
+ }
+
+ GC_ADDREF(obj);
+
+ RETURN_OBJ(obj);
+}
+
+ZEND_METHOD(UserCache_Cache, getPools)
+{
+ zend_string *pool;
+ zval *instance;
+
+ ZEND_PARSE_PARAMETERS_NONE();
+
+ array_init(return_value);
+
+ if (UC_G(pool_table) == NULL) {
+ return;
+ }
+
+ ZEND_HASH_FOREACH_STR_KEY_VAL(UC_G(pool_table), pool, instance) {
+ if (pool == NULL) {
+ continue;
+ }
+
+ Z_ADDREF_P(instance);
+
+ /* Preserve numeric-looking pool names as string keys. */
+ zend_hash_add(Z_ARRVAL_P(return_value), pool, instance);
+ } ZEND_HASH_FOREACH_END();
+}
+
+ZEND_METHOD(UserCache_Cache, getStatus)
+{
+ ZEND_PARSE_PARAMETERS_NONE();
+
+ user_cache_return_status(return_value);
+
+ if (EG(exception)) {
+ RETURN_THROWS();
+ }
+}
+
+ZEND_METHOD(UserCache_Cache, store)
+{
+ user_cache_store_method(INTERNAL_FUNCTION_PARAM_PASSTHRU, false);
+}
+
+ZEND_METHOD(UserCache_Cache, add)
+{
+ user_cache_store_method(INTERNAL_FUNCTION_PARAM_PASSTHRU, true);
+}
+
+ZEND_METHOD(UserCache_Cache, storeMultiple)
+{
+ php_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 = user_cache_object_from_this(ZEND_THIS);
+ if (cache == NULL) {
+ RETURN_THROWS();
+ }
+
+ if (!user_cache_validate_store_array(values, 1)) {
+ RETURN_THROWS();
+ }
+
+ if (!user_cache_validate_non_negative(ttl, 2)) {
+ RETURN_THROWS();
+ }
+
+ if (!user_cache_instance_store_multiple(cache, values, ttl)) {
+ if (EG(exception)) {
+ RETURN_THROWS();
+ }
+
+ RETURN_FALSE;
+ }
+
+ RETURN_TRUE;
+}
+
+ZEND_METHOD(UserCache_Cache, increment)
+{
+ user_cache_atomic_update_method(INTERNAL_FUNCTION_PARAM_PASSTHRU, false);
+}
+
+ZEND_METHOD(UserCache_Cache, decrement)
+{
+ user_cache_atomic_update_method(INTERNAL_FUNCTION_PARAM_PASSTHRU, true);
+}
+
+ZEND_METHOD(UserCache_Cache, fetch)
+{
+ php_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 = user_cache_object_from_this(ZEND_THIS);
+ if (cache == NULL) {
+ RETURN_THROWS();
+ }
+
+ storage_key = user_cache_validated_storage_key(cache, key, 1);
+ if (storage_key == NULL) {
+ RETURN_THROWS();
+ }
+
+ if (default_value == NULL) {
+ ZVAL_NULL(&default_null);
+ default_value = &default_null;
+ }
+
+ user_cache_fetch_api(storage_key, default_value, return_value);
+
+ zend_string_release(storage_key);
+}
+
+ZEND_METHOD(UserCache_Cache, fetchMultiple)
+{
+ php_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 = user_cache_object_from_this(ZEND_THIS);
+ if (cache == NULL) {
+ RETURN_THROWS();
+ }
+
+ if (default_value == NULL) {
+ ZVAL_NULL(&default_null);
+ default_value = &default_null;
+ }
+
+ if (user_cache_fetch_multiple_api(cache, keys, default_value, return_value) == FAILURE) {
+ if (EG(exception)) {
+ RETURN_THROWS();
+ }
+
+ RETURN_EMPTY_ARRAY();
+ }
+}
+
+ZEND_METHOD(UserCache_Cache, has)
+{
+ php_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 = user_cache_object_from_this(ZEND_THIS);
+ if (cache == NULL) {
+ RETURN_THROWS();
+ }
+
+ storage_key = user_cache_validated_storage_key(cache, key, 1);
+ if (storage_key == NULL) {
+ RETURN_THROWS();
+ }
+
+ exists = user_cache_exists_api(storage_key);
+
+ zend_string_release(storage_key);
+
+ RETURN_BOOL(exists);
+}
+
+ZEND_METHOD(UserCache_Cache, delete)
+{
+ php_user_cache_object *cache;
+ zend_string *key, *storage_key;
+
+ ZEND_PARSE_PARAMETERS_START(1, 1)
+ Z_PARAM_STR(key)
+ ZEND_PARSE_PARAMETERS_END();
+
+ cache = user_cache_object_from_this(ZEND_THIS);
+ if (cache == NULL) {
+ RETURN_THROWS();
+ }
+
+ storage_key = user_cache_validated_storage_key(cache, key, 1);
+ if (storage_key == NULL) {
+ RETURN_THROWS();
+ }
+
+ if (!user_cache_can_write()) {
+ zend_string_release(storage_key);
+
+ if (EG(exception)) {
+ RETURN_THROWS();
+ }
+
+ RETURN_TRUE;
+ }
+
+ if (!user_cache_delete_storage_key_prevalidated(storage_key)) {
+ zend_string_release(storage_key);
+
+ if (EG(exception)) {
+ RETURN_THROWS();
+ }
+
+ RETURN_FALSE;
+ }
+
+ zend_string_release(storage_key);
+
+ RETURN_TRUE;
+}
+
+ZEND_METHOD(UserCache_Cache, deleteMultiple)
+{
+ php_user_cache_object *cache;
+ HashTable *keys;
+
+ ZEND_PARSE_PARAMETERS_START(1, 1)
+ Z_PARAM_ARRAY_HT(keys)
+ ZEND_PARSE_PARAMETERS_END();
+
+ cache = user_cache_object_from_this(ZEND_THIS);
+ if (cache == NULL) {
+ RETURN_THROWS();
+ }
+
+ if (!user_cache_instance_delete_multiple(cache, keys)) {
+ if (EG(exception)) {
+ RETURN_THROWS();
+ }
+
+ RETURN_FALSE;
+ }
+
+ RETURN_TRUE;
+}
+
+ZEND_METHOD(UserCache_Cache, clear)
+{
+ php_user_cache_object *cache;
+
+ ZEND_PARSE_PARAMETERS_NONE();
+
+ cache = user_cache_object_from_this(ZEND_THIS);
+ if (cache == NULL) {
+ RETURN_THROWS();
+ }
+
+ if (!user_cache_can_write()) {
+ if (EG(exception)) {
+ RETURN_THROWS();
+ }
+
+ RETURN_TRUE;
+ }
+
+ if (!user_cache_clear_scope_prevalidated(cache->scope_prefix)) {
+ if (EG(exception)) {
+ RETURN_THROWS();
+ }
+
+ RETURN_FALSE;
+ }
+
+ RETURN_TRUE;
+}
+
+ZEND_METHOD(UserCache_Cache, lock)
+{
+ php_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 = user_cache_object_from_this(ZEND_THIS);
+ if (cache == NULL) {
+ RETURN_THROWS();
+ }
+
+ storage_key = user_cache_validated_storage_key(cache, key, 1);
+ if (storage_key == NULL) {
+ RETURN_THROWS();
+ }
+
+ if (!user_cache_validate_non_negative(lease, 2)) {
+ zend_string_release(storage_key);
+
+ RETURN_THROWS();
+ }
+
+ locked = user_cache_lock_api(storage_key, lease);
+ zend_string_release(storage_key);
+
+ RETURN_BOOL(locked);
+}
+
+ZEND_METHOD(UserCache_Cache, unlock)
+{
+ php_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 = user_cache_object_from_this(ZEND_THIS);
+ if (cache == NULL) {
+ RETURN_THROWS();
+ }
+
+ storage_key = user_cache_validated_storage_key(cache, key, 1);
+ if (storage_key == NULL) {
+ RETURN_THROWS();
+ }
+
+ unlocked = user_cache_unlock_api(storage_key);
+ zend_string_release(storage_key);
+
+ RETURN_BOOL(unlocked);
+}
+
+static bool user_cache_invoke_remember_callback(
+ zend_string *key,
+ zend_string *storage_key,
+ zend_fcall_info *fci,
+ zend_fcall_info_cache *fcc,
+ zend_long ttl,
+ zval *result)
+{
+ zval key_zv;
+
+ ZVAL_UNDEF(result);
+ fci->retval = result;
+
+ ZVAL_STR(&key_zv, key);
+ fci->param_count = 1;
+ fci->params = &key_zv;
+ fci->named_params = NULL;
+
+ if (zend_call_function(fci, fcc) != SUCCESS || EG(exception)) {
+ return true;
+ }
+
+ if (Z_TYPE_P(result) == IS_UNDEF) {
+ return true;
+ }
+
+ if (!user_cache_validate_remember_value(result)) {
+ return false;
+ }
+
+ if (user_cache_can_write() && user_cache_store_storage_key_prevalidated(storage_key, result, ttl, false)) {
+ return true;
+ }
+
+ /* Failing to store the memoized value is not itself an error unless it raised an exception. */
+ return !EG(exception);
+}
+
+ZEND_METHOD(UserCache_Cache, remember)
+{
+ php_user_cache_object *cache;
+ zend_long ttl = 0;
+ zend_string *key, *storage_key;
+ zend_fcall_info fci;
+ zend_fcall_info_cache fcc;
+ zval result;
+ bool found = false, locked = false, callback_failed = 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 = user_cache_object_from_this(ZEND_THIS);
+ if (cache == NULL) {
+ RETURN_THROWS();
+ }
+
+ storage_key = user_cache_validated_storage_key(cache, key, 1);
+ if (storage_key == NULL) {
+ RETURN_THROWS();
+ }
+
+ if (!user_cache_validate_non_negative(ttl, 3)) {
+ zend_string_release(storage_key);
+
+ RETURN_THROWS();
+ }
+
+ found = user_cache_fetch_if_present_api(storage_key, return_value);
+ if (found) {
+ zend_string_release(storage_key);
+
+ return;
+ }
+
+ /* A restore hook or autoloader threw while materializing the entry: the
+ * entry exists, so propagate the exception instead of recomputing. */
+ if (EG(exception)) {
+ zend_string_release(storage_key);
+
+ RETURN_THROWS();
+ }
+
+ locked = user_cache_lock_api(storage_key, 0);
+
+ /* Another request may have populated the entry while the lock was acquired. */
+ found = user_cache_fetch_if_present_api(storage_key, return_value);
+ if (found) {
+ if (locked) {
+ locked = user_cache_unlock_api(storage_key);
+ }
+
+ zend_string_release(storage_key);
+
+ return;
+ }
+
+ if (EG(exception)) {
+ if (locked) {
+ locked = user_cache_unlock_api(storage_key);
+ }
+
+ zend_string_release(storage_key);
+
+ RETURN_THROWS();
+ }
+
+ /* Do not return before zend_end_try() restores the bailout chain. */
+ zend_try {
+ callback_failed =
+ !user_cache_invoke_remember_callback(key, storage_key, &fci, &fcc, ttl, &result);
+ } zend_catch {
+ if (locked) {
+ locked = user_cache_unlock_api(storage_key);
+ }
+
+ zend_string_release(storage_key);
+
+ zend_bailout();
+ } zend_end_try();
+
+ if (callback_failed) {
+ zval_ptr_dtor(&result);
+
+ locked = user_cache_unlock_api(storage_key);
+
+ zend_string_release(storage_key);
+
+ RETURN_THROWS();
+ }
+
+ if (locked) {
+ locked = user_cache_unlock_api(storage_key);
+ }
+
+ zend_string_release(storage_key);
+
+ if (EG(exception)) {
+ if (Z_TYPE(result) != IS_UNDEF) {
+ zval_ptr_dtor(&result);
+ }
+
+ RETURN_THROWS();
+ }
+
+ if (Z_TYPE(result) == IS_UNDEF) {
+ RETURN_NULL();
+ }
+
+ RETURN_COPY_VALUE(&result);
+}
+
+ZEND_METHOD(UserCache_Cache, getPoolStatus)
+{
+ php_user_cache_object *cache;
+
+ ZEND_PARSE_PARAMETERS_NONE();
+
+ cache = user_cache_object_from_this(ZEND_THIS);
+ if (cache == NULL) {
+ RETURN_THROWS();
+ }
+
+ user_cache_return_pool_status(cache, return_value);
+
+ if (EG(exception)) {
+ RETURN_THROWS();
+ }
+}
+
+static ZEND_INI_MH(OnUpdateUserCacheShmSize)
+{
+ zend_long *p, size;
+
+ p = (zend_long *) ZEND_INI_GET_ADDR();
+ size = zend_ini_parse_quantity_warn(new_value, entry->name);
+
+ if (size < 0) {
+ zend_error(E_WARNING, "user_cache.shm_size must be greater than or equal to 0, " ZEND_LONG_FMT " given", size);
+
+ return FAILURE;
+ }
+
+ if ((zend_ulong) size > UINT32_MAX - ZEND_MM_ALIGNMENT) {
+ size = (zend_long) (UINT32_MAX - ZEND_MM_ALIGNMENT);
+ zend_error(E_WARNING, "user_cache.shm_size is limited to slightly under 4096M; clamping");
+ }
+
+ *p = size;
+
+ return SUCCESS;
+}
+
+PHP_INI_BEGIN()
+ STD_PHP_INI_ENTRY("user_cache.enable", "1", PHP_INI_SYSTEM, OnUpdateBool, enable, php_user_cache_globals, user_cache_globals)
+ STD_PHP_INI_ENTRY("user_cache.enable_cli", "0", PHP_INI_SYSTEM, OnUpdateBool, enable_cli, php_user_cache_globals, user_cache_globals)
+ STD_PHP_INI_ENTRY("user_cache.shm_size", "16M", PHP_INI_SYSTEM, OnUpdateUserCacheShmSize, shm_size, php_user_cache_globals, user_cache_globals)
+ STD_PHP_INI_ENTRY("user_cache.lockfile_path", "/tmp", PHP_INI_SYSTEM, OnUpdateString, lockfile_path, php_user_cache_globals, user_cache_globals)
+ STD_PHP_INI_ENTRY("user_cache.preferred_memory_model", "", PHP_INI_SYSTEM, OnUpdateStringUnempty, memory_model, php_user_cache_globals, user_cache_globals)
+PHP_INI_END()
+
+ZEND_FUNCTION(user_cache_reset)
+{
+ ZEND_PARSE_PARAMETERS_NONE();
+
+ php_user_cache_invalidate_active();
+
+ RETURN_TRUE;
+}
+
+static PHP_MINIT_FUNCTION(cache)
+{
+ REGISTER_INI_ENTRIES();
+
+ php_user_cache_minit();
+
+ return SUCCESS;
+}
+
+static PHP_MSHUTDOWN_FUNCTION(cache)
+{
+ php_user_cache_mshutdown();
+
+ UNREGISTER_INI_ENTRIES();
+
+ return SUCCESS;
+}
+
+static PHP_RSHUTDOWN_FUNCTION(cache)
+{
+ return php_user_cache_rshutdown();
+}
+
+static PHP_MINFO_FUNCTION(cache)
+{
+ php_info_print_table_start();
+ php_info_print_table_row(2, "User Cache support", "enabled");
+ php_info_print_table_end();
+
+ DISPLAY_INI_ENTRIES();
+}
+
+zend_module_entry user_cache_module_entry = {
+ STANDARD_MODULE_HEADER,
+ "user_cache",
+ ext_functions,
+ PHP_MINIT(cache),
+ PHP_MSHUTDOWN(cache),
+ NULL,
+ PHP_RSHUTDOWN(cache),
+ PHP_MINFO(cache),
+ PHP_VERSION,
+ NO_MODULE_GLOBALS,
+ php_user_cache_post_deactivate,
+ STANDARD_MODULE_PROPERTIES_EX
+};
diff --git a/ext/user_cache/user_cache.stub.php b/ext/user_cache/user_cache.stub.php
new file mode 100644
index 000000000000..2db5970d6fa2
--- /dev/null
+++ b/ext/user_cache/user_cache.stub.php
@@ -0,0 +1,127 @@
+ */
+ public function getEntryKeys(): array {}
+
+ public function getUsedMemory(): int {}
+}
+
+/** @not-serializable */
+final class Cache
+{
+ public static function hasPool(string $pool): bool {}
+
+ public static function getPool(string $pool): Cache {}
+
+ /** @return array */
+ public static function getPools(): array {}
+
+ public static function getStatus(): CacheStatus {}
+
+ private function __construct() {}
+
+ public function store(string $key, null|bool|int|float|string|array|object $value, int $ttl = 0): bool {}
+
+ public function add(string $key, null|bool|int|float|string|array|object $value, int $ttl = 0): bool {}
+
+ public function storeMultiple(array $values, int $ttl = 0): bool {}
+
+ public function increment(string $key, int $step = 1, int $ttl = 0): ?int {}
+
+ public function decrement(string $key, int $step = 1, int $ttl = 0): ?int {}
+
+ public function fetch(string $key, mixed $default = null): mixed {}
+
+ public function fetchMultiple(array $keys, mixed $default = null): array {}
+
+ public function delete(string $key): bool {}
+
+ public function deleteMultiple(array $keys): bool {}
+
+ public function clear(): bool {}
+
+ public function has(string $key): bool {}
+
+ public function lock(string $key, int $lease = 0): bool {}
+
+ public function unlock(string $key): bool {}
+
+ public function remember(string $key, callable $callback, int $ttl = 0): mixed {}
+
+ public function getPoolStatus(): CachePoolStatus {}
+}
+
+}
+
+namespace {
+
+function user_cache_reset(): bool {}
+
+}
diff --git a/ext/user_cache/user_cache_alloc_posix.c b/ext/user_cache/user_cache_alloc_posix.c
new file mode 100644
index 000000000000..5d575066af74
--- /dev/null
+++ b/ext/user_cache/user_cache_alloc_posix.c
@@ -0,0 +1,128 @@
+/*
+ +----------------------------------------------------------------------+
+ | 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 "user_cache_shm.h"
+
+#ifdef PHP_USER_CACHE_USE_SHM_OPEN
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+typedef struct {
+ php_user_cache_shm_segment common;
+ int shm_fd;
+} php_user_cache_shm_segment_posix;
+
+static int user_cache_alloc_posix_create_segments(size_t requested_size, php_user_cache_shm_segment_posix ***shared_segments_p, int *shared_segments_count, const char **error_in)
+{
+ php_user_cache_shm_segment_posix *shared_segment;
+ char shared_segment_name[sizeof("/php_user_cache.") + 20];
+ int shared_segment_flags = O_RDWR|O_CREAT|O_TRUNC;
+ mode_t shared_segment_mode = 0600;
+
+#if defined(HAVE_SHM_CREATE_LARGEPAGE)
+ /* Prefer the largest compatible page size. */
+ size_t i, shared_segment_sizes = 0, shared_segment_lg_index = 0;
+ size_t shared_segment_sindexes[3] = {0};
+ const size_t entries = sizeof(shared_segment_sindexes) / sizeof(shared_segment_sindexes[0]);
+
+ shared_segment_sizes = getpagesizes(shared_segment_sindexes, entries);
+
+ if (shared_segment_sizes > 0) {
+ for (i = shared_segment_sizes; i-- > 0; ) {
+ if (shared_segment_sindexes[i] != 0 &&
+ !(requested_size % shared_segment_sindexes[i])) {
+ shared_segment_lg_index = i;
+ break;
+ }
+ }
+ }
+#endif
+
+ *shared_segments_count = 1;
+ *shared_segments_p = (php_user_cache_shm_segment_posix **) calloc(1, sizeof(php_user_cache_shm_segment_posix) + sizeof(void *));
+ if (!*shared_segments_p) {
+ *error_in = "calloc";
+ return PHP_USER_CACHE_ALLOC_FAILURE;
+ }
+ shared_segment = (php_user_cache_shm_segment_posix *)((char *)(*shared_segments_p) + sizeof(void *));
+ (*shared_segments_p)[0] = shared_segment;
+
+ snprintf(shared_segment_name, sizeof(shared_segment_name), "/php_user_cache.%d", getpid());
+#if defined(HAVE_SHM_CREATE_LARGEPAGE)
+ if (shared_segment_lg_index > 0) {
+ shared_segment->shm_fd = shm_create_largepage(shared_segment_name, shared_segment_flags, shared_segment_lg_index, SHM_LARGEPAGE_ALLOC_DEFAULT, shared_segment_mode);
+ if (shared_segment->shm_fd != -1) {
+ goto truncate_segment;
+ }
+ }
+#endif
+
+ shared_segment->shm_fd = shm_open(shared_segment_name, shared_segment_flags, shared_segment_mode);
+ if (shared_segment->shm_fd == -1) {
+ *error_in = "shm_open";
+ return PHP_USER_CACHE_ALLOC_FAILURE;
+ }
+
+#if defined(HAVE_SHM_CREATE_LARGEPAGE)
+truncate_segment:
+#endif
+ if (ftruncate(shared_segment->shm_fd, requested_size) != 0) {
+ *error_in = "ftruncate";
+ shm_unlink(shared_segment_name);
+
+ return PHP_USER_CACHE_ALLOC_FAILURE;
+ }
+
+ shared_segment->common.p = mmap(0, requested_size, PROT_READ | PROT_WRITE, MAP_SHARED, shared_segment->shm_fd, 0);
+ if (shared_segment->common.p == MAP_FAILED) {
+ *error_in = "mmap";
+ shm_unlink(shared_segment_name);
+
+ return PHP_USER_CACHE_ALLOC_FAILURE;
+ }
+
+ shm_unlink(shared_segment_name);
+
+ shared_segment->common.pos = 0;
+ shared_segment->common.size = requested_size;
+
+ return PHP_USER_CACHE_ALLOC_SUCCESS;
+}
+
+static int user_cache_alloc_posix_detach_segment(php_user_cache_shm_segment_posix *shared_segment)
+{
+ munmap(shared_segment->common.p, shared_segment->common.size);
+ close(shared_segment->shm_fd);
+
+ return 0;
+}
+
+static size_t user_cache_alloc_posix_segment_type_size(void)
+{
+ return sizeof(php_user_cache_shm_segment_posix);
+}
+
+const php_user_cache_shm_handlers php_user_cache_alloc_posix_handlers = {
+ (php_user_cache_create_segments_t)user_cache_alloc_posix_create_segments,
+ (php_user_cache_detach_segment_t)user_cache_alloc_posix_detach_segment,
+ user_cache_alloc_posix_segment_type_size
+};
+
+#endif /* PHP_USER_CACHE_USE_SHM_OPEN */
diff --git a/ext/user_cache/user_cache_alloc_shm.c b/ext/user_cache/user_cache_alloc_shm.c
new file mode 100644
index 000000000000..5a3f271d4594
--- /dev/null
+++ b/ext/user_cache/user_cache_alloc_shm.c
@@ -0,0 +1,134 @@
+/*
+ +----------------------------------------------------------------------+
+ | 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 "user_cache_shm.h"
+
+#ifdef PHP_USER_CACHE_USE_SHM
+
+#if defined(__FreeBSD__)
+# include
+#endif
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+#include
+#include
+
+#define PHP_USER_CACHE_MIN(x, y) ((x) > (y) ? (y) : (x))
+
+#define PHP_USER_CACHE_SEG_ALLOC_SIZE_MIN (2 * 1024 * 1024)
+
+typedef struct {
+ php_user_cache_shm_segment common;
+ int shm_id;
+} php_user_cache_shm_segment_shm;
+
+static int user_cache_alloc_shm_create_segments(size_t requested_size, php_user_cache_shm_segment_shm ***shared_segments_p, int *shared_segments_count, const char **error_in)
+{
+ int i;
+ size_t allocate_size = 0, remaining_bytes, seg_allocate_size;
+ int first_segment_id = -1;
+ key_t first_segment_key = -1;
+ struct shmid_ds sds;
+ int shmget_flags;
+ php_user_cache_shm_segment_shm *shared_segments;
+
+ shmget_flags = IPC_CREAT|SHM_R|SHM_W|IPC_EXCL;
+
+ seg_allocate_size = requested_size;
+ first_segment_id = shmget(first_segment_key, seg_allocate_size, shmget_flags);
+ if (UNEXPECTED(first_segment_id == -1)) {
+ /* Fall back to decreasing power-of-two segments. */
+ seg_allocate_size = PHP_USER_CACHE_SEG_ALLOC_SIZE_MIN;
+ while (seg_allocate_size < requested_size / 2) {
+ seg_allocate_size *= 2;
+ }
+
+ while (seg_allocate_size >= PHP_USER_CACHE_SEG_ALLOC_SIZE_MIN) {
+ first_segment_id = shmget(first_segment_key, seg_allocate_size, shmget_flags);
+ if (first_segment_id != -1) {
+ break;
+ }
+ seg_allocate_size >>= 1;
+ }
+
+ if (first_segment_id == -1) {
+ *error_in = "shmget";
+ return PHP_USER_CACHE_ALLOC_FAILURE;
+ }
+ }
+
+ *shared_segments_count = ((requested_size - 1) / seg_allocate_size) + 1;
+ *shared_segments_p = (php_user_cache_shm_segment_shm **) calloc(1, (*shared_segments_count) * sizeof(php_user_cache_shm_segment_shm) + sizeof(void *) * (*shared_segments_count));
+ if (!*shared_segments_p) {
+ *error_in = "calloc";
+ return PHP_USER_CACHE_ALLOC_FAILURE;
+ }
+ shared_segments = (php_user_cache_shm_segment_shm *)((char *)(*shared_segments_p) + sizeof(void *) * (*shared_segments_count));
+ for (i = 0; i < *shared_segments_count; i++) {
+ (*shared_segments_p)[i] = shared_segments + i;
+ }
+
+ remaining_bytes = requested_size;
+ for (i = 0; i < *shared_segments_count; i++) {
+ allocate_size = PHP_USER_CACHE_MIN(remaining_bytes, seg_allocate_size);
+ if (i != 0) {
+ shared_segments[i].shm_id = shmget(IPC_PRIVATE, allocate_size, shmget_flags);
+ } else {
+ shared_segments[i].shm_id = first_segment_id;
+ }
+
+ if (shared_segments[i].shm_id == -1) {
+ return PHP_USER_CACHE_ALLOC_FAILURE;
+ }
+
+ shared_segments[i].common.p = shmat(shared_segments[i].shm_id, NULL, 0);
+ if (shared_segments[i].common.p == (void *)-1) {
+ *error_in = "shmat";
+ shmctl(shared_segments[i].shm_id, IPC_RMID, &sds);
+ return PHP_USER_CACHE_ALLOC_FAILURE;
+ }
+ shmctl(shared_segments[i].shm_id, IPC_RMID, &sds);
+
+ shared_segments[i].common.pos = 0;
+ shared_segments[i].common.size = allocate_size;
+ remaining_bytes -= allocate_size;
+ }
+ return PHP_USER_CACHE_ALLOC_SUCCESS;
+}
+
+static int user_cache_alloc_shm_detach_segment(php_user_cache_shm_segment_shm *shared_segment)
+{
+ shmdt(shared_segment->common.p);
+ return 0;
+}
+
+static size_t user_cache_alloc_shm_segment_type_size(void)
+{
+ return sizeof(php_user_cache_shm_segment_shm);
+}
+
+const php_user_cache_shm_handlers php_user_cache_alloc_shm_handlers = {
+ (php_user_cache_create_segments_t)user_cache_alloc_shm_create_segments,
+ (php_user_cache_detach_segment_t)user_cache_alloc_shm_detach_segment,
+ user_cache_alloc_shm_segment_type_size
+};
+
+#endif /* PHP_USER_CACHE_USE_SHM */
diff --git a/ext/user_cache/user_cache_arginfo.h b/ext/user_cache/user_cache_arginfo.h
new file mode 100644
index 000000000000..25023d384f6d
--- /dev/null
+++ b/ext/user_cache/user_cache_arginfo.h
@@ -0,0 +1,267 @@
+/* This is a generated file, edit user_cache.stub.php instead.
+ * Stub hash: f475d675d1f7d8fc96ecc58b290107267010d958
+ * Has decl header: yes */
+
+ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_user_cache_reset, 0, 0, _IS_BOOL, 0)
+ZEND_END_ARG_INFO()
+
+ZEND_BEGIN_ARG_INFO_EX(arginfo_class_UserCache_CacheStatus___construct, 0, 0, 0)
+ZEND_END_ARG_INFO()
+
+ZEND_BEGIN_ARG_WITH_RETURN_OBJ_INFO_EX(arginfo_class_UserCache_CacheStatus_getAvailability, 0, 0, UserCache\\CacheAvailability, 0)
+ZEND_END_ARG_INFO()
+
+ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_UserCache_CacheStatus_getConfiguredMemory, 0, 0, IS_LONG, 0)
+ZEND_END_ARG_INFO()
+
+#define arginfo_class_UserCache_CacheStatus_getSharedMemorySize arginfo_class_UserCache_CacheStatus_getConfiguredMemory
+
+#define arginfo_class_UserCache_CacheStatus_getUsedMemory arginfo_class_UserCache_CacheStatus_getConfiguredMemory
+
+#define arginfo_class_UserCache_CacheStatus_getFreeMemory arginfo_class_UserCache_CacheStatus_getConfiguredMemory
+
+#define arginfo_class_UserCache_CacheStatus_getWastedMemory arginfo_class_UserCache_CacheStatus_getConfiguredMemory
+
+#define arginfo_class_UserCache_CacheStatus_getEntryCount arginfo_class_UserCache_CacheStatus_getConfiguredMemory
+
+#define arginfo_class_UserCache_CacheStatus_getEntryCapacity arginfo_class_UserCache_CacheStatus_getConfiguredMemory
+
+#define arginfo_class_UserCache_CacheStatus_getTombstoneCount arginfo_class_UserCache_CacheStatus_getConfiguredMemory
+
+#define arginfo_class_UserCache_CacheStatus_getExpungeCount arginfo_class_UserCache_CacheStatus_getConfiguredMemory
+
+#define arginfo_class_UserCache_CacheStatus_getStoreFailureCount arginfo_class_UserCache_CacheStatus_getConfiguredMemory
+
+#define arginfo_class_UserCache_CachePoolStatus___construct arginfo_class_UserCache_CacheStatus___construct
+
+ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_UserCache_CachePoolStatus_getPoolName, 0, 0, IS_STRING, 0)
+ZEND_END_ARG_INFO()
+
+#define arginfo_class_UserCache_CachePoolStatus_getEntryCount arginfo_class_UserCache_CacheStatus_getConfiguredMemory
+
+ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_UserCache_CachePoolStatus_getEntryKeys, 0, 0, IS_ARRAY, 0)
+ZEND_END_ARG_INFO()
+
+#define arginfo_class_UserCache_CachePoolStatus_getUsedMemory arginfo_class_UserCache_CacheStatus_getConfiguredMemory
+
+ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_UserCache_Cache_hasPool, 0, 1, _IS_BOOL, 0)
+ ZEND_ARG_TYPE_INFO(0, pool, IS_STRING, 0)
+ZEND_END_ARG_INFO()
+
+ZEND_BEGIN_ARG_WITH_RETURN_OBJ_INFO_EX(arginfo_class_UserCache_Cache_getPool, 0, 1, UserCache\\Cache, 0)
+ ZEND_ARG_TYPE_INFO(0, pool, IS_STRING, 0)
+ZEND_END_ARG_INFO()
+
+#define arginfo_class_UserCache_Cache_getPools arginfo_class_UserCache_CachePoolStatus_getEntryKeys
+
+ZEND_BEGIN_ARG_WITH_RETURN_OBJ_INFO_EX(arginfo_class_UserCache_Cache_getStatus, 0, 0, UserCache\\CacheStatus, 0)
+ZEND_END_ARG_INFO()
+
+#define arginfo_class_UserCache_Cache___construct arginfo_class_UserCache_CacheStatus___construct
+
+ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_UserCache_Cache_store, 0, 2, _IS_BOOL, 0)
+ ZEND_ARG_TYPE_INFO(0, key, IS_STRING, 0)
+ ZEND_ARG_TYPE_MASK(0, value, MAY_BE_NULL|MAY_BE_BOOL|MAY_BE_LONG|MAY_BE_DOUBLE|MAY_BE_STRING|MAY_BE_ARRAY|MAY_BE_OBJECT, NULL)
+ ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, ttl, IS_LONG, 0, "0")
+ZEND_END_ARG_INFO()
+
+#define arginfo_class_UserCache_Cache_add arginfo_class_UserCache_Cache_store
+
+ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_UserCache_Cache_storeMultiple, 0, 1, _IS_BOOL, 0)
+ ZEND_ARG_TYPE_INFO(0, values, IS_ARRAY, 0)
+ ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, ttl, IS_LONG, 0, "0")
+ZEND_END_ARG_INFO()
+
+ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_UserCache_Cache_increment, 0, 1, IS_LONG, 1)
+ ZEND_ARG_TYPE_INFO(0, key, IS_STRING, 0)
+ ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, step, IS_LONG, 0, "1")
+ ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, ttl, IS_LONG, 0, "0")
+ZEND_END_ARG_INFO()
+
+#define arginfo_class_UserCache_Cache_decrement arginfo_class_UserCache_Cache_increment
+
+ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_UserCache_Cache_fetch, 0, 1, IS_MIXED, 0)
+ ZEND_ARG_TYPE_INFO(0, key, IS_STRING, 0)
+ ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, default, IS_MIXED, 0, "null")
+ZEND_END_ARG_INFO()
+
+ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_UserCache_Cache_fetchMultiple, 0, 1, IS_ARRAY, 0)
+ ZEND_ARG_TYPE_INFO(0, keys, IS_ARRAY, 0)
+ ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, default, IS_MIXED, 0, "null")
+ZEND_END_ARG_INFO()
+
+ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_UserCache_Cache_delete, 0, 1, _IS_BOOL, 0)
+ ZEND_ARG_TYPE_INFO(0, key, IS_STRING, 0)
+ZEND_END_ARG_INFO()
+
+ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_UserCache_Cache_deleteMultiple, 0, 1, _IS_BOOL, 0)
+ ZEND_ARG_TYPE_INFO(0, keys, IS_ARRAY, 0)
+ZEND_END_ARG_INFO()
+
+#define arginfo_class_UserCache_Cache_clear arginfo_user_cache_reset
+
+#define arginfo_class_UserCache_Cache_has arginfo_class_UserCache_Cache_delete
+
+ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_UserCache_Cache_lock, 0, 1, _IS_BOOL, 0)
+ ZEND_ARG_TYPE_INFO(0, key, IS_STRING, 0)
+ ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, lease, IS_LONG, 0, "0")
+ZEND_END_ARG_INFO()
+
+#define arginfo_class_UserCache_Cache_unlock arginfo_class_UserCache_Cache_delete
+
+ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_UserCache_Cache_remember, 0, 2, IS_MIXED, 0)
+ ZEND_ARG_TYPE_INFO(0, key, IS_STRING, 0)
+ ZEND_ARG_TYPE_INFO(0, callback, IS_CALLABLE, 0)
+ ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, ttl, IS_LONG, 0, "0")
+ZEND_END_ARG_INFO()
+
+ZEND_BEGIN_ARG_WITH_RETURN_OBJ_INFO_EX(arginfo_class_UserCache_Cache_getPoolStatus, 0, 0, UserCache\\CachePoolStatus, 0)
+ZEND_END_ARG_INFO()
+
+ZEND_FUNCTION(user_cache_reset);
+ZEND_METHOD(UserCache_CacheStatus, __construct);
+ZEND_METHOD(UserCache_CacheStatus, getAvailability);
+ZEND_METHOD(UserCache_CacheStatus, getConfiguredMemory);
+ZEND_METHOD(UserCache_CacheStatus, getSharedMemorySize);
+ZEND_METHOD(UserCache_CacheStatus, getUsedMemory);
+ZEND_METHOD(UserCache_CacheStatus, getFreeMemory);
+ZEND_METHOD(UserCache_CacheStatus, getWastedMemory);
+ZEND_METHOD(UserCache_CacheStatus, getEntryCount);
+ZEND_METHOD(UserCache_CacheStatus, getEntryCapacity);
+ZEND_METHOD(UserCache_CacheStatus, getTombstoneCount);
+ZEND_METHOD(UserCache_CacheStatus, getExpungeCount);
+ZEND_METHOD(UserCache_CacheStatus, getStoreFailureCount);
+ZEND_METHOD(UserCache_CachePoolStatus, __construct);
+ZEND_METHOD(UserCache_CachePoolStatus, getPoolName);
+ZEND_METHOD(UserCache_CachePoolStatus, getEntryCount);
+ZEND_METHOD(UserCache_CachePoolStatus, getEntryKeys);
+ZEND_METHOD(UserCache_CachePoolStatus, getUsedMemory);
+ZEND_METHOD(UserCache_Cache, hasPool);
+ZEND_METHOD(UserCache_Cache, getPool);
+ZEND_METHOD(UserCache_Cache, getPools);
+ZEND_METHOD(UserCache_Cache, getStatus);
+ZEND_METHOD(UserCache_Cache, __construct);
+ZEND_METHOD(UserCache_Cache, store);
+ZEND_METHOD(UserCache_Cache, add);
+ZEND_METHOD(UserCache_Cache, storeMultiple);
+ZEND_METHOD(UserCache_Cache, increment);
+ZEND_METHOD(UserCache_Cache, decrement);
+ZEND_METHOD(UserCache_Cache, fetch);
+ZEND_METHOD(UserCache_Cache, fetchMultiple);
+ZEND_METHOD(UserCache_Cache, delete);
+ZEND_METHOD(UserCache_Cache, deleteMultiple);
+ZEND_METHOD(UserCache_Cache, clear);
+ZEND_METHOD(UserCache_Cache, has);
+ZEND_METHOD(UserCache_Cache, lock);
+ZEND_METHOD(UserCache_Cache, unlock);
+ZEND_METHOD(UserCache_Cache, remember);
+ZEND_METHOD(UserCache_Cache, getPoolStatus);
+
+static const zend_function_entry ext_functions[] = {
+ ZEND_FE(user_cache_reset, arginfo_user_cache_reset)
+ ZEND_FE_END
+};
+
+static const zend_function_entry class_UserCache_CacheStatus_methods[] = {
+ ZEND_ME(UserCache_CacheStatus, __construct, arginfo_class_UserCache_CacheStatus___construct, ZEND_ACC_PRIVATE)
+ ZEND_ME(UserCache_CacheStatus, getAvailability, arginfo_class_UserCache_CacheStatus_getAvailability, ZEND_ACC_PUBLIC)
+ ZEND_ME(UserCache_CacheStatus, getConfiguredMemory, arginfo_class_UserCache_CacheStatus_getConfiguredMemory, ZEND_ACC_PUBLIC)
+ ZEND_ME(UserCache_CacheStatus, getSharedMemorySize, arginfo_class_UserCache_CacheStatus_getSharedMemorySize, ZEND_ACC_PUBLIC)
+ ZEND_ME(UserCache_CacheStatus, getUsedMemory, arginfo_class_UserCache_CacheStatus_getUsedMemory, ZEND_ACC_PUBLIC)
+ ZEND_ME(UserCache_CacheStatus, getFreeMemory, arginfo_class_UserCache_CacheStatus_getFreeMemory, ZEND_ACC_PUBLIC)
+ ZEND_ME(UserCache_CacheStatus, getWastedMemory, arginfo_class_UserCache_CacheStatus_getWastedMemory, ZEND_ACC_PUBLIC)
+ ZEND_ME(UserCache_CacheStatus, getEntryCount, arginfo_class_UserCache_CacheStatus_getEntryCount, ZEND_ACC_PUBLIC)
+ ZEND_ME(UserCache_CacheStatus, getEntryCapacity, arginfo_class_UserCache_CacheStatus_getEntryCapacity, ZEND_ACC_PUBLIC)
+ ZEND_ME(UserCache_CacheStatus, getTombstoneCount, arginfo_class_UserCache_CacheStatus_getTombstoneCount, ZEND_ACC_PUBLIC)
+ ZEND_ME(UserCache_CacheStatus, getExpungeCount, arginfo_class_UserCache_CacheStatus_getExpungeCount, ZEND_ACC_PUBLIC)
+ ZEND_ME(UserCache_CacheStatus, getStoreFailureCount, arginfo_class_UserCache_CacheStatus_getStoreFailureCount, ZEND_ACC_PUBLIC)
+ ZEND_FE_END
+};
+
+static const zend_function_entry class_UserCache_CachePoolStatus_methods[] = {
+ ZEND_ME(UserCache_CachePoolStatus, __construct, arginfo_class_UserCache_CachePoolStatus___construct, ZEND_ACC_PRIVATE)
+ ZEND_ME(UserCache_CachePoolStatus, getPoolName, arginfo_class_UserCache_CachePoolStatus_getPoolName, ZEND_ACC_PUBLIC)
+ ZEND_ME(UserCache_CachePoolStatus, getEntryCount, arginfo_class_UserCache_CachePoolStatus_getEntryCount, ZEND_ACC_PUBLIC)
+ ZEND_ME(UserCache_CachePoolStatus, getEntryKeys, arginfo_class_UserCache_CachePoolStatus_getEntryKeys, ZEND_ACC_PUBLIC)
+ ZEND_ME(UserCache_CachePoolStatus, getUsedMemory, arginfo_class_UserCache_CachePoolStatus_getUsedMemory, ZEND_ACC_PUBLIC)
+ ZEND_FE_END
+};
+
+static const zend_function_entry class_UserCache_Cache_methods[] = {
+ ZEND_ME(UserCache_Cache, hasPool, arginfo_class_UserCache_Cache_hasPool, ZEND_ACC_PUBLIC|ZEND_ACC_STATIC)
+ ZEND_ME(UserCache_Cache, getPool, arginfo_class_UserCache_Cache_getPool, ZEND_ACC_PUBLIC|ZEND_ACC_STATIC)
+ ZEND_ME(UserCache_Cache, getPools, arginfo_class_UserCache_Cache_getPools, ZEND_ACC_PUBLIC|ZEND_ACC_STATIC)
+ ZEND_ME(UserCache_Cache, getStatus, arginfo_class_UserCache_Cache_getStatus, ZEND_ACC_PUBLIC|ZEND_ACC_STATIC)
+ ZEND_ME(UserCache_Cache, __construct, arginfo_class_UserCache_Cache___construct, ZEND_ACC_PRIVATE)
+ ZEND_ME(UserCache_Cache, store, arginfo_class_UserCache_Cache_store, ZEND_ACC_PUBLIC)
+ ZEND_ME(UserCache_Cache, add, arginfo_class_UserCache_Cache_add, ZEND_ACC_PUBLIC)
+ ZEND_ME(UserCache_Cache, storeMultiple, arginfo_class_UserCache_Cache_storeMultiple, ZEND_ACC_PUBLIC)
+ ZEND_ME(UserCache_Cache, increment, arginfo_class_UserCache_Cache_increment, ZEND_ACC_PUBLIC)
+ ZEND_ME(UserCache_Cache, decrement, arginfo_class_UserCache_Cache_decrement, ZEND_ACC_PUBLIC)
+ ZEND_ME(UserCache_Cache, fetch, arginfo_class_UserCache_Cache_fetch, ZEND_ACC_PUBLIC)
+ ZEND_ME(UserCache_Cache, fetchMultiple, arginfo_class_UserCache_Cache_fetchMultiple, ZEND_ACC_PUBLIC)
+ ZEND_ME(UserCache_Cache, delete, arginfo_class_UserCache_Cache_delete, ZEND_ACC_PUBLIC)
+ ZEND_ME(UserCache_Cache, deleteMultiple, arginfo_class_UserCache_Cache_deleteMultiple, ZEND_ACC_PUBLIC)
+ ZEND_ME(UserCache_Cache, clear, arginfo_class_UserCache_Cache_clear, ZEND_ACC_PUBLIC)
+ ZEND_ME(UserCache_Cache, has, arginfo_class_UserCache_Cache_has, ZEND_ACC_PUBLIC)
+ ZEND_ME(UserCache_Cache, lock, arginfo_class_UserCache_Cache_lock, ZEND_ACC_PUBLIC)
+ ZEND_ME(UserCache_Cache, unlock, arginfo_class_UserCache_Cache_unlock, ZEND_ACC_PUBLIC)
+ ZEND_ME(UserCache_Cache, remember, arginfo_class_UserCache_Cache_remember, ZEND_ACC_PUBLIC)
+ ZEND_ME(UserCache_Cache, getPoolStatus, arginfo_class_UserCache_Cache_getPoolStatus, ZEND_ACC_PUBLIC)
+ ZEND_FE_END
+};
+
+static zend_class_entry *register_class_UserCache_CacheAvailability(void)
+{
+ zend_class_entry *class_entry = zend_register_internal_enum("UserCache\\CacheAvailability", IS_UNDEF, NULL);
+
+ zend_enum_add_case_cstr(class_entry, "Available", NULL);
+
+ zend_enum_add_case_cstr(class_entry, "DisabledByIni", NULL);
+
+ zend_enum_add_case_cstr(class_entry, "DisabledBySapi", NULL);
+
+ zend_enum_add_case_cstr(class_entry, "UnavailableBySharedMemoryInitializationFailed", NULL);
+
+ zend_enum_add_case_cstr(class_entry, "UnavailableByBackendNotInitializedBeforeWorkerStartup", NULL);
+
+ zend_enum_add_case_cstr(class_entry, "UnavailableByBackendInitializedAfterWorkerStartup", NULL);
+
+ zend_enum_add_case_cstr(class_entry, "UnavailableByCgiFastCgiBoundary", NULL);
+
+ zend_enum_add_case_cstr(class_entry, "UnavailableByLsapiBoundary", NULL);
+
+ zend_enum_add_case_cstr(class_entry, "UnavailableByUnknownReason", NULL);
+
+ return class_entry;
+}
+
+static zend_class_entry *register_class_UserCache_CacheStatus(void)
+{
+ zend_class_entry ce, *class_entry;
+
+ INIT_NS_CLASS_ENTRY(ce, "UserCache", "CacheStatus", class_UserCache_CacheStatus_methods);
+ class_entry = zend_register_internal_class_with_flags(&ce, NULL, ZEND_ACC_FINAL|ZEND_ACC_NO_DYNAMIC_PROPERTIES|ZEND_ACC_NOT_SERIALIZABLE|ZEND_ACC_READONLY_CLASS);
+
+ return class_entry;
+}
+
+static zend_class_entry *register_class_UserCache_CachePoolStatus(void)
+{
+ zend_class_entry ce, *class_entry;
+
+ INIT_NS_CLASS_ENTRY(ce, "UserCache", "CachePoolStatus", class_UserCache_CachePoolStatus_methods);
+ class_entry = zend_register_internal_class_with_flags(&ce, NULL, ZEND_ACC_FINAL|ZEND_ACC_NO_DYNAMIC_PROPERTIES|ZEND_ACC_NOT_SERIALIZABLE|ZEND_ACC_READONLY_CLASS);
+
+ return class_entry;
+}
+
+static zend_class_entry *register_class_UserCache_Cache(void)
+{
+ zend_class_entry ce, *class_entry;
+
+ INIT_NS_CLASS_ENTRY(ce, "UserCache", "Cache", class_UserCache_Cache_methods);
+ class_entry = zend_register_internal_class_with_flags(&ce, NULL, ZEND_ACC_FINAL|ZEND_ACC_NOT_SERIALIZABLE);
+
+ return class_entry;
+}
diff --git a/ext/user_cache/user_cache_decl.h b/ext/user_cache/user_cache_decl.h
new file mode 100644
index 000000000000..03d993c33468
--- /dev/null
+++ b/ext/user_cache/user_cache_decl.h
@@ -0,0 +1,19 @@
+/* This is a generated file, edit user_cache.stub.php instead.
+ * Stub hash: f475d675d1f7d8fc96ecc58b290107267010d958 */
+
+#ifndef ZEND_USER_CACHE_DECL_f475d675d1f7d8fc96ecc58b290107267010d958_H
+#define ZEND_USER_CACHE_DECL_f475d675d1f7d8fc96ecc58b290107267010d958_H
+
+typedef enum zend_enum_UserCache_CacheAvailability {
+ ZEND_ENUM_UserCache_CacheAvailability_Available = 1,
+ ZEND_ENUM_UserCache_CacheAvailability_DisabledByIni = 2,
+ ZEND_ENUM_UserCache_CacheAvailability_DisabledBySapi = 3,
+ ZEND_ENUM_UserCache_CacheAvailability_UnavailableBySharedMemoryInitializationFailed = 4,
+ ZEND_ENUM_UserCache_CacheAvailability_UnavailableByBackendNotInitializedBeforeWorkerStartup = 5,
+ ZEND_ENUM_UserCache_CacheAvailability_UnavailableByBackendInitializedAfterWorkerStartup = 6,
+ ZEND_ENUM_UserCache_CacheAvailability_UnavailableByCgiFastCgiBoundary = 7,
+ ZEND_ENUM_UserCache_CacheAvailability_UnavailableByLsapiBoundary = 8,
+ ZEND_ENUM_UserCache_CacheAvailability_UnavailableByUnknownReason = 9,
+} zend_enum_UserCache_CacheAvailability;
+
+#endif /* ZEND_USER_CACHE_DECL_f475d675d1f7d8fc96ecc58b290107267010d958_H */
diff --git a/ext/user_cache/user_cache_entries.c b/ext/user_cache/user_cache_entries.c
new file mode 100644
index 000000000000..c31e7be0673b
--- /dev/null
+++ b/ext/user_cache/user_cache_entries.c
@@ -0,0 +1,4218 @@
+/*
+ +----------------------------------------------------------------------+
+ | 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 "user_cache_internal.h"
+
+#include "Zend/zend_closures.h"
+#include "Zend/zend_gc.h"
+#include "Zend/zend_objects.h"
+
+#define PHP_USER_CACHE_REQUEST_LOCAL_NO_DEEP_CLONE ((void *) 1)
+#define PHP_USER_CACHE_REQUEST_LOCAL_NEEDS_DEEP_CLONE ((void *) 2)
+
+/* Release the lock before propagating a bailout. */
+#define PHP_USER_CACHE_TRY_UNLOCK_ON_BAILOUT(stmt) \
+ do { \
+ zend_try { \
+ stmt \
+ } zend_catch { \
+ php_user_cache_unlock_if_held(); \
+ zend_bailout(); \
+ } zend_end_try(); \
+ } while (0)
+
+typedef enum {
+ PHP_USER_CACHE_REQUEST_LOCAL_SLOT_MISS,
+ PHP_USER_CACHE_REQUEST_LOCAL_SLOT_HIT
+} php_user_cache_request_local_slot_result;
+
+typedef enum {
+ PHP_USER_CACHE_FIND_SLOT_IGNORE_EXPIRY = 0,
+ PHP_USER_CACHE_FIND_SLOT_SKIP_EXPIRED,
+ PHP_USER_CACHE_FIND_SLOT_DELETE_EXPIRED
+} php_user_cache_find_slot_expiry_mode;
+
+typedef enum {
+ PHP_USER_CACHE_FETCH_FINISH_USE_REQUEST_LOCAL_SLOT = 1 << 0,
+ PHP_USER_CACHE_FETCH_FINISH_NO_ALIASES = 1 << 1,
+ PHP_USER_CACHE_FETCH_FINISH_DEFER_REQUEST_LOCAL_SLOT = 1 << 2
+} php_user_cache_fetch_finish_flags;
+
+typedef enum {
+ PHP_USER_CACHE_FETCH_LOCATE_MISS,
+ PHP_USER_CACHE_FETCH_LOCATE_SCALAR_HIT,
+ PHP_USER_CACHE_FETCH_LOCATE_SLOT,
+ PHP_USER_CACHE_FETCH_LOCATE_UNCACHED
+} php_user_cache_fetch_locate_result;
+
+typedef enum {
+ PHP_USER_CACHE_OBJECT_STORABLE_VIA_HOOKS,
+ PHP_USER_CACHE_OBJECT_SERDES,
+ PHP_USER_CACHE_OBJECT_OPAQUE,
+ PHP_USER_CACHE_OBJECT_SCAN_MEMBERS
+} php_user_cache_object_storability;
+
+typedef enum {
+ PHP_USER_CACHE_STORE_ATTEMPT_STORED,
+ PHP_USER_CACHE_STORE_ATTEMPT_RETRY,
+ PHP_USER_CACHE_STORE_ATTEMPT_FAILED
+} php_user_cache_store_attempt_result;
+
+typedef struct {
+ HashTable arrays;
+ HashTable objects;
+ HashTable references;
+ HashTable *clone_verdicts;
+ bool track_identity;
+} php_user_cache_request_local_clone_context;
+
+typedef struct {
+ HashTable *seen_arrays;
+ HashTable *seen_objects;
+ const char **failure_message;
+} php_user_cache_unstorable_context;
+
+static bool user_cache_clone_request_local_value(
+ php_user_cache_request_local_clone_context *ctx,
+ zval *dst,
+ zval *src
+);
+
+static bool user_cache_collect_request_local_clone_verdicts_inner(
+ zval *value,
+ HashTable *seen_arrs,
+ HashTable *seen_objs,
+ HashTable *verdicts,
+ bool record_array_result
+);
+
+static bool user_cache_find_unstorable_value(
+ zval *value,
+ HashTable *seen_arrs,
+ HashTable *seen_objs,
+ const char **msg
+);
+
+static void user_cache_rehash_locked(php_user_cache_header *header);
+static bool user_cache_expunge_expired_locked(void);
+static bool user_cache_expunge_expired_bounded_locked(void);
+
+static zend_always_inline bool user_cache_value_uses_offset(uint8_t value_type)
+{
+ return
+ value_type == PHP_USER_CACHE_VALUE_STRING ||
+ value_type == PHP_USER_CACHE_VALUE_SHARED_GRAPH
+ ;
+}
+
+static zend_always_inline uint32_t user_cache_max_capacity_for_size(size_t size)
+{
+ size_t usable;
+
+ if (size <= sizeof(php_user_cache_header)) {
+ return 0;
+ }
+
+ usable = (size - sizeof(php_user_cache_header)) / sizeof(php_user_cache_entry);
+
+ return usable > UINT32_MAX
+ ? UINT32_MAX
+ : (uint32_t) usable
+ ;
+}
+
+static zend_always_inline uint8_t *user_cache_ptr_in_header(
+ const php_user_cache_header *header,
+ uint32_t offset)
+{
+ return (uint8_t *) header + offset;
+}
+
+static zend_always_inline bool user_cache_key_equals(
+ const php_user_cache_header *header,
+ const php_user_cache_entry *entry,
+ zend_string *key,
+ zend_ulong hash)
+{
+ if (entry->state != PHP_USER_CACHE_ENTRY_USED || entry->hash != hash || entry->key_len != ZSTR_LEN(key)) {
+ return false;
+ }
+
+ return memcmp(
+ user_cache_ptr_in_header(header, entry->key_offset),
+ ZSTR_VAL(key),
+ ZSTR_LEN(key)
+ ) == 0;
+}
+
+static zend_always_inline uint32_t user_cache_write_payload_locked(
+ const php_user_cache_header *header,
+ uint32_t reusable_offset,
+ size_t size,
+ const void *src)
+{
+ if (reusable_offset != 0 && php_user_cache_block_payload_capacity(reusable_offset) >= size) {
+ memcpy(user_cache_ptr_in_header(header, reusable_offset), src, size);
+
+ return reusable_offset;
+ }
+
+ return php_user_cache_alloc_locked(size, src);
+}
+
+static zend_always_inline php_user_cache_lookup_entry *user_cache_lookup_cache_set(zend_ulong hash)
+{
+ uint32_t set_idx = (uint32_t) (hash & (PHP_USER_CACHE_LOOKUP_SETS - 1));
+
+ return &UC_G(lookup_entry_storage)[set_idx * PHP_USER_CACHE_LOOKUP_WAYS];
+}
+
+static zend_always_inline void user_cache_lookup_entry_release_key(
+ php_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 user_cache_lookup_cache_store(
+ php_user_cache_lookup_entry *lookup_entry,
+ zend_ulong hash,
+ uint64_t epoch,
+ uint32_t slot_idx,
+ uint8_t state)
+{
+ if (lookup_entry == NULL) {
+ return;
+ }
+
+ user_cache_lookup_entry_release_key(lookup_entry);
+
+ lookup_entry->hash = hash;
+ lookup_entry->mutation_epoch = epoch;
+ lookup_entry->context = php_user_cache_active_context();
+ lookup_entry->slot_index = slot_idx;
+ lookup_entry->state = state;
+ lookup_entry->value_type = PHP_USER_CACHE_LOOKUP_VALUE_NONE;
+}
+
+/* Prefer the current context, then empty, stale and miss slots. */
+static zend_always_inline php_user_cache_lookup_entry *user_cache_lookup_cache_select_slot(
+ php_user_cache_lookup_entry *lookup_entries,
+ zend_ulong hash,
+ uint64_t epoch,
+ bool allow_hit_eviction)
+{
+ const void *ctx = php_user_cache_active_context();
+ php_user_cache_lookup_entry *preferred, *alternate;
+ uint32_t way = (uint32_t) ((((uint64_t) hash >> 32) ^ hash) & (PHP_USER_CACHE_LOOKUP_WAYS - 1));
+
+ if (lookup_entries == NULL) {
+ return NULL;
+ }
+
+ preferred = &lookup_entries[way];
+ alternate = preferred == &lookup_entries[0] ? &lookup_entries[1] : &lookup_entries[0];
+
+ if (preferred->state != PHP_USER_CACHE_LOOKUP_EMPTY &&
+ preferred->hash == hash &&
+ preferred->mutation_epoch == epoch &&
+ preferred->context == ctx
+ ) {
+ return preferred;
+ }
+ if (alternate->state != PHP_USER_CACHE_LOOKUP_EMPTY &&
+ alternate->hash == hash &&
+ alternate->mutation_epoch == epoch &&
+ alternate->context == ctx
+ ) {
+ return alternate;
+ }
+
+ if (preferred->state == PHP_USER_CACHE_LOOKUP_EMPTY ||
+ preferred->mutation_epoch != epoch ||
+ preferred->context != ctx
+ ) {
+ return preferred;
+ }
+ if (alternate->state == PHP_USER_CACHE_LOOKUP_EMPTY ||
+ alternate->mutation_epoch != epoch ||
+ alternate->context != ctx
+ ) {
+ return alternate;
+ }
+
+ if (preferred->state == PHP_USER_CACHE_LOOKUP_MISS) {
+ return preferred;
+ }
+ if (alternate->state == PHP_USER_CACHE_LOOKUP_MISS) {
+ return alternate;
+ }
+
+ return allow_hit_eviction ? preferred : NULL;
+}
+
+static zend_always_inline void user_cache_lookup_cache_store_miss(
+ php_user_cache_lookup_entry *lookup_entries,
+ zend_ulong hash,
+ uint64_t epoch,
+ zend_string *key)
+{
+ php_user_cache_lookup_entry *victim =
+ user_cache_lookup_cache_select_slot(lookup_entries, hash, epoch, false)
+ ;
+
+ if (victim == NULL) {
+ return;
+ }
+
+ user_cache_lookup_cache_store(
+ victim,
+ hash,
+ epoch,
+ PHP_USER_CACHE_LOOKUP_INVALID_SLOT,
+ PHP_USER_CACHE_LOOKUP_MISS
+ );
+
+ /* MISS entries must still distinguish hash collisions. */
+ victim->key = zend_string_copy(key);
+}
+
+static zend_always_inline void user_cache_lookup_cache_store_hit(
+ php_user_cache_lookup_entry *lookup_entry,
+ zend_ulong hash,
+ uint64_t epoch,
+ uint32_t slot_idx,
+ zend_string *key,
+ const php_user_cache_entry *entry)
+{
+ if (lookup_entry == NULL) {
+ return;
+ }
+
+ user_cache_lookup_cache_store(
+ lookup_entry,
+ hash,
+ epoch,
+ slot_idx,
+ PHP_USER_CACHE_LOOKUP_HIT
+ );
+
+ lookup_entry->key = zend_string_copy(key);
+
+ if (entry->expires_at != 0) {
+ return;
+ }
+
+ switch (entry->value_type) {
+ case PHP_USER_CACHE_VALUE_NULL:
+ case PHP_USER_CACHE_VALUE_TRUE:
+ case PHP_USER_CACHE_VALUE_FALSE:
+ case PHP_USER_CACHE_VALUE_LONG:
+ case PHP_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 void user_cache_lookup_cache_reset_entry(php_user_cache_lookup_entry *lookup_entry)
+{
+ if (lookup_entry == NULL) {
+ return;
+ }
+
+ user_cache_lookup_entry_release_key(lookup_entry);
+
+ memset(lookup_entry, 0, sizeof(*lookup_entry));
+}
+
+static zend_always_inline void 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 == PHP_USER_CACHE_VALUE_SHARED_GRAPH) {
+ graph_quiescent = php_user_cache_quiesce_graph_payloads_locked();
+
+ if (php_user_cache_shared_graph_retire_payload_locked(value_offset)) {
+ if (graph_quiescent) {
+ php_user_cache_free_locked(value_offset);
+ } else {
+ php_user_cache_shared_graph_orphan_payload_locked(value_offset);
+ }
+ }
+ } else if (user_cache_value_uses_offset(value_type)) {
+ php_user_cache_free_locked(value_offset);
+ }
+}
+
+static zend_always_inline void user_cache_release_entry_storage_except_locked(
+ php_user_cache_entry *entry,
+ const php_user_cache_entry *kept_entry)
+{
+ bool combined, kept_combined;
+
+ combined = (entry->reserved & PHP_USER_CACHE_ENTRY_RESERVED_COMBINED_VALUE_KEY) != 0;
+ kept_combined = kept_entry != NULL &&
+ (kept_entry->reserved & PHP_USER_CACHE_ENTRY_RESERVED_COMBINED_VALUE_KEY) != 0
+ ;
+ if (entry->key_offset != 0 && !combined &&
+ (kept_entry == NULL || kept_combined || entry->key_offset != kept_entry->key_offset)
+ ) {
+ php_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
+ )
+ ) {
+ user_cache_release_value_storage_locked(entry->value_type, entry->value_offset);
+ }
+}
+
+static zend_always_inline void user_cache_release_entry_storage_locked(php_user_cache_entry *entry)
+{
+ user_cache_release_entry_storage_except_locked(entry, NULL);
+}
+
+static zend_always_inline void user_cache_delete_entry_locked(php_user_cache_header *header, php_user_cache_entry *entry)
+{
+ if (entry->state == PHP_USER_CACHE_ENTRY_USED && header->count != 0) {
+ header->count--;
+ }
+
+ if (entry->state != PHP_USER_CACHE_ENTRY_TOMBSTONE) {
+ header->tombstone_count++;
+ }
+
+ user_cache_release_entry_storage_locked(entry);
+
+ entry->hash = 0;
+ entry->key_offset = 0;
+ entry->key_len = 0;
+ entry->state = PHP_USER_CACHE_ENTRY_TOMBSTONE;
+ entry->value_type = PHP_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;
+
+ php_user_cache_bump_mutation_epoch_locked(header);
+}
+
+static zend_always_inline void user_cache_release_request_local_slot_table(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 user_cache_is_expired(const php_user_cache_entry *entry, uint64_t now)
+{
+ return entry->state == PHP_USER_CACHE_ENTRY_USED &&
+ entry->expires_at != 0 &&
+ entry->expires_at <= now
+ ;
+}
+
+static zend_always_inline bool user_cache_is_expired_now(const php_user_cache_entry *entry, uint64_t *now)
+{
+ if (entry->state != PHP_USER_CACHE_ENTRY_USED || entry->expires_at == 0) {
+ return false;
+ }
+
+ if (*now == 0) {
+ *now = (uint64_t) time(NULL);
+ }
+
+ return user_cache_is_expired(entry, *now);
+}
+
+static zend_always_inline void user_cache_note_expired_read(void)
+{
+ UC_G(expired_read_observations)++;
+}
+
+static zend_always_inline bool user_cache_scalar_to_zval(
+ uint8_t value_type,
+ zend_long lval,
+ double dval,
+ zval *return_value)
+{
+ switch (value_type) {
+ case PHP_USER_CACHE_VALUE_NULL:
+ ZVAL_NULL(return_value);
+
+ return true;
+ case PHP_USER_CACHE_VALUE_TRUE:
+ ZVAL_TRUE(return_value);
+
+ return true;
+ case PHP_USER_CACHE_VALUE_FALSE:
+ ZVAL_FALSE(return_value);
+
+ return true;
+ case PHP_USER_CACHE_VALUE_LONG:
+ ZVAL_LONG(return_value, lval);
+
+ return true;
+ case PHP_USER_CACHE_VALUE_DOUBLE:
+ ZVAL_DOUBLE(return_value, dval);
+
+ return true;
+ default:
+ return false;
+ }
+}
+
+static zend_always_inline bool user_cache_payload_can_fit_locked(size_t size)
+{
+ php_user_cache_header *header = php_user_cache_header_ptr();
+ size_t total_size;
+
+ if (!header || size == 0 || size > UINT32_MAX - sizeof(php_user_cache_block)) {
+ return false;
+ }
+
+ total_size = PHP_USER_CACHE_ALIGNED_SIZE(sizeof(php_user_cache_block) + size);
+
+ return total_size <= UINT32_MAX && total_size <= header->data_size;
+}
+
+static zend_always_inline void user_cache_init_prepared_value(php_user_cache_prepared_value *prepared)
+{
+ memset(prepared, 0, sizeof(*prepared));
+
+ prepared->value_type = PHP_USER_CACHE_VALUE_NULL;
+}
+
+static zend_always_inline bool user_cache_prepared_value_should_seed_request_local_slot(
+ const php_user_cache_prepared_value *prepared)
+{
+ return prepared->value_type == PHP_USER_CACHE_VALUE_STRING &&
+ prepared->value_len >= PHP_USER_CACHE_REQUEST_LOCAL_STRING_MIN_LEN
+ ;
+}
+
+static zend_always_inline bool user_cache_long_add_overflow(
+ 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 user_cache_long_sub_overflow(
+ 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 user_cache_maybe_rehash_locked(void)
+{
+ php_user_cache_header *header = php_user_cache_header_ptr();
+
+ if (header != NULL &&
+ php_user_cache_header_is_initialized_locked() &&
+ header->tombstone_count > header->capacity / 4
+ ) {
+ user_cache_rehash_locked(header);
+ }
+}
+
+static zend_always_inline void user_cache_maybe_expunge_expired_locked(void)
+{
+ if (EXPECTED(UC_G(expired_read_observations) < PHP_USER_CACHE_EXPIRED_READ_EXPUNGE_THRESHOLD)) {
+ return;
+ }
+
+ UC_G(expired_read_observations) = 0;
+
+ (void) user_cache_expunge_expired_bounded_locked();
+}
+
+#ifdef PHP_USER_CACHE_HAVE_OPTIMISTIC
+static zend_always_inline bool user_cache_optimistic_payload_in_bounds(
+ const php_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(php_user_cache_block) &&
+ (uint64_t) offset + len <= limit
+ ;
+}
+
+static zend_always_inline bool user_cache_optimistic_header(
+ php_user_cache_header **header_ptr,
+ uint64_t *seq_ptr)
+{
+ php_user_cache_header *header = php_user_cache_header_ptr();
+ uint64_t seq;
+
+ if (header == NULL) {
+ return false;
+ }
+
+ seq = php_user_cache_seq_load(&header->write_seq);
+ if (seq < 2 || (seq & 1) != 0) {
+ return false;
+ }
+
+ if (header->magic != PHP_USER_CACHE_MAGIC ||
+ header->version != PHP_USER_CACHE_VERSION ||
+ header->capacity < PHP_USER_CACHE_MIN_CAPACITY ||
+ header->capacity > user_cache_max_capacity_for_size(
+ php_user_cache_active_context()->storage.size
+ )
+ ) {
+ return false;
+ }
+
+ if (php_user_cache_seq_reload(&header->write_seq) != seq) {
+ return false;
+ }
+
+ *header_ptr = header;
+ *seq_ptr = seq;
+
+ return true;
+}
+
+static zend_always_inline php_user_cache_request_local_slot *user_cache_alloc_request_local_slot(
+ uint64_t gen,
+ bool no_aliases,
+ bool has_value)
+{
+ php_user_cache_request_local_slot *slot;
+
+ slot = emalloc(sizeof(php_user_cache_request_local_slot));
+ slot->generation = gen;
+ slot->context = php_user_cache_active_context();
+ slot->needs_deep_clone = false;
+ slot->has_clone_verdicts = false;
+ slot->no_aliases = no_aliases;
+ slot->has_value = has_value;
+
+ ZVAL_UNDEF(&slot->value);
+
+ return slot;
+}
+
+/* The caller must recheck write_seq before using the snapshot. */
+static bool user_cache_optimistic_probe(
+ const php_user_cache_header *header,
+ zend_string *key,
+ zend_ulong hash,
+ const php_user_cache_entry *entries,
+ php_user_cache_entry *snapshot,
+ uint32_t *slot_idx,
+ bool *found)
+{
+ const php_user_cache_entry *entry;
+ uint64_t now = 0;
+ uint32_t i, step;
+
+ i = (uint32_t) (hash % header->capacity);
+
+ for (step = 0; step < header->capacity; step++) {
+ entry = &entries[i];
+
+ switch (entry->state) {
+ case PHP_USER_CACHE_ENTRY_EMPTY:
+ *found = false;
+
+ return true;
+ case PHP_USER_CACHE_ENTRY_USED:
+ if (entry->hash == hash && entry->key_len == ZSTR_LEN(key)) {
+ if (!user_cache_optimistic_payload_in_bounds(header, entry->key_offset, entry->key_len)) {
+ return false;
+ }
+
+ if (memcmp(user_cache_ptr_in_header(header, entry->key_offset), ZSTR_VAL(key), ZSTR_LEN(key)) == 0) {
+ if (user_cache_is_expired_now(entry, &now)) {
+ user_cache_note_expired_read();
+ *found = false;
+
+ return true;
+ }
+
+ *snapshot = *entry;
+ *slot_idx = i;
+ *found = true;
+
+ return true;
+ }
+ }
+ break;
+ case PHP_USER_CACHE_ENTRY_TOMBSTONE:
+ break;
+ default:
+ return false;
+ }
+
+ ++i;
+ if (i == header->capacity) {
+ i = 0;
+ }
+ }
+
+ *found = false;
+
+ return true;
+}
+#endif
+
+static void user_cache_request_local_slot_dtor(zval *slot_zv)
+{
+ php_user_cache_request_local_slot *slot = Z_PTR_P(slot_zv);
+
+ if (slot->has_clone_verdicts) {
+ zend_hash_destroy(&slot->clone_verdicts);
+ }
+
+ if (!Z_ISUNDEF(slot->value)) {
+ zval_ptr_dtor(&slot->value);
+ }
+
+ efree(slot);
+}
+
+static bool user_cache_value_needs_request_local_deep_clone_inner(
+ zval *value,
+ HashTable *seen_arrs)
+{
+ zend_ulong arr_key;
+ zval *elem;
+
+ if (php_user_cache_stack_overflowed()) {
+ return true;
+ }
+
+ if (Z_ISREF_P(value)) {
+ return true;
+ }
+
+ switch (Z_TYPE_P(value)) {
+ case IS_OBJECT:
+ return true;
+ case IS_ARRAY:
+ arr_key = (zend_ulong) (uintptr_t) Z_ARRVAL_P(value);
+ if (!php_user_cache_seen_test_and_add(seen_arrs, Z_ARRVAL_P(value))) {
+ return false;
+ }
+
+ ZEND_HASH_FOREACH_VAL(Z_ARRVAL_P(value), elem) {
+ if (user_cache_value_needs_request_local_deep_clone_inner(elem, seen_arrs)) {
+ zend_hash_index_del(seen_arrs, arr_key);
+
+ return true;
+ }
+ } ZEND_HASH_FOREACH_END();
+
+ zend_hash_index_del(seen_arrs, arr_key);
+
+ return false;
+ default:
+ return false;
+ }
+}
+
+static bool user_cache_value_needs_request_local_deep_clone(zval *value)
+{
+ HashTable seen_arrs;
+ 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(&seen_arrs, 8, NULL, NULL, 0);
+
+ result = user_cache_value_needs_request_local_deep_clone_inner(value, &seen_arrs);
+ zend_hash_destroy(&seen_arrs);
+
+ return result;
+}
+
+static bool user_cache_collect_request_local_object_clone_verdict(
+ zend_object *obj,
+ HashTable *seen_arrs,
+ HashTable *seen_objs,
+ HashTable *verdicts)
+{
+ zend_ulong obj_key;
+ zval *prop, *src, *end;
+ bool members_need_clone = false;
+ void *flag;
+
+ if (php_user_cache_stack_overflowed()) {
+ /* A failed walk cannot prove that the value is acyclic. */
+ UC_G(request_local_slot_may_cycle) = true;
+
+ return true;
+ }
+
+ if (obj == NULL) {
+ return true;
+ }
+
+ obj_key = (zend_ulong) (uintptr_t) obj;
+
+ flag = zend_hash_index_find_ptr(verdicts, obj_key);
+ if (flag != NULL) {
+ return flag == PHP_USER_CACHE_REQUEST_LOCAL_NEEDS_DEEP_CLONE;
+ }
+
+ if (!php_user_cache_seen_test_and_add(seen_objs, obj)) {
+ /* A seen unfinished object is a back edge. */
+ UC_G(request_local_slot_may_cycle) = true;
+
+ return true;
+ }
+
+ if (obj->ce->default_properties_count) {
+ src = obj->properties_table;
+ end = src + obj->ce->default_properties_count;
+
+ do {
+ if (user_cache_collect_request_local_clone_verdicts_inner(
+ src,
+ seen_arrs,
+ seen_objs,
+ verdicts,
+ true
+ )
+ ) {
+ members_need_clone = true;
+ }
+
+ src++;
+ } while (src != end);
+ }
+
+ if (obj->properties != NULL && zend_hash_num_elements(obj->properties) != 0) {
+ members_need_clone = true;
+
+ ZEND_HASH_MAP_FOREACH_VAL(obj->properties, prop) {
+ if (Z_TYPE_P(prop) != IS_INDIRECT) {
+ user_cache_collect_request_local_clone_verdicts_inner(
+ prop,
+ seen_arrs,
+ seen_objs,
+ verdicts,
+ true
+ );
+ }
+ } ZEND_HASH_FOREACH_END();
+ }
+
+ zend_hash_index_update_ptr(
+ verdicts,
+ obj_key,
+ members_need_clone
+ ? PHP_USER_CACHE_REQUEST_LOCAL_NEEDS_DEEP_CLONE
+ : PHP_USER_CACHE_REQUEST_LOCAL_NO_DEEP_CLONE
+ );
+
+ return members_need_clone;
+}
+
+static bool user_cache_collect_request_local_clone_verdicts_inner(
+ zval *value,
+ HashTable *seen_arrs,
+ HashTable *seen_objs,
+ HashTable *verdicts,
+ bool record_array_result)
+{
+ zend_ulong arr_key;
+ zval *elem;
+ bool needs_deep_clone = false;
+ void *flag;
+
+ if (php_user_cache_stack_overflowed()) {
+ UC_G(request_local_slot_may_cycle) = true;
+
+ return true;
+ }
+
+ if (Z_ISREF_P(value)) {
+ user_cache_collect_request_local_clone_verdicts_inner(
+ &Z_REF_P(value)->val,
+ seen_arrs,
+ seen_objs,
+ verdicts,
+ true
+ );
+
+ return true;
+ }
+
+ switch (Z_TYPE_P(value)) {
+ case IS_OBJECT:
+ user_cache_collect_request_local_object_clone_verdict(
+ Z_OBJ_P(value),
+ seen_arrs,
+ seen_objs,
+ verdicts
+ );
+
+ return true;
+ case IS_ARRAY:
+ arr_key = (zend_ulong) (uintptr_t) Z_ARRVAL_P(value);
+ flag = zend_hash_index_find_ptr(verdicts, arr_key);
+ if (flag != NULL) {
+ return flag == PHP_USER_CACHE_REQUEST_LOCAL_NEEDS_DEEP_CLONE;
+ }
+
+ if (!php_user_cache_seen_test_and_add(seen_arrs, Z_ARRVAL_P(value))) {
+ /* A seen active array is a back edge. */
+ UC_G(request_local_slot_may_cycle) = true;
+
+ return false;
+ }
+
+ ZEND_HASH_FOREACH_VAL(Z_ARRVAL_P(value), elem) {
+ if (user_cache_collect_request_local_clone_verdicts_inner(
+ elem,
+ seen_arrs,
+ seen_objs,
+ verdicts,
+ false
+ )
+ ) {
+ needs_deep_clone = true;
+ }
+ } ZEND_HASH_FOREACH_END();
+
+ zend_hash_index_del(seen_arrs, arr_key);
+
+ if (needs_deep_clone || record_array_result) {
+ zend_hash_index_update_ptr(
+ verdicts,
+ arr_key,
+ needs_deep_clone
+ ? PHP_USER_CACHE_REQUEST_LOCAL_NEEDS_DEEP_CLONE
+ : PHP_USER_CACHE_REQUEST_LOCAL_NO_DEEP_CLONE
+ );
+ }
+
+ return needs_deep_clone;
+ default:
+ return false;
+ }
+}
+
+static bool user_cache_collect_request_local_clone_verdicts(
+ zval *value,
+ HashTable *verdicts)
+{
+ HashTable seen_arrs, seen_objs;
+ bool result;
+
+ zend_hash_init(&seen_arrs, 8, NULL, NULL, 0);
+ zend_hash_init(&seen_objs, 8, NULL, NULL, 0);
+
+ result = user_cache_collect_request_local_clone_verdicts_inner(
+ value,
+ &seen_arrs,
+ &seen_objs,
+ verdicts,
+ true
+ );
+
+ zend_hash_destroy(&seen_objs);
+ zend_hash_destroy(&seen_arrs);
+
+ return result;
+}
+
+static void user_cache_request_local_clone_array_dtor(zval *zv)
+{
+ zend_array *array = Z_PTR_P(zv);
+
+ zend_array_release(array);
+}
+
+static void user_cache_request_local_clone_context_init(
+ php_user_cache_request_local_clone_context *ctx,
+ HashTable *verdicts,
+ bool track_identity)
+{
+ zend_hash_init(&ctx->arrays, 8, NULL, user_cache_request_local_clone_array_dtor, 0);
+ zend_hash_init(&ctx->objects, 8, NULL, php_user_cache_object_table_dtor, 0);
+ zend_hash_init(&ctx->references, 8, NULL, php_user_cache_reference_table_dtor, 0);
+
+ ctx->clone_verdicts = verdicts;
+ ctx->track_identity = track_identity;
+}
+
+static void user_cache_request_local_clone_context_destroy(
+ php_user_cache_request_local_clone_context *ctx)
+{
+ zend_hash_destroy(&ctx->references);
+ zend_hash_destroy(&ctx->objects);
+ zend_hash_destroy(&ctx->arrays);
+}
+
+static bool user_cache_value_needs_request_local_deep_clone_cached(
+ php_user_cache_request_local_clone_context *ctx,
+ 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 (ctx->clone_verdicts != NULL) {
+ flag = zend_hash_index_find_ptr(ctx->clone_verdicts, (zend_ulong) (uintptr_t) Z_ARRVAL_P(value));
+ if (flag != NULL) {
+ return flag == PHP_USER_CACHE_REQUEST_LOCAL_NEEDS_DEEP_CLONE;
+ }
+ }
+
+ return user_cache_value_needs_request_local_deep_clone(value);
+}
+
+static bool user_cache_clone_request_local_array(
+ php_user_cache_request_local_clone_context *ctx,
+ zval *dst,
+ zval *src,
+ bool known_needs_deep_clone)
+{
+ zend_ulong key = (zend_ulong) (uintptr_t) Z_ARRVAL_P(src);
+ zend_array *array;
+ zval *elem, cloned_elem;
+ void *flag;
+
+ if (!known_needs_deep_clone) {
+ if (ctx->clone_verdicts != NULL) {
+ flag = zend_hash_index_find_ptr(ctx->clone_verdicts, key);
+ if (flag == PHP_USER_CACHE_REQUEST_LOCAL_NO_DEEP_CLONE) {
+ ZVAL_COPY(dst, src);
+
+ return true;
+ } else if (flag == PHP_USER_CACHE_REQUEST_LOCAL_NEEDS_DEEP_CLONE) {
+ known_needs_deep_clone = true;
+ }
+ }
+
+ if (!known_needs_deep_clone && !user_cache_value_needs_request_local_deep_clone(src)) {
+ ZVAL_COPY(dst, src);
+
+ return true;
+ }
+ }
+
+ if (ctx->track_identity) {
+ array = zend_hash_index_find_ptr(&ctx->arrays, key);
+ if (array != NULL) {
+ GC_ADDREF(array);
+ ZVAL_ARR(dst, array);
+
+ return true;
+ }
+ }
+
+ array = zend_array_dup(Z_ARRVAL_P(src));
+ if (ctx->track_identity) {
+ zend_hash_index_update_ptr(&ctx->arrays, key, array);
+ }
+
+ ZEND_HASH_FOREACH_VAL(array, elem) {
+ if (Z_TYPE_P(elem) == IS_INDIRECT ||
+ !user_cache_value_needs_request_local_deep_clone_cached(ctx, elem)
+ ) {
+ continue;
+ }
+
+ if (!user_cache_clone_request_local_value(ctx, &cloned_elem, elem)) {
+ if (!ctx->track_identity) {
+ zend_array_release(array);
+ }
+
+ ZVAL_UNDEF(dst);
+
+ return false;
+ }
+
+ zval_ptr_dtor(elem);
+
+ ZVAL_COPY_VALUE(elem, &cloned_elem);
+ } ZEND_HASH_FOREACH_END();
+
+ if (ctx->track_identity) {
+ GC_ADDREF(array);
+ }
+
+ ZVAL_ARR(dst, array);
+
+ return true;
+}
+
+static bool user_cache_clone_request_local_reference(
+ php_user_cache_request_local_clone_context *ctx,
+ zval *dst,
+ zend_reference *src_ref)
+{
+ zend_ulong key = (zend_ulong) (uintptr_t) src_ref;
+ zend_reference *new_ref;
+ zval inner;
+
+ if (ctx->track_identity) {
+ new_ref = zend_hash_index_find_ptr(&ctx->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 (ctx->track_identity) {
+ zend_hash_index_update_ptr(&ctx->references, key, new_ref);
+ }
+
+ if (!user_cache_clone_request_local_value(ctx, &inner, &src_ref->val)) {
+ if (!ctx->track_identity) {
+ zval_ptr_dtor(dst);
+ }
+
+ ZVAL_UNDEF(dst);
+
+ return false;
+ }
+
+ ZVAL_COPY_VALUE(&new_ref->val, &inner);
+
+ if (ctx->track_identity) {
+ GC_ADDREF(new_ref);
+ ZVAL_REF(dst, new_ref);
+ }
+
+ return true;
+}
+
+static bool user_cache_clone_request_local_object_members(
+ php_user_cache_request_local_clone_context *ctx,
+ zend_object *old_obj,
+ zend_object *new_obj)
+{
+ zend_ulong num_key;
+ zend_string *key;
+ zend_property_info *prop_info;
+ zval *src, *dst, *end, *prop, new_prop;
+ uint32_t prop_idx = 0;
+
+ if (old_obj->ce->default_properties_count) {
+ src = old_obj->properties_table;
+ dst = new_obj->properties_table;
+ end = src + old_obj->ce->default_properties_count;
+
+ do {
+ if (!user_cache_clone_request_local_value(ctx, &new_prop, src)) {
+ return false;
+ }
+
+ zval_ptr_dtor(dst);
+
+ ZVAL_COPY_VALUE(dst, &new_prop);
+ Z_PROP_FLAG_P(dst) = Z_PROP_FLAG_P(src);
+
+ if (Z_ISREF_P(dst) && new_obj->ce->properties_info_table != NULL) {
+ prop_info = new_obj->ce->properties_info_table[prop_idx];
+ if (prop_info != NULL &&
+ prop_info != ZEND_WRONG_PROPERTY_INFO &&
+ ZEND_TYPE_IS_SET(prop_info->type)
+ ) {
+ ZEND_REF_ADD_TYPE_SOURCE(Z_REF_P(dst), prop_info);
+ }
+ }
+
+ src++;
+ dst++;
+ prop_idx++;
+ } while (src != end);
+ }
+
+ if (old_obj->properties != NULL && zend_hash_num_elements(old_obj->properties) != 0) {
+ if (new_obj->properties != NULL) {
+ zend_hash_clean(new_obj->properties);
+ zend_hash_extend(new_obj->properties, zend_hash_num_elements(old_obj->properties), 0);
+ } else {
+ new_obj->properties = zend_new_array(zend_hash_num_elements(old_obj->properties));
+ zend_hash_real_init_mixed(new_obj->properties);
+ }
+
+ HT_FLAGS(new_obj->properties) |=
+ HT_FLAGS(old_obj->properties) & HASH_FLAG_HAS_EMPTY_IND
+ ;
+
+ ZEND_HASH_MAP_FOREACH_KEY_VAL(old_obj->properties, num_key, key, prop) {
+ if (Z_TYPE_P(prop) == IS_INDIRECT) {
+ ZVAL_INDIRECT(
+ &new_prop,
+ new_obj->properties_table + (Z_INDIRECT_P(prop) - old_obj->properties_table)
+ );
+ } else if (!user_cache_clone_request_local_value(ctx, &new_prop, prop)) {
+ return false;
+ }
+
+ if (key != NULL) {
+ _zend_hash_append(new_obj->properties, key, &new_prop);
+ } else {
+ zend_hash_index_add_new(new_obj->properties, num_key, &new_prop);
+ }
+ } ZEND_HASH_FOREACH_END();
+ }
+
+ return true;
+}
+
+static bool user_cache_clone_request_local_std_object(
+ php_user_cache_request_local_clone_context *ctx,
+ zend_object *old_obj,
+ zend_object **new_obj_ptr)
+{
+ zend_object *new_obj;
+ zval *src, *dst, *end;
+ void *member_flag = NULL;
+
+ new_obj = zend_objects_new(old_obj->ce);
+
+ if (ctx->track_identity) {
+ zend_hash_index_update_ptr(
+ &ctx->objects,
+ (zend_ulong) (uintptr_t) old_obj,
+ new_obj
+ );
+ }
+
+ if (ctx->clone_verdicts != NULL) {
+ member_flag = zend_hash_index_find_ptr(
+ ctx->clone_verdicts,
+ (zend_ulong) (uintptr_t) old_obj
+ );
+ }
+
+ if (member_flag == PHP_USER_CACHE_REQUEST_LOCAL_NO_DEEP_CLONE &&
+ old_obj->properties == NULL
+ ) {
+ if (old_obj->ce->default_properties_count) {
+ src = old_obj->properties_table;
+ dst = new_obj->properties_table;
+ end = src + old_obj->ce->default_properties_count;
+
+ memcpy(dst, src, sizeof(zval) * old_obj->ce->default_properties_count);
+
+ do {
+ if (Z_REFCOUNTED_P(src)) {
+ Z_ADDREF_P(src);
+ }
+
+ src++;
+ } while (src != end);
+ }
+ } else {
+ if (new_obj->ce->default_properties_count) {
+ dst = new_obj->properties_table;
+ end = dst + new_obj->ce->default_properties_count;
+
+ do {
+ ZVAL_UNDEF(dst);
+
+ dst++;
+ } while (dst != end);
+ }
+
+ if (!user_cache_clone_request_local_object_members(ctx, old_obj, new_obj)) {
+ if (!ctx->track_identity) {
+ OBJ_RELEASE(new_obj);
+ }
+
+ return false;
+ }
+ }
+
+ if (ctx->track_identity) {
+ GC_ADDREF(new_obj);
+ }
+
+ *new_obj_ptr = new_obj;
+
+ return true;
+}
+
+static bool user_cache_clone_request_local_value_callback(
+ void *ctx,
+ zval *dst,
+ zval *src)
+{
+ return user_cache_clone_request_local_value(
+ (php_user_cache_request_local_clone_context *) ctx,
+ dst,
+ src
+ );
+}
+
+static bool user_cache_clone_request_local_safe_direct_object(
+ php_user_cache_request_local_clone_context *ctx,
+ zend_object *old_obj,
+ zend_object **new_obj_ptr)
+{
+ php_user_cache_safe_direct_state_copy_func_t copy_func;
+ zend_ulong key;
+ zend_class_entry *ce;
+ zend_object *new_obj;
+ zval new_zv;
+
+ ce = old_obj->ce;
+
+ copy_func = php_user_cache_safe_direct_state_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_obj = Z_OBJ(new_zv);
+ key = (zend_ulong) (uintptr_t) old_obj;
+
+ if (ctx->track_identity) {
+ zend_hash_index_update_ptr(&ctx->objects, key, new_obj);
+ }
+
+ if (!copy_func(
+ ctx,
+ new_obj,
+ old_obj,
+ user_cache_clone_request_local_value_callback
+ )
+ ) {
+ if (!ctx->track_identity) {
+ OBJ_RELEASE(new_obj);
+ }
+
+ return false;
+ }
+
+ if (!user_cache_clone_request_local_object_members(ctx, old_obj, new_obj)) {
+ if (!ctx->track_identity) {
+ OBJ_RELEASE(new_obj);
+ }
+
+ return false;
+ }
+
+ if (ctx->track_identity) {
+ GC_ADDREF(new_obj);
+ }
+
+ *new_obj_ptr = new_obj;
+
+ return true;
+}
+
+static bool user_cache_clone_request_local_object(
+ php_user_cache_request_local_clone_context *ctx,
+ zend_object *old_obj,
+ zend_object **new_obj_ptr)
+{
+ zend_ulong key;
+ zend_object *new_obj;
+
+ if (old_obj == NULL || zend_object_is_lazy(old_obj)) {
+ return false;
+ }
+
+ if (ctx->track_identity) {
+ key = (zend_ulong) (uintptr_t) old_obj;
+ new_obj = zend_hash_index_find_ptr(&ctx->objects, key);
+
+ if (new_obj != NULL) {
+ GC_ADDREF(new_obj);
+ *new_obj_ptr = new_obj;
+
+ return true;
+ }
+ }
+
+ if (old_obj->handlers == zend_get_std_object_handlers()) {
+ return user_cache_clone_request_local_std_object(ctx, old_obj, new_obj_ptr);
+ }
+
+ return user_cache_clone_request_local_safe_direct_object(ctx, old_obj, new_obj_ptr);
+}
+
+static bool user_cache_clone_request_local_value(
+ php_user_cache_request_local_clone_context *ctx,
+ zval *dst,
+ zval *src)
+{
+ zend_object *obj;
+
+ if (php_user_cache_stack_overflowed()) {
+ ZVAL_UNDEF(dst);
+
+ return false;
+ }
+
+ if (Z_ISREF_P(src)) {
+ return user_cache_clone_request_local_reference(ctx, dst, Z_REF_P(src));
+ }
+
+ switch (Z_TYPE_P(src)) {
+ case IS_ARRAY:
+ return user_cache_clone_request_local_array(ctx, dst, src, false);
+ case IS_OBJECT:
+ if (!user_cache_clone_request_local_object(ctx, Z_OBJ_P(src), &obj)) {
+ return false;
+ }
+
+ ZVAL_OBJ(dst, obj);
+
+ return true;
+ default:
+ ZVAL_COPY(dst, src);
+
+ return true;
+ }
+}
+
+static bool user_cache_clone_request_local_slot_value_known(
+ zval *dst,
+ zval *src,
+ bool needs_deep_clone,
+ HashTable *verdicts,
+ bool no_aliases)
+{
+ php_user_cache_request_local_clone_context ctx;
+ bool result;
+
+ if (!needs_deep_clone) {
+ ZVAL_COPY(dst, src);
+
+ return true;
+ }
+
+ user_cache_request_local_clone_context_init(&ctx, verdicts, !no_aliases);
+
+ if (Z_TYPE_P(src) == IS_ARRAY) {
+ result = user_cache_clone_request_local_array(&ctx, dst, src, true);
+ } else {
+ result = user_cache_clone_request_local_value(&ctx, dst, src);
+ }
+
+ user_cache_request_local_clone_context_destroy(&ctx);
+
+ return result;
+}
+
+static HashTable *user_cache_request_local_slots(void)
+{
+ HashTable **slots_ptr = &UC_G(request_local_slot_table);
+
+ if (*slots_ptr == NULL) {
+ ALLOC_HASHTABLE(*slots_ptr);
+ zend_hash_init(*slots_ptr, 0, NULL, user_cache_request_local_slot_dtor, 0);
+ }
+
+ return *slots_ptr;
+}
+
+static zend_never_inline void user_cache_reacquire_read_lock_or_fail(const char *cache_name)
+{
+ if (!php_user_cache_rlock()) {
+ zend_error_noreturn(E_ERROR, "Unable to reacquire the %s read lock after userland execution", cache_name);
+ }
+}
+
+static bool user_cache_materialize_shared_graph_locked(
+ const php_user_cache_header *header,
+ zend_string *key,
+ const char *cache_name,
+ uint8_t value_type,
+ uint32_t value_offset,
+ uint32_t value_len,
+ bool throw_if_missing,
+ zval *return_value)
+{
+ bool result, ref_registered, lock_safe;
+
+ switch (value_type) {
+ case PHP_USER_CACHE_VALUE_SHARED_GRAPH:
+ ref_registered = php_user_cache_has_request_shared_graph_ref(value_offset);
+ if (!ref_registered) {
+ /* Register before decode can invoke user code and bail out. */
+ php_user_cache_shared_graph_ref_reserve();
+
+ if (!php_user_cache_shared_graph_acquire_ref(value_offset)) {
+ if (throw_if_missing) {
+ PHP_USER_CACHE_TRY_UNLOCK_ON_BAILOUT(
+ zend_throw_exception_ex(php_user_cache_exception_ce, 0, "Stored %s value for key \"%s\" is corrupted", cache_name, ZSTR_VAL(key));
+ );
+ }
+
+ return false;
+ }
+
+ php_user_cache_register_shared_graph_ref(value_offset);
+ }
+
+ lock_safe = php_user_cache_shared_graph_decode_is_lock_safe(value_offset);
+ if (!lock_safe) {
+ php_user_cache_unlock();
+ }
+
+ ZVAL_UNDEF(return_value);
+
+ if (lock_safe) {
+ PHP_USER_CACHE_TRY_UNLOCK_ON_BAILOUT(
+ result = php_user_cache_shared_graph_decode(
+ user_cache_ptr_in_header(header, value_offset),
+ value_len,
+ return_value
+ );
+ );
+ } else {
+ result = php_user_cache_shared_graph_decode(
+ user_cache_ptr_in_header(header, value_offset),
+ value_len,
+ return_value
+ );
+ }
+
+ if (!lock_safe && !result && Z_TYPE_P(return_value) != IS_UNDEF) {
+ zval_ptr_dtor(return_value);
+ ZVAL_UNDEF(return_value);
+ }
+
+ if (!lock_safe) {
+ 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);
+ }
+
+ /* Request cleanup releases the registered reference. */
+ if (!EG(exception) && throw_if_missing) {
+ PHP_USER_CACHE_TRY_UNLOCK_ON_BAILOUT(
+ zend_throw_exception_ex(php_user_cache_exception_ce, 0, "Stored %s value for key \"%s\" is corrupted", cache_name, ZSTR_VAL(key));
+ );
+ }
+
+ return false;
+ }
+
+ return true;
+ default:
+ return false;
+ }
+}
+
+static php_user_cache_request_local_slot *user_cache_find_request_local_slot(
+ zend_string *key,
+ uint64_t gen,
+ bool evict_stale)
+{
+ php_user_cache_request_local_slot *slot;
+ HashTable **slots_ptr = &UC_G(request_local_slot_table);
+
+ if (*slots_ptr == NULL) {
+ return NULL;
+ }
+
+ slot = zend_hash_find_ptr(*slots_ptr, key);
+ if (slot == NULL) {
+ return NULL;
+ }
+
+ if (slot->generation != gen ||
+ slot->context != (const void *) php_user_cache_active_context()
+ ) {
+ if (evict_stale) {
+ zend_hash_del(*slots_ptr, key);
+ }
+
+ return NULL;
+ }
+
+ return slot;
+}
+
+static php_user_cache_request_local_slot_result user_cache_fetch_request_local_slot(
+ zend_string *key,
+ uint64_t gen,
+ zval *return_value)
+{
+ php_user_cache_request_local_slot *slot = user_cache_find_request_local_slot(key, gen, true);
+
+ if (slot == NULL || !slot->has_value) {
+ return PHP_USER_CACHE_REQUEST_LOCAL_SLOT_MISS;
+ }
+
+ if (!user_cache_clone_request_local_slot_value_known(
+ return_value,
+ &slot->value,
+ slot->needs_deep_clone,
+ slot->has_clone_verdicts ? &slot->clone_verdicts : NULL,
+ slot->no_aliases
+ )
+ ) {
+ zend_hash_del(UC_G(request_local_slot_table), key);
+
+ return PHP_USER_CACHE_REQUEST_LOCAL_SLOT_MISS;
+ }
+
+ return PHP_USER_CACHE_REQUEST_LOCAL_SLOT_HIT;
+}
+
+static void user_cache_mark_request_local_slot(zend_string *key, uint64_t gen)
+{
+ php_user_cache_request_local_slot *slot = user_cache_alloc_request_local_slot(gen, true, false);
+
+ zend_hash_update_ptr(user_cache_request_local_slots(), key, slot);
+}
+
+static void user_cache_throw_key_not_found_guarded(zend_string *key)
+{
+ PHP_USER_CACHE_TRY_UNLOCK_ON_BAILOUT(
+ zend_throw_exception_ex(php_user_cache_exception_ce, 0, "Cache key \"%s\" was not found", ZSTR_VAL(key));
+ );
+}
+
+static void user_cache_throw_unknown_value_type_guarded(zend_string *key, const char *cache_name)
+{
+ PHP_USER_CACHE_TRY_UNLOCK_ON_BAILOUT(
+ zend_throw_exception_ex(php_user_cache_exception_ce, 0, "Stored %s value for key \"%s\" has an unknown type", cache_name, ZSTR_VAL(key));
+ );
+}
+
+static bool user_cache_find_slot_in_header_locked(
+ php_user_cache_header *header,
+ zend_string *key,
+ zend_ulong hash,
+ php_user_cache_find_slot_expiry_mode expiry_mode,
+ uint32_t *slot_idx,
+ bool *found)
+{
+ php_user_cache_entry *entries, *entry;
+ uint64_t now = 0;
+ uint32_t i, first_tombstone = UINT32_MAX, step;
+
+ if (header == NULL) {
+ return false;
+ }
+
+ entries = php_user_cache_entries_ptr(header);
+ i = (uint32_t) (hash % header->capacity);
+
+ for (step = 0; step < header->capacity; step++) {
+ entry = &entries[i];
+
+ if (entry->state == PHP_USER_CACHE_ENTRY_EMPTY) {
+ *slot_idx = first_tombstone != UINT32_MAX ? first_tombstone : i;
+ *found = false;
+
+ return true;
+ }
+
+ if (entry->state == PHP_USER_CACHE_ENTRY_TOMBSTONE) {
+ if (first_tombstone == UINT32_MAX) {
+ first_tombstone = i;
+ }
+ } else if (expiry_mode != PHP_USER_CACHE_FIND_SLOT_IGNORE_EXPIRY &&
+ user_cache_is_expired_now(entry, &now)
+ ) {
+ if (expiry_mode == PHP_USER_CACHE_FIND_SLOT_DELETE_EXPIRED) {
+ user_cache_delete_entry_locked(header, entry);
+ } else {
+ user_cache_note_expired_read();
+ }
+
+ if (first_tombstone == UINT32_MAX) {
+ first_tombstone = i;
+ }
+ } else if (user_cache_key_equals(header, entry, key, hash)) {
+ *slot_idx = i;
+ *found = true;
+
+ return true;
+ }
+
+ ++i;
+
+ if (i == header->capacity) {
+ i = 0;
+ }
+ }
+
+ if (first_tombstone != UINT32_MAX) {
+ *slot_idx = first_tombstone;
+ *found = false;
+
+ return true;
+ }
+
+ return false;
+}
+
+static bool user_cache_find_slot_locked(
+ zend_string *key,
+ zend_ulong hash,
+ php_user_cache_find_slot_expiry_mode expiry_mode,
+ php_user_cache_header **header_ptr,
+ uint32_t *slot_idx,
+ bool *found)
+{
+ php_user_cache_header *header = php_user_cache_header_ptr();
+
+ if (!header || !php_user_cache_header_init_locked()) {
+ return false;
+ }
+
+ if (header_ptr != NULL) {
+ *header_ptr = header;
+ }
+
+ return user_cache_find_slot_in_header_locked(
+ header,
+ key,
+ hash,
+ expiry_mode,
+ slot_idx,
+ found
+ );
+}
+
+static bool user_cache_find_slot_ignore_expiry_locked(
+ zend_string *key,
+ zend_ulong hash,
+ php_user_cache_header **header_ptr,
+ uint32_t *slot_idx,
+ bool *found)
+{
+ return user_cache_find_slot_locked(
+ key,
+ hash,
+ PHP_USER_CACHE_FIND_SLOT_IGNORE_EXPIRY,
+ header_ptr,
+ slot_idx,
+ found
+ );
+}
+
+static bool user_cache_find_slot_for_read_locked(
+ zend_string *key,
+ zend_ulong hash,
+ php_user_cache_header **header_ptr,
+ uint32_t *slot_idx,
+ bool *found)
+{
+ return user_cache_find_slot_locked(
+ key,
+ hash,
+ PHP_USER_CACHE_FIND_SLOT_SKIP_EXPIRED,
+ header_ptr,
+ slot_idx,
+ found
+ );
+}
+
+static bool user_cache_find_slot_for_write_locked(
+ zend_string *key,
+ zend_ulong hash,
+ php_user_cache_header **header_ptr,
+ uint32_t *slot_idx,
+ bool *found)
+{
+ php_user_cache_header *header;
+
+ if (!user_cache_find_slot_locked(
+ key,
+ hash,
+ PHP_USER_CACHE_FIND_SLOT_DELETE_EXPIRED,
+ header_ptr,
+ slot_idx,
+ found
+ )
+ ) {
+ return false;
+ }
+
+ header = *header_ptr;
+ if (!*found && header->count >= header->capacity - header->capacity / 8) {
+ return false;
+ }
+
+ return true;
+}
+
+static void user_cache_rehash_locked(php_user_cache_header *header)
+{
+ php_user_cache_entry *entries, *snapshot, *entry, *target;
+ uint32_t i, slot, step;
+
+ entries = php_user_cache_entries_ptr(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 (i = 0; i < header->capacity; i++) {
+ entry = &snapshot[i];
+
+ if (entry->state != PHP_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 == PHP_USER_CACHE_ENTRY_EMPTY) {
+ *target = *entry;
+ header->count++;
+
+ break;
+ }
+
+ ++slot;
+
+ if (slot == header->capacity) {
+ slot = 0;
+ }
+ }
+ }
+
+ efree(snapshot);
+
+ php_user_cache_bump_mutation_epoch_locked(header);
+}
+
+static bool user_cache_expunge_expired_locked(void)
+{
+ php_user_cache_header *header = php_user_cache_header_ptr();
+ php_user_cache_entry *entries;
+ uint64_t now;
+ uint32_t i;
+ bool removed = false;
+
+ if (!header || !php_user_cache_header_init_locked()) {
+ return false;
+ }
+
+ now = (uint64_t) time(NULL);
+ entries = php_user_cache_entries_ptr(header);
+ for (i = 0; i < header->capacity; i++) {
+ if (user_cache_is_expired(&entries[i], now)) {
+ user_cache_delete_entry_locked(header, &entries[i]);
+ removed = true;
+ }
+ }
+
+ php_user_cache_shared_graph_reclaim_orphaned_locked();
+
+ return removed;
+}
+
+/* Bound each expiry scan and resume from a process-local cursor. */
+static bool user_cache_expunge_expired_bounded_locked(void)
+{
+ php_user_cache_header *header = php_user_cache_header_ptr();
+ php_user_cache_entry *entries;
+ uint64_t now;
+ uint32_t cursor, scan_len, i;
+ bool removed = false;
+
+ if (!header || !php_user_cache_header_init_locked()) {
+ return false;
+ }
+
+ now = (uint64_t) time(NULL);
+ entries = php_user_cache_entries_ptr(header);
+
+ cursor = UC_G(expired_expunge_cursor);
+ if (cursor >= header->capacity) {
+ cursor = 0;
+ }
+
+ scan_len = header->capacity < PHP_USER_CACHE_EXPUNGE_SCAN_MAX
+ ? header->capacity
+ : PHP_USER_CACHE_EXPUNGE_SCAN_MAX
+ ;
+ for (i = 0; i < scan_len; i++) {
+ if (user_cache_is_expired(&entries[cursor], now)) {
+ user_cache_delete_entry_locked(header, &entries[cursor]);
+ removed = true;
+ }
+
+ ++cursor;
+
+ if (cursor == header->capacity) {
+ cursor = 0;
+ }
+ }
+
+ UC_G(expired_expunge_cursor) = cursor;
+
+ php_user_cache_shared_graph_reclaim_orphaned_locked();
+
+ return removed;
+}
+
+static void user_cache_handle_store_failure(const char *msg, bool throw_on_failure, bool honor_strict_store_failure)
+{
+ php_user_cache_context *ctx = php_user_cache_active_context();
+
+ if (honor_strict_store_failure && ctx->strict_store_failure && !throw_on_failure) {
+ zend_throw_exception_ex(php_user_cache_exception_ce, 0, "%s", msg);
+
+ return;
+ }
+
+ if (throw_on_failure) {
+ zend_throw_exception_ex(php_user_cache_exception_ce, 0, "%s", msg);
+ }
+}
+
+static bool user_cache_safe_direct_value_has_unstorable(
+ void *ctx_ptr,
+ const zval *value)
+{
+ php_user_cache_unstorable_context *ctx = ctx_ptr;
+
+ return user_cache_find_unstorable_value(
+ (zval *) value,
+ ctx->seen_arrays,
+ ctx->seen_objects,
+ ctx->failure_message
+ );
+}
+
+static php_user_cache_object_storability user_cache_object_class_storability(
+ zend_class_entry *ce)
+{
+ if (php_user_cache_class_uses_magic_serialize(ce) ||
+ php_user_cache_class_uses_serialize_props(ce)
+ ) {
+ return PHP_USER_CACHE_OBJECT_STORABLE_VIA_HOOKS;
+ }
+
+ if (php_user_cache_class_uses_magic_unserialize(ce)) {
+ return PHP_USER_CACHE_OBJECT_STORABLE_VIA_HOOKS;
+ }
+
+ if (php_user_cache_class_uses_serdes(ce)) {
+ return PHP_USER_CACHE_OBJECT_SERDES;
+ }
+
+ if ((ce->ce_flags & ZEND_ACC_ENUM) == 0 &&
+ php_user_cache_safe_direct_state_serialize_func(ce) == NULL &&
+ (
+ (ce->ce_flags & ZEND_ACC_NOT_SERIALIZABLE) != 0 ||
+ (ce->type != ZEND_USER_CLASS && ce->create_object != NULL)
+ )
+ ) {
+ return PHP_USER_CACHE_OBJECT_OPAQUE;
+ }
+
+ return PHP_USER_CACHE_OBJECT_SCAN_MEMBERS;
+}
+
+static bool user_cache_find_unstorable_in_array(
+ zval *value,
+ HashTable *seen_arrs,
+ HashTable *seen_objs,
+ const char **msg)
+{
+ zval *elem;
+
+ if (!php_user_cache_seen_test_and_add(seen_arrs, Z_ARR_P(value))) {
+ return false;
+ }
+
+ ZEND_HASH_FOREACH_VAL(Z_ARRVAL_P(value), elem) {
+ if (user_cache_find_unstorable_value(elem, seen_arrs, seen_objs, msg)) {
+ return true;
+ }
+ } ZEND_HASH_FOREACH_END();
+
+ return false;
+}
+
+static bool user_cache_find_unstorable_in_object(
+ zval *value,
+ HashTable *seen_arrs,
+ HashTable *seen_objs,
+ const char **msg)
+{
+ php_user_cache_safe_direct_state_has_unstorable_func_t state_has_unstorable;
+ php_user_cache_unstorable_context uctx;
+ php_user_cache_object_storability storability;
+ zend_object *obj = Z_OBJ_P(value);
+ zval *elem, *prop, *end;
+
+ if (zend_object_is_lazy(obj)) {
+ *msg = PHP_USER_CACHE_MSG_LAZY_OBJECT_UNSTORABLE;
+
+ return true;
+ }
+
+ if (!php_user_cache_seen_test_and_add(seen_objs, obj)) {
+ return false;
+ }
+
+ storability = user_cache_object_class_storability(obj->ce);
+ if (storability == PHP_USER_CACHE_OBJECT_STORABLE_VIA_HOOKS) {
+ return false;
+ }
+
+ state_has_unstorable = php_user_cache_safe_direct_state_has_unstorable_func(obj->ce);
+ if (state_has_unstorable != NULL) {
+ uctx.seen_arrays = seen_arrs;
+ uctx.seen_objects = seen_objs;
+ uctx.failure_message = msg;
+
+ if (state_has_unstorable(
+ &uctx,
+ value,
+ user_cache_safe_direct_value_has_unstorable)
+ ) {
+ if (*msg == NULL) {
+ *msg = "objects of this class contain values that cannot be stored in the user cache";
+ }
+
+ return true;
+ }
+ }
+
+ if (storability == PHP_USER_CACHE_OBJECT_SERDES) {
+ return false;
+ }
+
+ if (storability == PHP_USER_CACHE_OBJECT_OPAQUE) {
+ *msg = PHP_USER_CACHE_MSG_OPAQUE_OBJECT_UNSTORABLE;
+
+ return true;
+ }
+
+ if (obj->ce->default_properties_count != 0) {
+ prop = obj->properties_table;
+ end = prop + obj->ce->default_properties_count;
+
+ do {
+ if (Z_TYPE_P(prop) != IS_UNDEF &&
+ user_cache_find_unstorable_value(
+ prop, seen_arrs, seen_objs, msg
+ )
+ ) {
+ return true;
+ }
+
+ prop++;
+ } while (prop != end);
+ }
+
+ if (obj->properties != NULL) {
+ ZEND_HASH_FOREACH_VAL(obj->properties, elem) {
+ if (Z_TYPE_P(elem) == IS_INDIRECT) {
+ elem = Z_INDIRECT_P(elem);
+ if (Z_TYPE_P(elem) == IS_UNDEF) {
+ continue;
+ }
+ }
+
+ if (user_cache_find_unstorable_value(elem, seen_arrs, seen_objs, msg)) {
+ return true;
+ }
+ } ZEND_HASH_FOREACH_END();
+ }
+
+ return false;
+}
+
+static bool user_cache_find_unstorable_value(
+ zval *value,
+ HashTable *seen_arrs,
+ HashTable *seen_objs,
+ const char **msg)
+{
+ if (php_user_cache_stack_overflowed()) {
+ *msg = "value is nested too deeply to be stored in the user cache";
+
+ return true;
+ }
+
+ ZVAL_DEREF(value);
+
+ if (Z_TYPE_P(value) == IS_RESOURCE) {
+ *msg = PHP_USER_CACHE_MSG_RESOURCE_UNSTORABLE;
+
+ return true;
+ }
+
+ if (Z_TYPE_P(value) == IS_OBJECT && Z_OBJCE_P(value) == zend_ce_closure) {
+ *msg = PHP_USER_CACHE_MSG_CLOSURE_UNSTORABLE;
+
+ return true;
+ }
+
+ if (Z_TYPE_P(value) == IS_ARRAY) {
+ return user_cache_find_unstorable_in_array(value, seen_arrs, seen_objs, msg);
+ }
+
+ if (Z_TYPE_P(value) == IS_OBJECT) {
+ return user_cache_find_unstorable_in_object(value, seen_arrs, seen_objs, msg);
+ }
+
+ return false;
+}
+
+static bool user_cache_validate_storable_value(zval *value, bool throw_on_failure, bool honor_strict_store_failure)
+{
+ const char *msg = NULL;
+ HashTable seen_arrs, seen_objs;
+ bool found;
+
+ ZVAL_DEREF(value);
+
+ if (Z_TYPE_P(value) != IS_ARRAY && Z_TYPE_P(value) != IS_OBJECT) {
+ return true;
+ }
+
+ zend_hash_init(&seen_arrs, 8, NULL, NULL, 0);
+ zend_hash_init(&seen_objs, 8, NULL, NULL, 0);
+
+ found = user_cache_find_unstorable_value(value, &seen_arrs, &seen_objs, &msg);
+
+ zend_hash_destroy(&seen_objs);
+ zend_hash_destroy(&seen_arrs);
+
+ if (EG(exception)) {
+ return false;
+ }
+
+ if (!found) {
+ return true;
+ }
+
+ if (msg != NULL) {
+ zend_type_error("%s", msg);
+ }
+
+ return false;
+}
+
+static uint8_t *user_cache_reserve_combined_value_key_locked(
+ const php_user_cache_header *header,
+ 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 &&
+ php_user_cache_block_payload_capacity(reusable_offset) >= total_size
+ ) {
+ base_offset = reusable_offset;
+ } else {
+ base_offset = php_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 user_cache_ptr_in_header(header, base_offset);
+}
+
+static bool user_cache_publish_combined_value_key_locked(
+ const php_user_cache_header *header,
+ uint32_t reusable_offset,
+ zend_string *key,
+ size_t payload_size,
+ const void *src,
+ uint32_t *value_offset,
+ uint32_t *key_offset)
+{
+ uint8_t *payload;
+
+ if (src == NULL) {
+ return false;
+ }
+
+ payload = user_cache_reserve_combined_value_key_locked(
+ header,
+ reusable_offset,
+ key,
+ payload_size,
+ value_offset,
+ key_offset
+ );
+ if (payload == NULL) {
+ return false;
+ }
+
+ memcpy(payload, src, payload_size);
+ memcpy(payload + payload_size, ZSTR_VAL(key), ZSTR_LEN(key) + 1);
+
+ return true;
+}
+
+/* String roots are copied in place; other graphs normally use the staging
+ * buffer prepared before taking the write lock. */
+static bool user_cache_publish_prepared_shared_graph_locked(
+ const php_user_cache_prepared_value *prepared,
+ zval *value,
+ uint8_t *payload)
+{
+ zval pinned_root;
+
+ if (prepared->payload_source != NULL &&
+ php_user_cache_shared_graph_publish_copied_payload_locked(
+ payload,
+ prepared->payload_size,
+ prepared->payload_source,
+ prepared->payload_size,
+ prepared->payload_used_size,
+ prepared->has_verbatim_array,
+ prepared->fixup_offsets,
+ prepared->fixup_count
+ )
+ ) {
+ return true;
+ }
+
+ /* Use the string pinned during the sizing pass. */
+ if (prepared->owned_string != NULL) {
+ ZVAL_STR(&pinned_root, prepared->owned_string);
+ value = &pinned_root;
+ }
+
+ return php_user_cache_build_shared_graph_in_place(
+ value,
+ prepared->state_memo,
+ NULL,
+ payload,
+ prepared->payload_size,
+ NULL,
+ NULL,
+ NULL,
+ NULL
+ );
+}
+
+static bool user_cache_reclaim_space_for_store_locked(
+ bool can_clear,
+ bool *expired_retry_used,
+ bool *clear_retry_used)
+{
+ if (!*expired_retry_used) {
+ *expired_retry_used = true;
+
+ if (user_cache_expunge_expired_locked()) {
+ return true;
+ }
+ }
+
+ if (can_clear && php_user_cache_active_context()->clear_on_pressure && !*clear_retry_used) {
+ *clear_retry_used = true;
+
+ if (php_user_cache_entry_locks_allow_clear_locked() && php_user_cache_clear_locked()) {
+ php_user_cache_header_ptr()->expunge_count++;
+
+ return true;
+ }
+ }
+
+ return false;
+}
+
+static void user_cache_rollback_partial_store_allocs_locked(
+ uint32_t new_value_offset,
+ uint32_t old_value_offset,
+ uint32_t new_key_offset,
+ uint32_t old_key_offset,
+ uint16_t new_reserved)
+{
+ if (new_value_offset != 0 && new_value_offset != old_value_offset) {
+ php_user_cache_free_locked(new_value_offset);
+ }
+
+ if (new_key_offset != 0 && new_key_offset != old_key_offset &&
+ (new_reserved & PHP_USER_CACHE_ENTRY_RESERVED_COMBINED_VALUE_KEY) == 0
+ ) {
+ php_user_cache_free_locked(new_key_offset);
+ }
+}
+
+static php_user_cache_store_attempt_result user_cache_store_attempt_locked(
+ zend_string *key,
+ zval *value,
+ const php_user_cache_prepared_value *prepared,
+ zend_long ttl,
+ const php_user_cache_store_options *options,
+ php_user_cache_store_result *result,
+ size_t key_size,
+ bool *expired_retry_used,
+ bool *clear_retry_used)
+{
+ const char *msg;
+ php_user_cache_header *header;
+ php_user_cache_entry *entries, *entry, replaced_snapshot;
+ zend_long new_lval = 0;
+ uint64_t expires_at;
+ uint32_t slot_idx, 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_reuse_offset = 0;
+ uint16_t old_reserved = 0, new_reserved = 0;
+ uint8_t *combined_payload, old_value_type = PHP_USER_CACHE_VALUE_NULL, new_value_type = prepared->value_type;
+ bool found, old_combined, use_combined_publish, can_clear, capture_replaced = false, published = false;
+ double new_dval = 0;
+
+ expires_at = ttl == 0
+ ? 0
+ : (uint64_t) time(NULL) + (uint64_t) ttl
+ ;
+
+ if (!user_cache_find_slot_for_write_locked(
+ key,
+ prepared->hash,
+ &header,
+ &slot_idx,
+ &found
+ )
+ ) {
+ if (options->retry_after_memory_pressure &&
+ user_cache_reclaim_space_for_store_locked(
+ true,
+ expired_retry_used,
+ clear_retry_used
+ )
+ ) {
+ return PHP_USER_CACHE_STORE_ATTEMPT_RETRY;
+ }
+
+ php_user_cache_header_ptr()->store_failure_count++;
+ user_cache_handle_store_failure(
+ "cache hash table is full",
+ options->throw_on_failure,
+ options->honor_strict_store_failure
+ );
+
+ return PHP_USER_CACHE_STORE_ATTEMPT_FAILED;
+ }
+
+ entries = php_user_cache_entries_ptr(header);
+ entry = &entries[slot_idx];
+
+ if (found) {
+ capture_replaced = options->capture_replaced_entry && result != NULL;
+ replaced_snapshot = *entry;
+ 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 & PHP_USER_CACHE_ENTRY_RESERVED_COMBINED_VALUE_KEY) != 0;
+ reusable_offset = 0;
+
+ if (found && !capture_replaced && old_value_type != PHP_USER_CACHE_VALUE_SHARED_GRAPH && !old_combined) {
+ reusable_offset = old_value_offset;
+ }
+
+ new_key_offset = found ? old_key_offset : 0;
+
+ use_combined_publish = user_cache_value_uses_offset(prepared->value_type) &&
+ (!found || old_combined)
+ ;
+
+ if (old_combined && old_value_offset != 0 && !capture_replaced) {
+ if (old_value_type == PHP_USER_CACHE_VALUE_SHARED_GRAPH) {
+ /* The sizing pass includes worst-case alignment padding. */
+ if ((prepared->payload_source != NULL ||
+ prepared->value_type == PHP_USER_CACHE_VALUE_SHARED_GRAPH) &&
+ php_user_cache_shared_graph_can_overwrite_payload_locked(old_value_offset) &&
+ (prepared->value_type != PHP_USER_CACHE_VALUE_SHARED_GRAPH ||
+ prepared->payload_source == NULL ||
+ php_user_cache_shared_graph_copy_fits_buffer(
+ user_cache_ptr_in_header(header, old_value_offset),
+ prepared->payload_size,
+ prepared->payload_source,
+ prepared->payload_size,
+ prepared->payload_used_size
+ )
+ )
+ ) {
+ combined_reuse_offset = old_value_offset;
+ }
+ } else if (header->count == 1) {
+ combined_reuse_offset = old_value_offset;
+ }
+ }
+
+ if (!use_combined_publish && (!found || old_combined)) {
+ new_key_offset = php_user_cache_alloc_locked(key_size, ZSTR_VAL(key));
+ if (new_key_offset == 0) {
+ msg = "not enough shared memory left";
+ can_clear = user_cache_payload_can_fit_locked(key_size);
+
+ goto failure;
+ }
+ }
+
+ switch (prepared->value_type) {
+ case PHP_USER_CACHE_VALUE_NULL:
+ new_value_type = PHP_USER_CACHE_VALUE_NULL;
+
+ break;
+ case PHP_USER_CACHE_VALUE_TRUE:
+ new_value_type = PHP_USER_CACHE_VALUE_TRUE;
+
+ break;
+ case PHP_USER_CACHE_VALUE_FALSE:
+ new_value_type = PHP_USER_CACHE_VALUE_FALSE;
+
+ break;
+ case PHP_USER_CACHE_VALUE_LONG:
+ new_value_type = PHP_USER_CACHE_VALUE_LONG;
+ new_lval = prepared->long_value;
+
+ break;
+ case PHP_USER_CACHE_VALUE_DOUBLE:
+ new_value_type = PHP_USER_CACHE_VALUE_DOUBLE;
+ new_dval = prepared->double_value;
+
+ break;
+ case PHP_USER_CACHE_VALUE_STRING:
+ if (use_combined_publish) {
+ if (!user_cache_publish_combined_value_key_locked(
+ header,
+ combined_reuse_offset,
+ key,
+ prepared->payload_size,
+ prepared->payload_source,
+ &new_value_offset,
+ &new_key_offset)
+ ) {
+ msg = "not enough shared memory left";
+ can_clear = user_cache_payload_can_fit_locked(prepared->payload_size + key_size);
+
+ goto failure;
+ }
+
+ new_reserved = PHP_USER_CACHE_ENTRY_RESERVED_COMBINED_VALUE_KEY;
+ } else {
+ offset = user_cache_write_payload_locked(
+ header,
+ reusable_offset,
+ prepared->payload_size,
+ prepared->payload_source
+ );
+ if (offset == 0) {
+ msg = "not enough shared memory left";
+ can_clear = user_cache_payload_can_fit_locked(prepared->payload_size);
+
+ goto failure;
+ }
+
+ new_value_offset = offset;
+ }
+
+ new_value_type = prepared->value_type;
+ new_value_len = prepared->value_len;
+
+ break;
+ case PHP_USER_CACHE_VALUE_SHARED_GRAPH:
+ if (use_combined_publish) {
+ combined_payload = user_cache_reserve_combined_value_key_locked(
+ header,
+ combined_reuse_offset,
+ key,
+ prepared->payload_size,
+ &new_value_offset,
+ &new_key_offset
+ );
+
+ graph_offset = new_value_offset;
+
+ if (combined_payload != NULL) {
+ /* A reused block remains attached so a later read can discard it. */
+ zend_try {
+ published = user_cache_publish_prepared_shared_graph_locked(
+ prepared,
+ value,
+ combined_payload
+ );
+ } zend_catch {
+ if (graph_offset != combined_reuse_offset) {
+ php_user_cache_free_locked(graph_offset);
+ }
+ php_user_cache_unlock_if_held();
+ zend_bailout();
+ } zend_end_try();
+
+ if (published) {
+ memcpy(combined_payload + prepared->payload_size, ZSTR_VAL(key), key_size);
+ new_reserved = PHP_USER_CACHE_ENTRY_RESERVED_COMBINED_VALUE_KEY;
+ new_value_type = PHP_USER_CACHE_VALUE_SHARED_GRAPH;
+ new_value_offset = graph_offset;
+ new_value_len = prepared->value_len;
+
+ break;
+ }
+
+ if (graph_offset != combined_reuse_offset) {
+ php_user_cache_free_locked(graph_offset);
+ } else {
+ /* Detach the partially consumed combined block first. */
+ entry->value_type = PHP_USER_CACHE_VALUE_NULL;
+ entry->value_offset = 0;
+
+ user_cache_delete_entry_locked(header, entry);
+ php_user_cache_free_locked(combined_reuse_offset);
+
+ combined_reuse_offset = 0;
+ old_value_offset = 0;
+ old_key_offset = 0;
+ found = false;
+ }
+
+ graph_offset = 0;
+ new_value_offset = 0;
+ new_key_offset = found ? old_key_offset : 0;
+
+ if (EG(exception)) {
+ return PHP_USER_CACHE_STORE_ATTEMPT_FAILED;
+ }
+ } else if (options->retry_after_memory_pressure &&
+ user_cache_payload_can_fit_locked(prepared->payload_size + key_size) &&
+ user_cache_reclaim_space_for_store_locked(true, expired_retry_used, clear_retry_used)
+ ) {
+ return PHP_USER_CACHE_STORE_ATTEMPT_RETRY;
+ }
+ } else {
+ graph_offset = php_user_cache_alloc_locked(prepared->payload_size, NULL);
+
+ if (graph_offset != 0) {
+ /* Avoid leaking the block on an allocation bailout. */
+ zend_try {
+ published = user_cache_publish_prepared_shared_graph_locked(
+ prepared,
+ value,
+ user_cache_ptr_in_header(header, graph_offset)
+ );
+ } zend_catch {
+ php_user_cache_free_locked(graph_offset);
+ php_user_cache_unlock_if_held();
+ zend_bailout();
+ } zend_end_try();
+
+ if (published) {
+ new_value_type = PHP_USER_CACHE_VALUE_SHARED_GRAPH;
+ new_value_offset = graph_offset;
+ new_value_len = prepared->value_len;
+
+ break;
+ }
+
+ php_user_cache_free_locked(graph_offset);
+
+ if (EG(exception)) {
+ return PHP_USER_CACHE_STORE_ATTEMPT_FAILED;
+ }
+ } else if (options->retry_after_memory_pressure &&
+ user_cache_payload_can_fit_locked(prepared->payload_size) &&
+ user_cache_reclaim_space_for_store_locked(true, expired_retry_used, clear_retry_used)
+ ) {
+ return PHP_USER_CACHE_STORE_ATTEMPT_RETRY;
+ }
+ }
+
+ msg = "not enough shared memory left";
+ can_clear = user_cache_payload_can_fit_locked(prepared->payload_size);
+
+ goto failure;
+ default:
+ ZEND_UNREACHABLE();
+ }
+
+ if (!found && entry->state == PHP_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 = PHP_USER_CACHE_ENTRY_USED;
+ entry->value_type = new_value_type;
+ entry->reserved = new_reserved;
+ entry->long_value = new_lval;
+ entry->double_value = new_dval;
+
+ if (capture_replaced) {
+ result->replaced_entry.found = true;
+ result->replaced_entry.entry = replaced_snapshot;
+ } else {
+ if (found &&
+ old_key_offset != 0 &&
+ old_key_offset != new_key_offset &&
+ !old_combined
+ ) {
+ php_user_cache_free_locked(old_key_offset);
+ }
+
+ if (found &&
+ old_value_offset != 0 &&
+ old_value_offset != new_value_offset
+ ) {
+ user_cache_release_value_storage_locked(old_value_type, old_value_offset);
+ }
+ }
+
+ if (!found) {
+ header->count++;
+ }
+
+ php_user_cache_bump_mutation_epoch_locked(header);
+
+ /* Per-entry generations avoid invalidating unrelated request-local slots. */
+ entry->generation = header->mutation_epoch;
+
+ if (result != NULL) {
+ result->stored_generation = entry->generation;
+ result->should_seed_request_local_slot = user_cache_prepared_value_should_seed_request_local_slot(prepared);
+ }
+
+ return PHP_USER_CACHE_STORE_ATTEMPT_STORED;
+
+failure:
+ user_cache_rollback_partial_store_allocs_locked(
+ new_value_offset,
+ old_value_offset,
+ new_key_offset,
+ old_key_offset,
+ new_reserved
+ );
+
+ if (options->retry_after_memory_pressure &&
+ user_cache_reclaim_space_for_store_locked(can_clear, expired_retry_used, clear_retry_used)
+ ) {
+ return PHP_USER_CACHE_STORE_ATTEMPT_RETRY;
+ }
+
+ header->store_failure_count++;
+ user_cache_handle_store_failure(
+ msg,
+ options->throw_on_failure,
+ options->honor_strict_store_failure
+ );
+
+ return PHP_USER_CACHE_STORE_ATTEMPT_FAILED;
+}
+
+static bool user_cache_store_prepared_locked_impl(
+ zend_string *key,
+ zval *value,
+ const php_user_cache_prepared_value *prepared,
+ zend_long ttl,
+ const php_user_cache_store_options *options,
+ php_user_cache_store_result *result)
+{
+ php_user_cache_store_attempt_result attempt_result;
+ size_t key_size;
+ bool expired_retry_used = false, clear_retry_used = false;
+
+ ZVAL_DEREF(value);
+
+ if (result != NULL) {
+ result->stored_generation = 0;
+ result->should_seed_request_local_slot = false;
+ result->replaced_entry.found = false;
+
+ memset(&result->replaced_entry.entry, 0, sizeof(result->replaced_entry.entry));
+ }
+
+ if (prepared == NULL || options == NULL) {
+ return false;
+ }
+
+ key_size = ZSTR_LEN(key) + 1;
+
+ user_cache_maybe_rehash_locked();
+
+ for (;;) {
+ attempt_result = user_cache_store_attempt_locked(
+ key,
+ value,
+ prepared,
+ ttl,
+ options,
+ result,
+ key_size,
+ &expired_retry_used,
+ &clear_retry_used
+ );
+
+ if (attempt_result == PHP_USER_CACHE_STORE_ATTEMPT_RETRY) {
+ continue;
+ }
+
+ return attempt_result == PHP_USER_CACHE_STORE_ATTEMPT_STORED;
+ }
+}
+
+static bool user_cache_prepare_direct_value(
+ zval *value,
+ php_user_cache_prepared_value *prepared)
+{
+ switch (Z_TYPE_P(value)) {
+ case IS_NULL:
+ prepared->value_type = PHP_USER_CACHE_VALUE_NULL;
+
+ return true;
+ case IS_TRUE:
+ prepared->value_type = PHP_USER_CACHE_VALUE_TRUE;
+
+ return true;
+ case IS_FALSE:
+ prepared->value_type = PHP_USER_CACHE_VALUE_FALSE;
+
+ return true;
+ case IS_LONG:
+ prepared->value_type = PHP_USER_CACHE_VALUE_LONG;
+ prepared->long_value = Z_LVAL_P(value);
+
+ return true;
+ case IS_DOUBLE:
+ prepared->value_type = PHP_USER_CACHE_VALUE_DOUBLE;
+ prepared->double_value = Z_DVAL_P(value);
+
+ return true;
+ case IS_STRING:
+ if (Z_STRLEN_P(value) < PHP_USER_CACHE_DIRECT_STRING_MIN_LEN) {
+ prepared->value_type = PHP_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 uint8_t *) Z_STRVAL_P(value);
+
+ return true;
+ }
+
+ return false;
+ default:
+ return false;
+ }
+}
+
+static bool user_cache_prepare_shared_graph_value(
+ zval *value,
+ const php_user_cache_prepare_options *options,
+ HashTable *verbatim_verdicts,
+ size_t verbatim_graph_len,
+ php_user_cache_prepared_value *prepared)
+{
+ size_t graph_len = 0;
+ bool sized, build_at_publish;
+
+ /* Copy string roots directly under the write lock; this path cannot invoke
+ * user code. */
+ build_at_publish = options->caller_holds_write_lock || Z_TYPE_P(value) == IS_STRING;
+
+ if (!build_at_publish && prepared->state_memo == NULL) {
+ prepared->state_memo = emalloc(sizeof(HashTable));
+ zend_hash_init(prepared->state_memo, 8, NULL, ZVAL_PTR_DTOR, 0);
+ }
+
+ if (verbatim_graph_len != 0) {
+ /* The verbatim pass already computed the exact size. */
+ graph_len = verbatim_graph_len;
+ sized = true;
+ } else {
+ sized = php_user_cache_calculate_shared_graph_size(
+ value,
+ prepared->state_memo,
+ verbatim_verdicts,
+ &graph_len
+ );
+ }
+
+ if (sized) {
+ /* Do not truncate the graph length. */
+ if (graph_len > UINT32_MAX) {
+ return false;
+ }
+
+ prepared->value_type = PHP_USER_CACHE_VALUE_SHARED_GRAPH;
+ prepared->value_len = (uint32_t) graph_len;
+ prepared->payload_size = graph_len;
+
+ if (build_at_publish) {
+ if (Z_TYPE_P(value) == IS_STRING) {
+ prepared->owned_string = zend_string_copy(Z_STR_P(value));
+ }
+
+ return true;
+ }
+
+ prepared->owned_buffer = emalloc(graph_len);
+ /* Snapshot hooks may invalidate address-keyed verdicts. */
+ if (php_user_cache_build_shared_graph_in_place(
+ value,
+ prepared->state_memo,
+ zend_hash_num_elements(prepared->state_memo) == 0 ? verbatim_verdicts : NULL,
+ prepared->owned_buffer,
+ graph_len,
+ &prepared->payload_used_size,
+ &prepared->has_verbatim_array,
+ &prepared->fixup_offsets,
+ &prepared->fixup_count
+ )
+ ) {
+ 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;
+ }
+
+ 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)",
+ options->throw_on_failure,
+ options->honor_strict_store_failure
+ );
+
+ return false;
+}
+
+static bool user_cache_fetch_resolve_prototype_mode_locked(
+ const php_user_cache_entry *entry,
+ bool use_request_local_slot,
+ bool *no_aliases)
+{
+ *no_aliases = true;
+
+ if (!use_request_local_slot) {
+ return false;
+ }
+
+ switch (entry->value_type) {
+ case PHP_USER_CACHE_VALUE_SHARED_GRAPH:
+ if (!php_user_cache_shared_graph_prefers_prototype(entry->value_offset)) {
+ return false;
+ }
+
+ *no_aliases = !php_user_cache_shared_graph_payload_has_aliases(entry->value_offset);
+
+ return true;
+ case PHP_USER_CACHE_VALUE_STRING:
+ /* Repeated string fetches use the request-local slot. */
+ return true;
+ default:
+ /* Scalar hits are served by the lookup cache. */
+ return false;
+ }
+}
+
+/* Defer request-local cloning until after the read lock is released. */
+static bool user_cache_fetch_emit_value_locked(
+ const php_user_cache_header *header,
+ zend_string *key,
+ const char *cache_name,
+ php_user_cache_entry *entry,
+ uint64_t gen,
+ bool throw_if_missing,
+ bool use_request_local_slot,
+ zval *return_value,
+ php_user_cache_fetch_pending_seed *pending_seed)
+{
+ php_user_cache_request_local_slot_result slot_result;
+ uint32_t flags;
+ bool use_proto, no_aliases;
+
+ pending_seed->should_seed_request_local_slot = false;
+
+ use_proto = user_cache_fetch_resolve_prototype_mode_locked(entry, use_request_local_slot, &no_aliases);
+
+ if (use_proto) {
+ PHP_USER_CACHE_TRY_UNLOCK_ON_BAILOUT(
+ slot_result = user_cache_fetch_request_local_slot(key, gen, return_value);
+ );
+ if (slot_result == PHP_USER_CACHE_REQUEST_LOCAL_SLOT_HIT) {
+ return true;
+ }
+
+ if (EG(exception)) {
+ return false;
+ }
+ }
+
+ flags = 0;
+ if (use_proto) {
+ flags |= PHP_USER_CACHE_FETCH_FINISH_USE_REQUEST_LOCAL_SLOT;
+ }
+ if (no_aliases) {
+ flags |= PHP_USER_CACHE_FETCH_FINISH_NO_ALIASES;
+ }
+
+ if (user_cache_scalar_to_zval(entry->value_type, entry->long_value, entry->double_value, return_value)) {
+
+ return true;
+ }
+
+ switch (entry->value_type) {
+ case PHP_USER_CACHE_VALUE_STRING:
+ PHP_USER_CACHE_TRY_UNLOCK_ON_BAILOUT(
+ ZVAL_STRINGL(return_value, (const char *) user_cache_ptr_in_header(header, entry->value_offset), entry->value_len);
+ );
+ pending_seed->generation = gen;
+ pending_seed->flags = flags;
+ pending_seed->should_seed_request_local_slot = true;
+
+ return true;
+ case PHP_USER_CACHE_VALUE_SHARED_GRAPH:
+ if (!user_cache_materialize_shared_graph_locked(
+ header,
+ key,
+ cache_name,
+ entry->value_type,
+ entry->value_offset,
+ entry->value_len,
+ throw_if_missing,
+ return_value)
+ ) {
+ return false;
+ }
+
+ pending_seed->generation = gen;
+ pending_seed->flags = flags | PHP_USER_CACHE_FETCH_FINISH_DEFER_REQUEST_LOCAL_SLOT;
+ pending_seed->should_seed_request_local_slot = true;
+
+ return true;
+ default:
+ if (throw_if_missing) {
+ user_cache_throw_unknown_value_type_guarded(key, cache_name);
+ }
+
+ return false;
+ }
+}
+
+static php_user_cache_fetch_locate_result user_cache_fetch_probe_lookup_cache_locked(
+ php_user_cache_header *header,
+ zend_string *key,
+ zend_ulong hash,
+ php_user_cache_entry *entries,
+ php_user_cache_lookup_entry *lookup_entries,
+ uint64_t epoch,
+ zval *return_value,
+ uint32_t *slot_idx)
+{
+ const void *ctx = (const void *) php_user_cache_active_context();
+ php_user_cache_lookup_entry *lookup_entry;
+ php_user_cache_entry *entry;
+ uint64_t now;
+ uint32_t way;
+
+ for (way = 0; way < PHP_USER_CACHE_LOOKUP_WAYS; way++) {
+ lookup_entry = &lookup_entries[way];
+ if (lookup_entry->state == PHP_USER_CACHE_LOOKUP_EMPTY ||
+ lookup_entry->hash != hash ||
+ lookup_entry->context != ctx
+ ) {
+ continue;
+ }
+
+ if (lookup_entry->mutation_epoch != epoch) {
+ /* Revalidate stale hits; discard stale misses. */
+ if (lookup_entry->state != PHP_USER_CACHE_LOOKUP_HIT) {
+ user_cache_lookup_cache_reset_entry(lookup_entry);
+ continue;
+ }
+ } else {
+ if (lookup_entry->state == PHP_USER_CACHE_LOOKUP_MISS) {
+ /* A colliding MISS must not hide this key. */
+ if (lookup_entry->key == NULL || !zend_string_equals(lookup_entry->key, key)) {
+ continue;
+ }
+
+ return PHP_USER_CACHE_FETCH_LOCATE_MISS;
+ }
+
+ if (lookup_entry->key == key &&
+ user_cache_scalar_to_zval(
+ lookup_entry->value_type,
+ lookup_entry->long_value,
+ lookup_entry->double_value,
+ return_value
+ )
+ ) {
+ return PHP_USER_CACHE_FETCH_LOCATE_SCALAR_HIT;
+ }
+ }
+
+ if (lookup_entry->slot_index >= header->capacity) {
+ user_cache_lookup_cache_reset_entry(lookup_entry);
+ continue;
+ }
+
+ entry = &entries[lookup_entry->slot_index];
+ if (!user_cache_key_equals(header, entry, key, hash)) {
+ user_cache_lookup_cache_reset_entry(lookup_entry);
+ continue;
+ }
+
+ now = 0;
+ if (user_cache_is_expired_now(entry, &now)) {
+ user_cache_note_expired_read();
+ user_cache_lookup_cache_reset_entry(lookup_entry);
+ user_cache_lookup_cache_store_miss(lookup_entries, hash, epoch, key);
+
+ return PHP_USER_CACHE_FETCH_LOCATE_MISS;
+ }
+
+ *slot_idx = lookup_entry->slot_index;
+
+ /* Refresh the revived cache entry. */
+ if (lookup_entry->mutation_epoch != epoch || lookup_entry->key != key) {
+ user_cache_lookup_cache_store_hit(lookup_entry, hash, epoch, *slot_idx, key, entry);
+ }
+
+ return PHP_USER_CACHE_FETCH_LOCATE_SLOT;
+ }
+
+ return PHP_USER_CACHE_FETCH_LOCATE_UNCACHED;
+}
+
+static php_user_cache_fetch_locate_result user_cache_fetch_probe_entry_table_locked(
+ php_user_cache_header *header,
+ zend_string *key,
+ zend_ulong hash,
+ php_user_cache_entry *entries,
+ php_user_cache_lookup_entry *lookup_entries,
+ uint64_t epoch,
+ uint32_t *slot_idx)
+{
+ php_user_cache_lookup_entry *lookup_entry;
+ bool found;
+
+ if (!user_cache_find_slot_in_header_locked(
+ header,
+ key,
+ hash,
+ PHP_USER_CACHE_FIND_SLOT_SKIP_EXPIRED,
+ slot_idx,
+ &found
+ ) ||
+ !found
+ ) {
+ user_cache_lookup_cache_store_miss(lookup_entries, hash, epoch, key);
+
+ return PHP_USER_CACHE_FETCH_LOCATE_MISS;
+ }
+
+ lookup_entry = user_cache_lookup_cache_select_slot(lookup_entries, hash, epoch, true);
+ user_cache_lookup_cache_store_hit(lookup_entry, hash, epoch, *slot_idx, key, &entries[*slot_idx]);
+
+ return PHP_USER_CACHE_FETCH_LOCATE_SLOT;
+}
+
+static bool user_cache_atomic_insert_missing_locked(
+ zend_string *key,
+ zend_long step,
+ zend_long ttl,
+ bool decrement,
+ php_user_cache_atomic_update_result *result)
+{
+ php_user_cache_prepare_options prep_opts = {
+ .caller_holds_write_lock = true,
+ .throw_on_failure = false,
+ .honor_strict_store_failure = false,
+ };
+ php_user_cache_store_options store_opts = {
+ .retry_after_memory_pressure = true,
+ .throw_on_failure = false,
+ .honor_strict_store_failure = false,
+ .capture_replaced_entry = false,
+ };
+ php_user_cache_prepared_value prepared;
+ zend_long updated;
+ zval init_val = {0};
+ bool is_overflow, stored;
+
+ is_overflow = decrement
+ ? user_cache_long_sub_overflow(0, step, &updated)
+ : user_cache_long_add_overflow(0, step, &updated)
+ ;
+ if (is_overflow) {
+ result->is_overflow = true;
+
+ return false;
+ }
+
+ ZVAL_LONG(&init_val, updated);
+ if (!php_user_cache_prepare_value(key, &init_val, &prep_opts, &prepared)) {
+ php_user_cache_destroy_prepared_value(&prepared);
+
+ return false;
+ }
+
+ stored = php_user_cache_store_prepared_locked(
+ key,
+ &init_val,
+ &prepared,
+ ttl,
+ &store_opts,
+ NULL
+ );
+
+ php_user_cache_destroy_prepared_value(&prepared);
+
+ if (stored) {
+ result->new_value = Z_LVAL(init_val);
+
+ return true;
+ }
+
+ return false;
+}
+
+#ifdef PHP_USER_CACHE_HAVE_OPTIMISTIC
+/* Validate the final sequence before publishing the snapshot. */
+static php_user_cache_optimistic_result user_cache_optimistic_locate(
+ php_user_cache_header *header,
+ zend_string *key,
+ zend_ulong hash,
+ uint64_t seq,
+ uint64_t epoch,
+ php_user_cache_lookup_entry *lookup_entries,
+ bool have_snapshot,
+ php_user_cache_entry *snapshot,
+ uint32_t *slot_idx)
+{
+ php_user_cache_lookup_entry *lookup_entry;
+ bool found = have_snapshot;
+
+ if (!have_snapshot &&
+ !user_cache_optimistic_probe(
+ header,
+ key,
+ hash,
+ php_user_cache_entries_ptr(header),
+ snapshot,
+ slot_idx,
+ &found
+ )
+ ) {
+ return PHP_USER_CACHE_OPTIMISTIC_FALLBACK;
+ }
+
+ if (php_user_cache_seq_reload(&header->write_seq) != seq) {
+ return PHP_USER_CACHE_OPTIMISTIC_FALLBACK;
+ }
+
+ if (!found) {
+ user_cache_lookup_cache_store_miss(lookup_entries, hash, epoch, key);
+
+ return PHP_USER_CACHE_OPTIMISTIC_MISS;
+ }
+
+ lookup_entry = user_cache_lookup_cache_select_slot(lookup_entries, hash, epoch, true);
+ user_cache_lookup_cache_store_hit(lookup_entry, hash, epoch, *slot_idx, key, snapshot);
+
+ return PHP_USER_CACHE_OPTIMISTIC_FOUND;
+}
+
+static bool user_cache_optimistic_scan_lookup_cache(
+ php_user_cache_header *header,
+ zend_string *key,
+ zend_ulong hash,
+ uint64_t seq,
+ uint64_t epoch,
+ php_user_cache_lookup_entry *lookup_entries,
+ zval *return_value,
+ uint32_t *hint_slot,
+ php_user_cache_optimistic_result *result)
+{
+ const void *ctx = (const void *) php_user_cache_active_context();
+ php_user_cache_lookup_entry *lookup_entry;
+ uint32_t way;
+
+ *hint_slot = UINT32_MAX;
+
+ for (way = 0; way < PHP_USER_CACHE_LOOKUP_WAYS; way++) {
+ lookup_entry = &lookup_entries[way];
+
+ if (lookup_entry->state == PHP_USER_CACHE_LOOKUP_EMPTY ||
+ lookup_entry->hash != hash ||
+ lookup_entry->context != ctx
+ ) {
+ continue;
+ }
+
+ if (lookup_entry->state == PHP_USER_CACHE_LOOKUP_MISS) {
+ if (lookup_entry->mutation_epoch != epoch) {
+ user_cache_lookup_cache_reset_entry(lookup_entry);
+
+ continue;
+ }
+
+ if (lookup_entry->key == NULL || !zend_string_equals(lookup_entry->key, key)) {
+ continue;
+ }
+
+ *result = php_user_cache_seq_reload(&header->write_seq) != seq
+ ? PHP_USER_CACHE_OPTIMISTIC_FALLBACK
+ : PHP_USER_CACHE_OPTIMISTIC_MISS
+ ;
+
+ return true;
+ }
+
+ if (lookup_entry->mutation_epoch == epoch &&
+ lookup_entry->key == key &&
+ user_cache_scalar_to_zval(
+ lookup_entry->value_type,
+ lookup_entry->long_value,
+ lookup_entry->double_value,
+ return_value
+ )
+ ) {
+ *result = php_user_cache_seq_reload(&header->write_seq) != seq
+ ? PHP_USER_CACHE_OPTIMISTIC_FALLBACK
+ : PHP_USER_CACHE_OPTIMISTIC_FOUND
+ ;
+
+ return true;
+ }
+
+ *hint_slot = lookup_entry->slot_index;
+
+ break;
+ }
+
+ return false;
+}
+
+static bool user_cache_optimistic_try_hint_slot(
+ php_user_cache_header *header,
+ zend_string *key,
+ zend_ulong hash,
+ uint32_t hint_slot,
+ php_user_cache_entry *snapshot,
+ uint32_t *slot_idx)
+{
+ const php_user_cache_entry *hint_entry;
+
+ if (hint_slot == UINT32_MAX || hint_slot >= header->capacity) {
+ return false;
+ }
+
+ hint_entry = &php_user_cache_entries_ptr(header)[hint_slot];
+
+ if (hint_entry->state == PHP_USER_CACHE_ENTRY_USED &&
+ hint_entry->hash == hash &&
+ hint_entry->key_len == ZSTR_LEN(key) &&
+ user_cache_optimistic_payload_in_bounds(header, hint_entry->key_offset, hint_entry->key_len) &&
+ memcmp(user_cache_ptr_in_header(header, 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_idx = hint_slot;
+
+ return true;
+ }
+
+ return false;
+}
+
+static php_user_cache_optimistic_result user_cache_optimistic_emit_string(
+ php_user_cache_header *header,
+ zend_string *key,
+ uint64_t seq,
+ const php_user_cache_entry *snapshot,
+ zval *return_value)
+{
+ zend_string *str;
+
+ if (user_cache_fetch_request_local_slot(key, snapshot->generation, return_value) ==
+ PHP_USER_CACHE_REQUEST_LOCAL_SLOT_HIT
+ ) {
+ return PHP_USER_CACHE_OPTIMISTIC_FOUND;
+ }
+
+ if (!user_cache_optimistic_payload_in_bounds(header, snapshot->value_offset, snapshot->value_len)) {
+ return PHP_USER_CACHE_OPTIMISTIC_FALLBACK;
+ }
+
+ str = zend_string_init(
+ (const char *) user_cache_ptr_in_header(header, snapshot->value_offset),
+ snapshot->value_len,
+ 0
+ );
+
+ if (php_user_cache_seq_reload(&header->write_seq) != seq) {
+ zend_string_release(str);
+
+ return PHP_USER_CACHE_OPTIMISTIC_FALLBACK;
+ }
+
+ ZVAL_STR(return_value, str);
+ php_user_cache_fetch_finish(
+ key,
+ snapshot->generation,
+ return_value,
+ PHP_USER_CACHE_FETCH_FINISH_USE_REQUEST_LOCAL_SLOT | PHP_USER_CACHE_FETCH_FINISH_NO_ALIASES
+ );
+
+ return PHP_USER_CACHE_OPTIMISTIC_FOUND;
+}
+
+static php_user_cache_optimistic_result user_cache_optimistic_emit_shared_graph(
+ php_user_cache_header *header,
+ zend_string *key,
+ uint64_t seq,
+ const php_user_cache_entry *snapshot,
+ zval *return_value)
+{
+ uint32_t reader_slot = 0, flags;
+ bool use_proto, no_aliases, ref_registered;
+
+ if (!user_cache_optimistic_payload_in_bounds(
+ header,
+ snapshot->value_offset,
+ sizeof(php_user_cache_shared_graph_header) + ZEND_MM_ALIGNMENT) ||
+ !user_cache_optimistic_payload_in_bounds(
+ header,
+ snapshot->value_offset,
+ snapshot->value_len
+ )
+ ) {
+ return PHP_USER_CACHE_OPTIMISTIC_FALLBACK;
+ }
+
+ use_proto = php_user_cache_shared_graph_prefers_prototype(snapshot->value_offset);
+ no_aliases = !php_user_cache_shared_graph_payload_has_aliases(snapshot->value_offset);
+
+ if (php_user_cache_seq_reload(&header->write_seq) != seq) {
+ return PHP_USER_CACHE_OPTIMISTIC_FALLBACK;
+ }
+
+ if (use_proto &&
+ user_cache_fetch_request_local_slot(
+ key, snapshot->generation, return_value
+ ) == PHP_USER_CACHE_REQUEST_LOCAL_SLOT_HIT
+ ) {
+ return PHP_USER_CACHE_OPTIMISTIC_FOUND;
+ }
+
+ ref_registered = php_user_cache_has_request_shared_graph_ref(snapshot->value_offset);
+ if (!ref_registered) {
+ php_user_cache_shared_graph_ref_reserve();
+
+ if (!php_user_cache_optimistic_reader_begin(header, &reader_slot)) {
+ return PHP_USER_CACHE_OPTIMISTIC_FALLBACK;
+ }
+
+ if (php_user_cache_seq_reload(&header->write_seq) != seq) {
+ php_user_cache_optimistic_reader_end(header, reader_slot);
+
+ return PHP_USER_CACHE_OPTIMISTIC_FALLBACK;
+ }
+
+ if (!php_user_cache_shared_graph_acquire_ref(snapshot->value_offset)) {
+ php_user_cache_optimistic_reader_end(header, reader_slot);
+
+ return PHP_USER_CACHE_OPTIMISTIC_FALLBACK;
+ }
+
+ php_user_cache_register_shared_graph_ref(snapshot->value_offset);
+ php_user_cache_optimistic_reader_end(header, reader_slot);
+ }
+
+ ZVAL_UNDEF(return_value);
+
+ if (!php_user_cache_shared_graph_decode(
+ user_cache_ptr_in_header(header, 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);
+ }
+
+ return PHP_USER_CACHE_OPTIMISTIC_FALLBACK;
+ }
+
+ flags = PHP_USER_CACHE_FETCH_FINISH_DEFER_REQUEST_LOCAL_SLOT;
+ if (use_proto) {
+ flags |= PHP_USER_CACHE_FETCH_FINISH_USE_REQUEST_LOCAL_SLOT;
+ }
+ if (no_aliases) {
+ flags |= PHP_USER_CACHE_FETCH_FINISH_NO_ALIASES;
+ }
+
+ php_user_cache_fetch_finish(key, snapshot->generation, return_value, flags);
+
+ return PHP_USER_CACHE_OPTIMISTIC_FOUND;
+}
+#endif
+
+bool php_user_cache_clear_locked(void)
+{
+ php_user_cache_header *header = php_user_cache_header_ptr();
+ php_user_cache_entry *entries;
+ uint64_t epoch;
+ uint32_t i;
+
+ if (!header || !php_user_cache_header_init_locked()) {
+ return false;
+ }
+
+ epoch = header->mutation_epoch;
+ entries = php_user_cache_entries_ptr(header);
+ for (i = 0; i < header->capacity; i++) {
+ if (entries[i].state == PHP_USER_CACHE_ENTRY_USED) {
+ user_cache_release_entry_storage_locked(&entries[i]);
+ }
+ }
+
+ memset(entries, 0, sizeof(php_user_cache_entry) * header->capacity);
+
+ php_user_cache_shared_graph_reclaim_orphaned_locked();
+
+ header->count = 0;
+ header->tombstone_count = 0;
+ header->mutation_epoch = epoch + 1;
+
+ if (header->mutation_epoch == 0) {
+ header->mutation_epoch = 1;
+ }
+
+ return true;
+}
+
+void php_user_cache_lookup_cache_clear(void)
+{
+ uint32_t i;
+
+ for (i = 0; i < PHP_USER_CACHE_LOOKUP_BUCKETS; i++) {
+ user_cache_lookup_entry_release_key(&UC_G(lookup_entry_storage)[i]);
+ }
+
+ memset(
+ UC_G(lookup_entry_storage),
+ 0,
+ sizeof(php_user_cache_lookup_entry) * PHP_USER_CACHE_LOOKUP_BUCKETS
+ );
+}
+
+bool php_user_cache_prepare_value(
+ zend_string *key,
+ zval *value,
+ const php_user_cache_prepare_options *options,
+ php_user_cache_prepared_value *prepared)
+{
+ HashTable verbatim_verdicts;
+ size_t verbatim_graph_len = 0;
+ bool verbatim_eligible = false, has_verbatim_verdicts = false, result;
+
+ if (prepared == NULL || options == NULL) {
+ return false;
+ }
+
+ user_cache_init_prepared_value(prepared);
+ ZVAL_DEREF(value);
+ prepared->hash = zend_string_hash_val(key);
+
+ if (Z_TYPE_P(value) == IS_RESOURCE) {
+ user_cache_handle_store_failure(
+ PHP_USER_CACHE_MSG_RESOURCE_UNSTORABLE,
+ options->throw_on_failure,
+ options->honor_strict_store_failure
+ );
+
+ return false;
+ }
+
+ if (Z_TYPE_P(value) == IS_OBJECT && Z_OBJCE_P(value) == zend_ce_closure) {
+ user_cache_handle_store_failure(
+ PHP_USER_CACHE_MSG_CLOSURE_UNSTORABLE,
+ options->throw_on_failure,
+ options->honor_strict_store_failure
+ );
+
+ return false;
+ }
+
+ if (Z_TYPE_P(value) == IS_ARRAY && EXPECTED(!EG(exception))) {
+ zend_hash_init(&verbatim_verdicts, 8, NULL, NULL, 0);
+ has_verbatim_verdicts = true;
+ /* Prove eligibility and compute the size in one walk. */
+ switch (php_user_cache_shared_graph_calc_verbatim_root(
+ value,
+ &verbatim_verdicts,
+ &verbatim_graph_len
+ )) {
+ case PHP_USER_CACHE_VERBATIM_ROOT_SIZED:
+ case PHP_USER_CACHE_VERBATIM_ROOT_ELIGIBLE_UNSIZED:
+ verbatim_eligible = true;
+ break;
+ case PHP_USER_CACHE_VERBATIM_ROOT_INELIGIBLE:
+ break;
+ case PHP_USER_CACHE_VERBATIM_ROOT_UNDECIDED:
+ verbatim_eligible = php_user_cache_shared_graph_can_copy_verbatim_root(
+ value,
+ &verbatim_verdicts
+ );
+ break;
+ }
+ }
+
+ /* A verbatim root contains no values rejected by validation. */
+ if (!verbatim_eligible &&
+ !user_cache_validate_storable_value(
+ value,
+ options->throw_on_failure,
+ options->honor_strict_store_failure)
+ ) {
+ result = false;
+
+ goto done;
+ }
+
+ if (user_cache_prepare_direct_value(value, prepared)) {
+ result = true;
+
+ goto done;
+ }
+
+ if (!has_verbatim_verdicts) {
+ zend_hash_init(&verbatim_verdicts, 8, NULL, NULL, 0);
+ has_verbatim_verdicts = true;
+ }
+
+ result = user_cache_prepare_shared_graph_value(value, options, &verbatim_verdicts, verbatim_graph_len, prepared);
+
+done:
+ if (has_verbatim_verdicts) {
+ zend_hash_destroy(&verbatim_verdicts);
+ }
+
+ return result;
+}
+
+void php_user_cache_destroy_prepared_value(php_user_cache_prepared_value *prepared)
+{
+ if (prepared == NULL) {
+ return;
+ }
+
+ if (prepared->owned_buffer != NULL) {
+ efree(prepared->owned_buffer);
+ }
+
+ if (prepared->fixup_offsets != NULL) {
+ efree(prepared->fixup_offsets);
+ }
+
+ 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);
+ }
+
+ user_cache_init_prepared_value(prepared);
+}
+
+/* Use zend_array_release here to avoid changing GC timing. */
+void php_user_cache_object_table_dtor(zval *zv)
+{
+ zend_object *obj = Z_PTR_P(zv);
+
+ OBJ_RELEASE(obj);
+}
+
+void php_user_cache_reference_table_dtor(zval *zv)
+{
+ zval ref_zv;
+
+ ZVAL_REF(&ref_zv, (zend_reference *) Z_PTR_P(zv));
+ zval_ptr_dtor(&ref_zv);
+}
+
+void php_user_cache_store_request_local_slot(zend_string *key, uint64_t gen, zval *value, bool no_aliases)
+{
+ php_user_cache_request_local_slot *slot;
+ HashTable verdicts;
+ zval slot_zv;
+ bool needs_deep_clone = false, has_verdicts = false;
+
+ ZVAL_DEREF(value);
+
+ slot = user_cache_alloc_request_local_slot(gen, no_aliases, true);
+
+ /* Collect once to avoid rescanning nested arrays during cloning. */
+ if (Z_TYPE_P(value) == IS_ARRAY || Z_TYPE_P(value) == IS_OBJECT || Z_ISREF_P(value)) {
+ zend_hash_init(&verdicts, 8, NULL, NULL, 0);
+ has_verdicts = true;
+ needs_deep_clone = user_cache_collect_request_local_clone_verdicts(value, &verdicts);
+ }
+
+ if (!user_cache_clone_request_local_slot_value_known(
+ &slot->value,
+ value,
+ needs_deep_clone,
+ has_verdicts ? &verdicts : NULL,
+ no_aliases
+ )
+ ) {
+ if (has_verdicts) {
+ zend_hash_destroy(&verdicts);
+ }
+
+ ZVAL_PTR(&slot_zv, slot);
+
+ user_cache_request_local_slot_dtor(&slot_zv);
+
+ return;
+ }
+
+ slot->needs_deep_clone = needs_deep_clone;
+
+ if (has_verdicts) {
+ if (needs_deep_clone) {
+ /* Recollect verdicts for the cloned identities. */
+ zend_hash_destroy(&verdicts);
+ zend_hash_init(&slot->clone_verdicts, 8, NULL, NULL, 0);
+ user_cache_collect_request_local_clone_verdicts(&slot->value, &slot->clone_verdicts);
+ } else {
+ /* Source verdicts remain valid without a deep clone. */
+ slot->clone_verdicts = verdicts;
+ }
+
+ slot->has_clone_verdicts = true;
+ }
+
+ zend_hash_update_ptr(user_cache_request_local_slots(), key, slot);
+}
+
+bool php_user_cache_store_prepared_locked(
+ zend_string *key,
+ zval *value,
+ const php_user_cache_prepared_value *prepared,
+ zend_long ttl,
+ const php_user_cache_store_options *options,
+ php_user_cache_store_result *result)
+{
+ bool stored = false;
+
+ user_cache_maybe_expunge_expired_locked();
+
+ PHP_USER_CACHE_TRY_UNLOCK_ON_BAILOUT(
+ stored = user_cache_store_prepared_locked_impl(
+ key,
+ value,
+ prepared,
+ ttl,
+ options,
+ result
+ );
+ );
+
+ return stored;
+}
+
+bool php_user_cache_fetch_finish(
+ zend_string *key,
+ uint64_t gen,
+ zval *return_value,
+ uint32_t flags)
+{
+ php_user_cache_request_local_slot *slot;
+ bool should_store;
+
+ if (flags & PHP_USER_CACHE_FETCH_FINISH_USE_REQUEST_LOCAL_SLOT) {
+ if (!(flags & PHP_USER_CACHE_FETCH_FINISH_DEFER_REQUEST_LOCAL_SLOT)) {
+ should_store = true;
+ } else {
+ /* Leave an empty marker only if no value was promoted. */
+ slot = user_cache_find_request_local_slot(key, gen, true);
+
+ if (slot == NULL) {
+ user_cache_mark_request_local_slot(key, gen);
+ should_store = false;
+ } else if (!slot->has_value) {
+ should_store = true;
+ } else {
+ /* Preserve a slot promoted by a nested fetch. */
+ should_store = false;
+ }
+ }
+
+ if (should_store) {
+ php_user_cache_store_request_local_slot(
+ key,
+ gen,
+ return_value,
+ (flags & PHP_USER_CACHE_FETCH_FINISH_NO_ALIASES) != 0
+ );
+ }
+ }
+
+ return true;
+}
+
+bool php_user_cache_fetch_locked(
+ zend_string *key,
+ bool throw_if_missing,
+ bool use_request_local_slot,
+ zval *return_value,
+ bool *found,
+ php_user_cache_fetch_pending_seed *pending_seed)
+{
+ const char *cache_name = php_user_cache_active_context()->name;
+ php_user_cache_header *header;
+ php_user_cache_entry *entries, *entry;
+ php_user_cache_lookup_entry *lookup_entries;
+ php_user_cache_fetch_locate_result locate;
+ zend_ulong hash;
+ uint64_t epoch;
+ uint32_t slot_idx = 0;
+
+ if (found != NULL) {
+ *found = false;
+ }
+
+ pending_seed->should_seed_request_local_slot = false;
+
+ hash = zend_string_hash_val(key);
+
+ header = php_user_cache_header_ptr();
+ if (!header || !php_user_cache_header_is_initialized_locked()) {
+ if (throw_if_missing) {
+ user_cache_throw_key_not_found_guarded(key);
+ }
+
+ return false;
+ }
+
+ entries = php_user_cache_entries_ptr(header);
+ epoch = header->mutation_epoch;
+ lookup_entries = user_cache_lookup_cache_set(hash);
+
+ locate = user_cache_fetch_probe_lookup_cache_locked(
+ header, key, hash, entries, lookup_entries, epoch, return_value, &slot_idx
+ );
+
+ if (locate == PHP_USER_CACHE_FETCH_LOCATE_UNCACHED) {
+ locate = user_cache_fetch_probe_entry_table_locked(
+ header, key, hash, entries, lookup_entries, epoch, &slot_idx
+ );
+ }
+
+ if (locate == PHP_USER_CACHE_FETCH_LOCATE_SCALAR_HIT) {
+ if (found != NULL) {
+ *found = true;
+ }
+
+ return true;
+ }
+
+ if (locate == PHP_USER_CACHE_FETCH_LOCATE_MISS) {
+ if (throw_if_missing) {
+ user_cache_throw_key_not_found_guarded(key);
+ }
+
+ return false;
+ }
+
+ entry = &entries[slot_idx];
+
+ if (found != NULL) {
+ *found = true;
+ }
+
+ return user_cache_fetch_emit_value_locked(
+ header,
+ key,
+ cache_name,
+ entry,
+ entry->generation,
+ throw_if_missing,
+ use_request_local_slot,
+ return_value,
+ pending_seed
+ );
+}
+
+bool php_user_cache_exists_locked(zend_string *key)
+{
+ zend_ulong hash = zend_string_hash_val(key);
+ uint32_t slot_idx;
+ bool found;
+
+ if (!user_cache_find_slot_for_read_locked(key, hash, NULL, &slot_idx, &found)) {
+ return false;
+ }
+
+ return found;
+}
+
+void php_user_cache_discard_replaced_entry_locked(
+ zend_string *key,
+ php_user_cache_replaced_entry *replaced_entry)
+{
+ php_user_cache_header *header;
+ php_user_cache_entry *entries, *current_entry = NULL;
+ uint32_t slot_idx;
+ bool found;
+
+ if (replaced_entry == NULL || !replaced_entry->found) {
+ return;
+ }
+
+ if (user_cache_find_slot_ignore_expiry_locked(
+ key,
+ replaced_entry->entry.hash,
+ &header,
+ &slot_idx,
+ &found
+ ) &&
+ found
+ ) {
+ entries = php_user_cache_entries_ptr(header);
+ current_entry = &entries[slot_idx];
+ }
+
+ 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 php_user_cache_rollback_replaced_entry_locked(
+ zend_string *key,
+ php_user_cache_replaced_entry *replaced_entry)
+{
+ php_user_cache_header *header;
+ php_user_cache_entry *entries, *entry;
+ uint32_t slot_idx;
+ bool found;
+
+ if (!user_cache_find_slot_ignore_expiry_locked(
+ key,
+ zend_string_hash_val(key),
+ &header,
+ &slot_idx,
+ &found
+ )
+ ) {
+ return;
+ }
+
+ entries = php_user_cache_entries_ptr(header);
+ entry = &entries[slot_idx];
+
+ if (replaced_entry != NULL && replaced_entry->found) {
+ if (found) {
+ user_cache_release_entry_storage_except_locked(entry, &replaced_entry->entry);
+ } else {
+ if (entry->state == PHP_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) {
+ user_cache_delete_entry_locked(header, entry);
+ }
+}
+
+void php_user_cache_delete_locked(zend_string *key)
+{
+ php_user_cache_header *header;
+ php_user_cache_entry *entries;
+ zend_ulong hash = zend_string_hash_val(key);
+ uint32_t slot_idx;
+ bool found;
+
+ user_cache_maybe_expunge_expired_locked();
+
+ if (!user_cache_find_slot_for_write_locked(key, hash, &header, &slot_idx, &found) || !found) {
+ return;
+ }
+
+ entries = php_user_cache_entries_ptr(header);
+ user_cache_delete_entry_locked(header, &entries[slot_idx]);
+
+ user_cache_maybe_rehash_locked();
+}
+
+void php_user_cache_delete_by_prefix_locked(zend_string *prefix)
+{
+ php_user_cache_header *header;
+ php_user_cache_entry *entries, *entry;
+ uint32_t i;
+
+ header = php_user_cache_header_ptr();
+ if (!header || !php_user_cache_header_is_initialized_locked()) {
+ return;
+ }
+
+ entries = php_user_cache_entries_ptr(header);
+ for (i = 0; i < header->capacity; i++) {
+ entry = &entries[i];
+ if (entry->state != PHP_USER_CACHE_ENTRY_USED ||
+ entry->key_len < ZSTR_LEN(prefix) ||
+ memcmp(
+ user_cache_ptr_in_header(header, entry->key_offset),
+ ZSTR_VAL(prefix),
+ ZSTR_LEN(prefix)
+ ) != 0
+ ) {
+ continue;
+ }
+
+ user_cache_delete_entry_locked(header, entry);
+ }
+
+ user_cache_maybe_rehash_locked();
+}
+
+bool php_user_cache_atomic_update_locked(
+ zend_string *key,
+ zend_long step,
+ zend_long ttl,
+ bool decrement,
+ bool insert_if_missing,
+ php_user_cache_atomic_update_result *result)
+{
+ php_user_cache_header *header;
+ php_user_cache_entry *entries, *entry;
+ zend_ulong hash = zend_string_hash_val(key);
+ zend_long updated;
+ uint32_t slot_idx;
+ bool found, is_overflow;
+
+ result->new_value = 0;
+ result->is_overflow = false;
+ result->is_type_error = false;
+
+ user_cache_maybe_expunge_expired_locked();
+
+ if (!user_cache_find_slot_for_write_locked(key, hash, &header, &slot_idx, &found) || !found) {
+ return insert_if_missing
+ ? user_cache_atomic_insert_missing_locked(key, step, ttl, decrement, result)
+ : false
+ ;
+ }
+
+ entries = php_user_cache_entries_ptr(header);
+ entry = &entries[slot_idx];
+ if (entry->value_type != PHP_USER_CACHE_VALUE_LONG) {
+ result->is_type_error = true;
+
+ return false;
+ }
+
+ is_overflow = decrement
+ ? user_cache_long_sub_overflow(entry->long_value, step, &updated)
+ : user_cache_long_add_overflow(entry->long_value, step, &updated)
+ ;
+ if (is_overflow) {
+ result->is_overflow = true;
+
+ return false;
+ }
+
+ entry->long_value = updated;
+
+ php_user_cache_bump_mutation_epoch_locked(header);
+ entry->generation = header->mutation_epoch;
+
+ result->new_value = entry->long_value;
+
+ return true;
+}
+
+void php_user_cache_release_request_local_slots(void)
+{
+ user_cache_release_request_local_slot_table(&UC_G(request_local_slot_table));
+}
+
+void php_user_cache_release_active_request_local_slots(void)
+{
+ /* Generation and context checks make over-invalidation safe. */
+ php_user_cache_release_request_local_slots();
+}
+
+void php_user_cache_release_active_request_local_slots_by_prefix(zend_string *prefix)
+{
+ zend_string *key, **keys;
+ HashTable **slots_ptr = &UC_G(request_local_slot_table);
+ uint32_t i, slot_count, count = 0;
+
+ if (*slots_ptr == NULL) {
+ return;
+ }
+
+ slot_count = zend_hash_num_elements(*slots_ptr);
+ if (slot_count == 0) {
+ user_cache_release_request_local_slot_table(slots_ptr);
+
+ 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 (i = 0; i < count; i++) {
+ zend_hash_del(*slots_ptr, keys[i]);
+ zend_string_release(keys[i]);
+ }
+
+ efree(keys);
+
+ if (zend_hash_num_elements(*slots_ptr) == 0) {
+ user_cache_release_request_local_slot_table(slots_ptr);
+ }
+}
+
+#ifdef PHP_USER_CACHE_HAVE_OPTIMISTIC
+php_user_cache_optimistic_result php_user_cache_fetch_optimistic(
+ zend_string *key,
+ zval *return_value)
+{
+ php_user_cache_header *header;
+ php_user_cache_entry snapshot;
+ php_user_cache_lookup_entry *lookup_entries;
+ php_user_cache_optimistic_result result;
+ zend_ulong hash;
+ uint64_t seq, epoch;
+ uint32_t slot_idx = 0, hint_slot;
+ bool have_snapshot;
+
+ if (!user_cache_optimistic_header(&header, &seq)) {
+ return PHP_USER_CACHE_OPTIMISTIC_FALLBACK;
+ }
+
+ epoch = header->mutation_epoch;
+ hash = zend_string_hash_val(key);
+ lookup_entries = user_cache_lookup_cache_set(hash);
+
+ if (user_cache_optimistic_scan_lookup_cache(
+ header, key, hash, seq, epoch, lookup_entries, return_value, &hint_slot, &result
+ )
+ ) {
+ return result;
+ }
+
+ have_snapshot = user_cache_optimistic_try_hint_slot(
+ header, key, hash, hint_slot, &snapshot, &slot_idx
+ );
+
+ result = user_cache_optimistic_locate(
+ header, key, hash, seq, epoch, lookup_entries, have_snapshot, &snapshot, &slot_idx
+ );
+ if (result != PHP_USER_CACHE_OPTIMISTIC_FOUND) {
+ return result;
+ }
+
+ if (user_cache_scalar_to_zval(snapshot.value_type, snapshot.long_value, snapshot.double_value, return_value)) {
+ return PHP_USER_CACHE_OPTIMISTIC_FOUND;
+ }
+
+ switch (snapshot.value_type) {
+ case PHP_USER_CACHE_VALUE_STRING:
+ return user_cache_optimistic_emit_string(header, key, seq, &snapshot, return_value);
+ case PHP_USER_CACHE_VALUE_SHARED_GRAPH:
+ return user_cache_optimistic_emit_shared_graph(header, key, seq, &snapshot, return_value);
+ default:
+ return PHP_USER_CACHE_OPTIMISTIC_FALLBACK;
+ }
+}
+
+php_user_cache_optimistic_result php_user_cache_exists_optimistic(zend_string *key)
+{
+ const void *ctx;
+ php_user_cache_header *header;
+ php_user_cache_entry snapshot;
+ php_user_cache_lookup_entry *lookup_entries, *lookup_entry;
+ zend_ulong hash;
+ uint64_t seq, epoch;
+ uint32_t way, slot_idx = 0;
+
+ if (!user_cache_optimistic_header(&header, &seq)) {
+ return PHP_USER_CACHE_OPTIMISTIC_FALLBACK;
+ }
+
+ epoch = header->mutation_epoch;
+ hash = zend_string_hash_val(key);
+ lookup_entries = user_cache_lookup_cache_set(hash);
+
+ ctx = (const void *) php_user_cache_active_context();
+ for (way = 0; way < PHP_USER_CACHE_LOOKUP_WAYS; way++) {
+ lookup_entry = &lookup_entries[way];
+
+ if (lookup_entry->state == PHP_USER_CACHE_LOOKUP_EMPTY ||
+ lookup_entry->hash != hash ||
+ lookup_entry->mutation_epoch != epoch ||
+ lookup_entry->context != ctx
+ ) {
+ continue;
+ }
+
+ if (lookup_entry->state == PHP_USER_CACHE_LOOKUP_MISS) {
+ if (lookup_entry->key == NULL || !zend_string_equals(lookup_entry->key, key)) {
+ continue;
+ }
+
+ if (php_user_cache_seq_reload(&header->write_seq) != seq) {
+ return PHP_USER_CACHE_OPTIMISTIC_FALLBACK;
+ }
+
+ return PHP_USER_CACHE_OPTIMISTIC_MISS;
+ }
+
+ break;
+ }
+
+ return user_cache_optimistic_locate(
+ header, key, hash, seq, epoch, lookup_entries, false, &snapshot, &slot_idx
+ );
+}
+#else
+php_user_cache_optimistic_result php_user_cache_fetch_optimistic(
+ zend_string *key,
+ zval *return_value)
+{
+ (void) key;
+ (void) return_value;
+
+ return PHP_USER_CACHE_OPTIMISTIC_FALLBACK;
+}
+
+php_user_cache_optimistic_result php_user_cache_exists_optimistic(zend_string *key)
+{
+ (void) key;
+
+ return PHP_USER_CACHE_OPTIMISTIC_FALLBACK;
+}
+#endif
diff --git a/ext/user_cache/user_cache_internal.h b/ext/user_cache/user_cache_internal.h
new file mode 100644
index 000000000000..e9fc98cd3aa2
--- /dev/null
+++ b/ext/user_cache/user_cache_internal.h
@@ -0,0 +1,1163 @@
+/*
+ +----------------------------------------------------------------------+
+ | 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 PHP_USER_CACHE_INTERNAL_H
+#define PHP_USER_CACHE_INTERNAL_H
+
+#include "php.h"
+
+#include
+#ifdef ZTS
+# include "TSRM/TSRM.h"
+#endif
+#if defined(ZEND_WIN32) && defined(_MSC_VER)
+# include
+#endif
+#ifndef ZEND_WIN32
+# include
+#endif
+
+#include "Zend/zend_attributes.h"
+#include "Zend/zend_ast.h"
+#include "Zend/zend_atomic.h"
+#include "Zend/zend_call_stack.h"
+#include "Zend/zend_enum.h"
+#include "Zend/zend_exceptions.h"
+#include "Zend/zend_smart_str.h"
+#include "Zend/zend_system_id.h"
+
+#include "php_user_cache.h"
+#include "user_cache_shm.h"
+
+#include "ext/standard/php_var.h"
+
+#include "SAPI.h"
+
+#define PHP_USER_CACHE_MAGIC 0xCAC17E01U
+#define PHP_USER_CACHE_VERSION 1U
+#define PHP_USER_CACHE_MIN_CAPACITY 127U
+#define PHP_USER_CACHE_SLOT_BYTES 256U
+
+#define PHP_USER_CACHE_KEY_DELIMITER "\x1f"
+#define PHP_USER_CACHE_KEY_DELIMITER_CHAR '\x1f'
+#define PHP_USER_CACHE_KEY_DELIMITER_NAME "0x1F"
+
+#define PHP_USER_CACHE_MSG_RESOURCE_UNSTORABLE "resources cannot be stored in the user cache"
+#define PHP_USER_CACHE_MSG_CLOSURE_UNSTORABLE "Closure objects cannot be stored in the user cache"
+#define PHP_USER_CACHE_MSG_LAZY_OBJECT_UNSTORABLE "lazy objects cannot be stored in the user cache"
+#define PHP_USER_CACHE_MSG_OPAQUE_OBJECT_UNSTORABLE "objects with opaque internal state (e.g. Fiber, Generator, PDO) cannot be stored in the user cache"
+
+#define PHP_USER_CACHE_ENTRY_EMPTY 0
+#define PHP_USER_CACHE_ENTRY_USED 1
+#define PHP_USER_CACHE_ENTRY_TOMBSTONE 2
+
+#define PHP_USER_CACHE_VALUE_NULL 0
+#define PHP_USER_CACHE_VALUE_TRUE 1
+#define PHP_USER_CACHE_VALUE_FALSE 2
+#define PHP_USER_CACHE_VALUE_LONG 3
+#define PHP_USER_CACHE_VALUE_DOUBLE 4
+#define PHP_USER_CACHE_VALUE_STRING 5
+#define PHP_USER_CACHE_VALUE_SHARED_GRAPH 8
+
+#define PHP_USER_CACHE_SHARED_GRAPH_MAGIC 0xCAC17E02U
+#define PHP_USER_CACHE_SHARED_GRAPH_VERSION 1U
+#define PHP_USER_CACHE_SHARED_GRAPH_FLAG_HAS_SHARED_IDENTITY 0x2U
+#define PHP_USER_CACHE_SHARED_GRAPH_FLAG_HAS_OBJECT 0x4U
+#define PHP_USER_CACHE_SHARED_GRAPH_FLAG_PREFERS_PROTOTYPE 0x8U
+#define PHP_USER_CACHE_SHARED_GRAPH_RETIRED (1 << 30)
+#define PHP_USER_CACHE_SHARED_GRAPH_REFCOUNT_MASK (PHP_USER_CACHE_SHARED_GRAPH_RETIRED - 1)
+
+#define PHP_USER_CACHE_SHARED_GRAPH_VALUE_UNDEF 0
+#define PHP_USER_CACHE_SHARED_GRAPH_VALUE_NULL 1
+#define PHP_USER_CACHE_SHARED_GRAPH_VALUE_TRUE 2
+#define PHP_USER_CACHE_SHARED_GRAPH_VALUE_FALSE 3
+#define PHP_USER_CACHE_SHARED_GRAPH_VALUE_LONG 4
+#define PHP_USER_CACHE_SHARED_GRAPH_VALUE_DOUBLE 5
+#define PHP_USER_CACHE_SHARED_GRAPH_VALUE_STRING 6
+#define PHP_USER_CACHE_SHARED_GRAPH_VALUE_ARRAY 7
+#define PHP_USER_CACHE_SHARED_GRAPH_VALUE_OBJECT 8
+#define PHP_USER_CACHE_SHARED_GRAPH_VALUE_DYNAMIC_ARRAY 10
+#define PHP_USER_CACHE_SHARED_GRAPH_VALUE_OBJECT_REF 11
+#define PHP_USER_CACHE_SHARED_GRAPH_VALUE_REFERENCE 13
+#define PHP_USER_CACHE_SHARED_GRAPH_VALUE_REFERENCE_REF 14
+#define PHP_USER_CACHE_SHARED_GRAPH_VALUE_ARRAY_REF 15
+#define PHP_USER_CACHE_SHARED_GRAPH_VALUE_ENUM 16
+#define PHP_USER_CACHE_SHARED_GRAPH_VALUE_SAFE_DIRECT_OBJECT 18
+#define PHP_USER_CACHE_SHARED_GRAPH_VALUE_SERIALIZED_OBJECT 19
+#define PHP_USER_CACHE_SHARED_GRAPH_VALUE_SERDES_OBJECT 20
+#define PHP_USER_CACHE_SHARED_GRAPH_VALUE_SLEEP_OBJECT 21
+#define PHP_USER_CACHE_SHARED_GRAPH_VALUE_SHAPED_ARRAY 22
+#define PHP_USER_CACHE_SHARED_GRAPH_VALUE_SERIALIZED_SHAPED_OBJECT 23
+#define PHP_USER_CACHE_SHARED_GRAPH_VALUE_SLEEP_SHAPED_OBJECT 24
+
+#define PHP_USER_CACHE_SHARED_GRAPH_OBJECT_FLAG_SHARED 0x1U
+
+#define PHP_USER_CACHE_SHARED_GRAPH_ARRAY_FLAG_PACKED 0x2U
+#define PHP_USER_CACHE_SHARED_GRAPH_ARRAY_SHAPE_MAX_KEYS 8U
+
+#define PHP_USER_CACHE_LOOKUP_BUCKETS 256U
+#define PHP_USER_CACHE_LOOKUP_WAYS 2U
+#define PHP_USER_CACHE_LOOKUP_SETS (PHP_USER_CACHE_LOOKUP_BUCKETS / PHP_USER_CACHE_LOOKUP_WAYS)
+#define PHP_USER_CACHE_LOOKUP_INVALID_SLOT UINT32_MAX
+
+#define PHP_USER_CACHE_REQUEST_LOCAL_STRING_MIN_LEN 256U
+
+#define PHP_USER_CACHE_DIRECT_STRING_MIN_LEN 4096U
+#define PHP_USER_CACHE_DECODE_DIRECT_CACHE_SLOTS 4U
+
+#define PHP_USER_CACHE_EXPIRED_READ_EXPUNGE_THRESHOLD 64U
+
+#define PHP_USER_CACHE_EXPUNGE_SCAN_MAX 4096U
+
+#define PHP_USER_CACHE_ENTRY_LOCK_TABLE_SIZE 1024U
+#define PHP_USER_CACHE_ENTRY_LOCK_WAIT_TIMEOUT_US (10U * 1000U * 1000U)
+#define PHP_USER_CACHE_ENTRY_LOCK_EMPTY 0
+#define PHP_USER_CACHE_ENTRY_LOCK_USED 1
+#define PHP_USER_CACHE_ENTRY_LOCK_TOMBSTONE 2
+
+#if defined(__GNUC__) || defined(__clang__)
+# define PHP_USER_CACHE_HAVE_OPTIMISTIC 1
+#elif defined(ZEND_WIN32) && defined(_MSC_VER)
+# define PHP_USER_CACHE_HAVE_OPTIMISTIC 1
+# define PHP_USER_CACHE_OPTIMISTIC_MSVC 1
+#endif
+
+#if !defined(ZEND_WIN32) && defined(__linux__)
+# define PHP_USER_CACHE_HAVE_SHARED_MUTEX 1
+#endif
+
+/* Boundary partitions require named shared memory. */
+#if !defined(ZEND_WIN32) && defined(PHP_USER_CACHE_USE_SHM_OPEN)
+# define PHP_USER_CACHE_HAVE_BOUNDARY_SHM 1
+#endif
+
+#if defined(PHP_USER_CACHE_USE_MMAP) && !defined(ZEND_WIN32)
+# define PHP_USER_CACHE_HAVE_ANON_MMAP 1
+#endif
+
+#define PHP_USER_CACHE_LOCK_MODEL_FCNTL 0U
+#define PHP_USER_CACHE_LOCK_MODEL_MUTEX 1U
+
+#define PHP_USER_CACHE_READER_SLOTS 256U
+#define PHP_USER_CACHE_READER_DRAIN_SPIN 1024U
+#define PHP_USER_CACHE_READER_DRAIN_TIMEOUT_US 10000U
+#ifdef PHP_USER_CACHE_HAVE_OPTIMISTIC
+# define PHP_USER_CACHE_READER_CLAIM_MAX 4U
+#endif
+
+#define PHP_USER_CACHE_ORPHANED_GRAPH_SLOTS 32U
+
+#define PHP_USER_CACHE_LOOKUP_EMPTY 0
+#define PHP_USER_CACHE_LOOKUP_HIT 1
+#define PHP_USER_CACHE_LOOKUP_MISS 2
+
+#define PHP_USER_CACHE_ENTRY_RESERVED_COMBINED_VALUE_KEY 0x0001U
+
+#define PHP_USER_CACHE_BLOCK_FREE 1U
+#define PHP_USER_CACHE_LOOKUP_VALUE_NONE 0xFFU
+
+#ifndef ZEND_WIN32
+# define PHP_USER_CACHE_SEM_FILENAME_PREFIX ".PhpUserCacheSem."
+#endif
+
+/* Clear the debug-only flag before exposing a decoded table. */
+#if ZEND_DEBUG
+# define PHP_USER_CACHE_HT_DISALLOW_COW_VIOLATION(ht) HT_FLAGS(ht) &= ~HASH_FLAG_ALLOW_COW_VIOLATION
+#else
+# define PHP_USER_CACHE_HT_DISALLOW_COW_VIOLATION(ht)
+#endif
+
+#ifdef PHP_USER_CACHE_HAVE_OPTIMISTIC
+#ifdef PHP_USER_CACHE_OPTIMISTIC_MSVC
+# define PHP_USER_CACHE_ATOMIC_LOAD_64(target) \
+ ((uint64_t) _InterlockedOr64((volatile __int64 *) (target), 0))
+# define PHP_USER_CACHE_ATOMIC_STORE_64(target, value) \
+ ((void) _InterlockedExchange64((volatile __int64 *) (target), (__int64) (value)))
+# define PHP_USER_CACHE_ATOMIC_CAS_64(target, expected, desired) \
+ ((uint64_t) _InterlockedCompareExchange64( \
+ (volatile __int64 *) (target), \
+ (__int64) (desired), \
+ (__int64) (expected) \
+ ) == (expected))
+# define PHP_USER_CACHE_ATOMIC_LOAD_32(target) \
+ ((uint32_t) _InterlockedOr((volatile long *) (target), 0))
+# define PHP_USER_CACHE_ATOMIC_STORE_32(target, value) \
+ ((void) _InterlockedExchange((volatile long *) (target), (long) (value)))
+# define PHP_USER_CACHE_ATOMIC_CAS_32(target, expected, desired) \
+ ((uint32_t) _InterlockedCompareExchange((volatile long *) (target), (long) (desired), (long) (expected)) == (expected))
+# define PHP_USER_CACHE_ATOMIC_FENCE_SEQ_CST() MemoryBarrier()
+# define PHP_USER_CACHE_ATOMIC_FENCE_ACQUIRE() MemoryBarrier()
+#else
+# define PHP_USER_CACHE_ATOMIC_LOAD_64(target) \
+ __atomic_load_n((target), __ATOMIC_ACQUIRE)
+# define PHP_USER_CACHE_ATOMIC_STORE_64(target, value) \
+ __atomic_store_n((target), (value), __ATOMIC_RELEASE)
+# define PHP_USER_CACHE_ATOMIC_CAS_64(target, expected, desired) \
+ __atomic_compare_exchange_n((target), &(expected), (desired), false, __ATOMIC_ACQ_REL, __ATOMIC_ACQUIRE)
+# define PHP_USER_CACHE_ATOMIC_LOAD_32(target) \
+ __atomic_load_n((target), __ATOMIC_ACQUIRE)
+# define PHP_USER_CACHE_ATOMIC_STORE_32(target, value) \
+ __atomic_store_n((target), (value), __ATOMIC_RELEASE)
+# define PHP_USER_CACHE_ATOMIC_CAS_32(target, expected, desired) \
+ __atomic_compare_exchange_n((target), &(expected), (desired), false, __ATOMIC_ACQ_REL, __ATOMIC_ACQUIRE)
+# define PHP_USER_CACHE_ATOMIC_FENCE_SEQ_CST() __atomic_thread_fence(__ATOMIC_SEQ_CST)
+# define PHP_USER_CACHE_ATOMIC_FENCE_ACQUIRE() __atomic_thread_fence(__ATOMIC_ACQUIRE)
+#endif
+#endif
+
+#ifdef PHP_USER_CACHE_HAVE_SHARED_MUTEX
+typedef union {
+ pthread_mutex_t mutex;
+ char padding[64];
+} php_user_cache_shared_mutex;
+#endif
+
+typedef struct {
+ size_t configured_memory;
+ php_user_cache_reason failure_reason;
+ bool enabled;
+ bool available;
+} php_user_cache_runtime;
+
+typedef struct {
+ const php_user_cache_shm_handlers *handler;
+ php_user_cache_shm_segment **segments;
+ int segment_count;
+ size_t size;
+ bool initialized;
+ uint32_t startup_complete;
+ bool initialized_before_request;
+ bool lock_initialized;
+ bool use_shared_mutex;
+ int lock_file;
+ char lockfile_name[MAXPATHLEN];
+ /* Cached for the lifetime of the attached segment. */
+ bool layout_memo_valid;
+ uint32_t capacity_memo;
+ uint32_t data_offset_memo;
+#ifdef PHP_USER_CACHE_HAVE_BOUNDARY_SHM
+ /* Cached for the lifetime of the partition. */
+ bool boundary_digest_memoized;
+ uint8_t boundary_digest_memo[32];
+#endif
+#ifdef ZTS
+ MUTEX_T zts_lock;
+#endif
+} php_user_cache_storage;
+
+typedef struct {
+ php_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;
+ bool boundary_shared;
+ const char *boundary_identity;
+ size_t boundary_identity_len;
+} php_user_cache_context;
+
+struct _php_user_cache_partition {
+ php_user_cache_context context;
+ char *name;
+ struct _php_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];
+} php_user_cache_entry_lock_record;
+
+typedef struct {
+ uint64_t owner_pid;
+ uint64_t owner_start_time;
+ uint32_t active;
+ uint32_t reserved;
+} php_user_cache_reader_slot;
+
+/* Cross-references are offsets from the segment base. */
+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;
+ uint64_t write_seq;
+ uint64_t expunge_count;
+ uint64_t store_failure_count;
+ uint32_t tombstone_count;
+ uint32_t lock_model;
+ uint32_t lock_model_reserved;
+ uint32_t orphaned_graphs_saturated;
+ uint32_t orphaned_graphs_reserved;
+ uint8_t boundary_identity_digest[32];
+ uint32_t boundary_identity_digest_set;
+ uint32_t boundary_identity_reserved;
+ uint32_t orphaned_graphs[PHP_USER_CACHE_ORPHANED_GRAPH_SLOTS];
+ php_user_cache_reader_slot reader_slots[PHP_USER_CACHE_READER_SLOTS];
+#ifdef PHP_USER_CACHE_HAVE_SHARED_MUTEX
+ php_user_cache_shared_mutex global_shared_mutex;
+#endif
+ php_user_cache_entry_lock_record entry_lock_records[PHP_USER_CACHE_ENTRY_LOCK_TABLE_SIZE];
+#ifndef ZEND_WIN32
+ uint32_t reserved_lock;
+#endif
+} php_user_cache_header;
+
+typedef struct {
+ uint32_t size;
+ uint32_t prev_size;
+ uint32_t next_free;
+ uint32_t prev_free;
+ uint32_t flags;
+} php_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;
+} php_user_cache_entry;
+
+typedef struct {
+ bool found;
+ php_user_cache_entry entry;
+} php_user_cache_replaced_entry;
+
+typedef struct {
+ zend_ulong hash;
+ uint64_t mutation_epoch;
+ const void *context;
+ uint32_t slot_index;
+ uint8_t state;
+ uint8_t value_type;
+ uint8_t reserved[2];
+ zend_string *key;
+ zend_long long_value;
+ double double_value;
+} php_user_cache_lookup_entry;
+
+typedef struct {
+ uint64_t generation;
+ const void *context;
+ bool needs_deep_clone;
+ bool has_clone_verdicts;
+ bool no_aliases;
+ bool has_value;
+ zval value;
+ HashTable clone_verdicts;
+} php_user_cache_request_local_slot;
+
+typedef struct {
+ zend_ulong hash;
+ size_t payload_size;
+ size_t payload_used_size;
+ const uint8_t *payload_source;
+ uint8_t *owned_buffer;
+ zend_string *owned_string;
+ /* Owned by the prepared value. */
+ uint32_t *fixup_offsets;
+ zend_long long_value;
+ double double_value;
+ HashTable *state_memo;
+ uint32_t value_len;
+ uint32_t fixup_count;
+ uint8_t value_type;
+ bool has_verbatim_array;
+ uint8_t reserved[2];
+} php_user_cache_prepared_value;
+
+typedef struct {
+ bool caller_holds_write_lock;
+ bool throw_on_failure;
+ bool honor_strict_store_failure;
+} php_user_cache_prepare_options;
+
+typedef struct {
+ bool retry_after_memory_pressure;
+ bool throw_on_failure;
+ bool honor_strict_store_failure;
+ bool capture_replaced_entry;
+} php_user_cache_store_options;
+
+typedef struct {
+ php_user_cache_replaced_entry replaced_entry;
+ uint64_t stored_generation;
+ bool should_seed_request_local_slot;
+} php_user_cache_store_result;
+
+/* Deferred until after the read lock is released. */
+typedef struct {
+ uint64_t generation;
+ uint32_t flags;
+ bool should_seed_request_local_slot;
+} php_user_cache_fetch_pending_seed;
+
+typedef struct {
+ zend_long new_value;
+ bool is_overflow;
+ bool is_type_error;
+} php_user_cache_atomic_update_result;
+
+typedef struct {
+ uint32_t magic;
+ uint32_t version;
+ uint32_t root_offset;
+ uint32_t root_type;
+ uint32_t flags;
+ zend_atomic_int ref_state;
+} php_user_cache_shared_graph_header;
+
+typedef struct {
+ php_user_cache_context *context;
+ uint32_t payload_offset;
+} php_user_cache_shared_graph_ref;
+
+typedef struct {
+ php_user_cache_context *context;
+ uint64_t owner_pid;
+ zend_long lease;
+ bool preserve_lease;
+} php_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;
+} php_user_cache_shared_graph_value;
+
+typedef struct {
+ uint32_t name_offset;
+ /* Declared property index plus one for __sleep state, otherwise zero. */
+ uint32_t reserved;
+ php_user_cache_shared_graph_value value;
+} php_user_cache_shared_graph_property;
+
+typedef struct {
+ zend_ulong h;
+ uint32_t key_offset;
+ uint32_t reserved;
+ php_user_cache_shared_graph_value value;
+} php_user_cache_shared_graph_array_element;
+
+typedef struct {
+ uint32_t count;
+ uint32_t next_free;
+ uint32_t elements_offset;
+ uint32_t reserved;
+} php_user_cache_shared_graph_array;
+
+typedef struct {
+ uint32_t key_offset;
+} php_user_cache_shared_graph_array_shape_element;
+
+typedef struct {
+ uint32_t count;
+ uint32_t elements_offset;
+ uint32_t reserved;
+ uint32_t reserved2;
+} php_user_cache_shared_graph_array_shape;
+
+typedef struct {
+ uint32_t count;
+ uint32_t next_free;
+ uint32_t shape_offset;
+ uint32_t reserved;
+ uint32_t values_offset;
+ uint32_t reserved2;
+} php_user_cache_shared_graph_shaped_array;
+
+typedef struct {
+ uint32_t class_name_offset;
+ uint32_t property_count;
+ uint32_t properties_offset;
+ uint32_t reserved;
+} php_user_cache_shared_graph_object;
+
+typedef struct {
+ uint32_t class_name_offset;
+ uint32_t shape_offset;
+ uint32_t count;
+} php_user_cache_shared_graph_state_schema;
+
+typedef struct {
+ uint32_t state_schema_offset;
+ uint32_t reserved;
+ uint32_t state_values_offset;
+ uint32_t state_next_free;
+} php_user_cache_shared_graph_shaped_state_object;
+
+/* Shared layout for safe-direct and serialized object state. */
+typedef struct {
+ uint32_t class_name_offset;
+ uint32_t property_count;
+ uint32_t properties_offset;
+ uint32_t reserved;
+ php_user_cache_shared_graph_value state;
+} php_user_cache_shared_graph_safe_direct_object;
+
+typedef struct {
+ uint32_t blob_len;
+ uint32_t flags;
+} php_user_cache_shared_graph_serdes_object;
+
+typedef struct {
+ uint32_t flags;
+ uint32_t reserved;
+ php_user_cache_shared_graph_value inner;
+} php_user_cache_shared_graph_reference;
+
+typedef struct {
+ uint32_t class_name_offset;
+ uint32_t case_name_offset;
+} php_user_cache_shared_graph_enum;
+
+typedef struct {
+ size_t size;
+ HashTable seen_arrays;
+ HashTable seen_objects;
+ HashTable seen_references;
+ HashTable string_dedup;
+ HashTable array_shape_dedup;
+ HashTable state_schema_dedup;
+ HashTable direct_array_dedup;
+ HashTable direct_verdicts;
+ bool verbatim_arrays_allowed;
+ /* Distinguishes a sizing failure from an ineligible node. */
+ bool reserve_failed;
+ /* Array address to verbatim eligibility, shared by CALC and COPY. */
+ HashTable *shared_verdicts;
+ HashTable *state_memo;
+} php_user_cache_shared_graph_calc_context;
+
+typedef struct {
+ uint8_t *buffer;
+ size_t size;
+ size_t position;
+ /* Absolute pointer slots to patch if the payload moves. */
+ uint32_t *fixup_offsets;
+ uint32_t fixup_count;
+ uint32_t fixup_capacity;
+ HashTable seen_arrays;
+ /* Packs the aligned object offset and flags-field displacement. */
+ HashTable seen_objects;
+ HashTable seen_references;
+ HashTable string_dedup;
+ HashTable array_shape_dedup;
+ HashTable state_schema_dedup;
+ HashTable direct_array_dedup;
+ HashTable direct_verdicts;
+ bool has_shared_identity;
+ bool has_object;
+ bool prefers_prototype;
+ bool has_userland_restore_object;
+ bool has_verbatim_array;
+ bool verbatim_arrays_allowed;
+ /* CALC verdicts remain valid only if no snapshot hook ran. */
+ const HashTable *shared_verdicts;
+ HashTable *state_memo;
+} php_user_cache_shared_graph_copy_context;
+
+typedef struct {
+ const uint8_t *old_base;
+ const uint8_t *new_base;
+ size_t len;
+ ptrdiff_t delta;
+ HashTable *seen;
+} php_user_cache_shared_graph_rebase_context;
+
+typedef enum {
+ PHP_USER_CACHE_OPTIMISTIC_FALLBACK = 0,
+ PHP_USER_CACHE_OPTIMISTIC_FOUND,
+ PHP_USER_CACHE_OPTIMISTIC_MISS
+} php_user_cache_optimistic_result;
+
+/* Result of the root-array verbatim sizing attempt. */
+typedef enum {
+ PHP_USER_CACHE_VERBATIM_ROOT_UNDECIDED = 0,
+ PHP_USER_CACHE_VERBATIM_ROOT_SIZED,
+ PHP_USER_CACHE_VERBATIM_ROOT_ELIGIBLE_UNSIZED,
+ PHP_USER_CACHE_VERBATIM_ROOT_INELIGIBLE
+} php_user_cache_verbatim_root_result;
+
+#ifdef PHP_USER_CACHE_HAVE_OPTIMISTIC
+typedef struct {
+ php_user_cache_header *header;
+ uint32_t slot_index;
+} php_user_cache_reader_claim;
+#endif
+
+typedef struct {
+ php_user_cache_partition *active_partition;
+ php_user_cache_runtime runtime_state;
+ php_user_cache_context *active_context_ptr;
+ php_user_cache_reason request_unavailable_reason;
+ bool lock_held;
+ bool lock_held_is_write;
+ bool runtime_resolved;
+ bool runtime_resolved_enabled;
+ const php_user_cache_context *runtime_resolved_context;
+ php_user_cache_shared_graph_ref *shared_graph_refs;
+ uint32_t shared_graph_ref_count;
+ uint32_t shared_graph_ref_capacity;
+ uint64_t shared_graph_ref_owner_pid;
+ HashTable *shared_graph_ref_index;
+ php_user_cache_lookup_entry lookup_entry_storage[PHP_USER_CACHE_LOOKUP_BUCKETS];
+ HashTable *request_local_slot_table;
+ HashTable *entry_lock_table;
+ HashTable *pool_table;
+ HashTable *decode_identity_map;
+ HashTable *decode_reference_map;
+ HashTable *decode_array_map;
+ HashTable *decode_resolve_cache;
+ HashTable *decode_shape_prototype_cache;
+ /* Request-local class route cache. */
+ HashTable *object_route_memo;
+ const void *decode_resolve_direct_keys[PHP_USER_CACHE_DECODE_DIRECT_CACHE_SLOTS];
+ void *decode_resolve_direct_values[PHP_USER_CACHE_DECODE_DIRECT_CACHE_SLOTS];
+ const void *decode_shape_prototype_direct_keys[PHP_USER_CACHE_DECODE_DIRECT_CACHE_SLOTS];
+ zend_array *decode_shape_prototype_direct_values[PHP_USER_CACHE_DECODE_DIRECT_CACHE_SLOTS];
+ uint8_t decode_resolve_direct_next;
+ uint8_t decode_shape_prototype_direct_next;
+#ifndef ZEND_WIN32
+ zend_ulong entry_lock_owner_pid;
+#endif
+ bool write_seq_bumped;
+ bool stack_overflowed;
+ /* Request shutdown must collect cyclic slot clones. */
+ bool request_local_slot_may_cycle;
+#ifdef PHP_USER_CACHE_HAVE_OPTIMISTIC
+ int8_t reader_drain_state;
+ php_user_cache_reader_claim reader_claims[PHP_USER_CACHE_READER_CLAIM_MAX];
+ uint32_t reader_claim_count;
+#endif
+ uint32_t expired_read_observations;
+ /* Process-local resume point for bounded expiry scans. */
+ uint32_t expired_expunge_cursor;
+ uint64_t entry_lock_owner_probe_pid;
+ uint64_t entry_lock_owner_probe_start_time;
+ uint64_t entry_lock_owner_probe_at;
+ bool entry_lock_owner_probe_dead;
+ /* Recomputed after fork. */
+ uint64_t self_start_time_pid;
+ uint64_t self_start_time_token;
+ bool enable;
+ bool enable_cli;
+ zend_long shm_size;
+ char *lockfile_path;
+ char *memory_model;
+} php_user_cache_globals;
+
+#ifdef ZTS
+# define UC_G(v) ZEND_TSRMG_FAST(user_cache_globals_offset, php_user_cache_globals *, v)
+extern int user_cache_globals_id;
+extern size_t user_cache_globals_offset;
+#else
+# define UC_G(v) (user_cache_globals.v)
+extern php_user_cache_globals user_cache_globals;
+#endif
+
+extern zend_class_entry *php_user_cache_exception_ce;
+extern zend_class_entry *php_user_cache_status_ce;
+extern zend_class_entry *php_user_cache_pool_status_ce;
+extern php_user_cache_context php_user_cache_context_state;
+extern bool php_user_cache_runtime_opted_in;
+extern php_user_cache_partition *php_user_cache_partitions;
+
+uint64_t php_user_cache_cached_pid(void);
+void php_user_cache_reset_runtime(void);
+void php_user_cache_reset_storage(void);
+bool php_user_cache_header_init_locked(void);
+void php_user_cache_free_locked(uint32_t payload_offset);
+uint32_t php_user_cache_alloc_locked(size_t size, const void *src);
+bool php_user_cache_startup_storage_before_request(void);
+void php_user_cache_shutdown_storage(void);
+void php_user_cache_ensure_ready_slow(void);
+bool php_user_cache_rlock(void);
+bool php_user_cache_wlock(void);
+bool php_user_cache_wlock_for_entry_mutation(zend_string *key);
+void php_user_cache_unlock(void);
+void php_user_cache_unlock_if_held(void);
+bool php_user_cache_try_acquire_entry_lock(zend_string *key, zend_long lease);
+bool php_user_cache_release_entry_lock(zend_string *key);
+/* Keys must be sorted with duplicates adjacent. */
+bool php_user_cache_acquire_entry_locks(zend_string **keys, bool *acquired, uint32_t count);
+void php_user_cache_release_entry_locks(zend_string **keys, const bool *acquired, uint32_t count);
+bool php_user_cache_entry_locks_allow_clear_locked(void);
+void php_user_cache_release_request_entry_locks(void);
+void php_user_cache_safe_direct_handlers_init(void);
+void php_user_cache_safe_direct_handlers_destroy(void);
+const php_user_cache_safe_direct_handlers *php_user_cache_safe_direct_find_handlers(
+ zend_class_entry *ce,
+ zend_class_entry **base_ce_ptr
+);
+php_user_cache_safe_direct_state_copy_func_t php_user_cache_safe_direct_state_copy_func(
+ zend_class_entry *ce,
+ zend_class_entry **base_ce_ptr
+);
+zend_class_entry *php_user_cache_safe_direct_find_base(zend_class_entry *ce);
+php_user_cache_safe_direct_state_has_unstorable_func_t php_user_cache_safe_direct_state_has_unstorable_func(
+ zend_class_entry *ce
+);
+php_user_cache_safe_direct_state_serialize_func_t php_user_cache_safe_direct_state_serialize_func(
+ zend_class_entry *ce
+);
+php_user_cache_safe_direct_state_unserialize_func_t php_user_cache_safe_direct_state_unserialize_func(
+ zend_class_entry *ce
+);
+bool php_user_cache_safe_direct_prefers_request_local_prototype(zend_class_entry *ce);
+bool php_user_cache_serdes_encode(zval *value, smart_str *buf, const char **failure_msg);
+bool php_user_cache_serdes_decode(const uint8_t *data, size_t len, zval *dst);
+uint32_t php_user_cache_serdes_declared_property_index_plus_one(
+ zend_class_entry *ce,
+ zend_string *name
+);
+bool php_user_cache_serdes_get_sleep_state(
+ zval *obj_zv,
+ zval *state,
+ const char **failure_msg
+);
+bool php_user_cache_serdes_call_magic_serialize(zend_object *obj, zval *state);
+bool php_user_cache_shared_graph_update_object_property(
+ zval *obj_zv,
+ zend_string *prop_name,
+ zval *prop_val
+);
+bool php_user_cache_shared_graph_update_object_property_at(
+ zval *obj_zv,
+ zend_string *prop_name,
+ uint32_t prop_idx,
+ zval *prop_val
+);
+bool php_user_cache_shared_graph_can_copy_verbatim_root(const zval *value, HashTable *verbatim_verdicts);
+php_user_cache_verbatim_root_result php_user_cache_shared_graph_calc_verbatim_root(const zval *value, HashTable *verbatim_verdicts, size_t *buf_len);
+bool php_user_cache_calculate_shared_graph_size(const zval *value, HashTable *state_memo, HashTable *verbatim_verdicts, size_t *buf_len);
+bool php_user_cache_build_shared_graph_in_place(const zval *value, HashTable *state_memo, const HashTable *verbatim_verdicts, uint8_t *buf, size_t buf_len, size_t *graph_len, bool *has_verbatim_array, uint32_t **fixup_offsets, uint32_t *fixup_count);
+bool php_user_cache_shared_graph_copy_fits_buffer(
+ const uint8_t *dst_buf,
+ size_t dst_buf_len,
+ const uint8_t *src_buf,
+ size_t src_buf_len,
+ size_t src_graph_len
+);
+bool php_user_cache_shared_graph_decode(const uint8_t *buf, size_t buf_len, zval *dst);
+bool php_user_cache_shared_graph_prefers_prototype(uint32_t payload_offset);
+bool php_user_cache_shared_graph_decode_is_lock_safe(uint32_t payload_offset);
+bool php_user_cache_shared_graph_payload_has_aliases(uint32_t payload_offset);
+void php_user_cache_decode_resolve_cache_release(void);
+void php_user_cache_decode_shape_prototype_cache_release(void);
+void php_user_cache_decode_maps_teardown(void);
+bool php_user_cache_shared_graph_can_overwrite_payload_locked(uint32_t payload_offset);
+bool php_user_cache_shared_graph_publish_copied_payload_locked(
+ uint8_t *dst_buf,
+ size_t dst_buf_len,
+ const uint8_t *src_buf,
+ size_t src_buf_len,
+ size_t src_graph_len,
+ bool has_verbatim_array,
+ const uint32_t *fixup_offsets,
+ uint32_t fixup_count
+);
+bool php_user_cache_shared_graph_acquire_ref(uint32_t payload_offset);
+bool php_user_cache_shared_graph_retire_payload_locked(uint32_t payload_offset);
+bool php_user_cache_shared_graph_release_ref_locked(uint32_t payload_offset);
+bool php_user_cache_has_request_shared_graph_ref(uint32_t payload_offset);
+void php_user_cache_register_shared_graph_ref(uint32_t payload_offset);
+bool php_user_cache_release_request_shared_graph_refs(void);
+bool php_user_cache_clear_locked(void);
+void php_user_cache_lookup_cache_clear(void);
+bool php_user_cache_prepare_value(
+ zend_string *key,
+ zval *value,
+ const php_user_cache_prepare_options *options,
+ php_user_cache_prepared_value *prepared
+);
+void php_user_cache_destroy_prepared_value(php_user_cache_prepared_value *prepared);
+bool php_user_cache_store_prepared_locked(
+ zend_string *key,
+ zval *value,
+ const php_user_cache_prepared_value *prepared,
+ zend_long ttl,
+ const php_user_cache_store_options *options,
+ php_user_cache_store_result *result
+);
+bool php_user_cache_fetch_locked(
+ zend_string *key,
+ bool throw_if_missing,
+ bool use_request_local_slot,
+ zval *return_value,
+ bool *found,
+ php_user_cache_fetch_pending_seed *pending_seed
+);
+bool php_user_cache_fetch_finish(zend_string *key, uint64_t gen, zval *return_value, uint32_t flags);
+bool php_user_cache_exists_locked(zend_string *key);
+void php_user_cache_delete_locked(zend_string *key);
+void php_user_cache_discard_replaced_entry_locked(zend_string *key, php_user_cache_replaced_entry *replaced_entry);
+void php_user_cache_rollback_replaced_entry_locked(zend_string *key, php_user_cache_replaced_entry *replaced_entry);
+void php_user_cache_delete_by_prefix_locked(zend_string *prefix);
+bool php_user_cache_atomic_update_locked(
+ zend_string *key,
+ zend_long step,
+ zend_long ttl,
+ bool decrement,
+ bool insert_if_missing,
+ php_user_cache_atomic_update_result *result
+);
+void php_user_cache_release_request_local_slots(void);
+void php_user_cache_release_active_request_local_slots(void);
+void php_user_cache_release_active_request_local_slots_by_prefix(zend_string *prefix);
+void php_user_cache_store_request_local_slot(zend_string *key, uint64_t gen, zval *value, bool no_aliases);
+void php_user_cache_object_table_dtor(zval *zv);
+void php_user_cache_reference_table_dtor(zval *zv);
+php_user_cache_optimistic_result php_user_cache_fetch_optimistic(
+ zend_string *key,
+ zval *return_value
+);
+php_user_cache_optimistic_result php_user_cache_exists_optimistic(zend_string *key);
+bool php_user_cache_optimistic_reader_begin(php_user_cache_header *header, uint32_t *slot_idx_ptr);
+void php_user_cache_optimistic_reader_end(php_user_cache_header *header, uint32_t slot_idx);
+void php_user_cache_optimistic_fork_setup(void);
+#if defined(ZTS) && defined(PHP_USER_CACHE_HAVE_OPTIMISTIC)
+void php_user_cache_release_thread_reader_claims(php_user_cache_globals *globals);
+#endif
+bool php_user_cache_quiesce_graph_payloads_locked(void);
+void php_user_cache_shared_graph_ref_reserve(void);
+void php_user_cache_shared_graph_orphan_payload_locked(uint32_t payload_offset);
+void php_user_cache_shared_graph_reclaim_orphaned_locked(void);
+
+static zend_always_inline uint64_t php_user_cache_current_pid(void)
+{
+#ifdef ZEND_WIN32
+ return (uint64_t) GetCurrentProcessId();
+#else
+ return (uint64_t) getpid();
+#endif
+}
+
+static zend_always_inline bool php_user_cache_stack_overflowed(void)
+{
+#ifdef ZEND_CHECK_STACK_LIMIT
+ bool overflowed = UNEXPECTED(zend_call_stack_overflowed(EG(stack_limit)));
+
+ /* Latch the failure for the rest of the request. */
+ if (overflowed) {
+ UC_G(stack_overflowed) = true;
+ }
+
+ return overflowed;
+#else
+ return false;
+#endif
+}
+
+#ifdef PHP_USER_CACHE_HAVE_OPTIMISTIC
+static zend_always_inline uint64_t php_user_cache_atomic_load_64(const uint64_t *target)
+{
+ return PHP_USER_CACHE_ATOMIC_LOAD_64(target);
+}
+
+static zend_always_inline void php_user_cache_atomic_fence_acquire(void)
+{
+ PHP_USER_CACHE_ATOMIC_FENCE_ACQUIRE();
+}
+#endif
+
+static zend_always_inline php_user_cache_context *php_user_cache_owning_context(void)
+{
+ return UC_G(active_partition) != NULL
+ ? &UC_G(active_partition)->context
+ : &php_user_cache_context_state
+ ;
+}
+
+static zend_always_inline php_user_cache_context *php_user_cache_active_context(void)
+{
+ if (UC_G(active_context_ptr) != NULL) {
+ return UC_G(active_context_ptr);
+ }
+
+ return php_user_cache_owning_context();
+}
+
+static zend_always_inline void *php_user_cache_base(void)
+{
+ php_user_cache_storage *storage = &php_user_cache_active_context()->storage;
+
+ if (!storage->initialized ||
+ storage->segment_count != 1
+ ) {
+ return NULL;
+ }
+
+ return storage->segments[0]->p;
+}
+
+static zend_always_inline php_user_cache_header *php_user_cache_header_ptr(void)
+{
+ return (php_user_cache_header *) php_user_cache_base();
+}
+
+static zend_always_inline uint8_t *php_user_cache_ptr(uint32_t offset)
+{
+ return (uint8_t *) php_user_cache_base() + offset;
+}
+
+static zend_always_inline php_user_cache_block *php_user_cache_block_ptr(uint32_t offset)
+{
+ return (php_user_cache_block *) php_user_cache_ptr(offset);
+}
+
+static zend_always_inline uint32_t php_user_cache_block_payload_capacity(uint32_t payload_offset)
+{
+ php_user_cache_block *block;
+
+ if (payload_offset < sizeof(php_user_cache_block)) {
+ return 0;
+ }
+
+ block = php_user_cache_block_ptr(payload_offset - sizeof(php_user_cache_block));
+ if (block->size < sizeof(php_user_cache_block)) {
+ return 0;
+ }
+
+ return block->size - (uint32_t) sizeof(php_user_cache_block);
+}
+
+static zend_always_inline bool php_user_cache_block_is_free(const php_user_cache_block *block)
+{
+ return (block->flags & PHP_USER_CACHE_BLOCK_FREE) != 0;
+}
+
+static zend_always_inline php_user_cache_context *php_user_cache_activate_context(php_user_cache_context *context)
+{
+ php_user_cache_context *previous = UC_G(active_context_ptr);
+
+ UC_G(active_context_ptr) = context;
+
+ return previous;
+}
+
+static zend_always_inline void php_user_cache_restore_context(php_user_cache_context *context)
+{
+ UC_G(active_context_ptr) = context;
+}
+
+static zend_always_inline php_user_cache_runtime *php_user_cache_active_runtime(void)
+{
+ return &UC_G(runtime_state);
+}
+
+static zend_always_inline uint64_t php_user_cache_seq_load(const uint64_t *seq)
+{
+#ifdef PHP_USER_CACHE_HAVE_OPTIMISTIC
+ return php_user_cache_atomic_load_64(seq);
+#else
+ return *seq;
+#endif
+}
+
+static zend_always_inline uint64_t php_user_cache_seq_reload(const uint64_t *seq)
+{
+#ifdef PHP_USER_CACHE_HAVE_OPTIMISTIC
+ php_user_cache_atomic_fence_acquire();
+
+ return php_user_cache_atomic_load_64(seq);
+#else
+ return *seq;
+#endif
+}
+
+static zend_always_inline bool php_user_cache_header_is_initialized_locked(void)
+{
+ php_user_cache_header *header = php_user_cache_header_ptr();
+
+ return header != NULL &&
+ header->magic == PHP_USER_CACHE_MAGIC &&
+ header->version == PHP_USER_CACHE_VERSION
+ ;
+}
+
+static zend_always_inline void php_user_cache_bump_mutation_epoch_locked(php_user_cache_header *header)
+{
+ if (header == NULL) {
+ return;
+ }
+
+ header->mutation_epoch++;
+ if (header->mutation_epoch == 0) {
+ header->mutation_epoch = 1;
+ }
+}
+
+static zend_always_inline php_user_cache_entry *php_user_cache_entries_ptr(php_user_cache_header *header)
+{
+ return (php_user_cache_entry *) ((char *) header + sizeof(php_user_cache_header));
+}
+
+static zend_always_inline bool php_user_cache_seen_test_and_add(HashTable *seen, const void *ptr)
+{
+ zend_ulong key = (zend_ulong) (uintptr_t) ptr;
+
+ if (zend_hash_index_exists(seen, key)) {
+ return false;
+ }
+
+ return zend_hash_index_add_empty_element(seen, key) != NULL;
+}
+
+static zend_always_inline void php_user_cache_ensure_ready(void)
+{
+ php_user_cache_context *ctx = php_user_cache_active_context();
+
+ if (UC_G(runtime_resolved) &&
+ UC_G(runtime_resolved_context) == ctx &&
+ UC_G(runtime_resolved_enabled) == UC_G(enable)
+ ) {
+ return;
+ }
+
+ php_user_cache_ensure_ready_slow();
+}
+
+static inline bool php_user_cache_class_overrides_safe_direct_magic_serialize_ex(
+ zend_class_entry *ce,
+ const php_user_cache_safe_direct_handlers *handlers,
+ zend_class_entry *base_ce)
+{
+ if (ce->type != ZEND_USER_CLASS ||
+ ce->__serialize == NULL ||
+ ce->__unserialize == NULL ||
+ (ce->ce_flags & (ZEND_ACC_NOT_SERIALIZABLE|ZEND_ACC_ENUM)) != 0 ||
+ handlers == NULL ||
+ base_ce == ce
+ ) {
+ return false;
+ }
+
+ return
+ (
+ ce->__serialize->type == ZEND_USER_FUNCTION ||
+ ce->__unserialize->type == ZEND_USER_FUNCTION
+ ) &&
+ (
+ ce->__serialize->common.scope != base_ce ||
+ ce->__unserialize->common.scope != base_ce
+ )
+ ;
+}
+
+static inline bool php_user_cache_class_has_safe_direct_state_funcs(zend_class_entry *ce)
+{
+ /* Registered as an all-or-nothing set. */
+ return php_user_cache_safe_direct_find_handlers(ce, NULL) != NULL;
+}
+
+static inline bool php_user_cache_class_magic_route_active(zend_class_entry *ce)
+{
+ const php_user_cache_safe_direct_handlers *handlers;
+ zend_class_entry *base_ce = NULL;
+
+ if (ce->ce_flags & (ZEND_ACC_NOT_SERIALIZABLE|ZEND_ACC_ENUM)) {
+ return false;
+ }
+
+ handlers = php_user_cache_safe_direct_find_handlers(ce, &base_ce);
+
+ if (php_user_cache_class_overrides_safe_direct_magic_serialize_ex(ce, handlers, base_ce)) {
+ return true;
+ }
+
+ return handlers == NULL;
+}
+
+static inline bool php_user_cache_class_uses_magic_serialize(zend_class_entry *ce)
+{
+ return ce->__serialize != NULL && ce->__unserialize != NULL &&
+ php_user_cache_class_magic_route_active(ce)
+ ;
+}
+
+static inline bool php_user_cache_class_uses_serialize_props(zend_class_entry *ce)
+{
+ /* Native serialization restores this state as properties. */
+ return ce->__serialize != NULL && ce->__unserialize == NULL &&
+ php_user_cache_class_magic_route_active(ce)
+ ;
+}
+
+static inline bool php_user_cache_class_uses_magic_unserialize(zend_class_entry *ce)
+{
+ if (ce->__serialize != NULL || ce->__unserialize == NULL) {
+ return false;
+ }
+
+ /* Match native serialization precedence. */
+ if (ce->serialize != NULL && ce->unserialize != NULL) {
+ return false;
+ }
+
+ return php_user_cache_class_magic_route_active(ce);
+}
+
+static inline bool php_user_cache_class_uses_serdes(zend_class_entry *ce)
+{
+ if (ce->ce_flags & (ZEND_ACC_NOT_SERIALIZABLE|ZEND_ACC_ENUM)) {
+ return false;
+ }
+
+ if (php_user_cache_class_uses_magic_serialize(ce) ||
+ php_user_cache_class_uses_magic_unserialize(ce) ||
+ php_user_cache_class_has_safe_direct_state_funcs(ce)
+ ) {
+ return false;
+ }
+
+ if (zend_hash_find_known_hash(&ce->function_table, ZSTR_KNOWN(ZEND_STR_SLEEP)) != NULL ||
+ zend_hash_find_known_hash(&ce->function_table, ZSTR_KNOWN(ZEND_STR_WAKEUP)) != NULL
+ ) {
+ return true;
+ }
+
+ /* Honor native and Serializable handlers. */
+ return ce->serialize != NULL &&
+ ce->unserialize != NULL
+ ;
+}
+
+#endif /* PHP_USER_CACHE_INTERNAL_H */
diff --git a/ext/user_cache/user_cache_serdes.c b/ext/user_cache/user_cache_serdes.c
new file mode 100644
index 000000000000..3b2ab9b20bef
--- /dev/null
+++ b/ext/user_cache/user_cache_serdes.c
@@ -0,0 +1,1661 @@
+/*
+ +----------------------------------------------------------------------+
+ | 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 "user_cache_internal.h"
+
+#include "Zend/zend_closures.h"
+#include "Zend/zend_enum.h"
+#include "Zend/zend_interfaces.h"
+
+/* Position-independent tagged stream using native byte order. */
+#define PHP_USER_CACHE_SERDES_TAG_NULL 1
+#define PHP_USER_CACHE_SERDES_TAG_FALSE 2
+#define PHP_USER_CACHE_SERDES_TAG_TRUE 3
+#define PHP_USER_CACHE_SERDES_TAG_LONG 4
+#define PHP_USER_CACHE_SERDES_TAG_DOUBLE 5
+#define PHP_USER_CACHE_SERDES_TAG_STRING 6
+#define PHP_USER_CACHE_SERDES_TAG_EMPTY_ARRAY 7
+#define PHP_USER_CACHE_SERDES_TAG_PACKED_ARRAY 8
+#define PHP_USER_CACHE_SERDES_TAG_HASHED_ARRAY 9
+#define PHP_USER_CACHE_SERDES_TAG_OBJECT 10
+#define PHP_USER_CACHE_SERDES_TAG_SERIALIZED_OBJECT 11
+#define PHP_USER_CACHE_SERDES_TAG_CUSTOM_OBJECT 12
+#define PHP_USER_CACHE_SERDES_TAG_ENUM 13
+#define PHP_USER_CACHE_SERDES_TAG_REFERENCE 14
+#define PHP_USER_CACHE_SERDES_TAG_BACKREF 15
+
+#define PHP_USER_CACHE_SERDES_KEY_LONG 0
+#define PHP_USER_CACHE_SERDES_KEY_STRING 1
+
+#define PHP_USER_CACHE_SERDES_PROPERTY_INDEX_NONE 0
+
+#define PHP_USER_CACHE_SERDES_STRING_RAW 0
+#define PHP_USER_CACHE_SERDES_STRING_INLINE 1
+#define PHP_USER_CACHE_SERDES_STRING_REF 2
+
+#define PHP_USER_CACHE_SERDES_INLINE_IDS 8
+#define PHP_USER_CACHE_SERDES_INLINE_STRINGS 32
+
+typedef struct {
+ smart_str buf;
+ HashTable seen;
+ HashTable strings;
+ uint32_t string_count;
+ const char *failure_message;
+} php_user_cache_serdes_encoder;
+
+typedef struct {
+ const uint8_t *data;
+ size_t len;
+ size_t pos;
+ zval *ids;
+ uint32_t id_count;
+ uint32_t id_capacity;
+ zend_string **strings;
+ uint32_t string_count;
+ uint32_t string_capacity;
+ zval inline_ids[PHP_USER_CACHE_SERDES_INLINE_IDS];
+ zend_string *inline_strings[PHP_USER_CACHE_SERDES_INLINE_STRINGS];
+} php_user_cache_serdes_decoder;
+
+static bool user_cache_serdes_encode_value(
+ php_user_cache_serdes_encoder *enc,
+ zval *value
+);
+
+static bool user_cache_serdes_decode_value(
+ php_user_cache_serdes_decoder *dec,
+ zval *dst
+);
+
+static bool user_cache_serdes_decoder_register_string(
+ php_user_cache_serdes_decoder *dec,
+ zend_string *str
+);
+
+static zend_always_inline void user_cache_serdes_put_pad(smart_str *buf, size_t align)
+{
+ size_t len = smart_str_get_len(buf), pad = (align - (len & (align - 1))) & (align - 1);
+
+ if (pad != 0) {
+ memset(smart_str_extend(buf, pad), 0, pad);
+ }
+}
+
+static zend_always_inline void user_cache_serdes_put_u8(smart_str *buf, uint8_t value)
+{
+ smart_str_appendc(buf, (char) value);
+}
+
+static zend_always_inline void user_cache_serdes_put_fixed(smart_str *buf, const void *value, size_t size)
+{
+ user_cache_serdes_put_pad(buf, size);
+ smart_str_appendl(buf, (const char *) value, size);
+}
+
+static zend_always_inline void user_cache_serdes_put_u32(smart_str *buf, uint32_t value)
+{
+ user_cache_serdes_put_fixed(buf, &value, sizeof(value));
+}
+
+static zend_always_inline void user_cache_serdes_put_u64(smart_str *buf, uint64_t value)
+{
+ user_cache_serdes_put_fixed(buf, &value, sizeof(value));
+}
+
+static zend_always_inline void user_cache_serdes_put_long(smart_str *buf, zend_long value)
+{
+ user_cache_serdes_put_fixed(buf, &value, sizeof(value));
+}
+
+static zend_always_inline void user_cache_serdes_put_double(smart_str *buf, double value)
+{
+ user_cache_serdes_put_fixed(buf, &value, sizeof(value));
+}
+
+static zend_always_inline size_t user_cache_serdes_reserve_u32(smart_str *buf)
+{
+ user_cache_serdes_put_pad(buf, sizeof(uint32_t));
+ memset(smart_str_extend(buf, sizeof(uint32_t)), 0, sizeof(uint32_t));
+
+ return smart_str_get_len(buf) - sizeof(uint32_t);
+}
+
+static zend_always_inline void user_cache_serdes_patch_u32(
+ smart_str *buf,
+ size_t pos,
+ uint32_t value)
+{
+ ZEND_ASSERT(buf->s != NULL && pos + sizeof(uint32_t) <= ZSTR_LEN(buf->s));
+
+ memcpy(ZSTR_VAL(buf->s) + pos, &value, sizeof(value));
+}
+
+static zend_always_inline bool user_cache_serdes_encode_backref_or_register(
+ php_user_cache_serdes_encoder *enc,
+ const void *container,
+ bool *is_backref)
+{
+ zval *slot;
+ uintptr_t id;
+
+ slot = zend_hash_index_lookup(&enc->seen, (zend_ulong) (uintptr_t) container);
+
+ if (Z_TYPE_P(slot) == IS_PTR) {
+ *is_backref = true;
+
+ user_cache_serdes_put_u8(&enc->buf, PHP_USER_CACHE_SERDES_TAG_BACKREF);
+ user_cache_serdes_put_u32(&enc->buf, (uint32_t) ((uintptr_t) Z_PTR_P(slot) - 1));
+
+ return true;
+ }
+
+ *is_backref = false;
+ id = (uintptr_t) zend_hash_num_elements(&enc->seen);
+
+ if (id > UINT32_MAX) {
+ enc->failure_message = "value contains too many arrays or objects to be stored in the user cache";
+
+ return false;
+ }
+
+ ZVAL_PTR(slot, (void *) id);
+
+ return true;
+}
+
+static zend_always_inline bool user_cache_serdes_skip_pad(
+ php_user_cache_serdes_decoder *dec,
+ size_t align)
+{
+ size_t pad = (align - (dec->pos & (align - 1))) & (align - 1);
+
+ if (pad > dec->len - dec->pos) {
+ return false;
+ }
+
+ dec->pos += pad;
+
+ return true;
+}
+
+static zend_always_inline bool user_cache_serdes_get_u8(
+ php_user_cache_serdes_decoder *dec,
+ uint8_t *value)
+{
+ if (dec->pos >= dec->len) {
+ return false;
+ }
+
+ *value = dec->data[dec->pos++];
+
+ return true;
+}
+
+static zend_always_inline bool user_cache_serdes_get_fixed(
+ php_user_cache_serdes_decoder *dec,
+ void *value,
+ size_t size)
+{
+ if (!user_cache_serdes_skip_pad(dec, size) ||
+ size > dec->len - dec->pos
+ ) {
+ return false;
+ }
+
+ memcpy(value, dec->data + dec->pos, size);
+ dec->pos += size;
+
+ return true;
+}
+
+static zend_always_inline bool user_cache_serdes_get_u32(
+ php_user_cache_serdes_decoder *dec,
+ uint32_t *value)
+{
+ return user_cache_serdes_get_fixed(dec, value, sizeof(*value));
+}
+
+static zend_always_inline bool user_cache_serdes_get_u64(
+ php_user_cache_serdes_decoder *dec,
+ uint64_t *value)
+{
+ return user_cache_serdes_get_fixed(dec, value, sizeof(*value));
+}
+
+static zend_always_inline bool user_cache_serdes_get_long(
+ php_user_cache_serdes_decoder *dec,
+ zend_long *value)
+{
+ return user_cache_serdes_get_fixed(dec, value, sizeof(*value));
+}
+
+static zend_always_inline bool user_cache_serdes_get_double(
+ php_user_cache_serdes_decoder *dec,
+ double *value)
+{
+ return user_cache_serdes_get_fixed(dec, value, sizeof(*value));
+}
+
+static zend_always_inline bool user_cache_serdes_get_bytes(
+ php_user_cache_serdes_decoder *dec,
+ size_t len,
+ const uint8_t **bytes)
+{
+ if (len > dec->len - dec->pos) {
+ return false;
+ }
+
+ *bytes = dec->data + dec->pos;
+ dec->pos += len;
+
+ return true;
+}
+
+static zend_always_inline bool user_cache_serdes_get_str(
+ php_user_cache_serdes_decoder *dec,
+ zend_string **value)
+{
+ const uint8_t *bytes;
+ zend_string *str;
+ uint32_t byte_len, string_id;
+ uint8_t kind;
+
+ if (!user_cache_serdes_get_u8(dec, &kind)) {
+ return false;
+ }
+
+ if (kind == PHP_USER_CACHE_SERDES_STRING_REF) {
+ if (!user_cache_serdes_get_u32(dec, &string_id) ||
+ string_id >= dec->string_count
+ ) {
+ return false;
+ }
+
+ *value = zend_string_copy(dec->strings[string_id]);
+
+ return true;
+ }
+
+ if ((kind != PHP_USER_CACHE_SERDES_STRING_INLINE &&
+ kind != PHP_USER_CACHE_SERDES_STRING_RAW) ||
+ !user_cache_serdes_get_u32(dec, &byte_len) ||
+ !user_cache_serdes_get_bytes(dec, byte_len, &bytes)
+ ) {
+ return false;
+ }
+
+ str = zend_string_init((const char *) bytes, byte_len, 0);
+ if (kind == PHP_USER_CACHE_SERDES_STRING_RAW) {
+ *value = str;
+
+ return true;
+ }
+
+ if (!user_cache_serdes_decoder_register_string(dec, str)) {
+ zend_string_release(str);
+
+ return false;
+ }
+
+ *value = zend_string_copy(str);
+
+ return true;
+}
+
+static bool user_cache_serdes_put_str(
+ php_user_cache_serdes_encoder *enc,
+ const char *value,
+ size_t len)
+{
+ zval *string_id, new_string_id;
+ uint32_t id;
+
+ string_id = zend_hash_str_find(&enc->strings, value, len);
+ if (string_id != NULL) {
+ if (Z_LVAL_P(string_id) == 0) {
+ if (enc->string_count >= UINT32_MAX) {
+ enc->failure_message = "value is too large to be stored in the user cache";
+
+ return false;
+ }
+ id = enc->string_count++;
+
+ ZVAL_LONG(string_id, (zend_long) id + 1);
+
+ user_cache_serdes_put_u8(&enc->buf, PHP_USER_CACHE_SERDES_STRING_INLINE);
+ user_cache_serdes_put_u32(&enc->buf, (uint32_t) len);
+ if (len != 0) {
+ smart_str_appendl(&enc->buf, value, len);
+ }
+
+ return true;
+ }
+
+ id = (uint32_t) (Z_LVAL_P(string_id) - 1);
+ user_cache_serdes_put_u8(&enc->buf, PHP_USER_CACHE_SERDES_STRING_REF);
+ user_cache_serdes_put_u32(&enc->buf, id);
+
+ return true;
+ }
+
+ if (len > UINT32_MAX) {
+ enc->failure_message = "value is too large to be stored in the user cache";
+
+ return false;
+ }
+
+ ZVAL_LONG(&new_string_id, 0);
+ if (zend_hash_str_add_new(&enc->strings, value, len, &new_string_id) == NULL) {
+ return false;
+ }
+
+ user_cache_serdes_put_u8(&enc->buf, PHP_USER_CACHE_SERDES_STRING_RAW);
+ user_cache_serdes_put_u32(&enc->buf, (uint32_t) len);
+ if (len != 0) {
+ smart_str_appendl(&enc->buf, value, len);
+ }
+
+ return true;
+}
+
+static zend_result user_cache_serdes_try_add_sleep_prop(
+ zval *obj_zv,
+ HashTable *props,
+ zend_string *name,
+ zend_string *error_name,
+ HashTable *ht)
+{
+ zval *val = zend_hash_find(props, name);
+
+ if (val == NULL) {
+ return FAILURE;
+ }
+
+ if (Z_TYPE_P(val) == IS_INDIRECT) {
+ val = Z_INDIRECT_P(val);
+ if (Z_TYPE_P(val) == IS_UNDEF) {
+ if (zend_get_typed_property_info_for_slot(Z_OBJ_P(obj_zv), val) != NULL) {
+ return SUCCESS;
+ }
+
+ return FAILURE;
+ }
+ }
+
+ if (!zend_hash_add(ht, name, val)) {
+ php_error_docref(NULL, E_WARNING,
+ "\"%s\" is returned from __sleep() multiple times",
+ ZSTR_VAL(error_name)
+ );
+
+ return SUCCESS;
+ }
+
+ Z_TRY_ADDREF_P(val);
+
+ return SUCCESS;
+}
+
+static bool user_cache_serdes_get_sleep_props(
+ zval *obj_zv,
+ HashTable *sleep_retval,
+ zval *state)
+{
+ zend_result added;
+ zend_string *name, *tmp_name, *candidate;
+ zend_class_entry *ce = Z_OBJCE_P(obj_zv);
+ zval *name_val;
+ HashTable *props = zend_get_properties_for(obj_zv, ZEND_PROP_PURPOSE_SERIALIZE);
+ bool result = true;
+
+ array_init_size(state, zend_hash_num_elements(sleep_retval));
+
+ ZEND_HASH_FOREACH_VAL_IND(sleep_retval, name_val) {
+ ZVAL_DEREF(name_val);
+ if (Z_TYPE_P(name_val) != IS_STRING) {
+ php_error_docref(NULL, E_WARNING,
+ "%s::__sleep() should return an array only containing the names of instance-variables to serialize",
+ ZSTR_VAL(ce->name)
+ );
+ }
+
+ name = zval_get_tmp_string(name_val, &tmp_name);
+
+ /* Try unmangled, private and protected names in order. */
+ added = user_cache_serdes_try_add_sleep_prop(obj_zv, props, name, name, Z_ARRVAL_P(state));
+
+ if (added == FAILURE && !EG(exception)) {
+ candidate = zend_mangle_property_name(
+ ZSTR_VAL(ce->name), ZSTR_LEN(ce->name),
+ ZSTR_VAL(name), ZSTR_LEN(name), ce->type == ZEND_INTERNAL_CLASS
+ );
+
+ added = user_cache_serdes_try_add_sleep_prop(obj_zv, props, candidate, name, Z_ARRVAL_P(state));
+
+ zend_string_release(candidate);
+ }
+
+ if (added == FAILURE && !EG(exception)) {
+ candidate = zend_mangle_property_name(
+ "*", 1, ZSTR_VAL(name), ZSTR_LEN(name), ce->type == ZEND_INTERNAL_CLASS
+ );
+
+ added = user_cache_serdes_try_add_sleep_prop(obj_zv, props, candidate, name, Z_ARRVAL_P(state));
+
+ zend_string_release(candidate);
+ }
+
+ if (added == FAILURE) {
+ if (EG(exception)) {
+ zend_tmp_string_release(tmp_name);
+ result = false;
+
+ break;
+ }
+
+ php_error_docref(
+ NULL,
+ E_WARNING,
+ "\"%s\" returned as member variable from __sleep() but does not exist",
+ ZSTR_VAL(name)
+ );
+ }
+
+ zend_tmp_string_release(tmp_name);
+ } ZEND_HASH_FOREACH_END();
+
+ zend_release_properties(props);
+
+ return result;
+}
+
+static bool user_cache_serdes_encode_property(
+ php_user_cache_serdes_encoder *enc,
+ zend_class_entry *ce,
+ zend_string *name,
+ zval *value)
+{
+ if (!user_cache_serdes_put_str(enc, ZSTR_VAL(name), ZSTR_LEN(name))) {
+ return false;
+ }
+
+ user_cache_serdes_put_u32(
+ &enc->buf,
+ php_user_cache_serdes_declared_property_index_plus_one(ce, name)
+ );
+
+ return user_cache_serdes_encode_value(enc, value);
+}
+
+static bool user_cache_serdes_encode_property_table(
+ php_user_cache_serdes_encoder *enc,
+ zend_class_entry *ce,
+ HashTable *props,
+ size_t count_pos)
+{
+ zend_ulong h;
+ zend_string *name;
+ zval *value;
+ uint32_t count = 0;
+ size_t digits_len;
+ char numeric_key_buf[32], *digits;
+
+ ZEND_HASH_FOREACH_KEY_VAL(props, h, name, value) {
+ if (Z_TYPE_P(value) == IS_INDIRECT) {
+ value = Z_INDIRECT_P(value);
+ }
+
+ if (Z_TYPE_P(value) == IS_UNDEF) {
+ continue;
+ }
+
+ if (name != NULL) {
+ if (!user_cache_serdes_encode_property(enc, ce, name, value)) {
+ return false;
+ }
+ } else {
+ digits = zend_print_long_to_buf(numeric_key_buf + sizeof(numeric_key_buf) - 1, (zend_long) h);
+ digits_len = numeric_key_buf + sizeof(numeric_key_buf) - 1 - digits;
+
+ if (!user_cache_serdes_put_str(enc, digits, digits_len)) {
+ return false;
+ }
+
+ user_cache_serdes_put_u32(&enc->buf, PHP_USER_CACHE_SERDES_PROPERTY_INDEX_NONE);
+
+ if (!user_cache_serdes_encode_value(enc, value)) {
+ return false;
+ }
+ }
+
+ count++;
+ } ZEND_HASH_FOREACH_END();
+
+ user_cache_serdes_patch_u32(&enc->buf, count_pos, count);
+
+ return true;
+}
+
+static bool user_cache_serdes_encode_serialized_object(
+ php_user_cache_serdes_encoder *enc,
+ zval *value)
+{
+ zend_object *obj = Z_OBJ_P(value);
+ zend_class_entry *ce = obj->ce;
+ zend_string *class_name = ce->name;
+ zval retval;
+ bool result;
+
+ user_cache_serdes_put_u8(&enc->buf, PHP_USER_CACHE_SERDES_TAG_SERIALIZED_OBJECT);
+ if (!user_cache_serdes_put_str(enc, ZSTR_VAL(class_name), ZSTR_LEN(class_name))) {
+ return false;
+ }
+
+ if (!php_user_cache_serdes_call_magic_serialize(obj, &retval)) {
+ return false;
+ }
+
+ result = user_cache_serdes_encode_value(enc, &retval);
+
+ zval_ptr_dtor(&retval);
+
+ return result;
+}
+
+static bool user_cache_serdes_encode_custom_object(
+ php_user_cache_serdes_encoder *enc,
+ zval *value)
+{
+ zend_class_entry *ce = Z_OBJCE_P(value);
+ zend_string *class_name = ce->name;
+ php_serialize_data_t ser_data;
+ uint8_t *ser_buf = NULL;
+ size_t ser_len = 0;
+ bool result;
+
+ if (ce->serialize == NULL || ce->unserialize == NULL) {
+ enc->failure_message = PHP_USER_CACHE_MSG_OPAQUE_OBJECT_UNSTORABLE;
+
+ return false;
+ }
+
+ PHP_VAR_SERIALIZE_INIT(ser_data);
+ result = ce->serialize(value, &ser_buf, &ser_len, (zend_serialize_data *) ser_data) == SUCCESS;
+ PHP_VAR_SERIALIZE_DESTROY(ser_data);
+
+ if (!result || ser_len > UINT32_MAX) {
+ if (ser_buf != NULL) {
+ efree(ser_buf);
+ }
+
+ enc->failure_message = "the object could not be serialized for the user cache";
+
+ return false;
+ }
+
+ user_cache_serdes_put_u8(&enc->buf, PHP_USER_CACHE_SERDES_TAG_CUSTOM_OBJECT);
+ if (!user_cache_serdes_put_str(enc, ZSTR_VAL(class_name), ZSTR_LEN(class_name))) {
+ if (ser_buf != NULL) {
+ efree(ser_buf);
+ }
+
+ return false;
+ }
+
+ user_cache_serdes_put_u32(&enc->buf, (uint32_t) ser_len);
+ if (ser_len != 0) {
+ smart_str_appendl(&enc->buf, (const char *) ser_buf, ser_len);
+ }
+
+ if (ser_buf != NULL) {
+ efree(ser_buf);
+ }
+
+ return true;
+}
+
+static bool user_cache_serdes_put_object_header(
+ php_user_cache_serdes_encoder *enc,
+ zend_string *class_name,
+ bool has_wakeup,
+ size_t *count_pos)
+{
+ user_cache_serdes_put_u8(&enc->buf, PHP_USER_CACHE_SERDES_TAG_OBJECT);
+
+ if (!user_cache_serdes_put_str(enc, ZSTR_VAL(class_name), ZSTR_LEN(class_name))) {
+ return false;
+ }
+
+ user_cache_serdes_put_u8(&enc->buf, has_wakeup ? 1 : 0);
+
+ *count_pos = user_cache_serdes_reserve_u32(&enc->buf);
+
+ return true;
+}
+
+static bool user_cache_serdes_encode_sleep_object(
+ php_user_cache_serdes_encoder *enc,
+ zval *value,
+ bool has_wakeup)
+{
+ zend_class_entry *ce = Z_OBJCE_P(value);
+ zend_string *class_name = ce->name;
+ zval sleep_state;
+ size_t count_pos;
+ bool result;
+
+ if (!user_cache_serdes_put_object_header(enc, class_name, has_wakeup, &count_pos)) {
+ return false;
+ }
+
+ if (!php_user_cache_serdes_get_sleep_state(value, &sleep_state, &enc->failure_message)) {
+ return false;
+ }
+
+ result = user_cache_serdes_encode_property_table(enc, ce, Z_ARRVAL(sleep_state), count_pos);
+
+ zval_ptr_dtor(&sleep_state);
+
+ return result;
+}
+
+static bool user_cache_serdes_encode_plain_object(
+ php_user_cache_serdes_encoder *enc,
+ zval *value,
+ bool has_wakeup)
+{
+ zend_class_entry *ce = Z_OBJCE_P(value);
+ zend_string *class_name = ce->name;
+ HashTable *props;
+ size_t count_pos;
+ bool result;
+
+ if (ce->type != ZEND_USER_CLASS && ce->create_object != NULL && !has_wakeup) {
+ enc->failure_message = PHP_USER_CACHE_MSG_OPAQUE_OBJECT_UNSTORABLE;
+
+ return false;
+ }
+
+ if (!user_cache_serdes_put_object_header(enc, class_name, has_wakeup, &count_pos)) {
+ return false;
+ }
+
+ props = zend_get_properties_for(value, ZEND_PROP_PURPOSE_SERIALIZE);
+ if (props == NULL) {
+ user_cache_serdes_patch_u32(&enc->buf, count_pos, 0);
+
+ return true;
+ }
+
+ result = user_cache_serdes_encode_property_table(enc, ce, props, count_pos);
+
+ zend_release_properties(props);
+
+ return result;
+}
+
+static bool user_cache_serdes_encode_object(
+ php_user_cache_serdes_encoder *enc,
+ zval *value)
+{
+ zend_object *obj = Z_OBJ_P(value);
+ zend_class_entry *ce = obj->ce;
+ zend_string *class_name = ce->name, *case_name;
+ bool has_sleep, has_wakeup, is_backref;
+
+ if (ce == zend_ce_closure) {
+ enc->failure_message = PHP_USER_CACHE_MSG_CLOSURE_UNSTORABLE;
+
+ return false;
+ }
+
+ if (zend_object_is_lazy(obj)) {
+ enc->failure_message = PHP_USER_CACHE_MSG_LAZY_OBJECT_UNSTORABLE;
+
+ return false;
+ }
+
+ if (ce->ce_flags & ZEND_ACC_ENUM) {
+ user_cache_serdes_put_u8(&enc->buf, PHP_USER_CACHE_SERDES_TAG_ENUM);
+
+ if (!user_cache_serdes_put_str(enc, ZSTR_VAL(class_name), ZSTR_LEN(class_name))) {
+ return false;
+ }
+
+ case_name = Z_STR_P(zend_enum_fetch_case_name(obj));
+ if (!user_cache_serdes_put_str(enc, ZSTR_VAL(case_name), ZSTR_LEN(case_name))) {
+ return false;
+ }
+
+ return true;
+ }
+
+ if (ce->ce_flags & ZEND_ACC_NOT_SERIALIZABLE) {
+ enc->failure_message = PHP_USER_CACHE_MSG_OPAQUE_OBJECT_UNSTORABLE;
+
+ return false;
+ }
+
+ if (!user_cache_serdes_encode_backref_or_register(enc, obj, &is_backref)) {
+ return false;
+ }
+
+ if (is_backref) {
+ return true;
+ }
+
+ if (ce->__serialize != NULL && ce->__unserialize != NULL) {
+ return user_cache_serdes_encode_serialized_object(enc, value);
+ }
+
+ /* Match native serialization precedence. */
+ if (ce->serialize != NULL || ce->unserialize != NULL) {
+ return user_cache_serdes_encode_custom_object(enc, value);
+ }
+
+ has_sleep = zend_hash_find_known_hash(&ce->function_table, ZSTR_KNOWN(ZEND_STR_SLEEP)) != NULL;
+ has_wakeup = zend_hash_find_known_hash(&ce->function_table, ZSTR_KNOWN(ZEND_STR_WAKEUP)) != NULL;
+
+ if (has_sleep) {
+ return user_cache_serdes_encode_sleep_object(enc, value, has_wakeup);
+ }
+
+ return user_cache_serdes_encode_plain_object(enc, value, has_wakeup);
+}
+
+static bool user_cache_serdes_encode_array(
+ php_user_cache_serdes_encoder *enc,
+ zval *value)
+{
+ zend_string *key;
+ zend_ulong h;
+ zval *elem;
+ HashTable *arr = Z_ARRVAL_P(value);
+ uint32_t count = zend_hash_num_elements(arr);
+ bool is_backref;
+
+ if (count == 0 && arr->nNextFreeElement == 0) {
+ user_cache_serdes_put_u8(&enc->buf, PHP_USER_CACHE_SERDES_TAG_EMPTY_ARRAY);
+
+ return true;
+ }
+
+ if (!user_cache_serdes_encode_backref_or_register(enc, arr, &is_backref)) {
+ return false;
+ }
+
+ if (is_backref) {
+ return true;
+ }
+
+ if (HT_IS_PACKED(arr) && arr->nNumUsed == count) {
+ user_cache_serdes_put_u8(&enc->buf, PHP_USER_CACHE_SERDES_TAG_PACKED_ARRAY);
+ user_cache_serdes_put_u64(&enc->buf, (uint64_t) arr->nNextFreeElement);
+ user_cache_serdes_put_u32(&enc->buf, count);
+
+ ZEND_HASH_PACKED_FOREACH_VAL(arr, elem) {
+ if (!user_cache_serdes_encode_value(enc, elem)) {
+ return false;
+ }
+ } ZEND_HASH_FOREACH_END();
+
+ return true;
+ }
+
+ user_cache_serdes_put_u8(&enc->buf, PHP_USER_CACHE_SERDES_TAG_HASHED_ARRAY);
+ user_cache_serdes_put_u64(&enc->buf, (uint64_t) arr->nNextFreeElement);
+ user_cache_serdes_put_u32(&enc->buf, count);
+
+ ZEND_HASH_FOREACH_KEY_VAL(arr, h, key, elem) {
+ if (key != NULL) {
+ user_cache_serdes_put_u8(&enc->buf, PHP_USER_CACHE_SERDES_KEY_STRING);
+ if (!user_cache_serdes_put_str(enc, ZSTR_VAL(key), ZSTR_LEN(key))) {
+ return false;
+ }
+ } else {
+ user_cache_serdes_put_u8(&enc->buf, PHP_USER_CACHE_SERDES_KEY_LONG);
+ user_cache_serdes_put_u64(&enc->buf, (uint64_t) h);
+ }
+
+ if (!user_cache_serdes_encode_value(enc, elem)) {
+ return false;
+ }
+ } ZEND_HASH_FOREACH_END();
+
+ return true;
+}
+
+static bool user_cache_serdes_encode_value(
+ php_user_cache_serdes_encoder *enc,
+ zval *value)
+{
+ zend_reference *ref;
+ bool is_backref;
+
+ if (php_user_cache_stack_overflowed()) {
+ enc->failure_message = "value is nested too deeply to be stored in the user cache";
+
+ return false;
+ }
+
+ switch (Z_TYPE_P(value)) {
+ case IS_UNDEF:
+ case IS_NULL:
+ user_cache_serdes_put_u8(&enc->buf, PHP_USER_CACHE_SERDES_TAG_NULL);
+
+ return true;
+ case IS_FALSE:
+ user_cache_serdes_put_u8(&enc->buf, PHP_USER_CACHE_SERDES_TAG_FALSE);
+
+ return true;
+ case IS_TRUE:
+ user_cache_serdes_put_u8(&enc->buf, PHP_USER_CACHE_SERDES_TAG_TRUE);
+
+ return true;
+ case IS_LONG:
+ user_cache_serdes_put_u8(&enc->buf, PHP_USER_CACHE_SERDES_TAG_LONG);
+ user_cache_serdes_put_long(&enc->buf, Z_LVAL_P(value));
+
+ return true;
+ case IS_DOUBLE:
+ user_cache_serdes_put_u8(&enc->buf, PHP_USER_CACHE_SERDES_TAG_DOUBLE);
+ user_cache_serdes_put_double(&enc->buf, Z_DVAL_P(value));
+
+ return true;
+ case IS_STRING:
+ user_cache_serdes_put_u8(&enc->buf, PHP_USER_CACHE_SERDES_TAG_STRING);
+ if (!user_cache_serdes_put_str(enc, Z_STRVAL_P(value), Z_STRLEN_P(value))) {
+ return false;
+ }
+
+ return true;
+ case IS_ARRAY:
+ return user_cache_serdes_encode_array(enc, value);
+ case IS_OBJECT:
+ return user_cache_serdes_encode_object(enc, value);
+ case IS_REFERENCE:
+ ref = Z_REF_P(value);
+
+ if (!user_cache_serdes_encode_backref_or_register(enc, ref, &is_backref)) {
+ return false;
+ }
+
+ if (is_backref) {
+ return true;
+ }
+
+ user_cache_serdes_put_u8(&enc->buf, PHP_USER_CACHE_SERDES_TAG_REFERENCE);
+
+ return user_cache_serdes_encode_value(enc, &ref->val);
+ case IS_RESOURCE:
+ enc->failure_message = PHP_USER_CACHE_MSG_RESOURCE_UNSTORABLE;
+
+ return false;
+ default:
+ enc->failure_message = "value cannot be stored in the user cache";
+
+ return false;
+ }
+}
+
+static void *user_cache_serdes_registry_reserve(
+ void *items,
+ void *inline_items,
+ uint32_t count,
+ uint32_t *cap,
+ size_t elem_size)
+{
+ void *new_items;
+ uint32_t new_cap;
+
+ if (count != *cap) {
+ return items;
+ }
+
+ if (*cap > UINT32_MAX / 2) {
+ return NULL;
+ }
+
+ new_cap = *cap * 2;
+ if (items == inline_items) {
+ new_items = safe_emalloc(new_cap, elem_size, 0);
+ memcpy(new_items, inline_items, count * elem_size);
+ } else {
+ new_items = safe_erealloc(items, new_cap, elem_size, 0);
+ }
+
+ *cap = new_cap;
+
+ return new_items;
+}
+
+static bool user_cache_serdes_decoder_register_string(
+ php_user_cache_serdes_decoder *dec,
+ zend_string *str)
+{
+ zend_string **strings;
+
+ if (dec->string_count == UINT32_MAX) {
+ return false;
+ }
+
+ strings = user_cache_serdes_registry_reserve(
+ dec->strings, dec->inline_strings, dec->string_count,
+ &dec->string_capacity, sizeof(zend_string *)
+ );
+ if (strings == NULL) {
+ return false;
+ }
+
+ dec->strings = strings;
+ dec->strings[dec->string_count] = str;
+ dec->string_count++;
+
+ return true;
+}
+
+static bool user_cache_serdes_decoder_register(
+ php_user_cache_serdes_decoder *dec,
+ zval *container)
+{
+ zval *ids;
+
+ if (dec->id_count == UINT32_MAX) {
+ return false;
+ }
+
+ ids = user_cache_serdes_registry_reserve(
+ dec->ids, dec->inline_ids, dec->id_count,
+ &dec->id_capacity, sizeof(zval)
+ );
+ if (ids == NULL) {
+ return false;
+ }
+
+ dec->ids = ids;
+ ZVAL_COPY(&dec->ids[dec->id_count], container);
+ dec->id_count++;
+
+ return true;
+}
+
+static zend_class_entry *user_cache_serdes_decode_class(
+ php_user_cache_serdes_decoder *dec)
+{
+ zend_string *class_name;
+ zend_class_entry *ce;
+
+ if (!user_cache_serdes_get_str(dec, &class_name)) {
+ return NULL;
+ }
+
+ ce = zend_lookup_class(class_name);
+ zend_string_release(class_name);
+
+ return ce;
+}
+
+static bool user_cache_serdes_decode_object_properties(
+ php_user_cache_serdes_decoder *dec,
+ bool call_wakeup,
+ zval *dst)
+{
+ zend_class_entry *ce = Z_OBJCE_P(dst);
+ zend_string *prop_name;
+ zval *wakeup_zv, prop_val, retval;
+ uint32_t count, i, prop_idx_plus_one;
+
+ if (!user_cache_serdes_get_u32(dec, &count)) {
+ return false;
+ }
+
+ for (i = 0; i < count; i++) {
+ if (!user_cache_serdes_get_str(dec, &prop_name)) {
+ return false;
+ }
+
+ if (!user_cache_serdes_get_u32(dec, &prop_idx_plus_one)) {
+ zend_string_release(prop_name);
+
+ return false;
+ }
+
+ ZVAL_UNDEF(&prop_val);
+
+ if (!user_cache_serdes_decode_value(dec, &prop_val)) {
+ zval_ptr_dtor(&prop_val);
+ zend_string_release(prop_name);
+
+ return false;
+ }
+
+ if (prop_idx_plus_one != PHP_USER_CACHE_SERDES_PROPERTY_INDEX_NONE) {
+ if (!php_user_cache_shared_graph_update_object_property_at(
+ dst,
+ prop_name,
+ prop_idx_plus_one - 1,
+ &prop_val
+ )
+ ) {
+ zval_ptr_dtor(&prop_val);
+ zend_string_release(prop_name);
+
+ return false;
+ }
+ } else if (!php_user_cache_shared_graph_update_object_property(
+ dst,
+ prop_name,
+ &prop_val
+ )
+ ) {
+ zval_ptr_dtor(&prop_val);
+
+ zend_string_release(prop_name);
+
+ return false;
+ }
+
+ zval_ptr_dtor(&prop_val);
+
+ zend_string_release(prop_name);
+ }
+
+ if (call_wakeup) {
+ wakeup_zv = zend_hash_find_known_hash(&ce->function_table, ZSTR_KNOWN(ZEND_STR_WAKEUP));
+ if (wakeup_zv != NULL) {
+ zend_call_known_instance_method(Z_FUNC_P(wakeup_zv), Z_OBJ_P(dst), &retval, 0, NULL);
+
+ zval_ptr_dtor(&retval);
+
+ if (EG(exception)) {
+ return false;
+ }
+ }
+ }
+
+ return true;
+}
+
+static bool user_cache_serdes_decode_array(
+ php_user_cache_serdes_decoder *dec,
+ uint8_t tag,
+ zval *dst)
+{
+ zend_string *key = NULL;
+ zval elem;
+ uint64_t next_free, key_h = 0;
+ uint32_t count, i;
+ uint8_t key_kind;
+ bool result;
+
+ if (!user_cache_serdes_get_u64(dec, &next_free) ||
+ !user_cache_serdes_get_u32(dec, &count)
+ ) {
+ return false;
+ }
+
+ /* Reject impossible counts before allocating. */
+ if (count > dec->len - dec->pos) {
+ return false;
+ }
+
+ array_init_size(dst, count);
+
+ /* Initialize before registering the array: registration raises its refcount,
+ * but the first insertion requires a refcount of one. */
+ if (tag == PHP_USER_CACHE_SERDES_TAG_PACKED_ARRAY) {
+ zend_hash_real_init_packed(Z_ARRVAL_P(dst));
+ } else {
+ zend_hash_real_init_mixed(Z_ARRVAL_P(dst));
+ }
+
+ HT_ALLOW_COW_VIOLATION(Z_ARRVAL_P(dst));
+
+ if (!user_cache_serdes_decoder_register(dec, dst)) {
+ zval_ptr_dtor(dst);
+ ZVAL_UNDEF(dst);
+
+ return false;
+ }
+
+ for (i = 0; i < count; i++) {
+ key = NULL;
+ if (tag == PHP_USER_CACHE_SERDES_TAG_HASHED_ARRAY) {
+ if (!user_cache_serdes_get_u8(dec, &key_kind)) {
+ goto failure;
+ }
+
+ if (key_kind == PHP_USER_CACHE_SERDES_KEY_STRING) {
+ if (!user_cache_serdes_get_str(dec, &key)) {
+ goto failure;
+ }
+ } else if (key_kind == PHP_USER_CACHE_SERDES_KEY_LONG) {
+ if (!user_cache_serdes_get_u64(dec, &key_h)) {
+ goto failure;
+ }
+ } else {
+ goto failure;
+ }
+ }
+
+ ZVAL_UNDEF(&elem);
+ if (!user_cache_serdes_decode_value(dec, &elem)) {
+ zval_ptr_dtor(&elem);
+ if (key != NULL) {
+ zend_string_release(key);
+ }
+
+ goto failure;
+ }
+
+ if (key != NULL) {
+ result = zend_hash_add_new(Z_ARRVAL_P(dst), key, &elem) != NULL;
+ zend_string_release(key);
+ } else if (tag == PHP_USER_CACHE_SERDES_TAG_HASHED_ARRAY) {
+ result = zend_hash_index_add_new(Z_ARRVAL_P(dst), (zend_ulong) key_h, &elem) != NULL;
+ } else {
+ result = zend_hash_next_index_insert_new(Z_ARRVAL_P(dst), &elem) != NULL;
+ }
+
+ if (!result) {
+ zval_ptr_dtor(&elem);
+
+ goto failure;
+ }
+ }
+
+ Z_ARRVAL_P(dst)->nNextFreeElement = (zend_long) next_free;
+ PHP_USER_CACHE_HT_DISALLOW_COW_VIOLATION(Z_ARRVAL_P(dst));
+ return true;
+
+failure:
+ zval_ptr_dtor(dst);
+ ZVAL_UNDEF(dst);
+
+ return false;
+}
+
+static bool user_cache_serdes_decode_object(
+ php_user_cache_serdes_decoder *dec,
+ zval *dst)
+{
+ zend_class_entry *ce;
+ uint8_t call_wakeup;
+
+ ce = user_cache_serdes_decode_class(dec);
+ if (ce == NULL ||
+ (ce->ce_flags & (ZEND_ACC_NOT_SERIALIZABLE|ZEND_ACC_ENUM)) != 0 ||
+ object_init_ex(dst, ce) != SUCCESS
+ ) {
+ return false;
+ }
+
+ if (!user_cache_serdes_get_u8(dec, &call_wakeup) ||
+ !user_cache_serdes_decoder_register(dec, dst) ||
+ !user_cache_serdes_decode_object_properties(dec, call_wakeup != 0, dst)
+ ) {
+ zval_ptr_dtor(dst);
+
+ ZVAL_UNDEF(dst);
+
+ return false;
+ }
+
+ return true;
+}
+
+static bool user_cache_serdes_decode_serialized_object(
+ php_user_cache_serdes_decoder *dec,
+ zval *dst)
+{
+ zend_class_entry *ce;
+ zval state;
+
+ ce = user_cache_serdes_decode_class(dec);
+ if (ce == NULL ||
+ ce->__unserialize == NULL ||
+ (ce->ce_flags & ZEND_ACC_NOT_SERIALIZABLE) != 0 ||
+ object_init_ex(dst, ce) != SUCCESS
+ ) {
+ return false;
+ }
+
+ if (!user_cache_serdes_decoder_register(dec, dst)) {
+ zval_ptr_dtor(dst);
+
+ ZVAL_UNDEF(dst);
+
+ return false;
+ }
+
+ ZVAL_UNDEF(&state);
+ if (!user_cache_serdes_decode_value(dec, &state) ||
+ Z_TYPE(state) != IS_ARRAY
+ ) {
+ zval_ptr_dtor(&state);
+ zval_ptr_dtor(dst);
+
+ ZVAL_UNDEF(dst);
+
+ return false;
+ }
+
+ zend_call_known_instance_method_with_1_params(ce->__unserialize, Z_OBJ_P(dst), NULL, &state);
+
+ zval_ptr_dtor(&state);
+
+ if (EG(exception)) {
+ zval_ptr_dtor(dst);
+
+ ZVAL_UNDEF(dst);
+
+ return false;
+ }
+
+ return true;
+}
+
+static bool user_cache_serdes_decode_custom_object(
+ php_user_cache_serdes_decoder *dec,
+ zval *dst)
+{
+ const uint8_t *payload;
+ zend_class_entry *ce;
+ php_unserialize_data_t unser_data;
+ uint32_t payload_len;
+ bool result;
+
+ ce = user_cache_serdes_decode_class(dec);
+ if (ce == NULL ||
+ ce->unserialize == NULL ||
+ (ce->ce_flags & ZEND_ACC_NOT_SERIALIZABLE) != 0 ||
+ !user_cache_serdes_get_u32(dec, &payload_len) ||
+ !user_cache_serdes_get_bytes(dec, payload_len, &payload)
+ ) {
+ return false;
+ }
+
+ PHP_VAR_UNSERIALIZE_INIT(unser_data);
+ result = ce->unserialize(dst, ce, payload, payload_len, (zend_unserialize_data *) unser_data) == SUCCESS;
+ PHP_VAR_UNSERIALIZE_DESTROY(unser_data);
+
+ if (!result || EG(exception)) {
+ zval_ptr_dtor(dst);
+
+ ZVAL_UNDEF(dst);
+
+ return false;
+ }
+
+ if (!user_cache_serdes_decoder_register(dec, dst)) {
+ zval_ptr_dtor(dst);
+
+ ZVAL_UNDEF(dst);
+
+ return false;
+ }
+
+ return true;
+}
+
+static bool user_cache_serdes_decode_enum(
+ php_user_cache_serdes_decoder *dec,
+ zval *dst)
+{
+ zend_string *case_name;
+ zend_class_entry *ce;
+ zend_object *case_obj;
+
+ ce = user_cache_serdes_decode_class(dec);
+ if (ce == NULL ||
+ (ce->ce_flags & ZEND_ACC_ENUM) == 0 ||
+ !user_cache_serdes_get_str(dec, &case_name)
+ ) {
+ return false;
+ }
+
+ case_obj = zend_enum_get_case(ce, case_name);
+
+ zend_string_release(case_name);
+
+ if (case_obj == NULL) {
+ return false;
+ }
+
+ ZVAL_OBJ_COPY(dst, case_obj);
+
+ return true;
+}
+
+static bool user_cache_serdes_decode_reference(
+ php_user_cache_serdes_decoder *dec,
+ zval *dst)
+{
+ zend_reference *ref;
+ zval val;
+
+ ZVAL_NEW_EMPTY_REF(dst);
+ ref = Z_REF_P(dst);
+ ZVAL_NULL(&ref->val);
+
+ if (!user_cache_serdes_decoder_register(dec, dst)) {
+ zval_ptr_dtor(dst);
+
+ ZVAL_UNDEF(dst);
+
+ return false;
+ }
+
+ ZVAL_UNDEF(&val);
+
+ if (!user_cache_serdes_decode_value(dec, &val)) {
+ zval_ptr_dtor(&val);
+ zval_ptr_dtor(dst);
+
+ ZVAL_UNDEF(dst);
+
+ return false;
+ }
+
+ zval_ptr_dtor(&ref->val);
+
+ ZVAL_COPY_VALUE(&ref->val, &val);
+
+ return true;
+}
+
+static bool user_cache_serdes_decode_value(
+ php_user_cache_serdes_decoder *dec,
+ zval *dst)
+{
+ zend_long lval;
+ zend_string *str;
+ uint32_t backref_id;
+ uint8_t tag;
+ double dval;
+
+ ZVAL_UNDEF(dst);
+
+ if (php_user_cache_stack_overflowed()) {
+ return false;
+ }
+
+ if (!user_cache_serdes_get_u8(dec, &tag)) {
+ return false;
+ }
+
+ switch (tag) {
+ case PHP_USER_CACHE_SERDES_TAG_NULL:
+ ZVAL_NULL(dst);
+
+ return true;
+ case PHP_USER_CACHE_SERDES_TAG_FALSE:
+ ZVAL_FALSE(dst);
+
+ return true;
+ case PHP_USER_CACHE_SERDES_TAG_TRUE:
+ ZVAL_TRUE(dst);
+
+ return true;
+ case PHP_USER_CACHE_SERDES_TAG_LONG:
+ if (!user_cache_serdes_get_long(dec, &lval)) {
+ return false;
+ }
+
+ ZVAL_LONG(dst, lval);
+
+ return true;
+ case PHP_USER_CACHE_SERDES_TAG_DOUBLE:
+ if (!user_cache_serdes_get_double(dec, &dval)) {
+ return false;
+ }
+
+ ZVAL_DOUBLE(dst, dval);
+
+ return true;
+ case PHP_USER_CACHE_SERDES_TAG_STRING:
+ if (!user_cache_serdes_get_str(dec, &str)) {
+ return false;
+ }
+
+ ZVAL_STR(dst, str);
+
+ return true;
+ case PHP_USER_CACHE_SERDES_TAG_EMPTY_ARRAY:
+ ZVAL_EMPTY_ARRAY(dst);
+
+ return true;
+ case PHP_USER_CACHE_SERDES_TAG_PACKED_ARRAY:
+ case PHP_USER_CACHE_SERDES_TAG_HASHED_ARRAY:
+ return user_cache_serdes_decode_array(dec, tag, dst);
+ case PHP_USER_CACHE_SERDES_TAG_OBJECT:
+ return user_cache_serdes_decode_object(dec, dst);
+ case PHP_USER_CACHE_SERDES_TAG_SERIALIZED_OBJECT:
+ return user_cache_serdes_decode_serialized_object(dec, dst);
+ case PHP_USER_CACHE_SERDES_TAG_CUSTOM_OBJECT:
+ return user_cache_serdes_decode_custom_object(dec, dst);
+ case PHP_USER_CACHE_SERDES_TAG_ENUM:
+ return user_cache_serdes_decode_enum(dec, dst);
+ case PHP_USER_CACHE_SERDES_TAG_REFERENCE:
+ return user_cache_serdes_decode_reference(dec, dst);
+ case PHP_USER_CACHE_SERDES_TAG_BACKREF:
+ if (!user_cache_serdes_get_u32(dec, &backref_id) ||
+ backref_id >= dec->id_count
+ ) {
+ return false;
+ }
+
+ ZVAL_COPY(dst, &dec->ids[backref_id]);
+
+ return true;
+ default:
+ return false;
+ }
+}
+
+uint32_t php_user_cache_serdes_declared_property_index_plus_one(
+ zend_class_entry *ce,
+ zend_string *name)
+{
+ zend_property_info *prop_info;
+ uint32_t prop_idx;
+
+ if (ZSTR_LEN(name) == 0 ||
+ ZSTR_VAL(name)[0] == '\0' ||
+ ce->type != ZEND_USER_CLASS ||
+ ce->properties_info_table == NULL
+ ) {
+ return PHP_USER_CACHE_SERDES_PROPERTY_INDEX_NONE;
+ }
+
+ prop_info = zend_get_property_info(ce, name, true);
+ if (prop_info == NULL ||
+ prop_info == ZEND_WRONG_PROPERTY_INFO ||
+ (prop_info->flags & (ZEND_ACC_STATIC|ZEND_ACC_VIRTUAL)) != 0 ||
+ prop_info->offset == ZEND_VIRTUAL_PROPERTY_OFFSET
+ ) {
+ return PHP_USER_CACHE_SERDES_PROPERTY_INDEX_NONE;
+ }
+
+ prop_idx = OBJ_PROP_TO_NUM(prop_info->offset);
+ if (prop_idx >= ce->default_properties_count || prop_idx == UINT32_MAX) {
+ return PHP_USER_CACHE_SERDES_PROPERTY_INDEX_NONE;
+ }
+
+ return prop_idx + 1;
+}
+
+bool php_user_cache_serdes_get_sleep_state(
+ zval *obj_zv,
+ zval *state,
+ const char **failure_msg)
+{
+ zend_object *obj = Z_OBJ_P(obj_zv);
+ zend_class_entry *ce = obj->ce;
+ zval *sleep_zv, retval;
+ bool result;
+
+ ZVAL_UNDEF(state);
+
+ if (failure_msg != NULL) {
+ *failure_msg = NULL;
+ }
+
+ sleep_zv = zend_hash_find_known_hash(&ce->function_table, ZSTR_KNOWN(ZEND_STR_SLEEP));
+ if (sleep_zv == NULL) {
+ return false;
+ }
+
+ GC_ADDREF(obj);
+ zend_call_known_instance_method(Z_FUNC_P(sleep_zv), obj, &retval, 0, NULL);
+ OBJ_RELEASE(obj);
+
+ if (Z_ISUNDEF(retval) || EG(exception)) {
+ zval_ptr_dtor(&retval);
+
+ return false;
+ }
+
+ if (Z_TYPE(retval) != IS_ARRAY) {
+ zval_ptr_dtor(&retval);
+
+ php_error_docref(NULL, E_WARNING,
+ "%s::__sleep() should return an array only containing the names of instance-variables to serialize",
+ ZSTR_VAL(ce->name)
+ );
+
+ if (failure_msg != NULL) {
+ *failure_msg = "__sleep() did not return an array of member names; the object cannot be stored in the user cache";
+ }
+
+ return false;
+ }
+
+ /* Failure may leave a partially initialized state array. */
+ result = user_cache_serdes_get_sleep_props(obj_zv, Z_ARRVAL(retval), state);
+ zval_ptr_dtor(&retval);
+
+ if (!result) {
+ zval_ptr_dtor(state);
+
+ ZVAL_UNDEF(state);
+ }
+
+ return result;
+}
+
+bool php_user_cache_serdes_call_magic_serialize(zend_object *obj, zval *state)
+{
+ zend_call_known_instance_method_with_0_params(obj->ce->__serialize, obj, state);
+ if (EG(exception)) {
+ zval_ptr_dtor(state);
+
+ ZVAL_UNDEF(state);
+
+ return false;
+ }
+
+ if (Z_TYPE_P(state) != IS_ARRAY) {
+ zval_ptr_dtor(state);
+
+ ZVAL_UNDEF(state);
+
+ zend_type_error("%s::__serialize() must return an array", ZSTR_VAL(obj->ce->name));
+
+ return false;
+ }
+
+ return true;
+}
+
+bool php_user_cache_serdes_encode(zval *value, smart_str *buf, const char **failure_msg)
+{
+ php_user_cache_serdes_encoder enc;
+ bool result;
+
+ if (failure_msg != NULL) {
+ *failure_msg = NULL;
+ }
+
+ enc.buf = *buf;
+ enc.failure_message = NULL;
+ enc.string_count = 0;
+
+ zend_hash_init(&enc.seen, 8, NULL, NULL, 0);
+ zend_hash_init(&enc.strings, 32, NULL, NULL, 0);
+
+ result = user_cache_serdes_encode_value(&enc, value);
+
+ zend_hash_destroy(&enc.strings);
+ zend_hash_destroy(&enc.seen);
+
+ *buf = enc.buf;
+
+ if (!result && failure_msg != NULL) {
+ *failure_msg = enc.failure_message;
+ }
+
+ return result;
+}
+
+bool php_user_cache_serdes_decode(const uint8_t *data, size_t len, zval *dst)
+{
+ php_user_cache_serdes_decoder dec;
+ uint32_t i;
+ bool result;
+
+ ZVAL_UNDEF(dst);
+
+ if (data == NULL || len == 0) {
+ return false;
+ }
+
+ dec.data = data;
+ dec.len = len;
+ dec.pos = 0;
+ dec.ids = dec.inline_ids;
+ dec.id_count = 0;
+ dec.id_capacity = PHP_USER_CACHE_SERDES_INLINE_IDS;
+ dec.strings = dec.inline_strings;
+ dec.string_count = 0;
+ dec.string_capacity = PHP_USER_CACHE_SERDES_INLINE_STRINGS;
+
+ result = user_cache_serdes_decode_value(&dec, dst);
+
+ if (result && dec.pos != dec.len) {
+ result = false;
+ }
+
+ for (i = 0; i < dec.id_count; i++) {
+ zval_ptr_dtor(&dec.ids[i]);
+ }
+ for (i = 0; i < dec.string_count; i++) {
+ zend_string_release(dec.strings[i]);
+ }
+
+ if (dec.ids != dec.inline_ids) {
+ efree(dec.ids);
+ }
+ if (dec.strings != dec.inline_strings) {
+ efree(dec.strings);
+ }
+
+ if (!result && !Z_ISUNDEF_P(dst)) {
+ zval_ptr_dtor(dst);
+ ZVAL_UNDEF(dst);
+ }
+
+ return result;
+}
diff --git a/ext/user_cache/user_cache_shared_graph.c b/ext/user_cache/user_cache_shared_graph.c
new file mode 100644
index 000000000000..81f1e4b3a71f
--- /dev/null
+++ b/ext/user_cache/user_cache_shared_graph.c
@@ -0,0 +1,6296 @@
+/*
+ +----------------------------------------------------------------------+
+ | 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 "user_cache_internal.h"
+
+#include "Zend/zend_interfaces.h"
+#include "Zend/zend_closures.h"
+#include "Zend/zend_enum.h"
+#include "Zend/zend_operators.h"
+
+#if (defined(__GNUC__) && ZEND_GCC_VERSION >= 4003) || __has_attribute(hot)
+# define PHP_USER_CACHE_DECODE_HOT __attribute__((hot))
+#else
+# define PHP_USER_CACHE_DECODE_HOT
+#endif
+
+/* Request-local maps preserve identity for repeated graph nodes. */
+#define PHP_USER_CACHE_DEFINE_DECODE_MAP(name, ctype, dtor, release_on_fail) \
+ static zend_always_inline void user_cache_decode_##name##_map_teardown(void) \
+ { \
+ if (UC_G(decode_##name##_map) != NULL) { \
+ zend_hash_destroy(UC_G(decode_##name##_map)); \
+ efree(UC_G(decode_##name##_map)); \
+ UC_G(decode_##name##_map) = NULL; \
+ } \
+ } \
+ \
+ static zend_always_inline bool user_cache_decode_##name##_map_insert(uint32_t offset, ctype *entry) \
+ { \
+ if (UC_G(decode_##name##_map) == NULL) { \
+ UC_G(decode_##name##_map) = emalloc(sizeof(HashTable)); \
+ zend_hash_init(UC_G(decode_##name##_map), 8, NULL, dtor, 0); \
+ } \
+ \
+ GC_ADDREF(entry); \
+ if (zend_hash_index_add_ptr(UC_G(decode_##name##_map), offset, entry) == NULL) { \
+ release_on_fail; \
+ \
+ return false; \
+ } \
+ \
+ return true; \
+ } \
+ \
+ static zend_always_inline ctype *user_cache_decode_##name##_map_find(uint32_t offset) \
+ { \
+ if (UC_G(decode_##name##_map) == NULL) { \
+ return NULL; \
+ } \
+ \
+ return zend_hash_index_find_ptr(UC_G(decode_##name##_map), offset); \
+ }
+
+/* CALC and COPY must use the same route precedence. */
+typedef enum {
+ PHP_USER_CACHE_OBJECT_ROUTE_UNSTORABLE = 0,
+ PHP_USER_CACHE_OBJECT_ROUTE_SAFE_DIRECT,
+ PHP_USER_CACHE_OBJECT_ROUTE_MAGIC_SERIALIZE,
+ PHP_USER_CACHE_OBJECT_ROUTE_SERIALIZE_PROPS,
+ PHP_USER_CACHE_OBJECT_ROUTE_MAGIC_UNSERIALIZE,
+ PHP_USER_CACHE_OBJECT_ROUTE_SLEEP,
+ PHP_USER_CACHE_OBJECT_ROUTE_WAKEUP,
+ PHP_USER_CACHE_OBJECT_ROUTE_SERDES,
+ PHP_USER_CACHE_OBJECT_ROUTE_PLAIN
+} php_user_cache_object_route;
+
+typedef enum {
+ PHP_USER_CACHE_COPY_OBJECT_REF_NEW = 0,
+ PHP_USER_CACHE_COPY_OBJECT_REF_EMITTED,
+ PHP_USER_CACHE_COPY_OBJECT_REF_ERROR
+} php_user_cache_copy_object_ref_result;
+
+typedef bool (*php_user_cache_shared_graph_state_getter_t)(
+ const zval *value,
+ HashTable *state_memo,
+ zval **state_ptr,
+ zval *owned_state
+);
+
+static void user_cache_decode_array_dtor(zval *zv);
+static void user_cache_decode_shape_prototype_dtor(zval *zv);
+static void user_cache_shared_graph_refs_check_fork(void);
+static bool user_cache_shared_graph_calc_value(
+ php_user_cache_shared_graph_calc_context *ctx,
+ const zval *value
+);
+static bool user_cache_shared_graph_copy_value(
+ php_user_cache_shared_graph_copy_context *ctx,
+ const zval *src,
+ php_user_cache_shared_graph_value *dst
+);
+static PHP_USER_CACHE_DECODE_HOT bool user_cache_shared_graph_decode_value(
+ const uint8_t *buf,
+ size_t buf_len,
+ const php_user_cache_shared_graph_value *value,
+ zval *dst
+);
+#if ZEND_DEBUG
+static bool user_cache_shared_graph_rebase_verbatim_array(
+ php_user_cache_shared_graph_rebase_context *ctx,
+ zend_array *arr
+);
+#endif
+
+static zend_always_inline bool user_cache_decode_range_ok(
+ size_t buf_len,
+ uint32_t offset,
+ size_t need
+)
+{
+ return offset <= buf_len && need <= buf_len - offset;
+}
+
+/* Check without overflowing size_t. */
+static zend_always_inline bool user_cache_decode_array_range_ok(
+ size_t buf_len,
+ uint32_t offset,
+ uint32_t count,
+ size_t elem_size
+)
+{
+ if (offset > buf_len) {
+ return false;
+ }
+
+ return count <= (buf_len - offset) / elem_size;
+}
+
+static zend_always_inline bool user_cache_decode_fail_zval(zval *dst)
+{
+ zval_ptr_dtor(dst);
+
+ ZVAL_UNDEF(dst);
+
+ return false;
+}
+
+static zend_always_inline zend_string *user_cache_decode_string_at(
+ const uint8_t *buf,
+ size_t buf_len,
+ uint32_t offset
+)
+{
+ zend_string *string;
+
+ if (!user_cache_decode_range_ok(buf_len, offset, _ZSTR_HEADER_SIZE)) {
+ return NULL;
+ }
+
+ string = (zend_string *) (void *) (buf + offset);
+ if (ZSTR_LEN(string) > buf_len ||
+ !user_cache_decode_range_ok(
+ buf_len,
+ offset,
+ _ZSTR_STRUCT_SIZE(ZSTR_LEN(string))
+ )
+ ) {
+ return NULL;
+ }
+
+ return string;
+}
+
+static zend_always_inline size_t user_cache_decode_node_header_size(uint8_t type)
+{
+ switch (type) {
+ case PHP_USER_CACHE_SHARED_GRAPH_VALUE_DYNAMIC_ARRAY:
+ return sizeof(php_user_cache_shared_graph_array);
+ case PHP_USER_CACHE_SHARED_GRAPH_VALUE_SHAPED_ARRAY:
+ return sizeof(php_user_cache_shared_graph_shaped_array);
+ case PHP_USER_CACHE_SHARED_GRAPH_VALUE_OBJECT:
+ case PHP_USER_CACHE_SHARED_GRAPH_VALUE_SLEEP_OBJECT:
+ return sizeof(php_user_cache_shared_graph_object);
+ case PHP_USER_CACHE_SHARED_GRAPH_VALUE_SAFE_DIRECT_OBJECT:
+ case PHP_USER_CACHE_SHARED_GRAPH_VALUE_SERIALIZED_OBJECT:
+ return sizeof(php_user_cache_shared_graph_safe_direct_object);
+ case PHP_USER_CACHE_SHARED_GRAPH_VALUE_SLEEP_SHAPED_OBJECT:
+ case PHP_USER_CACHE_SHARED_GRAPH_VALUE_SERIALIZED_SHAPED_OBJECT:
+ return sizeof(php_user_cache_shared_graph_shaped_state_object);
+ case PHP_USER_CACHE_SHARED_GRAPH_VALUE_SERDES_OBJECT:
+ return sizeof(php_user_cache_shared_graph_serdes_object);
+ case PHP_USER_CACHE_SHARED_GRAPH_VALUE_ENUM:
+ return sizeof(php_user_cache_shared_graph_enum);
+ case PHP_USER_CACHE_SHARED_GRAPH_VALUE_REFERENCE:
+ return sizeof(php_user_cache_shared_graph_reference);
+ default:
+ return 0;
+ }
+}
+
+/* Address-keyed entries are valid while the decoded payload remains pinned. */
+static zend_always_inline void *user_cache_decode_resolve_cache_find(const void *addr)
+{
+ uint32_t i;
+
+ for (i = 0; i < PHP_USER_CACHE_DECODE_DIRECT_CACHE_SLOTS; i++) {
+ if (UC_G(decode_resolve_direct_keys)[i] == addr) {
+ return UC_G(decode_resolve_direct_values)[i];
+ }
+ }
+
+ if (UC_G(decode_resolve_cache) == NULL) {
+ return NULL;
+ }
+
+ return zend_hash_index_find_ptr(
+ UC_G(decode_resolve_cache),
+ (zend_ulong) (uintptr_t) addr
+ );
+}
+
+static zend_always_inline void user_cache_decode_resolve_cache_store(const void *addr, void *value)
+{
+ uint32_t slot;
+
+ slot = UC_G(decode_resolve_direct_next)++ % PHP_USER_CACHE_DECODE_DIRECT_CACHE_SLOTS;
+ UC_G(decode_resolve_direct_keys)[slot] = addr;
+ UC_G(decode_resolve_direct_values)[slot] = value;
+
+ if (UC_G(decode_resolve_cache) == NULL) {
+ UC_G(decode_resolve_cache) = emalloc(sizeof(HashTable));
+ zend_hash_init(UC_G(decode_resolve_cache), 8, NULL, NULL, 0);
+ }
+
+ zend_hash_index_add_ptr(
+ UC_G(decode_resolve_cache),
+ (zend_ulong) (uintptr_t) addr,
+ value
+ );
+}
+
+PHP_USER_CACHE_DEFINE_DECODE_MAP(
+ identity,
+ zend_object,
+ php_user_cache_object_table_dtor,
+ OBJ_RELEASE(entry)
+)
+
+PHP_USER_CACHE_DEFINE_DECODE_MAP(
+ reference,
+ zend_reference,
+ php_user_cache_reference_table_dtor,
+ if (GC_DELREF(entry) == 0) efree_size(entry, sizeof(zend_reference))
+)
+
+PHP_USER_CACHE_DEFINE_DECODE_MAP(
+ array,
+ zend_array,
+ user_cache_decode_array_dtor,
+ if (GC_DELREF(entry) == 0) zend_array_destroy(entry)
+)
+
+static zend_always_inline HashTable *user_cache_decode_shape_prototype_cache(void)
+{
+ if (UC_G(decode_shape_prototype_cache) == NULL) {
+ UC_G(decode_shape_prototype_cache) = emalloc(sizeof(HashTable));
+ zend_hash_init(
+ UC_G(decode_shape_prototype_cache),
+ 8,
+ NULL,
+ user_cache_decode_shape_prototype_dtor,
+ 0
+ );
+ }
+
+ return UC_G(decode_shape_prototype_cache);
+}
+
+static zend_always_inline size_t user_cache_shared_graph_alignment_padding(const void *buf)
+{
+ uintptr_t raw_addr, aligned_addr;
+
+ raw_addr = (uintptr_t) buf;
+ aligned_addr = (uintptr_t) ZEND_MM_ALIGNED_SIZE(raw_addr);
+
+ return (size_t) (aligned_addr - raw_addr);
+}
+
+static zend_always_inline bool user_cache_shared_graph_header_is_valid(
+ const php_user_cache_shared_graph_header *header
+)
+{
+ return header->magic == PHP_USER_CACHE_SHARED_GRAPH_MAGIC &&
+ header->version == PHP_USER_CACHE_SHARED_GRAPH_VERSION
+ ;
+}
+
+static zend_always_inline const uint8_t *user_cache_shared_graph_locate(
+ const uint8_t *buf,
+ size_t buf_len,
+ size_t *graph_len)
+{
+ const php_user_cache_shared_graph_header *header;
+ size_t padding;
+
+ padding = user_cache_shared_graph_alignment_padding(buf);
+ if (padding > buf_len || buf_len - padding < sizeof(php_user_cache_shared_graph_header)) {
+ return NULL;
+ }
+
+ buf += padding;
+ buf_len -= padding;
+ header = (const php_user_cache_shared_graph_header *) buf;
+ if (!user_cache_shared_graph_header_is_valid(header)) {
+ return NULL;
+ }
+
+ if (graph_len != NULL) {
+ *graph_len = buf_len;
+ }
+
+ return buf;
+}
+
+static zend_always_inline void user_cache_shared_graph_calc_init(php_user_cache_shared_graph_calc_context *ctx)
+{
+ ctx->size = 0;
+ ctx->reserve_failed = false;
+ ctx->shared_verdicts = NULL;
+
+ zend_hash_init(&ctx->seen_arrays, 8, NULL, NULL, 0);
+ zend_hash_init(&ctx->seen_objects, 8, NULL, NULL, 0);
+ zend_hash_init(&ctx->seen_references, 8, NULL, NULL, 0);
+ zend_hash_init(&ctx->string_dedup, 8, NULL, NULL, 0);
+ zend_hash_init(&ctx->array_shape_dedup, 8, NULL, NULL, 0);
+ zend_hash_init(&ctx->state_schema_dedup, 8, NULL, NULL, 0);
+ zend_hash_init(&ctx->direct_array_dedup, 8, NULL, NULL, 0);
+ zend_hash_init(&ctx->direct_verdicts, 8, NULL, NULL, 0);
+}
+
+static zend_always_inline void user_cache_shared_graph_calc_destroy(php_user_cache_shared_graph_calc_context *ctx)
+{
+ zend_hash_destroy(&ctx->direct_verdicts);
+ zend_hash_destroy(&ctx->direct_array_dedup);
+ zend_hash_destroy(&ctx->state_schema_dedup);
+ zend_hash_destroy(&ctx->array_shape_dedup);
+ zend_hash_destroy(&ctx->string_dedup);
+ zend_hash_destroy(&ctx->seen_references);
+ zend_hash_destroy(&ctx->seen_objects);
+ zend_hash_destroy(&ctx->seen_arrays);
+}
+
+static zend_always_inline bool user_cache_shared_graph_calc_reserve(
+ php_user_cache_shared_graph_calc_context *ctx, size_t amount)
+{
+ size_t aligned_amount;
+
+ aligned_amount = PHP_USER_CACHE_ALIGNED_SIZE(amount);
+ if (ctx->size > SIZE_MAX - aligned_amount) {
+ ctx->reserve_failed = true;
+
+ return false;
+ }
+
+ ctx->size += aligned_amount;
+
+ if (UNEXPECTED(ctx->size > UINT32_MAX)) {
+ ctx->reserve_failed = true;
+
+ return false;
+ }
+
+ return true;
+}
+
+static zend_always_inline bool user_cache_shared_graph_calc_reserve_string(
+ php_user_cache_shared_graph_calc_context *ctx,
+ const zend_string *string)
+{
+ if (zend_hash_exists(&ctx->string_dedup, (zend_string *) string)) {
+ return true;
+ }
+
+ if (!user_cache_shared_graph_calc_reserve(ctx, _ZSTR_STRUCT_SIZE(ZSTR_LEN(string)))) {
+ return false;
+ }
+
+ return zend_hash_add_empty_element(&ctx->string_dedup, (zend_string *) string) != NULL;
+}
+
+static zend_always_inline void user_cache_shared_graph_key_append_u32(smart_str *key, uint32_t value)
+{
+ smart_str_appendl(key, (const char *) &value, sizeof(value));
+}
+
+static zend_always_inline bool user_cache_shared_graph_array_has_shape(const HashTable *arr)
+{
+ zend_string *key;
+
+ if (arr->nNumOfElements == 0 ||
+ HT_IS_PACKED(arr) ||
+ arr->nNumOfElements > PHP_USER_CACHE_SHARED_GRAPH_ARRAY_SHAPE_MAX_KEYS ||
+ arr->nNextFreeElement < 0 ||
+ arr->nNextFreeElement > UINT32_MAX ||
+ !HT_IS_WITHOUT_HOLES(arr)
+ ) {
+ return false;
+ }
+
+ ZEND_HASH_FOREACH_STR_KEY((HashTable *) arr, key) {
+ if (key == NULL) {
+ return false;
+ }
+ } ZEND_HASH_FOREACH_END();
+
+ return true;
+}
+
+static zend_always_inline bool user_cache_shared_graph_can_restore_direct(zend_class_entry *ce)
+{
+ if (ce->ce_flags & ZEND_ACC_NOT_SERIALIZABLE) {
+ return false;
+ }
+
+ if (ce->type != ZEND_USER_CLASS && ce->create_object != NULL) {
+ return false;
+ }
+
+ return true;
+}
+
+static zend_always_inline bool user_cache_shared_graph_can_use_verbatim_arrays(void)
+{
+#ifdef ZEND_WIN32
+ /* Verbatim arrays contain absolute pointers and cannot cross mapping bases. */
+ return false;
+#else
+ return !php_user_cache_active_context()->boundary_shared;
+#endif
+}
+
+static zend_always_inline bool user_cache_shared_graph_is_unmangled_property_name(zend_string *prop_name)
+{
+ return ZSTR_LEN(prop_name) != 0 && ZSTR_VAL(prop_name)[0] != '\0';
+}
+
+/*
+ * The state table pointer is captured before recursing into the state graph:
+ * the memoized state zval lives in a HashTable whose bucket array is
+ * reallocated when nested objects add their own memo entries, so a pointer
+ * into that bucket must not be dereferenced afterwards. The referenced state
+ * array itself is stable, so its HashTable is safe to hold across recursion.
+ */
+static zend_always_inline bool user_cache_shared_graph_safe_direct_property_shadows_state(
+ zend_string *prop_name, const HashTable *state_ht)
+{
+ return state_ht != NULL &&
+ user_cache_shared_graph_is_unmangled_property_name(prop_name) &&
+ zend_hash_exists(state_ht, prop_name)
+ ;
+}
+
+static inline bool user_cache_class_overrides_safe_direct_magic_serialize(zend_class_entry *ce)
+{
+ zend_class_entry *base_ce = NULL;
+ const php_user_cache_safe_direct_handlers *handlers;
+
+ if (ce->type != ZEND_USER_CLASS ||
+ ce->__serialize == NULL ||
+ ce->__unserialize == NULL ||
+ (ce->ce_flags & (ZEND_ACC_NOT_SERIALIZABLE|ZEND_ACC_ENUM)) != 0
+ ) {
+ return false;
+ }
+
+ handlers = php_user_cache_safe_direct_find_handlers(ce, &base_ce);
+
+ return php_user_cache_class_overrides_safe_direct_magic_serialize_ex(ce, handlers, base_ce);
+}
+
+static zend_always_inline bool user_cache_shared_graph_can_use_safe_direct(zend_class_entry *ce)
+{
+ zend_class_entry *base_ce = php_user_cache_safe_direct_find_base(ce);
+
+ return base_ce != NULL &&
+ !user_cache_class_overrides_safe_direct_magic_serialize(ce) &&
+ php_user_cache_safe_direct_state_serialize_func(ce) != NULL &&
+ php_user_cache_safe_direct_state_unserialize_func(ce) != NULL
+ ;
+}
+
+static zend_always_inline bool user_cache_shared_graph_can_use_sleep_object(zend_class_entry *ce)
+{
+ if (ce->type != ZEND_USER_CLASS ||
+ (ce->ce_flags & (ZEND_ACC_NOT_SERIALIZABLE|ZEND_ACC_ENUM)) != 0 ||
+ (ce->serialize != NULL && ce->unserialize != NULL)
+ ) {
+ return false;
+ }
+
+ return zend_hash_find_known_hash(&ce->function_table, ZSTR_KNOWN(ZEND_STR_SLEEP)) != NULL;
+}
+
+static zend_always_inline bool user_cache_shared_graph_can_use_wakeup_object(zend_class_entry *ce)
+{
+ if (ce->type != ZEND_USER_CLASS ||
+ (ce->ce_flags & (ZEND_ACC_NOT_SERIALIZABLE|ZEND_ACC_ENUM)) != 0 ||
+ ce->__serialize != NULL ||
+ ce->__unserialize != NULL ||
+ (ce->serialize != NULL && ce->unserialize != NULL) ||
+ zend_hash_find_known_hash(&ce->function_table, ZSTR_KNOWN(ZEND_STR_SLEEP)) != NULL
+ ) {
+ return false;
+ }
+
+ return zend_hash_find_known_hash(&ce->function_table, ZSTR_KNOWN(ZEND_STR_WAKEUP)) != NULL;
+}
+
+static zend_always_inline bool user_cache_shared_graph_pointer_in_range(
+ const void *ptr,
+ const uint8_t *base,
+ size_t len)
+{
+ uintptr_t addr, start;
+
+ if (ptr == NULL || base == NULL || len == 0) {
+ return false;
+ }
+
+ addr = (uintptr_t) ptr;
+ start = (uintptr_t) base;
+
+ return addr >= start && addr - start < len;
+}
+
+#if ZEND_DEBUG
+static zend_always_inline void *user_cache_shared_graph_rebase_pointer(
+ void *ptr,
+ const uint8_t *old_base,
+ size_t len,
+ ptrdiff_t delta)
+{
+ if (!user_cache_shared_graph_pointer_in_range(ptr, old_base, len)) {
+ return ptr;
+ }
+
+ return (void *) ((char *) ptr - delta);
+}
+#endif
+
+static zend_string *user_cache_shared_graph_array_shape_key(const HashTable *arr)
+{
+ zend_string *key;
+ smart_str shape_key = {0};
+ uint32_t count, key_len;
+
+ ZEND_ASSERT(user_cache_shared_graph_array_has_shape(arr));
+
+ count = (uint32_t) arr->nNumOfElements;
+ smart_str_appendl(&shape_key, "ucshape", sizeof("ucshape") - 1);
+ user_cache_shared_graph_key_append_u32(&shape_key, count);
+
+ ZEND_HASH_FOREACH_STR_KEY((HashTable *) arr, key) {
+ if (ZSTR_LEN(key) > UINT32_MAX) {
+ smart_str_free(&shape_key);
+
+ return NULL;
+ }
+
+ key_len = (uint32_t) ZSTR_LEN(key);
+
+ user_cache_shared_graph_key_append_u32(&shape_key, key_len);
+
+ smart_str_appendl(&shape_key, ZSTR_VAL(key), ZSTR_LEN(key));
+ } ZEND_HASH_FOREACH_END();
+
+ return smart_str_extract(&shape_key);
+}
+
+static bool user_cache_shared_graph_calc_array_shape(
+ php_user_cache_shared_graph_calc_context *ctx,
+ const HashTable *arr,
+ zend_string *shape_key
+)
+{
+ zend_string *key;
+ bool result = true, owns_shape_key = false;
+
+ if (shape_key == NULL) {
+ shape_key = user_cache_shared_graph_array_shape_key(arr);
+ if (shape_key == NULL) {
+ return false;
+ }
+
+ owns_shape_key = true;
+ }
+
+ if (zend_hash_exists(&ctx->array_shape_dedup, shape_key)) {
+ if (owns_shape_key) {
+ zend_string_release(shape_key);
+ }
+
+ return true;
+ }
+
+ if (!user_cache_shared_graph_calc_reserve(
+ ctx,
+ sizeof(php_user_cache_shared_graph_array_shape)
+ ) ||
+ !user_cache_shared_graph_calc_reserve(
+ ctx,
+ (size_t) arr->nNumOfElements * sizeof(php_user_cache_shared_graph_array_shape_element)
+ )
+ ) {
+ if (owns_shape_key) {
+ zend_string_release(shape_key);
+ }
+
+ return false;
+ }
+
+ ZEND_HASH_FOREACH_STR_KEY((HashTable *) arr, key) {
+ if (!user_cache_shared_graph_calc_reserve_string(ctx, key)) {
+ result = false;
+
+ break;
+ }
+ } ZEND_HASH_FOREACH_END();
+
+ if (result) {
+ result = zend_hash_add_empty_element(&ctx->array_shape_dedup, shape_key) != NULL;
+ }
+
+ if (owns_shape_key) {
+ zend_string_release(shape_key);
+ }
+
+ return result;
+}
+
+static zend_string *user_cache_shared_graph_state_schema_key(
+ zend_class_entry *ce,
+ const HashTable *arr,
+ zend_string **shape_key_out
+)
+{
+ zend_string *shape_key;
+ smart_str schema_key = {0};
+ uint32_t class_name_len, shape_key_len;
+
+ shape_key = user_cache_shared_graph_array_shape_key(arr);
+ if (shape_key == NULL || ZSTR_LEN(ce->name) > UINT32_MAX || ZSTR_LEN(shape_key) > UINT32_MAX) {
+ if (shape_key != NULL) {
+ zend_string_release(shape_key);
+ }
+
+ return NULL;
+ }
+
+ class_name_len = (uint32_t) ZSTR_LEN(ce->name);
+ shape_key_len = (uint32_t) ZSTR_LEN(shape_key);
+
+ smart_str_appendl(&schema_key, "ucstate", sizeof("ucstate") - 1);
+ user_cache_shared_graph_key_append_u32(&schema_key, class_name_len);
+ smart_str_appendl(&schema_key, ZSTR_VAL(ce->name), ZSTR_LEN(ce->name));
+ user_cache_shared_graph_key_append_u32(&schema_key, shape_key_len);
+ smart_str_appendl(&schema_key, ZSTR_VAL(shape_key), ZSTR_LEN(shape_key));
+
+ *shape_key_out = shape_key;
+
+ return smart_str_extract(&schema_key);
+}
+
+static bool user_cache_shared_graph_calc_state_schema(
+ php_user_cache_shared_graph_calc_context *ctx,
+ zend_class_entry *ce,
+ const HashTable *arr
+)
+{
+ zend_string *schema_key, *shape_key = NULL;
+ bool result;
+
+ schema_key = user_cache_shared_graph_state_schema_key(ce, arr, &shape_key);
+ if (schema_key == NULL) {
+ return false;
+ }
+
+ if (zend_hash_exists(&ctx->state_schema_dedup, schema_key)) {
+ zend_string_release(shape_key);
+ zend_string_release(schema_key);
+
+ return true;
+ }
+
+ result = user_cache_shared_graph_calc_reserve(
+ ctx,
+ sizeof(php_user_cache_shared_graph_state_schema)
+ ) &&
+ user_cache_shared_graph_calc_reserve_string(ctx, ce->name) &&
+ user_cache_shared_graph_calc_array_shape(ctx, arr, shape_key)
+ ;
+
+ zend_string_release(shape_key);
+
+ if (result) {
+ result = zend_hash_add_empty_element(&ctx->state_schema_dedup, schema_key) != NULL;
+ }
+
+ zend_string_release(schema_key);
+
+ return result;
+}
+
+static bool user_cache_shared_graph_state_value_fits_schema(
+ zval *value,
+ const HashTable *state_array,
+ HashTable *seen_arrs
+)
+{
+ zend_ulong arr_key;
+ zval *elem;
+
+ if (php_user_cache_stack_overflowed()) {
+ return false;
+ }
+
+ ZVAL_DEREF(value);
+
+ switch (Z_TYPE_P(value)) {
+ case IS_UNDEF:
+ case IS_NULL:
+ case IS_FALSE:
+ case IS_TRUE:
+ case IS_LONG:
+ case IS_DOUBLE:
+ case IS_STRING:
+ return true;
+ case IS_ARRAY:
+ if (Z_ARRVAL_P(value) == state_array) {
+ return false;
+ }
+
+ arr_key = (zend_ulong) (uintptr_t) Z_ARRVAL_P(value);
+ if (!php_user_cache_seen_test_and_add(seen_arrs, Z_ARRVAL_P(value))) {
+ return true;
+ }
+
+ ZEND_HASH_FOREACH_VAL(Z_ARRVAL_P(value), elem) {
+ if (!user_cache_shared_graph_state_value_fits_schema(
+ elem,
+ state_array,
+ seen_arrs
+ )
+ ) {
+ zend_hash_index_del(seen_arrs, arr_key);
+
+ return false;
+ }
+ } ZEND_HASH_FOREACH_END();
+
+ zend_hash_index_del(seen_arrs, arr_key);
+
+ return true;
+ default:
+ return false;
+ }
+}
+
+static bool user_cache_shared_graph_state_array_fits_schema(const HashTable *arr)
+{
+ zval *elem;
+ HashTable seen_arrs;
+ bool result = true;
+
+ if (!user_cache_shared_graph_array_has_shape(arr)) {
+ return false;
+ }
+
+ zend_hash_init(&seen_arrs, 8, NULL, NULL, 0);
+
+ ZEND_HASH_FOREACH_VAL((HashTable *) arr, elem) {
+ if (!user_cache_shared_graph_state_value_fits_schema(
+ elem,
+ arr,
+ &seen_arrs
+ )
+ ) {
+ result = false;
+
+ break;
+ }
+ } ZEND_HASH_FOREACH_END();
+
+ zend_hash_destroy(&seen_arrs);
+
+ return result;
+}
+
+static bool user_cache_shared_graph_calc_shaped_state_object(
+ php_user_cache_shared_graph_calc_context *ctx,
+ zend_class_entry *ce,
+ const HashTable *state_array
+)
+{
+ zval *elem;
+
+ if (!user_cache_shared_graph_calc_reserve(
+ ctx,
+ sizeof(php_user_cache_shared_graph_shaped_state_object)
+ ) ||
+ !user_cache_shared_graph_calc_reserve(
+ ctx,
+ (size_t) state_array->nNumOfElements * sizeof(php_user_cache_shared_graph_value)
+ ) ||
+ !user_cache_shared_graph_calc_state_schema(ctx, ce, state_array)
+ ) {
+ return false;
+ }
+
+ ZEND_HASH_FOREACH_VAL((HashTable *) state_array, elem) {
+ if (!user_cache_shared_graph_calc_value(ctx, elem)) {
+ return false;
+ }
+ } ZEND_HASH_FOREACH_END();
+
+ return true;
+}
+
+static php_user_cache_object_route user_cache_shared_graph_classify_object_route_uncached(zend_class_entry *ce)
+{
+ if (user_cache_shared_graph_can_use_safe_direct(ce)) {
+ return PHP_USER_CACHE_OBJECT_ROUTE_SAFE_DIRECT;
+ }
+
+ if (php_user_cache_class_uses_magic_serialize(ce)) {
+ return PHP_USER_CACHE_OBJECT_ROUTE_MAGIC_SERIALIZE;
+ }
+
+ if (php_user_cache_class_uses_serialize_props(ce)) {
+ return PHP_USER_CACHE_OBJECT_ROUTE_SERIALIZE_PROPS;
+ }
+
+ if (php_user_cache_class_uses_magic_unserialize(ce)) {
+ return PHP_USER_CACHE_OBJECT_ROUTE_MAGIC_UNSERIALIZE;
+ }
+
+ if (user_cache_shared_graph_can_use_sleep_object(ce)) {
+ return PHP_USER_CACHE_OBJECT_ROUTE_SLEEP;
+ }
+
+ if (user_cache_shared_graph_can_use_wakeup_object(ce)) {
+ return PHP_USER_CACHE_OBJECT_ROUTE_WAKEUP;
+ }
+
+ if (php_user_cache_class_uses_serdes(ce)) {
+ return PHP_USER_CACHE_OBJECT_ROUTE_SERDES;
+ }
+
+ if (!user_cache_shared_graph_can_restore_direct(ce)) {
+ return PHP_USER_CACHE_OBJECT_ROUTE_UNSTORABLE;
+ }
+
+ return PHP_USER_CACHE_OBJECT_ROUTE_PLAIN;
+}
+
+/* The route is immutable per class and cached for the request. */
+static php_user_cache_object_route user_cache_shared_graph_classify_object_route(zend_class_entry *ce)
+{
+ zval *cached, route_zv;
+ php_user_cache_object_route route;
+
+ if (UC_G(object_route_memo) != NULL) {
+ cached = zend_hash_index_find(UC_G(object_route_memo), (zend_ulong) (uintptr_t) ce);
+ if (cached != NULL) {
+ return (php_user_cache_object_route) Z_LVAL_P(cached);
+ }
+ } else {
+ UC_G(object_route_memo) = emalloc(sizeof(HashTable));
+ zend_hash_init(UC_G(object_route_memo), 8, NULL, NULL, 0);
+ }
+
+ route = user_cache_shared_graph_classify_object_route_uncached(ce);
+
+ ZVAL_LONG(&route_zv, (zend_long) route);
+ zend_hash_index_add(UC_G(object_route_memo), (zend_ulong) (uintptr_t) ce, &route_zv);
+
+ return route;
+}
+
+static void user_cache_shared_graph_object_route_memo_release(void)
+{
+ if (UC_G(object_route_memo) != NULL) {
+ zend_hash_destroy(UC_G(object_route_memo));
+ efree(UC_G(object_route_memo));
+
+ UC_G(object_route_memo) = NULL;
+ }
+}
+
+static bool user_cache_shared_graph_can_copy_verbatim_value(HashTable *seen_arrs, HashTable *direct_verdicts, const zval *value)
+{
+ const zval *packed_value;
+ const HashTable *arr;
+ const Bucket *bucket;
+ zend_ulong arr_key;
+ zval *cached, verdict;
+ uint32_t i;
+ bool result = true;
+
+ if (php_user_cache_stack_overflowed()) {
+ return false;
+ }
+
+ 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:
+ arr = Z_ARRVAL_P(value);
+ if (arr->nNumOfElements == 0) {
+ return true;
+ }
+
+ arr_key = (zend_ulong) (uintptr_t) arr;
+
+ if (GC_REFCOUNT(arr) > 1) {
+ cached = zend_hash_index_find(direct_verdicts, arr_key);
+ if (cached != NULL) {
+ return Z_TYPE_P(cached) == IS_TRUE;
+ }
+ }
+
+ if (!php_user_cache_seen_test_and_add(seen_arrs, arr)) {
+ return false;
+ }
+
+ if (HT_IS_PACKED(arr)) {
+ for (i = 0; i < arr->nNumUsed; i++) {
+ packed_value = &arr->arPacked[i];
+ if (!user_cache_shared_graph_can_copy_verbatim_value(
+ seen_arrs,
+ direct_verdicts,
+ packed_value
+ )
+ ) {
+ result = false;
+
+ break;
+ }
+ }
+ } else {
+ bucket = arr->arData;
+
+ for (i = 0; i < arr->nNumUsed; i++) {
+ if (Z_TYPE(bucket[i].val) != IS_UNDEF &&
+ !user_cache_shared_graph_can_copy_verbatim_value(
+ seen_arrs,
+ direct_verdicts,
+ &bucket[i].val
+ )
+ ) {
+ result = false;
+
+ break;
+ }
+ }
+ }
+
+ zend_hash_index_del(seen_arrs, arr_key);
+
+ if (GC_REFCOUNT(arr) > 1) {
+ ZVAL_BOOL(&verdict, result);
+ zend_hash_index_add(direct_verdicts, arr_key, &verdict);
+ }
+
+ return result;
+ default:
+ return false;
+ }
+}
+
+static bool user_cache_shared_graph_calc_verbatim_value(
+ php_user_cache_shared_graph_calc_context *ctx,
+ const zval *value)
+{
+ const zval *packed_value;
+ const HashTable *arr;
+ const Bucket *bucket;
+ zend_ulong arr_key;
+ zval *cached, seen_marker;
+ uint32_t i;
+ size_t data_size;
+ bool result = true;
+
+ if (php_user_cache_stack_overflowed()) {
+ return false;
+ }
+
+ 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 user_cache_shared_graph_calc_reserve_string(ctx, Z_STR_P(value));
+ case IS_ARRAY:
+ arr = Z_ARRVAL_P(value);
+ if (arr->nNumOfElements == 0) {
+ return true;
+ }
+
+ arr_key = (zend_ulong) (uintptr_t) arr;
+ if (GC_REFCOUNT(arr) > 1) {
+ cached = zend_hash_index_find(&ctx->direct_array_dedup, arr_key);
+ if (cached != NULL) {
+ return true;
+ }
+ }
+
+ if (!php_user_cache_seen_test_and_add(&ctx->seen_arrays, arr)) {
+ return false;
+ }
+
+ data_size = HT_IS_PACKED(arr) ? HT_PACKED_USED_SIZE(arr) : HT_USED_SIZE(arr);
+ if (!user_cache_shared_graph_calc_reserve(ctx, sizeof(zend_array)) ||
+ !user_cache_shared_graph_calc_reserve(ctx, data_size)
+ ) {
+ result = false;
+
+ goto done;
+ }
+
+ if (HT_IS_PACKED(arr)) {
+ for (i = 0; i < arr->nNumUsed; i++) {
+ packed_value = &arr->arPacked[i];
+ if (!user_cache_shared_graph_calc_verbatim_value(ctx, packed_value)) {
+ result = false;
+
+ break;
+ }
+ }
+ } else {
+ bucket = arr->arData;
+ for (i = 0; i < arr->nNumUsed; i++) {
+ if (bucket[i].key != NULL &&
+ !user_cache_shared_graph_calc_reserve_string(ctx, bucket[i].key)
+ ) {
+ result = false;
+
+ break;
+ }
+
+ if (Z_TYPE(bucket[i].val) != IS_UNDEF &&
+ !user_cache_shared_graph_calc_verbatim_value(ctx, &bucket[i].val)
+ ) {
+ result = false;
+
+ break;
+ }
+ }
+ }
+
+done:
+ zend_hash_index_del(&ctx->seen_arrays, arr_key);
+
+ if (result && GC_REFCOUNT(arr) > 1) {
+ ZVAL_TRUE(&seen_marker);
+ zend_hash_index_add(&ctx->direct_array_dedup, arr_key, &seen_marker);
+ }
+
+ return result;
+ default:
+ return false;
+ }
+}
+
+static bool user_cache_shared_graph_produce_safe_direct_state(const zval *value, zval *state)
+{
+ php_user_cache_safe_direct_state_serialize_func_t serialize_func;
+
+ serialize_func = php_user_cache_safe_direct_state_serialize_func(Z_OBJCE_P(value));
+ if (serialize_func == NULL ||
+ !serialize_func(state, value) ||
+ Z_TYPE_P(state) != IS_ARRAY
+ ) {
+ if (!Z_ISUNDEF_P(state)) {
+ zval_ptr_dtor(state);
+ ZVAL_UNDEF(state);
+ }
+
+ return false;
+ }
+
+ return true;
+}
+
+static bool user_cache_shared_graph_extract_serialize_snapshot(const zval *value, zval *state)
+{
+ return php_user_cache_serdes_call_magic_serialize(Z_OBJ_P(value), state);
+}
+
+static bool user_cache_shared_graph_extract_property_snapshot(const zval *value, zval *state)
+{
+ zend_ulong num_key;
+ zend_string *key;
+ zval *prop, elem;
+ HashTable *props;
+ bool result = true;
+
+ props = zend_get_properties_for((zval *) value, ZEND_PROP_PURPOSE_SERIALIZE);
+ if (props == NULL) {
+ array_init(state);
+
+ return true;
+ }
+
+ array_init_size(state, zend_hash_num_elements(props));
+
+ ZEND_HASH_FOREACH_KEY_VAL(props, num_key, key, prop) {
+ if (Z_TYPE_P(prop) == IS_INDIRECT) {
+ prop = Z_INDIRECT_P(prop);
+ if (Z_TYPE_P(prop) == IS_UNDEF) {
+ continue;
+ }
+ }
+
+ if (Z_ISREF_P(prop) && Z_REFCOUNT_P(prop) == 1) {
+ prop = Z_REFVAL_P(prop);
+ }
+
+ ZVAL_COPY(&elem, prop);
+ if (key != NULL) {
+ result = zend_hash_add_new(Z_ARRVAL_P(state), key, &elem) != NULL;
+ } else {
+ result = zend_hash_index_add_new(Z_ARRVAL_P(state), num_key, &elem) != NULL;
+ }
+
+ if (!result) {
+ zval_ptr_dtor(&elem);
+
+ break;
+ }
+ } ZEND_HASH_FOREACH_END();
+
+ zend_release_properties(props);
+
+ if (!result) {
+ zval_ptr_dtor(state);
+ ZVAL_UNDEF(state);
+ }
+
+ return result;
+}
+
+static bool user_cache_shared_graph_extract_sleep_snapshot(const zval *value, zval *state)
+{
+ const char *msg = NULL;
+
+ if (!php_user_cache_serdes_get_sleep_state((zval *) value, state, &msg)) {
+ if (msg != NULL) {
+ zend_type_error("%s", msg);
+ }
+
+ return false;
+ }
+
+ return true;
+}
+
+static bool user_cache_shared_graph_extract_unserialize_route_snapshot(const zval *value, zval *state)
+{
+ if (zend_hash_find_known_hash(&Z_OBJCE_P(value)->function_table, ZSTR_KNOWN(ZEND_STR_SLEEP)) != NULL) {
+ return user_cache_shared_graph_extract_sleep_snapshot(value, state);
+ }
+
+ return user_cache_shared_graph_extract_property_snapshot(value, state);
+}
+
+/* A NULL memo means the write lock is held, so state hooks may not run. */
+static bool user_cache_shared_graph_get_memoized_state(
+ const zval *value,
+ HashTable *state_memo,
+ bool (*produce_state)(const zval *value, zval *state),
+ zval **state_ptr,
+ zval *owned_state)
+{
+ zend_ulong memo_key;
+ zval *memo_state;
+
+ ZVAL_UNDEF(owned_state);
+ *state_ptr = NULL;
+
+ if (state_memo == NULL) {
+ return false;
+ }
+
+ memo_key = (zend_ulong) (uintptr_t) Z_OBJ_P(value);
+ 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;
+ }
+
+ if (!produce_state(value, owned_state)) {
+ if (!Z_ISUNDEF_P(owned_state)) {
+ zval_ptr_dtor(owned_state);
+ ZVAL_UNDEF(owned_state);
+ }
+
+ return false;
+ }
+
+ 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;
+}
+
+/* Safe-direct state may be produced under the write lock. */
+static bool user_cache_shared_graph_get_safe_direct_state(
+ const zval *value,
+ HashTable *state_memo,
+ zval **state_ptr,
+ zval *owned_state)
+{
+ if (state_memo != NULL) {
+ return user_cache_shared_graph_get_memoized_state(
+ value,
+ state_memo,
+ user_cache_shared_graph_produce_safe_direct_state,
+ state_ptr,
+ owned_state
+ );
+ }
+
+ ZVAL_UNDEF(owned_state);
+ *state_ptr = NULL;
+
+ if (!user_cache_shared_graph_produce_safe_direct_state(value, owned_state)) {
+ if (!Z_ISUNDEF_P(owned_state)) {
+ zval_ptr_dtor(owned_state);
+ ZVAL_UNDEF(owned_state);
+ }
+
+ return false;
+ }
+
+ *state_ptr = owned_state;
+
+ return true;
+}
+
+static bool user_cache_shared_graph_get_magic_serialized_state(
+ const zval *value,
+ HashTable *state_memo,
+ zval **state_ptr,
+ zval *owned_state)
+{
+ return user_cache_shared_graph_get_memoized_state(
+ value,
+ state_memo,
+ user_cache_shared_graph_extract_serialize_snapshot,
+ state_ptr,
+ owned_state
+ );
+}
+
+static bool user_cache_shared_graph_get_magic_unserialized_state(
+ const zval *value,
+ HashTable *state_memo,
+ zval **state_ptr,
+ zval *owned_state)
+{
+ return user_cache_shared_graph_get_memoized_state(
+ value,
+ state_memo,
+ user_cache_shared_graph_extract_unserialize_route_snapshot,
+ state_ptr,
+ owned_state
+ );
+}
+
+static bool user_cache_shared_graph_get_sleep_state(
+ const zval *value,
+ HashTable *state_memo,
+ zval **state_ptr,
+ zval *owned_state)
+{
+ return user_cache_shared_graph_get_memoized_state(
+ value,
+ state_memo,
+ user_cache_shared_graph_extract_sleep_snapshot,
+ state_ptr,
+ owned_state
+ );
+}
+
+static bool user_cache_shared_graph_get_wakeup_state(
+ const zval *value,
+ HashTable *state_memo,
+ zval **state_ptr,
+ zval *owned_state)
+{
+ return user_cache_shared_graph_get_memoized_state(
+ value,
+ state_memo,
+ user_cache_shared_graph_extract_property_snapshot,
+ state_ptr,
+ owned_state
+ );
+}
+
+static bool user_cache_shared_graph_get_serdes_blob(
+ const zval *value,
+ HashTable *state_memo,
+ zend_string **blob_ptr)
+{
+ const char *msg = NULL;
+ zend_ulong memo_key;
+ zend_string *blob;
+ zend_object *obj = Z_OBJ_P(value);
+ zval *memo_state, blob_zv;
+ smart_str buf = {0};
+
+ *blob_ptr = NULL;
+
+ /* Serialization may invoke user code and is forbidden under the write lock. */
+ if (state_memo == NULL) {
+ return false;
+ }
+
+ memo_key = (zend_ulong) (uintptr_t) obj;
+ memo_state = zend_hash_index_find(state_memo, memo_key);
+ if (memo_state != NULL) {
+ if (Z_TYPE_P(memo_state) != IS_STRING) {
+ return false;
+ }
+
+ *blob_ptr = Z_STR_P(memo_state);
+
+ return true;
+ }
+
+ if (!php_user_cache_serdes_encode((zval *) value, &buf, &msg)) {
+ if (msg != NULL) {
+ zend_type_error("%s", msg);
+ }
+
+ smart_str_free(&buf);
+
+ return false;
+ }
+
+ blob = smart_str_extract(&buf);
+ if (ZSTR_LEN(blob) == 0 || ZSTR_LEN(blob) > UINT32_MAX) {
+ zend_string_release(blob);
+
+ return false;
+ }
+
+ ZVAL_STR(&blob_zv, blob);
+ memo_state = zend_hash_index_add(state_memo, memo_key, &blob_zv);
+ if (memo_state == NULL) {
+ zend_string_release(blob);
+
+ return false;
+ }
+
+ *blob_ptr = Z_STR_P(memo_state);
+
+ return true;
+}
+
+static php_user_cache_shared_graph_state_getter_t user_cache_shared_graph_route_state_getter(
+ php_user_cache_object_route route)
+{
+ switch (route) {
+ case PHP_USER_CACHE_OBJECT_ROUTE_MAGIC_SERIALIZE:
+ case PHP_USER_CACHE_OBJECT_ROUTE_SERIALIZE_PROPS:
+ return user_cache_shared_graph_get_magic_serialized_state;
+ case PHP_USER_CACHE_OBJECT_ROUTE_MAGIC_UNSERIALIZE:
+ return user_cache_shared_graph_get_magic_unserialized_state;
+ case PHP_USER_CACHE_OBJECT_ROUTE_SLEEP:
+ return user_cache_shared_graph_get_sleep_state;
+ case PHP_USER_CACHE_OBJECT_ROUTE_WAKEUP:
+ return user_cache_shared_graph_get_wakeup_state;
+ default:
+ return NULL;
+ }
+}
+
+static bool user_cache_shared_graph_calc_magic_state_object(
+ php_user_cache_shared_graph_calc_context *ctx,
+ const zval *value,
+ zend_object *obj,
+ php_user_cache_object_route route)
+{
+ php_user_cache_shared_graph_state_getter_t get_state;
+ zval *state_ptr, state;
+ bool result;
+
+ /* Reserve shared objects only once. */
+ if (!php_user_cache_seen_test_and_add(&ctx->seen_objects, obj)) {
+ return true;
+ }
+
+ get_state = user_cache_shared_graph_route_state_getter(route);
+ if (!get_state(value, ctx->state_memo, &state_ptr, &state)) {
+ return false;
+ }
+
+ if (user_cache_shared_graph_state_array_fits_schema(Z_ARRVAL_P(state_ptr))) {
+ result = user_cache_shared_graph_calc_shaped_state_object(
+ ctx,
+ obj->ce,
+ Z_ARRVAL_P(state_ptr)
+ );
+ } else {
+ result = user_cache_shared_graph_calc_reserve(ctx,
+ sizeof(php_user_cache_shared_graph_safe_direct_object)) &&
+ user_cache_shared_graph_calc_reserve_string(ctx, obj->ce->name) &&
+ user_cache_shared_graph_calc_value(ctx, state_ptr)
+ ;
+ }
+
+ if (!Z_ISUNDEF(state)) {
+ zval_ptr_dtor(&state);
+ }
+
+ return result;
+}
+
+static bool user_cache_shared_graph_calc_sleep_state_object(
+ php_user_cache_shared_graph_calc_context *ctx,
+ const zval *value,
+ zend_object *obj,
+ php_user_cache_object_route route)
+{
+ php_user_cache_shared_graph_state_getter_t get_state;
+ zend_ulong num_key;
+ zend_string *prop_name, *resolved_name;
+ zval *state_ptr, state, *prop_val;
+ HashTable *props;
+ uint32_t prop_count;
+ bool result;
+
+ if (!php_user_cache_seen_test_and_add(&ctx->seen_objects, obj)) {
+ return true;
+ }
+
+ get_state = user_cache_shared_graph_route_state_getter(route);
+ if (!get_state(value, ctx->state_memo, &state_ptr, &state)) {
+ return false;
+ }
+
+ props = Z_ARRVAL_P(state_ptr);
+ if (user_cache_shared_graph_state_array_fits_schema(props)) {
+ result = user_cache_shared_graph_calc_shaped_state_object(
+ ctx,
+ obj->ce,
+ props
+ );
+ } else {
+ prop_count = zend_hash_num_elements(props);
+ result = user_cache_shared_graph_calc_reserve(
+ ctx,
+ sizeof(php_user_cache_shared_graph_object)
+ ) &&
+ user_cache_shared_graph_calc_reserve_string(ctx, obj->ce->name) &&
+ (prop_count == 0 ||
+ user_cache_shared_graph_calc_reserve(
+ ctx,
+ (size_t) prop_count * sizeof(php_user_cache_shared_graph_property)
+ )
+ )
+ ;
+
+ if (result) {
+ /* Native serialization restores integer keys as property names. */
+ ZEND_HASH_FOREACH_KEY_VAL(props, num_key, prop_name, prop_val) {
+ resolved_name = prop_name != NULL
+ ? zend_string_copy(prop_name)
+ : zend_long_to_str((zend_long) num_key)
+ ;
+
+ if (!user_cache_shared_graph_calc_reserve_string(ctx, resolved_name) ||
+ !user_cache_shared_graph_calc_value(ctx, prop_val)
+ ) {
+ zend_string_release(resolved_name);
+ result = false;
+
+ break;
+ }
+
+ zend_string_release(resolved_name);
+ } ZEND_HASH_FOREACH_END();
+ }
+ }
+
+ if (!Z_ISUNDEF(state)) {
+ zval_ptr_dtor(&state);
+ }
+
+ return result;
+}
+
+static bool user_cache_shared_graph_calc_safe_direct_object(
+ php_user_cache_shared_graph_calc_context *ctx,
+ const zval *value,
+ zend_object *obj)
+{
+ zend_string *prop_name;
+ zval *prop_val, *src_val, *state_ptr, state;
+ const HashTable *state_ht;
+ HashTable *props;
+ uint32_t prop_count;
+ bool result;
+
+ if (!user_cache_shared_graph_get_safe_direct_state(
+ value,
+ ctx->state_memo,
+ &state_ptr,
+ &state
+ )
+ ) {
+ return false;
+ }
+
+ /* state_ptr may dangle once recursion resizes the memo; keep the array. */
+ state_ht = Z_TYPE_P(state_ptr) == IS_ARRAY ? Z_ARRVAL_P(state_ptr) : NULL;
+
+ /* Reserve shared objects only once. */
+ if (!php_user_cache_seen_test_and_add(&ctx->seen_objects, obj)) {
+ result = true;
+
+ goto cleanup;
+ }
+
+ if (!user_cache_shared_graph_calc_reserve(ctx,
+ sizeof(php_user_cache_shared_graph_safe_direct_object)) ||
+ !user_cache_shared_graph_calc_reserve_string(ctx, obj->ce->name) ||
+ !user_cache_shared_graph_calc_value(ctx, state_ptr)
+ ) {
+ result = false;
+
+ goto cleanup;
+ }
+
+ props = zend_std_get_properties(obj);
+ prop_count = props != NULL ? props->nNumOfElements : 0;
+ if (prop_count != 0 &&
+ !user_cache_shared_graph_calc_reserve(
+ ctx,
+ (size_t) prop_count * sizeof(php_user_cache_shared_graph_property)
+ )
+ ) {
+ result = false;
+
+ goto cleanup;
+ }
+
+ result = true;
+ if (props != NULL) {
+ ZEND_HASH_FOREACH_STR_KEY_VAL(props, prop_name, prop_val) {
+ if (prop_name == NULL) {
+ result = false;
+
+ break;
+ }
+
+ src_val = Z_TYPE_P(prop_val) == IS_INDIRECT
+ ? Z_INDIRECT_P(prop_val)
+ : prop_val
+ ;
+
+ if (user_cache_shared_graph_safe_direct_property_shadows_state(
+ prop_name,
+ state_ht
+ )
+ ) {
+ continue;
+ }
+
+ if (!user_cache_shared_graph_calc_reserve_string(ctx, prop_name) ||
+ !user_cache_shared_graph_calc_value(ctx, src_val)
+ ) {
+ result = false;
+
+ break;
+ }
+ } ZEND_HASH_FOREACH_END();
+ }
+
+cleanup:
+ if (!Z_ISUNDEF(state)) {
+ zval_ptr_dtor(&state);
+ }
+
+ return result;
+}
+
+static bool user_cache_shared_graph_calc_plain_object(
+ php_user_cache_shared_graph_calc_context *ctx,
+ const zval *value,
+ zend_object *obj)
+{
+ zend_string *prop_name;
+ zval *prop_val, *src_val;
+ HashTable *props;
+ uint32_t prop_count;
+
+ if (!php_user_cache_seen_test_and_add(&ctx->seen_objects, obj)) {
+ return true;
+ }
+
+ if (!user_cache_shared_graph_calc_reserve(
+ ctx,
+ sizeof(php_user_cache_shared_graph_object)
+ ) ||
+ !user_cache_shared_graph_calc_reserve_string(ctx, obj->ce->name)
+ ) {
+ return false;
+ }
+
+ props = zend_get_properties_for((zval *) value, ZEND_PROP_PURPOSE_SERIALIZE);
+ prop_count = props != NULL ? props->nNumOfElements : 0;
+ if (prop_count != 0 &&
+ !user_cache_shared_graph_calc_reserve(
+ ctx,
+ (size_t) prop_count * sizeof(php_user_cache_shared_graph_property)
+ )
+ ) {
+ if (props != NULL) {
+ zend_release_properties(props);
+ }
+
+ return false;
+ }
+
+ if (props != NULL) {
+ ZEND_HASH_FOREACH_STR_KEY_VAL(props, prop_name, prop_val) {
+ if (prop_name == NULL) {
+ zend_release_properties(props);
+
+ return false;
+ }
+
+ src_val = Z_TYPE_P(prop_val) == IS_INDIRECT
+ ? Z_INDIRECT_P(prop_val)
+ : prop_val
+ ;
+
+ if (!user_cache_shared_graph_calc_reserve_string(ctx, prop_name)) {
+ zend_release_properties(props);
+
+ return false;
+ }
+
+ if (!user_cache_shared_graph_calc_value(ctx, src_val)) {
+ zend_release_properties(props);
+
+ return false;
+ }
+ } ZEND_HASH_FOREACH_END();
+
+ zend_release_properties(props);
+ }
+
+ return true;
+}
+
+static bool user_cache_shared_graph_calc_object(
+ php_user_cache_shared_graph_calc_context *ctx,
+ const zval *value
+)
+{
+ zend_string *case_name, *serdes_blob;
+ zend_class_entry *ce;
+ zend_object *obj;
+
+ obj = Z_OBJ_P(value);
+ ce = obj->ce;
+ if (ce == zend_ce_closure) {
+ zend_type_error(PHP_USER_CACHE_MSG_CLOSURE_UNSTORABLE);
+
+ return false;
+ }
+
+ /* Do not initialize lazy objects while walking serialized state. */
+ if (zend_object_is_lazy(obj)) {
+ zend_type_error(PHP_USER_CACHE_MSG_LAZY_OBJECT_UNSTORABLE);
+
+ return false;
+ }
+
+ if (ce->ce_flags & ZEND_ACC_ENUM) {
+ case_name = Z_STR_P(zend_enum_fetch_case_name(obj));
+
+ return user_cache_shared_graph_calc_reserve(
+ ctx,
+ sizeof(php_user_cache_shared_graph_enum)
+ ) &&
+ user_cache_shared_graph_calc_reserve_string(ctx, ce->name) &&
+ user_cache_shared_graph_calc_reserve_string(ctx, case_name)
+ ;
+ }
+
+ switch (user_cache_shared_graph_classify_object_route(ce)) {
+ case PHP_USER_CACHE_OBJECT_ROUTE_SAFE_DIRECT:
+ return user_cache_shared_graph_calc_safe_direct_object(ctx, value, obj);
+ case PHP_USER_CACHE_OBJECT_ROUTE_MAGIC_SERIALIZE:
+ return user_cache_shared_graph_calc_magic_state_object(
+ ctx, value, obj, PHP_USER_CACHE_OBJECT_ROUTE_MAGIC_SERIALIZE
+ );
+ case PHP_USER_CACHE_OBJECT_ROUTE_SERIALIZE_PROPS:
+ return user_cache_shared_graph_calc_sleep_state_object(
+ ctx, value, obj, PHP_USER_CACHE_OBJECT_ROUTE_SERIALIZE_PROPS
+ );
+ case PHP_USER_CACHE_OBJECT_ROUTE_MAGIC_UNSERIALIZE:
+ return user_cache_shared_graph_calc_magic_state_object(
+ ctx, value, obj, PHP_USER_CACHE_OBJECT_ROUTE_MAGIC_UNSERIALIZE
+ );
+ case PHP_USER_CACHE_OBJECT_ROUTE_SLEEP:
+ return user_cache_shared_graph_calc_sleep_state_object(
+ ctx, value, obj, PHP_USER_CACHE_OBJECT_ROUTE_SLEEP
+ );
+ case PHP_USER_CACHE_OBJECT_ROUTE_WAKEUP:
+ return user_cache_shared_graph_calc_sleep_state_object(
+ ctx, value, obj, PHP_USER_CACHE_OBJECT_ROUTE_WAKEUP
+ );
+ case PHP_USER_CACHE_OBJECT_ROUTE_SERDES:
+ if (!php_user_cache_seen_test_and_add(&ctx->seen_objects, obj)) {
+ return true;
+ }
+
+ if (!user_cache_shared_graph_get_serdes_blob(value, ctx->state_memo, &serdes_blob)) {
+ return false;
+ }
+
+ return user_cache_shared_graph_calc_reserve(
+ ctx,
+ sizeof(php_user_cache_shared_graph_serdes_object) + ZSTR_LEN(serdes_blob)
+ );
+ case PHP_USER_CACHE_OBJECT_ROUTE_PLAIN:
+ return user_cache_shared_graph_calc_plain_object(ctx, value, obj);
+ case PHP_USER_CACHE_OBJECT_ROUTE_UNSTORABLE:
+ return false;
+ }
+
+ return false;
+}
+
+static bool user_cache_shared_graph_calc_array(
+ php_user_cache_shared_graph_calc_context *ctx,
+ const zval *value
+)
+{
+ const HashTable *arr;
+ zend_ulong arr_key;
+ zend_string *key;
+ zval *elem, *verdict, verdict_zv;
+ bool result, verbatim;
+
+ arr = Z_ARRVAL_P(value);
+
+ if (arr->nNumOfElements == 0) {
+ if (arr->nNextFreeElement != 0) {
+ return user_cache_shared_graph_calc_reserve(ctx,
+ sizeof(php_user_cache_shared_graph_array))
+ ;
+ }
+
+ return true;
+ }
+
+ if (ctx->verbatim_arrays_allowed) {
+ if (GC_FLAGS(arr) & IS_ARRAY_IMMUTABLE) {
+ return user_cache_shared_graph_calc_verbatim_value(ctx, value);
+ }
+
+ /* A state hook may free an array keyed by address in the verdict map. */
+ verdict = NULL;
+ if (ctx->shared_verdicts != NULL &&
+ (ctx->state_memo == NULL || zend_hash_num_elements(ctx->state_memo) == 0)
+ ) {
+ verdict = zend_hash_index_find(ctx->shared_verdicts, (zend_ulong) (uintptr_t) arr);
+ }
+
+ if (verdict != NULL) {
+ verbatim = Z_TYPE_P(verdict) == IS_TRUE;
+ } else {
+ verbatim = user_cache_shared_graph_can_copy_verbatim_value(
+ &ctx->seen_arrays,
+ &ctx->direct_verdicts,
+ value
+ );
+
+ if (ctx->shared_verdicts != NULL) {
+ ZVAL_BOOL(&verdict_zv, verbatim);
+ zend_hash_index_add(ctx->shared_verdicts, (zend_ulong) (uintptr_t) arr, &verdict_zv);
+ }
+ }
+
+ if (verbatim) {
+ return user_cache_shared_graph_calc_verbatim_value(ctx, value);
+ }
+ }
+
+ if (!php_user_cache_seen_test_and_add(&ctx->seen_arrays, arr)) {
+ return true;
+ }
+
+ arr_key = (zend_ulong) (uintptr_t) arr;
+ result = true;
+
+ if (user_cache_shared_graph_array_has_shape(arr)) {
+ if (!user_cache_shared_graph_calc_reserve(
+ ctx,
+ sizeof(php_user_cache_shared_graph_shaped_array)
+ ) ||
+ !user_cache_shared_graph_calc_reserve(
+ ctx,
+ (size_t) arr->nNumOfElements * sizeof(php_user_cache_shared_graph_value)
+ ) ||
+ !user_cache_shared_graph_calc_array_shape(ctx, arr, NULL)
+ ) {
+ result = false;
+
+ goto done;
+ }
+
+ ZEND_HASH_FOREACH_VAL((HashTable *) arr, elem) {
+ if (!user_cache_shared_graph_calc_value(ctx, elem)) {
+ result = false;
+
+ break;
+ }
+ } ZEND_HASH_FOREACH_END();
+
+ goto done;
+ }
+
+ if (!user_cache_shared_graph_calc_reserve(ctx, sizeof(php_user_cache_shared_graph_array)) ||
+ !user_cache_shared_graph_calc_reserve(
+ ctx,
+ (size_t) arr->nNumOfElements * sizeof(php_user_cache_shared_graph_array_element)
+ )
+ ) {
+ result = false;
+
+ goto done;
+ }
+
+ ZEND_HASH_FOREACH_STR_KEY_VAL((HashTable *) arr, key, elem) {
+ if (key != NULL && !user_cache_shared_graph_calc_reserve_string(ctx, key)) {
+ result = false;
+
+ break;
+ }
+
+ if (!user_cache_shared_graph_calc_value(ctx, elem)) {
+ result = false;
+
+ break;
+ }
+ } ZEND_HASH_FOREACH_END();
+
+done:
+ zend_hash_index_del(&ctx->seen_arrays, arr_key);
+
+ return result;
+}
+
+static bool user_cache_shared_graph_calc_value(
+ php_user_cache_shared_graph_calc_context *ctx,
+ const zval *value
+)
+{
+ zend_reference *ref;
+
+ if (php_user_cache_stack_overflowed()) {
+ return false;
+ }
+
+ 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 user_cache_shared_graph_calc_reserve_string(ctx, Z_STR_P(value));
+ case IS_RESOURCE:
+ zend_type_error(PHP_USER_CACHE_MSG_RESOURCE_UNSTORABLE);
+
+ return false;
+ case IS_ARRAY:
+ return user_cache_shared_graph_calc_array(ctx, value);
+ case IS_OBJECT:
+ return user_cache_shared_graph_calc_object(ctx, value);
+ case IS_REFERENCE:
+ ref = Z_REF_P(value);
+
+ if (!php_user_cache_seen_test_and_add(&ctx->seen_references, ref)) {
+ return true;
+ }
+
+ if (!user_cache_shared_graph_calc_reserve(ctx,
+ sizeof(php_user_cache_shared_graph_reference))
+ ) {
+ return false;
+ }
+
+ return user_cache_shared_graph_calc_value(ctx, &ref->val);
+ default:
+ return false;
+ }
+}
+
+static void user_cache_shared_graph_copy_init(
+ php_user_cache_shared_graph_copy_context *ctx,
+ uint8_t *buf,
+ size_t size
+)
+{
+ ctx->buffer = buf;
+ ctx->size = size;
+ ctx->position = 0;
+ ctx->fixup_offsets = NULL;
+ ctx->fixup_count = 0;
+ ctx->fixup_capacity = 0;
+
+ zend_hash_init(&ctx->seen_arrays, 8, NULL, NULL, 0);
+ zend_hash_init(&ctx->seen_objects, 8, NULL, NULL, 0);
+ zend_hash_init(&ctx->seen_references, 8, NULL, NULL, 0);
+ zend_hash_init(&ctx->string_dedup, 8, NULL, NULL, 0);
+ zend_hash_init(&ctx->array_shape_dedup, 8, NULL, NULL, 0);
+ zend_hash_init(&ctx->state_schema_dedup, 8, NULL, NULL, 0);
+ zend_hash_init(&ctx->direct_array_dedup, 8, NULL, NULL, 0);
+ zend_hash_init(&ctx->direct_verdicts, 8, NULL, NULL, 0);
+
+ ctx->has_shared_identity = false;
+ ctx->has_object = false;
+ ctx->prefers_prototype = false;
+ ctx->has_userland_restore_object = false;
+ ctx->has_verbatim_array = false;
+ ctx->verbatim_arrays_allowed = user_cache_shared_graph_can_use_verbatim_arrays();
+ ctx->shared_verdicts = NULL;
+}
+
+static void user_cache_shared_graph_copy_destroy(php_user_cache_shared_graph_copy_context *ctx)
+{
+ if (ctx->fixup_offsets != NULL) {
+ efree(ctx->fixup_offsets);
+
+ ctx->fixup_offsets = NULL;
+ }
+
+ zend_hash_destroy(&ctx->direct_verdicts);
+ zend_hash_destroy(&ctx->direct_array_dedup);
+ zend_hash_destroy(&ctx->state_schema_dedup);
+ zend_hash_destroy(&ctx->array_shape_dedup);
+ zend_hash_destroy(&ctx->string_dedup);
+ zend_hash_destroy(&ctx->seen_references);
+ zend_hash_destroy(&ctx->seen_objects);
+ zend_hash_destroy(&ctx->seen_arrays);
+}
+
+/* Pack the flags displacement into the two free alignment bits. */
+static bool user_cache_shared_graph_seen_record_object_offsets(
+ php_user_cache_shared_graph_copy_context *ctx,
+ const zend_object *obj,
+ uint32_t obj_offset,
+ uint32_t flags_offset
+)
+{
+ uint32_t delta;
+
+ delta = flags_offset - obj_offset;
+
+ ZEND_ASSERT((obj_offset & 3) == 0);
+ ZEND_ASSERT((delta == 4 || delta == 12) && "unexpected reserved/flags field offset");
+
+ return zend_hash_index_add_ptr(
+ &ctx->seen_objects,
+ (zend_ulong) (uintptr_t) obj,
+ (void *) (uintptr_t) (obj_offset | (delta >> 2))
+ ) != NULL;
+}
+
+static bool user_cache_shared_graph_seen_promote_object_to_shared(
+ php_user_cache_shared_graph_copy_context *ctx,
+ const zend_object *obj,
+ uint32_t *obj_offset
+)
+{
+ void *seen_offset;
+ uint32_t packed, flags_offset;
+
+ seen_offset = zend_hash_index_find_ptr(&ctx->seen_objects, (zend_ulong) (uintptr_t) obj);
+ if (seen_offset == NULL) {
+ return false;
+ }
+
+ packed = (uint32_t) (uintptr_t) seen_offset;
+ *obj_offset = packed & ~(uint32_t) 3;
+ flags_offset = *obj_offset + ((packed & 3) << 2);
+
+ *(uint32_t *) (ctx->buffer + flags_offset) |= PHP_USER_CACHE_SHARED_GRAPH_OBJECT_FLAG_SHARED;
+ ctx->has_shared_identity = true;
+
+ return true;
+}
+
+static php_user_cache_copy_object_ref_result user_cache_shared_graph_copy_emit_object_ref_if_seen(
+ php_user_cache_shared_graph_copy_context *ctx,
+ const zend_object *obj,
+ php_user_cache_shared_graph_value *dst
+)
+{
+ uint32_t shared_offset;
+
+ if (zend_hash_index_find_ptr(&ctx->seen_objects, (zend_ulong) (uintptr_t) obj) == NULL) {
+ return PHP_USER_CACHE_COPY_OBJECT_REF_NEW;
+ }
+
+ if (!user_cache_shared_graph_seen_promote_object_to_shared(ctx, obj, &shared_offset)) {
+ return PHP_USER_CACHE_COPY_OBJECT_REF_ERROR;
+ }
+
+ dst->type = PHP_USER_CACHE_SHARED_GRAPH_VALUE_OBJECT_REF;
+ dst->payload.offset = shared_offset;
+
+ return PHP_USER_CACHE_COPY_OBJECT_REF_EMITTED;
+}
+
+/* Callers must initialize the full unaligned region. */
+static bool user_cache_shared_graph_copy_alloc(
+ php_user_cache_shared_graph_copy_context *ctx,
+ size_t amount,
+ uint32_t *offset
+)
+{
+ size_t aligned_amount;
+
+ aligned_amount = PHP_USER_CACHE_ALIGNED_SIZE(amount);
+ if (ctx->position > ctx->size || aligned_amount > ctx->size - ctx->position) {
+ return false;
+ }
+
+ *offset = (uint32_t) ctx->position;
+
+ if (aligned_amount > amount) {
+ memset(ctx->buffer + ctx->position + amount, 0, aligned_amount - amount);
+ }
+
+ ctx->position += aligned_amount;
+
+ return true;
+}
+
+/* Record absolute pointer slots for relocation. */
+static zend_always_inline void user_cache_shared_graph_copy_record_fixup(
+ php_user_cache_shared_graph_copy_context *ctx,
+ const void *slot)
+{
+ ZEND_ASSERT(user_cache_shared_graph_pointer_in_range(slot, ctx->buffer, ctx->size));
+ ZEND_ASSERT(((uintptr_t) slot & (sizeof(uintptr_t) - 1)) == 0);
+
+ if (ctx->fixup_count == ctx->fixup_capacity) {
+ ctx->fixup_capacity = ctx->fixup_capacity == 0 ? 8 : ctx->fixup_capacity * 2;
+ ctx->fixup_offsets = erealloc(ctx->fixup_offsets, sizeof(*ctx->fixup_offsets) * ctx->fixup_capacity);
+ }
+
+ ctx->fixup_offsets[ctx->fixup_count++] = (uint32_t) ((const uint8_t *) slot - ctx->buffer);
+}
+
+static bool user_cache_shared_graph_copy_string(
+ php_user_cache_shared_graph_copy_context *ctx,
+ const zend_string *str,
+ uint32_t *offset
+)
+{
+ zend_string *new_str;
+ zval *cached, cached_offset;
+ uint32_t str_offset;
+ size_t str_size;
+
+ cached = zend_hash_find(&ctx->string_dedup, (zend_string *) str);
+ if (cached != NULL) {
+ *offset = (uint32_t) Z_LVAL_P(cached);
+
+ return true;
+ }
+
+ str_size = _ZSTR_STRUCT_SIZE(ZSTR_LEN(str));
+ if (!user_cache_shared_graph_copy_alloc(ctx, str_size, &str_offset)) {
+ return false;
+ }
+
+ new_str = (zend_string *) (ctx->buffer + str_offset);
+
+ memcpy(new_str, str, str_size);
+
+ GC_SET_REFCOUNT(new_str, 2);
+ GC_TYPE_INFO(new_str) = GC_STRING | ((IS_STR_INTERNED | IS_STR_PERMANENT) << GC_FLAGS_SHIFT);
+ *offset = str_offset;
+
+ ZVAL_LONG(&cached_offset, (zend_long) str_offset);
+ zend_hash_add(&ctx->string_dedup, (zend_string *) str, &cached_offset);
+
+ return true;
+}
+
+static bool user_cache_shared_graph_copy_array_shape(
+ php_user_cache_shared_graph_copy_context *ctx,
+ const HashTable *arr,
+ uint32_t *offset,
+ zend_string *shape_key
+)
+{
+ php_user_cache_shared_graph_array_shape *graph_shape;
+ php_user_cache_shared_graph_array_shape_element *shape_elems, *shape_elem;
+ zend_string *key;
+ zval *cached, cached_offset;
+ uint32_t shape_offset, elems_offset, key_offset;
+ bool owns_shape_key = false;
+
+ if (shape_key == NULL) {
+ shape_key = user_cache_shared_graph_array_shape_key(arr);
+ if (shape_key == NULL) {
+ return false;
+ }
+
+ owns_shape_key = true;
+ }
+
+ cached = zend_hash_find(&ctx->array_shape_dedup, shape_key);
+ if (cached != NULL) {
+ *offset = (uint32_t) Z_LVAL_P(cached);
+
+ if (owns_shape_key) {
+ zend_string_release(shape_key);
+ }
+
+ return true;
+ }
+
+ if (!user_cache_shared_graph_copy_alloc(ctx, sizeof(*graph_shape), &shape_offset) ||
+ !user_cache_shared_graph_copy_alloc(
+ ctx,
+ (size_t) arr->nNumOfElements * sizeof(*shape_elems),
+ &elems_offset
+ )
+ ) {
+ if (owns_shape_key) {
+ zend_string_release(shape_key);
+ }
+
+ return false;
+ }
+
+ graph_shape = (php_user_cache_shared_graph_array_shape *) (ctx->buffer + shape_offset);
+ graph_shape->count = (uint32_t) arr->nNumOfElements;
+ graph_shape->elements_offset = elems_offset;
+ graph_shape->reserved = 0;
+ graph_shape->reserved2 = 0;
+
+ shape_elems = (php_user_cache_shared_graph_array_shape_element *) (ctx->buffer + elems_offset);
+ shape_elem = shape_elems;
+
+ ZEND_HASH_FOREACH_STR_KEY((HashTable *) arr, key) {
+ ZEND_ASSERT(key != NULL);
+
+ if (!user_cache_shared_graph_copy_string(ctx, key, &key_offset)) {
+ if (owns_shape_key) {
+ zend_string_release(shape_key);
+ }
+
+ return false;
+ }
+
+ shape_elem->key_offset = key_offset;
+
+ ++shape_elem;
+ } ZEND_HASH_FOREACH_END();
+
+ ZVAL_LONG(&cached_offset, (zend_long) shape_offset);
+ if (zend_hash_add(&ctx->array_shape_dedup, shape_key, &cached_offset) == NULL) {
+ if (owns_shape_key) {
+ zend_string_release(shape_key);
+ }
+
+ return false;
+ }
+
+ *offset = shape_offset;
+
+ if (owns_shape_key) {
+ zend_string_release(shape_key);
+ }
+
+ return true;
+}
+
+static bool user_cache_shared_graph_copy_state_schema(
+ php_user_cache_shared_graph_copy_context *ctx,
+ zend_class_entry *ce,
+ const HashTable *arr,
+ uint32_t *offset
+)
+{
+ php_user_cache_shared_graph_state_schema *schema;
+ zend_string *schema_key, *shape_key = NULL;
+ zval *cached, cached_offset;
+ uint32_t schema_offset, class_name_offset, shape_offset;
+
+ schema_key = user_cache_shared_graph_state_schema_key(ce, arr, &shape_key);
+ if (schema_key == NULL) {
+ return false;
+ }
+
+ cached = zend_hash_find(&ctx->state_schema_dedup, schema_key);
+ if (cached != NULL) {
+ *offset = (uint32_t) Z_LVAL_P(cached);
+ zend_string_release(shape_key);
+ zend_string_release(schema_key);
+
+ return true;
+ }
+
+ if (!user_cache_shared_graph_copy_alloc(ctx, sizeof(*schema), &schema_offset) ||
+ !user_cache_shared_graph_copy_string(ctx, ce->name, &class_name_offset) ||
+ !user_cache_shared_graph_copy_array_shape(ctx, arr, &shape_offset, shape_key)
+ ) {
+ zend_string_release(shape_key);
+ zend_string_release(schema_key);
+
+ return false;
+ }
+
+ schema = (php_user_cache_shared_graph_state_schema *) (ctx->buffer + schema_offset);
+ schema->class_name_offset = class_name_offset;
+ schema->shape_offset = shape_offset;
+ schema->count = (uint32_t) arr->nNumOfElements;
+
+ zend_string_release(shape_key);
+
+ ZVAL_LONG(&cached_offset, (zend_long) schema_offset);
+ if (zend_hash_add(&ctx->state_schema_dedup, schema_key, &cached_offset) == NULL) {
+ zend_string_release(schema_key);
+
+ return false;
+ }
+
+ *offset = schema_offset;
+ zend_string_release(schema_key);
+
+ return true;
+}
+
+static bool user_cache_shared_graph_copy_shaped_state_values(
+ php_user_cache_shared_graph_copy_context *ctx,
+ const HashTable *state_array,
+ uint32_t values_offset
+)
+{
+ php_user_cache_shared_graph_value *graph_vals;
+ zval *elem;
+ uint32_t i;
+
+ graph_vals = (php_user_cache_shared_graph_value *) (ctx->buffer + values_offset);
+ i = 0;
+
+ ZEND_HASH_FOREACH_VAL((HashTable *) state_array, elem) {
+ if (!user_cache_shared_graph_copy_value(ctx, elem, &graph_vals[i])) {
+ return false;
+ }
+
+ ++i;
+ } ZEND_HASH_FOREACH_END();
+
+ return i == state_array->nNumOfElements;
+}
+
+static bool user_cache_shared_graph_copy_verbatim_value(
+ php_user_cache_shared_graph_copy_context *ctx,
+ const zval *src,
+ zval *dst)
+{
+ const zval *src_packed;
+ const HashTable *src_arr;
+ const Bucket *src_bucket;
+ zend_ulong arr_key;
+ zend_array *target;
+ zval *dst_packed, *cached, cached_offset;
+ Bucket *dst_bucket;
+ uint32_t i, string_offset, array_offset, data_offset, key_offset;
+ size_t data_size;
+ bool result = true;
+
+ if (php_user_cache_stack_overflowed()) {
+ return false;
+ }
+
+ switch (Z_TYPE_P(src)) {
+ case IS_UNDEF:
+ ZVAL_UNDEF(dst);
+ return true;
+ case IS_NULL:
+ ZVAL_NULL(dst);
+ return true;
+ case IS_TRUE:
+ ZVAL_TRUE(dst);
+ return true;
+ case IS_FALSE:
+ ZVAL_FALSE(dst);
+ return true;
+ case IS_LONG:
+ ZVAL_LONG(dst, Z_LVAL_P(src));
+ return true;
+ case IS_DOUBLE:
+ ZVAL_DOUBLE(dst, Z_DVAL_P(src));
+ return true;
+ case IS_STRING:
+ if (!user_cache_shared_graph_copy_string(ctx, Z_STR_P(src), &string_offset)) {
+ return false;
+ }
+
+ ZVAL_INTERNED_STR(dst, (zend_string *) (void *) (ctx->buffer + string_offset));
+
+ /* Stack roots do not need relocation fixups. */
+ if (user_cache_shared_graph_pointer_in_range(dst, ctx->buffer, ctx->size)) {
+ user_cache_shared_graph_copy_record_fixup(ctx, dst);
+ }
+
+ return true;
+ case IS_ARRAY:
+ src_arr = Z_ARRVAL_P(src);
+ if (src_arr->nNumOfElements == 0) {
+ ZVAL_EMPTY_ARRAY(dst);
+
+ return true;
+ }
+
+ if (GC_REFCOUNT(src_arr) > 1) {
+ cached = zend_hash_index_find(&ctx->direct_array_dedup, (zend_ulong) (uintptr_t) src_arr);
+ if (cached != NULL) {
+ ZVAL_ARR(dst, (zend_array *) (void *) (ctx->buffer + (uint32_t) Z_LVAL_P(cached)));
+ Z_TYPE_FLAGS_P(dst) = 0;
+
+ if (user_cache_shared_graph_pointer_in_range(dst, ctx->buffer, ctx->size)) {
+ user_cache_shared_graph_copy_record_fixup(ctx, dst);
+ }
+
+ return true;
+ }
+ }
+
+ if (!php_user_cache_seen_test_and_add(&ctx->seen_arrays, src_arr)) {
+ return false;
+ }
+
+ arr_key = (zend_ulong) (uintptr_t) src_arr;
+ data_size = HT_IS_PACKED(src_arr) ? HT_PACKED_USED_SIZE(src_arr) : HT_USED_SIZE(src_arr);
+ if (!user_cache_shared_graph_copy_alloc(ctx, sizeof(zend_array), &array_offset) ||
+ !user_cache_shared_graph_copy_alloc(ctx, data_size, &data_offset)
+ ) {
+ result = false;
+
+ goto done;
+ }
+
+ target = (zend_array *) (ctx->buffer + array_offset);
+
+ memcpy(target, src_arr, sizeof(zend_array));
+ memcpy(ctx->buffer + data_offset, HT_GET_DATA_ADDR(src_arr), 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;
+ HT_SET_ITERATORS_COUNT(target, 0);
+
+ target->pDestructor = NULL;
+ target->nInternalPointer = 0;
+
+ HT_SET_DATA_ADDR(target, ctx->buffer + data_offset);
+
+ /* arData moves with the containing buffer. */
+ user_cache_shared_graph_copy_record_fixup(ctx, &target->arData);
+
+ if (HT_IS_PACKED(src_arr)) {
+ dst_packed = target->arPacked;
+ for (i = 0; i < src_arr->nNumUsed; i++) {
+ src_packed = &src_arr->arPacked[i];
+ if (!user_cache_shared_graph_copy_verbatim_value(ctx, src_packed, &dst_packed[i])) {
+ result = false;
+
+ break;
+ }
+ }
+ } else {
+ src_bucket = src_arr->arData;
+ dst_bucket = target->arData;
+ for (i = 0; i < src_arr->nNumUsed; i++) {
+ if (src_bucket[i].key != NULL) {
+ if (!user_cache_shared_graph_copy_string(
+ ctx,
+ src_bucket[i].key,
+ &key_offset
+ )
+ ) {
+ result = false;
+
+ break;
+ }
+
+ dst_bucket[i].key = (zend_string *) (void *) (ctx->buffer + key_offset);
+
+ user_cache_shared_graph_copy_record_fixup(ctx, &dst_bucket[i].key);
+ } else {
+ dst_bucket[i].key = NULL;
+ }
+
+ if (!user_cache_shared_graph_copy_verbatim_value(ctx, &src_bucket[i].val, &dst_bucket[i].val)) {
+ result = false;
+
+ break;
+ }
+ }
+ }
+
+done:
+ zend_hash_index_del(&ctx->seen_arrays, arr_key);
+
+ if (!result) {
+ return false;
+ }
+
+ if (GC_REFCOUNT(src_arr) > 1) {
+ ZVAL_LONG(&cached_offset, (zend_long) array_offset);
+ zend_hash_index_add(&ctx->direct_array_dedup, arr_key, &cached_offset);
+ }
+
+ ZVAL_ARR(dst, (zend_array *) (void *) (ctx->buffer + array_offset));
+ Z_TYPE_FLAGS_P(dst) = 0;
+
+ if (user_cache_shared_graph_pointer_in_range(dst, ctx->buffer, ctx->size)) {
+ user_cache_shared_graph_copy_record_fixup(ctx, dst);
+ }
+
+ return true;
+ default:
+ return false;
+ }
+}
+
+static bool user_cache_shared_graph_copy_shaped_state_object(
+ php_user_cache_shared_graph_copy_context *ctx,
+ zend_object *obj,
+ const HashTable *state_array,
+ uint8_t node_type,
+ php_user_cache_shared_graph_value *dst
+)
+{
+ php_user_cache_shared_graph_shaped_state_object *graph_shaped_state;
+ uint32_t shaped_state_offset, state_schema_offset, state_values_offset;
+
+ if (!user_cache_shared_graph_copy_alloc(
+ ctx,
+ sizeof(php_user_cache_shared_graph_shaped_state_object),
+ &shaped_state_offset
+ ) ||
+ !user_cache_shared_graph_copy_state_schema(
+ ctx,
+ obj->ce,
+ state_array,
+ &state_schema_offset
+ ) ||
+ !user_cache_shared_graph_copy_alloc(
+ ctx,
+ (size_t) state_array->nNumOfElements * sizeof(php_user_cache_shared_graph_value),
+ &state_values_offset
+ )
+ ) {
+ return false;
+ }
+
+ if (!user_cache_shared_graph_seen_record_object_offsets(
+ ctx,
+ obj,
+ shaped_state_offset,
+ shaped_state_offset + offsetof(php_user_cache_shared_graph_shaped_state_object, reserved)
+ )
+ ) {
+ return false;
+ }
+
+ graph_shaped_state = (php_user_cache_shared_graph_shaped_state_object *)
+ (ctx->buffer + shaped_state_offset)
+ ;
+ graph_shaped_state->state_schema_offset = state_schema_offset;
+ graph_shaped_state->reserved = 0;
+ graph_shaped_state->state_values_offset = state_values_offset;
+ graph_shaped_state->state_next_free = (uint32_t) state_array->nNextFreeElement;
+
+ if (!user_cache_shared_graph_copy_shaped_state_values(
+ ctx,
+ state_array,
+ state_values_offset
+ )
+ ) {
+ return false;
+ }
+
+ dst->type = node_type;
+ dst->payload.offset = shaped_state_offset;
+
+ return true;
+}
+
+static bool user_cache_shared_graph_copy_magic_state_object(
+ php_user_cache_shared_graph_copy_context *ctx,
+ const zval *src,
+ zend_object *obj,
+ php_user_cache_object_route route,
+ php_user_cache_shared_graph_value *dst)
+{
+ php_user_cache_shared_graph_safe_direct_object *graph_safe_direct;
+ php_user_cache_copy_object_ref_result ref_result;
+ php_user_cache_shared_graph_state_getter_t get_state;
+ zval *sd_state_ptr, sd_state;
+ uint32_t sd_offset, sd_class_offset;
+ bool result;
+
+ ctx->has_userland_restore_object = true;
+
+ ref_result = user_cache_shared_graph_copy_emit_object_ref_if_seen(ctx, obj, dst);
+ if (ref_result != PHP_USER_CACHE_COPY_OBJECT_REF_NEW) {
+ return ref_result == PHP_USER_CACHE_COPY_OBJECT_REF_EMITTED;
+ }
+
+ get_state = user_cache_shared_graph_route_state_getter(route);
+ if (!get_state(src, ctx->state_memo, &sd_state_ptr, &sd_state)) {
+ return false;
+ }
+
+ if (user_cache_shared_graph_state_array_fits_schema(Z_ARRVAL_P(sd_state_ptr))) {
+ result = user_cache_shared_graph_copy_shaped_state_object(
+ ctx,
+ obj,
+ Z_ARRVAL_P(sd_state_ptr),
+ PHP_USER_CACHE_SHARED_GRAPH_VALUE_SERIALIZED_SHAPED_OBJECT,
+ dst
+ );
+
+ goto cleanup;
+ }
+
+ if (!user_cache_shared_graph_copy_alloc(
+ ctx,
+ sizeof(php_user_cache_shared_graph_safe_direct_object), &sd_offset
+ ) ||
+ !user_cache_shared_graph_copy_string(ctx, obj->ce->name, &sd_class_offset)
+ ) {
+ result = false;
+
+ goto cleanup;
+ }
+
+ if (!user_cache_shared_graph_seen_record_object_offsets(
+ ctx,
+ obj,
+ sd_offset,
+ sd_offset + offsetof(php_user_cache_shared_graph_safe_direct_object, reserved)
+ )
+ ) {
+ result = false;
+
+ goto cleanup;
+ }
+
+ graph_safe_direct = (php_user_cache_shared_graph_safe_direct_object *) (ctx->buffer + sd_offset);
+ graph_safe_direct->class_name_offset = sd_class_offset;
+ graph_safe_direct->property_count = 0;
+ graph_safe_direct->properties_offset = 0;
+ graph_safe_direct->reserved = 0;
+
+ if (!user_cache_shared_graph_copy_value(ctx, sd_state_ptr, &graph_safe_direct->state)) {
+ result = false;
+
+ goto cleanup;
+ }
+
+ dst->type = PHP_USER_CACHE_SHARED_GRAPH_VALUE_SERIALIZED_OBJECT;
+ dst->payload.offset = sd_offset;
+ result = true;
+
+cleanup:
+ if (!Z_ISUNDEF(sd_state)) {
+ zval_ptr_dtor(&sd_state);
+ }
+
+ return result;
+}
+
+static bool user_cache_shared_graph_copy_sleep_state_object(
+ php_user_cache_shared_graph_copy_context *ctx,
+ const zval *src,
+ zend_object *obj,
+ php_user_cache_object_route route,
+ php_user_cache_shared_graph_value *dst)
+{
+ php_user_cache_shared_graph_object *graph_obj;
+ php_user_cache_shared_graph_property *graph_properties;
+ php_user_cache_copy_object_ref_result ref_result;
+ php_user_cache_shared_graph_state_getter_t get_state;
+ zend_ulong num_key;
+ zend_string *prop_name, *resolved_name;
+ zval *state_ptr, state, *prop_val;
+ HashTable *props;
+ uint32_t obj_offset, class_name_offset, properties_offset,
+ prop_idx, prop_count
+ ;
+ bool result;
+
+ /* Values requiring restore hooks cannot use request-local cloning. */
+ if (route != PHP_USER_CACHE_OBJECT_ROUTE_SERIALIZE_PROPS ||
+ zend_hash_find_known_hash(&obj->ce->function_table, ZSTR_KNOWN(ZEND_STR_WAKEUP)) != NULL
+ ) {
+ ctx->has_userland_restore_object = true;
+ }
+
+ ref_result = user_cache_shared_graph_copy_emit_object_ref_if_seen(ctx, obj, dst);
+ if (ref_result != PHP_USER_CACHE_COPY_OBJECT_REF_NEW) {
+ return ref_result == PHP_USER_CACHE_COPY_OBJECT_REF_EMITTED;
+ }
+
+ get_state = user_cache_shared_graph_route_state_getter(route);
+ if (!get_state(src, ctx->state_memo, &state_ptr, &state)) {
+ return false;
+ }
+
+ props = Z_ARRVAL_P(state_ptr);
+ if (user_cache_shared_graph_state_array_fits_schema(props)) {
+ result = user_cache_shared_graph_copy_shaped_state_object(
+ ctx,
+ obj,
+ props,
+ PHP_USER_CACHE_SHARED_GRAPH_VALUE_SLEEP_SHAPED_OBJECT,
+ dst
+ );
+
+ goto cleanup;
+ }
+
+ prop_count = zend_hash_num_elements(props);
+ properties_offset = 0;
+
+ if (!user_cache_shared_graph_copy_alloc(ctx, sizeof(*graph_obj), &obj_offset) ||
+ !user_cache_shared_graph_copy_string(ctx, obj->ce->name, &class_name_offset) ||
+ (prop_count != 0 &&
+ !user_cache_shared_graph_copy_alloc(
+ ctx,
+ (size_t) prop_count * sizeof(php_user_cache_shared_graph_property),
+ &properties_offset
+ )
+ )
+ ) {
+ result = false;
+
+ goto cleanup;
+ }
+
+ if (!user_cache_shared_graph_seen_record_object_offsets(
+ ctx,
+ obj,
+ obj_offset,
+ obj_offset + offsetof(php_user_cache_shared_graph_object, reserved)
+ )
+ ) {
+ result = false;
+
+ goto cleanup;
+ }
+
+ graph_obj = (php_user_cache_shared_graph_object *) (ctx->buffer + obj_offset);
+ graph_obj->class_name_offset = class_name_offset;
+ graph_obj->property_count = prop_count;
+ graph_obj->properties_offset = properties_offset;
+ graph_obj->reserved = 0;
+
+ if (prop_count == 0) {
+ dst->type = PHP_USER_CACHE_SHARED_GRAPH_VALUE_SLEEP_OBJECT;
+ dst->payload.offset = obj_offset;
+ result = true;
+
+ goto cleanup;
+ }
+
+ graph_properties = (php_user_cache_shared_graph_property *) (ctx->buffer + properties_offset);
+ prop_idx = 0;
+ result = true;
+
+ /* Native serialization restores integer keys as property names. */
+ ZEND_HASH_FOREACH_KEY_VAL(props, num_key, prop_name, prop_val) {
+ resolved_name = prop_name != NULL
+ ? zend_string_copy(prop_name)
+ : zend_long_to_str((zend_long) num_key)
+ ;
+
+ graph_properties[prop_idx].name_offset = 0;
+ graph_properties[prop_idx].reserved =
+ php_user_cache_serdes_declared_property_index_plus_one(obj->ce, resolved_name)
+ ;
+ graph_properties[prop_idx].value.type = PHP_USER_CACHE_SHARED_GRAPH_VALUE_UNDEF;
+
+ if (!user_cache_shared_graph_copy_string(
+ ctx,
+ resolved_name,
+ &graph_properties[prop_idx].name_offset
+ ) ||
+ !user_cache_shared_graph_copy_value(
+ ctx,
+ prop_val,
+ &graph_properties[prop_idx].value
+ )
+ ) {
+ zend_string_release(resolved_name);
+ result = false;
+
+ break;
+ }
+
+ zend_string_release(resolved_name);
+ ++prop_idx;
+ } ZEND_HASH_FOREACH_END();
+
+ if (result) {
+ dst->type = PHP_USER_CACHE_SHARED_GRAPH_VALUE_SLEEP_OBJECT;
+ dst->payload.offset = obj_offset;
+ }
+
+cleanup:
+ if (!Z_ISUNDEF(state)) {
+ zval_ptr_dtor(&state);
+ }
+
+ return result;
+}
+
+static bool user_cache_shared_graph_copy_safe_direct_object(
+ php_user_cache_shared_graph_copy_context *ctx,
+ const zval *src,
+ zend_object *obj,
+ php_user_cache_shared_graph_value *dst)
+{
+ const HashTable *sd_state_ht;
+ php_user_cache_shared_graph_safe_direct_object *graph_safe_direct;
+ php_user_cache_shared_graph_property *graph_properties;
+ php_user_cache_copy_object_ref_result ref_result;
+ zend_string *prop_name;
+ zval *prop_val, *src_val, *sd_state_ptr, sd_state;
+ HashTable *props;
+ uint32_t string_offset, prop_idx, prop_count,
+ sd_offset, sd_class_offset,
+ sd_props_offset
+ ;
+ bool result;
+
+ if (php_user_cache_safe_direct_prefers_request_local_prototype(obj->ce)) {
+ ctx->prefers_prototype = true;
+ }
+
+ ref_result = user_cache_shared_graph_copy_emit_object_ref_if_seen(ctx, obj, dst);
+ if (ref_result != PHP_USER_CACHE_COPY_OBJECT_REF_NEW) {
+ return ref_result == PHP_USER_CACHE_COPY_OBJECT_REF_EMITTED;
+ }
+
+ if (!user_cache_shared_graph_get_safe_direct_state(
+ src,
+ ctx->state_memo,
+ &sd_state_ptr,
+ &sd_state
+ )
+ ) {
+ return false;
+ }
+
+ /* sd_state_ptr may dangle once recursion resizes the memo; keep the array. */
+ sd_state_ht = Z_TYPE_P(sd_state_ptr) == IS_ARRAY ? Z_ARRVAL_P(sd_state_ptr) : NULL;
+
+ if (!user_cache_shared_graph_copy_alloc(
+ ctx,
+ sizeof(php_user_cache_shared_graph_safe_direct_object),
+ &sd_offset
+ ) ||
+ !user_cache_shared_graph_copy_string(
+ ctx,
+ obj->ce->name,
+ &sd_class_offset
+ )
+ ) {
+ result = false;
+
+ goto cleanup;
+ }
+
+ if (!user_cache_shared_graph_seen_record_object_offsets(
+ ctx,
+ obj,
+ sd_offset,
+ sd_offset + offsetof(php_user_cache_shared_graph_safe_direct_object, reserved)
+ )
+ ) {
+ result = false;
+
+ goto cleanup;
+ }
+
+ graph_safe_direct = (php_user_cache_shared_graph_safe_direct_object *) (ctx->buffer + sd_offset);
+ graph_safe_direct->class_name_offset = sd_class_offset;
+ graph_safe_direct->reserved = 0;
+
+ if (!user_cache_shared_graph_copy_value(ctx, sd_state_ptr, &graph_safe_direct->state)) {
+ result = false;
+
+ goto cleanup;
+ }
+
+ props = zend_std_get_properties(obj);
+ prop_count = props != NULL ? props->nNumOfElements : 0;
+ graph_safe_direct->property_count = prop_count;
+ graph_safe_direct->properties_offset = 0;
+ result = true;
+
+ if (prop_count != 0) {
+ if (!user_cache_shared_graph_copy_alloc(
+ ctx,
+ (size_t) prop_count * sizeof(php_user_cache_shared_graph_property),
+ &sd_props_offset
+ )
+ ) {
+ result = false;
+
+ goto cleanup;
+ }
+
+ graph_safe_direct->properties_offset = sd_props_offset;
+ graph_properties = (php_user_cache_shared_graph_property *) (ctx->buffer + sd_props_offset);
+ prop_idx = 0;
+
+ ZEND_HASH_FOREACH_STR_KEY_VAL(props, prop_name, prop_val) {
+ /* Zero the skipped slot; copy_alloc() only clears alignment padding. */
+ memset(&graph_properties[prop_idx], 0, sizeof(graph_properties[prop_idx]));
+
+ if (prop_name == NULL) {
+ result = false;
+
+ break;
+ }
+
+ src_val = Z_TYPE_P(prop_val) == IS_INDIRECT
+ ? Z_INDIRECT_P(prop_val)
+ : prop_val
+ ;
+
+ if (user_cache_shared_graph_safe_direct_property_shadows_state(
+ prop_name,
+ sd_state_ht
+ )
+ ) {
+ ++prop_idx;
+
+ continue;
+ }
+
+ if (!user_cache_shared_graph_copy_string(ctx, prop_name, &string_offset)) {
+ result = false;
+
+ break;
+ }
+
+ graph_properties[prop_idx].name_offset = string_offset;
+
+ if (!user_cache_shared_graph_copy_value(
+ ctx,
+ src_val,
+ &graph_properties[prop_idx].value
+ )
+ ) {
+ result = false;
+
+ break;
+ }
+
+ ++prop_idx;
+ } ZEND_HASH_FOREACH_END();
+
+ if (!result) {
+ goto cleanup;
+ }
+ }
+
+ dst->type = PHP_USER_CACHE_SHARED_GRAPH_VALUE_SAFE_DIRECT_OBJECT;
+ dst->payload.offset = sd_offset;
+
+cleanup:
+ if (!Z_ISUNDEF(sd_state)) {
+ zval_ptr_dtor(&sd_state);
+ }
+
+ return result;
+}
+
+static bool user_cache_shared_graph_copy_serdes_object(
+ php_user_cache_shared_graph_copy_context *ctx,
+ const zval *src,
+ zend_object *obj,
+ php_user_cache_shared_graph_value *dst)
+{
+ php_user_cache_shared_graph_serdes_object *graph_serdes;
+ php_user_cache_copy_object_ref_result ref_result;
+ zend_string *serdes_blob;
+ uint32_t serdes_offset;
+
+ ctx->has_userland_restore_object = true;
+
+ ref_result = user_cache_shared_graph_copy_emit_object_ref_if_seen(ctx, obj, dst);
+ if (ref_result != PHP_USER_CACHE_COPY_OBJECT_REF_NEW) {
+ return ref_result == PHP_USER_CACHE_COPY_OBJECT_REF_EMITTED;
+ }
+
+ if (!user_cache_shared_graph_get_serdes_blob(src, ctx->state_memo, &serdes_blob)) {
+ return false;
+ }
+
+ if (!user_cache_shared_graph_copy_alloc(
+ ctx,
+ sizeof(php_user_cache_shared_graph_serdes_object) + ZSTR_LEN(serdes_blob),
+ &serdes_offset
+ )
+ ) {
+ return false;
+ }
+
+ if (!user_cache_shared_graph_seen_record_object_offsets(
+ ctx,
+ obj,
+ serdes_offset,
+ serdes_offset + offsetof(php_user_cache_shared_graph_serdes_object, flags)
+ )
+ ) {
+ return false;
+ }
+
+ graph_serdes = (php_user_cache_shared_graph_serdes_object *) (ctx->buffer + serdes_offset);
+ graph_serdes->blob_len = (uint32_t) ZSTR_LEN(serdes_blob);
+ graph_serdes->flags = 0;
+
+ memcpy(graph_serdes + 1, ZSTR_VAL(serdes_blob), ZSTR_LEN(serdes_blob));
+
+ dst->type = PHP_USER_CACHE_SHARED_GRAPH_VALUE_SERDES_OBJECT;
+ dst->payload.offset = serdes_offset;
+
+ return true;
+}
+
+static bool user_cache_shared_graph_copy_plain_object(
+ php_user_cache_shared_graph_copy_context *ctx,
+ const zval *src,
+ zend_object *obj,
+ php_user_cache_shared_graph_value *dst)
+{
+ php_user_cache_shared_graph_object *graph_obj;
+ php_user_cache_shared_graph_property *graph_properties;
+ php_user_cache_copy_object_ref_result ref_result;
+ zend_string *prop_name;
+ zval *prop_val, *src_val;
+ HashTable *props;
+ uint32_t obj_offset, class_name_offset, properties_offset,
+ prop_idx, prop_count
+ ;
+
+ ctx->prefers_prototype = true;
+
+ ref_result = user_cache_shared_graph_copy_emit_object_ref_if_seen(ctx, obj, dst);
+ if (ref_result != PHP_USER_CACHE_COPY_OBJECT_REF_NEW) {
+ return ref_result == PHP_USER_CACHE_COPY_OBJECT_REF_EMITTED;
+ }
+
+ if (!user_cache_shared_graph_copy_alloc(
+ ctx,
+ sizeof(*graph_obj),
+ &obj_offset
+ ) ||
+ !user_cache_shared_graph_seen_record_object_offsets(
+ ctx,
+ obj,
+ obj_offset,
+ obj_offset + offsetof(php_user_cache_shared_graph_object, reserved)
+ ) ||
+ !user_cache_shared_graph_copy_string(
+ ctx,
+ obj->ce->name,
+ &class_name_offset
+ )
+ ) {
+ return false;
+ }
+
+ props = zend_get_properties_for((zval *) src, ZEND_PROP_PURPOSE_SERIALIZE);
+ prop_count = props != NULL ? props->nNumOfElements : 0;
+ properties_offset = 0;
+ if (prop_count != 0 &&
+ !user_cache_shared_graph_copy_alloc(
+ ctx,
+ ((size_t) prop_count * sizeof(php_user_cache_shared_graph_property)),
+ &properties_offset
+ )
+ ) {
+ if (props != NULL) {
+ zend_release_properties(props);
+ }
+
+ return false;
+ }
+
+ graph_obj = (php_user_cache_shared_graph_object *) (ctx->buffer + obj_offset);
+ graph_obj->class_name_offset = class_name_offset;
+ graph_obj->property_count = prop_count;
+ graph_obj->properties_offset = properties_offset;
+ graph_obj->reserved = 0;
+
+ if (prop_count == 0) {
+ if (props != NULL) {
+ zend_release_properties(props);
+ }
+
+ dst->type = PHP_USER_CACHE_SHARED_GRAPH_VALUE_OBJECT;
+ dst->payload.offset = obj_offset;
+
+ return true;
+ }
+
+ graph_properties = (php_user_cache_shared_graph_property *) (ctx->buffer + properties_offset);
+ prop_idx = 0;
+
+ ZEND_HASH_FOREACH_STR_KEY_VAL(props, prop_name, prop_val) {
+ if (prop_name == NULL) {
+ zend_release_properties(props);
+
+ return false;
+ }
+
+ graph_properties[prop_idx].name_offset = 0;
+ graph_properties[prop_idx].reserved = 0;
+ graph_properties[prop_idx].value.type = PHP_USER_CACHE_SHARED_GRAPH_VALUE_UNDEF;
+
+ src_val = Z_TYPE_P(prop_val) == IS_INDIRECT
+ ? Z_INDIRECT_P(prop_val)
+ : prop_val
+ ;
+
+ if (!user_cache_shared_graph_copy_string(ctx, prop_name, &graph_properties[prop_idx].name_offset)) {
+ zend_release_properties(props);
+
+ return false;
+ }
+
+ if (!user_cache_shared_graph_copy_value(
+ ctx,
+ src_val,
+ &graph_properties[prop_idx].value
+ )
+ ) {
+ zend_release_properties(props);
+
+ return false;
+ }
+
+ ++prop_idx;
+ } ZEND_HASH_FOREACH_END();
+
+ zend_release_properties(props);
+
+ dst->type = PHP_USER_CACHE_SHARED_GRAPH_VALUE_OBJECT;
+ dst->payload.offset = obj_offset;
+
+ return true;
+}
+
+static bool user_cache_shared_graph_copy_enum(
+ php_user_cache_shared_graph_copy_context *ctx,
+ const zval *src,
+ php_user_cache_shared_graph_value *dst
+)
+{
+ php_user_cache_shared_graph_enum *graph_enum;
+ zend_class_entry *ce;
+ zend_string *case_name;
+ uint32_t enum_offset, enum_class_offset, enum_case_offset;
+
+ ce = Z_OBJCE_P(src);
+ case_name = Z_STR_P(zend_enum_fetch_case_name(Z_OBJ_P(src)));
+
+ if (!user_cache_shared_graph_copy_alloc(ctx,
+ sizeof(php_user_cache_shared_graph_enum), &enum_offset) ||
+ !user_cache_shared_graph_copy_string(ctx, ce->name, &enum_class_offset) ||
+ !user_cache_shared_graph_copy_string(ctx, case_name, &enum_case_offset)
+ ) {
+ return false;
+ }
+
+ graph_enum = (php_user_cache_shared_graph_enum *) (ctx->buffer + enum_offset);
+ graph_enum->class_name_offset = enum_class_offset;
+ graph_enum->case_name_offset = enum_case_offset;
+
+ dst->type = PHP_USER_CACHE_SHARED_GRAPH_VALUE_ENUM;
+ dst->payload.offset = enum_offset;
+
+ return true;
+}
+
+static bool user_cache_shared_graph_copy_reference(
+ php_user_cache_shared_graph_copy_context *ctx,
+ const zval *src,
+ php_user_cache_shared_graph_value *dst
+)
+{
+ php_user_cache_shared_graph_reference *graph_reference;
+ zend_reference *ref;
+ uint32_t shared_offset, reference_offset;
+ void *seen_offset;
+
+ ref = Z_REF_P(src);
+ seen_offset = zend_hash_index_find_ptr(&ctx->seen_references, (zend_ulong) (uintptr_t) ref);
+
+ if (seen_offset != NULL) {
+ shared_offset = (uint32_t) (uintptr_t) seen_offset;
+ ((php_user_cache_shared_graph_reference *) (ctx->buffer + shared_offset))->flags |=
+ PHP_USER_CACHE_SHARED_GRAPH_OBJECT_FLAG_SHARED
+ ;
+
+ dst->type = PHP_USER_CACHE_SHARED_GRAPH_VALUE_REFERENCE_REF;
+ dst->payload.offset = shared_offset;
+ ctx->has_shared_identity = true;
+
+ return true;
+ }
+
+ if (!user_cache_shared_graph_copy_alloc(
+ ctx,
+ sizeof(php_user_cache_shared_graph_reference),
+ &reference_offset
+ ) ||
+ zend_hash_index_add_ptr(
+ &ctx->seen_references,
+ (zend_ulong) (uintptr_t) ref,
+ (void *) (uintptr_t) reference_offset
+ ) == NULL
+ ) {
+ return false;
+ }
+
+ graph_reference = (php_user_cache_shared_graph_reference *) (ctx->buffer + reference_offset);
+ graph_reference->flags = 0;
+ graph_reference->reserved = 0;
+
+ if (!user_cache_shared_graph_copy_value(
+ ctx,
+ &ref->val,
+ &graph_reference->inner
+ )
+ ) {
+ return false;
+ }
+
+ dst->type = PHP_USER_CACHE_SHARED_GRAPH_VALUE_REFERENCE;
+ dst->payload.offset = reference_offset;
+ ctx->has_shared_identity = true;
+
+ return true;
+}
+
+static bool user_cache_shared_graph_copy_array(
+ php_user_cache_shared_graph_copy_context *ctx,
+ const zval *src,
+ php_user_cache_shared_graph_value *dst
+)
+{
+ php_user_cache_shared_graph_array *graph_array;
+ php_user_cache_shared_graph_array_element *graph_elems, *graph_elem;
+ php_user_cache_shared_graph_shaped_array *graph_shaped_array;
+ php_user_cache_shared_graph_value *graph_vals;
+ zend_ulong arr_key, h;
+ zend_string *key;
+ zval *elem, *verdict, array_value;
+ uint32_t elem_idx,
+ array_offset, elems_offset, key_offset,
+ shape_offset, values_offset,
+ shared_offset
+ ;
+ bool result, verbatim;
+ void *seen_offset;
+
+ result = true;
+
+ if (Z_ARRVAL_P(src)->nNumOfElements == 0) {
+ if (Z_ARRVAL_P(src)->nNextFreeElement == 0) {
+ dst->type = PHP_USER_CACHE_SHARED_GRAPH_VALUE_ARRAY;
+ dst->payload.offset = 0;
+
+ return true;
+ }
+
+ if (!user_cache_shared_graph_copy_alloc(ctx, sizeof(*graph_array), &array_offset)) {
+ return false;
+ }
+
+ graph_array = (php_user_cache_shared_graph_array *) (ctx->buffer + array_offset);
+ graph_array->count = 0;
+ graph_array->next_free = (uint32_t) Z_ARRVAL_P(src)->nNextFreeElement;
+ graph_array->elements_offset = 0;
+ graph_array->reserved = 0;
+
+ dst->type = PHP_USER_CACHE_SHARED_GRAPH_VALUE_DYNAMIC_ARRAY;
+ dst->payload.offset = array_offset;
+
+ return true;
+ }
+
+ if (ctx->verbatim_arrays_allowed) {
+ if (GC_FLAGS(Z_ARRVAL_P(src)) & IS_ARRAY_IMMUTABLE) {
+ verbatim = true;
+ } else {
+ /* No state hook ran, so address-keyed verdicts are still valid. */
+ verdict = ctx->shared_verdicts != NULL
+ ? zend_hash_index_find(ctx->shared_verdicts, (zend_ulong) (uintptr_t) Z_ARRVAL_P(src))
+ : NULL;
+ if (verdict != NULL) {
+ verbatim = Z_TYPE_P(verdict) == IS_TRUE;
+ } else {
+ verbatim = user_cache_shared_graph_can_copy_verbatim_value(
+ &ctx->seen_arrays,
+ &ctx->direct_verdicts,
+ src
+ );
+ }
+ }
+
+ if (verbatim) {
+ if (!user_cache_shared_graph_copy_verbatim_value(ctx, src, &array_value)) {
+ return false;
+ }
+
+ dst->type = PHP_USER_CACHE_SHARED_GRAPH_VALUE_ARRAY;
+ dst->payload.offset = (uint32_t) ((uint8_t *) Z_ARRVAL(array_value) - ctx->buffer);
+
+ /* Verbatim arrays require relocation fixups. */
+ ctx->has_verbatim_array = true;
+
+ return true;
+ }
+ }
+
+ arr_key = (zend_ulong) (uintptr_t) Z_ARRVAL_P(src);
+
+ seen_offset = zend_hash_index_find_ptr(&ctx->seen_arrays, arr_key);
+ if (seen_offset != NULL) {
+ shared_offset = (uint32_t) (uintptr_t) seen_offset;
+ ((php_user_cache_shared_graph_array *) (ctx->buffer + shared_offset))->reserved |=
+ PHP_USER_CACHE_SHARED_GRAPH_OBJECT_FLAG_SHARED
+ ;
+
+ dst->type = PHP_USER_CACHE_SHARED_GRAPH_VALUE_ARRAY_REF;
+ dst->payload.offset = shared_offset;
+ ctx->has_shared_identity = true;
+
+ return true;
+ }
+
+ if (user_cache_shared_graph_array_has_shape(Z_ARRVAL_P(src))) {
+ if (!user_cache_shared_graph_copy_alloc(ctx, sizeof(*graph_shaped_array), &array_offset) ||
+ !user_cache_shared_graph_copy_alloc(
+ ctx,
+ (size_t) Z_ARRVAL_P(src)->nNumOfElements * sizeof(*graph_vals),
+ &values_offset
+ ) ||
+ !user_cache_shared_graph_copy_array_shape(ctx, Z_ARRVAL_P(src), &shape_offset, NULL)
+ ) {
+ result = false;
+
+ goto done;
+ }
+
+ graph_shaped_array = (php_user_cache_shared_graph_shaped_array *) (ctx->buffer + array_offset);
+ graph_shaped_array->count = Z_ARRVAL_P(src)->nNumOfElements;
+ graph_shaped_array->next_free = (uint32_t) Z_ARRVAL_P(src)->nNextFreeElement;
+ graph_shaped_array->shape_offset = shape_offset;
+ graph_shaped_array->reserved = 0;
+ graph_shaped_array->values_offset = values_offset;
+ graph_shaped_array->reserved2 = 0;
+
+ if (zend_hash_index_add_ptr(&ctx->seen_arrays, arr_key, (void *) (uintptr_t) array_offset) == NULL) {
+ result = false;
+
+ goto done;
+ }
+
+ graph_vals = (php_user_cache_shared_graph_value *) (ctx->buffer + values_offset);
+ elem_idx = 0;
+
+ ZEND_HASH_FOREACH_VAL(Z_ARRVAL_P(src), elem) {
+ if (!user_cache_shared_graph_copy_value(ctx, elem, &graph_vals[elem_idx])) {
+ result = false;
+
+ break;
+ }
+
+ ++elem_idx;
+ } ZEND_HASH_FOREACH_END();
+
+ if (result) {
+ dst->type = PHP_USER_CACHE_SHARED_GRAPH_VALUE_SHAPED_ARRAY;
+ dst->payload.offset = array_offset;
+ }
+
+ goto done;
+ }
+
+ if (!user_cache_shared_graph_copy_alloc(ctx, sizeof(*graph_array), &array_offset) ||
+ !user_cache_shared_graph_copy_alloc(
+ ctx,
+ (size_t) Z_ARRVAL_P(src)->nNumOfElements * sizeof(*graph_elems),
+ &elems_offset
+ )
+ ) {
+ result = false;
+
+ goto done;
+ }
+
+ graph_array = (php_user_cache_shared_graph_array *) (ctx->buffer + array_offset);
+ graph_array->count = Z_ARRVAL_P(src)->nNumOfElements;
+ graph_array->next_free = (uint32_t) Z_ARRVAL_P(src)->nNextFreeElement;
+ graph_array->elements_offset = elems_offset;
+ graph_array->reserved =
+ (HT_IS_PACKED(Z_ARRVAL_P(src)) && HT_IS_WITHOUT_HOLES(Z_ARRVAL_P(src))) ?
+ PHP_USER_CACHE_SHARED_GRAPH_ARRAY_FLAG_PACKED :
+ 0
+ ;
+
+ if (zend_hash_index_add_ptr(&ctx->seen_arrays, arr_key, (void *) (uintptr_t) array_offset) == NULL) {
+ result = false;
+
+ goto done;
+ }
+
+ graph_elems = (php_user_cache_shared_graph_array_element *) (ctx->buffer + elems_offset);
+ graph_elem = graph_elems;
+
+ ZEND_HASH_FOREACH_KEY_VAL(Z_ARRVAL_P(src), h, key, elem) {
+ memset(graph_elem, 0, sizeof(*graph_elem));
+ graph_elem->h = h;
+
+ if (key != NULL) {
+ if (!user_cache_shared_graph_copy_string(ctx, key, &key_offset)) {
+ result = false;
+
+ break;
+ }
+
+ graph_elem->key_offset = key_offset;
+ }
+
+ if (!user_cache_shared_graph_copy_value(ctx, elem, &graph_elem->value)) {
+ result = false;
+
+ break;
+ }
+
+ ++graph_elem;
+ } ZEND_HASH_FOREACH_END();
+
+ if (result) {
+ dst->type = PHP_USER_CACHE_SHARED_GRAPH_VALUE_DYNAMIC_ARRAY;
+ dst->payload.offset = array_offset;
+ }
+
+done:
+ zend_hash_index_del(&ctx->seen_arrays, arr_key);
+
+ return result;
+}
+
+static bool user_cache_shared_graph_copy_object(
+ php_user_cache_shared_graph_copy_context *ctx,
+ const zval *src,
+ php_user_cache_shared_graph_value *dst
+)
+{
+ zend_class_entry *ce;
+ zend_object *obj;
+
+ ctx->has_object = true;
+ ce = Z_OBJCE_P(src);
+
+ if (ce->ce_flags & ZEND_ACC_ENUM) {
+ return user_cache_shared_graph_copy_enum(ctx, src, dst);
+ }
+
+ obj = Z_OBJ_P(src);
+
+ switch (user_cache_shared_graph_classify_object_route(obj->ce)) {
+ case PHP_USER_CACHE_OBJECT_ROUTE_SAFE_DIRECT:
+ return user_cache_shared_graph_copy_safe_direct_object(ctx, src, obj, dst);
+ case PHP_USER_CACHE_OBJECT_ROUTE_MAGIC_SERIALIZE:
+ return user_cache_shared_graph_copy_magic_state_object(
+ ctx, src, obj, PHP_USER_CACHE_OBJECT_ROUTE_MAGIC_SERIALIZE, dst
+ );
+ case PHP_USER_CACHE_OBJECT_ROUTE_SERIALIZE_PROPS:
+ return user_cache_shared_graph_copy_sleep_state_object(
+ ctx, src, obj, PHP_USER_CACHE_OBJECT_ROUTE_SERIALIZE_PROPS, dst
+ );
+ case PHP_USER_CACHE_OBJECT_ROUTE_MAGIC_UNSERIALIZE:
+ return user_cache_shared_graph_copy_magic_state_object(
+ ctx, src, obj, PHP_USER_CACHE_OBJECT_ROUTE_MAGIC_UNSERIALIZE, dst
+ );
+ case PHP_USER_CACHE_OBJECT_ROUTE_SLEEP:
+ return user_cache_shared_graph_copy_sleep_state_object(
+ ctx, src, obj, PHP_USER_CACHE_OBJECT_ROUTE_SLEEP, dst
+ );
+ case PHP_USER_CACHE_OBJECT_ROUTE_WAKEUP:
+ return user_cache_shared_graph_copy_sleep_state_object(
+ ctx, src, obj, PHP_USER_CACHE_OBJECT_ROUTE_WAKEUP, dst
+ );
+ case PHP_USER_CACHE_OBJECT_ROUTE_SERDES:
+ return user_cache_shared_graph_copy_serdes_object(ctx, src, obj, dst);
+ case PHP_USER_CACHE_OBJECT_ROUTE_PLAIN:
+ return user_cache_shared_graph_copy_plain_object(ctx, src, obj, dst);
+ case PHP_USER_CACHE_OBJECT_ROUTE_UNSTORABLE:
+ return false;
+ }
+
+ return false;
+}
+
+static bool user_cache_shared_graph_copy_value(
+ php_user_cache_shared_graph_copy_context *ctx,
+ const zval *src,
+ php_user_cache_shared_graph_value *dst
+)
+{
+ uint32_t string_offset;
+
+ if (php_user_cache_stack_overflowed()) {
+ return false;
+ }
+
+ memset(dst, 0, sizeof(*dst));
+
+ switch (Z_TYPE_P(src)) {
+ case IS_UNDEF:
+ dst->type = PHP_USER_CACHE_SHARED_GRAPH_VALUE_UNDEF;
+
+ return true;
+ case IS_NULL:
+ dst->type = PHP_USER_CACHE_SHARED_GRAPH_VALUE_NULL;
+
+ return true;
+ case IS_TRUE:
+ dst->type = PHP_USER_CACHE_SHARED_GRAPH_VALUE_TRUE;
+
+ return true;
+ case IS_FALSE:
+ dst->type = PHP_USER_CACHE_SHARED_GRAPH_VALUE_FALSE;
+
+ return true;
+ case IS_LONG:
+ dst->type = PHP_USER_CACHE_SHARED_GRAPH_VALUE_LONG;
+ dst->payload.long_value = Z_LVAL_P(src);
+
+ return true;
+ case IS_DOUBLE:
+ dst->type = PHP_USER_CACHE_SHARED_GRAPH_VALUE_DOUBLE;
+ dst->payload.double_value = Z_DVAL_P(src);
+
+ return true;
+ case IS_STRING:
+ if (!user_cache_shared_graph_copy_string(ctx, Z_STR_P(src), &string_offset)) {
+ return false;
+ }
+
+ dst->type = PHP_USER_CACHE_SHARED_GRAPH_VALUE_STRING;
+ dst->payload.offset = string_offset;
+
+ return true;
+ case IS_ARRAY:
+ return user_cache_shared_graph_copy_array(ctx, src, dst);
+ case IS_OBJECT:
+ return user_cache_shared_graph_copy_object(ctx, src, dst);
+ case IS_REFERENCE:
+ return user_cache_shared_graph_copy_reference(ctx, src, dst);
+ default:
+ return false;
+ }
+}
+
+static PHP_USER_CACHE_DECODE_HOT bool user_cache_shared_graph_try_update_declared_property(
+ zend_object *obj,
+ zend_string *prop_name,
+ zend_property_info *prop_info,
+ zval *prop_val,
+ bool *failed
+)
+{
+ zval *slot, tmp, indirect;
+
+ *failed = false;
+
+ if (prop_info == NULL ||
+ prop_info == ZEND_WRONG_PROPERTY_INFO ||
+ !zend_string_equals(prop_info->name, prop_name) ||
+ (prop_info->flags & (ZEND_ACC_STATIC|ZEND_ACC_VIRTUAL)) != 0 ||
+ prop_info->offset == ZEND_VIRTUAL_PROPERTY_OFFSET
+ ) {
+ return false;
+ }
+
+ slot = OBJ_PROP(obj, prop_info->offset);
+
+ if (Z_ISREF_P(prop_val)) {
+ if (ZEND_TYPE_IS_SET(prop_info->type)) {
+ if (!zend_verify_prop_assignable_by_ref(prop_info, prop_val, true)) {
+ *failed = true;
+
+ return false;
+ }
+
+ ZEND_REF_ADD_TYPE_SOURCE(Z_REF_P(prop_val), prop_info);
+ }
+
+ zval_ptr_dtor(slot);
+ ZVAL_COPY(slot, prop_val);
+
+ if (obj->properties != NULL) {
+ ZVAL_INDIRECT(&indirect, slot);
+ zend_hash_update(obj->properties, prop_name, &indirect);
+ }
+
+ return true;
+ }
+
+ if ((prop_info->flags & (ZEND_ACC_READONLY|ZEND_ACC_PPP_SET_MASK)) != 0 ||
+ (prop_info->flags & ZEND_ACC_PPP_MASK) != ZEND_ACC_PUBLIC
+ ) {
+ return false;
+ }
+
+ ZVAL_COPY_DEREF(&tmp, prop_val);
+
+ if (ZEND_TYPE_IS_SET(prop_info->type) &&
+ !zend_verify_property_type(prop_info, &tmp, true)
+ ) {
+ zval_ptr_dtor(&tmp);
+
+ *failed = true;
+
+ return false;
+ }
+
+ zval_ptr_dtor(slot);
+ ZVAL_COPY_VALUE(slot, &tmp);
+
+ if (obj->properties != NULL) {
+ ZVAL_INDIRECT(&indirect, slot);
+ zend_hash_update(obj->properties, prop_name, &indirect);
+ }
+
+ return true;
+}
+
+static PHP_USER_CACHE_DECODE_HOT zend_class_entry *user_cache_decode_lookup_class(zend_string *class_name)
+{
+ zend_class_entry *ce;
+
+ ce = user_cache_decode_resolve_cache_find(class_name);
+ if (ce != NULL) {
+ return ce;
+ }
+
+ ce = zend_lookup_class(class_name);
+ if (ce != NULL) {
+ user_cache_decode_resolve_cache_store(class_name, ce);
+ }
+
+ return ce;
+}
+
+static PHP_USER_CACHE_DECODE_HOT zend_class_entry *user_cache_decode_lookup_state_schema_class(
+ const uint8_t *buf,
+ size_t buf_len,
+ const php_user_cache_shared_graph_state_schema *state_schema
+)
+{
+ zend_string *class_name;
+ zend_class_entry *ce;
+
+ ce = user_cache_decode_resolve_cache_find(state_schema);
+ if (ce != NULL) {
+ return ce;
+ }
+
+ class_name = user_cache_decode_string_at(buf, buf_len, state_schema->class_name_offset);
+ if (class_name == NULL) {
+ return NULL;
+ }
+
+ ce = user_cache_decode_lookup_class(class_name);
+ if (ce != NULL) {
+ user_cache_decode_resolve_cache_store(state_schema, ce);
+ }
+
+ return ce;
+}
+
+static void user_cache_decode_array_dtor(zval *zv)
+{
+ zval tmp;
+
+ ZVAL_ARR(&tmp, (zend_array *) Z_PTR_P(zv));
+
+ zval_ptr_dtor(&tmp);
+}
+
+static void user_cache_decode_shape_prototype_dtor(zval *zv)
+{
+ zend_array_destroy((zend_array *) Z_PTR_P(zv));
+}
+
+static zend_array *user_cache_decode_shape_prototype_create(
+ const uint8_t *buf,
+ size_t buf_len,
+ const php_user_cache_shared_graph_array_shape *graph_shape
+)
+{
+ const php_user_cache_shared_graph_array_shape_element *shape_elems, *shape_elem;
+ zend_string *prop_name;
+ zend_array *proto;
+ zval empty;
+ uint32_t i;
+
+ if (graph_shape->count == 0 ||
+ graph_shape->count > PHP_USER_CACHE_SHARED_GRAPH_ARRAY_SHAPE_MAX_KEYS ||
+ !user_cache_decode_array_range_ok(
+ buf_len,
+ graph_shape->elements_offset,
+ graph_shape->count,
+ sizeof(*shape_elems)
+ )
+ ) {
+ return NULL;
+ }
+
+ proto = zend_new_array(graph_shape->count);
+ if (HT_FLAGS(proto) & HASH_FLAG_UNINITIALIZED) {
+ zend_hash_real_init_mixed(proto);
+ }
+
+ shape_elems =
+ (const php_user_cache_shared_graph_array_shape_element *) (buf + graph_shape->elements_offset)
+ ;
+
+ ZVAL_LONG(&empty, 0);
+
+ for (i = 0; i < graph_shape->count; i++) {
+ shape_elem = &shape_elems[i];
+ prop_name = user_cache_decode_string_at(buf, buf_len, shape_elem->key_offset);
+ if (shape_elem->key_offset == 0 || prop_name == NULL) {
+ zend_array_destroy(proto);
+
+ return NULL;
+ }
+
+ if (_zend_hash_append_ex(proto, prop_name, &empty, true) == NULL) {
+ zend_array_destroy(proto);
+
+ return NULL;
+ }
+ }
+
+ proto->nNextFreeElement = 0;
+
+ return proto;
+}
+
+static zend_always_inline void user_cache_decode_shape_prototype_direct_cache_store(
+ const php_user_cache_shared_graph_array_shape *graph_shape,
+ zend_array *proto
+)
+{
+ uint32_t slot;
+
+ slot = UC_G(decode_shape_prototype_direct_next)++ % PHP_USER_CACHE_DECODE_DIRECT_CACHE_SLOTS;
+
+ UC_G(decode_shape_prototype_direct_keys)[slot] = graph_shape;
+ UC_G(decode_shape_prototype_direct_values)[slot] = proto;
+}
+
+static zend_array *user_cache_decode_shape_prototype_get(
+ const uint8_t *buf,
+ size_t buf_len,
+ const php_user_cache_shared_graph_array_shape *graph_shape
+)
+{
+ zend_ulong key;
+ zend_array *proto;
+ HashTable *cache;
+ uint32_t i;
+
+ for (i = 0; i < PHP_USER_CACHE_DECODE_DIRECT_CACHE_SLOTS; i++) {
+ if (UC_G(decode_shape_prototype_direct_keys)[i] == graph_shape) {
+ return UC_G(decode_shape_prototype_direct_values)[i];
+ }
+ }
+
+ key = (zend_ulong) (uintptr_t) graph_shape;
+ if (UC_G(decode_shape_prototype_cache) != NULL) {
+ proto = zend_hash_index_find_ptr(UC_G(decode_shape_prototype_cache), key);
+ if (proto != NULL) {
+ user_cache_decode_shape_prototype_direct_cache_store(graph_shape, proto);
+
+ return proto;
+ }
+ }
+
+ proto = user_cache_decode_shape_prototype_create(buf, buf_len, graph_shape);
+ if (proto == NULL) {
+ return NULL;
+ }
+
+ cache = user_cache_decode_shape_prototype_cache();
+ if (zend_hash_index_add_ptr(cache, key, proto) == NULL) {
+ zend_array_destroy(proto);
+
+ return NULL;
+ }
+
+ user_cache_decode_shape_prototype_direct_cache_store(graph_shape, proto);
+
+ return proto;
+}
+
+static PHP_USER_CACHE_DECODE_HOT bool user_cache_decode_shape_prototype_clone(
+ const uint8_t *buf,
+ size_t buf_len,
+ const php_user_cache_shared_graph_array_shape *graph_shape,
+ uint32_t count,
+ uint32_t next_free,
+ zval *dst
+)
+{
+ zend_array *proto, *arr;
+
+ if (graph_shape->count != count) {
+ return false;
+ }
+
+ proto = user_cache_decode_shape_prototype_get(buf, buf_len, graph_shape);
+ if (proto == NULL || proto->nNumUsed != count || proto->nNumOfElements != count) {
+ return false;
+ }
+
+ ALLOC_HASHTABLE(arr);
+
+ GC_SET_REFCOUNT(arr, 1);
+ GC_TYPE_INFO(arr) = GC_ARRAY;
+
+ HT_FLAGS(arr) = HT_FLAGS(proto) & HASH_FLAG_MASK;
+
+ arr->nTableMask = proto->nTableMask;
+ arr->nNumUsed = proto->nNumUsed;
+ arr->nNumOfElements = proto->nNumOfElements;
+ arr->nTableSize = proto->nTableSize;
+ arr->nInternalPointer = 0;
+ arr->nNextFreeElement = (zend_long) next_free;
+ arr->pDestructor = ZVAL_PTR_DTOR;
+
+ HT_SET_DATA_ADDR(arr, emalloc(HT_SIZE(arr)));
+
+ memcpy(HT_GET_DATA_ADDR(arr), HT_GET_DATA_ADDR(proto), HT_USED_SIZE(proto));
+
+ ZVAL_ARR(dst, arr);
+
+ return true;
+}
+
+static zend_always_inline bool user_cache_shared_graph_decode_simple_value(
+ const uint8_t *buf,
+ size_t buf_len,
+ const php_user_cache_shared_graph_value *value,
+ zval *dst
+)
+{
+ zend_string *str;
+
+ switch (value->type) {
+ case PHP_USER_CACHE_SHARED_GRAPH_VALUE_UNDEF:
+ ZVAL_UNDEF(dst);
+ return true;
+ case PHP_USER_CACHE_SHARED_GRAPH_VALUE_NULL:
+ ZVAL_NULL(dst);
+ return true;
+ case PHP_USER_CACHE_SHARED_GRAPH_VALUE_TRUE:
+ ZVAL_TRUE(dst);
+ return true;
+ case PHP_USER_CACHE_SHARED_GRAPH_VALUE_FALSE:
+ ZVAL_FALSE(dst);
+ return true;
+ case PHP_USER_CACHE_SHARED_GRAPH_VALUE_LONG:
+ ZVAL_LONG(dst, value->payload.long_value);
+ return true;
+ case PHP_USER_CACHE_SHARED_GRAPH_VALUE_DOUBLE:
+ ZVAL_DOUBLE(dst, value->payload.double_value);
+ return true;
+ case PHP_USER_CACHE_SHARED_GRAPH_VALUE_STRING:
+ str = user_cache_decode_string_at(buf, buf_len, (uint32_t) value->payload.offset);
+ if (str == NULL) {
+ return false;
+ }
+
+ ZVAL_INTERNED_STR(dst, str);
+
+ return true;
+ default:
+ return false;
+ }
+}
+
+static PHP_USER_CACHE_DECODE_HOT bool user_cache_shared_graph_decode_shaped_array(
+ const uint8_t *buf,
+ size_t buf_len,
+ uint32_t shape_offset,
+ uint32_t values_offset,
+ uint32_t count,
+ uint32_t next_free,
+ uint32_t node_offset,
+ bool shared,
+ zval *dst
+)
+{
+ const php_user_cache_shared_graph_array_shape *graph_shape;
+ const php_user_cache_shared_graph_value *graph_vals;
+ zval val;
+ Bucket *bucket;
+ uint32_t i;
+
+ if (!user_cache_decode_range_ok(buf_len, shape_offset, sizeof(*graph_shape)) ||
+ !user_cache_decode_array_range_ok(buf_len, values_offset, count, sizeof(*graph_vals))
+ ) {
+ return false;
+ }
+
+ graph_shape = (const php_user_cache_shared_graph_array_shape *) (buf + shape_offset);
+ if (!user_cache_decode_shape_prototype_clone(
+ buf,
+ buf_len,
+ graph_shape,
+ count,
+ next_free,
+ dst
+ )
+ ) {
+ return false;
+ }
+
+ if (shared) {
+ if (!user_cache_decode_array_map_insert(node_offset, Z_ARRVAL_P(dst))) {
+ return user_cache_decode_fail_zval(dst);
+ }
+ }
+
+ HT_ALLOW_COW_VIOLATION(Z_ARRVAL_P(dst));
+
+ graph_vals = (const php_user_cache_shared_graph_value *) (buf + values_offset);
+ bucket = Z_ARRVAL_P(dst)->arData;
+ for (i = 0; i < count; i++) {
+ if (user_cache_shared_graph_decode_simple_value(buf, buf_len, &graph_vals[i], &bucket[i].val)) {
+ continue;
+ }
+
+ ZVAL_UNDEF(&val);
+ if (!user_cache_shared_graph_decode_value(buf, buf_len, &graph_vals[i], &val)) {
+ return user_cache_decode_fail_zval(dst);
+ }
+
+ ZVAL_COPY_VALUE(&bucket[i].val, &val);
+ }
+
+ PHP_USER_CACHE_HT_DISALLOW_COW_VIOLATION(Z_ARRVAL_P(dst));
+
+ return true;
+}
+
+static PHP_USER_CACHE_DECODE_HOT bool user_cache_shared_graph_decode_dynamic_array(
+ const uint8_t *buf,
+ size_t buf_len,
+ const php_user_cache_shared_graph_value *value,
+ zval *dst
+)
+{
+ const php_user_cache_shared_graph_array *graph_array;
+ const php_user_cache_shared_graph_array_element *graph_elems, *graph_elem;
+ zend_string *key;
+ zval val;
+ uint32_t i;
+
+ if (!user_cache_decode_range_ok(buf_len, (uint32_t) value->payload.offset, sizeof(*graph_array))) {
+ return false;
+ }
+
+ graph_array = (const php_user_cache_shared_graph_array *) (buf + (uint32_t) value->payload.offset);
+ if (!user_cache_decode_array_range_ok(buf_len, graph_array->elements_offset, graph_array->count, sizeof(*graph_elems))) {
+ return false;
+ }
+
+ array_init_size(dst, graph_array->count);
+
+ /* Initialize before identity registration raises the array refcount. */
+ if (graph_array->count > 0) {
+ if (graph_array->reserved & PHP_USER_CACHE_SHARED_GRAPH_ARRAY_FLAG_PACKED) {
+ zend_hash_real_init_packed(Z_ARRVAL_P(dst));
+ } else {
+ zend_hash_real_init_mixed(Z_ARRVAL_P(dst));
+ }
+
+ HT_ALLOW_COW_VIOLATION(Z_ARRVAL_P(dst));
+ }
+
+ if (graph_array->reserved & PHP_USER_CACHE_SHARED_GRAPH_OBJECT_FLAG_SHARED) {
+ if (!user_cache_decode_array_map_insert((uint32_t) value->payload.offset, Z_ARRVAL_P(dst))) {
+ return user_cache_decode_fail_zval(dst);
+ }
+ }
+
+ graph_elems = (const php_user_cache_shared_graph_array_element *) (buf + graph_array->elements_offset);
+
+ if (graph_array->reserved & PHP_USER_CACHE_SHARED_GRAPH_ARRAY_FLAG_PACKED) {
+ for (i = 0; i < graph_array->count; i++) {
+ graph_elem = &graph_elems[i];
+
+ if (graph_elem->key_offset != 0 || graph_elem->h != i) {
+ return user_cache_decode_fail_zval(dst);
+ }
+
+ if (!user_cache_shared_graph_decode_simple_value(buf, buf_len, &graph_elem->value, &val)) {
+ ZVAL_UNDEF(&val);
+
+ if (!user_cache_shared_graph_decode_value(buf, buf_len, &graph_elem->value, &val)) {
+ return user_cache_decode_fail_zval(dst);
+ }
+ }
+
+ if (zend_hash_next_index_insert_new(Z_ARRVAL_P(dst), &val) == NULL) {
+ zval_ptr_dtor(&val);
+ return user_cache_decode_fail_zval(dst);
+ }
+ }
+
+ Z_ARRVAL_P(dst)->nNextFreeElement = graph_array->next_free;
+ PHP_USER_CACHE_HT_DISALLOW_COW_VIOLATION(Z_ARRVAL_P(dst));
+ return true;
+ }
+
+ for (i = 0; i < graph_array->count; i++) {
+ graph_elem = &graph_elems[i];
+
+ if (!user_cache_shared_graph_decode_simple_value(buf, buf_len, &graph_elem->value, &val)) {
+ ZVAL_UNDEF(&val);
+
+ if (!user_cache_shared_graph_decode_value(buf, buf_len, &graph_elem->value, &val)) {
+ return user_cache_decode_fail_zval(dst);
+ }
+ }
+
+ if (graph_elem->key_offset != 0) {
+ key = user_cache_decode_string_at(buf, buf_len, graph_elem->key_offset);
+ if (key == NULL) {
+ zval_ptr_dtor(&val);
+ return user_cache_decode_fail_zval(dst);
+ }
+
+ if (zend_hash_add_new(Z_ARRVAL_P(dst), key, &val) == NULL) {
+ zval_ptr_dtor(&val);
+ return user_cache_decode_fail_zval(dst);
+ }
+ } else {
+ if (zend_hash_index_add_new(Z_ARRVAL_P(dst), graph_elem->h, &val) == NULL) {
+ zval_ptr_dtor(&val);
+ return user_cache_decode_fail_zval(dst);
+ }
+ }
+ }
+
+ Z_ARRVAL_P(dst)->nNextFreeElement = graph_array->next_free;
+ PHP_USER_CACHE_HT_DISALLOW_COW_VIOLATION(Z_ARRVAL_P(dst));
+ return true;
+}
+
+static PHP_USER_CACHE_DECODE_HOT bool user_cache_shared_graph_decode_init_object(
+ zend_class_entry *ce,
+ uint32_t node_offset,
+ bool shared,
+ zval *dst
+)
+{
+ if (object_init_ex(dst, ce) != SUCCESS) {
+ return false;
+ }
+
+ if (shared && !user_cache_decode_identity_map_insert(node_offset, Z_OBJ_P(dst))) {
+ return user_cache_decode_fail_zval(dst);
+ }
+
+ return true;
+}
+
+static PHP_USER_CACHE_DECODE_HOT bool user_cache_shared_graph_decode_apply_property(
+ zval *dst,
+ zend_string *prop_name,
+ uint32_t slot_idx_plus_one,
+ zval *prop_val
+)
+{
+ bool result;
+
+ if (slot_idx_plus_one != 0) {
+ result = php_user_cache_shared_graph_update_object_property_at(
+ dst,
+ prop_name,
+ slot_idx_plus_one - 1,
+ prop_val
+ );
+ } else {
+ result = php_user_cache_shared_graph_update_object_property(
+ dst,
+ prop_name,
+ prop_val
+ );
+ }
+
+ if (!result) {
+ zval_ptr_dtor(prop_val);
+ return user_cache_decode_fail_zval(dst);
+ }
+
+ zval_ptr_dtor(prop_val);
+
+ return true;
+}
+
+static PHP_USER_CACHE_DECODE_HOT bool user_cache_shared_graph_decode_object_properties(
+ const uint8_t *buf,
+ size_t buf_len,
+ uint32_t properties_offset,
+ uint32_t property_count,
+ bool use_sleep_slots,
+ zval *dst
+)
+{
+ const php_user_cache_shared_graph_property *props, *prop;
+ zend_string *prop_name;
+ zval prop_val;
+ uint32_t i;
+
+ if (property_count == 0) {
+ return true;
+ }
+
+ if (!user_cache_decode_array_range_ok(buf_len, properties_offset, property_count, sizeof(*props))) {
+ return user_cache_decode_fail_zval(dst);
+ }
+
+ props = (const php_user_cache_shared_graph_property *) (buf + properties_offset);
+ for (i = 0; i < property_count; i++) {
+ prop = &props[i];
+ if (prop->value.type == PHP_USER_CACHE_SHARED_GRAPH_VALUE_UNDEF) {
+ continue;
+ }
+
+ prop_name = user_cache_decode_string_at(buf, buf_len, prop->name_offset);
+ if (prop_name == NULL) {
+ return user_cache_decode_fail_zval(dst);
+ }
+
+ if (!user_cache_shared_graph_decode_simple_value(buf, buf_len, &prop->value, &prop_val)) {
+ ZVAL_UNDEF(&prop_val);
+
+ if (!user_cache_shared_graph_decode_value(buf, buf_len, &prop->value, &prop_val)) {
+ zval_ptr_dtor(&prop_val);
+ return user_cache_decode_fail_zval(dst);
+ }
+ }
+
+ if (!user_cache_shared_graph_decode_apply_property(
+ dst,
+ prop_name,
+ use_sleep_slots ? prop->reserved : i + 1,
+ &prop_val
+ )
+ ) {
+ return false;
+ }
+ }
+
+ return true;
+}
+
+static PHP_USER_CACHE_DECODE_HOT bool user_cache_shared_graph_decode_call_wakeup(zend_class_entry *ce, zval *dst)
+{
+ zval retval, *wakeup_zv;
+
+ wakeup_zv = zend_hash_find_known_hash(&ce->function_table, ZSTR_KNOWN(ZEND_STR_WAKEUP));
+ if (wakeup_zv != NULL) {
+ zend_call_known_instance_method(Z_FUNC_P(wakeup_zv), Z_OBJ_P(dst), &retval, 0, NULL);
+
+ zval_ptr_dtor(&retval);
+
+ if (EG(exception)) {
+ return user_cache_decode_fail_zval(dst);
+ }
+ }
+
+ return true;
+}
+
+static PHP_USER_CACHE_DECODE_HOT bool user_cache_shared_graph_decode_call_unserialize(zend_class_entry *ce, zval *dst, zval *state)
+{
+ zend_call_known_instance_method_with_1_params(
+ ce->__unserialize,
+ Z_OBJ_P(dst),
+ NULL,
+ state
+ );
+
+ zval_ptr_dtor(state);
+
+ if (EG(exception)) {
+ return user_cache_decode_fail_zval(dst);
+ }
+
+ return true;
+}
+
+static PHP_USER_CACHE_DECODE_HOT bool user_cache_shared_graph_decode_object(
+ const uint8_t *buf,
+ size_t buf_len,
+ const php_user_cache_shared_graph_value *value,
+ zval *dst
+)
+{
+ const php_user_cache_shared_graph_object *graph_obj;
+ zend_string *class_name;
+ zend_class_entry *ce;
+
+ graph_obj = (const php_user_cache_shared_graph_object *) (buf + (uint32_t) value->payload.offset);
+ class_name = user_cache_decode_string_at(buf, buf_len, graph_obj->class_name_offset);
+ if (class_name == NULL) {
+ return false;
+ }
+ ce = user_cache_decode_lookup_class(class_name);
+
+ if (ce == NULL) {
+ return false;
+ }
+
+ if (!user_cache_shared_graph_decode_init_object(
+ ce,
+ (uint32_t) value->payload.offset,
+ (graph_obj->reserved & PHP_USER_CACHE_SHARED_GRAPH_OBJECT_FLAG_SHARED) != 0,
+ dst
+ )
+ ) {
+ return false;
+ }
+
+ return user_cache_shared_graph_decode_object_properties(
+ buf,
+ buf_len,
+ graph_obj->properties_offset,
+ graph_obj->property_count,
+ false,
+ dst
+ );
+}
+
+static PHP_USER_CACHE_DECODE_HOT bool user_cache_shared_graph_decode_sleep_object(
+ const uint8_t *buf,
+ size_t buf_len,
+ const php_user_cache_shared_graph_value *value,
+ zval *dst
+)
+{
+ const php_user_cache_shared_graph_object *graph_obj;
+ zend_string *class_name;
+ zend_class_entry *ce;
+
+ graph_obj = (const php_user_cache_shared_graph_object *) (buf + (uint32_t) value->payload.offset);
+ class_name = user_cache_decode_string_at(buf, buf_len, graph_obj->class_name_offset);
+ if (class_name == NULL) {
+ return false;
+ }
+
+ ce = user_cache_decode_lookup_class(class_name);
+ if (ce == NULL) {
+ return false;
+ }
+
+ if (!user_cache_shared_graph_decode_init_object(
+ ce,
+ (uint32_t) value->payload.offset,
+ (graph_obj->reserved & PHP_USER_CACHE_SHARED_GRAPH_OBJECT_FLAG_SHARED) != 0,
+ dst
+ )
+ ) {
+ return false;
+ }
+
+ if (!user_cache_shared_graph_decode_object_properties(
+ buf,
+ buf_len,
+ graph_obj->properties_offset,
+ graph_obj->property_count,
+ true,
+ dst
+ )
+ ) {
+ return false;
+ }
+
+ return user_cache_shared_graph_decode_call_wakeup(ce, dst);
+}
+
+static PHP_USER_CACHE_DECODE_HOT bool user_cache_shared_graph_decode_sleep_shaped_object(
+ const uint8_t *buf,
+ size_t buf_len,
+ const php_user_cache_shared_graph_value *value,
+ zval *dst
+)
+{
+ const php_user_cache_shared_graph_shaped_state_object *graph_shaped_state;
+ const php_user_cache_shared_graph_state_schema *state_schema;
+ const php_user_cache_shared_graph_array_shape *graph_shape;
+ const php_user_cache_shared_graph_array_shape_element *shape_elems;
+ const php_user_cache_shared_graph_value *graph_vals;
+ zend_string *prop_name;
+ zend_class_entry *ce;
+ zval prop_val;
+ uint32_t i, prop_idx_plus_one;
+
+ graph_shaped_state = (const php_user_cache_shared_graph_shaped_state_object *) (buf + (uint32_t) value->payload.offset);
+ if (!user_cache_decode_range_ok(buf_len, graph_shaped_state->state_schema_offset, sizeof(*state_schema))) {
+ return false;
+ }
+
+ state_schema = (const php_user_cache_shared_graph_state_schema *) (buf + graph_shaped_state->state_schema_offset);
+ ce = user_cache_decode_lookup_state_schema_class(buf, buf_len, state_schema);
+
+ if (ce == NULL) {
+ return false;
+ }
+
+ if (!user_cache_shared_graph_decode_init_object(
+ ce,
+ (uint32_t) value->payload.offset,
+ (graph_shaped_state->reserved & PHP_USER_CACHE_SHARED_GRAPH_OBJECT_FLAG_SHARED) != 0,
+ dst
+ )
+ ) {
+ return false;
+ }
+
+ if (!user_cache_decode_range_ok(buf_len, state_schema->shape_offset, sizeof(*graph_shape))) {
+ return user_cache_decode_fail_zval(dst);
+ }
+
+ graph_shape = (const php_user_cache_shared_graph_array_shape *) (buf + state_schema->shape_offset);
+ if (graph_shape->count != state_schema->count) {
+ return user_cache_decode_fail_zval(dst);
+ }
+
+ if (!user_cache_decode_array_range_ok(buf_len, graph_shape->elements_offset, state_schema->count, sizeof(*shape_elems)) ||
+ !user_cache_decode_array_range_ok(buf_len, graph_shaped_state->state_values_offset, state_schema->count, sizeof(*graph_vals))
+ ) {
+ return user_cache_decode_fail_zval(dst);
+ }
+
+ shape_elems = (const php_user_cache_shared_graph_array_shape_element *) (buf + graph_shape->elements_offset);
+ graph_vals = (const php_user_cache_shared_graph_value *) (buf + graph_shaped_state->state_values_offset);
+ for (i = 0; i < state_schema->count; i++) {
+ prop_name = user_cache_decode_string_at(buf, buf_len, shape_elems[i].key_offset);
+ if (prop_name == NULL) {
+ return user_cache_decode_fail_zval(dst);
+ }
+
+ if (!user_cache_shared_graph_decode_simple_value(buf, buf_len, &graph_vals[i], &prop_val)) {
+ ZVAL_UNDEF(&prop_val);
+
+ if (!user_cache_shared_graph_decode_value(buf, buf_len, &graph_vals[i], &prop_val)) {
+ zval_ptr_dtor(&prop_val);
+ return user_cache_decode_fail_zval(dst);
+ }
+ }
+
+ prop_idx_plus_one = php_user_cache_serdes_declared_property_index_plus_one(ce, prop_name);
+ if (!user_cache_shared_graph_decode_apply_property(
+ dst,
+ prop_name,
+ prop_idx_plus_one,
+ &prop_val
+ )
+ ) {
+ return false;
+ }
+ }
+
+ return user_cache_shared_graph_decode_call_wakeup(ce, dst);
+}
+
+static PHP_USER_CACHE_DECODE_HOT bool user_cache_shared_graph_decode_safe_direct_object(
+ const uint8_t *buf,
+ size_t buf_len,
+ const php_user_cache_shared_graph_value *value,
+ zval *dst
+)
+{
+ const php_user_cache_shared_graph_safe_direct_object *graph_safe_direct;
+ zend_string *class_name;
+ zend_class_entry *ce;
+ php_user_cache_safe_direct_state_unserialize_func_t unserialize_func;
+ zval sd_state;
+
+ graph_safe_direct = (const php_user_cache_shared_graph_safe_direct_object *) (buf + (uint32_t) value->payload.offset);
+
+ class_name = user_cache_decode_string_at(buf, buf_len, graph_safe_direct->class_name_offset);
+ if (class_name == NULL) {
+ return false;
+ }
+
+ ce = user_cache_decode_lookup_class(class_name);
+ if (ce == NULL) {
+ return false;
+ }
+
+ unserialize_func = php_user_cache_safe_direct_state_unserialize_func(ce);
+
+ if (unserialize_func == NULL ||
+ !user_cache_shared_graph_decode_init_object(
+ ce,
+ (uint32_t) value->payload.offset,
+ (graph_safe_direct->reserved & PHP_USER_CACHE_SHARED_GRAPH_OBJECT_FLAG_SHARED) != 0,
+ dst
+ )
+ ) {
+ return false;
+ }
+
+ ZVAL_UNDEF(&sd_state);
+ if (!user_cache_shared_graph_decode_value(buf, buf_len, &graph_safe_direct->state, &sd_state) ||
+ Z_TYPE(sd_state) != IS_ARRAY ||
+ !unserialize_func(dst, &sd_state)
+ ) {
+ zval_ptr_dtor(&sd_state);
+ return user_cache_decode_fail_zval(dst);
+ }
+
+ zval_ptr_dtor(&sd_state);
+
+ return user_cache_shared_graph_decode_object_properties(
+ buf,
+ buf_len,
+ graph_safe_direct->properties_offset,
+ graph_safe_direct->property_count,
+ false,
+ dst
+ );
+}
+
+static PHP_USER_CACHE_DECODE_HOT bool user_cache_shared_graph_decode_serialized_object(
+ const uint8_t *buf,
+ size_t buf_len,
+ const php_user_cache_shared_graph_value *value,
+ zval *dst
+)
+{
+ const php_user_cache_shared_graph_safe_direct_object *graph_serialized;
+ zend_string *class_name;
+ zend_class_entry *ce;
+ zval state;
+
+ graph_serialized = (const php_user_cache_shared_graph_safe_direct_object *) (buf + (uint32_t) value->payload.offset);
+
+ class_name = user_cache_decode_string_at(buf, buf_len, graph_serialized->class_name_offset);
+ if (class_name == NULL) {
+ return false;
+ }
+
+ ce = user_cache_decode_lookup_class(class_name);
+ if (ce == NULL || ce->__unserialize == NULL || (ce->ce_flags & ZEND_ACC_NOT_SERIALIZABLE)) {
+ return false;
+ }
+
+ if (!user_cache_shared_graph_decode_init_object(
+ ce,
+ (uint32_t) value->payload.offset,
+ (graph_serialized->reserved & PHP_USER_CACHE_SHARED_GRAPH_OBJECT_FLAG_SHARED) != 0,
+ dst
+ )
+ ) {
+ return false;
+ }
+
+ ZVAL_UNDEF(&state);
+
+ if (!user_cache_shared_graph_decode_value(buf, buf_len, &graph_serialized->state, &state) ||
+ Z_TYPE(state) != IS_ARRAY
+ ) {
+ zval_ptr_dtor(&state);
+ return user_cache_decode_fail_zval(dst);
+ }
+
+ return user_cache_shared_graph_decode_call_unserialize(ce, dst, &state);
+}
+
+static PHP_USER_CACHE_DECODE_HOT bool user_cache_shared_graph_decode_serialized_shaped_object(
+ const uint8_t *buf,
+ size_t buf_len,
+ const php_user_cache_shared_graph_value *value,
+ zval *dst
+)
+{
+ const php_user_cache_shared_graph_shaped_state_object *graph_shaped_state;
+ const php_user_cache_shared_graph_state_schema *state_schema;
+ zend_class_entry *ce;
+ zval state;
+
+ graph_shaped_state = (const php_user_cache_shared_graph_shaped_state_object *) (buf + (uint32_t) value->payload.offset);
+ if (!user_cache_decode_range_ok(buf_len, graph_shaped_state->state_schema_offset, sizeof(*state_schema))) {
+ return false;
+ }
+
+ state_schema = (const php_user_cache_shared_graph_state_schema *) (buf + graph_shaped_state->state_schema_offset);
+ ce = user_cache_decode_lookup_state_schema_class(buf, buf_len, state_schema);
+ if (ce == NULL || ce->__unserialize == NULL || (ce->ce_flags & ZEND_ACC_NOT_SERIALIZABLE)) {
+ return false;
+ }
+
+ if (!user_cache_shared_graph_decode_init_object(
+ ce,
+ (uint32_t) value->payload.offset,
+ (graph_shaped_state->reserved & PHP_USER_CACHE_SHARED_GRAPH_OBJECT_FLAG_SHARED) != 0,
+ dst
+ )
+ ) {
+ return false;
+ }
+
+ ZVAL_UNDEF(&state);
+
+ if (!user_cache_shared_graph_decode_shaped_array(
+ buf,
+ buf_len,
+ state_schema->shape_offset,
+ graph_shaped_state->state_values_offset,
+ state_schema->count,
+ graph_shaped_state->state_next_free,
+ 0,
+ false,
+ &state
+ ) ||
+ Z_TYPE(state) != IS_ARRAY
+ ) {
+ zval_ptr_dtor(&state);
+ return user_cache_decode_fail_zval(dst);
+ }
+
+ return user_cache_shared_graph_decode_call_unserialize(ce, dst, &state);
+}
+
+static PHP_USER_CACHE_DECODE_HOT bool user_cache_shared_graph_decode_serdes_object(
+ const uint8_t *buf,
+ size_t buf_len,
+ const php_user_cache_shared_graph_value *value,
+ zval *dst
+)
+{
+ const php_user_cache_shared_graph_serdes_object *graph_serdes;
+
+ graph_serdes =
+ (const php_user_cache_shared_graph_serdes_object *) (buf + (uint32_t) value->payload.offset)
+ ;
+
+ if (!user_cache_decode_range_ok(
+ buf_len,
+ (uint32_t) value->payload.offset + (uint32_t) sizeof(*graph_serdes),
+ graph_serdes->blob_len)
+ ) {
+ return false;
+ }
+
+ if (!php_user_cache_serdes_decode(
+ (const uint8_t *) (graph_serdes + 1),
+ graph_serdes->blob_len,
+ dst
+ ) ||
+ Z_TYPE_P(dst) != IS_OBJECT
+ ) {
+ return user_cache_decode_fail_zval(dst);
+ }
+
+ if ((graph_serdes->flags & PHP_USER_CACHE_SHARED_GRAPH_OBJECT_FLAG_SHARED) &&
+ !user_cache_decode_identity_map_insert(
+ (uint32_t) value->payload.offset,
+ Z_OBJ_P(dst)
+ )
+ ) {
+ return user_cache_decode_fail_zval(dst);
+ }
+
+ return true;
+}
+
+static PHP_USER_CACHE_DECODE_HOT bool user_cache_shared_graph_decode_enum(
+ const uint8_t *buf,
+ size_t buf_len,
+ const php_user_cache_shared_graph_value *value,
+ zval *dst
+)
+{
+ const php_user_cache_shared_graph_enum *graph_enum;
+ zend_string *class_name, *case_name;
+ zend_class_entry *ce;
+ zend_object *case_obj;
+
+ graph_enum = (const php_user_cache_shared_graph_enum *) (buf + (uint32_t) value->payload.offset);
+ case_obj = user_cache_decode_resolve_cache_find(graph_enum);
+ if (case_obj == NULL) {
+ class_name = user_cache_decode_string_at(buf, buf_len, graph_enum->class_name_offset);
+ case_name = user_cache_decode_string_at(buf, buf_len, graph_enum->case_name_offset);
+ if (class_name == NULL || case_name == NULL) {
+ return false;
+ }
+
+ ce = user_cache_decode_lookup_class(class_name);
+
+ if (ce == NULL || !(ce->ce_flags & ZEND_ACC_ENUM)) {
+ return false;
+ }
+
+ case_obj = zend_enum_get_case(ce, case_name);
+ if (case_obj == NULL) {
+ return false;
+ }
+
+ user_cache_decode_resolve_cache_store(graph_enum, case_obj);
+ }
+
+ ZVAL_OBJ(dst, case_obj);
+ GC_ADDREF(case_obj);
+
+ return true;
+}
+
+static PHP_USER_CACHE_DECODE_HOT bool user_cache_shared_graph_decode_reference(
+ const uint8_t *buf,
+ size_t buf_len,
+ const php_user_cache_shared_graph_value *value,
+ zval *dst
+)
+{
+ const php_user_cache_shared_graph_reference *graph_ref;
+ zend_reference *ref;
+
+ graph_ref = (const php_user_cache_shared_graph_reference *) (buf + (uint32_t) value->payload.offset);
+
+ ZVAL_NEW_EMPTY_REF(dst);
+ ref = Z_REF_P(dst);
+
+ ZVAL_UNDEF(&ref->val);
+
+ if ((graph_ref->flags & PHP_USER_CACHE_SHARED_GRAPH_OBJECT_FLAG_SHARED) &&
+ !user_cache_decode_reference_map_insert(
+ (uint32_t) value->payload.offset,
+ ref
+ )
+ ) {
+ return user_cache_decode_fail_zval(dst);
+ }
+
+ if (!user_cache_shared_graph_decode_value(
+ buf,
+ buf_len,
+ &graph_ref->inner,
+ &ref->val
+ )
+ ) {
+ return user_cache_decode_fail_zval(dst);
+ }
+
+ return true;
+}
+
+static PHP_USER_CACHE_DECODE_HOT bool user_cache_shared_graph_decode_value(
+ const uint8_t *buf,
+ size_t buf_len,
+ const php_user_cache_shared_graph_value *value,
+ zval *dst
+)
+{
+ const php_user_cache_shared_graph_shaped_array *graph_shaped_array;
+ zend_reference *shared_reference;
+ zend_array *shared_array;
+ zend_object *shared_obj;
+ size_t node_header_size;
+
+ if (php_user_cache_stack_overflowed()) {
+ return false;
+ }
+
+ /* Validate the node header before dereferencing it. */
+ node_header_size = user_cache_decode_node_header_size(value->type);
+ if (node_header_size != 0 &&
+ !user_cache_decode_range_ok(buf_len, (uint32_t) value->payload.offset, node_header_size)
+ ) {
+ return false;
+ }
+
+ switch (value->type) {
+ case PHP_USER_CACHE_SHARED_GRAPH_VALUE_UNDEF:
+ case PHP_USER_CACHE_SHARED_GRAPH_VALUE_NULL:
+ case PHP_USER_CACHE_SHARED_GRAPH_VALUE_TRUE:
+ case PHP_USER_CACHE_SHARED_GRAPH_VALUE_FALSE:
+ case PHP_USER_CACHE_SHARED_GRAPH_VALUE_LONG:
+ case PHP_USER_CACHE_SHARED_GRAPH_VALUE_DOUBLE:
+ case PHP_USER_CACHE_SHARED_GRAPH_VALUE_STRING:
+ return user_cache_shared_graph_decode_simple_value(buf, buf_len, value, dst);
+ case PHP_USER_CACHE_SHARED_GRAPH_VALUE_ARRAY:
+ if ((uint32_t) value->payload.offset == 0) {
+ ZVAL_EMPTY_ARRAY(dst);
+ } else {
+ if (!user_cache_decode_range_ok(
+ buf_len,
+ (uint32_t) value->payload.offset,
+ sizeof(zend_array))
+ ) {
+ return false;
+ }
+
+ ZVAL_ARR(dst, (zend_array *) (void *) (buf + (uint32_t) value->payload.offset));
+ Z_TYPE_FLAGS_P(dst) = 0;
+ }
+
+ return true;
+ case PHP_USER_CACHE_SHARED_GRAPH_VALUE_DYNAMIC_ARRAY:
+ return user_cache_shared_graph_decode_dynamic_array(buf, buf_len, value, dst);
+ case PHP_USER_CACHE_SHARED_GRAPH_VALUE_SHAPED_ARRAY:
+ graph_shaped_array = (const php_user_cache_shared_graph_shaped_array *) (buf + (uint32_t) value->payload.offset);
+
+ return user_cache_shared_graph_decode_shaped_array(
+ buf,
+ buf_len,
+ graph_shaped_array->shape_offset,
+ graph_shaped_array->values_offset,
+ graph_shaped_array->count,
+ graph_shaped_array->next_free,
+ (uint32_t) value->payload.offset,
+ (graph_shaped_array->reserved & PHP_USER_CACHE_SHARED_GRAPH_OBJECT_FLAG_SHARED) != 0,
+ dst
+ );
+ case PHP_USER_CACHE_SHARED_GRAPH_VALUE_ARRAY_REF: {
+ shared_array = user_cache_decode_array_map_find((uint32_t) value->payload.offset);
+ if (shared_array == NULL) {
+ return false;
+ }
+
+ ZVAL_ARR(dst, shared_array);
+ GC_ADDREF(shared_array);
+
+ return true;
+ }
+ case PHP_USER_CACHE_SHARED_GRAPH_VALUE_OBJECT:
+ return user_cache_shared_graph_decode_object(buf, buf_len, value, dst);
+ case PHP_USER_CACHE_SHARED_GRAPH_VALUE_SLEEP_OBJECT:
+ return user_cache_shared_graph_decode_sleep_object(buf, buf_len, value, dst);
+ case PHP_USER_CACHE_SHARED_GRAPH_VALUE_SLEEP_SHAPED_OBJECT:
+ return user_cache_shared_graph_decode_sleep_shaped_object(buf, buf_len, value, dst);
+ case PHP_USER_CACHE_SHARED_GRAPH_VALUE_OBJECT_REF: {
+ shared_obj = user_cache_decode_identity_map_find((uint32_t) value->payload.offset);
+ if (shared_obj == NULL) {
+ return false;
+ }
+
+ ZVAL_OBJ(dst, shared_obj);
+ GC_ADDREF(shared_obj);
+
+ return true;
+ }
+ case PHP_USER_CACHE_SHARED_GRAPH_VALUE_SAFE_DIRECT_OBJECT:
+ return user_cache_shared_graph_decode_safe_direct_object(buf, buf_len, value, dst);
+ case PHP_USER_CACHE_SHARED_GRAPH_VALUE_SERIALIZED_OBJECT:
+ return user_cache_shared_graph_decode_serialized_object(buf, buf_len, value, dst);
+ case PHP_USER_CACHE_SHARED_GRAPH_VALUE_SERIALIZED_SHAPED_OBJECT:
+ return user_cache_shared_graph_decode_serialized_shaped_object(buf, buf_len, value, dst);
+ case PHP_USER_CACHE_SHARED_GRAPH_VALUE_SERDES_OBJECT:
+ return user_cache_shared_graph_decode_serdes_object(buf, buf_len, value, dst);
+ case PHP_USER_CACHE_SHARED_GRAPH_VALUE_ENUM:
+ return user_cache_shared_graph_decode_enum(buf, buf_len, value, dst);
+ case PHP_USER_CACHE_SHARED_GRAPH_VALUE_REFERENCE:
+ return user_cache_shared_graph_decode_reference(buf, buf_len, value, dst);
+ case PHP_USER_CACHE_SHARED_GRAPH_VALUE_REFERENCE_REF: {
+ shared_reference = user_cache_decode_reference_map_find((uint32_t) value->payload.offset);
+
+ if (shared_reference == NULL) {
+ return false;
+ }
+
+ ZVAL_REF(dst, shared_reference);
+ GC_ADDREF(shared_reference);
+
+ return true;
+ }
+ default:
+ return false;
+ }
+}
+
+static php_user_cache_shared_graph_header *user_cache_shared_graph_payload_header(uint32_t payload_offset)
+{
+ const uint8_t *graph_buf;
+ php_user_cache_shared_graph_header *header;
+ size_t buf_len;
+
+ if (payload_offset == 0) {
+ return NULL;
+ }
+
+ buf_len = php_user_cache_block_payload_capacity(payload_offset);
+ if (buf_len == 0) {
+ return NULL;
+ }
+
+ graph_buf = user_cache_shared_graph_locate(
+ php_user_cache_ptr(payload_offset),
+ buf_len,
+ NULL
+ );
+ if (graph_buf == NULL) {
+ return NULL;
+ }
+
+ header = (php_user_cache_shared_graph_header *) graph_buf;
+
+ return header;
+}
+
+/* Debug-only cross-check for recorded relocation fixups. */
+#if ZEND_DEBUG
+static bool user_cache_shared_graph_rebase_verbatim_zval(
+ php_user_cache_shared_graph_rebase_context *ctx,
+ zval *value)
+{
+ zend_array *arr;
+
+ switch (Z_TYPE_P(value)) {
+ case IS_STRING:
+ Z_STR_P(value) = (zend_string *) user_cache_shared_graph_rebase_pointer(
+ Z_STR_P(value),
+ ctx->old_base,
+ ctx->len,
+ ctx->delta
+ );
+
+ return true;
+ case IS_ARRAY:
+ arr = (zend_array *) user_cache_shared_graph_rebase_pointer(
+ Z_ARR_P(value),
+ ctx->old_base,
+ ctx->len,
+ ctx->delta
+ );
+
+ Z_ARR_P(value) = arr;
+
+ if (!user_cache_shared_graph_pointer_in_range(arr, ctx->new_base, ctx->len)) {
+ return true;
+ }
+
+ return user_cache_shared_graph_rebase_verbatim_array(ctx, arr);
+ default:
+ return true;
+ }
+}
+
+static bool user_cache_shared_graph_rebase_verbatim_array(
+ php_user_cache_shared_graph_rebase_context *ctx,
+ zend_array *arr)
+{
+ zval *packed;
+ Bucket *bucket;
+ uint32_t i;
+ void *data;
+
+ if (php_user_cache_stack_overflowed()) {
+ return false;
+ }
+
+ if (!user_cache_shared_graph_pointer_in_range(
+ arr,
+ ctx->new_base,
+ ctx->len
+ )
+ ) {
+ return true;
+ }
+
+ if (!php_user_cache_seen_test_and_add(ctx->seen, arr)) {
+ return true;
+ }
+
+ data = HT_GET_DATA_ADDR(arr);
+ data = user_cache_shared_graph_rebase_pointer(data, ctx->old_base, ctx->len, ctx->delta);
+
+ HT_SET_DATA_ADDR(arr, data);
+
+ if (!user_cache_shared_graph_pointer_in_range(data, ctx->new_base, ctx->len)) {
+ return false;
+ }
+
+ if (HT_IS_PACKED(arr)) {
+ packed = arr->arPacked;
+ for (i = 0; i < arr->nNumUsed; i++) {
+ if (!user_cache_shared_graph_rebase_verbatim_zval(ctx, &packed[i])) {
+ return false;
+ }
+ }
+ } else {
+ bucket = arr->arData;
+ for (i = 0; i < arr->nNumUsed; i++) {
+ if (bucket[i].key != NULL) {
+ bucket[i].key = (zend_string *) user_cache_shared_graph_rebase_pointer(
+ bucket[i].key,
+ ctx->old_base,
+ ctx->len,
+ ctx->delta
+ );
+
+ if (!user_cache_shared_graph_pointer_in_range(bucket[i].key, ctx->new_base, ctx->len)) {
+ return false;
+ }
+ }
+
+ if (!user_cache_shared_graph_rebase_verbatim_zval(ctx, &bucket[i].val)) {
+ return false;
+ }
+ }
+ }
+
+ return true;
+}
+
+static bool user_cache_shared_graph_rebase_graph_value(
+ php_user_cache_shared_graph_rebase_context *ctx,
+ const php_user_cache_shared_graph_value *value)
+{
+ const php_user_cache_shared_graph_array *graph_array;
+ const php_user_cache_shared_graph_array_element *graph_elems;
+ const php_user_cache_shared_graph_shaped_array *graph_shaped_array;
+ const php_user_cache_shared_graph_value *graph_vals;
+ const php_user_cache_shared_graph_object *graph_obj;
+ const php_user_cache_shared_graph_property *props;
+ const php_user_cache_shared_graph_safe_direct_object *graph_safe_direct;
+ const php_user_cache_shared_graph_shaped_state_object *graph_shaped_state;
+ const php_user_cache_shared_graph_state_schema *state_schema;
+ zend_array *arr;
+ uint32_t i;
+
+ if (php_user_cache_stack_overflowed()) {
+ return false;
+ }
+
+ switch (value->type) {
+ case PHP_USER_CACHE_SHARED_GRAPH_VALUE_ARRAY:
+ if ((uint32_t) value->payload.offset == 0) {
+ return true;
+ }
+
+ arr = (zend_array *) (void *) (ctx->new_base + (uint32_t) value->payload.offset);
+
+ return user_cache_shared_graph_rebase_verbatim_array(ctx, arr);
+ case PHP_USER_CACHE_SHARED_GRAPH_VALUE_DYNAMIC_ARRAY:
+ graph_array = (const php_user_cache_shared_graph_array *) (ctx->new_base + (uint32_t) value->payload.offset);
+ graph_elems = (const php_user_cache_shared_graph_array_element *) (ctx->new_base + graph_array->elements_offset);
+
+ for (i = 0; i < graph_array->count; i++) {
+ if (!user_cache_shared_graph_rebase_graph_value(ctx, &graph_elems[i].value)) {
+ return false;
+ }
+ }
+
+ return true;
+ case PHP_USER_CACHE_SHARED_GRAPH_VALUE_SHAPED_ARRAY:
+ graph_shaped_array = (const php_user_cache_shared_graph_shaped_array *) (ctx->new_base + (uint32_t) value->payload.offset);
+ graph_vals = (const php_user_cache_shared_graph_value *) (ctx->new_base + graph_shaped_array->values_offset);
+
+ for (i = 0; i < graph_shaped_array->count; i++) {
+ if (!user_cache_shared_graph_rebase_graph_value(ctx, &graph_vals[i])) {
+ return false;
+ }
+ }
+
+ return true;
+ case PHP_USER_CACHE_SHARED_GRAPH_VALUE_OBJECT:
+ case PHP_USER_CACHE_SHARED_GRAPH_VALUE_SLEEP_OBJECT:
+ graph_obj = (const php_user_cache_shared_graph_object *) (ctx->new_base + (uint32_t) value->payload.offset);
+ props = (const php_user_cache_shared_graph_property *) (ctx->new_base + graph_obj->properties_offset);
+ for (i = 0; i < graph_obj->property_count; i++) {
+ if (!user_cache_shared_graph_rebase_graph_value(ctx, &props[i].value)) {
+ return false;
+ }
+ }
+
+ return true;
+ case PHP_USER_CACHE_SHARED_GRAPH_VALUE_REFERENCE:
+ return user_cache_shared_graph_rebase_graph_value(
+ ctx,
+ &((const php_user_cache_shared_graph_reference *) (ctx->new_base + (uint32_t) value->payload.offset))->inner
+ );
+ case PHP_USER_CACHE_SHARED_GRAPH_VALUE_SLEEP_SHAPED_OBJECT:
+ case PHP_USER_CACHE_SHARED_GRAPH_VALUE_SERIALIZED_SHAPED_OBJECT:
+ graph_shaped_state =
+ (const php_user_cache_shared_graph_shaped_state_object *) (ctx->new_base + (uint32_t) value->payload.offset)
+ ;
+ state_schema =
+ (const php_user_cache_shared_graph_state_schema *) (ctx->new_base + graph_shaped_state->state_schema_offset)
+ ;
+ graph_vals = (const php_user_cache_shared_graph_value *) (ctx->new_base + graph_shaped_state->state_values_offset);
+
+ for (i = 0; i < state_schema->count; i++) {
+ if (!user_cache_shared_graph_rebase_graph_value(ctx, &graph_vals[i])) {
+ return false;
+ }
+ }
+
+ return true;
+ case PHP_USER_CACHE_SHARED_GRAPH_VALUE_SERIALIZED_OBJECT:
+ ZEND_FALLTHROUGH;
+ case PHP_USER_CACHE_SHARED_GRAPH_VALUE_SAFE_DIRECT_OBJECT: {
+ graph_safe_direct =
+ (const php_user_cache_shared_graph_safe_direct_object *) (ctx->new_base + (uint32_t) value->payload.offset)
+ ;
+
+ if (!user_cache_shared_graph_rebase_graph_value(ctx, &graph_safe_direct->state)) {
+ return false;
+ }
+
+ props = (const php_user_cache_shared_graph_property *) (ctx->new_base + graph_safe_direct->properties_offset);
+ for (i = 0; i < graph_safe_direct->property_count; i++) {
+ if (!user_cache_shared_graph_rebase_graph_value(ctx, &props[i].value)) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+ default:
+ return true;
+ }
+}
+#endif
+
+static void user_cache_shared_graph_force_retire_locked(uint32_t payload_offset)
+{
+ php_user_cache_shared_graph_header *header = 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 & PHP_USER_CACHE_SHARED_GRAPH_RETIRED) != 0) {
+ return;
+ }
+
+ expected = state;
+ if (zend_atomic_int_compare_exchange_ex(&header->ref_state, &expected, state | PHP_USER_CACHE_SHARED_GRAPH_RETIRED)) {
+ return;
+ }
+ }
+}
+
+static int user_cache_shared_graph_orphan_offset_compare(const void *a, const void *b)
+{
+ uint32_t lhs = *(const uint32_t *) a;
+ uint32_t rhs = *(const uint32_t *) b;
+
+ return lhs < rhs ? -1 : (lhs > rhs ? 1 : 0);
+}
+
+/* Snapshot live offsets once for orphan recovery. */
+static uint32_t user_cache_shared_graph_snapshot_live_offsets_locked(
+ php_user_cache_header *header,
+ uint32_t **offsets_out
+)
+{
+ php_user_cache_entry *entries, *entry;
+ uint32_t *offsets;
+ uint32_t i, count;
+
+ entries = php_user_cache_entries_ptr(header);
+ offsets = emalloc((size_t) header->capacity * 2 * sizeof(*offsets));
+ count = 0;
+
+ for (i = 0; i < header->capacity; i++) {
+ entry = &entries[i];
+ if (entry->state != PHP_USER_CACHE_ENTRY_USED) {
+ continue;
+ }
+
+ if (entry->key_offset != 0) {
+ offsets[count++] = entry->key_offset;
+ }
+
+ if (entry->value_offset != 0) {
+ offsets[count++] = entry->value_offset;
+ }
+ }
+
+ qsort(offsets, count, sizeof(*offsets), user_cache_shared_graph_orphan_offset_compare);
+
+ *offsets_out = offsets;
+
+ return count;
+}
+
+static bool user_cache_orphaned_graph_block_is_referenced_locked(
+ const uint32_t *live_offsets,
+ uint32_t live_offset_count,
+ uint32_t block_offset,
+ uint32_t block_size
+)
+{
+ uint32_t payload_start, lo, hi, mid;
+
+ payload_start = block_offset + (uint32_t) sizeof(php_user_cache_block);
+ lo = 0;
+ hi = live_offset_count;
+
+ while (lo < hi) {
+ mid = lo + (hi - lo) / 2;
+
+ if (live_offsets[mid] < payload_start) {
+ lo = mid + 1;
+ } else {
+ hi = mid;
+ }
+ }
+
+ return lo < live_offset_count && live_offsets[lo] < block_offset + block_size;
+}
+
+static bool user_cache_shared_graph_reclaim_orphaned_by_scan_locked(php_user_cache_header *header)
+{
+ php_user_cache_block *block;
+ php_user_cache_shared_graph_header *graph_header;
+ uint32_t *live_offsets;
+ uint32_t used_end, offset, block_size, payload_offset, live_offset_count;
+ bool reclaimed = false, restart;
+
+ live_offset_count = user_cache_shared_graph_snapshot_live_offsets_locked(header, &live_offsets);
+
+ do {
+ restart = false;
+ used_end = header->data_offset + header->next_free;
+ offset = header->data_offset;
+
+ while (offset < used_end) {
+ block = php_user_cache_block_ptr(offset);
+ block_size = block->size;
+
+ if (block_size < PHP_USER_CACHE_ALIGNED_SIZE(sizeof(php_user_cache_block) + 1) ||
+ block_size > used_end - offset
+ ) {
+ goto done;
+ }
+
+ if (!php_user_cache_block_is_free(block) &&
+ !user_cache_orphaned_graph_block_is_referenced_locked(
+ live_offsets, live_offset_count, offset, block_size
+ )
+ ) {
+ payload_offset = offset + (uint32_t) sizeof(php_user_cache_block);
+ graph_header = user_cache_shared_graph_payload_header(payload_offset);
+
+ if (graph_header != NULL &&
+ zend_atomic_int_load_ex(&graph_header->ref_state) == PHP_USER_CACHE_SHARED_GRAPH_RETIRED
+ ) {
+ php_user_cache_free_locked(payload_offset);
+ reclaimed = true;
+ restart = true;
+
+ break;
+ }
+ }
+
+ offset += block_size;
+ }
+ } while (restart);
+
+done:
+ efree(live_offsets);
+
+ return reclaimed;
+}
+
+static bool user_cache_shared_graph_load_root_value(
+ const php_user_cache_shared_graph_header *header,
+ size_t buf_len,
+ php_user_cache_shared_graph_value *root_value
+)
+{
+ uint32_t root_type;
+
+ if (header->root_offset != 0 && header->root_offset >= buf_len) {
+ return false;
+ }
+
+ root_type = header->root_type != 0 ? header->root_type : PHP_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 PHP_USER_CACHE_SHARED_GRAPH_VALUE_ARRAY:
+ case PHP_USER_CACHE_SHARED_GRAPH_VALUE_DYNAMIC_ARRAY:
+ case PHP_USER_CACHE_SHARED_GRAPH_VALUE_SHAPED_ARRAY:
+ case PHP_USER_CACHE_SHARED_GRAPH_VALUE_ENUM:
+ case PHP_USER_CACHE_SHARED_GRAPH_VALUE_STRING:
+ return true;
+ case PHP_USER_CACHE_SHARED_GRAPH_VALUE_OBJECT:
+ case PHP_USER_CACHE_SHARED_GRAPH_VALUE_SLEEP_OBJECT:
+ case PHP_USER_CACHE_SHARED_GRAPH_VALUE_SLEEP_SHAPED_OBJECT:
+ case PHP_USER_CACHE_SHARED_GRAPH_VALUE_SAFE_DIRECT_OBJECT:
+ case PHP_USER_CACHE_SHARED_GRAPH_VALUE_SERIALIZED_OBJECT:
+ case PHP_USER_CACHE_SHARED_GRAPH_VALUE_SERIALIZED_SHAPED_OBJECT:
+ case PHP_USER_CACHE_SHARED_GRAPH_VALUE_SERDES_OBJECT:
+ return header->root_offset != 0;
+ default:
+ return false;
+ }
+}
+
+#if ZEND_DEBUG
+static bool user_cache_shared_graph_rebase_payload_pointers(
+ const uint8_t *buf,
+ size_t graph_len,
+ const uint8_t *old_base,
+ ptrdiff_t delta)
+{
+ const php_user_cache_shared_graph_header *header;
+ php_user_cache_shared_graph_rebase_context ctx;
+ php_user_cache_shared_graph_value root_value;
+ HashTable seen_arrs;
+ bool result;
+
+ header = (const php_user_cache_shared_graph_header *) buf;
+
+ /* This path has not called shared_graph_locate(). */
+ if (!user_cache_shared_graph_header_is_valid(header) ||
+ !user_cache_shared_graph_load_root_value(header, graph_len, &root_value)
+ ) {
+ return false;
+ }
+
+ zend_hash_init(&seen_arrs, 8, NULL, NULL, 0);
+
+ ctx.old_base = old_base;
+ ctx.new_base = buf;
+ ctx.len = graph_len;
+ ctx.delta = delta;
+ ctx.seen = &seen_arrs;
+
+ result = user_cache_shared_graph_rebase_graph_value(&ctx, &root_value);
+
+ zend_hash_destroy(&seen_arrs);
+
+ return result;
+}
+
+/* Verify that relocation left no pointers to rewrite. */
+static void user_cache_shared_graph_check_rebase_complete(
+ const uint8_t *dst_base,
+ size_t graph_len,
+ const uint8_t *src_base)
+{
+ uint8_t *check_buf;
+ bool result;
+
+ check_buf = emalloc(graph_len);
+ memcpy(check_buf, dst_base, graph_len);
+
+ result = user_cache_shared_graph_rebase_payload_pointers(
+ dst_base,
+ graph_len,
+ src_base,
+ (ptrdiff_t) (src_base - dst_base)
+ );
+ ZEND_ASSERT(result);
+ ZEND_ASSERT(memcmp(check_buf, dst_base, graph_len) == 0);
+
+ efree(check_buf);
+}
+#else
+# define user_cache_shared_graph_check_rebase_complete(dst_base, graph_len, src_base)
+#endif
+
+static void user_cache_destroy_shared_graph_ref_index(void)
+{
+ if (UC_G(shared_graph_ref_index) != NULL) {
+ zend_hash_destroy(UC_G(shared_graph_ref_index));
+ efree(UC_G(shared_graph_ref_index));
+
+ UC_G(shared_graph_ref_index) = NULL;
+ }
+}
+
+static void user_cache_shared_graph_refs_check_fork(void)
+{
+ uint64_t pid = php_user_cache_cached_pid();
+
+ if (UC_G(shared_graph_ref_owner_pid) == pid) {
+ return;
+ }
+
+ if (UC_G(shared_graph_ref_owner_pid) != 0) {
+ if (UC_G(shared_graph_refs) != NULL) {
+ efree(UC_G(shared_graph_refs));
+
+ UC_G(shared_graph_refs) = NULL;
+ }
+
+ UC_G(shared_graph_ref_count) = 0;
+ UC_G(shared_graph_ref_capacity) = 0;
+
+ user_cache_destroy_shared_graph_ref_index();
+ }
+
+ UC_G(shared_graph_ref_owner_pid) = pid;
+}
+
+static void user_cache_grow_shared_graph_refs(
+ php_user_cache_shared_graph_ref **refs,
+ uint32_t *capacity,
+ uint32_t initial_capacity)
+{
+ *capacity = *capacity == 0 ? initial_capacity : *capacity * 2;
+ *refs = erealloc(*refs, sizeof(**refs) * *capacity);
+}
+
+static void user_cache_ensure_shared_graph_ref_index(void)
+{
+ if (UC_G(shared_graph_ref_index) == NULL) {
+ UC_G(shared_graph_ref_index) = emalloc(sizeof(HashTable));
+ zend_hash_init(UC_G(shared_graph_ref_index), 8, NULL, NULL, 0);
+ }
+}
+
+/* Include the context to disambiguate equal offsets in different pools. */
+static zend_ulong user_cache_shared_graph_ref_index_key(
+ const php_user_cache_context *ctx,
+ uint32_t payload_offset
+)
+{
+ return ((zend_ulong) (uintptr_t) ctx) ^ (zend_ulong) payload_offset;
+}
+
+PHP_USER_CACHE_DECODE_HOT bool php_user_cache_shared_graph_update_object_property(
+ zval *obj_zv,
+ zend_string *prop_name,
+ zval *prop_val
+)
+{
+ const char *class_name, *unmangled_name;
+ zend_string *cname;
+ zend_class_entry *scope;
+ zend_object *obj;
+ zend_property_info *prop_info;
+ HashTable *props;
+ size_t unmangled_name_len;
+ bool failed;
+
+ obj = Z_OBJ_P(obj_zv);
+ if (user_cache_shared_graph_is_unmangled_property_name(prop_name)) {
+ prop_info = zend_get_property_info(obj->ce, prop_name, true);
+ if (user_cache_shared_graph_try_update_declared_property(
+ obj,
+ prop_name,
+ prop_info,
+ prop_val,
+ &failed)
+ ) {
+ return true;
+ }
+
+ if (failed) {
+ return false;
+ }
+
+ if (Z_ISREF_P(prop_val)) {
+ props = zend_std_get_properties(obj);
+
+ if (props != NULL) {
+ Z_TRY_ADDREF_P(prop_val);
+
+ zend_hash_update(props, prop_name, prop_val);
+
+ return true;
+ }
+ }
+
+ scope = obj->ce;
+ if (prop_info != NULL &&
+ prop_info != ZEND_WRONG_PROPERTY_INFO &&
+ prop_info->ce != NULL
+ ) {
+ scope = prop_info->ce;
+ }
+
+ zend_update_property(scope, obj, ZSTR_VAL(prop_name), ZSTR_LEN(prop_name), prop_val);
+
+ return !EG(exception);
+ }
+
+ if (zend_unmangle_property_name_ex(prop_name, &class_name, &unmangled_name, &unmangled_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_string_release_ex(cname, 0);
+
+ return false;
+ }
+
+ zend_update_property(scope, obj, unmangled_name, unmangled_name_len, prop_val);
+ zend_string_release_ex(cname, 0);
+ } else {
+ scope = obj->ce;
+ prop_info = zend_hash_str_find_ptr(&obj->ce->properties_info, unmangled_name, unmangled_name_len);
+ if (prop_info != NULL && prop_info->ce != NULL) {
+ scope = prop_info->ce;
+ }
+
+ zend_update_property(scope, obj, unmangled_name, unmangled_name_len, prop_val);
+ }
+ }
+
+ return !EG(exception);
+}
+
+PHP_USER_CACHE_DECODE_HOT bool php_user_cache_shared_graph_update_object_property_at(
+ zval *obj_zv,
+ zend_string *prop_name,
+ uint32_t prop_idx,
+ zval *prop_val
+)
+{
+ zend_object *obj;
+ zend_property_info *prop_info;
+ bool failed;
+
+ obj = Z_OBJ_P(obj_zv);
+ if (user_cache_shared_graph_is_unmangled_property_name(prop_name) &&
+ obj->ce->type == ZEND_USER_CLASS &&
+ obj->ce->properties_info_table != NULL &&
+ prop_idx < obj->ce->default_properties_count
+ ) {
+ prop_info = obj->ce->properties_info_table[prop_idx];
+
+ if (user_cache_shared_graph_try_update_declared_property(
+ obj,
+ prop_name,
+ prop_info,
+ prop_val,
+ &failed
+ )
+ ) {
+ return true;
+ }
+
+ if (failed) {
+ return false;
+ }
+ }
+
+ return php_user_cache_shared_graph_update_object_property(
+ obj_zv,
+ prop_name,
+ prop_val
+ );
+}
+
+void php_user_cache_decode_resolve_cache_release(void)
+{
+ user_cache_shared_graph_object_route_memo_release();
+
+ memset((void *) UC_G(decode_resolve_direct_keys), 0, sizeof(UC_G(decode_resolve_direct_keys)));
+ memset(UC_G(decode_resolve_direct_values), 0, sizeof(UC_G(decode_resolve_direct_values)));
+
+ UC_G(decode_resolve_direct_next) = 0;
+
+ if (UC_G(decode_resolve_cache) == NULL) {
+ return;
+ }
+
+ zend_hash_destroy(UC_G(decode_resolve_cache));
+
+ efree(UC_G(decode_resolve_cache));
+
+ UC_G(decode_resolve_cache) = NULL;
+}
+
+void php_user_cache_decode_shape_prototype_cache_release(void)
+{
+ memset((void *) UC_G(decode_shape_prototype_direct_keys), 0, sizeof(UC_G(decode_shape_prototype_direct_keys)));
+ memset(UC_G(decode_shape_prototype_direct_values), 0, sizeof(UC_G(decode_shape_prototype_direct_values)));
+
+ UC_G(decode_shape_prototype_direct_next) = 0;
+
+ if (UC_G(decode_shape_prototype_cache) == NULL) {
+ return;
+ }
+
+ zend_hash_destroy(UC_G(decode_shape_prototype_cache));
+ efree(UC_G(decode_shape_prototype_cache));
+
+ UC_G(decode_shape_prototype_cache) = NULL;
+}
+
+/* A bailout may bypass normal decode-map cleanup. */
+void php_user_cache_decode_maps_teardown(void)
+{
+ user_cache_decode_identity_map_teardown();
+ user_cache_decode_reference_map_teardown();
+ user_cache_decode_array_map_teardown();
+}
+
+bool php_user_cache_shared_graph_prefers_prototype(uint32_t payload_offset)
+{
+ php_user_cache_shared_graph_header *header =
+ user_cache_shared_graph_payload_header(payload_offset)
+ ;
+
+ if (header == NULL) {
+ return true;
+ }
+
+ return (header->flags & PHP_USER_CACHE_SHARED_GRAPH_FLAG_PREFERS_PROTOTYPE) != 0;
+}
+
+bool php_user_cache_shared_graph_payload_has_aliases(uint32_t payload_offset)
+{
+ php_user_cache_shared_graph_header *header =
+ user_cache_shared_graph_payload_header(payload_offset)
+ ;
+
+ if (header == NULL) {
+ return true;
+ }
+
+ return (header->flags & PHP_USER_CACHE_SHARED_GRAPH_FLAG_HAS_SHARED_IDENTITY) != 0;
+}
+
+bool php_user_cache_shared_graph_decode_is_lock_safe(uint32_t payload_offset)
+{
+ php_user_cache_shared_graph_header *header =
+ user_cache_shared_graph_payload_header(payload_offset)
+ ;
+
+ return header != NULL &&
+ (header->flags & PHP_USER_CACHE_SHARED_GRAPH_FLAG_HAS_OBJECT) == 0
+ ;
+}
+
+/* Prove verbatim eligibility and compute the root size in one walk. */
+php_user_cache_verbatim_root_result php_user_cache_shared_graph_calc_verbatim_root(
+ const zval *value,
+ HashTable *verbatim_verdicts,
+ size_t *buf_len)
+{
+ php_user_cache_shared_graph_calc_context calc_ctx;
+ php_user_cache_verbatim_root_result result;
+ zval verdict_zv;
+ bool immutable, completed;
+
+ if (!user_cache_shared_graph_can_use_verbatim_arrays() ||
+ Z_ARRVAL_P(value)->nNumOfElements == 0
+ ) {
+ return PHP_USER_CACHE_VERBATIM_ROOT_UNDECIDED;
+ }
+
+ immutable = (GC_FLAGS(Z_ARRVAL_P(value)) & IS_ARRAY_IMMUTABLE) != 0;
+
+ user_cache_shared_graph_calc_init(&calc_ctx);
+ calc_ctx.verbatim_arrays_allowed = true;
+ calc_ctx.state_memo = NULL;
+
+ completed = user_cache_shared_graph_calc_reserve(
+ &calc_ctx,
+ sizeof(php_user_cache_shared_graph_header)
+ ) &&
+ user_cache_shared_graph_calc_verbatim_value(&calc_ctx, value)
+ ;
+
+ if (completed && calc_ctx.size <= UINT32_MAX - (ZEND_MM_ALIGNMENT - 1)) {
+ *buf_len = calc_ctx.size + (ZEND_MM_ALIGNMENT - 1);
+ result = PHP_USER_CACHE_VERBATIM_ROOT_SIZED;
+ } else if (immutable || completed) {
+ /* Immutable arrays cannot fail verbatim eligibility. */
+ result = PHP_USER_CACHE_VERBATIM_ROOT_ELIGIBLE_UNSIZED;
+ } else if (calc_ctx.reserve_failed) {
+ result = PHP_USER_CACHE_VERBATIM_ROOT_UNDECIDED;
+ } else {
+ result = PHP_USER_CACHE_VERBATIM_ROOT_INELIGIBLE;
+ }
+
+ user_cache_shared_graph_calc_destroy(&calc_ctx);
+
+ if (!immutable && result != PHP_USER_CACHE_VERBATIM_ROOT_UNDECIDED) {
+ ZVAL_BOOL(&verdict_zv, result != PHP_USER_CACHE_VERBATIM_ROOT_INELIGIBLE);
+ zend_hash_index_add(verbatim_verdicts, (zend_ulong) (uintptr_t) Z_ARRVAL_P(value), &verdict_zv);
+ }
+
+ return result;
+}
+
+bool php_user_cache_shared_graph_can_copy_verbatim_root(const zval *value, HashTable *verbatim_verdicts)
+{
+ HashTable seen_arrs, direct_verdicts;
+ zval verdict_zv;
+ bool eligible;
+
+ if (!user_cache_shared_graph_can_use_verbatim_arrays()) {
+ return false;
+ }
+
+ if (GC_FLAGS(Z_ARRVAL_P(value)) & IS_ARRAY_IMMUTABLE) {
+ return true;
+ }
+
+ zend_hash_init(&seen_arrs, 8, NULL, NULL, 0);
+ zend_hash_init(&direct_verdicts, 8, NULL, NULL, 0);
+
+ eligible = user_cache_shared_graph_can_copy_verbatim_value(
+ &seen_arrs,
+ &direct_verdicts,
+ value
+ );
+
+ zend_hash_destroy(&direct_verdicts);
+ zend_hash_destroy(&seen_arrs);
+
+ ZVAL_BOOL(&verdict_zv, eligible);
+ zend_hash_index_add(verbatim_verdicts, (zend_ulong) (uintptr_t) Z_ARRVAL_P(value), &verdict_zv);
+
+ return eligible;
+}
+
+bool php_user_cache_calculate_shared_graph_size(
+ const zval *value,
+ HashTable *state_memo,
+ HashTable *verbatim_verdicts,
+ size_t *buf_len
+)
+{
+ php_user_cache_shared_graph_calc_context calc_ctx;
+ zend_class_entry *root_ce;
+ bool result;
+
+ if (!buf_len) {
+ return false;
+ }
+
+ *buf_len = 0;
+
+ if (Z_TYPE_P(value) == IS_OBJECT) {
+ root_ce = Z_OBJCE_P(value);
+ if (!(root_ce->ce_flags & ZEND_ACC_ENUM) &&
+ user_cache_shared_graph_classify_object_route(root_ce) == PHP_USER_CACHE_OBJECT_ROUTE_UNSTORABLE
+ ) {
+ return false;
+ }
+ } else if (Z_TYPE_P(value) != IS_ARRAY && Z_TYPE_P(value) != IS_STRING) {
+ return false;
+ }
+
+ user_cache_shared_graph_calc_init(&calc_ctx);
+
+ calc_ctx.verbatim_arrays_allowed = user_cache_shared_graph_can_use_verbatim_arrays();
+ calc_ctx.shared_verdicts = verbatim_verdicts;
+ calc_ctx.state_memo = state_memo;
+ result = user_cache_shared_graph_calc_reserve(
+ &calc_ctx,
+ sizeof(php_user_cache_shared_graph_header)
+ );
+
+ if (result) {
+ result = user_cache_shared_graph_calc_value(&calc_ctx, value);
+ }
+
+ if (result) {
+ if (calc_ctx.size > UINT32_MAX - (ZEND_MM_ALIGNMENT - 1)) {
+ result = false;
+ } else {
+ calc_ctx.size += ZEND_MM_ALIGNMENT - 1;
+ }
+ }
+
+ if (result) {
+ *buf_len = calc_ctx.size;
+ }
+
+ user_cache_shared_graph_calc_destroy(&calc_ctx);
+
+ return result;
+}
+
+bool php_user_cache_build_shared_graph_in_place(
+ const zval *value,
+ HashTable *state_memo,
+ const HashTable *verbatim_verdicts,
+ uint8_t *buf,
+ size_t buf_len,
+ size_t *graph_len,
+ bool *has_verbatim_array,
+ uint32_t **fixup_offsets,
+ uint32_t *fixup_count
+)
+{
+ php_user_cache_shared_graph_copy_context copy_ctx;
+ php_user_cache_shared_graph_header *header;
+ php_user_cache_shared_graph_value root_value;
+ uint32_t header_offset, root_offset, root_type;
+ size_t padding;
+ bool result;
+
+ if (buf == NULL) {
+ return false;
+ }
+
+ padding = user_cache_shared_graph_alignment_padding(buf);
+ if (padding > buf_len || buf_len - padding < sizeof(php_user_cache_shared_graph_header)) {
+ return false;
+ }
+
+ if (padding != 0) {
+ memset(buf, 0, padding);
+ }
+
+ buf += padding;
+ buf_len -= padding;
+
+ user_cache_shared_graph_copy_init(©_ctx, buf, buf_len);
+
+ copy_ctx.shared_verdicts = verbatim_verdicts;
+ copy_ctx.state_memo = state_memo;
+ root_offset = 0;
+ root_type = 0;
+
+ result = user_cache_shared_graph_copy_alloc(©_ctx, sizeof(*header), &header_offset) && header_offset == 0;
+ if (result) {
+ if (Z_TYPE_P(value) == IS_OBJECT) {
+ result = user_cache_shared_graph_copy_value(©_ctx, value, &root_value) &&
+ (
+ root_value.type == PHP_USER_CACHE_SHARED_GRAPH_VALUE_OBJECT ||
+ root_value.type == PHP_USER_CACHE_SHARED_GRAPH_VALUE_SAFE_DIRECT_OBJECT ||
+ root_value.type == PHP_USER_CACHE_SHARED_GRAPH_VALUE_SERIALIZED_OBJECT ||
+ root_value.type == PHP_USER_CACHE_SHARED_GRAPH_VALUE_SERIALIZED_SHAPED_OBJECT ||
+ root_value.type == PHP_USER_CACHE_SHARED_GRAPH_VALUE_SERDES_OBJECT ||
+ root_value.type == PHP_USER_CACHE_SHARED_GRAPH_VALUE_SLEEP_OBJECT ||
+ root_value.type == PHP_USER_CACHE_SHARED_GRAPH_VALUE_SLEEP_SHAPED_OBJECT ||
+ root_value.type == PHP_USER_CACHE_SHARED_GRAPH_VALUE_ENUM
+ )
+ ;
+ } else if (Z_TYPE_P(value) == IS_ARRAY) {
+ result = user_cache_shared_graph_copy_value(©_ctx, value, &root_value) &&
+ (
+ root_value.type == PHP_USER_CACHE_SHARED_GRAPH_VALUE_ARRAY ||
+ root_value.type == PHP_USER_CACHE_SHARED_GRAPH_VALUE_DYNAMIC_ARRAY ||
+ root_value.type == PHP_USER_CACHE_SHARED_GRAPH_VALUE_SHAPED_ARRAY
+ )
+ ;
+ } else if (Z_TYPE_P(value) == IS_STRING) {
+ result = user_cache_shared_graph_copy_value(©_ctx, value, &root_value) &&
+ root_value.type == PHP_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) {
+ user_cache_shared_graph_copy_destroy(©_ctx);
+
+ return false;
+ }
+
+ header = (php_user_cache_shared_graph_header *) buf;
+ header->magic = PHP_USER_CACHE_SHARED_GRAPH_MAGIC;
+ header->version = PHP_USER_CACHE_SHARED_GRAPH_VERSION;
+ header->root_offset = root_offset;
+ header->root_type = root_type;
+ header->flags =
+ (copy_ctx.has_shared_identity ? PHP_USER_CACHE_SHARED_GRAPH_FLAG_HAS_SHARED_IDENTITY : 0) |
+ (copy_ctx.has_object ? PHP_USER_CACHE_SHARED_GRAPH_FLAG_HAS_OBJECT : 0) |
+ ((copy_ctx.prefers_prototype && !copy_ctx.has_userland_restore_object) ? PHP_USER_CACHE_SHARED_GRAPH_FLAG_PREFERS_PROTOTYPE : 0)
+ ;
+
+ ZEND_ATOMIC_INT_INIT(&header->ref_state, 0);
+
+ if (graph_len != NULL) {
+ *graph_len = copy_ctx.position;
+ }
+
+ if (has_verbatim_array != NULL) {
+ *has_verbatim_array = copy_ctx.has_verbatim_array;
+ }
+
+ ZEND_ASSERT(copy_ctx.has_verbatim_array || copy_ctx.fixup_count == 0);
+
+ /* Transfer relocation fixups to the prepared value. */
+ if (fixup_offsets != NULL) {
+ *fixup_offsets = copy_ctx.fixup_offsets;
+ *fixup_count = copy_ctx.fixup_count;
+
+ copy_ctx.fixup_offsets = NULL;
+ copy_ctx.fixup_count = 0;
+ copy_ctx.fixup_capacity = 0;
+ }
+
+ user_cache_shared_graph_copy_destroy(©_ctx);
+
+ return true;
+}
+
+bool php_user_cache_shared_graph_copy_fits_buffer(
+ const uint8_t *dst_buf,
+ size_t dst_buf_len,
+ const uint8_t *src_buf,
+ size_t src_buf_len,
+ size_t src_graph_len
+)
+{
+ size_t dst_padding;
+
+ if (src_buf == NULL || dst_buf == NULL || src_graph_len == 0 || src_graph_len > src_buf_len) {
+ return false;
+ }
+
+ dst_padding = user_cache_shared_graph_alignment_padding(dst_buf);
+
+ return dst_padding <= dst_buf_len &&
+ (src_graph_len <= dst_buf_len - dst_padding)
+ ;
+}
+
+bool php_user_cache_shared_graph_decode(
+ const uint8_t *buf,
+ size_t buf_len,
+ zval *dst
+)
+{
+ const php_user_cache_shared_graph_header *header;
+ const uint8_t *graph_buf;
+ php_user_cache_shared_graph_value root_value;
+ HashTable *saved_identity_map, *saved_reference_map, *saved_array_map;
+ bool result;
+
+ graph_buf = user_cache_shared_graph_locate(
+ buf,
+ buf_len,
+ &buf_len
+ );
+ if (graph_buf == NULL) {
+ return false;
+ }
+
+ buf = graph_buf;
+
+ header = (const php_user_cache_shared_graph_header *) buf;
+ if (!user_cache_shared_graph_load_root_value(header, buf_len, &root_value)) {
+ return false;
+ }
+
+ /* Restore hooks may re-enter fetch. */
+ saved_identity_map = UC_G(decode_identity_map);
+ UC_G(decode_identity_map) = NULL;
+ saved_reference_map = UC_G(decode_reference_map);
+ UC_G(decode_reference_map) = NULL;
+ saved_array_map = UC_G(decode_array_map);
+ UC_G(decode_array_map) = NULL;
+
+ result = user_cache_shared_graph_decode_value(buf, buf_len, &root_value, dst);
+
+ user_cache_decode_identity_map_teardown();
+ user_cache_decode_reference_map_teardown();
+ user_cache_decode_array_map_teardown();
+
+ UC_G(decode_identity_map) = saved_identity_map;
+ UC_G(decode_reference_map) = saved_reference_map;
+ UC_G(decode_array_map) = saved_array_map;
+
+ /* Drop address-keyed caches after releasing a failed payload. */
+ if (!result) {
+ php_user_cache_decode_resolve_cache_release();
+ php_user_cache_decode_shape_prototype_cache_release();
+ }
+
+ return result;
+}
+
+bool php_user_cache_shared_graph_can_overwrite_payload_locked(uint32_t payload_offset)
+{
+ php_user_cache_shared_graph_header *header =
+ user_cache_shared_graph_payload_header(payload_offset)
+ ;
+
+ if (header == NULL) {
+ return false;
+ }
+
+ if (!php_user_cache_quiesce_graph_payloads_locked()) {
+ return false;
+ }
+
+ return zend_atomic_int_load_ex(&header->ref_state) == 0;
+}
+
+bool php_user_cache_shared_graph_publish_copied_payload_locked(
+ uint8_t *dst_buf,
+ size_t dst_buf_len,
+ const uint8_t *src_buf,
+ size_t src_buf_len,
+ size_t src_graph_len,
+ bool has_verbatim_array,
+ const uint32_t *fixup_offsets,
+ uint32_t fixup_count)
+{
+ const uint8_t *src_base;
+ php_user_cache_shared_graph_header *header;
+ uint8_t *dst_base;
+ uintptr_t delta;
+ size_t src_padding, dst_padding;
+ uint32_t i;
+
+ if (!php_user_cache_shared_graph_copy_fits_buffer(
+ dst_buf,
+ dst_buf_len,
+ src_buf,
+ src_buf_len,
+ src_graph_len
+ )
+ ) {
+ return false;
+ }
+
+ src_padding = user_cache_shared_graph_alignment_padding(src_buf);
+ if (src_padding > src_buf_len ||
+ src_buf_len - src_padding < src_graph_len ||
+ src_graph_len < sizeof(*header)
+ ) {
+ return false;
+ }
+
+ src_base = src_buf + src_padding;
+ dst_padding = user_cache_shared_graph_alignment_padding(dst_buf);
+ if (dst_padding != 0) {
+ memset(dst_buf, 0, dst_padding);
+ }
+
+ dst_base = dst_buf + dst_padding;
+
+ if (src_base != dst_base) {
+ memcpy(dst_base, src_base, src_graph_len);
+ }
+
+ header = (php_user_cache_shared_graph_header *) dst_base;
+ ZEND_ATOMIC_INT_INIT(&header->ref_state, 0);
+
+ if (src_base == dst_base) {
+ return true;
+ }
+
+ /* Only non-empty verbatim arrays contain absolute pointers. */
+ if (!has_verbatim_array) {
+ ZEND_ASSERT(fixup_count == 0);
+
+ user_cache_shared_graph_check_rebase_complete(dst_base, src_graph_len, src_base);
+
+ return true;
+ }
+
+ /* Patch every recorded absolute pointer slot. */
+ ZEND_ASSERT(fixup_offsets != NULL && fixup_count > 0);
+
+ delta = (uintptr_t) dst_base - (uintptr_t) src_base;
+ for (i = 0; i < fixup_count; i++) {
+ ZEND_ASSERT(fixup_offsets[i] <= src_graph_len - sizeof(uintptr_t));
+ ZEND_ASSERT((((uintptr_t) dst_base + fixup_offsets[i]) & (sizeof(uintptr_t) - 1)) == 0);
+
+ *(uintptr_t *) (void *) (dst_base + fixup_offsets[i]) += delta;
+ }
+
+ user_cache_shared_graph_check_rebase_complete(dst_base, src_graph_len, src_base);
+
+ return true;
+}
+
+void php_user_cache_shared_graph_orphan_payload_locked(uint32_t payload_offset)
+{
+ php_user_cache_header *header = php_user_cache_header_ptr();
+ uint32_t i;
+
+ if (header == NULL || payload_offset == 0) {
+ return;
+ }
+
+ user_cache_shared_graph_force_retire_locked(payload_offset);
+
+ for (i = 0; i < PHP_USER_CACHE_ORPHANED_GRAPH_SLOTS; i++) {
+ if (header->orphaned_graphs[i] == payload_offset) {
+ return;
+ }
+
+ if (header->orphaned_graphs[i] == 0) {
+ header->orphaned_graphs[i] = payload_offset;
+
+ return;
+ }
+ }
+
+ header->orphaned_graphs_saturated = 1;
+}
+
+void php_user_cache_shared_graph_reclaim_orphaned_locked(void)
+{
+ php_user_cache_header *header = php_user_cache_header_ptr();
+ php_user_cache_shared_graph_header *graph_header;
+ uint32_t i, payload_offset;
+ bool checked_quiescent = false;
+
+ if (header == NULL) {
+ return;
+ }
+
+ for (i = 0; i < PHP_USER_CACHE_ORPHANED_GRAPH_SLOTS; i++) {
+ payload_offset = header->orphaned_graphs[i];
+ if (payload_offset == 0) {
+ continue;
+ }
+
+ if (!checked_quiescent) {
+ if (!php_user_cache_quiesce_graph_payloads_locked()) {
+ return;
+ }
+
+ checked_quiescent = true;
+ }
+
+ graph_header = user_cache_shared_graph_payload_header(payload_offset);
+
+ if (graph_header == NULL) {
+ header->orphaned_graphs[i] = 0;
+
+ continue;
+ }
+
+ if (zend_atomic_int_load_ex(&graph_header->ref_state) !=
+ PHP_USER_CACHE_SHARED_GRAPH_RETIRED
+ ) {
+ header->orphaned_graphs[i] = 0;
+
+ continue;
+ }
+
+ header->orphaned_graphs[i] = 0;
+
+ php_user_cache_free_locked(payload_offset);
+ }
+
+ if (header->orphaned_graphs_saturated != 0) {
+ if (!checked_quiescent) {
+ if (!php_user_cache_quiesce_graph_payloads_locked()) {
+ return;
+ }
+
+ checked_quiescent = true;
+ }
+
+ user_cache_shared_graph_reclaim_orphaned_by_scan_locked(header);
+
+ header->orphaned_graphs_saturated = 0;
+ }
+}
+
+bool php_user_cache_shared_graph_acquire_ref(uint32_t payload_offset)
+{
+ php_user_cache_shared_graph_header *header = 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 & PHP_USER_CACHE_SHARED_GRAPH_REFCOUNT_MASK;
+ expected = state;
+
+ if ((state & PHP_USER_CACHE_SHARED_GRAPH_RETIRED) != 0 ||
+ refcount == PHP_USER_CACHE_SHARED_GRAPH_REFCOUNT_MASK
+ ) {
+ return false;
+ }
+
+ if (zend_atomic_int_compare_exchange_ex(&header->ref_state, &expected, state + 1)) {
+ return true;
+ }
+ }
+}
+
+/* Abnormally terminated owners can leave retired payloads pinned. Reclaiming
+ * them safely requires per-owner reference records and a layout version bump. */
+bool php_user_cache_shared_graph_retire_payload_locked(uint32_t payload_offset)
+{
+ php_user_cache_shared_graph_header *header = 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 & PHP_USER_CACHE_SHARED_GRAPH_REFCOUNT_MASK;
+ expected = state;
+
+ if (refcount == 0) {
+ return true;
+ }
+
+ if ((state & PHP_USER_CACHE_SHARED_GRAPH_RETIRED) != 0) {
+ return false;
+ }
+
+ if (zend_atomic_int_compare_exchange_ex(
+ &header->ref_state,
+ &expected,
+ (state | PHP_USER_CACHE_SHARED_GRAPH_RETIRED)
+ )
+ ) {
+ return false;
+ }
+ }
+}
+
+bool php_user_cache_shared_graph_release_ref_locked(uint32_t payload_offset)
+{
+ php_user_cache_shared_graph_header *header = 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 & PHP_USER_CACHE_SHARED_GRAPH_REFCOUNT_MASK;
+ expected = state;
+
+ if (refcount == 0) {
+ return false;
+ }
+
+ desired = (state & PHP_USER_CACHE_SHARED_GRAPH_RETIRED) | (refcount - 1);
+ if (zend_atomic_int_compare_exchange_ex(&header->ref_state, &expected, desired)) {
+ return (desired & PHP_USER_CACHE_SHARED_GRAPH_RETIRED) != 0 &&
+ (desired & PHP_USER_CACHE_SHARED_GRAPH_REFCOUNT_MASK) == 0
+ ;
+ }
+ }
+}
+
+bool php_user_cache_has_request_shared_graph_ref(uint32_t payload_offset)
+{
+ php_user_cache_context *ctx;
+ zval *index_zv;
+ uint32_t i;
+
+ user_cache_shared_graph_refs_check_fork();
+
+ if (UC_G(shared_graph_ref_count) == 0) {
+ return false;
+ }
+
+ ctx = php_user_cache_active_context();
+
+ if (UC_G(shared_graph_ref_index) != NULL) {
+ index_zv = zend_hash_index_find(
+ UC_G(shared_graph_ref_index),
+ user_cache_shared_graph_ref_index_key(ctx, payload_offset)
+ );
+ if (index_zv == NULL) {
+ return false;
+ }
+
+ i = (uint32_t) Z_LVAL_P(index_zv);
+ if (i < UC_G(shared_graph_ref_count) &&
+ UC_G(shared_graph_refs)[i].context == ctx &&
+ UC_G(shared_graph_refs)[i].payload_offset == payload_offset
+ ) {
+ return true;
+ }
+ }
+
+ for (i = 0; i < UC_G(shared_graph_ref_count); i++) {
+ if (UC_G(shared_graph_refs)[i].context == ctx &&
+ UC_G(shared_graph_refs)[i].payload_offset == payload_offset
+ ) {
+ return true;
+ }
+ }
+
+ return false;
+}
+
+void php_user_cache_shared_graph_ref_reserve(void)
+{
+ user_cache_shared_graph_refs_check_fork();
+
+ if (UC_G(shared_graph_ref_count) == UC_G(shared_graph_ref_capacity)) {
+ user_cache_grow_shared_graph_refs(&UC_G(shared_graph_refs), &UC_G(shared_graph_ref_capacity), 8);
+ }
+
+ user_cache_ensure_shared_graph_ref_index();
+
+ zend_hash_extend(
+ UC_G(shared_graph_ref_index),
+ zend_hash_num_elements(UC_G(shared_graph_ref_index)) + 1,
+ 0
+ );
+}
+
+void php_user_cache_register_shared_graph_ref(uint32_t payload_offset)
+{
+ php_user_cache_context *ctx;
+ zval index_zv;
+
+ user_cache_shared_graph_refs_check_fork();
+
+ if (UC_G(shared_graph_ref_count) == UC_G(shared_graph_ref_capacity)) {
+ user_cache_grow_shared_graph_refs(&UC_G(shared_graph_refs), &UC_G(shared_graph_ref_capacity), 8);
+ }
+
+ ctx = php_user_cache_active_context();
+
+ UC_G(shared_graph_refs)[UC_G(shared_graph_ref_count)].context = ctx;
+ UC_G(shared_graph_refs)[UC_G(shared_graph_ref_count)].payload_offset = payload_offset;
+
+ user_cache_ensure_shared_graph_ref_index();
+
+ ZVAL_LONG(&index_zv, (zend_long) UC_G(shared_graph_ref_count));
+
+ zend_hash_index_add(
+ UC_G(shared_graph_ref_index),
+ user_cache_shared_graph_ref_index_key(ctx, payload_offset),
+ &index_zv
+ );
+
+ UC_G(shared_graph_ref_count)++;
+}
+
+bool php_user_cache_release_request_shared_graph_refs(void)
+{
+ php_user_cache_shared_graph_ref *ref;
+ php_user_cache_context *ctx, *prev_ctx;
+ uint32_t i, inner;
+ bool released = false;
+
+ user_cache_shared_graph_refs_check_fork();
+
+ if (UC_G(shared_graph_ref_count) == 0) {
+ user_cache_destroy_shared_graph_ref_index();
+
+ return false;
+ }
+
+ for (i = 0; i < UC_G(shared_graph_ref_count); i++) {
+ ctx = UC_G(shared_graph_refs)[i].context;
+
+ if (ctx == NULL) {
+ continue;
+ }
+
+ prev_ctx = php_user_cache_activate_context(ctx);
+
+ if (php_user_cache_wlock()) {
+ if (php_user_cache_header_is_initialized_locked()) {
+ for (inner = i; inner < UC_G(shared_graph_ref_count); inner++) {
+ ref = &UC_G(shared_graph_refs)[inner];
+
+ if (ref->context != ctx) {
+ continue;
+ }
+
+ if (ref->payload_offset == 0) {
+ ref->context = NULL;
+ continue;
+ }
+
+ released = true;
+ if (php_user_cache_shared_graph_release_ref_locked(ref->payload_offset)) {
+ if (php_user_cache_quiesce_graph_payloads_locked()) {
+ php_user_cache_free_locked(ref->payload_offset);
+ } else {
+ php_user_cache_shared_graph_orphan_payload_locked(ref->payload_offset);
+ }
+ }
+
+ ref->context = NULL;
+ }
+
+ php_user_cache_shared_graph_reclaim_orphaned_locked();
+ }
+
+ php_user_cache_unlock();
+ }
+
+ php_user_cache_restore_context(prev_ctx);
+ }
+
+ efree(UC_G(shared_graph_refs));
+
+ UC_G(shared_graph_refs) = NULL;
+ UC_G(shared_graph_ref_count) = 0;
+ UC_G(shared_graph_ref_capacity) = 0;
+
+ user_cache_destroy_shared_graph_ref_index();
+
+ return released;
+}
diff --git a/ext/user_cache/user_cache_shm.h b/ext/user_cache/user_cache_shm.h
new file mode 100644
index 000000000000..d87eceb54e70
--- /dev/null
+++ b/ext/user_cache/user_cache_shm.h
@@ -0,0 +1,112 @@
+/*
+ +----------------------------------------------------------------------+
+ | 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 PHP_USER_CACHE_SHM_H
+#define PHP_USER_CACHE_SHM_H
+
+#include "php.h"
+
+#if defined(__APPLE__) && defined(__MACH__) /* Darwin */
+# ifdef HAVE_SHM_MMAP_POSIX
+# define PHP_USER_CACHE_USE_SHM_OPEN 1
+# endif
+# ifdef HAVE_SHM_MMAP_ANON
+# define PHP_USER_CACHE_USE_MMAP 1
+# endif
+#elif defined(__linux__) || defined(_AIX)
+# ifdef HAVE_SHM_MMAP_POSIX
+# define PHP_USER_CACHE_USE_SHM_OPEN 1
+# endif
+# ifdef HAVE_SHM_IPC
+# define PHP_USER_CACHE_USE_SHM 1
+# endif
+# ifdef HAVE_SHM_MMAP_ANON
+# define PHP_USER_CACHE_USE_MMAP 1
+# endif
+#elif defined(__sparc) || defined(__sun)
+# ifdef HAVE_SHM_MMAP_POSIX
+# define PHP_USER_CACHE_USE_SHM_OPEN 1
+# endif
+# ifdef HAVE_SHM_IPC
+# define PHP_USER_CACHE_USE_SHM 1
+# endif
+# if defined(__i386)
+# ifdef HAVE_SHM_MMAP_ANON
+# define PHP_USER_CACHE_USE_MMAP 1
+# endif
+# endif
+#else
+# ifdef HAVE_SHM_MMAP_POSIX
+# define PHP_USER_CACHE_USE_SHM_OPEN 1
+# endif
+# ifdef HAVE_SHM_MMAP_ANON
+# define PHP_USER_CACHE_USE_MMAP 1
+# endif
+# ifdef HAVE_SHM_IPC
+# define PHP_USER_CACHE_USE_SHM 1
+# endif
+#endif
+
+#define PHP_USER_CACHE_ALLOC_FAILURE 0
+#define PHP_USER_CACHE_ALLOC_SUCCESS 1
+
+/* Part of the shared-memory layout; bump PHP_USER_CACHE_VERSION if changed. */
+typedef union {
+ void *ptr;
+ double dbl;
+ zend_long lng;
+} php_user_cache_align_test;
+
+#if defined(__STDC_VERSION__) && __STDC_VERSION__ >= 202311L
+# define PHP_USER_CACHE_PLATFORM_ALIGNMENT (alignof(php_user_cache_align_test) < 8 ? 8 : alignof(php_user_cache_align_test))
+#elif defined(__STDC_VERSION__) && __STDC_VERSION__ >= 201112L
+# define PHP_USER_CACHE_PLATFORM_ALIGNMENT (_Alignof(php_user_cache_align_test) < 8 ? 8 : _Alignof(php_user_cache_align_test))
+#elif ZEND_GCC_VERSION >= 2000 || defined(__clang__)
+# define PHP_USER_CACHE_PLATFORM_ALIGNMENT (__alignof__(php_user_cache_align_test) < 8 ? 8 : __alignof__(php_user_cache_align_test))
+#else
+# define PHP_USER_CACHE_PLATFORM_ALIGNMENT (sizeof(php_user_cache_align_test))
+#endif
+
+#define PHP_USER_CACHE_ALIGNED_SIZE(size) \
+ ZEND_MM_ALIGNED_SIZE_EX(size, PHP_USER_CACHE_PLATFORM_ALIGNMENT)
+
+typedef struct {
+ size_t size;
+ size_t end;
+ size_t pos;
+ void *p;
+} php_user_cache_shm_segment;
+
+typedef int (*php_user_cache_create_segments_t)(size_t requested_size, php_user_cache_shm_segment ***shared_segments, int *shared_segment_count, const char **error_in);
+typedef int (*php_user_cache_detach_segment_t)(php_user_cache_shm_segment *shared_segment);
+
+typedef struct {
+ php_user_cache_create_segments_t create_segments;
+ php_user_cache_detach_segment_t detach_segment;
+ size_t (*segment_type_size)(void);
+} php_user_cache_shm_handlers;
+
+typedef struct {
+ const char *name;
+ const php_user_cache_shm_handlers *handler;
+} php_user_cache_shm_handler_entry;
+
+#ifdef PHP_USER_CACHE_USE_SHM
+extern const php_user_cache_shm_handlers php_user_cache_alloc_shm_handlers;
+#endif
+#ifdef PHP_USER_CACHE_USE_SHM_OPEN
+extern const php_user_cache_shm_handlers php_user_cache_alloc_posix_handlers;
+#endif
+
+#endif /* PHP_USER_CACHE_SHM_H */
diff --git a/ext/user_cache/user_cache_storage.c b/ext/user_cache/user_cache_storage.c
new file mode 100644
index 000000000000..df37742c1361
--- /dev/null
+++ b/ext/user_cache/user_cache_storage.c
@@ -0,0 +1,3947 @@
+/*
+ +----------------------------------------------------------------------+
+ | 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 "user_cache_internal.h"
+#include "user_cache_storage_portability.h"
+
+#include "ext/hash/php_hash.h"
+#include "ext/hash/php_hash_sha.h"
+
+typedef enum {
+ PHP_USER_CACHE_ENTRY_LOCK_RELEASE_DROP,
+ PHP_USER_CACHE_ENTRY_LOCK_RELEASE_CLEAR,
+ PHP_USER_CACHE_ENTRY_LOCK_RELEASE_PRESERVE_LEASES
+} php_user_cache_entry_lock_release_mode;
+
+typedef struct {
+ char *key;
+ uint32_t key_len;
+ zend_ulong hash;
+ uint64_t owner_pid;
+ uint64_t owner_start_time;
+ uint64_t expires_at;
+} php_user_cache_recovered_entry_lock;
+
+typedef struct {
+ zend_string *key;
+ php_user_cache_entry_lock *lock;
+} php_user_cache_entry_lock_release_pair;
+
+static const uint32_t php_user_cache_capacity_primes[] = {
+ 127U,
+ 251U,
+ 509U,
+ 1021U,
+ 2039U,
+ 4093U,
+ 8191U,
+ 16381U,
+ 32749U,
+ 65521U,
+ 131071U,
+ 262139U,
+ 524287U,
+ 1048573U,
+ 2097143U,
+ 4194301U,
+ 8388593U,
+ 16777213U,
+};
+
+#ifdef PHP_USER_CACHE_HAVE_OPTIMISTIC
+static uint64_t php_user_cache_reader_incarnation = 0;
+#endif
+
+static php_user_cache_startup_lock php_user_cache_startup_storage_lock_state =
+ PHP_USER_CACHE_STARTUP_LOCK_INITIALIZER
+;
+
+static inline uint64_t user_cache_process_start_time_token(uint64_t pid);
+
+static zend_always_inline bool user_cache_startup_failure_forced(void)
+{
+ const char *value = getenv("PHP_USER_CACHE_FORCE_STARTUP_FAILURE");
+
+ return value != NULL && value[0] != '\0' && value[0] != '0';
+}
+
+static zend_always_inline bool 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 user_cache_is_opted_in(void)
+{
+ return php_user_cache_runtime_opted_in;
+}
+
+static zend_always_inline bool user_cache_cache_is_disabled_for_sapi(void)
+{
+ if (!UC_G(enable)) {
+ return true;
+ }
+
+ return !UC_G(enable_cli) &&
+ sapi_module.name != NULL &&
+ (strcmp(sapi_module.name, "cli") == 0 || strcmp(sapi_module.name, "phpdbg") == 0)
+ ;
+}
+
+static zend_always_inline void user_cache_set_unavailable(php_user_cache_reason reason)
+{
+ php_user_cache_runtime *runtime = php_user_cache_active_runtime();
+
+ runtime->available = false;
+ runtime->failure_reason = reason;
+}
+
+static zend_always_inline void user_cache_set_available(void)
+{
+ php_user_cache_runtime *runtime = php_user_cache_active_runtime();
+
+ runtime->available = true;
+ runtime->failure_reason = PHP_USER_CACHE_REASON_NONE;
+}
+
+static zend_always_inline HashTable **user_cache_entry_lock_table_ptr(void)
+{
+ return &UC_G(entry_lock_table);
+}
+
+static zend_always_inline uint32_t user_cache_entry_lock_table_index(zend_ulong hash)
+{
+ return (uint32_t) (hash % PHP_USER_CACHE_ENTRY_LOCK_TABLE_SIZE);
+}
+
+static zend_always_inline uint32_t user_cache_used_end_offset_locked(const php_user_cache_header *header)
+{
+ return header->data_offset + header->next_free;
+}
+
+static zend_always_inline void user_cache_block_mark_free(php_user_cache_block *block)
+{
+ block->flags |= PHP_USER_CACHE_BLOCK_FREE;
+}
+
+static zend_always_inline void user_cache_block_mark_used(php_user_cache_block *block)
+{
+ block->flags &= ~PHP_USER_CACHE_BLOCK_FREE;
+ block->next_free = 0;
+ block->prev_free = 0;
+}
+
+static zend_always_inline bool user_cache_entry_lock_record_key_matches(
+ const php_user_cache_entry_lock_record *record,
+ zend_string *key,
+ zend_ulong hash)
+{
+ return record->state == PHP_USER_CACHE_ENTRY_LOCK_USED &&
+ record->hash == hash &&
+ record->key_len == ZSTR_LEN(key) &&
+ memcmp(php_user_cache_ptr(record->key_offset), ZSTR_VAL(key), ZSTR_LEN(key)) == 0
+ ;
+}
+
+static zend_always_inline php_user_cache_entry_lock *user_cache_find_local_entry_lock(
+ const php_user_cache_context *ctx,
+ zend_string *key)
+{
+ HashTable **locks_ptr = user_cache_entry_lock_table_ptr();
+ php_user_cache_entry_lock *lock;
+
+ if (*locks_ptr == NULL) {
+ return NULL;
+ }
+
+ lock = zend_hash_find_ptr(*locks_ptr, key);
+ if (lock == NULL || lock->context != ctx) {
+ return NULL;
+ }
+
+ return lock;
+}
+
+/* Do not cache failures or values inherited across fork. */
+static zend_always_inline uint64_t user_cache_cached_self_start_time_token(uint64_t self_pid)
+{
+ if (UC_G(self_start_time_pid) != self_pid || UC_G(self_start_time_token) == 0) {
+ UC_G(self_start_time_pid) = self_pid;
+ UC_G(self_start_time_token) = user_cache_process_start_time_token(self_pid);
+ }
+
+ return UC_G(self_start_time_token);
+}
+
+#if defined(ZTS) && defined(PHP_USER_CACHE_HAVE_OPTIMISTIC)
+static zend_always_inline bool user_cache_storage_holds_header(
+ const php_user_cache_storage *storage,
+ const php_user_cache_header *header)
+{
+ return storage->initialized &&
+ storage->segment_count == 1 &&
+ storage->segments[0]->p == (const void *) header
+ ;
+}
+#endif
+
+#ifdef PHP_USER_CACHE_HAVE_OPTIMISTIC
+static zend_always_inline void user_cache_atomic_store_64(uint64_t *target, uint64_t value)
+{
+ PHP_USER_CACHE_ATOMIC_STORE_64(target, value);
+}
+
+static zend_always_inline bool user_cache_atomic_cas_64(uint64_t *target, uint64_t expected, uint64_t desired)
+{
+ return PHP_USER_CACHE_ATOMIC_CAS_64(target, expected, desired);
+}
+
+static zend_always_inline uint32_t user_cache_atomic_load_32(const uint32_t *target)
+{
+ return PHP_USER_CACHE_ATOMIC_LOAD_32(target);
+}
+
+static zend_always_inline void user_cache_atomic_store_32(uint32_t *target, uint32_t value)
+{
+ PHP_USER_CACHE_ATOMIC_STORE_32(target, value);
+}
+
+static zend_always_inline bool user_cache_atomic_cas_32(uint32_t *target, uint32_t expected, uint32_t desired)
+{
+ return PHP_USER_CACHE_ATOMIC_CAS_32(target, expected, desired);
+}
+
+static zend_always_inline void user_cache_atomic_fence_seq_cst(void)
+{
+ PHP_USER_CACHE_ATOMIC_FENCE_SEQ_CST();
+}
+#endif
+
+static zend_always_inline void user_cache_seq_announce(uint64_t *seq, uint64_t value)
+{
+#ifdef PHP_USER_CACHE_HAVE_OPTIMISTIC
+ user_cache_atomic_store_64(seq, value);
+ user_cache_atomic_fence_seq_cst();
+#else
+ *seq = value;
+#endif
+}
+
+static zend_always_inline void user_cache_seq_publish(uint64_t *seq, uint64_t value)
+{
+#ifdef PHP_USER_CACHE_HAVE_OPTIMISTIC
+ user_cache_atomic_store_64(seq, value);
+#else
+ *seq = value;
+#endif
+}
+
+#ifdef PHP_USER_CACHE_HAVE_SHARED_MUTEX
+static zend_always_inline bool user_cache_header_is_initialized_acquire(void)
+{
+ php_user_cache_header *header = php_user_cache_header_ptr();
+
+ if (header == NULL) {
+ return false;
+ }
+
+#ifdef PHP_USER_CACHE_HAVE_OPTIMISTIC
+ if (user_cache_atomic_load_32(&header->magic) != PHP_USER_CACHE_MAGIC) {
+ return false;
+ }
+
+ php_user_cache_atomic_fence_acquire();
+
+ return user_cache_atomic_load_32(&header->version) == PHP_USER_CACHE_VERSION;
+#else
+ return php_user_cache_header_is_initialized_locked();
+#endif
+}
+#endif
+
+static inline bool user_cache_zts_lock_init(php_user_cache_storage *storage)
+{
+#ifdef ZTS
+ storage->zts_lock = tsrm_mutex_alloc();
+
+ return storage->zts_lock != NULL;
+#else
+ (void) storage;
+
+ return true;
+#endif
+}
+
+static inline void user_cache_zts_lock_destroy(php_user_cache_storage *storage)
+{
+#ifdef ZTS
+ tsrm_mutex_free(storage->zts_lock);
+ storage->zts_lock = NULL;
+#else
+ (void) storage;
+#endif
+}
+
+static inline bool user_cache_zts_lock(php_user_cache_storage *storage)
+{
+#ifdef ZTS
+ return tsrm_mutex_lock(storage->zts_lock) == 0;
+#else
+ (void) storage;
+
+ return true;
+#endif
+}
+
+static inline void user_cache_zts_unlock(php_user_cache_storage *storage)
+{
+#ifdef ZTS
+ tsrm_mutex_unlock(storage->zts_lock);
+#else
+ (void) storage;
+#endif
+}
+
+static inline void user_cache_startup_lock_acquire(php_user_cache_startup_lock *lock)
+{
+#ifdef ZTS
+# ifdef ZEND_WIN32
+ AcquireSRWLockExclusive(lock);
+# else
+ pthread_mutex_lock(lock);
+# endif
+#else
+ (void) lock;
+#endif
+}
+
+static inline void user_cache_startup_lock_release(php_user_cache_startup_lock *lock)
+{
+#ifdef ZTS
+# ifdef ZEND_WIN32
+ ReleaseSRWLockExclusive(lock);
+# else
+ pthread_mutex_unlock(lock);
+# endif
+#else
+ (void) lock;
+#endif
+}
+
+static inline uint32_t user_cache_sleep_entry_lock_retry_interval(void)
+{
+#ifdef ZEND_WIN32
+ Sleep(PHP_USER_CACHE_ENTRY_LOCK_RETRY_INTERVAL_US / 1000);
+#elif defined(HAVE_UNISTD_H)
+ usleep(PHP_USER_CACHE_ENTRY_LOCK_RETRY_INTERVAL_US);
+#endif
+
+ return PHP_USER_CACHE_ENTRY_LOCK_RETRY_INTERVAL_US;
+}
+
+static inline uint64_t user_cache_process_start_time_token(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__)
+ const char *p;
+ ssize_t stat_len;
+ int fd, field;
+ unsigned long long start_time = 0;
+ char path[64], stat_buf[1024];
+
+ 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_buf, sizeof(stat_buf) - 1);
+ close(fd);
+ if (stat_len <= 0) {
+ return 0;
+ }
+
+ stat_buf[stat_len] = '\0';
+
+ /* Field 22 follows the closing parenthesis of comm. */
+ p = strrchr(stat_buf, ')');
+ if (p == NULL) {
+ return 0;
+ }
+
+ p++;
+ for (field = 0; field < 19 && *p != '\0'; field++) {
+ while (*p == ' ') {
+ p++;
+ }
+ while (*p != '\0' && *p != ' ') {
+ p++;
+ }
+ }
+
+ if (sscanf(p, " %llu", &start_time) != 1) {
+ return 0;
+ }
+
+ return (uint64_t) start_time;
+#elif defined(__APPLE__) || defined(__FreeBSD__)
+ struct kinfo_proc info;
+ size_t size = sizeof(info);
+ int mib[4] = { CTL_KERN, KERN_PROC, KERN_PROC_PID, (int) pid };
+
+ if (sysctl(mib, 4, &info, &size, NULL, 0) != 0 || size < sizeof(info)) {
+ return 0;
+ }
+
+ /* Only stable, non-zero equality is required. */
+# ifdef __APPLE__
+ return (((uint64_t) info.kp_proc.p_starttime.tv_sec << 20)
+ | (uint64_t) info.kp_proc.p_starttime.tv_usec) + 1
+ ;
+# else
+ return (((uint64_t) info.ki_start.tv_sec << 20)
+ | (uint64_t) info.ki_start.tv_usec) + 1
+ ;
+# endif
+#else
+ (void) pid;
+
+ return 0;
+#endif
+}
+
+static inline bool user_cache_process_has_exited(uint64_t pid)
+{
+#ifdef ZEND_WIN32
+ HANDLE process = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, FALSE, (DWORD) 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);
+
+ return false;
+#else
+ return kill((pid_t) pid, 0) == -1 && errno == ESRCH;
+#endif
+}
+
+static inline bool user_cache_startup_storage_is_complete(void)
+{
+#if defined(ZTS) && defined(PHP_USER_CACHE_HAVE_OPTIMISTIC)
+ return user_cache_atomic_load_32(&php_user_cache_active_context()->storage.startup_complete) != 0;
+#else
+ return false;
+#endif
+}
+
+static inline void user_cache_startup_storage_lock(void)
+{
+ user_cache_startup_lock_acquire(&php_user_cache_startup_storage_lock_state);
+}
+
+static inline void user_cache_startup_storage_unlock(void)
+{
+ user_cache_startup_lock_release(&php_user_cache_startup_storage_lock_state);
+}
+
+#ifdef ZEND_WIN32
+static inline void user_cache_win32_set_segment(
+ php_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 = mapping_base;
+ segment->segment.pos = 0;
+ segment->segment.size = requested_size;
+}
+#endif
+
+#if defined(PHP_USER_CACHE_HAVE_ANON_MMAP) || defined(PHP_USER_CACHE_HAVE_BOUNDARY_SHM)
+static int user_cache_wrap_mapped_segment(
+ void *mapping,
+ size_t requested_size,
+ php_user_cache_shm_segment ***shared_segments_p,
+ int *shared_segments_count,
+ const char **error_in
+)
+{
+ php_user_cache_shm_segment *segment;
+
+ *shared_segments_count = 1;
+ *shared_segments_p = (php_user_cache_shm_segment **) calloc(1, sizeof(php_user_cache_shm_segment *) + sizeof(php_user_cache_shm_segment));
+ if (*shared_segments_p == NULL) {
+ munmap(mapping, requested_size);
+ *error_in = "calloc";
+
+ return PHP_USER_CACHE_ALLOC_FAILURE;
+ }
+
+ segment = (php_user_cache_shm_segment *) ((char *) *shared_segments_p + sizeof(php_user_cache_shm_segment *));
+ (*shared_segments_p)[0] = segment;
+
+ segment->p = mapping;
+ segment->pos = 0;
+ segment->size = requested_size;
+ segment->end = requested_size;
+
+ return PHP_USER_CACHE_ALLOC_SUCCESS;
+}
+
+static int user_cache_munmap_detach_segment(php_user_cache_shm_segment *shared_segment)
+{
+ munmap(shared_segment->p, shared_segment->size);
+
+ return 0;
+}
+
+static size_t user_cache_mmap_segment_type_size(void)
+{
+ return sizeof(php_user_cache_shm_segment);
+}
+#endif
+
+#ifdef PHP_USER_CACHE_HAVE_ANON_MMAP
+static int user_cache_mmap_create_segments(
+ size_t requested_size,
+ php_user_cache_shm_segment ***shared_segments_p,
+ int *shared_segments_count,
+ const char **error_in
+)
+{
+ 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 PHP_USER_CACHE_ALLOC_FAILURE;
+ }
+
+ return user_cache_wrap_mapped_segment(mapping, requested_size, shared_segments_p, shared_segments_count, error_in);
+}
+#endif
+
+#ifdef PHP_USER_CACHE_HAVE_BOUNDARY_SHM
+static void user_cache_shared_boundary_digest(
+ const php_user_cache_context *ctx,
+ size_t requested_size,
+ uint8_t digest[32]
+)
+{
+ const char *identity;
+ PHP_SHA256_CTX sha_ctx;
+ size_t identity_len, update_len = 0;
+ int prefix_len;
+ char prefix[64];
+
+ identity = ctx->boundary_identity != NULL
+ ? ctx->boundary_identity
+ : (ctx->lock_name != NULL ? ctx->lock_name : "")
+ ;
+ identity_len = ctx->boundary_identity != NULL
+ ? ctx->boundary_identity_len
+ : strlen(identity)
+ ;
+ prefix_len = snprintf(
+ prefix,
+ sizeof(prefix),
+ "PhpUserCache.boundary-shm|%u|%zu|%zu|",
+ PHP_USER_CACHE_VERSION,
+ requested_size,
+ identity_len
+ );
+
+ PHP_SHA256Init(&sha_ctx);
+ if (prefix_len > 0) {
+ update_len = (size_t) prefix_len;
+ if (update_len >= sizeof(prefix)) {
+ update_len = sizeof(prefix) - 1;
+ }
+
+ PHP_SHA256Update(&sha_ctx, (const uint8_t *) prefix, update_len);
+ }
+
+ PHP_SHA256Update(&sha_ctx, (const uint8_t *) identity, identity_len);
+ PHP_SHA256Final(digest, &sha_ctx);
+}
+
+static void user_cache_shared_boundary_digest_to_identity(
+ const uint8_t digest[32],
+ uint64_t *identity_high,
+ uint32_t *identity_low
+)
+{
+ *identity_high =
+ ((uint64_t) digest[0] << 56) |
+ ((uint64_t) digest[1] << 48) |
+ ((uint64_t) digest[2] << 40) |
+ ((uint64_t) digest[3] << 32) |
+ ((uint64_t) digest[4] << 24) |
+ ((uint64_t) digest[5] << 16) |
+ ((uint64_t) digest[6] << 8) |
+ (uint64_t) digest[7]
+ ;
+ *identity_low =
+ ((uint32_t) digest[8] << 24) |
+ ((uint32_t) digest[9] << 16) |
+ ((uint32_t) digest[10] << 8) |
+ (uint32_t) digest[11]
+ ;
+}
+
+static void user_cache_shared_boundary_identity(
+ const php_user_cache_context *ctx,
+ size_t requested_size,
+ uint64_t *identity_high,
+ uint32_t *identity_low
+)
+{
+ uint8_t digest[32];
+
+ user_cache_shared_boundary_digest(ctx, requested_size, digest);
+ user_cache_shared_boundary_digest_to_identity(digest, identity_high, identity_low);
+}
+
+static void user_cache_shared_boundary_shm_name(char *buf, size_t buf_size, size_t requested_size)
+{
+ uint64_t identity_high;
+ uint32_t identity_low;
+
+ user_cache_shared_boundary_identity(
+ php_user_cache_active_context(),
+ requested_size,
+ &identity_high,
+ &identity_low
+ );
+
+ snprintf(buf, buf_size, "/ZUC.%016" PRIx64 "%08" PRIx32, identity_high, identity_low);
+}
+
+static void user_cache_shared_boundary_lock_name(char *buf, size_t buf_size, size_t requested_size)
+{
+ uint64_t identity_high;
+ uint32_t identity_low;
+
+ user_cache_shared_boundary_identity(
+ php_user_cache_active_context(),
+ requested_size,
+ &identity_high,
+ &identity_low
+ );
+
+ snprintf(
+ buf,
+ buf_size,
+ "%s/.ZendUserCacheBnd.%016" PRIx64 "%08" PRIx32 ".lock",
+ UC_G(lockfile_path),
+ identity_high,
+ identity_low
+ );
+}
+
+static bool user_cache_shared_boundary_fd_is_trusted(int fd, struct stat *st)
+{
+ if (fstat(fd, st) != 0) {
+ return false;
+ }
+
+ return st->st_uid == geteuid() && (st->st_mode & 0077) == 0;
+}
+
+/* Recreate a zero-length shm object left by a failed creator. */
+static int user_cache_shared_boundary_open_shm_fd(
+ const char *shm_name,
+ size_t requested_size,
+ const char **error_in
+)
+{
+ struct stat st;
+ uint32_t attempts;
+ int fd, create_attempts;
+
+ for (create_attempts = 0; create_attempts < 2; create_attempts++) {
+ fd = shm_open(shm_name, O_RDWR | O_CREAT | O_EXCL, 0600);
+ if (fd >= 0) {
+ if (ftruncate(fd, (off_t) requested_size) != 0) {
+ close(fd);
+ shm_unlink(shm_name);
+ *error_in = "ftruncate";
+
+ return -1;
+ }
+
+ return fd;
+ }
+
+ if (errno != EEXIST) {
+ *error_in = "shm_open";
+
+ return -1;
+ }
+
+ fd = shm_open(shm_name, O_RDWR, 0600);
+ if (fd < 0) {
+ *error_in = "shm_open";
+
+ return -1;
+ }
+
+ if (!user_cache_shared_boundary_fd_is_trusted(fd, &st)) {
+ close(fd);
+ *error_in = "shm ownership";
+
+ return -1;
+ }
+
+ for (attempts = 0; (size_t) st.st_size != requested_size && attempts < 1000; attempts++) {
+ if (st.st_size != 0) {
+ break;
+ }
+
+ usleep(1000);
+
+ if (fstat(fd, &st) != 0) {
+ break;
+ }
+ }
+
+ if ((size_t) st.st_size == requested_size) {
+ return fd;
+ }
+
+ close(fd);
+
+ if (st.st_size == 0 && create_attempts == 0) {
+ shm_unlink(shm_name);
+
+ continue;
+ }
+
+ *error_in = "shm size";
+
+ return -1;
+ }
+
+ *error_in = "shm size";
+
+ return -1;
+}
+
+static int user_cache_shared_boundary_create_segments(
+ size_t requested_size,
+ php_user_cache_shm_segment ***shared_segments_p,
+ int *shared_segments_count,
+ const char **error_in
+)
+{
+ int fd;
+ char shm_name[64];
+ void *mapping;
+
+ if (requested_size > (size_t) SSIZE_MAX) {
+ *error_in = "size overflow";
+
+ return PHP_USER_CACHE_ALLOC_FAILURE;
+ }
+
+ user_cache_shared_boundary_shm_name(shm_name, sizeof(shm_name), requested_size);
+
+ fd = user_cache_shared_boundary_open_shm_fd(shm_name, requested_size, error_in);
+ if (fd < 0) {
+ return PHP_USER_CACHE_ALLOC_FAILURE;
+ }
+
+ mapping = mmap(NULL, requested_size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
+ close(fd);
+ if (mapping == MAP_FAILED) {
+ *error_in = "mmap";
+
+ return PHP_USER_CACHE_ALLOC_FAILURE;
+ }
+
+ return user_cache_wrap_mapped_segment(mapping, requested_size, shared_segments_p, shared_segments_count, error_in);
+}
+
+static const php_user_cache_shm_handler_entry *user_cache_shared_boundary_handler_entry(void)
+{
+ static const php_user_cache_shm_handlers handlers = {
+ user_cache_shared_boundary_create_segments,
+ user_cache_munmap_detach_segment,
+ user_cache_mmap_segment_type_size
+ };
+ static const php_user_cache_shm_handler_entry entry = {
+ "boundary-shm", &handlers
+ };
+
+ return &entry;
+}
+#endif
+
+#ifdef ZEND_WIN32
+static void user_cache_win32_format_name(char *buf, size_t buf_size, const char *name, size_t unique_id)
+{
+ const char *sapi_name = sapi_module.name != NULL ? sapi_module.name : "";
+ php_user_cache_context *ctx = php_user_cache_active_context();
+
+ snprintf(
+ buf,
+ buf_size,
+ "%s@%.32s@%.20s@%.32s@%s@%zx",
+ name,
+ zend_system_id,
+ sapi_name,
+ zend_system_id,
+ ctx->lock_name,
+ unique_id
+ );
+}
+
+static int user_cache_win32_reattach_segment(
+ php_user_cache_win32_segment *segment,
+ HANDLE memfile,
+ size_t requested_size,
+ const char **error_in
+)
+{
+ MEMORY_BASIC_INFORMATION info;
+ void *mapping_base;
+
+ mapping_base = MapViewOfFileEx(memfile, FILE_MAP_ALL_ACCESS, 0, 0, 0, NULL);
+ if (mapping_base == NULL) {
+ *error_in = "MapViewOfFileEx";
+
+ return PHP_USER_CACHE_ALLOC_FAILURE;
+ }
+
+ /* The mapped object must cover the requested range. */
+ if (VirtualQuery(mapping_base, &info, sizeof(info)) == 0 ||
+ info.RegionSize < requested_size
+ ) {
+ UnmapViewOfFile(mapping_base);
+ *error_in = "VirtualQuery";
+
+ return PHP_USER_CACHE_ALLOC_FAILURE;
+ }
+
+ user_cache_win32_set_segment(segment, memfile, mapping_base, requested_size, requested_size);
+
+ return PHP_USER_CACHE_ALLOC_SUCCESS;
+}
+
+static int user_cache_win32_create_segment(
+ php_user_cache_win32_segment *segment,
+ const char *mapping_name,
+ size_t requested_size,
+ const char **error_in
+)
+{
+ HANDLE memfile;
+ DWORD size_high, size_low;
+ int result;
+ void *mapping_base;
+
+#if defined(_WIN64)
+ size_high = (DWORD) (requested_size >> 32);
+ size_low = (DWORD) (requested_size & 0xffffffff);
+#else
+ if (requested_size > UINT32_MAX) {
+ *error_in = "size overflow";
+ return PHP_USER_CACHE_ALLOC_FAILURE;
+ }
+
+ size_high = 0;
+ size_low = (DWORD) requested_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 PHP_USER_CACHE_ALLOC_FAILURE;
+ }
+
+ if (GetLastError() == ERROR_ALREADY_EXISTS) {
+ result = user_cache_win32_reattach_segment(segment, memfile, requested_size, error_in);
+ if (result != PHP_USER_CACHE_ALLOC_SUCCESS) {
+ CloseHandle(memfile);
+ }
+
+ return result;
+ }
+
+ mapping_base = MapViewOfFileEx(memfile, FILE_MAP_ALL_ACCESS, 0, 0, 0, NULL);
+ if (mapping_base == NULL) {
+ CloseHandle(memfile);
+ *error_in = "MapViewOfFileEx";
+
+ return PHP_USER_CACHE_ALLOC_FAILURE;
+ }
+
+ user_cache_win32_set_segment(segment, memfile, mapping_base, requested_size, requested_size);
+
+ return PHP_USER_CACHE_ALLOC_SUCCESS;
+}
+
+static int user_cache_win32_create_segments(
+ size_t requested_size,
+ php_user_cache_shm_segment ***shared_segments_p,
+ int *shared_segments_count,
+ const char **error_in
+)
+{
+ php_user_cache_win32_segment *segment;
+ HANDLE mutex = NULL, memfile = NULL;
+ DWORD wait_result;
+ bool mutex_acquired = false;
+ int result = PHP_USER_CACHE_ALLOC_FAILURE;
+ char mapping_name[MAXPATHLEN], mutex_name[MAXPATHLEN];
+
+ *shared_segments_count = 1;
+ *shared_segments_p = (php_user_cache_shm_segment **) calloc(
+ 1,
+ sizeof(php_user_cache_shm_segment *) + sizeof(php_user_cache_win32_segment)
+ );
+ if (*shared_segments_p == NULL) {
+ *error_in = "calloc";
+
+ return PHP_USER_CACHE_ALLOC_FAILURE;
+ }
+
+ segment = (php_user_cache_win32_segment *) ((char *) *shared_segments_p + sizeof(php_user_cache_shm_segment *));
+ (*shared_segments_p)[0] = (php_user_cache_shm_segment *) segment;
+
+ user_cache_win32_format_name(
+ mapping_name,
+ sizeof(mapping_name),
+ PHP_USER_CACHE_WIN32_MAPPING_NAME,
+ requested_size
+ );
+ user_cache_win32_format_name(
+ mutex_name,
+ sizeof(mutex_name),
+ PHP_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 = user_cache_win32_reattach_segment(segment, memfile, requested_size, error_in);
+ if (result != PHP_USER_CACHE_ALLOC_SUCCESS) {
+ CloseHandle(memfile);
+ }
+ } else {
+ result = 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 == PHP_USER_CACHE_ALLOC_SUCCESS) {
+ return PHP_USER_CACHE_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 PHP_USER_CACHE_ALLOC_FAILURE;
+}
+
+static int user_cache_win32_detach_segment(php_user_cache_shm_segment *shared_segment)
+{
+ php_user_cache_win32_segment *segment = (php_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 user_cache_win32_segment_type_size(void)
+{
+ return sizeof(php_user_cache_win32_segment);
+}
+#endif
+
+static const php_user_cache_shm_handler_entry *user_cache_handler_table(void)
+{
+#ifdef PHP_USER_CACHE_HAVE_ANON_MMAP
+ static const php_user_cache_shm_handlers mmap_handlers = {
+ user_cache_mmap_create_segments,
+ user_cache_munmap_detach_segment,
+ user_cache_mmap_segment_type_size
+ };
+#endif
+#ifdef ZEND_WIN32
+ static const php_user_cache_shm_handlers win32_handlers = {
+ user_cache_win32_create_segments,
+ user_cache_win32_detach_segment,
+ user_cache_win32_segment_type_size
+ };
+#endif
+ static const php_user_cache_shm_handler_entry handlers[] = {
+#ifdef PHP_USER_CACHE_HAVE_ANON_MMAP
+ { "mmap", &mmap_handlers },
+#endif
+#ifdef PHP_USER_CACHE_USE_SHM
+ { "shm", &php_user_cache_alloc_shm_handlers },
+#endif
+#ifdef PHP_USER_CACHE_USE_SHM_OPEN
+ { "posix", &php_user_cache_alloc_posix_handlers },
+#endif
+#ifdef ZEND_WIN32
+ { "win32", &win32_handlers },
+#endif
+ { NULL, NULL }
+ };
+
+ return handlers;
+}
+
+static void user_cache_cleanup_segments(const php_user_cache_shm_handlers *handler, php_user_cache_shm_segment **segments, int segment_count)
+{
+ int i;
+
+ if (!handler || !segments) {
+ return;
+ }
+
+ for (i = 0; i < segment_count; i++) {
+ if (segments[i]->p && segments[i]->p != (void *) -1) {
+ handler->detach_segment(segments[i]);
+ }
+ }
+
+ free(segments);
+}
+
+static bool user_cache_entry_lock_owner_is_dead_uncached(uint64_t owner_pid, uint64_t owner_start_time)
+{
+ uint64_t current_start_time;
+
+ if (user_cache_process_has_exited(owner_pid)) {
+ return true;
+ }
+
+ if (owner_start_time != 0) {
+ current_start_time = user_cache_process_start_time_token(owner_pid);
+ if (current_start_time != 0 && current_start_time != owner_start_time) {
+ return true;
+ }
+ }
+
+ return false;
+}
+
+static bool user_cache_entry_lock_owner_is_dead(uint64_t owner_pid, uint64_t owner_start_time)
+{
+ uint64_t now = (uint64_t) time(NULL);
+
+ if (owner_pid == UC_G(entry_lock_owner_probe_pid) &&
+ owner_start_time == UC_G(entry_lock_owner_probe_start_time) &&
+ now == UC_G(entry_lock_owner_probe_at)
+ ) {
+ return UC_G(entry_lock_owner_probe_dead);
+ }
+
+ UC_G(entry_lock_owner_probe_dead) = user_cache_entry_lock_owner_is_dead_uncached(owner_pid, owner_start_time);
+ UC_G(entry_lock_owner_probe_pid) = owner_pid;
+ UC_G(entry_lock_owner_probe_start_time) = owner_start_time;
+ UC_G(entry_lock_owner_probe_at) = now;
+
+ return UC_G(entry_lock_owner_probe_dead);
+}
+
+static bool user_cache_recovery_lock_key_is_readable(
+ const php_user_cache_header *header,
+ const php_user_cache_entry_lock_record *record)
+{
+ uint32_t data_end;
+
+ if (record->key_offset == 0 || record->key_len == 0 || record->key_offset < header->data_offset) {
+ return false;
+ }
+
+ if (header->data_size > UINT32_MAX - header->data_offset) {
+ return false;
+ }
+
+ data_end = header->data_offset + header->data_size;
+ if (record->key_offset > data_end) {
+ return false;
+ }
+
+ return record->key_len <= data_end - record->key_offset;
+}
+
+/* Dead owners may remain live until their lease expires. */
+static bool user_cache_entry_lock_record_is_active_locked(
+ php_user_cache_entry_lock_record *record,
+ uint64_t now,
+ bool demote_dead_owner)
+{
+ if (record->state != PHP_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 (user_cache_entry_lock_owner_is_dead(record->owner_pid, record->owner_start_time)) {
+ if (!demote_dead_owner) {
+ return false;
+ }
+
+ record->owner_pid = 0;
+ record->owner_start_time = 0;
+
+ return record->expires_at != 0;
+ }
+
+ return true;
+}
+
+static php_user_cache_recovered_entry_lock *user_cache_collect_recovery_entry_locks(
+ php_user_cache_header *header,
+ uint32_t *count_ptr)
+{
+ php_user_cache_recovered_entry_lock *locks;
+ php_user_cache_entry_lock_record *record;
+ uint64_t now = (uint64_t) time(NULL);
+ uint32_t i, count = 0;
+ char *key;
+
+ for (i = 0; i < PHP_USER_CACHE_ENTRY_LOCK_TABLE_SIZE; i++) {
+ record = &header->entry_lock_records[i];
+ if (user_cache_entry_lock_record_is_active_locked(record, now, false) &&
+ user_cache_recovery_lock_key_is_readable(header, record)
+ ) {
+ count++;
+ }
+ }
+
+ *count_ptr = 0;
+ if (count == 0) {
+ return NULL;
+ }
+
+ locks = (php_user_cache_recovered_entry_lock *) calloc(count, sizeof(*locks));
+ if (locks == NULL) {
+ return NULL;
+ }
+
+ for (i = 0; i < PHP_USER_CACHE_ENTRY_LOCK_TABLE_SIZE; i++) {
+ record = &header->entry_lock_records[i];
+ if (!user_cache_entry_lock_record_is_active_locked(record, now, false) ||
+ !user_cache_recovery_lock_key_is_readable(header, record)
+ ) {
+ continue;
+ }
+
+ key = (char *) malloc(record->key_len);
+ if (key == NULL) {
+ continue;
+ }
+
+ memcpy(key, php_user_cache_ptr(record->key_offset), record->key_len);
+
+ locks[*count_ptr].key = key;
+ locks[*count_ptr].key_len = record->key_len;
+ locks[*count_ptr].hash = record->hash;
+ locks[*count_ptr].owner_pid = record->owner_pid;
+ locks[*count_ptr].owner_start_time = record->owner_start_time;
+ locks[*count_ptr].expires_at = record->expires_at;
+
+ (*count_ptr)++;
+ }
+
+ return locks;
+}
+
+static void user_cache_free_recovery_entry_locks(
+ php_user_cache_recovered_entry_lock *locks,
+ uint32_t count)
+{
+ uint32_t i;
+
+ if (locks == NULL) {
+ return;
+ }
+
+ for (i = 0; i < count; i++) {
+ free(locks[i].key);
+ }
+
+ free(locks);
+}
+
+static void user_cache_restore_recovery_entry_locks(
+ php_user_cache_header *header,
+ php_user_cache_recovered_entry_lock *locks,
+ uint32_t count)
+{
+ php_user_cache_entry_lock_record *record;
+ uint32_t i, probe, slot_idx, key_offset;
+
+ for (i = 0; i < count; i++) {
+ if (locks[i].key == NULL) {
+ continue;
+ }
+
+ for (probe = 0; probe < PHP_USER_CACHE_ENTRY_LOCK_TABLE_SIZE; probe++) {
+ slot_idx = (user_cache_entry_lock_table_index(locks[i].hash) + probe) %
+ PHP_USER_CACHE_ENTRY_LOCK_TABLE_SIZE
+ ;
+
+ record = &header->entry_lock_records[slot_idx];
+ if (record->state == PHP_USER_CACHE_ENTRY_LOCK_USED) {
+ continue;
+ }
+
+ key_offset = php_user_cache_alloc_locked(locks[i].key_len, locks[i].key);
+ if (key_offset == 0) {
+ break;
+ }
+
+ memset(record, 0, sizeof(*record));
+
+ record->hash = locks[i].hash;
+ record->owner_pid = locks[i].owner_pid;
+ record->owner_start_time = locks[i].owner_start_time;
+ record->expires_at = locks[i].expires_at;
+ record->key_offset = key_offset;
+ record->key_len = locks[i].key_len;
+ record->state = PHP_USER_CACHE_ENTRY_LOCK_USED;
+
+ break;
+ }
+ }
+}
+
+#ifdef PHP_USER_CACHE_HAVE_BOUNDARY_SHM
+static bool user_cache_header_boundary_identity_matches_locked(
+ const php_user_cache_header *header,
+ size_t requested_size)
+{
+ php_user_cache_context *ctx = php_user_cache_active_context();
+ php_user_cache_storage *storage = &ctx->storage;
+
+ if (!ctx->boundary_shared) {
+ return header->boundary_identity_digest_set == 0;
+ }
+
+ if (header->boundary_identity_digest_set == 0) {
+ return false;
+ }
+
+ /* The boundary identity is immutable for the partition lifetime. */
+ if (!storage->boundary_digest_memoized) {
+ user_cache_shared_boundary_digest(ctx, requested_size, storage->boundary_digest_memo);
+ storage->boundary_digest_memoized = true;
+ }
+
+ return memcmp(header->boundary_identity_digest, storage->boundary_digest_memo, sizeof(storage->boundary_digest_memo)) == 0;
+}
+#endif
+
+/* Recover an interrupted write while holding the exclusive lock. */
+static void user_cache_recover_after_owner_death(void)
+{
+ php_user_cache_header *header = php_user_cache_header_ptr();
+ php_user_cache_recovered_entry_lock *locks;
+ php_user_cache_reader_slot *slot;
+ uint64_t owner_pid, owner_start_time;
+ uint32_t lock_count = 0, i;
+
+ if (header == NULL || !php_user_cache_header_is_initialized_locked()) {
+ return;
+ }
+
+ if ((header->write_seq & 1) == 0) {
+ return;
+ }
+
+ locks = user_cache_collect_recovery_entry_locks(header, &lock_count);
+
+ memset(
+ php_user_cache_entries_ptr(header),
+ 0,
+ (size_t) header->capacity * sizeof(php_user_cache_entry)
+ );
+ memset(header->entry_lock_records, 0, sizeof(header->entry_lock_records));
+ memset(header->orphaned_graphs, 0, sizeof(header->orphaned_graphs));
+
+ for (i = 0; i < PHP_USER_CACHE_READER_SLOTS; i++) {
+ slot = &header->reader_slots[i];
+
+ /* Reader slots may be claimed lock-free. */
+#ifdef PHP_USER_CACHE_HAVE_OPTIMISTIC
+ owner_pid = php_user_cache_atomic_load_64(&slot->owner_pid);
+ owner_start_time = php_user_cache_atomic_load_64(&slot->owner_start_time);
+#else
+ owner_pid = slot->owner_pid;
+ owner_start_time = slot->owner_start_time;
+#endif
+
+ if (owner_pid != 0 &&
+ user_cache_entry_lock_owner_is_dead(owner_pid, owner_start_time)
+ ) {
+#ifdef PHP_USER_CACHE_HAVE_OPTIMISTIC
+ /* Clear start time before pid so scanners cannot combine owners. */
+ user_cache_atomic_cas_32(&slot->active, 1, 0);
+ user_cache_atomic_store_64(&slot->owner_start_time, 0);
+ user_cache_atomic_cas_64(&slot->owner_pid, owner_pid, 0);
+#else
+ memset(slot, 0, sizeof(*slot));
+#endif
+ }
+ }
+
+ header->count = 0;
+ header->tombstone_count = 0;
+ header->next_free = 0;
+ header->free_list = 0;
+ header->last_block_offset = 0;
+ header->orphaned_graphs_saturated = 0;
+
+ user_cache_restore_recovery_entry_locks(header, locks, lock_count);
+ user_cache_free_recovery_entry_locks(locks, lock_count);
+
+ php_user_cache_bump_mutation_epoch_locked(header);
+
+ user_cache_seq_publish(&header->write_seq, header->write_seq + 1);
+}
+
+#ifdef PHP_USER_CACHE_HAVE_SHARED_MUTEX
+static bool user_cache_shared_mutex_init(php_user_cache_header *header)
+{
+ pthread_mutexattr_t attr;
+ bool result;
+
+ if (pthread_mutexattr_init(&attr) != 0) {
+ return false;
+ }
+
+ result = 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 result;
+}
+
+static bool user_cache_shared_mutex_lock(void)
+{
+ php_user_cache_header *header = php_user_cache_header_ptr();
+ int result;
+
+ if (header == NULL) {
+ return false;
+ }
+
+ result = pthread_mutex_lock(&header->global_shared_mutex.mutex);
+ if (result == EOWNERDEAD) {
+ /* A failed robust-mutex recovery makes the segment unusable. */
+ if (pthread_mutex_consistent(&header->global_shared_mutex.mutex) != 0) {
+ php_error_docref(NULL, E_WARNING, "Cache: unable to make the shared lock consistent after owner death");
+ pthread_mutex_unlock(&header->global_shared_mutex.mutex);
+
+ return false;
+ }
+
+ user_cache_recover_after_owner_death();
+
+ result = 0;
+ }
+
+ if (result != 0) {
+ return false;
+ }
+
+ UC_G(lock_held) = true;
+
+ return true;
+}
+#endif
+
+#ifndef ZEND_WIN32
+static bool user_cache_lock_impl(short lock_type)
+{
+ php_user_cache_storage *storage = &php_user_cache_active_context()->storage;
+ struct flock mem_lock;
+
+ if (!storage->lock_initialized) {
+ return false;
+ }
+
+#ifdef PHP_USER_CACHE_HAVE_SHARED_MUTEX
+ if (storage->use_shared_mutex) {
+ return user_cache_shared_mutex_lock();
+ }
+#endif
+
+ if (!user_cache_zts_lock(storage)) {
+ return false;
+ }
+
+ 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) {
+ user_cache_zts_unlock(storage);
+
+ return false;
+ }
+ }
+
+ UC_G(lock_held) = true;
+
+ return true;
+}
+
+static bool user_cache_create_lock(void)
+{
+ php_user_cache_context *ctx = php_user_cache_active_context();
+ php_user_cache_storage *storage = &ctx->storage;
+ int val;
+#ifdef PHP_USER_CACHE_USE_SHM_OPEN
+ struct stat lock_st;
+#endif
+
+ if (storage->lock_initialized) {
+ return true;
+ }
+
+ if (!user_cache_zts_lock_init(storage)) {
+ return false;
+ }
+
+#ifdef PHP_USER_CACHE_USE_SHM_OPEN
+ if (ctx->boundary_shared) {
+ user_cache_shared_boundary_lock_name(
+ storage->lockfile_name,
+ sizeof(storage->lockfile_name),
+ storage->size
+ );
+
+ /* Refuse symlinks in the predictable world-writable lock path. */
+ storage->lock_file = open(storage->lockfile_name, O_RDWR | O_CREAT | O_CLOEXEC | O_NOFOLLOW, 0600);
+ if (storage->lock_file >= 0 &&
+ !user_cache_shared_boundary_fd_is_trusted(storage->lock_file, &lock_st)
+ ) {
+ close(storage->lock_file);
+ storage->lock_file = -1;
+ }
+
+ if (storage->lock_file < 0) {
+ user_cache_zts_lock_destroy(storage);
+
+ return false;
+ }
+
+ storage->lock_initialized = true;
+
+ return true;
+ }
+#endif
+
+#if defined(__linux__) && defined(HAVE_MEMFD_CREATE) && defined(MFD_CLOEXEC)
+ storage->lock_file = memfd_create(ctx->lock_name, MFD_CLOEXEC);
+ if (storage->lock_file >= 0) {
+ storage->lock_initialized = true;
+
+ return true;
+ }
+#endif
+
+#ifdef O_TMPFILE
+ storage->lock_file = open(UC_G(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",
+ UC_G(lockfile_path),
+ ctx->sem_filename_prefix
+ );
+
+ storage->lock_file = mkstemp(storage->lockfile_name);
+ if (storage->lock_file == -1) {
+ user_cache_zts_lock_destroy(storage);
+
+ return false;
+ }
+
+ if (fchmod(storage->lock_file, 0666) == -1) {
+ close(storage->lock_file);
+ storage->lock_file = -1;
+ user_cache_zts_lock_destroy(storage);
+
+ 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 user_cache_destroy_lock(void)
+{
+ php_user_cache_storage *storage = &php_user_cache_active_context()->storage;
+
+ if (!storage->lock_initialized) {
+ return;
+ }
+
+ if (storage->lock_file >= 0) {
+ close(storage->lock_file);
+ storage->lock_file = -1;
+ }
+
+ user_cache_zts_lock_destroy(storage);
+
+ storage->lock_initialized = false;
+}
+
+static bool user_cache_rlock_impl(void)
+{
+ return user_cache_lock_impl(F_RDLCK);
+}
+
+static bool user_cache_wlock_impl(void)
+{
+ return user_cache_lock_impl(F_WRLCK);
+}
+
+static void user_cache_unlock_impl(void)
+{
+ php_user_cache_storage *storage = &php_user_cache_active_context()->storage;
+ struct flock mem_unlock;
+
+ if (!storage->lock_initialized) {
+ return;
+ }
+
+#ifdef PHP_USER_CACHE_HAVE_SHARED_MUTEX
+ if (storage->use_shared_mutex) {
+ php_user_cache_header *header = php_user_cache_header_ptr();
+
+ if (header != NULL) {
+ pthread_mutex_unlock(&header->global_shared_mutex.mutex);
+ }
+
+ UC_G(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);
+
+ user_cache_zts_unlock(storage);
+ UC_G(lock_held) = false;
+}
+#else
+static bool user_cache_win32_open_lock_file_at(php_user_cache_storage *storage, const char *dir, const char *base_name)
+{
+ size_t dir_len;
+ const char *sep;
+
+ if (dir == NULL || dir[0] == '\0') {
+ return false;
+ }
+
+ dir_len = strlen(dir);
+ sep = dir[dir_len - 1] == '/' || dir[dir_len - 1] == '\\'
+ ? ""
+ : "/"
+ ;
+
+ snprintf(
+ storage->lockfile_name,
+ sizeof(storage->lockfile_name),
+ "%s%s%s.lock",
+ dir,
+ sep,
+ 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 user_cache_win32_open_lock_file(php_user_cache_context *ctx)
+{
+ php_user_cache_storage *storage = &ctx->storage;
+ DWORD tmp_path_w_len;
+ wchar_t tmp_path_w[MAXPATHLEN];
+ size_t tmp_path_len;
+ bool opened;
+ char base_name[MAXPATHLEN], *tmp_path, *p;
+
+ user_cache_win32_format_name(
+ base_name,
+ sizeof(base_name),
+ PHP_USER_CACHE_WIN32_LOCK_FILE_NAME,
+ storage->size
+ );
+
+ for (p = base_name; *p != '\0'; p++) {
+ if ((uint8_t) *p < 32 ||
+ *p == '<' ||
+ *p == '>' ||
+ *p == ':' ||
+ *p == '"' ||
+ *p == '/' ||
+ *p == '\\' ||
+ *p == '|' ||
+ *p == '?' ||
+ *p == '*'
+ ) {
+ *p = '_';
+ }
+ }
+
+ /* Preserve temporary paths outside the ANSI codepage. */
+ tmp_path_w_len = GetTempPathW(MAXPATHLEN, tmp_path_w);
+ if (tmp_path_w_len == 0 || tmp_path_w_len >= MAXPATHLEN) {
+ return false;
+ }
+
+ tmp_path = php_win32_ioutil_conv_w_to_any(tmp_path_w, tmp_path_w_len, &tmp_path_len);
+ if (tmp_path == NULL) {
+ return false;
+ }
+
+ opened = user_cache_win32_open_lock_file_at(storage, tmp_path, base_name);
+ free(tmp_path);
+
+ return opened;
+}
+
+static bool user_cache_win32_lock_range(php_user_cache_storage *storage, DWORD offset, bool exclusive, bool blocking)
+{
+ OVERLAPPED overlapped;
+ HANDLE handle;
+ DWORD flags = 0;
+
+ if (!storage->lock_initialized || storage->lock_file < 0) {
+ return false;
+ }
+
+ handle = (HANDLE) _get_osfhandle(storage->lock_file);
+ if (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(handle, flags, 0, 1, 0, &overlapped) == TRUE;
+}
+
+static void user_cache_win32_unlock_range(php_user_cache_storage *storage, DWORD offset)
+{
+ OVERLAPPED overlapped;
+ HANDLE handle;
+
+ if (!storage->lock_initialized || storage->lock_file < 0) {
+ return;
+ }
+
+ handle = (HANDLE) _get_osfhandle(storage->lock_file);
+ if (handle == INVALID_HANDLE_VALUE) {
+ return;
+ }
+
+ memset(&overlapped, 0, sizeof(overlapped));
+ overlapped.Offset = offset;
+
+ UnlockFileEx(handle, 0, 1, 0, &overlapped);
+}
+
+static bool user_cache_lock_impl(bool exclusive)
+{
+ php_user_cache_storage *storage = &php_user_cache_active_context()->storage;
+
+ if (!storage->lock_initialized) {
+ return false;
+ }
+
+ if (!user_cache_zts_lock(storage)) {
+ return false;
+ }
+
+ if (!user_cache_win32_lock_range(storage, 0, exclusive, true)) {
+ user_cache_zts_unlock(storage);
+
+ return false;
+ }
+
+ UC_G(lock_held) = true;
+
+ return true;
+}
+
+static bool user_cache_create_lock(void)
+{
+ php_user_cache_context *ctx = php_user_cache_active_context();
+ php_user_cache_storage *storage = &ctx->storage;
+
+ if (storage->lock_initialized) {
+ return true;
+ }
+
+ if (!user_cache_zts_lock_init(storage)) {
+ return false;
+ }
+
+ if (!user_cache_win32_open_lock_file(ctx)) {
+ user_cache_zts_lock_destroy(storage);
+
+ return false;
+ }
+
+ storage->lock_initialized = true;
+
+ return true;
+}
+
+static void user_cache_destroy_lock(void)
+{
+ php_user_cache_storage *storage = &php_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;
+ }
+
+ user_cache_zts_lock_destroy(storage);
+
+ storage->lock_initialized = false;
+}
+
+static bool user_cache_rlock_impl(void)
+{
+ return user_cache_lock_impl(false);
+}
+
+static bool user_cache_wlock_impl(void)
+{
+ return user_cache_lock_impl(true);
+}
+
+static void user_cache_unlock_impl(void)
+{
+ php_user_cache_storage *storage = &php_user_cache_active_context()->storage;
+
+ if (!storage->lock_initialized) {
+ return;
+ }
+
+ user_cache_win32_unlock_range(storage, 0);
+
+ user_cache_zts_unlock(storage);
+
+ UC_G(lock_held) = false;
+}
+#endif
+
+static uint64_t 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 void user_cache_remove_entry_lock_record_locked(
+ php_user_cache_entry_lock_record *record)
+{
+ if (record->state == PHP_USER_CACHE_ENTRY_LOCK_USED && record->key_offset != 0) {
+ php_user_cache_free_locked(record->key_offset);
+ }
+
+ memset(record, 0, sizeof(*record));
+
+ record->state = PHP_USER_CACHE_ENTRY_LOCK_TOMBSTONE;
+}
+
+static bool user_cache_find_entry_lock_record_slot_locked(
+ php_user_cache_header *header,
+ zend_string *key,
+ zend_ulong hash,
+ uint32_t *slot_idx,
+ bool *found)
+{
+ php_user_cache_entry_lock_record *record;
+ uint64_t now = 0;
+ uint32_t first_avail = UINT32_MAX, i, probe;
+
+ *found = false;
+
+ for (probe = 0; probe < PHP_USER_CACHE_ENTRY_LOCK_TABLE_SIZE; probe++) {
+ i = (user_cache_entry_lock_table_index(hash) + probe) % PHP_USER_CACHE_ENTRY_LOCK_TABLE_SIZE;
+ record = &header->entry_lock_records[i];
+
+ if (record->state == PHP_USER_CACHE_ENTRY_LOCK_EMPTY) {
+ *slot_idx = first_avail != UINT32_MAX ? first_avail : i;
+
+ return true;
+ }
+
+ if (record->state == PHP_USER_CACHE_ENTRY_LOCK_TOMBSTONE) {
+ if (first_avail == UINT32_MAX) {
+ first_avail = i;
+ }
+
+ continue;
+ }
+
+ /* Avoid a clock read for an empty table. */
+ if (now == 0) {
+ now = (uint64_t) time(NULL);
+ }
+
+ if (!user_cache_entry_lock_record_is_active_locked(record, now, true)) {
+ user_cache_remove_entry_lock_record_locked(record);
+
+ if (first_avail == UINT32_MAX) {
+ first_avail = i;
+ }
+
+ continue;
+ }
+
+ if (user_cache_entry_lock_record_key_matches(record, key, hash)) {
+ *slot_idx = i;
+ *found = true;
+
+ return true;
+ }
+ }
+
+ if (first_avail != UINT32_MAX) {
+ *slot_idx = first_avail;
+
+ return true;
+ }
+
+ return false;
+}
+
+static bool user_cache_insert_entry_lock_record_locked(
+ php_user_cache_header *header,
+ uint32_t slot_idx,
+ zend_string *key,
+ zend_ulong hash,
+ zend_long lease)
+{
+ php_user_cache_entry_lock_record *record = &header->entry_lock_records[slot_idx];
+ uint64_t owner_pid;
+ uint32_t key_offset;
+
+ if (ZSTR_LEN(key) > UINT32_MAX) {
+ return false;
+ }
+
+ key_offset = php_user_cache_alloc_locked(ZSTR_LEN(key), ZSTR_VAL(key));
+ if (key_offset == 0) {
+ return false;
+ }
+
+ owner_pid = php_user_cache_cached_pid();
+
+ memset(record, 0, sizeof(*record));
+
+ record->hash = hash;
+ record->owner_pid = owner_pid;
+ record->owner_start_time = user_cache_cached_self_start_time_token(owner_pid);
+ record->expires_at = lease > 0 ? user_cache_entry_lock_expires_at(lease) : 0;
+ record->key_offset = key_offset;
+ record->key_len = (uint32_t) ZSTR_LEN(key);
+ record->state = PHP_USER_CACHE_ENTRY_LOCK_USED;
+
+ return true;
+}
+
+static bool user_cache_update_entry_lock_record_lease_locked(
+ php_user_cache_header *header,
+ zend_string *key,
+ zend_long lease)
+{
+ php_user_cache_entry_lock_record *record;
+ zend_ulong hash;
+ uint64_t expires_at;
+ uint32_t slot_idx;
+ bool found;
+
+ if (lease <= 0) {
+ return true;
+ }
+
+ hash = zend_string_hash_val(key);
+ if (!user_cache_find_entry_lock_record_slot_locked(header, key, hash, &slot_idx, &found) || !found) {
+ return false;
+ }
+
+ record = &header->entry_lock_records[slot_idx];
+ expires_at = user_cache_entry_lock_expires_at(lease);
+ if (record->expires_at < expires_at) {
+ record->expires_at = expires_at;
+ }
+
+ return true;
+}
+
+static php_user_cache_entry_lock *user_cache_create_local_entry_lock(
+ php_user_cache_context *ctx,
+ zend_long lease,
+ bool preserve_lease)
+{
+ php_user_cache_entry_lock *lock;
+
+ lock = emalloc(sizeof(php_user_cache_entry_lock));
+ lock->context = ctx;
+ lock->owner_pid = php_user_cache_cached_pid();
+ lock->lease = lease;
+ lock->preserve_lease = preserve_lease;
+
+ return lock;
+}
+
+static bool user_cache_add_local_entry_lock(
+ HashTable *locks,
+ zend_string *key,
+ php_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 user_cache_release_entry_lock_records_locked(
+ php_user_cache_header *header,
+ const php_user_cache_entry_lock_release_pair *pairs,
+ uint32_t pair_count,
+ php_user_cache_entry_lock_release_mode mode)
+{
+ php_user_cache_entry_lock *lock;
+ zend_ulong hash;
+ zend_string *key;
+ uint32_t i, slot_idx;
+ bool found;
+
+ for (i = 0; i < pair_count; i++) {
+ key = pairs[i].key;
+ lock = pairs[i].lock;
+
+ hash = zend_string_hash_val(key);
+ if (!user_cache_find_entry_lock_record_slot_locked(header, key, hash, &slot_idx, &found) || !found) {
+ continue;
+ }
+
+ if (header->entry_lock_records[slot_idx].owner_pid != lock->owner_pid) {
+ continue;
+ }
+
+ if (mode == PHP_USER_CACHE_ENTRY_LOCK_RELEASE_PRESERVE_LEASES &&
+ lock->preserve_lease &&
+ lock->lease > 0
+ ) {
+ header->entry_lock_records[slot_idx].owner_pid = 0;
+ header->entry_lock_records[slot_idx].owner_start_time = 0;
+ header->entry_lock_records[slot_idx].expires_at =
+ user_cache_entry_lock_expires_at(lock->lease)
+ ;
+ } else {
+ user_cache_remove_entry_lock_record_locked(&header->entry_lock_records[slot_idx]);
+ }
+ }
+}
+
+static void user_cache_destroy_entry_locks_if_empty(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 void user_cache_release_entry_locks_for_context(
+ php_user_cache_context *ctx,
+ HashTable **locks_ptr,
+ php_user_cache_entry_lock_release_mode mode)
+{
+ php_user_cache_context *prev_ctx;
+ php_user_cache_entry_lock *lock;
+ php_user_cache_header *header;
+ php_user_cache_entry_lock_release_pair *pairs;
+ zend_string *key;
+ uint32_t i, lock_count, pair_count = 0;
+
+ if (*locks_ptr == NULL) {
+ return;
+ }
+
+ lock_count = zend_hash_num_elements(*locks_ptr);
+ if (lock_count == 0 || mode == PHP_USER_CACHE_ENTRY_LOCK_RELEASE_DROP) {
+ zend_hash_destroy(*locks_ptr);
+ FREE_HASHTABLE(*locks_ptr);
+ *locks_ptr = NULL;
+
+ return;
+ }
+
+ pairs = safe_emalloc(lock_count, sizeof(php_user_cache_entry_lock_release_pair), 0);
+
+ ZEND_HASH_FOREACH_STR_KEY_PTR(*locks_ptr, key, lock) {
+ if (key == NULL || lock == NULL || lock->context != ctx) {
+ continue;
+ }
+
+ pairs[pair_count].key = zend_string_copy(key);
+ pairs[pair_count].lock = lock;
+
+ pair_count++;
+ } ZEND_HASH_FOREACH_END();
+
+ if (pair_count == 0) {
+ efree(pairs);
+
+ return;
+ }
+
+ prev_ctx = php_user_cache_activate_context(ctx);
+ if (php_user_cache_wlock()) {
+ header = php_user_cache_header_ptr();
+
+ if (php_user_cache_header_init_locked()) {
+ user_cache_release_entry_lock_records_locked(
+ header,
+ pairs,
+ pair_count,
+ mode
+ );
+ }
+
+ php_user_cache_unlock();
+ }
+
+ php_user_cache_restore_context(prev_ctx);
+
+ for (i = 0; i < pair_count; i++) {
+ zend_hash_del(*locks_ptr, pairs[i].key);
+ zend_string_release(pairs[i].key);
+ }
+
+ efree(pairs);
+
+ user_cache_destroy_entry_locks_if_empty(locks_ptr);
+}
+
+static void user_cache_release_entry_locks_all_contexts(
+ HashTable **locks_ptr,
+ php_user_cache_entry_lock_release_mode mode)
+{
+ php_user_cache_context *ctx = NULL;
+ php_user_cache_entry_lock *lock;
+
+ while (*locks_ptr != NULL) {
+ ctx = NULL;
+
+ ZEND_HASH_FOREACH_PTR(*locks_ptr, lock) {
+ if (lock != NULL && lock->context != NULL) {
+ ctx = lock->context;
+
+ break;
+ }
+ } ZEND_HASH_FOREACH_END();
+
+ if (ctx == NULL) {
+ user_cache_release_entry_locks_for_context(
+ php_user_cache_owning_context(),
+ locks_ptr,
+ PHP_USER_CACHE_ENTRY_LOCK_RELEASE_DROP
+ );
+
+ return;
+ }
+
+ user_cache_release_entry_locks_for_context(ctx, locks_ptr, mode);
+ }
+}
+
+static void user_cache_ensure_entry_lock_owner(void)
+{
+#ifndef ZEND_WIN32
+ zend_ulong cur_pid = (zend_ulong) php_user_cache_cached_pid();
+
+ if (UC_G(entry_lock_owner_pid) == 0) {
+ UC_G(entry_lock_owner_pid) = cur_pid;
+
+ return;
+ }
+
+ if (UC_G(entry_lock_owner_pid) == cur_pid) {
+ return;
+ }
+
+ user_cache_release_entry_locks_for_context(
+ php_user_cache_owning_context(),
+ &UC_G(entry_lock_table),
+ PHP_USER_CACHE_ENTRY_LOCK_RELEASE_DROP
+ );
+
+ UC_G(entry_lock_owner_pid) = cur_pid;
+#endif
+}
+
+static void user_cache_entry_lock_dtor(zval *lock_zv)
+{
+ php_user_cache_entry_lock *lock = Z_PTR_P(lock_zv);
+
+ if (lock != NULL) {
+ efree(lock);
+ }
+}
+
+static HashTable *user_cache_prepare_entry_locks_for_insert(uint32_t reserve)
+{
+ HashTable **locks_ptr = user_cache_entry_lock_table_ptr();
+
+ if (*locks_ptr == NULL) {
+ ALLOC_HASHTABLE(*locks_ptr);
+ zend_hash_init(*locks_ptr, 0, NULL, user_cache_entry_lock_dtor, 0);
+ }
+
+ zend_hash_extend(*locks_ptr, zend_hash_num_elements(*locks_ptr) + reserve, 0);
+
+ return *locks_ptr;
+}
+
+static bool user_cache_upgrade_held_entry_lock(
+ php_user_cache_context *ctx,
+ zend_string *key,
+ zend_long lease,
+ bool preserve_lease,
+ php_user_cache_entry_lock *lock)
+{
+ php_user_cache_context *prev_ctx;
+ php_user_cache_header *header;
+ bool updated;
+
+ if (lease > lock->lease) {
+ prev_ctx = php_user_cache_activate_context(ctx);
+ if (!php_user_cache_wlock()) {
+ php_user_cache_restore_context(prev_ctx);
+
+ return false;
+ }
+
+ header = php_user_cache_header_ptr();
+ updated = php_user_cache_header_init_locked() &&
+ user_cache_update_entry_lock_record_lease_locked(header, key, lease)
+ ;
+
+ php_user_cache_unlock();
+ php_user_cache_restore_context(prev_ctx);
+
+ if (!updated) {
+ return false;
+ }
+
+ lock->lease = lease;
+ }
+
+ lock->preserve_lease = lock->preserve_lease || preserve_lease;
+
+ return true;
+}
+
+static bool user_cache_remove_owned_entry_lock_record(
+ php_user_cache_context *ctx,
+ zend_string *key,
+ zend_ulong hash,
+ uint64_t owner_pid)
+{
+ php_user_cache_context *prev_ctx;
+ php_user_cache_header *header;
+ uint32_t slot_idx;
+ bool found;
+
+ prev_ctx = php_user_cache_activate_context(ctx);
+ if (!php_user_cache_wlock()) {
+ php_user_cache_restore_context(prev_ctx);
+
+ return false;
+ }
+
+ header = php_user_cache_header_ptr();
+
+ if (php_user_cache_header_is_initialized_locked() &&
+ user_cache_find_entry_lock_record_slot_locked(
+ header,
+ key,
+ hash,
+ &slot_idx,
+ &found
+ ) &&
+ found &&
+ header->entry_lock_records[slot_idx].owner_pid == owner_pid
+ ) {
+ user_cache_remove_entry_lock_record_locked(
+ &header->entry_lock_records[slot_idx]
+ );
+ }
+
+ php_user_cache_unlock();
+ php_user_cache_restore_context(prev_ctx);
+
+ return true;
+}
+
+/* Release all flagged entry locks under one write lock. */
+static void user_cache_release_entry_locks_for_keys(
+ php_user_cache_context *ctx,
+ zend_string **keys,
+ const bool *acquired,
+ uint32_t count)
+{
+ php_user_cache_context *prev_ctx;
+ php_user_cache_entry_lock *lock;
+ php_user_cache_header *header;
+ zend_ulong hash;
+ HashTable **locks_ptr = user_cache_entry_lock_table_ptr();
+ uint32_t i, slot_idx;
+ bool found, any_held = false;
+
+ user_cache_ensure_entry_lock_owner();
+
+ for (i = 0; i < count; i++) {
+ if (acquired[i] && user_cache_find_local_entry_lock(ctx, keys[i]) != NULL) {
+ any_held = true;
+
+ break;
+ }
+ }
+
+ if (!any_held) {
+ return;
+ }
+
+ prev_ctx = php_user_cache_activate_context(ctx);
+
+ if (!php_user_cache_wlock()) {
+ php_user_cache_restore_context(prev_ctx);
+
+ return;
+ }
+
+ header = php_user_cache_header_ptr();
+
+ if (php_user_cache_header_is_initialized_locked()) {
+ for (i = 0; i < count; i++) {
+ if (!acquired[i]) {
+ continue;
+ }
+
+ lock = user_cache_find_local_entry_lock(ctx, keys[i]);
+ if (lock == NULL) {
+ continue;
+ }
+
+ hash = zend_string_hash_val(keys[i]);
+ if (user_cache_find_entry_lock_record_slot_locked(header, keys[i], hash, &slot_idx, &found) &&
+ found &&
+ header->entry_lock_records[slot_idx].owner_pid == lock->owner_pid
+ ) {
+ user_cache_remove_entry_lock_record_locked(&header->entry_lock_records[slot_idx]);
+ }
+ }
+ }
+
+ php_user_cache_unlock();
+ php_user_cache_restore_context(prev_ctx);
+
+ for (i = 0; i < count; i++) {
+ if (acquired[i] && user_cache_find_local_entry_lock(ctx, keys[i]) != NULL) {
+ zend_hash_del(*locks_ptr, keys[i]);
+ }
+ }
+
+ user_cache_destroy_entry_locks_if_empty(locks_ptr);
+}
+
+static bool user_cache_acquire_entry_lock_record(
+ php_user_cache_context *ctx,
+ zend_string *key,
+ zend_long lease,
+ bool preserve_lease,
+ bool blocking)
+{
+ php_user_cache_context *prev_ctx;
+ php_user_cache_entry_lock *lock;
+ php_user_cache_header *header;
+ zend_ulong hash = zend_string_hash_val(key);
+ HashTable *locks, **locks_ptr = user_cache_entry_lock_table_ptr();
+ uint64_t waited_us = 0;
+ uint32_t slot_idx;
+ bool found, inserted, insert_failed;
+
+ user_cache_ensure_entry_lock_owner();
+
+ lock = user_cache_find_local_entry_lock(ctx, key);
+ if (lock != NULL) {
+ return user_cache_upgrade_held_entry_lock(ctx, key, lease, preserve_lease, lock);
+ }
+
+ locks = user_cache_prepare_entry_locks_for_insert(1);
+ lock = user_cache_create_local_entry_lock(ctx, lease, preserve_lease);
+
+ for (;;) {
+ inserted = false;
+ found = false;
+ insert_failed = false;
+ prev_ctx = php_user_cache_activate_context(ctx);
+
+ if (!php_user_cache_wlock()) {
+ php_user_cache_restore_context(prev_ctx);
+ efree(lock);
+ user_cache_destroy_entry_locks_if_empty(locks_ptr);
+
+ return false;
+ }
+
+ header = php_user_cache_header_ptr();
+
+ if (!php_user_cache_header_init_locked()) {
+ php_user_cache_unlock();
+ php_user_cache_restore_context(prev_ctx);
+ efree(lock);
+ user_cache_destroy_entry_locks_if_empty(locks_ptr);
+
+ return false;
+ }
+
+ if (user_cache_find_entry_lock_record_slot_locked(header, key, hash, &slot_idx, &found)) {
+ if (!found &&
+ !(inserted = user_cache_insert_entry_lock_record_locked(header, slot_idx, key, hash, lease))
+ ) {
+ insert_failed = true;
+ }
+ }
+
+ php_user_cache_unlock();
+ php_user_cache_restore_context(prev_ctx);
+
+ if (inserted) {
+ if (user_cache_add_local_entry_lock(locks, key, lock)) {
+ return true;
+ }
+
+ /* Roll back a record that cannot be tracked locally. */
+ user_cache_remove_owned_entry_lock_record(ctx, key, hash, lock->owner_pid);
+ efree(lock);
+ user_cache_destroy_entry_locks_if_empty(locks_ptr);
+
+ return false;
+ }
+
+ if (insert_failed) {
+ efree(lock);
+ user_cache_destroy_entry_locks_if_empty(locks_ptr);
+
+ return false;
+ }
+
+ if (!blocking || waited_us >= PHP_USER_CACHE_ENTRY_LOCK_WAIT_TIMEOUT_US) {
+ efree(lock);
+ user_cache_destroy_entry_locks_if_empty(locks_ptr);
+
+ return false;
+ }
+
+ waited_us += user_cache_sleep_entry_lock_retry_interval();
+ }
+}
+
+/* Keys are sorted so concurrent bulk operations acquire locks in one order. */
+static bool user_cache_acquire_entry_lock_records(
+ php_user_cache_context *ctx,
+ zend_string **keys,
+ bool *acquired,
+ uint32_t count)
+{
+ php_user_cache_context *prev_ctx;
+ php_user_cache_entry_lock *existing, **pending_locks;
+ php_user_cache_header *header;
+ zend_ulong hash;
+ HashTable *locks, **locks_ptr = user_cache_entry_lock_table_ptr();
+ uint64_t waited_us = 0;
+ uint32_t i, slot_idx, start = 0, stop, insert_end, blocked_idx = UINT32_MAX, pending_count = 0;
+ bool *collisions, found, blocked, insert_failed, add_failed;
+
+ user_cache_ensure_entry_lock_owner();
+
+ pending_locks = safe_emalloc(count, sizeof(*pending_locks), 0);
+ collisions = safe_emalloc(count, sizeof(*collisions), 0);
+
+ for (i = 0; i < count; i++) {
+ acquired[i] = false;
+ pending_locks[i] = NULL;
+ collisions[i] = false;
+
+ /* Collapse adjacent duplicates. */
+ if (i > 0 && zend_string_equals(keys[i], keys[i - 1])) {
+ continue;
+ }
+
+ existing = *locks_ptr != NULL ? zend_hash_find_ptr(*locks_ptr, keys[i]) : NULL;
+ if (existing != NULL && existing->context == ctx) {
+ continue;
+ }
+
+ /* Local tracking will reject a record from another context. */
+ collisions[i] = existing != NULL;
+ pending_locks[i] = user_cache_create_local_entry_lock(ctx, 0, false);
+
+ pending_count++;
+ }
+
+ if (pending_count == 0) {
+ efree(collisions);
+ efree(pending_locks);
+
+ return true;
+ }
+
+ locks = user_cache_prepare_entry_locks_for_insert(pending_count);
+
+ for (;;) {
+ stop = count;
+ insert_end = count;
+ blocked = false;
+ insert_failed = false;
+ add_failed = false;
+ prev_ctx = php_user_cache_activate_context(ctx);
+
+ if (!php_user_cache_wlock()) {
+ php_user_cache_restore_context(prev_ctx);
+
+ goto bailout;
+ }
+
+ header = php_user_cache_header_ptr();
+
+ if (!php_user_cache_header_init_locked()) {
+ php_user_cache_unlock();
+ php_user_cache_restore_context(prev_ctx);
+
+ goto bailout;
+ }
+
+ /* Stop before waiting so the global lock is not held. */
+ for (i = start; i < count; i++) {
+ if (pending_locks[i] == NULL) {
+ continue;
+ }
+
+ hash = zend_string_hash_val(keys[i]);
+
+ if (!user_cache_find_entry_lock_record_slot_locked(header, keys[i], hash, &slot_idx, &found) || found) {
+ blocked = true;
+ stop = i;
+ insert_end = i;
+
+ break;
+ }
+
+ if (!user_cache_insert_entry_lock_record_locked(header, slot_idx, keys[i], hash, 0)) {
+ insert_failed = true;
+ stop = i;
+ insert_end = i;
+
+ break;
+ }
+
+ if (collisions[i]) {
+ /* Do not insert records beyond the known failure. */
+ stop = i;
+ insert_end = i + 1;
+
+ break;
+ }
+ }
+
+ php_user_cache_unlock();
+ php_user_cache_restore_context(prev_ctx);
+
+ /* Roll back records that cannot be tracked locally. */
+ for (i = start; i < insert_end; i++) {
+ if (pending_locks[i] == NULL) {
+ continue;
+ }
+
+ if (add_failed || !user_cache_add_local_entry_lock(locks, keys[i], pending_locks[i])) {
+ user_cache_remove_owned_entry_lock_record(
+ ctx,
+ keys[i],
+ zend_string_hash_val(keys[i]),
+ pending_locks[i]->owner_pid
+ );
+ efree(pending_locks[i]);
+ pending_locks[i] = NULL;
+ add_failed = true;
+
+ continue;
+ }
+
+ pending_locks[i] = NULL;
+ acquired[i] = true;
+ }
+
+ if (add_failed || insert_failed) {
+ goto bailout;
+ }
+
+ if (!blocked) {
+ efree(collisions);
+ efree(pending_locks);
+
+ return true;
+ }
+
+ if (blocked_idx != stop) {
+ /* Each blocked key gets a fresh wait budget. */
+ blocked_idx = stop;
+ waited_us = 0;
+ }
+
+ if (waited_us >= PHP_USER_CACHE_ENTRY_LOCK_WAIT_TIMEOUT_US) {
+ goto bailout;
+ }
+
+ waited_us += user_cache_sleep_entry_lock_retry_interval();
+ start = stop;
+ }
+
+bailout:
+ for (i = 0; i < count; i++) {
+ if (pending_locks[i] != NULL) {
+ efree(pending_locks[i]);
+ }
+ }
+
+ efree(collisions);
+ efree(pending_locks);
+ user_cache_destroy_entry_locks_if_empty(locks_ptr);
+
+ return false;
+}
+
+static uint32_t user_cache_calculate_capacity(size_t size)
+{
+ uint32_t capacity = PHP_USER_CACHE_MIN_CAPACITY;
+ size_t target = size / PHP_USER_CACHE_SLOT_BYTES, data_offset;
+ unsigned int i;
+
+ /* Leave room for value data after the entry table. */
+ for (i = 0; i < sizeof(php_user_cache_capacity_primes) / sizeof(php_user_cache_capacity_primes[0]); i++) {
+ if (php_user_cache_capacity_primes[i] > target) {
+ break;
+ }
+
+ data_offset = PHP_USER_CACHE_ALIGNED_SIZE(
+ sizeof(php_user_cache_header)
+ + (size_t) php_user_cache_capacity_primes[i] * sizeof(php_user_cache_entry)
+ );
+ if (data_offset >= size) {
+ break;
+ }
+
+ capacity = php_user_cache_capacity_primes[i];
+ }
+
+ return capacity;
+}
+
+static void user_cache_free_list_remove_locked(php_user_cache_header *header, uint32_t block_offset)
+{
+ php_user_cache_block *block = php_user_cache_block_ptr(block_offset);
+
+ if (block->prev_free != 0) {
+ php_user_cache_block_ptr(block->prev_free)->next_free = block->next_free;
+ } else {
+ header->free_list = block->next_free;
+ }
+
+ if (block->next_free != 0) {
+ php_user_cache_block_ptr(block->next_free)->prev_free = block->prev_free;
+ }
+
+ block->next_free = 0;
+ block->prev_free = 0;
+ block->flags &= ~PHP_USER_CACHE_BLOCK_FREE;
+}
+
+static void user_cache_free_list_insert_locked(php_user_cache_header *header, uint32_t block_offset)
+{
+ php_user_cache_block *block = php_user_cache_block_ptr(block_offset);
+
+ block->prev_free = 0;
+ block->next_free = header->free_list;
+
+ if (header->free_list != 0) {
+ php_user_cache_block_ptr(header->free_list)->prev_free = block_offset;
+ }
+
+ user_cache_block_mark_free(block);
+
+ header->free_list = block_offset;
+}
+
+static void user_cache_update_following_prev_size_locked(
+ php_user_cache_header *header,
+ uint32_t block_offset,
+ const php_user_cache_block *block
+)
+{
+ uint32_t next_offset = block_offset + block->size;
+
+ if (next_offset < user_cache_used_end_offset_locked(header)) {
+ php_user_cache_block_ptr(next_offset)->prev_size = block->size;
+ }
+}
+
+static void user_cache_trim_tail_free_blocks_locked(
+ php_user_cache_header *header,
+ uint32_t block_offset
+)
+{
+ php_user_cache_block *block = php_user_cache_block_ptr(block_offset);
+ uint32_t prev_offset;
+
+ while (block_offset >= header->data_offset &&
+ header->last_block_offset == block_offset &&
+ php_user_cache_block_is_free(block) &&
+ block_offset + block->size == user_cache_used_end_offset_locked(header)
+ ) {
+ prev_offset = 0;
+ 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 = php_user_cache_block_ptr(block_offset);
+ }
+}
+
+static void user_cache_write_section_enter(void)
+{
+ php_user_cache_header *header = php_user_cache_header_ptr();
+
+ UC_G(write_seq_bumped) = false;
+
+#ifdef PHP_USER_CACHE_HAVE_OPTIMISTIC
+ UC_G(reader_drain_state) = 0;
+#endif
+
+ if (header == NULL || !php_user_cache_header_is_initialized_locked()) {
+ return;
+ }
+
+ user_cache_seq_announce(&header->write_seq, header->write_seq + 1);
+
+ UC_G(write_seq_bumped) = true;
+}
+
+static void user_cache_write_section_leave(void)
+{
+ php_user_cache_header *header;
+
+ if (!UC_G(write_seq_bumped)) {
+ return;
+ }
+
+ UC_G(write_seq_bumped) = false;
+
+#ifdef PHP_USER_CACHE_HAVE_OPTIMISTIC
+ UC_G(reader_drain_state) = 0;
+#endif
+
+ header = php_user_cache_header_ptr();
+ if (header == NULL) {
+ return;
+ }
+
+ user_cache_seq_publish(&header->write_seq, header->write_seq + 1);
+}
+
+static void user_cache_write_section_note_header_initialized(php_user_cache_header *header)
+{
+ if (!UC_G(lock_held_is_write) || UC_G(write_seq_bumped)) {
+ return;
+ }
+
+ user_cache_seq_announce(&header->write_seq, header->write_seq + 1);
+ UC_G(write_seq_bumped) = true;
+
+#ifdef PHP_USER_CACHE_HAVE_OPTIMISTIC
+ UC_G(reader_drain_state) = 0;
+#endif
+}
+
+/* Publish a fresh segment by storing its magic last. */
+static bool user_cache_header_format_fresh_locked(
+ php_user_cache_header *header,
+ uint32_t capacity,
+ uint32_t data_offset)
+{
+ php_user_cache_storage *storage = &php_user_cache_active_context()->storage;
+
+ 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;
+ /* Preserve an existing odd sequence until the write section ends. */
+ header->write_seq = UC_G(write_seq_bumped) ? 1u : 2u;
+#ifdef PHP_USER_CACHE_HAVE_SHARED_MUTEX
+ header->lock_model = user_cache_shared_mutex_init(header)
+ ? PHP_USER_CACHE_LOCK_MODEL_MUTEX
+ : PHP_USER_CACHE_LOCK_MODEL_FCNTL
+ ;
+#else
+ header->lock_model = PHP_USER_CACHE_LOCK_MODEL_FCNTL;
+#endif
+ if (php_user_cache_active_context()->boundary_shared) {
+#ifdef PHP_USER_CACHE_HAVE_BOUNDARY_SHM
+ user_cache_shared_boundary_digest(
+ php_user_cache_active_context(),
+ storage->size,
+ header->boundary_identity_digest
+ );
+
+ header->boundary_identity_digest_set = 1;
+#else
+ header->boundary_identity_digest_set = 0;
+#endif
+ }
+
+ header->version = PHP_USER_CACHE_VERSION;
+#ifdef PHP_USER_CACHE_HAVE_OPTIMISTIC
+ user_cache_atomic_fence_seq_cst();
+#endif
+ header->magic = PHP_USER_CACHE_MAGIC;
+
+ user_cache_write_section_note_header_initialized(header);
+
+ return true;
+}
+
+static uint32_t user_cache_alloc_from_free_list_locked(
+ php_user_cache_header *header,
+ size_t size,
+ uint32_t total_size,
+ const void *src)
+{
+ php_user_cache_block *block, *remainder;
+ uint32_t min_split_size, best_offset = 0, best_size = UINT32_MAX, *free_offset_ptr;
+
+ min_split_size = (uint32_t) PHP_USER_CACHE_ALIGNED_SIZE(sizeof(php_user_cache_block) + 1);
+
+ free_offset_ptr = &header->free_list;
+ while (*free_offset_ptr != 0) {
+ block = php_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) {
+ return 0;
+ }
+
+ block = php_user_cache_block_ptr(best_offset);
+ user_cache_free_list_remove_locked(header, best_offset);
+
+ if (block->size >= total_size + min_split_size) {
+ remainder = php_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 = PHP_USER_CACHE_BLOCK_FREE;
+
+ block->size = total_size;
+
+ user_cache_update_following_prev_size_locked(header, best_offset + total_size, remainder);
+ 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 {
+ user_cache_update_following_prev_size_locked(header, best_offset, block);
+ }
+
+ user_cache_block_mark_used(block);
+
+ if (src != NULL) {
+ memcpy(php_user_cache_ptr(best_offset + sizeof(php_user_cache_block)), src, size);
+ }
+
+ return best_offset + (uint32_t) sizeof(php_user_cache_block);
+}
+
+static uint32_t user_cache_alloc_from_tail_locked(
+ php_user_cache_header *header,
+ size_t size,
+ uint32_t total_size,
+ const void *src)
+{
+ php_user_cache_block *block;
+ uint32_t block_offset;
+
+ 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 = php_user_cache_block_ptr(block_offset);
+ block->size = total_size;
+ block->prev_size = header->last_block_offset != 0 ? php_user_cache_block_ptr(header->last_block_offset)->size : 0;
+ block->next_free = 0;
+ block->prev_free = 0;
+ block->flags = 0;
+
+ if (src != NULL) {
+ memcpy(php_user_cache_ptr(block_offset + sizeof(php_user_cache_block)), src, size);
+ }
+
+ header->next_free += total_size;
+ header->last_block_offset = block_offset;
+
+ return block_offset + (uint32_t) sizeof(php_user_cache_block);
+}
+
+static void user_cache_log_storage_handler_failure(const char *error_in, int error_code)
+{
+#ifdef ZEND_WIN32
+ char *msg = php_win32_error_to_msg(error_code);
+
+ php_error_docref(
+ NULL, E_WARNING,
+ "Cache: shared memory initialization failed: %s: %s (%d)",
+ error_in != NULL ? error_in : "unknown",
+ msg,
+ error_code
+ );
+
+ php_win32_error_msg_free(msg);
+#else
+ php_error_docref(
+ NULL, E_WARNING,
+ "Cache: shared memory initialization failed: %s: %s (%d)",
+ error_in != NULL ? error_in : "unknown",
+ strerror(error_code),
+ error_code
+ );
+#endif
+}
+
+static bool user_cache_try_storage_handler(
+ const php_user_cache_shm_handler_entry *handler_entry,
+ const char **error_in_ptr,
+ int *error_code_ptr
+)
+{
+ const char *error_in = NULL;
+ php_user_cache_context *ctx = php_user_cache_active_context();
+ php_user_cache_runtime *runtime = php_user_cache_active_runtime();
+ php_user_cache_storage *storage = &ctx->storage;
+ php_user_cache_shm_segment **segments = NULL;
+ int segment_count = 0;
+
+ if (handler_entry->handler->create_segments(
+ runtime->configured_memory,
+ &segments,
+ &segment_count,
+ &error_in
+ ) != PHP_USER_CACHE_ALLOC_SUCCESS
+ ) {
+ /* Preserve the error across cleanup. */
+#ifdef ZEND_WIN32
+ *error_code_ptr = (int) GetLastError();
+#else
+ *error_code_ptr = errno;
+#endif
+ *error_in_ptr = error_in;
+ 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->initialized = true;
+
+ return true;
+}
+
+/* Boundary contexts must not fall back to process-local storage. */
+static bool user_cache_select_storage_handler(void)
+{
+ const php_user_cache_shm_handler_entry *handler_entry;
+ const char *model = UC_G(memory_model);
+ const char *error_in = NULL;
+ int saved_error = 0;
+
+#ifdef PHP_USER_CACHE_HAVE_BOUNDARY_SHM
+ if (php_user_cache_active_context()->boundary_shared) {
+ if (user_cache_try_storage_handler(
+ user_cache_shared_boundary_handler_entry(),
+ &error_in,
+ &saved_error
+ )
+ ) {
+ return true;
+ }
+
+ user_cache_log_storage_handler_failure(error_in, saved_error);
+
+ return false;
+ }
+#endif
+
+ if (model && model[0]) {
+ if (strcmp(model, "cgi") == 0) {
+ model = "shm";
+ }
+
+ for (handler_entry = user_cache_handler_table(); handler_entry->name; handler_entry++) {
+ if (strcmp(model, handler_entry->name) == 0 &&
+ user_cache_try_storage_handler(handler_entry, &error_in, &saved_error)
+ ) {
+ return true;
+ }
+ }
+ }
+
+ for (handler_entry = user_cache_handler_table(); handler_entry->name; handler_entry++) {
+ if (model && model[0] && strcmp(model, handler_entry->name) == 0) {
+ continue;
+ }
+
+ if (user_cache_try_storage_handler(handler_entry, &error_in, &saved_error)) {
+ return true;
+ }
+ }
+
+ user_cache_log_storage_handler_failure(error_in, saved_error);
+
+ return false;
+}
+
+static bool user_cache_negotiate_lock_model(void)
+{
+#ifdef PHP_USER_CACHE_HAVE_SHARED_MUTEX
+ php_user_cache_storage *storage = &php_user_cache_active_context()->storage;
+
+ if (!user_cache_wlock_impl()) {
+ return false;
+ }
+
+ if (user_cache_header_is_initialized_acquire() &&
+ php_user_cache_header_ptr()->lock_model == PHP_USER_CACHE_LOCK_MODEL_MUTEX
+ ) {
+ user_cache_unlock_impl();
+ storage->use_shared_mutex = true;
+
+ if (!php_user_cache_wlock()) {
+ return false;
+ }
+ } else {
+ UC_G(lock_held_is_write) = true;
+ user_cache_recover_after_owner_death();
+ user_cache_write_section_enter();
+ }
+
+ return true;
+#else
+ return php_user_cache_wlock();
+#endif
+}
+
+static bool user_cache_startup_storage_impl(void)
+{
+ php_user_cache_storage *storage = &php_user_cache_active_context()->storage;
+
+ if (storage->initialized) {
+ return true;
+ }
+
+ if (!user_cache_select_storage_handler()) {
+ return false;
+ }
+
+ if (storage->segment_count != 1) {
+ goto bailout;
+ }
+
+ if (!user_cache_create_lock()) {
+ goto bailout;
+ }
+
+ if (!user_cache_negotiate_lock_model()) {
+ user_cache_destroy_lock();
+
+ goto bailout;
+ }
+
+ if (!php_user_cache_header_init_locked()) {
+ php_user_cache_unlock();
+ user_cache_destroy_lock();
+
+ goto bailout;
+ }
+
+ php_user_cache_unlock();
+
+#ifdef PHP_USER_CACHE_HAVE_SHARED_MUTEX
+ storage->use_shared_mutex =
+ php_user_cache_header_ptr() != NULL &&
+ php_user_cache_header_ptr()->lock_model == PHP_USER_CACHE_LOCK_MODEL_MUTEX
+ ;
+#endif
+
+#ifdef PHP_USER_CACHE_HAVE_OPTIMISTIC
+ user_cache_atomic_store_32(&storage->startup_complete, 1);
+#else
+ storage->startup_complete = 1;
+#endif
+
+ return true;
+
+bailout:
+ user_cache_cleanup_segments(
+ storage->handler,
+ storage->segments,
+ storage->segment_count
+ );
+ php_user_cache_reset_storage();
+
+ return false;
+}
+
+static bool user_cache_startup_storage(void)
+{
+ bool result;
+
+ if (user_cache_startup_storage_is_complete()) {
+ return true;
+ }
+
+ user_cache_startup_storage_lock();
+ result = user_cache_startup_storage_impl();
+ user_cache_startup_storage_unlock();
+
+ return result;
+}
+
+static void user_cache_resolve_runtime(void)
+{
+ php_user_cache_context *ctx = php_user_cache_active_context();
+ php_user_cache_runtime *runtime;
+ php_user_cache_storage *storage = &ctx->storage;
+
+ php_user_cache_reset_runtime();
+
+ runtime = php_user_cache_active_runtime();
+ if (!runtime->enabled) {
+ return;
+ }
+
+ if (UC_G(request_unavailable_reason) != PHP_USER_CACHE_REASON_NONE) {
+ user_cache_set_unavailable(UC_G(request_unavailable_reason));
+
+ return;
+ }
+
+ if (!user_cache_is_opted_in()) {
+ user_cache_set_unavailable(PHP_USER_CACHE_REASON_SAPI_NOT_ENABLED);
+
+ return;
+ }
+
+ if (user_cache_cache_is_disabled_for_sapi()) {
+ user_cache_set_unavailable(PHP_USER_CACHE_REASON_DISABLED_BY_INI);
+
+ return;
+ }
+
+ if (!storage->initialized &&
+ user_cache_requires_pre_request_storage()
+ ) {
+ user_cache_set_unavailable(PHP_USER_CACHE_REASON_BACKEND_NOT_INITIALIZED_BEFORE_WORKER);
+
+ return;
+ }
+
+ if (!user_cache_startup_storage()) {
+ user_cache_set_unavailable(PHP_USER_CACHE_REASON_SHM_INIT_FAILED);
+
+ return;
+ }
+
+ if (user_cache_requires_pre_request_storage() &&
+ !storage->initialized_before_request
+ ) {
+ user_cache_set_unavailable(PHP_USER_CACHE_REASON_BACKEND_INITIALIZED_AFTER_WORKER);
+
+ return;
+ }
+
+ user_cache_set_available();
+}
+
+#ifdef PHP_USER_CACHE_HAVE_OPTIMISTIC
+static bool user_cache_reader_slot_owner_is_dead(const php_user_cache_reader_slot *slot)
+{
+ /* Reader slots are claimed lock-free. */
+ uint64_t owner_pid = php_user_cache_atomic_load_64(&slot->owner_pid),
+ owner_start_time = php_user_cache_atomic_load_64(&slot->owner_start_time)
+ ;
+
+ return user_cache_entry_lock_owner_is_dead(owner_pid, owner_start_time);
+}
+
+#ifndef ZEND_WIN32
+static void user_cache_optimistic_atfork_child(void)
+{
+ UC_G(reader_claim_count) = 0;
+}
+#endif
+
+static int32_t user_cache_claim_reader_slot(php_user_cache_header *header)
+{
+ php_user_cache_reader_slot *slot;
+ uint64_t my_pid, expected;
+ uint32_t i;
+
+ for (i = 0; i < UC_G(reader_claim_count); i++) {
+ if (UC_G(reader_claims)[i].header == header) {
+ return (int32_t) UC_G(reader_claims)[i].slot_index;
+ }
+ }
+
+ if (UC_G(reader_claim_count) == PHP_USER_CACHE_READER_CLAIM_MAX ||
+ php_user_cache_reader_incarnation == 0
+ ) {
+ return -1;
+ }
+
+ my_pid = php_user_cache_cached_pid();
+
+ for (i = 0; i < PHP_USER_CACHE_READER_SLOTS; i++) {
+ slot = &header->reader_slots[i];
+
+ expected = php_user_cache_atomic_load_64(&slot->owner_pid);
+
+ if (expected != 0) {
+ if (!user_cache_reader_slot_owner_is_dead(slot) ||
+ user_cache_atomic_load_32(&slot->active) != 0
+ ) {
+ continue;
+ }
+
+ /* Clear start time before pid so scanners cannot combine owners. */
+ user_cache_atomic_store_64(&slot->owner_start_time, 0);
+ if (!user_cache_atomic_cas_64(&slot->owner_pid, expected, 0)) {
+ continue;
+ }
+ }
+
+ if (user_cache_atomic_cas_64(&slot->owner_pid, 0, my_pid)) {
+ user_cache_atomic_store_64(&slot->owner_start_time, user_cache_cached_self_start_time_token(my_pid));
+
+ goto found;
+ }
+ }
+
+ return -1;
+
+found:
+ UC_G(reader_claims)[UC_G(reader_claim_count)].header = header;
+ UC_G(reader_claims)[UC_G(reader_claim_count)].slot_index = i;
+ UC_G(reader_claim_count)++;
+
+ return (int32_t) i;
+}
+
+bool php_user_cache_quiesce_graph_payloads_locked(void)
+{
+ php_user_cache_header *header;
+ php_user_cache_reader_slot *slot;
+ uint64_t waited_us = 0;
+ uint32_t i, count, spin = 0;
+
+ if (!UC_G(write_seq_bumped)) {
+ return true;
+ }
+
+ if (UC_G(reader_drain_state) != 0) {
+ return UC_G(reader_drain_state) > 0;
+ }
+
+ header = php_user_cache_header_ptr();
+ if (header == NULL) {
+ return true;
+ }
+
+ user_cache_atomic_fence_seq_cst();
+
+ for (;;) {
+ count = 0;
+ for (i = 0; i < PHP_USER_CACHE_READER_SLOTS; i++) {
+ if (user_cache_atomic_load_32(&header->reader_slots[i].active) != 0) {
+ count++;
+ }
+ }
+
+ if (count == 0) {
+ UC_G(reader_drain_state) = 1;
+
+ return true;
+ }
+
+ spin++;
+
+ if ((spin & 0xFFU) == 0) {
+ for (i = 0; i < PHP_USER_CACHE_READER_SLOTS; i++) {
+ slot = &header->reader_slots[i];
+
+ if (user_cache_atomic_load_32(&slot->active) == 0 ||
+ php_user_cache_atomic_load_64(&slot->owner_pid) == 0
+ ) {
+ continue;
+ }
+
+ if (user_cache_reader_slot_owner_is_dead(slot)) {
+ user_cache_atomic_cas_32(&slot->active, 1, 0);
+ }
+ }
+ }
+
+ if (spin > PHP_USER_CACHE_READER_DRAIN_SPIN) {
+#ifdef ZEND_WIN32
+ Sleep(1);
+ waited_us += 1000;
+#else
+ usleep(50);
+ waited_us += 50;
+#endif
+
+ if (waited_us > PHP_USER_CACHE_READER_DRAIN_TIMEOUT_US) {
+ UC_G(reader_drain_state) = -1;
+
+ return false;
+ }
+ }
+ }
+}
+
+void php_user_cache_optimistic_fork_setup(void)
+{
+ static bool registered = false;
+
+ if (php_user_cache_reader_incarnation == 0) {
+ php_user_cache_reader_incarnation =
+ ((uint64_t) time(NULL) << 20) ^
+ (uint64_t) (uintptr_t) &php_user_cache_reader_incarnation ^
+ php_user_cache_current_pid()
+ ;
+
+ if (php_user_cache_reader_incarnation == 0) {
+ php_user_cache_reader_incarnation = 1;
+ }
+ }
+
+ if (registered) {
+ return;
+ }
+
+#ifndef ZEND_WIN32
+ if (pthread_atfork(NULL, NULL, user_cache_optimistic_atfork_child) != 0) {
+ return;
+ }
+#endif
+
+ registered = true;
+}
+
+bool php_user_cache_optimistic_reader_begin(php_user_cache_header *header, uint32_t *slot_idx_ptr)
+{
+ int32_t slot_idx = user_cache_claim_reader_slot(header);
+
+ if (slot_idx < 0) {
+ return false;
+ }
+
+ *slot_idx_ptr = (uint32_t) slot_idx;
+
+ user_cache_atomic_store_32(&header->reader_slots[slot_idx].active, 1);
+ user_cache_atomic_fence_seq_cst();
+
+ return true;
+}
+
+void php_user_cache_optimistic_reader_end(php_user_cache_header *header, uint32_t slot_idx)
+{
+ user_cache_atomic_store_32(&header->reader_slots[slot_idx].active, 0);
+}
+
+#ifdef ZTS
+static bool user_cache_reader_claim_header_is_attached(const php_user_cache_header *header)
+{
+ const php_user_cache_partition *partition;
+
+ if (user_cache_storage_holds_header(&php_user_cache_context_state.storage, header)) {
+ return true;
+ }
+
+ for (partition = php_user_cache_partitions; partition != NULL; partition = partition->next) {
+ if (user_cache_storage_holds_header(&partition->context.storage, header)) {
+ return true;
+ }
+ }
+
+ return false;
+}
+
+/* Release per-thread reader slots before their live process pid makes them
+ * permanently unclaimable. Clear active and start time before owner_pid. */
+void php_user_cache_release_thread_reader_claims(php_user_cache_globals *globals)
+{
+ php_user_cache_reader_slot *slot;
+ uint64_t my_pid;
+ uint32_t i;
+
+ if (globals->reader_claim_count == 0) {
+ return;
+ }
+
+ my_pid = php_user_cache_cached_pid();
+
+ for (i = 0; i < globals->reader_claim_count; i++) {
+ if (!user_cache_reader_claim_header_is_attached(globals->reader_claims[i].header)) {
+ continue;
+ }
+
+ slot = &globals->reader_claims[i].header->reader_slots[globals->reader_claims[i].slot_index];
+
+ user_cache_atomic_store_32(&slot->active, 0);
+ user_cache_atomic_store_64(&slot->owner_start_time, 0);
+ user_cache_atomic_cas_64(&slot->owner_pid, my_pid, 0);
+ }
+
+ globals->reader_claim_count = 0;
+}
+#endif
+#else
+bool php_user_cache_quiesce_graph_payloads_locked(void)
+{
+ return true;
+}
+
+void php_user_cache_optimistic_fork_setup(void)
+{
+}
+
+bool php_user_cache_optimistic_reader_begin(php_user_cache_header *header, uint32_t *slot_idx_ptr)
+{
+ (void) header;
+ (void) slot_idx_ptr;
+
+ return false;
+}
+
+void php_user_cache_optimistic_reader_end(php_user_cache_header *header, uint32_t slot_idx)
+{
+ (void) header;
+ (void) slot_idx;
+}
+#endif
+
+void php_user_cache_reset_runtime(void)
+{
+ php_user_cache_runtime *runtime = php_user_cache_active_runtime();
+
+ UC_G(runtime_resolved) = false;
+
+ memset(runtime, 0, sizeof(*runtime));
+
+ runtime->configured_memory = UC_G(shm_size);
+
+ runtime->enabled = runtime->configured_memory != 0;
+ if (!runtime->enabled) {
+ runtime->available = false;
+ runtime->failure_reason = PHP_USER_CACHE_REASON_DISABLED_BY_INI;
+ }
+}
+
+void php_user_cache_reset_storage(void)
+{
+ php_user_cache_storage *storage = &php_user_cache_active_context()->storage;
+
+ memset(storage, 0, sizeof(*storage));
+
+ storage->lock_file = -1;
+}
+
+bool php_user_cache_header_init_locked(void)
+{
+ php_user_cache_storage *storage = &php_user_cache_active_context()->storage;
+ php_user_cache_header *header = php_user_cache_header_ptr();
+ uint32_t capacity, data_offset;
+
+ if (!header) {
+ return false;
+ }
+
+ /* Cache the layout for the lifetime of the attachment. */
+ if (!storage->layout_memo_valid) {
+ storage->capacity_memo = user_cache_calculate_capacity(storage->size);
+ storage->data_offset_memo = (uint32_t) PHP_USER_CACHE_ALIGNED_SIZE(
+ sizeof(php_user_cache_header) + storage->capacity_memo * sizeof(php_user_cache_entry)
+ );
+ storage->layout_memo_valid = true;
+ }
+ capacity = storage->capacity_memo;
+ data_offset = storage->data_offset_memo;
+
+ if (header->magic == PHP_USER_CACHE_MAGIC && header->version == PHP_USER_CACHE_VERSION) {
+ /* Reformat segments whose compile-time layout differs. */
+ if (header->capacity == capacity && header->data_offset == data_offset) {
+#ifdef PHP_USER_CACHE_HAVE_BOUNDARY_SHM
+ return user_cache_header_boundary_identity_matches_locked(header, storage->size);
+#else
+ return true;
+#endif
+ }
+ }
+
+ return user_cache_header_format_fresh_locked(header, capacity, data_offset);
+}
+
+void php_user_cache_free_locked(uint32_t payload_offset)
+{
+ php_user_cache_header *header = php_user_cache_header_ptr();
+ php_user_cache_block *block, *adjacent;
+ uint32_t block_offset, original_block_offset, next_offset, prev_offset;
+
+ if (!header || payload_offset < sizeof(php_user_cache_block)) {
+ return;
+ }
+
+ block_offset = payload_offset - (uint32_t) sizeof(php_user_cache_block);
+ original_block_offset = block_offset;
+ block = php_user_cache_block_ptr(block_offset);
+ if (php_user_cache_block_is_free(block)) {
+ return;
+ }
+
+ user_cache_block_mark_free(block);
+
+ next_offset = block_offset + block->size;
+ if (next_offset < user_cache_used_end_offset_locked(header)) {
+ adjacent = php_user_cache_block_ptr(next_offset);
+
+ if (php_user_cache_block_is_free(adjacent)) {
+ 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 = php_user_cache_block_ptr(prev_offset);
+ if (php_user_cache_block_is_free(adjacent)) {
+ 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;
+ }
+ }
+ }
+
+ user_cache_update_following_prev_size_locked(header, block_offset, block);
+ user_cache_free_list_insert_locked(header, block_offset);
+ user_cache_trim_tail_free_blocks_locked(header, block_offset);
+}
+
+uint32_t php_user_cache_alloc_locked(size_t size, const void *src)
+{
+ php_user_cache_header *header = php_user_cache_header_ptr();
+ uint32_t total_size, offset;
+ size_t aligned_size;
+
+ if (!header || size == 0 || size > UINT32_MAX - sizeof(php_user_cache_block)) {
+ return 0;
+ }
+
+ aligned_size = PHP_USER_CACHE_ALIGNED_SIZE(sizeof(php_user_cache_block) + size);
+ if (aligned_size > UINT32_MAX) {
+ return 0;
+ }
+
+ total_size = (uint32_t) aligned_size;
+
+ offset = user_cache_alloc_from_free_list_locked(header, size, total_size, src);
+ if (offset != 0) {
+ return offset;
+ }
+
+ return user_cache_alloc_from_tail_locked(header, size, total_size, src);
+}
+
+bool php_user_cache_startup_storage_before_request(void)
+{
+ php_user_cache_storage *storage = &php_user_cache_active_context()->storage;
+
+ if (user_cache_startup_failure_forced()) {
+ user_cache_set_unavailable(PHP_USER_CACHE_REASON_SHM_INIT_FAILED);
+
+ return false;
+ }
+
+ if (!user_cache_is_opted_in()) {
+ user_cache_set_unavailable(PHP_USER_CACHE_REASON_SAPI_NOT_ENABLED);
+
+ return true;
+ }
+
+ if (!user_cache_startup_storage()) {
+ user_cache_set_unavailable(PHP_USER_CACHE_REASON_SHM_INIT_FAILED);
+
+ return false;
+ }
+
+ storage->initialized_before_request = true;
+
+ return true;
+}
+
+void php_user_cache_shutdown_storage(void)
+{
+ php_user_cache_storage *storage = &php_user_cache_active_context()->storage;
+
+ user_cache_destroy_lock();
+ user_cache_cleanup_segments(
+ storage->handler,
+ storage->segments,
+ storage->segment_count
+ );
+ php_user_cache_reset_storage();
+}
+
+void php_user_cache_ensure_ready_slow(void)
+{
+ php_user_cache_context *ctx = php_user_cache_active_context();
+
+ user_cache_resolve_runtime();
+
+ UC_G(runtime_resolved) = true;
+ UC_G(runtime_resolved_context) = ctx;
+ UC_G(runtime_resolved_enabled) = UC_G(enable);
+}
+
+bool php_user_cache_rlock(void)
+{
+ php_user_cache_header *header;
+ uint32_t recovery_attempts = 0;
+
+ for (;;) {
+ if (!user_cache_rlock_impl()) {
+ return false;
+ }
+
+ UC_G(lock_held_is_write) = false;
+
+ header = php_user_cache_header_ptr();
+ if (!UNEXPECTED(
+ header != NULL &&
+ php_user_cache_header_is_initialized_locked() &&
+ (php_user_cache_seq_load(&header->write_seq) & 1) != 0
+ )
+ ) {
+ return true;
+ }
+
+ php_user_cache_unlock();
+
+ if (++recovery_attempts > 8) {
+ return false;
+ }
+
+ if (!php_user_cache_wlock()) {
+ return false;
+ }
+
+ php_user_cache_unlock();
+ }
+}
+
+bool php_user_cache_wlock(void)
+{
+ if (!user_cache_wlock_impl()) {
+ return false;
+ }
+
+ UC_G(lock_held_is_write) = true;
+
+ user_cache_recover_after_owner_death();
+
+ user_cache_write_section_enter();
+
+ return true;
+}
+
+bool php_user_cache_wlock_for_entry_mutation(zend_string *key)
+{
+ php_user_cache_entry_lock *local_lock;
+ php_user_cache_header *header;
+ zend_ulong hash = zend_string_hash_val(key);
+ uint64_t waited_us = 0;
+ uint32_t slot_idx;
+ bool found;
+
+ user_cache_ensure_entry_lock_owner();
+
+ for (;;) {
+ if (!php_user_cache_wlock()) {
+ return false;
+ }
+
+ if (!php_user_cache_header_init_locked()) {
+ php_user_cache_unlock();
+
+ return false;
+ }
+
+ header = php_user_cache_header_ptr();
+ if (!user_cache_find_entry_lock_record_slot_locked(header, key, hash, &slot_idx, &found) || !found) {
+ return true;
+ }
+
+ local_lock = user_cache_find_local_entry_lock(php_user_cache_active_context(), key);
+ if (local_lock != NULL &&
+ header->entry_lock_records[slot_idx].owner_pid == local_lock->owner_pid
+ ) {
+ return true;
+ }
+
+ php_user_cache_unlock();
+
+ if (waited_us >= PHP_USER_CACHE_ENTRY_LOCK_WAIT_TIMEOUT_US) {
+ return false;
+ }
+
+ waited_us += user_cache_sleep_entry_lock_retry_interval();
+ }
+}
+
+void php_user_cache_unlock(void)
+{
+ if (UC_G(lock_held_is_write)) {
+ user_cache_write_section_leave();
+ }
+
+ UC_G(lock_held_is_write) = false;
+
+ user_cache_unlock_impl();
+}
+
+void php_user_cache_unlock_if_held(void)
+{
+ if (UC_G(lock_held)) {
+ php_user_cache_unlock();
+ }
+}
+
+bool php_user_cache_try_acquire_entry_lock(zend_string *key, zend_long lease)
+{
+ return user_cache_acquire_entry_lock_record(
+ php_user_cache_active_context(),
+ key,
+ lease,
+ true,
+ false
+ );
+}
+
+bool php_user_cache_acquire_entry_locks(zend_string **keys, bool *acquired, uint32_t count)
+{
+ return user_cache_acquire_entry_lock_records(
+ php_user_cache_active_context(),
+ keys,
+ acquired,
+ count
+ );
+}
+
+bool php_user_cache_release_entry_lock(zend_string *key)
+{
+ php_user_cache_context *ctx = php_user_cache_active_context();
+ php_user_cache_entry_lock *lock;
+ zend_ulong hash;
+ HashTable **locks_ptr = user_cache_entry_lock_table_ptr();
+
+ user_cache_ensure_entry_lock_owner();
+
+ lock = user_cache_find_local_entry_lock(ctx, key);
+ if (lock == NULL) {
+ return false;
+ }
+
+ hash = zend_string_hash_val(key);
+ if (!user_cache_remove_owned_entry_lock_record(ctx, key, hash, lock->owner_pid)) {
+ return false;
+ }
+
+ zend_hash_del(*locks_ptr, key);
+ user_cache_destroy_entry_locks_if_empty(locks_ptr);
+
+ return true;
+}
+
+void php_user_cache_release_entry_locks(zend_string **keys, const bool *acquired, uint32_t count)
+{
+ user_cache_release_entry_locks_for_keys(
+ php_user_cache_active_context(),
+ keys,
+ acquired,
+ count
+ );
+}
+
+bool php_user_cache_entry_locks_allow_clear_locked(void)
+{
+ php_user_cache_header *header = php_user_cache_header_ptr();
+ php_user_cache_entry_lock_record *record;
+ uint64_t owner_pid, now;
+ uint32_t i;
+
+ user_cache_ensure_entry_lock_owner();
+
+ if (header == NULL || !php_user_cache_header_is_initialized_locked()) {
+ return true;
+ }
+
+ owner_pid = php_user_cache_cached_pid();
+ now = (uint64_t) time(NULL);
+
+ for (i = 0; i < PHP_USER_CACHE_ENTRY_LOCK_TABLE_SIZE; i++) {
+ record = &header->entry_lock_records[i];
+ if (record->state != PHP_USER_CACHE_ENTRY_LOCK_USED) {
+ continue;
+ }
+
+ if (!user_cache_entry_lock_record_is_active_locked(record, now, true)) {
+ user_cache_remove_entry_lock_record_locked(record);
+ continue;
+ }
+
+ if (record->owner_pid != owner_pid) {
+ return false;
+ }
+ }
+
+ return true;
+}
+
+void php_user_cache_release_request_entry_locks(void)
+{
+ user_cache_release_entry_locks_all_contexts(
+ &UC_G(entry_lock_table),
+ PHP_USER_CACHE_ENTRY_LOCK_RELEASE_PRESERVE_LEASES
+ );
+#ifndef ZEND_WIN32
+ UC_G(entry_lock_owner_pid) = 0;
+#endif
+}
diff --git a/ext/user_cache/user_cache_storage_portability.h b/ext/user_cache/user_cache_storage_portability.h
new file mode 100644
index 000000000000..e48df067de76
--- /dev/null
+++ b/ext/user_cache/user_cache_storage_portability.h
@@ -0,0 +1,96 @@
+/*
+ +----------------------------------------------------------------------+
+ | 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 PHP_USER_CACHE_STORAGE_PORTABILITY_H
+#define PHP_USER_CACHE_STORAGE_PORTABILITY_H
+
+#include "user_cache_internal.h"
+
+#ifdef ZEND_WIN32
+# include "zend_execute.h"
+# include "zend_system_id.h"
+# include "win32/ioutil.h"
+
+# include
+# include
+# include
+#else
+# include
+# include
+# include
+# include
+# include
+# include
+# ifdef HAVE_UNISTD_H
+# include
+# endif
+# if defined(PHP_USER_CACHE_HAVE_ANON_MMAP) || \
+ defined(PHP_USER_CACHE_HAVE_BOUNDARY_SHM) || \
+ (defined(__linux__) && defined(HAVE_MEMFD_CREATE))
+# include
+# endif
+#endif
+
+#if defined(__APPLE__) || defined(__FreeBSD__)
+# include
+# ifdef __FreeBSD__
+# include
+# include
+# endif
+#endif
+
+#ifdef PHP_USER_CACHE_HAVE_ANON_MMAP
+# if defined(MAP_ANON) && !defined(MAP_ANONYMOUS)
+# define MAP_ANONYMOUS MAP_ANON
+# endif
+#endif
+
+#ifdef ZEND_WIN32
+# define PHP_USER_CACHE_WIN32_MAPPING_NAME "PhpUserCache.SharedMemoryArea"
+# define PHP_USER_CACHE_WIN32_MAPPING_MUTEX_NAME "PhpUserCache.SharedMemoryMutex"
+# define PHP_USER_CACHE_WIN32_LOCK_FILE_NAME "PhpUserCache.LockFile"
+#endif
+
+#define PHP_USER_CACHE_ENTRY_LOCK_RETRY_INTERVAL_US 10000U
+
+#ifdef ZTS
+# ifdef ZEND_WIN32
+# define PHP_USER_CACHE_STARTUP_LOCK_INITIALIZER SRWLOCK_INIT
+# else
+# define PHP_USER_CACHE_STARTUP_LOCK_INITIALIZER PTHREAD_MUTEX_INITIALIZER
+# endif
+#else
+# define PHP_USER_CACHE_STARTUP_LOCK_INITIALIZER 0
+#endif
+
+#ifdef ZEND_WIN32
+typedef struct _php_user_cache_win32_segment {
+ php_user_cache_shm_segment segment;
+ HANDLE memfile;
+ void *mapping_base;
+ size_t mapping_size;
+} php_user_cache_win32_segment;
+#endif
+
+#ifdef ZTS
+# ifdef ZEND_WIN32
+typedef SRWLOCK php_user_cache_startup_lock;
+# else
+typedef pthread_mutex_t php_user_cache_startup_lock;
+# endif
+#else
+typedef char php_user_cache_startup_lock;
+#endif
+
+#endif /* PHP_USER_CACHE_STORAGE_PORTABILITY_H */
diff --git a/main/main.c b/main/main.c
index 9e77c99a45df..9fcd9a10929d 100644
--- a/main/main.c
+++ b/main/main.c
@@ -52,6 +52,7 @@
#include "ext/random/php_random_csprng.h"
#include "ext/random/php_random_zend_utils.h"
#include "ext/opcache/ZendAccelerator.h"
+#include "ext/user_cache/php_user_cache.h"
#ifdef HAVE_JIT
# include "ext/opcache/jit/zend_jit.h"
#endif
@@ -2828,6 +2829,7 @@ PHPAPI void php_reserve_tsrm_memory(void)
TSRM_ALIGNED_SIZE(sizeof(php_core_globals)) +
TSRM_ALIGNED_SIZE(sizeof(sapi_globals_struct)) +
TSRM_ALIGNED_SIZE(sizeof(zend_accel_globals)) +
+ TSRM_ALIGNED_SIZE(php_user_cache_globals_size()) +
#ifdef HAVE_JIT
TSRM_ALIGNED_SIZE(sizeof(zend_jit_globals)) +
#endif
@@ -2845,6 +2847,9 @@ PHPAPI bool php_tsrm_startup_ex(int expected_threads)
TSRM_ALIGNED_SIZE(sizeof(zend_compiler_globals)) +
TSRM_ALIGNED_SIZE(sizeof(zend_executor_globals)));
(void)ts_resource(0);
+ /* Allocated here rather than from the OPcache module wiring: SAPI
+ * activate hooks consume these globals before the module MINIT runs. */
+ php_user_cache_globals_startup();
return ret;
}
diff --git a/sapi/apache2handler/sapi_apache2.c b/sapi/apache2handler/sapi_apache2.c
index 83b3f02fb743..9bb58d7adda0 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/user_cache/php_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;
+ php_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;
}
+/* Linear over the vhost list, run once per request. */
+static php_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 *cur;
+ unsigned int i;
+
+ /* The partition entries are pool-allocated (freed with pconf); the
+ * php_user_cache_partition objects created below are owned by
+ * OPcache and released together in its MSHUTDOWN (partitions_shutdown),
+ * so no cgi/lsapi-style SAPI shutdown hook is required here. */
+ php_apache_user_cache_partitions = NULL;
+ php_user_cache_opt_in();
+
+ i = 0;
+ for (cur = server; cur != NULL; cur = cur->next, i++) {
+ hostname = cur->server_hostname != NULL ? cur->server_hostname : "default";
+ partition_name = apr_psprintf(
+ pconf,
+ "apache2handler:%u:%s:%u",
+ i,
+ hostname,
+ (unsigned int) cur->port
+ );
+
+ entry = apr_pcalloc(pconf, sizeof(*entry));
+ entry->server = cur;
+ entry->partition = php_user_cache_partition_create(partition_name);
+ if (entry->partition == NULL) {
+ ap_log_error(APLOG_MARK, APLOG_WARNING, 0, cur, "Unable to allocate OPcache User Cache partition");
+ continue;
+ }
+
+ if (!php_user_cache_partition_startup_storage(entry->partition)) {
+ ap_log_error(APLOG_MARK, APLOG_WARNING, 0, cur, "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();
+ php_user_cache_partition_activate(php_apache_user_cache_partition_for_server(r->server));
+
+ if (php_request_startup() == FAILURE) {
+ php_user_cache_partition_activate(NULL);
+
+ return FAILURE;
+ }
+
+ return SUCCESS;
}
static void php_apache_request_dtor(request_rec *r)
{
php_request_shutdown(NULL);
+ php_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 7efd8f159d59..06310bab17b7 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/user_cache/php_user_cache.h"
#ifdef PHP_WIN32
# include
@@ -778,6 +779,41 @@ static void sapi_cgi_log_message(const char *message, int syslog_type_int)
}
}
+static const char *cgi_user_cache_getenv(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 void cgi_user_cache_log_message(const char *message)
+{
+ sapi_cgi_log_message(message, 0);
+}
+
+static void cgi_user_cache_activate_request_partition(void)
+{
+ php_user_cache_activate_boundary_partition(
+ "cgi-fcgi",
+ cgi_user_cache_getenv,
+ cgi_user_cache_log_message,
+ PHP_USER_CACHE_REASON_CGI_BOUNDARY_UNAVAILABLE
+ );
+}
+
/* {{{ 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)
{
@@ -860,6 +896,11 @@ static void php_cgi_ini_activate_user_config(char *path, size_t path_len, const
static int sapi_cgi_activate(void)
{
+ /* Resolve the user-cache partition before any early return below: the
+ * activate return value is ignored by sapi_activate(), so bailing out
+ * first would leave the request without a partition. */
+ cgi_user_cache_activate_request_partition();
+
/* PATH_TRANSLATED should be defined at this stage but better safe than sorry :) */
if (!SG(request_info).path_translated) {
return FAILURE;
@@ -963,12 +1004,20 @@ static int sapi_cgi_deactivate(void)
sapi_cgi_flush(SG(server_context));
}
}
+ php_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;
+ }
+
+ php_user_cache_opt_in();
+
+ return SUCCESS;
}
/* {{{ sapi_module_struct cgi_sapi_module */
@@ -1547,6 +1596,7 @@ static PHP_MINIT_FUNCTION(cgi)
static PHP_MSHUTDOWN_FUNCTION(cgi)
{
zend_hash_destroy(&CGIG(user_config_cache));
+ php_user_cache_boundary_partitions_shutdown();
UNREGISTER_INI_ENTRIES();
return SUCCESS;
diff --git a/sapi/cli/php_cli.c b/sapi/cli/php_cli.c
index e5e573d1533c..205a063d2c58 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/user_cache/php_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) {
+ php_user_cache_opt_in();
+ }
+
+ return result;
}
/* }}} */
diff --git a/sapi/cli/php_cli_server.c b/sapi/cli/php_cli_server.c
index 797979b67305..92c43a9ec9e9 100644
--- a/sapi/cli/php_cli_server.c
+++ b/sapi/cli/php_cli_server.c
@@ -44,6 +44,8 @@
#include
+#include "ext/user_cache/php_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) {
+ php_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 (!php_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/config.m4 b/sapi/fpm/config.m4
index 89c53a0c4d28..3f7590337b35 100644
--- a/sapi/fpm/config.m4
+++ b/sapi/fpm/config.m4
@@ -462,6 +462,7 @@ if test "$PHP_FPM" != "no"; then
fpm/fpm_status.c \
fpm/fpm_stdio.c \
fpm/fpm_unix.c \
+ fpm/fpm_user_cache.c \
fpm/fpm_worker_pool.c \
fpm/zlog.c \
fpm/events/select.c \
diff --git a/sapi/fpm/fpm/fpm.c b/sapi/fpm/fpm/fpm.c
index cb86f403b89c..eec526874abd 100644
--- a/sapi/fpm/fpm/fpm.c
+++ b/sapi/fpm/fpm/fpm.c
@@ -19,6 +19,7 @@
#include "fpm_scoreboard.h"
#include "fpm_stdio.h"
#include "fpm_log.h"
+#include "fpm_user_cache.h"
#include "zlog.h"
struct fpm_globals_s fpm_globals = {
@@ -64,6 +65,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..8c02ea5d5d7a 100644
--- a/sapi/fpm/fpm/fpm_children.c
+++ b/sapi/fpm/fpm/fpm_children.c
@@ -29,6 +29,8 @@
#include "zlog.h"
+#include "ext/user_cache/php_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);
+ php_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_user_cache.c b/sapi/fpm/fpm/fpm_user_cache.c
new file mode 100644
index 000000000000..240b3499047e
--- /dev/null
+++ b/sapi/fpm/fpm/fpm_user_cache.c
@@ -0,0 +1,43 @@
+/*
+ +----------------------------------------------------------------------+
+ | 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 "fpm_config.h"
+
+#include "fpm.h"
+#include "fpm_worker_pool.h"
+#include "fpm_user_cache.h"
+#include "zlog.h"
+
+#include "ext/user_cache/php_user_cache.h"
+
+int fpm_user_cache_init_main(void)
+{
+ struct fpm_worker_pool_s *wp;
+
+ php_user_cache_opt_in();
+
+ for (wp = fpm_worker_all_pools; wp; wp = wp->next) {
+ wp->user_cache_partition = php_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 (!php_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;
+}
diff --git a/sapi/fpm/fpm/fpm_user_cache.h b/sapi/fpm/fpm/fpm_user_cache.h
new file mode 100644
index 000000000000..bae3d9e0432a
--- /dev/null
+++ b/sapi/fpm/fpm/fpm_user_cache.h
@@ -0,0 +1,20 @@
+/*
+ +----------------------------------------------------------------------+
+ | 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 FPM_USER_CACHE_H
+# define FPM_USER_CACHE_H
+
+int fpm_user_cache_init_main(void);
+
+#endif /* FPM_USER_CACHE_H */
diff --git a/sapi/fpm/fpm/fpm_worker_pool.h b/sapi/fpm/fpm/fpm_worker_pool.h
index efb8640cd32f..07a05b45b7c5 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/user_cache/php_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;
+ php_user_cache_partition *user_cache_partition;
/* for ondemand PM */
struct fpm_event_s *ondemand_event;
diff --git a/sapi/fpm/tests/tester.inc b/sapi/fpm/tests/tester.inc
index 907988654337..3856f8b1612f 100644
--- a/sapi/fpm/tests/tester.inc
+++ b/sapi/fpm/tests/tester.inc
@@ -548,7 +548,7 @@ class Tester
$this->masterProcess = proc_open($cmd, $desc, $pipes, null, $envVars);
register_shutdown_function(
- function ($masterProcess) use ($configFile) {
+ static function ($masterProcess) use ($configFile) {
@unlink($configFile);
if (is_resource($masterProcess)) {
@proc_terminate($masterProcess);
diff --git a/sapi/litespeed/lsapi_main.c b/sapi/litespeed/lsapi_main.c
index e7aec268489b..becf3f10410d 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/user_cache/php_user_cache.h"
+
#include
#include
@@ -63,6 +65,7 @@ typedef struct _user_config_cache_entry {
time_t expires;
HashTable user_config;
} user_config_cache_entry;
+
static HashTable user_config_cache;
static int lsapi_mode = 0;
@@ -176,6 +179,8 @@ static int sapi_lsapi_deactivate(void)
SG(request_info).path_translated = NULL;
}
+ php_user_cache_partition_activate(NULL);
+
return SUCCESS;
}
/* }}} */
@@ -513,6 +518,26 @@ static void sapi_lsapi_log_message(const char *message, int syslog_type_int)
}
/* }}} */
+static void lsapi_user_cache_log_message(const char *message)
+{
+ sapi_lsapi_log_message(message, 0);
+}
+
+static const char *lsapi_user_cache_get_boundary_value(const char *name)
+{
+ return sapi_lsapi_getenv(name, 0);
+}
+
+static void lsapi_user_cache_activate_request_partition(void)
+{
+ php_user_cache_activate_boundary_partition(
+ "litespeed",
+ lsapi_user_cache_get_boundary_value,
+ lsapi_user_cache_log_message,
+ PHP_USER_CACHE_REASON_LSAPI_BOUNDARY_UNAVAILABLE
+ );
+}
+
/* Set to 1 to turn on log messages useful during development:
*/
#if 0
@@ -541,6 +566,11 @@ static int sapi_lsapi_activate(void)
char *path, *server_name;
size_t path_len, server_name_len;
+ /* Resolve the user-cache partition before any early return below: the
+ * activate return value is ignored by sapi_activate(), so bailing out
+ * first would leave the request without a partition. */
+ lsapi_user_cache_activate_request_partition();
+
/* PATH_TRANSLATED should be defined at this stage but better safe than sorry :) */
if (!SG(request_info).path_translated) {
return FAILURE;
@@ -1517,6 +1547,8 @@ int main( int argc, char * argv[] )
return FAILURE;
}
+ php_user_cache_opt_in();
+
if ( climode ) {
return cli_main(argc, argv);
}
@@ -1627,6 +1659,7 @@ static PHP_MINIT_FUNCTION(litespeed)
static PHP_MSHUTDOWN_FUNCTION(litespeed)
{
zend_hash_destroy(&user_config_cache);
+ php_user_cache_boundary_partitions_shutdown();
/* UNREGISTER_INI_ENTRIES(); */
return SUCCESS;
diff --git a/sapi/phpdbg/phpdbg.c b/sapi/phpdbg/phpdbg.c
index 3c0d5e836dd0..aa325233a7a6 100644
--- a/sapi/phpdbg/phpdbg.c
+++ b/sapi/phpdbg/phpdbg.c
@@ -32,6 +32,8 @@
#include "ext/standard/basic_functions.h"
+#include "ext/user_cache/php_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;
}
+ php_user_cache_opt_in();
+
phpdbg_booted = 1;
return SUCCESS;