Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions NEWS
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ PHP NEWS
. Fixed incorrect internal pointer and foreach iterator positions when
compacting arrays with holes. (Weilin Du)

- Date:
. Fix unserialization of Time\Duration. (timwolla)

- DOM:
. Fixed use-after-free when re-constructing a DOMXPath whose php:function
registrations are freed while still reachable from the cycle collector.
Expand Down
2 changes: 2 additions & 0 deletions UPGRADING.INTERNALS
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,8 @@ PHP 8.6 INTERNALS UPGRADE NOTES
instead of a zval*. Accordingly, zend_get_closure_this_ptr() now returns
that zend_object*, or NULL when the closure is unbound, instead of a
zval* that is IS_UNDEF when the closure is unbound.
. object_properties_load() now verifies that the given value is assignable
to typed properties. The check is performed in non-strict mode.

- Added:
. New zend_class_entry.ce_flags2 and zend_function.fn_flags2 fields were
Expand Down
4 changes: 4 additions & 0 deletions Zend/zend_API.c
Original file line number Diff line number Diff line change
Expand Up @@ -1794,6 +1794,10 @@ ZEND_API void object_properties_load(zend_object *object, const HashTable *prope
return;
}
}
if (ZEND_TYPE_IS_SET(property_info->type) && !zend_verify_property_type(property_info, prop, /* strict */ false)) {
return;
}

zval_ptr_dtor(slot);
ZVAL_COPY_VALUE(slot, prop);
zval_add_ref(slot);
Expand Down
71 changes: 71 additions & 0 deletions ext/date/tests/time/duration/serialize.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
--TEST--
Time\Duration: serialize()
--FILE--
<?php

require __DIR__ . '/helper.inc';

var_dump($serialized = serialize(Time\Duration::fromSeconds(1, 2)->negate()));
echo f($unserialized = unserialize($serialized)), PHP_EOL;
var_dump(serialize($unserialized));
echo f($unserialized->add($unserialized)), PHP_EOL;

try {
// $negative is not bool, but coercible.
echo f(unserialize('O:13:"Time\Duration":3:{s:7:"seconds";i:1;s:11:"nanoseconds";i:1;s:8:"negative";i:999;}')), PHP_EOL;
} catch (Throwable $e) {
echo $e::class, ': ', $e->getMessage(), PHP_EOL;
}

try {
// $negative is not bool and not coercible.
unserialize('O:13:"Time\Duration":3:{s:7:"seconds";i:1;s:11:"nanoseconds";i:1;s:8:"negative";N;}');
} catch (Throwable $e) {
echo $e::class, ': ', $e->getMessage(), PHP_EOL;
}

try {
// $seconds is negative.
unserialize('O:13:"Time\Duration":3:{s:7:"seconds";i:-1;s:11:"nanoseconds";i:1;s:8:"negative";b:0;}');
} catch (Throwable $e) {
echo $e::class, ': ', $e->getMessage(), PHP_EOL;
}

try {
// Dynamic property.
unserialize('O:13:"Time\Duration":4:{s:7:"seconds";i:1;s:11:"nanoseconds";i:1;s:8:"negative";b:0;s:3:"foo";N;}');
} catch (Throwable $e) {
echo $e::class, ': ', $e->getMessage(), PHP_EOL;
}


try {
// Out of range nanoseconds
unserialize('O:13:"Time\Duration":3:{s:7:"seconds";i:1;s:11:"nanoseconds";i:1000000000;s:8:"negative";b:0;}');
} catch (Throwable $e) {
echo $e::class, ': ', $e->getMessage(), PHP_EOL;
}

try {
Time\Duration::fromSeconds(1, 1)
->__unserialize([
'seconds' => 2,
'nanoseconds' => 2,
'negative' => true,
]);
} catch (Throwable $e) {
echo $e::class, ': ', $e->getMessage(), PHP_EOL;
}

