Skip to content

Fix #81500: Interval serialization regression since 7.3.14 / 7.4.2 #7575

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 4 commits into from

Conversation

cmb69
Copy link
Member

@cmb69 cmb69 commented Oct 13, 2021

While it may not be desired, DateInterval::$f supports negative
values, at least with regard to calculations. We still need to guard
from assigning double values which are out of range for signed 64bit
integers (which would be undefined behavior). While we could restrict
this only to values which are actually out of range, the marker for
invalid µs is -1000000, so we cannot accept smaller numbers. To
retain symmetry, we also reject values greater than 1000000. We must
not do that only for unserialization, but also when the property is set
in the first place.

While it may not be desired, `DateInterval::$f` supports negative
values, at least with regard to calculations.  We still need to guard
from assigning double values which are out of range for signed 64bit
integers (which would be undefined behavior).  While we could restrict
this only to values which are actually out of range, the marker for
invalid µs is `-1000000`, so we cannot accept smaller numbers.  To
retain symmetry, we also reject values greater than `1000000`.  We must
not do that only for unserialization, but also when the property is set
in the first place.
@kylekatarnls
Copy link
Contributor

Sorry @cmb69 but this is still a different behavior than PHP < 8.1 and how second/minute/hour behaves. ->s = -66 works fine and out of range is even supported in constructor: new DateInterval('PT66S')

And actually it should be fine to represent an interval as 5 000 000 microseconds we should not be forced to convert it to second when doing calculation (see my example on #7572) as we are currently free to represent 5 minutes as 300 seconds.

@kylekatarnls
Copy link
Contributor

kylekatarnls commented Oct 13, 2021

I simply think PHP should not try to do any smart and fancy if-else tricks around inputs, it will just replacing existing bugs by even weirder bugs.

Comment on lines 4479 to 4481
if (val > -1000000 && val < 1000000) {
(*intobj)->diff->us = val;
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if (val > -1000000 && val < 1000000) {
(*intobj)->diff->us = val;
}
(*intobj)->diff->us = val;

Comment on lines 4334 to 4339
double val = zval_get_double(value) * 1000000;
if (val > -1000000 && val < 1000000) {
obj->diff->us = val;
} else {
obj->diff->us = -1000000;
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
double val = zval_get_double(value) * 1000000;
if (val > -1000000 && val < 1000000) {
obj->diff->us = val;
} else {
obj->diff->us = -1000000;
}
obj->diff->us = zval_get_double(value) * 1000000;

Copy link
Contributor

@kylekatarnls kylekatarnls left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please keep it simple. PHP 8.0 behavior is not wrong.

@cmb69
Copy link
Member Author

cmb69 commented Oct 13, 2021

@kylekatarnls, please read the commit message (first comment). We need to avoid triggering undefined behavior (implementation defined is okay-ish, but not undefined behavior). Thus we need to restrict the values here, although not necessarily that strict as I've done here.

@nikic
Copy link
Member

nikic commented Oct 14, 2021

Maybe use zend_dval_to_lval? The implementation is technically still UB on x86, but it makes sure that the behavior is consistent across platforms, and will not trigger ubsan warnings.

@cmb69
Copy link
Member Author

cmb69 commented Oct 14, 2021

Maybe use zend_dval_to_lval?

Do you mean just us = zend_dval_to_lval(value * 1000000);? That could still trigger signed integer overflow UB , but OTOH I'm having a hard time to set the invalid marker (-1000000), because the property read handler does not handle that correctly:

php-src/ext/date/php_date.c

Lines 4278 to 4284 in 3657693

if (fvalue != -1) {
ZVAL_DOUBLE(retval, fvalue);
} else if (value != -99999) {
ZVAL_LONG(retval, value);
} else {
ZVAL_FALSE(retval);
}

Instead of yielding false, it yields int(-1) (https://3v4l.org/AmubA), because value defaults to -1 instead of -99999, so -1000000 is not really invalid, and we could break existing code when changing this.

@kylekatarnls
Copy link
Contributor

kylekatarnls commented Oct 14, 2021

To be clear, the new limitation already broke existing code (I mean 8.1 RC3 disallow to use microseconds that are not in the range, while this is a fair usage and worked until this version).

Also the previous bug was scoped to serialize/unserialize, so at least everything went fine as long as you did not serialize the interval. But if we apply the current change, the breaking change will impact way more users as it will no longer be scoped to only serialization.

I would be favorable to just us = zend_dval_to_lval(value * 1000000) without further replacements when we assign the property.

Then on serialization, optionally to check possible overflow, but I would recommend to actually target overflow boundaries, not limit to [0:1[ range.

@codecov-commenter
Copy link

Codecov Report

Merging #7575 (3657693) into PHP-7.4 (788a701) will decrease coverage by 0.00%.
The diff coverage is 100.00%.

❗ Current head 3657693 differs from pull request most recent head 337deb1. Consider uploading reports for the commit 337deb1 to get more accurate results
Impacted file tree graph

@@             Coverage Diff             @@
##           PHP-7.4    #7575      +/-   ##
===========================================
- Coverage    73.09%   73.09%   -0.01%     
===========================================
  Files          803      803              
  Lines       283990   283998       +8     
===========================================
+ Hits        207588   207592       +4     
- Misses       76402    76406       +4     
Impacted Files Coverage Δ
main/main.c 80.44% <100.00%> (+0.14%) ⬆️
Zend/zend_strtod.c 87.41% <0.00%> (-0.41%) ⬇️
ext/standard/ftp_fopen_wrapper.c 51.88% <0.00%> (+0.17%) ⬆️

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update 788a701...337deb1. Read the comment docs.

If overflow would occur, the value is set to `0`, what is standard PHP
behavior anyway.  This way we can avoid setting the invalid marker,
which doesn't work as expected anyway.

We need to adapt some of the existing tests wrt. this behavior.  In
particular, we check for an arbitrary value in bug79015.phpt, to cater
to differences between 32bit and 64bit architectures.
@cmb69
Copy link
Member Author

cmb69 commented Oct 15, 2021

That could still trigger signed integer overflow UB, […]

No. After a more thorough check this cannot happen (it seems to me not even on x86); only unsigned to signed int conversion may overflow, but that is implementation defined behavior (not UB).

Copy link
Contributor

@kylekatarnls kylekatarnls left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

❤️

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants