Commit c0fa54f796f for php.net

commit c0fa54f796fa2687019c1efd0d1c59aca2252b1e
Author: Volker Dusch <volker@tideways-gmbh.com>
Date:   Fri May 29 15:28:38 2026 +0200

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

    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.

    closes #22182

diff --git a/NEWS b/NEWS
index 49b61d23696..ff552103ebc 100644
--- a/NEWS
+++ b/NEWS
@@ -2,6 +2,10 @@ PHP                                                                        NEWS
 |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
 ?? ??? ????, PHP 8.4.23

+- BCMath:
+  . Fixed issues with oversized allocations and signed overflow in bcround()
+    and BcMath\Number::round(). (edorian)
+
 - GD:
   . Fixed bug GH-22121 (Double free in gdImageSetStyle() after
     overflow-triggered early return). (iliaal)
diff --git a/UPGRADING b/UPGRADING
index 50c58169528..821fa4a723f 100644
--- a/UPGRADING
+++ b/UPGRADING
@@ -625,6 +625,10 @@ PHP 8.4 UPGRADE NOTES
 5. Changed Functions
 ========================================

+- BCMath:
+  . bcround() and BcMath\Number::round() now throw a ValueError when the precision
+    argument is out of range.
+
 - Core:
   . trigger_error() and user_error() now have a return type of true instead of
     bool.
diff --git a/ext/bcmath/bcmath.c b/ext/bcmath/bcmath.c
index 3628b95a78e..e54e5bfac77 100644
--- a/ext/bcmath/bcmath.c
+++ b/ext/bcmath/bcmath.c
@@ -157,6 +157,16 @@ static zend_always_inline zend_result bcmath_check_scale(zend_long scale, uint32
 	return SUCCESS;
 }

+static zend_result bcmath_check_precision(zend_long precision, uint32_t arg_num)
+{
+	if (ZEND_LONG_INT_OVFL(precision)) {
+		zend_argument_value_error(arg_num, "must be between " ZEND_LONG_FMT " and %d",
+			(zend_long) ZEND_LONG_MIN, INT_MAX);
+		return FAILURE;
+	}
+	return SUCCESS;
+}
+
 static void php_long2num(bc_num *num, zend_long lval)
 {
 	*num = bc_long2num(lval);
@@ -778,6 +788,10 @@ PHP_FUNCTION(bcround)
 		Z_PARAM_OBJ_OF_CLASS(mode_object, rounding_mode_ce)
 	ZEND_PARSE_PARAMETERS_END();

+	if (bcmath_check_precision(precision, 2) == FAILURE) {
+		RETURN_THROWS();
+	}
+
 	if (mode_object != NULL) {
 		mode = php_math_round_mode_from_enum(mode_object);
 	}
@@ -1784,6 +1798,10 @@ PHP_METHOD(BcMath_Number, round)
 		Z_PARAM_OBJ_OF_CLASS(mode_object, rounding_mode_ce);
 	ZEND_PARSE_PARAMETERS_END();

+	if (bcmath_check_precision(precision, 1) == FAILURE) {
+		RETURN_THROWS();
+	}
+
 	if (mode_object != NULL) {
 		rounding_mode = php_math_round_mode_from_enum(mode_object);
 	}
diff --git a/ext/bcmath/libbcmath/src/round.c b/ext/bcmath/libbcmath/src/round.c
index ac3c7c41a31..a495221ff00 100644
--- a/ext/bcmath/libbcmath/src/round.c
+++ b/ext/bcmath/libbcmath/src/round.c
@@ -70,12 +70,10 @@ void bc_round(bc_num num, zend_long precision, zend_long mode, bc_num *result)
 			return;
 		}

-		/* 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);
 		(*result)->n_value[0] = 1;
 		(*result)->n_sign = num->n_sign;
 		return;
diff --git a/ext/bcmath/tests/bcround_precision_bounds.phpt b/ext/bcmath/tests/bcround_precision_bounds.phpt
new file mode 100644
index 00000000000..4c4f910476a
--- /dev/null
+++ b/ext/bcmath/tests/bcround_precision_bounds.phpt
@@ -0,0 +1,28 @@
+--TEST--
+bcround() and BcMath\Number::round() reject $precision above INT_MAX
+--EXTENSIONS--
+bcmath
+--SKIPIF--
+<?php if (PHP_INT_SIZE != 8) die("skip: 64-bit only"); ?>
+--FILE--
+<?php
+try {
+    bcround('1', PHP_INT_MAX);
+} 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;
+}
+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 %d
+BcMath\Number::round(): Argument #1 ($precision) must be between %i and %d
+BcMath\Number::round(): Argument #1 ($precision) must be between %i and %d