Skip to content

Commit 00cc4fe

Browse files
authored
date: Fix unserialization of Time\Duration (#23629)
1 parent c07cc85 commit 00cc4fe

6 files changed

Lines changed: 140 additions & 8 deletions

File tree

NEWS

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,9 @@ PHP NEWS
88
. Fix handling of references to typed properties during unserialization
99
of various internal classes. (ndossche, timwolla)
1010

11+
- Date:
12+
. Fix unserialization of Time\Duration. (timwolla)
13+
1114
- DOM:
1215
. Fixed use-after-free when re-constructing a DOMXPath whose php:function
1316
registrations are freed while still reachable from the cycle collector.

ext/date/tests/time/duration/gh23639.phpt

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,6 @@ GH-23639 (object_properties_load allows creating readonly reference properties)
33
--CREDITS--
44
arnaud-lb
55
ndossche
6-
--XFAIL--
7-
Test can only succeed when GH-23629 is also merged
86
--FILE--
97
<?php
108

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
--TEST--
2+
Time\Duration: serialize()
3+
--FILE--
4+
<?php
5+
6+
require __DIR__ . '/helper.inc';
7+
8+
var_dump($serialized = serialize(Time\Duration::fromSeconds(1, 2)->negate()));
9+
echo f($unserialized = unserialize($serialized)), PHP_EOL;
10+
var_dump(serialize($unserialized));
11+
echo f($unserialized->add($unserialized)), PHP_EOL;
12+
13+
try {
14+
// $negative is not bool, but coercible.
15+
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;
16+
} catch (Throwable $e) {
17+
echo $e::class, ': ', $e->getMessage(), PHP_EOL;
18+
}
19+
20+
try {
21+
// $negative is not bool and not coercible.
22+
unserialize('O:13:"Time\Duration":3:{s:7:"seconds";i:1;s:11:"nanoseconds";i:1;s:8:"negative";N;}');
23+
} catch (Throwable $e) {
24+
echo $e::class, ': ', $e->getMessage(), PHP_EOL;
25+
}
26+
27+
try {
28+
// $seconds is negative.
29+
unserialize('O:13:"Time\Duration":3:{s:7:"seconds";i:-1;s:11:"nanoseconds";i:1;s:8:"negative";b:0;}');
30+
} catch (Throwable $e) {
31+
echo $e::class, ': ', $e->getMessage(), PHP_EOL;
32+
}
33+
34+
try {
35+
// Dynamic property.
36+
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;}');
37+
} catch (Throwable $e) {
38+
echo $e::class, ': ', $e->getMessage(), PHP_EOL;
39+
}
40+
41+
42+
try {
43+
// Out of range nanoseconds
44+
unserialize('O:13:"Time\Duration":3:{s:7:"seconds";i:1;s:11:"nanoseconds";i:1000000000;s:8:"negative";b:0;}');
45+
} catch (Throwable $e) {
46+
echo $e::class, ': ', $e->getMessage(), PHP_EOL;
47+
}
48+
49+
try {
50+
Time\Duration::fromSeconds(1, 1)
51+
->__unserialize([
52+
'seconds' => 2,
53+
'nanoseconds' => 2,
54+
'negative' => true,
55+
]);
56+
} catch (Throwable $e) {
57+
echo $e::class, ': ', $e->getMessage(), PHP_EOL;
58+
}
59+
60+
?>
61+
--EXPECT--
62+
string(85) "O:13:"Time\Duration":3:{s:7:"seconds";i:1;s:11:"nanoseconds";i:2;s:8:"negative";b:1;}"
63+
-1.000000002
64+
string(85) "O:13:"Time\Duration":3:{s:7:"seconds";i:1;s:11:"nanoseconds";i:2;s:8:"negative";b:1;}"
65+
-2.000000004
66+
Exception: Invalid serialization data for Time\Duration object
67+
Exception: Invalid serialization data for Time\Duration object
68+
Exception: Invalid serialization data for Time\Duration object
69+
Exception: Invalid serialization data for Time\Duration object
70+
Exception: Invalid serialization data for Time\Duration object
71+
Exception: Invalid serialization data for Time\Duration object

ext/date/time.stub.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,10 @@ private function __construct()
2121
{
2222
}
2323

