Skip to content

Commit e891539

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 e891539

File tree

3 files changed

+94
-0
lines changed

3 files changed

+94
-0
lines changed

ext/date/php_date.c

Lines changed: 69 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);
@@ -396,6 +397,7 @@ static PHP_GINIT_FUNCTION(date)
396397
date_globals->default_timezone = NULL;
397398
date_globals->timezone = NULL;
398399
date_globals->tzcache = NULL;
400+
date_globals->interval_props_cache = NULL;
399401
}
400402
/* }}} */
401403

@@ -407,6 +409,13 @@ static void _php_date_tzinfo_dtor(zval *zv) /* {{{ */
407409
timelib_tzinfo_dtor(tzi);
408410
} /* }}} */
409411

412+
static void _php_date_interval_props_dtor(zval *zv) /* {{{ */
413+
{
414+
HashTable *props = (HashTable*)Z_PTR_P(zv);
415+
416+
zend_hash_release(props);
417+
} /* }}} */
418+
410419
/* {{{ PHP_RINIT_FUNCTION */
411420
PHP_RINIT_FUNCTION(date)
412421
{
@@ -416,6 +425,7 @@ PHP_RINIT_FUNCTION(date)
416425
DATEG(timezone) = NULL;
417426
DATEG(tzcache) = NULL;
418427
DATEG(last_errors) = NULL;
428+
DATEG(interval_props_cache) = NULL;
419429

420430
return SUCCESS;
421431
}
@@ -441,6 +451,12 @@ ZEND_MODULE_POST_ZEND_DEACTIVATE_D(date)
441451
DATEG(tzcache) = NULL;
442452
}
443453

454+
if (DATEG(interval_props_cache)) {
455+
zend_hash_destroy(DATEG(interval_props_cache));
456+
FREE_HASHTABLE(DATEG(interval_props_cache));
457+
DATEG(interval_props_cache) = NULL;
458+
}
459+
444460
if (DATEG(last_errors)) {
445461
timelib_error_container_dtor(DATEG(last_errors));
446462
DATEG(last_errors) = NULL;
@@ -1816,6 +1832,7 @@ static void date_register_classes(void) /* {{{ */
18161832
date_object_handlers_interval.read_property = date_interval_read_property;
18171833
date_object_handlers_interval.write_property = date_interval_write_property;
18181834
date_object_handlers_interval.get_properties = date_object_get_properties_interval;
1835+
date_object_handlers_interval.get_properties_for = date_object_get_properties_for_interval;
18191836
date_object_handlers_interval.get_property_ptr_ptr = date_interval_get_property_ptr_ptr;
18201837
date_object_handlers_interval.get_gc = date_object_get_gc_interval;
18211838
date_object_handlers_interval.compare = date_interval_compare_objects;
@@ -2240,6 +2257,54 @@ static HashTable *date_object_get_properties_interval(zend_object *object) /* {{
22402257
return props;
22412258
} /* }}} */
22422259

2260+
static HashTable *date_object_get_properties_for_interval(zend_object *object, zend_prop_purpose purpose) /* {{{ */
2261+
{
2262+
HashTable *props;
2263+
HashTable *cached;
2264+
php_interval_obj *intervalobj;
2265+
zend_ulong handle = object->handle;
2266+
2267+
switch (purpose) {
2268+
case ZEND_PROP_PURPOSE_DEBUG:
2269+
case ZEND_PROP_PURPOSE_SERIALIZE:
2270+
case ZEND_PROP_PURPOSE_VAR_EXPORT:
2271+
case ZEND_PROP_PURPOSE_JSON:
2272+
case ZEND_PROP_PURPOSE_ARRAY_CAST:
2273+
break;
2274+
default:
2275+
return zend_std_get_properties_for(object, purpose);
2276+
}
2277+
2278+
intervalobj = php_interval_obj_from_obj(object);
2279+
2280+
if (!intervalobj->initialized) {
2281+
return zend_array_dup(zend_std_get_properties(object));
2282+
}
2283+
2284+
/* Lazily allocate the cache HashTable */
2285+
if (!DATEG(interval_props_cache)) {
2286+
ALLOC_HASHTABLE(DATEG(interval_props_cache));
2287+
zend_hash_init(DATEG(interval_props_cache), 8, NULL, _php_date_interval_props_dtor, 0);
2288+
}
2289+
2290+
/* If cache exists and is actively in use (refcount > 1), we're in a recursive
2291+
* call (e.g., circular reference during json_encode). Return the same cache
2292+
* so that circular reference detection works correctly. */
2293+
cached = zend_hash_index_find_ptr(DATEG(interval_props_cache), handle);
2294+
if (cached && GC_REFCOUNT(cached) > 1) {
2295+
GC_ADDREF(cached);
2296+
return cached;
2297+
}
2298+
2299+
/* Create new cache or replace stale one */
2300+
props = zend_array_dup(zend_std_get_properties(object));
2301+
date_interval_object_to_hash(intervalobj, props);
2302+
zend_hash_index_update_ptr(DATEG(interval_props_cache), handle, props);
2303+
2304+
GC_ADDREF(props);
2305+
return props;
2306+
} /* }}} */
2307+
22432308
static zend_object *date_object_new_period(zend_class_entry *class_type) /* {{{ */
22442309
{
22452310
php_period_obj *intern = zend_object_alloc(sizeof(php_period_obj), class_type);
@@ -2306,6 +2371,10 @@ static void date_object_free_storage_interval(zend_object *object) /* {{{ */
23062371
zend_string_release(intern->date_string);
23072372
intern->date_string = NULL;
23082373
}
2374+
/* Clean up cached properties for this object */
2375+
if (DATEG(interval_props_cache)) {
2376+
zend_hash_index_del(DATEG(interval_props_cache), object->handle);
2377+
}
23092378
timelib_rel_time_dtor(intern->diff);
23102379
zend_object_std_dtor(&intern->std);
23112380
} /* }}} */

ext/date/php_date.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,7 @@ ZEND_BEGIN_MODULE_GLOBALS(date)
108108
char *timezone;
109109
HashTable *tzcache;
110110
timelib_error_container *last_errors;
111+
HashTable *interval_props_cache;
111112
ZEND_END_MODULE_GLOBALS(date)
112113

113114
#define DATEG(v) ZEND_MODULE_GLOBALS_ACCESSOR(date, v)

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)