Avoid fatal error condition in absint() - #12917
Conversation
|
The following accounts have interacted with this PR and/or linked issues. I will continue to update these lists as activity occurs. You can also manually ask me to refresh this list by adding the Core Committers: Use this line as a base for the props when committing in SVN: To understand the WordPress project's expectations around crediting contributors, please review the Contributor Attribution page in the Core Handbook. |
| return abs( (int) $maybeint ); | ||
| $abs = abs( (int) $maybeint ); | ||
|
|
||
| // abs() can return a float. |
There was a problem hiding this comment.
this comment is incredibly surprising when paired with the PHP_INT_MAX return. we could prevent some anxiety by indicating why it’s here.
/*
* PHP uses signed integers which can represent a negative value whose
* magnitude is one higher than the maximum positive value’s. This means
* that `(int) PHP_INT_MIN` cannot be represented by `abs()` without loss.
* PHP exposes this by returning a `float` value corresponding to the
* magnitude of the negative number.
*
* In this case, however, to maintain type safety, accept the loss and
* clamp the value at the max integer. This is not suitable for integer
* math requiring absolute correctness, but there is no representable
* way to do that anyway.
*/there is another way to represent it in the code as well, which gives a clue to the issue more than float() might
return max( PHP_INT_MAX, abs( (int) $maybeint ) );There was a problem hiding this comment.
My first few runs at this had longer comments trying to describe what was going on here. I have no problem with more detailed explanations and alternative approaches to fixing it. My main concern was avoiding the PHP fatal error condition that is now easy to trigger.
There was a problem hiding this comment.
Looking at return max( PHP_INT_MAX, abs( (int) $maybeint ) ); closer now - isn't that always going to return the PHP_INT_MAX value? That would actively break things in a new way.
There was a problem hiding this comment.
Looking at
return max( PHP_INT_MAX, abs( (int) $maybeint ) );closer now - isn't that always going to return thePHP_INT_MAXvalue? That would actively break things in a new way.
Good point. If $maybeint is 1, then the result will always be PHP_INT_MAX.
There was a problem hiding this comment.
Here's a test of this function with various inputs: https://3v4l.org/LNMss#veol
Note how a warning is issued in PHP 8.5+ when attempting to cast a float to an int when it is definitely too large or too small.
Notice also how getting an int cast of a string which is larger than PHP_MAX_INT will automatically clamp to PHP_MAX_INT without the warning.
There was a problem hiding this comment.
Another interesting test:
var_dump( abs( (int) ( PHP_INT_MAX * 2 ) ) );This produces:
int(0)
I don't understand why it is zero, when (int) ( PHP_INT_MAX * 2 ) produces float(9.223372036854776E+18).
In PHP 8.5, this warning is included:
Warning: The float 1.8446744073709552E+19 is not representable as an int, cast occurred
There was a problem hiding this comment.
Another interesting finding: https://3v4l.org/9avIi#v8.5.9
var_dump( abs( PHP_INT_MIN ) );
var_dump( abs( PHP_INT_MIN + 1 ) );Prints:
float(9.223372036854776E+18)
int(9223372036854775807)
This is surprising because the docs say the return type is:
The absolute value of num. If the argument num is of type float, the return type is also float, otherwise it is int (as float usually has a bigger value range than int).
I read this to mean that if you pass a float, you get a float. If you pass an int, get an int. But here there is an extreme edge case:
PHP_INT_MIN = -9223372036854775808
PHP_INT_MAX = +9223372036854775807
So the minimum cannot be flipped to positive and keep its absolute value as an integer. And this is what Dennis's comment is all about.
There was a problem hiding this comment.
Since casting a float to an int which is too large results in int( 0 ), I think we need to add a few special cases. How about this:
function absint( $maybeint ): int {
if ( ! is_numeric( $maybeint ) ) {
return 0;
}
if ( is_float( $maybeint ) ) {
$value = abs( $maybeint );
if ( $value >= PHP_INT_MAX ) {
// TODO: Trigger a warning? In PHP 8.5 this a warning like this would occur when casting a float to an int. "The float X is not representable as an int"
return PHP_INT_MAX;
} else {
return (int) $value;
}
}
// Convert numeric-string to int.
$value = (int) $maybeint;
// Special case for the one integer which cannot survive abs().
if ( PHP_INT_MIN === $value ) {
return PHP_INT_MAX;
}
return abs( $value );
}Test run: https://3v4l.org/vuYa8#veol
There was a problem hiding this comment.
Looking at return max( PHP_INT_MAX, abs( (int) $maybeint ) ); closer now - isn't that always going to return the PHP_INT_MAX value? That would actively break things in a new way.
right because that was a mistake and should have been min() instead of max.
@westonruter the float-to-int casting is handled properly with the explicit (int) cast. I think all of the extra code in your example is already done by abs(). are you trying to avoid the warning? in such a case, the warning actually still stands.
nonetheless I just wanted to note the surprise in the code and ask for a comment. my proposed code change was just icing on the cake.
|
Hi @josephscott . Nice potential bug revealing. Can you consider an option to cast $maybeint into a separate variable and check the new variable if it is float? This would avoid an unnecessary call to the abs() function. |
| 'PHP_INT_MIN int' => array( | ||
| 'test_value' => PHP_INT_MIN, | ||
| 'expected_value' => PHP_INT_MAX, | ||
| ), | ||
| 'PHP_INT_MIN string' => array( | ||
| 'test_value' => '-9223372036854775808', | ||
| 'expected_value' => PHP_INT_MAX, | ||
| ), | ||
| 'out of range negative' => array( | ||
| 'test_value' => '-99999999999999999999', | ||
| 'expected_value' => PHP_INT_MAX, | ||
| ), |
There was a problem hiding this comment.
Can you add the cases I've included at https://3v4l.org/LNMss#veol
This may involve expecting some warnings on PHP 8.5.
There was a problem hiding this comment.
I did some back and forth with Claude Fable 5 to refine the absint() code and write more tests. We've got more protections and more tests.
With more tests too
| } | ||
|
|
||
| if ( abs( $maybeint ) >= (float) PHP_INT_MAX ) { | ||
| return PHP_INT_MAX; |
There was a problem hiding this comment.
Should this trigger a warning as is done in PHP 8.5+ when attempting to cast a float larger than PHP_INT_MAX to an int?
| */ | ||
| function absint( $maybeint ): int { | ||
| return abs( (int) $maybeint ); | ||
| /* |
There was a problem hiding this comment.
What about something like this to short-circuit before even trying to go further?
| /* | |
| if ( ! is_float( $maybeint ) && ! is_int( $maybeint ) && ! is_numeric( $maybeint ) && ! is_bool( $maybeint ) ) { | |
| return 0; | |
| } | |
| /* |
Including bool here since true passed to abs() is 1, and false is 0.
There was a problem hiding this comment.
I think that would be a BC break, because that would return 0 for a string that PHP would have parsed into an int.
For example, send 999 stuff through absint() - https://3v4l.org/3M0Zf
|
The test failures are unrelated. See https://wordpress.slack.com/archives/C02RQBWTW/p1786058519099039?thread_ts=1786057730.714189&cid=C02RQBWTW Should be fixed by merging in the latest from |
Co-authored-by: Weston Ruter <westonruter@gmail.com>
Test using WordPress PlaygroundThe changes in this pull request can previewed and tested using a WordPress Playground instance. WordPress Playground is an experimental project that creates a full WordPress instance entirely within the browser. Some things to be aware of
For more details about these limitations and more, check out the Limitations page in the WordPress Playground documentation. |
dmsnell
left a comment
There was a problem hiding this comment.
this has grown in complexity and it seems like we are now doing a lot of work in slow user-space PHP code that abs() was already doing, and creating a bunch of new questions we have to answer, such as whether to create a user space warning.
am I missing something here? the original proposed fix covered all of the bases, but was simply a bit surprising
https://core.trac.wordpress.org/ticket/65826
AI assistance: Yes
Tool(s): Claude
Model(s): Opus 4.8
Used for: I had Claude update the tests
This Pull Request is for code review only. Please keep all other discussion in the Trac ticket. Do not merge this Pull Request. See GitHub Pull Requests for Code Review in the Core Handbook for more details.