From ae9fd517fe62bab1572416fc283cb577395eca74 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20D=C3=BCsterhus?= Date: Sun, 19 Oct 2025 15:45:30 +0200 Subject: [PATCH 1/2] standard: Handle negative zero in `min()`/`max()` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Positive and negative zero normally compare equal, but when specifically retrieving a minimum or maximum from a list of numbers, we should consider `-0.0` to be lower than `0.0` instead of treating them the same and returning the “first match”. Fixes php/php-src#20221 --- ext/standard/array.c | 18 +++++-- ext/standard/tests/math/gh20221.phpt | 80 ++++++++++++++++++++++++++++ 2 files changed, 93 insertions(+), 5 deletions(-) create mode 100644 ext/standard/tests/math/gh20221.phpt diff --git a/ext/standard/array.c b/ext/standard/array.c index d68a8030b704a..c3df15ec24e82 100644 --- a/ext/standard/array.c +++ b/ext/standard/array.c @@ -1170,7 +1170,15 @@ PHP_FUNCTION(key) static int php_data_compare(const void *f, const void *s) /* {{{ */ { - return zend_compare((zval*)f, (zval*)s); + zval *zf = (zval*)f; + zval *zs = (zval*)s; + + int result = zend_compare(zf, zs); + if (UNEXPECTED(result == 0 && Z_TYPE_P(zf) == IS_DOUBLE && Z_TYPE_P(zs) == IS_DOUBLE)) { + return signbit(Z_DVAL_P(zs)) - signbit(Z_DVAL_P(zf)); + } + + return result; } /* }}} */ @@ -1235,7 +1243,7 @@ PHP_FUNCTION(min) for (i = 1; i < argc; i++) { if (EXPECTED(Z_TYPE(args[i]) == IS_DOUBLE)) { double_compare: - if (min_dval > Z_DVAL(args[i])) { + if (min_dval > Z_DVAL(args[i]) || UNEXPECTED(signbit(Z_DVAL(args[i])) > signbit(min_dval))) { min_dval = Z_DVAL(args[i]); min = &args[i]; } @@ -1289,7 +1297,7 @@ ZEND_FRAMELESS_FUNCTION(min, 2) if (EXPECTED(Z_TYPE_P(rhs) == IS_DOUBLE)) { double_compare: - RETURN_COPY_VALUE(lhs_dval < Z_DVAL_P(rhs) ? lhs : rhs); + RETURN_COPY_VALUE(lhs_dval < Z_DVAL_P(rhs) || UNEXPECTED(signbit(lhs_dval) > signbit(Z_DVAL_P(rhs))) ? lhs : rhs); } else if (Z_TYPE_P(rhs) == IS_LONG && (zend_dval_to_lval_silent((double) Z_LVAL_P(rhs)) == Z_LVAL_P(rhs))) { /* if the value can be exactly represented as a double, use double dedicated code otherwise generic */ RETURN_COPY_VALUE(lhs_dval < (double)Z_LVAL_P(rhs) ? lhs : rhs); @@ -1363,7 +1371,7 @@ PHP_FUNCTION(max) for (i = 1; i < argc; i++) { if (EXPECTED(Z_TYPE(args[i]) == IS_DOUBLE)) { double_compare: - if (max_dval < Z_DVAL(args[i])) { + if (max_dval < Z_DVAL(args[i]) || UNEXPECTED(signbit(Z_DVAL(args[i])) < signbit(max_dval))) { max_dval = Z_DVAL(args[i]); max = &args[i]; } @@ -1417,7 +1425,7 @@ ZEND_FRAMELESS_FUNCTION(max, 2) if (EXPECTED(Z_TYPE_P(rhs) == IS_DOUBLE)) { double_compare: - RETURN_COPY_VALUE(lhs_dval >= Z_DVAL_P(rhs) ? lhs : rhs); + RETURN_COPY_VALUE(lhs_dval > Z_DVAL_P(rhs) || UNEXPECTED(signbit(lhs_dval) < signbit(Z_DVAL_P(rhs))) ? lhs : rhs); } else if (Z_TYPE_P(rhs) == IS_LONG && (zend_dval_to_lval_silent((double) Z_LVAL_P(rhs)) == Z_LVAL_P(rhs))) { /* if the value can be exactly represented as a double, use double dedicated code otherwise generic */ RETURN_COPY_VALUE(lhs_dval >= (double)Z_LVAL_P(rhs) ? lhs : rhs); diff --git a/ext/standard/tests/math/gh20221.phpt b/ext/standard/tests/math/gh20221.phpt new file mode 100644 index 0000000000000..f49f34b264dd0 --- /dev/null +++ b/ext/standard/tests/math/gh20221.phpt @@ -0,0 +1,80 @@ +--TEST-- +GH-20221: max/min return wrong value if signed (negative) zero +--FILE-- + +--EXPECT-- +min(-0, 0) = -0 +min([-0, 0]) = -0 +max(-0, 0) = 0 +max([-0, 0]) = 0 + +min(0, -0) = -0 +min([0, -0]) = -0 +max(0, -0) = 0 +max([0, -0]) = 0 + +====== + +min(-0, -0, 0) = -0 +min([-0, -0, 0]) = -0 +max(-0, -0, 0) = 0 +max([-0, -0, 0]) = 0 + +min(-0, 0, -0) = -0 +min([-0, 0, -0]) = -0 +max(-0, 0, -0) = 0 +max([-0, 0, -0]) = 0 + +min(-0, 0, 0) = -0 +min([-0, 0, 0]) = -0 +max(-0, 0, 0) = 0 +max([-0, 0, 0]) = 0 + +min(0, -0, -0) = -0 +min([0, -0, -0]) = -0 +max(0, -0, -0) = 0 +max([0, -0, -0]) = 0 + +min(0, -0, 0) = -0 +min([0, -0, 0]) = -0 +max(0, -0, 0) = 0 +max([0, -0, 0]) = 0 + +min(0, 0, -0) = -0 +min([0, 0, -0]) = -0 +max(0, 0, -0) = 0 +max([0, 0, -0]) = 0 From 0c4ae1216e6693db98a371dc448586764b3aa77d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20D=C3=BCsterhus?= Date: Sun, 19 Oct 2025 15:58:40 +0200 Subject: [PATCH 2/2] standard: Handle `min()`/`max()` for `float(-0.0)` and `int(0)` --- ext/standard/array.c | 18 ++++++++++++------ ext/standard/tests/math/gh20221.phpt | 10 ++++++++++ 2 files changed, 22 insertions(+), 6 deletions(-) diff --git a/ext/standard/array.c b/ext/standard/array.c index c3df15ec24e82..4bc8db212987b 100644 --- a/ext/standard/array.c +++ b/ext/standard/array.c @@ -1278,7 +1278,7 @@ ZEND_FRAMELESS_FUNCTION(min, 2) Z_FLF_PARAM_ZVAL(1, lhs); Z_FLF_PARAM_ZVAL(2, rhs); - double lhs_dval; + double lhs_dval, rhs_dval; if (Z_TYPE_P(lhs) == IS_LONG) { zend_long lhs_lval = Z_LVAL_P(lhs); @@ -1297,10 +1297,13 @@ ZEND_FRAMELESS_FUNCTION(min, 2) if (EXPECTED(Z_TYPE_P(rhs) == IS_DOUBLE)) { double_compare: - RETURN_COPY_VALUE(lhs_dval < Z_DVAL_P(rhs) || UNEXPECTED(signbit(lhs_dval) > signbit(Z_DVAL_P(rhs))) ? lhs : rhs); + rhs_dval = Z_DVAL_P(rhs); +double_compare2: + RETURN_COPY_VALUE(lhs_dval < rhs_dval || UNEXPECTED(signbit(lhs_dval) > signbit(rhs_dval)) ? lhs : rhs); } else if (Z_TYPE_P(rhs) == IS_LONG && (zend_dval_to_lval_silent((double) Z_LVAL_P(rhs)) == Z_LVAL_P(rhs))) { /* if the value can be exactly represented as a double, use double dedicated code otherwise generic */ - RETURN_COPY_VALUE(lhs_dval < (double)Z_LVAL_P(rhs) ? lhs : rhs); + rhs_dval = (double)Z_LVAL_P(rhs); + goto double_compare2; } else { goto generic_compare; } @@ -1406,7 +1409,7 @@ ZEND_FRAMELESS_FUNCTION(max, 2) Z_FLF_PARAM_ZVAL(1, lhs); Z_FLF_PARAM_ZVAL(2, rhs); - double lhs_dval; + double lhs_dval, rhs_dval; if (Z_TYPE_P(lhs) == IS_LONG) { zend_long lhs_lval = Z_LVAL_P(lhs); @@ -1425,10 +1428,13 @@ ZEND_FRAMELESS_FUNCTION(max, 2) if (EXPECTED(Z_TYPE_P(rhs) == IS_DOUBLE)) { double_compare: - RETURN_COPY_VALUE(lhs_dval > Z_DVAL_P(rhs) || UNEXPECTED(signbit(lhs_dval) < signbit(Z_DVAL_P(rhs))) ? lhs : rhs); + rhs_dval = Z_DVAL_P(rhs); +double_compare2: + RETURN_COPY_VALUE(lhs_dval > rhs_dval || UNEXPECTED(signbit(lhs_dval) < signbit(rhs_dval)) ? lhs : rhs); } else if (Z_TYPE_P(rhs) == IS_LONG && (zend_dval_to_lval_silent((double) Z_LVAL_P(rhs)) == Z_LVAL_P(rhs))) { /* if the value can be exactly represented as a double, use double dedicated code otherwise generic */ - RETURN_COPY_VALUE(lhs_dval >= (double)Z_LVAL_P(rhs) ? lhs : rhs); + rhs_dval = (double)Z_LVAL_P(rhs); + goto double_compare2; } else { goto generic_compare; } diff --git a/ext/standard/tests/math/gh20221.phpt b/ext/standard/tests/math/gh20221.phpt index f49f34b264dd0..c79d827feb0a7 100644 --- a/ext/standard/tests/math/gh20221.phpt +++ b/ext/standard/tests/math/gh20221.phpt @@ -35,6 +35,11 @@ foreach ([-0.0, 0.0] as $a) { } } +var_dump(min( 0, -0.0 )); +var_dump(min( -0.0, 0 )); +var_dump(max( 0, -0.0 )); +var_dump(max( -0.0, 0 )); + ?> --EXPECT-- min(-0, 0) = -0 @@ -78,3 +83,8 @@ min(0, 0, -0) = -0 min([0, 0, -0]) = -0 max(0, 0, -0) = 0 max([0, 0, -0]) = 0 + +float(-0) +float(-0) +int(0) +int(0)