diff --git a/ext/date/php_date.c b/ext/date/php_date.c index 84fad72948fd4..13c24de305522 100644 --- a/ext/date/php_date.c +++ b/ext/date/php_date.c @@ -352,6 +352,7 @@ static HashTable *date_object_get_gc(zend_object *object, zval **table, int *n); static HashTable *date_object_get_properties_for(zend_object *object, zend_prop_purpose purpose); static HashTable *date_object_get_gc_interval(zend_object *object, zval **table, int *n); static HashTable *date_object_get_properties_interval(zend_object *object); +static HashTable *date_object_get_properties_for_interval(zend_object *object, zend_prop_purpose purpose); static HashTable *date_object_get_gc_period(zend_object *object, zval **table, int *n); static HashTable *date_object_get_properties_for_timezone(zend_object *object, zend_prop_purpose purpose); static HashTable *date_object_get_gc_timezone(zend_object *object, zval **table, int *n); @@ -1816,6 +1817,7 @@ static void date_register_classes(void) /* {{{ */ date_object_handlers_interval.read_property = date_interval_read_property; date_object_handlers_interval.write_property = date_interval_write_property; date_object_handlers_interval.get_properties = date_object_get_properties_interval; + date_object_handlers_interval.get_properties_for = date_object_get_properties_for_interval; date_object_handlers_interval.get_property_ptr_ptr = date_interval_get_property_ptr_ptr; date_object_handlers_interval.get_gc = date_object_get_gc_interval; date_object_handlers_interval.compare = date_interval_compare_objects; @@ -2235,6 +2237,40 @@ static HashTable *date_object_get_properties_interval(zend_object *object) /* {{ return props; } + /* Duplicate the table if it has refcount > 1 to avoid modifying shared data. + * This can happen with circular references (e.g., $obj->prop = $obj). */ + if (GC_REFCOUNT(props) > 1) { + props = zend_array_dup(props); + } + + date_interval_object_to_hash(intervalobj, props); + + return props; +} /* }}} */ + +static HashTable *date_object_get_properties_for_interval(zend_object *object, zend_prop_purpose purpose) /* {{{ */ +{ + HashTable *props; + php_interval_obj *intervalobj; + + switch (purpose) { + case ZEND_PROP_PURPOSE_DEBUG: + case ZEND_PROP_PURPOSE_SERIALIZE: + case ZEND_PROP_PURPOSE_VAR_EXPORT: + case ZEND_PROP_PURPOSE_JSON: + case ZEND_PROP_PURPOSE_ARRAY_CAST: + break; + default: + return zend_std_get_properties_for(object, purpose); + } + + intervalobj = php_interval_obj_from_obj(object); + props = zend_array_dup(zend_std_get_properties(object)); + + if (!intervalobj->initialized) { + return props; + } + date_interval_object_to_hash(intervalobj, props); return props; diff --git a/ext/date/tests/bug-gh20503.phpt b/ext/date/tests/bug-gh20503.phpt new file mode 100644 index 0000000000000..037b5d93bfe4e --- /dev/null +++ b/ext/date/tests/bug-gh20503.phpt @@ -0,0 +1,25 @@ +--TEST-- +GH-20503 (Assertion failure with DateInterval and json_encode on circular reference) +--FILE-- +circular = $obj; + +// json_encode with circular reference previously caused an assertion failure +// in debug builds when modifying a HashTable with refcount > 1 +$result = json_encode($obj); +var_dump($result === false); +// Error can be either JSON_ERROR_RECURSION or JSON_ERROR_DEPTH depending on detection method +var_dump(json_last_error() === JSON_ERROR_RECURSION || json_last_error() === JSON_ERROR_DEPTH); + +// Also verify array cast works +$props = (array) $obj; +var_dump(count($props) > 0); +var_dump(isset($props['circular'])); +?> +--EXPECTF-- +Deprecated: Creation of dynamic property DateInterval::$circular is deprecated in %s on line %d +bool(true) +bool(true) +bool(true) +bool(true)