24+
public function __unserialize(array $data): void
25+
{
26+
}
27+
2428
public static function fromSeconds(int $seconds, int $nanoseconds = 0): Duration
2529
{
2630
}

ext/date/time_arginfo.h

Lines changed: 7 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

ext/date/time_duration.c

Lines changed: 55 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -77,20 +77,24 @@ static inline php_date_time_duration *create_duration_shell(zval *target)
7777
return Z_DATE_TIME_DURATION_P(target);
7878
}
7979

80-
ZEND_ATTRIBUTE_NODISCARD static inline zend_result sync_properties(php_date_time_duration *object)
80+
static inline bool duration_representable(const timelib_duration *duration)
8181
{
82-
if (
82+
return
8383
/* Check if the duration would overflow the $seconds property. */
84-
object->duration.seconds > ((uint64_t)ZEND_LONG_MAX)
84+
duration->seconds <= ((uint64_t)ZEND_LONG_MAX)
8585
/* This constraint is an explicit part of PHP's API: It is the maximum $seconds
8686
* value that allows storing the entire duration as a single int64_t counting
8787
* nanoseconds, which might be desirable in the future when userland `int` is
8888
* consistently 64 bits.
8989
*
9090
* While it is currently also enforced by timelib, this might change
9191
* in a future version of timelib, thus we also enforce it manually. */
92-
|| object->duration.seconds > UINT64_C(9223372035)
93-
) {
92+
&& duration->seconds <= UINT64_C(9223372035);
93+
}
94+
95+
ZEND_ATTRIBUTE_NODISCARD static inline zend_result sync_properties(php_date_time_duration *object)
96+
{
97+
if (!duration_representable(&object->duration)) {
9498
throw_out_of_range_exception();
9599
return FAILURE;
96100
}
@@ -149,6 +153,52 @@ PHP_METHOD(Time_Duration, __construct)
149153
zend_throw_error(NULL, "Cannot directly construct Time\\Duration, use Time\\Duration::from*() methods instead");
150154
}
151155

156+
PHP_METHOD(Time_Duration, __unserialize)
157+
{
158+
php_date_time_duration *duration = Z_DATE_TIME_DURATION_P(ZEND_THIS);
159+
160+
HashTable *data;
161+
162+
ZEND_PARSE_PARAMETERS_START(1, 1)
163+
Z_PARAM_ARRAY_HT(data);
164+
ZEND_PARSE_PARAMETERS_END();
165+
166+
object_properties_load(&duration->std, data);
167+
if (EG(exception)) {
168+
goto fail;
169+
}
170+
171+
zval *seconds = OBJ_PROP_NUM(&duration->std, 0);
172+
zval *nanoseconds = OBJ_PROP_NUM(&duration->std, 1);
173+
zval *negative = OBJ_PROP_NUM(&duration->std, 2);
174+
175+
/* Verify that both properties are positive, since the timelib_duration_ctor_static() takes unsigned. */
176+
if (Z_LVAL_P(seconds) < 0 || Z_LVAL_P(nanoseconds) < 0) {
177+
goto fail;
178+
}
179+
180+
int error = timelib_duration_ctor_static(&duration->duration, Z_LVAL_P(seconds), Z_LVAL_P(nanoseconds), Z_TYPE_P(negative) == IS_TRUE);
181+
if (error != TIMELIB_ERROR_NO_ERROR) {
182+
throw_timelib_error(error);
183+
goto fail;
184+
}
185+
186+
if (!duration_representable(&duration->duration)) {
187+
throw_out_of_range_exception();
188+
goto fail;
189+
}
190+
191+
return;
192+
193+
fail:
194+
195+
/* If an exception is already active (e.g. for unrepresentable durations) it will be wrapped for
196+
* uniform exceptions thrown from unserialization handlers, but to still provide additional
197+
* context for a human reader. */
198+
zend_throw_exception_ex(NULL, 0, "Invalid serialization data for %s object", ZSTR_VAL(duration->std.ce->name));
199+
RETURN_THROWS();
200+
}
201+
152202
PHP_METHOD(Time_Duration, fromSeconds)
153203
{
154204
zend_ulong seconds;

0 commit comments

Comments
 (0)