?>
--EXPECT--
string(85) "O:13:"Time\Duration":3:{s:7:"seconds";i:1;s:11:"nanoseconds";i:2;s:8:"negative";b:1;}"
-1.000000002
string(85) "O:13:"Time\Duration":3:{s:7:"seconds";i:1;s:11:"nanoseconds";i:2;s:8:"negative";b:1;}"
-2.000000004
-1.000000001
Exception: Invalid serialization data for Time\Duration object
Exception: Invalid serialization data for Time\Duration object
Exception: Invalid serialization data for Time\Duration object
Exception: Invalid serialization data for Time\Duration object
Exception: Invalid serialization data for Time\Duration object
4 changes: 4 additions & 0 deletions ext/date/time.stub.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,10 @@ private function __construct()
{
}

public function __unserialize(array $data): void
{
}

public static function fromSeconds(int $seconds, int $nanoseconds = 0): Duration
{
}
Expand Down
8 changes: 7 additions & 1 deletion ext/date/time_arginfo.h

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

59 changes: 54 additions & 5 deletions ext/date/time_duration.c
Original file line number Diff line number Diff line change
Expand Up @@ -77,20 +77,24 @@ static inline php_date_time_duration *create_duration_shell(zval *target)
return Z_DATE_TIME_DURATION_P(target);
}

ZEND_ATTRIBUTE_NODISCARD static inline zend_result sync_properties(php_date_time_duration *object)
static inline bool duration_representable(const timelib_duration *duration)
{
if (
return
/* Check if the duration would overflow the $seconds property. */
object->duration.seconds > ((uint64_t)ZEND_LONG_MAX)
duration->seconds <= ((uint64_t)ZEND_LONG_MAX)
/* This constraint is an explicit part of PHP's API: It is the maximum $seconds
* value that allows storing the entire duration as a single int64_t counting
* nanoseconds, which might be desirable in the future when userland `int` is
* consistently 64 bits.
*
* While it is currently also enforced by timelib, this might change
* in a future version of timelib, thus we also enforce it manually. */
|| object->duration.seconds > UINT64_C(9223372035)
) {
&& duration->seconds <= UINT64_C(9223372035);
}

ZEND_ATTRIBUTE_NODISCARD static inline zend_result sync_properties(php_date_time_duration *object)
{
if (!duration_representable(&object->duration)) {
throw_out_of_range_exception();
return FAILURE;
}
Expand Down Expand Up @@ -149,6 +153,51 @@ PHP_METHOD(Time_Duration, __construct)
zend_throw_error(NULL, "Cannot directly construct Time\\Duration, use Time\\Duration::from*() methods instead");
}

PHP_METHOD(Time_Duration, __unserialize)
{
php_date_time_duration *duration = Z_DATE_TIME_DURATION_P(ZEND_THIS);

HashTable *data;

ZEND_PARSE_PARAMETERS_START(1, 1)
Z_PARAM_ARRAY_HT(data);
ZEND_PARSE_PARAMETERS_END();

object_properties_load(&duration->std, data);
if (EG(exception)) {
goto to_generic_error;
}

zval *seconds = OBJ_PROP_NUM(&duration->std, 0);
zval *nanoseconds = OBJ_PROP_NUM(&duration->std, 1);
zval *negative = OBJ_PROP_NUM(&duration->std, 2);

/* Verify that both properties are positive, since the timelib_duration_ctor_static() takes unsigned. */
if (Z_LVAL_P(seconds) < 0 || Z_LVAL_P(nanoseconds) < 0) {
zend_throw_exception_ex(NULL, 0, "Invalid serialization data for %s object", ZSTR_VAL(duration->std.ce->name));
RETURN_THROWS();
}

int error = timelib_duration_ctor_static(&duration->duration, Z_LVAL_P(seconds), Z_LVAL_P(nanoseconds), Z_TYPE_P(negative) == IS_TRUE);
if (error != TIMELIB_ERROR_NO_ERROR) {
throw_timelib_error(error);
goto to_generic_error;
}

if (!duration_representable(&duration->duration)) {
throw_out_of_range_exception();
goto to_generic_error;
}

return;

to_generic_error:

/* Wrap any errors thrown by existing checks into a generic error. */
zend_throw_exception_ex(NULL, 0, "Invalid serialization data for %s object", ZSTR_VAL(duration->std.ce->name));
RETURN_THROWS();
}

PHP_METHOD(Time_Duration, fromSeconds)
{
zend_ulong seconds;
Expand Down
Loading