Skip to content

Avoid fatal error condition in absint() - #12917

Open
josephscott wants to merge 5 commits into
WordPress:trunkfrom
josephscott:65826/absint-check-return-value
Open

Avoid fatal error condition in absint()#12917
josephscott wants to merge 5 commits into
WordPress:trunkfrom
josephscott:65826/absint-check-return-value

Conversation

@josephscott

Copy link
Copy Markdown
Contributor

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.

@github-actions

github-actions Bot commented Aug 6, 2026

Copy link
Copy Markdown

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 props-bot label.

Core Committers: Use this line as a base for the props when committing in SVN:

Props josephscott, dmsnell, westonruter, sawf1y.

To understand the WordPress project's expectations around crediting contributors, please review the Contributor Attribution page in the Core Handbook.

Comment thread src/wp-includes/load.php Outdated
return abs( (int) $maybeint );
$abs = abs( (int) $maybeint );

// abs() can return a float.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

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 ) );

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

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.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

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.

Good point. If $maybeint is 1, then the result will always be PHP_INT_MAX.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

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.

@westonruter westonruter Aug 6, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

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

See https://3v4l.org/3ZjEH#veol

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

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.

@westonruter westonruter Aug 6, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

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

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

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.

@sawfly

sawfly commented Aug 6, 2026

Copy link
Copy Markdown

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.

Comment on lines +80 to +91
'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,
),

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Can you add the cases I've included at https://3v4l.org/LNMss#veol

This may involve expecting some warnings on PHP 8.5.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

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
Comment thread src/wp-includes/load.php
}

if ( abs( $maybeint ) >= (float) PHP_INT_MAX ) {
return PHP_INT_MAX;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

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?

Comment thread src/wp-includes/load.php
*/
function absint( $maybeint ): int {
return abs( (int) $maybeint );
/*

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

What about something like this to short-circuit before even trying to go further?

Suggested change
/*
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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

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

Comment thread src/wp-includes/load.php Outdated
@westonruter

Copy link
Copy Markdown
Member

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 trunk.

@github-actions

github-actions Bot commented Aug 7, 2026

Copy link
Copy Markdown

Test using WordPress Playground

The 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

  • All changes will be lost when closing a tab with a Playground instance.
  • All changes will be lost when refreshing the page.
  • A fresh instance is created each time the link below is clicked.
  • Every time this pull request is updated, a new ZIP file containing all changes is created. If changes are not reflected in the Playground instance,
    it's possible that the most recent build failed, or has not completed. Check the list of workflow runs to be sure.

For more details about these limitations and more, check out the Limitations page in the WordPress Playground documentation.

Test this pull request with WordPress Playground.

@dmsnell dmsnell left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants