Skip to content

ext/bcmath: bounds-check $precision in bcround() and Number::round()#22182

Open
edorian wants to merge 5 commits into
php:masterfrom
edorian:bc-math-out-of-bounds-precission
Open

ext/bcmath: bounds-check $precision in bcround() and Number::round()#22182
edorian wants to merge 5 commits into
php:masterfrom
edorian:bc-math-out-of-bounds-precission

Conversation

@edorian
Copy link
Copy Markdown
Member

@edorian edorian commented May 29, 2026

Fix the ASAN issue with entry points passing $precision to bc_round() unchecked, allowing PHP_INT_MAX / PHP_INT_MIN to trigger oversized allocations and signed overflow in libbcmath/src/round.c.


Transparency Note: The ASAN issue was found during LLM-based scanning. Code partially generated by tooling, reviewed and tested by me.

edorian added 2 commits May 29, 2026 15:30
Fix ASAN issue withh entry points passed $precision to bc_round()
unchecked, allowing PHP_INT_MAX / PHP_INT_MIN to trigger oversized
allocations and signed overflow in libbcmath/src/round.c.
  - remove the zend_always_inline
  - use INT_MIN instead of -INT_MAX
  - use % matching in the tests so they work on 32bit
Comment thread ext/bcmath/bcmath.c Outdated
@SakiTakamachi
Copy link
Copy Markdown
Member

Thank you for finding this.

That said, I think it may be better to fix the code in round.c instead. My reasoning is based on the responsibility of the code and where this behavior should ideally be handled.

I’d like to think through a possible solution.

@SakiTakamachi
Copy link
Copy Markdown
Member

@edorian

Sorry, but could you clarify on which specific lines in round.c the oversized allocations and signed overflow occurred?

In my environment, memory is exhausted first, so I was unable to reproduce the issue.

@edorian
Copy link
Copy Markdown
Member Author

edorian commented May 30, 2026

@edorian

Sorry, but could you clarify on which specific lines in round.c the oversized allocations and signed overflow occurred?

In my environment, memory is exhausted first, so I was unable to reproduce the issue.

Sorry for not posting the error message, @SakiTakamachi

Running the test:

php run-tests.php --show-diff ext/bcmath/tests/bcround_precision_bounds.phpt

No --asan, no env:

001+ Fatal error: Allowed memory size of 134217728 bytes exhausted at file:0 (tried to allocate 9223372036854775840 bytes) in /workspace/ext/bcmath/tests/bcround_precision_bounds.php on line 3

With --asan because of the values it sets, it also shows this:

001+ Fatal error: Allowed memory size of 134217728 bytes exhausted at file:0 (tried to allocate 9223372036854775840 bytes) in /workspace/ext/bcmath/tests/bcround_precision_bounds.php on line 3

The standalone reproducer for the Asan request allocation error:

env -i HOME="$HOME" PATH="$PATH" \
    USE_ZEND_ALLOC=0 \
    ASAN_OPTIONS='symbolize=1:strict_string_checks=1:detect_stack_use_after_return=1:detect_leaks=0:print_summary=1' \
    UBSAN_OPTIONS='print_stacktrace=1:print_summary=1' \
    php /workspace/run-tests.php --show-diff /workspace/ext/bcmath/tests/bcround_precision_bounds.phpt
001+ =================================================================
002+ ==7163==ERROR: AddressSanitizer: requested allocation size 0x8000000000000020 (0x8000000000001020 after adjustments for alignment, red zones etc.) exceeds maximum supported size of 0x10000000000 (thread T0)
003+     #0 0xffffb9a2b734 in __interceptor_malloc ../../../../src/libsanitizer/asan/asan_malloc_linux.cpp:69
004+     #1 0xaaaad539c5a8 in __zend_malloc /opt/php-src/Zend/zend_alloc.c:3543
005+     #2 0xaaaad5394ec4 in _emalloc /opt/php-src/Zend/zend_alloc.c:2778
006+     #3 0xaaaad2bf98e4 in _bc_new_num_nonzeroed_ex_internal /opt/php-src/ext/bcmath/libbcmath/src/init.c:50
007+     #4 0xaaaad2bf9be8 in _bc_new_num_ex /opt/php-src/ext/bcmath/libbcmath/src/init.c:64
008+     #5 0xaaaad2c02d98 in bc_round /opt/php-src/ext/bcmath/libbcmath/src/round.c:86
009+     #6 0xaaaad2bdda10 in zif_bcround /opt/php-src/ext/bcmath/bcmath.c:823
010+     #7 0xaaaad55e97cc in ZEND_DO_ICALL_SPEC_RETVAL_UNUSED_HANDLER /opt/php-src/Zend/zend_vm_execute.h:1322
011+     #8 0xaaaad58d9e5c in execute_ex /opt/php-src/Zend/zend_vm_execute.h:110417
012+     #9 0xaaaad58f7a1c in zend_execute /opt/php-src/Zend/zend_vm_execute.h:115616
013+     #10 0xaaaad5bbd470 in zend_execute_script /opt/php-src/Zend/zend.c:1972
014+     #11 0xaaaad4fe0b78 in php_execute_script_ex /opt/php-src/main/main.c:2646
015+     #12 0xaaaad4fe1174 in php_execute_script /opt/php-src/main/main.c:2686
016+     #13 0xaaaad5bc7800 in do_cli /opt/php-src/sapi/cli/php_cli.c:947
017+     #14 0xaaaad5bcb4ac in main /opt/php-src/sapi/cli/php_cli.c:1370
018+     #15 0xffffb5707740 in __libc_start_call_main ../sysdeps/nptl/libc_start_call_main.h:58
019+     #16 0xffffb5707814 in __libc_start_main_impl ../csu/libc-start.c:360
020+     #17 0xaaaad26476ec in _start (/usr/local/php/bin/php+0x48476ec)
021+
022+ ==7163==HINT: if you don't care about these errors you may set allocator_may_return_null=1
023+ SUMMARY: AddressSanitizer: allocation-size-too-big ../../../../src/libsanitizer/asan/asan_malloc_linux.cpp:69 in __interceptor_malloc
024+ ==7163==ABORTING

