From 89e383d042c1e76e6feae542e1f64748bacf79ef Mon Sep 17 00:00:00 2001 From: Volker Dusch Date: Fri, 29 May 2026 15:28:38 +0200 Subject: [PATCH 1/5] ext/bcmath: bounds-check $precision in bcround() and Number::round() 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. --- ext/bcmath/bcmath.c | 17 ++++++++++ .../tests/bcround_precision_bounds.phpt | 32 +++++++++++++++++++ 2 files changed, 49 insertions(+) create mode 100644 ext/bcmath/tests/bcround_precision_bounds.phpt diff --git a/ext/bcmath/bcmath.c b/ext/bcmath/bcmath.c index 2e2d80f76f95..41ceb550f90e 100644 --- a/ext/bcmath/bcmath.c +++ b/ext/bcmath/bcmath.c @@ -155,6 +155,15 @@ static zend_always_inline zend_result bcmath_check_scale(zend_long scale, uint32 return SUCCESS; } +static zend_always_inline zend_result bcmath_check_precision(zend_long precision, uint32_t arg_num) +{ + if (UNEXPECTED(precision < -(zend_long) INT_MAX || precision > INT_MAX)) { + zend_argument_value_error(arg_num, "must be between %d and %d", -INT_MAX, INT_MAX); + return FAILURE; + } + return SUCCESS; +} + static void php_long2num(bc_num *num, zend_long lval) { *num = bc_long2num(lval); @@ -795,6 +804,10 @@ PHP_FUNCTION(bcround) Z_PARAM_ENUM(rounding_mode, rounding_mode_ce) ZEND_PARSE_PARAMETERS_END(); + if (bcmath_check_precision(precision, 2) == FAILURE) { + RETURN_THROWS(); + } + switch (rounding_mode) { case ZEND_ENUM_RoundingMode_HalfAwayFromZero: case ZEND_ENUM_RoundingMode_HalfTowardsZero: @@ -1794,6 +1807,10 @@ PHP_METHOD(BcMath_Number, round) Z_PARAM_ENUM(rounding_mode, rounding_mode_ce); ZEND_PARSE_PARAMETERS_END(); + if (bcmath_check_precision(precision, 1) == FAILURE) { + RETURN_THROWS(); + } + switch (rounding_mode) { case ZEND_ENUM_RoundingMode_HalfAwayFromZero: case ZEND_ENUM_RoundingMode_HalfTowardsZero: diff --git a/ext/bcmath/tests/bcround_precision_bounds.phpt b/ext/bcmath/tests/bcround_precision_bounds.phpt new file mode 100644 index 000000000000..9d01bef98c45 --- /dev/null +++ b/ext/bcmath/tests/bcround_precision_bounds.phpt @@ -0,0 +1,32 @@ +--TEST-- +bcround() and BcMath\Number::round() reject out-of-range $precision +--EXTENSIONS-- +bcmath +--FILE-- +getMessage() . \PHP_EOL; +} +try { + bcround('12345', -PHP_INT_MAX, RoundingMode::AwayFromZero); +} catch (\ValueError $e) { + echo $e->getMessage() . \PHP_EOL; +} +try { + bcround('12345', PHP_INT_MIN, RoundingMode::AwayFromZero); +} catch (\ValueError $e) { + echo $e->getMessage() . \PHP_EOL; +} +try { + (new BcMath\Number('1'))->round(PHP_INT_MAX); +} catch (\ValueError $e) { + echo $e->getMessage() . \PHP_EOL; +} +?> +--EXPECT-- +bcround(): Argument #2 ($precision) must be between -2147483647 and 2147483647 +bcround(): Argument #2 ($precision) must be between -2147483647 and 2147483647 +bcround(): Argument #2 ($precision) must be between -2147483647 and 2147483647 +BcMath\Number::round(): Argument #1 ($precision) must be between -2147483647 and 2147483647 From 5b86f5dddab959185f1df04af58926dbf55042e0 Mon Sep 17 00:00:00 2001 From: Volker Dusch Date: Fri, 29 May 2026 15:37:31 +0200 Subject: [PATCH 2/5] Inital review feedback - remove the zend_always_inline - use INT_MIN instead of -INT_MAX - use % matching in the tests so they work on 32bit --- ext/bcmath/bcmath.c | 6 +++--- ext/bcmath/tests/bcround_precision_bounds.phpt | 10 +++++----- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/ext/bcmath/bcmath.c b/ext/bcmath/bcmath.c index 41ceb550f90e..9d1eb4e8bf1d 100644 --- a/ext/bcmath/bcmath.c +++ b/ext/bcmath/bcmath.c @@ -155,10 +155,10 @@ static zend_always_inline zend_result bcmath_check_scale(zend_long scale, uint32 return SUCCESS; } -static zend_always_inline zend_result bcmath_check_precision(zend_long precision, uint32_t arg_num) +static zend_result bcmath_check_precision(zend_long precision, uint32_t arg_num) { - if (UNEXPECTED(precision < -(zend_long) INT_MAX || precision > INT_MAX)) { - zend_argument_value_error(arg_num, "must be between %d and %d", -INT_MAX, INT_MAX); + if (UNEXPECTED(precision < INT_MIN || precision > INT_MAX)) { + zend_argument_value_error(arg_num, "must be between %d and %d", INT_MIN, INT_MAX); return FAILURE; } return SUCCESS; diff --git a/ext/bcmath/tests/bcround_precision_bounds.phpt b/ext/bcmath/tests/bcround_precision_bounds.phpt index 9d01bef98c45..9769760a79cd 100644 --- a/ext/bcmath/tests/bcround_precision_bounds.phpt +++ b/ext/bcmath/tests/bcround_precision_bounds.phpt @@ -25,8 +25,8 @@ try { echo $e->getMessage() . \PHP_EOL; } ?> ---EXPECT-- -bcround(): Argument #2 ($precision) must be between -2147483647 and 2147483647 -bcround(): Argument #2 ($precision) must be between -2147483647 and 2147483647 -bcround(): Argument #2 ($precision) must be between -2147483647 and 2147483647 -BcMath\Number::round(): Argument #1 ($precision) must be between -2147483647 and 2147483647 +--EXPECTF-- +bcround(): Argument #2 ($precision) must be between %i and %i +bcround(): Argument #2 ($precision) must be between %i and %i +bcround(): Argument #2 ($precision) must be between %i and %i +BcMath\Number::round(): Argument #1 ($precision) must be between %i and %i From e508ea640d86a037002c6ec4df0c0d79be421093 Mon Sep 17 00:00:00 2001 From: Volker Dusch Date: Fri, 29 May 2026 16:01:55 +0200 Subject: [PATCH 3/5] Use ZEND_LONG_EXCEEDS_INT over manual bound checking --- ext/bcmath/bcmath.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ext/bcmath/bcmath.c b/ext/bcmath/bcmath.c index 9d1eb4e8bf1d..536fb1a7bf06 100644 --- a/ext/bcmath/bcmath.c +++ b/ext/bcmath/bcmath.c @@ -157,7 +157,7 @@ static zend_always_inline zend_result bcmath_check_scale(zend_long scale, uint32 static zend_result bcmath_check_precision(zend_long precision, uint32_t arg_num) { - if (UNEXPECTED(precision < INT_MIN || precision > INT_MAX)) { + if (ZEND_LONG_EXCEEDS_INT(precision)) { zend_argument_value_error(arg_num, "must be between %d and %d", INT_MIN, INT_MAX); return FAILURE; } From fcb31a45d30b7ac9d54c76e640d5f74ebb450dc1 Mon Sep 17 00:00:00 2001 From: Volker Dusch Date: Sat, 30 May 2026 19:41:29 +0200 Subject: [PATCH 4/5] Add test case from review --- ext/bcmath/tests/bcround_precision_bounds.phpt | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/ext/bcmath/tests/bcround_precision_bounds.phpt b/ext/bcmath/tests/bcround_precision_bounds.phpt index 9769760a79cd..dd974ce8e8dc 100644 --- a/ext/bcmath/tests/bcround_precision_bounds.phpt +++ b/ext/bcmath/tests/bcround_precision_bounds.phpt @@ -24,9 +24,15 @@ try { } catch (\ValueError $e) { echo $e->getMessage() . \PHP_EOL; } +try { + (new BcMath\Number('1'))->round(2147483648); // INT_MAX + 1 +} catch (\ValueError $e) { + echo $e->getMessage() . \PHP_EOL; +} ?> --EXPECTF-- bcround(): Argument #2 ($precision) must be between %i and %i bcround(): Argument #2 ($precision) must be between %i and %i bcround(): Argument #2 ($precision) must be between %i and %i BcMath\Number::round(): Argument #1 ($precision) must be between %i and %i +BcMath\Number::round(): Argument #1 ($precision) must be between %i and %i From da80c86892ea23669fb4db40e6bcd1e145765723 Mon Sep 17 00:00:00 2001 From: Volker Dusch Date: Sat, 30 May 2026 19:45:55 +0200 Subject: [PATCH 5/5] Skip 32bit testing as bug60377.phpt does On 32-bit, SIZEOF_INT == SIZEOF_ZEND_LONG and there's no zend_long value on 32-bit that can exceed INT_MAX --- ext/bcmath/tests/bcround_precision_bounds.phpt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/ext/bcmath/tests/bcround_precision_bounds.phpt b/ext/bcmath/tests/bcround_precision_bounds.phpt index dd974ce8e8dc..8fbd7da4a4a5 100644 --- a/ext/bcmath/tests/bcround_precision_bounds.phpt +++ b/ext/bcmath/tests/bcround_precision_bounds.phpt @@ -2,6 +2,8 @@ bcround() and BcMath\Number::round() reject out-of-range $precision --EXTENSIONS-- bcmath +--SKIPIF-- + --FILE--