From d544880ef8834c4edff65dda2cfa9550b27da25b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Marcin=20Brzuchalski?= Date: Mon, 20 Jul 2026 11:13:01 +0200 Subject: [PATCH] ext/date: guard property-table reads in php_date_interval_initialize_from_hash() php_date_interval_initialize_from_hash() reads the interval fields out of a HashTable which, for an uninitialized DateInterval, is the object's raw property table: date_object_get_properties_interval() returns zend_std_get_properties() unchanged when !intervalobj->initialized. A raw property table stores declared properties as IS_INDIRECT slots. The PHP_DATE_INTERVAL_READ_PROPERTY() family already accounts for this with a Z_TYPE_P(z_arg) <= IS_STRING guard, so the eighteen fields read through it are safe. The two fields read open-coded, "f" and "civil_or_wall", have no such guard and pass the IS_INDIRECT zval straight to zval_get_double() and zval_get_long(), which do not accept it and fall into ZEND_UNREACHABLE(). class B extends DateInterval { public float $f = 1.5; } $o = (new ReflectionClass('B'))->newInstanceWithoutConstructor(); $o->__wakeup(); In a debug build this aborts on an assertion in zval_get_double_func(). In a release build it is undefined behaviour; as observed the declared value is silently discarded rather than diagnosed. ext/zlib solves the same class of problem at the call site with ZVAL_DEINDIRECT(), whose comment in zlib.c notes that the |H ZPP specifier may leave HashTable entries wrapped in IS_INDIRECT. Apply the guard the sibling reads already use, and add a regression test. --- ext/date/php_date.c | 4 +- ...ate_interval_wakeup_declared_property.phpt | 41 +++++++++++++++++++ 2 files changed, 43 insertions(+), 2 deletions(-) create mode 100644 ext/date/tests/date_interval_wakeup_declared_property.phpt diff --git a/ext/date/php_date.c b/ext/date/php_date.c index 0e24e4450641..690713eeb5e5 100644 --- a/ext/date/php_date.c +++ b/ext/date/php_date.c @@ -4733,7 +4733,7 @@ static void php_date_interval_initialize_from_hash(php_interval_obj *intobj, con PHP_DATE_INTERVAL_READ_PROPERTY("s", s, timelib_sll, -1) { const zval *z_arg = zend_hash_str_find(myht, "f", sizeof("f") - 1); - if (z_arg) { + if (z_arg && Z_TYPE_P(z_arg) <= IS_STRING) { intobj->diff->us = zend_dval_to_lval(zval_get_double(z_arg) * 1000000.0); } } @@ -4749,7 +4749,7 @@ static void php_date_interval_initialize_from_hash(php_interval_obj *intobj, con { const zval *z_arg = zend_hash_str_find(myht, "civil_or_wall", sizeof("civil_or_wall") - 1); intobj->civil_or_wall = PHP_DATE_CIVIL; - if (z_arg) { + if (z_arg && Z_TYPE_P(z_arg) <= IS_STRING) { zend_long val = zval_get_long(z_arg); intobj->civil_or_wall = val; } diff --git a/ext/date/tests/date_interval_wakeup_declared_property.phpt b/ext/date/tests/date_interval_wakeup_declared_property.phpt new file mode 100644 index 000000000000..5478c2ef7ae8 --- /dev/null +++ b/ext/date/tests/date_interval_wakeup_declared_property.phpt @@ -0,0 +1,41 @@ +--TEST-- +DateInterval::__wakeup() must not pass raw property-table zvals to zval_get_double()/zval_get_long() +--FILE-- +newInstanceWithoutConstructor(); + $o->__wakeup(); + echo $cls, ": ok\n"; +} + +/* A dynamic property is a plain zval, so it is still read normally. */ +$o = (new ReflectionClass('Dynamic'))->newInstanceWithoutConstructor(); +$o->f = 0.25; +$o->__wakeup(); +echo "Dynamic: ", $o->f, "\n"; + +/* The normal round-trip is unaffected. */ +$i = new DateInterval('PT1S'); +$i->f = 0.5; +$restored = unserialize(serialize($i)); +echo "roundtrip f: ", $restored->f, "\n"; +echo "roundtrip s: ", $restored->s, "\n"; +?> +--EXPECT-- +DeclaredInitialized: ok +DeclaredUninitialized: ok +DeclaredCivilOrWall: ok +Dynamic: 0.25 +roundtrip f: 0.5 +roundtrip s: 1