Either way, this shouldn't OOM/Asan I'd say?

@SakiTakamachi
Copy link
Copy Markdown
Member

SakiTakamachi commented May 30, 2026

@edorian

Thank you!

First, allocation failures can occur in other functions as well, so it is not immediately clear whether we should focus solely on resolving the issue in BCMath.

e.g. :

<?php
$a = str_repeat(rand(0, 9), PHP_INT_MAX);
$b = str_repeat(rand(0, 9), PHP_INT_MAX);

However, I noticed that a different issue occurs with code like the following:

<?php

$num = new BcMath\Number('1');
$num = $num->round(2147483648); // INT_MAX + 1
$num2 = new BcMath\Number($num->value);

// Fatal error: Uncaught ValueError: BcMath\Number::__construct(): Argument #1 ($num) must be between 0 and 2147483647

Since the maximum value of BCMath’s scale is defined as the maximum value of a 32-bit signed integer, this can happen.

For this issue, it would probably be best to check for it in bcmath.c and throw a ValueError.

Since there is no limit on the integer part, we should check whether precision > INT_MAX here.

edit:
In other words, among your initial commits, we only check for overflow in the positive direction.
It might also make sense to use ZEND_LONG_INT_OVFL.

@edorian
Copy link
Copy Markdown
Member Author

edorian commented May 30, 2026

First, allocation failures can occur in other functions as well, so it is not immediately clear whether we should focus solely on resolving the issue in BCMath.

I'm also ok with the resolution of this being a "not an issue / won't fix" resolution. The upside is minor and I don't want to be a burden here.

e.g. :

<?php
$a = str_repeat(rand(0, 9), PHP_INT_MAX);

php > $a = str_repeat(rand(0, 9), PHP_INT_MAX);
PHP Fatal error: Allowed memory size of 805306368 bytes exhausted (tried to allocate 9223372036854775839 bytes) in php shell code on line 1

That for me is a regular "OOM" error while round.c:74 should be a overflow (-precision + 1 on a zend_long), which is UB independent of allocation size?

If I'm mistaken here I apologize!

edit: In other words, among your initial commits, we only check for overflow in the positive direction. It might also make sense to use ZEND_LONG_INT_OVFL.

I don't follow on only looking at the positive direction. Precision may be negative number according to the docs. So ensuring bounds in both directions felt correct to me.


Would you suggest something liek this for round.c?

-    /* If precision is -3, it becomes 1000. */
-    if (UNEXPECTED(precision == ZEND_LONG_MIN)) {
-      *result = bc_new_num((size_t) ZEND_LONG_MAX + 2, 0);
-    } else {
-      *result = bc_new_num(-precision + 1, 0);
-    }
+    /* If precision is -3, it becomes 1000. Negate in unsigned space so
+     * precision == ZEND_LONG_MIN doesn't overflow signed long. */
+    zend_ulong magnitude = -(zend_ulong) precision;
+    *result = bc_new_num((size_t) magnitude + 1, 0);

While the tests for this run I can't really tell if that's the

On 32-bit, SIZEOF_INT == SIZEOF_ZEND_LONG and there's no zend_long value on 32-bit that can exceed INT_MAX
@edorian edorian force-pushed the bc-math-out-of-bounds-precission branch from 047d19b to da80c86 Compare May 30, 2026 18:02
@SakiTakamachi
Copy link
Copy Markdown
Member

@edorian

That for me is a regular "OOM" error while round.c:74 should be a overflow (-precision + 1 on a zend_long), which is UB independent of allocation size?

From my understanding, it doesn’t look like an overflow can occur at round.c:74.

Looking at the ASan output, I also don’t see anything that indicates an overflow occurred there.

Is there any additional log output that hasn’t been shared yet?

I don't follow on only looking at the positive direction. Precision may be negative number according to the docs. So ensuring bounds in both directions felt correct to me.

Sorry, my wording was unclear.

That said, my comment was based on the assumption that no overflow is actually occurring in round.c and that this is simply a regular OOM error. So I’d prefer to put this discussion on hold for now.

@edorian
Copy link
Copy Markdown
Member Author

edorian commented May 30, 2026

That said, my comment was based on the assumption that no overflow is actually occurring in round.c and that this is simply a regular OOM error. So I’d prefer to put this discussion on hold for now.

Here's a reproducer that hopefully makes sense?

git clone https://github.com/php/php-src && cd php-src
  ./buildconf
  ./configure --enable-debug --enable-undefined-sanitizer --enable-bcmath --disable-all
  make -j"$(nproc)"

  sapi/cli/php -r 'bcround("12345", -PHP_INT_MAX, RoundingMode::AwayFromZero);'

=>

[...]/php-src/bcmath-ub/php-src/ext/bcmath/libbcmath/src/round.c:74:14: runtime error: signed integer overflow: 9223372036854775807 + 1 cannot be represented in type 'zend_long' (aka 'long long')

SUMMARY: UndefinedBehaviorSanitizer: undefined-behavior [...]/php-src/bcmath-ub/php-src/ext/bcmath/libbcmath/src/round.c:74:14

[1] 64722 abort sapi/cli/php -r 'bcround("12345", -PHP_INT_MAX, RoundingMode::AwayFromZero);

--

That said: Please let me know how you'd like to proceed.

  • Close this PR or something else?
  • Follow up for round.c or no?

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants