From 8fc858408afb215b31fc44895a712b809e3311c2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Heath=20Dutton=F0=9F=95=B4=EF=B8=8F?= Date: Tue, 16 Dec 2025 18:37:39 -0500 Subject: [PATCH] Fix GH-20503: Assertion failure with ext/date DateInterval property hash When a DateInterval object has a circular reference (e.g., $obj->prop = $obj), calling json_encode() triggered an assertion failure because the get_properties handler modified a HashTable with refcount > 1. Added a get_properties_for handler for DateInterval that duplicates the properties array before modification, matching the pattern used by DateTime and DateTimeZone. --- ext/date/php_date.c | 29 +++++++++++++++++++++++++++++ ext/date/tests/bug-gh20503.phpt | 20 ++++++++++++++++++++ 2 files changed, 49 insertions(+) create mode 100644 ext/date/tests/bug-gh20503.phpt diff --git a/ext/date/php_date.c b/ext/date/php_date.c index 84fad72948fd4..1ed70f8589b02 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; @@ -2240,6 +2242,33 @@ static HashTable *date_object_get_properties_interval(zend_object *object) /* {{ 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; +} /* }}} */ + static zend_object *date_object_new_period(zend_class_entry *class_type) /* {{{ */ { php_period_obj *intern = zend_object_alloc(sizeof(php_period_obj), class_type); diff --git a/ext/date/tests/bug-gh20503.phpt b/ext/date/tests/bug-gh20503.phpt new file mode 100644 index 0000000000000..218c033329015 --- /dev/null +++ b/ext/date/tests/bug-gh20503.phpt @@ -0,0 +1,20 @@ +--TEST-- +GH-20503 (Assertion failure with ext/date DateInterval property hash construction) +--FILE-- +prop3 = $obj; + +// Array cast triggers get_properties_for with ARRAY_CAST purpose +// This would previously cause an assertion failure when modifying +// a HashTable with refcount > 1 +$props = (array) $obj; +var_dump(count($props)); +var_dump(isset($props['prop3'])); +var_dump($props['y']); +?> +--EXPECTF-- +Deprecated: Creation of dynamic property DateInterval::$prop3 is deprecated in %s on line %d +int(11) +bool(true) +int(0)