Skip to content

Commit 6ec27cd

Browse files
committed
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. Fixed by duplicating the properties HashTable when its refcount is greater than 1 before modifying it.
1 parent fb1ec9a commit 6ec27cd

File tree

3 files changed

+74
-0
lines changed

3 files changed

+74
-0
lines changed

ext/date/php_date.c

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -352,6 +352,7 @@ static HashTable *date_object_get_gc(zend_object *object, zval **table, int *n);
352352
static HashTable *date_object_get_properties_for(zend_object *object, zend_prop_purpose purpose);
353353
static HashTable *date_object_get_gc_interval(zend_object *object, zval **table, int *n);
354354
static HashTable *date_object_get_properties_interval(zend_object *object);
355+
static HashTable *date_object_get_properties_for_interval(zend_object *object, zend_prop_purpose purpose);
355356
static HashTable *date_object_get_gc_period(zend_object *object, zval **table, int *n);
356357
static HashTable *date_object_get_properties_for_timezone(zend_object *object, zend_prop_purpose purpose);
357358
static HashTable *date_object_get_gc_timezone(zend_object *object, zval **table, int *n);
@@ -1816,6 +1817,7 @@ static void date_register_classes(void) /* {{{ */
18161817
date_object_handlers_interval.read_property = date_interval_read_property;
18171818
date_object_handlers_interval.write_property = date_interval_write_property;
18181819
date_object_handlers_interval.get_properties = date_object_get_properties_interval;
1820+
date_object_handlers_interval.get_properties_for = date_object_get_properties_for_interval;
18191821
date_object_handlers_interval.get_property_ptr_ptr = date_interval_get_property_ptr_ptr;
18201822
date_object_handlers_interval.get_gc = date_object_get_gc_interval;
18211823
date_object_handlers_interval.compare = date_interval_compare_objects;
@@ -2155,6 +2157,7 @@ static zend_object *date_object_new_interval(zend_class_entry *class_type) /* {{
21552157

21562158
zend_object_std_init(&intern->std, class_type);
21572159
object_properties_init(&intern->std, class_type);
2160+
intern->props_cache = NULL;
21582161

21592162
return &intern->std;
21602163
} /* }}} */
@@ -2240,6 +2243,48 @@ static HashTable *date_object_get_properties_interval(zend_object *object) /* {{
22402243
return props;
22412244
} /* }}} */
22422245

2246+
static HashTable *date_object_get_properties_for_interval(zend_object *object, zend_prop_purpose purpose) /* {{{ */
2247+
{
2248+
HashTable *props;
2249+
php_interval_obj *intervalobj;
2250+
2251+
switch (purpose) {
2252+
case ZEND_PROP_PURPOSE_DEBUG:
2253+
case ZEND_PROP_PURPOSE_SERIALIZE:
2254+
case ZEND_PROP_PURPOSE_VAR_EXPORT:
2255+
case ZEND_PROP_PURPOSE_JSON:
2256+
case ZEND_PROP_PURPOSE_ARRAY_CAST:
2257+
break;
2258+
default:
2259+
return zend_std_get_properties_for(object, purpose);
2260+
}
2261+
2262+
intervalobj = php_interval_obj_from_obj(object);
2263+
2264+
if (!intervalobj->initialized) {
2265+
return zend_array_dup(zend_std_get_properties(object));
2266+
}
2267+
2268+
/* If cache exists and is actively in use (refcount > 1), we're in a recursive
2269+
* call (e.g., circular reference during json_encode). Return the same cache
2270+
* so that circular reference detection works correctly. */
2271+
if (intervalobj->props_cache && GC_REFCOUNT(intervalobj->props_cache) > 1) {
2272+
GC_ADDREF(intervalobj->props_cache);
2273+
return intervalobj->props_cache;
2274+
}
2275+
2276+
/* Create new cache or replace stale one */
2277+
if (intervalobj->props_cache) {
2278+
zend_hash_release(intervalobj->props_cache);
2279+
}
2280+
props = zend_array_dup(zend_std_get_properties(object));
2281+
date_interval_object_to_hash(intervalobj, props);
2282+
intervalobj->props_cache = props;
2283+
2284+
GC_ADDREF(props);
2285+
return props;
2286+
} /* }}} */
2287+
22432288
static zend_object *date_object_new_period(zend_class_entry *class_type) /* {{{ */
22442289
{
22452290
php_period_obj *intern = zend_object_alloc(sizeof(php_period_obj), class_type);
@@ -2306,6 +2351,10 @@ static void date_object_free_storage_interval(zend_object *object) /* {{{ */
23062351
zend_string_release(intern->date_string);
23072352
intern->date_string = NULL;
23082353
}
2354+
if (intern->props_cache) {
2355+
zend_hash_release(intern->props_cache);
2356+
intern->props_cache = NULL;
2357+
}
23092358
timelib_rel_time_dtor(intern->diff);
23102359
zend_object_std_dtor(&intern->std);
23112360
} /* }}} */

ext/date/php_date.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@ struct _php_interval_obj {
7575
bool from_string;
7676
zend_string *date_string;
7777
bool initialized;
78+
HashTable *props_cache;
7879
zend_object std;
7980
};
8081

ext/date/tests/bug-gh20503.phpt

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
--TEST--
2+
GH-20503 (Assertion failure with DateInterval and json_encode on circular reference)
3+
--FILE--
4+
<?php
5+
$obj = new DateInterval('P1W');
6+
$obj->circular = $obj;
7+
8+
// json_encode with circular reference previously caused an assertion failure
9+
// in debug builds when modifying a HashTable with refcount > 1
10+
$result = json_encode($obj);
11+
var_dump($result === false);
12+
var_dump(json_last_error() === JSON_ERROR_RECURSION);
13+
14+
// Also verify array cast works
15+
$props = (array) $obj;
16+
var_dump(count($props) > 0);
17+
var_dump(isset($props['circular']));
18+
?>
19+
--EXPECTF--
20+
Deprecated: Creation of dynamic property DateInterval::$circular is deprecated in %s on line %d
21+
bool(true)
22+
bool(true)
23+
bool(true)
24+
bool(true)

0 commit comments

Comments
 (0)