Commit 0fb3d658dc3 for php.net

commit 0fb3d658dc3264d3288353a641cce2c6adeba1bc
Author: Ilia Alshanetsky <ilia@ilia.ws>
Date:   Fri Jun 26 06:42:53 2026 -0400

    ext/sodium: throw ValueError for pwhash argument errors (#22388)

    The four password-hashing functions reported out-of-range arguments (a
    non-positive or below-minimum opslimit or memlimit, an oversized hash
    length or password, a wrong-length salt) as a SodiumException. These are
    argument-value errors, so throw ValueError via zend_argument_value_error()
    instead, matching the rest of the engine. SodiumException is still used
    for internal libsodium failures.

    SodiumException's create_object empties the whole backtrace, which also
    protects caller frames holding the password; a plain ValueError does not,
    so each converted site keeps an explicit
    sodium_remove_param_values_from_backtrace(EG(exception)), mirroring the
    ZPP-failure paths.

diff --git a/NEWS b/NEWS
index f13f2e00ca4..9d38664136a 100644
--- a/NEWS
+++ b/NEWS
@@ -230,6 +230,8 @@ PHP                                                                        NEWS

 - Sodium:
   . Added support for libsodium 1.0.21 IPcrypt and XOF APIs. (jedisct1)
+  . pwhash argument-validation errors now throw ValueError instead of
+    SodiumException. (iliaal)

 - SPL:
   . DirectoryIterator key can now work better with filesystem supporting larger
diff --git a/UPGRADING b/UPGRADING
index ae0e3afc97a..58bf1f9c9a1 100644
--- a/UPGRADING
+++ b/UPGRADING
@@ -127,6 +127,15 @@ PHP 8.6 UPGRADE NOTES
     occurrence constraints and integer restriction facets. Negative minOccurs
     and maxOccurs values are rejected as well.

+- Sodium:
+  . The password-hashing functions sodium_crypto_pwhash(),
+    sodium_crypto_pwhash_str(),
+    sodium_crypto_pwhash_scryptsalsa208sha256() and
+    sodium_crypto_pwhash_scryptsalsa208sha256_str() now throw ValueError
+    instead of SodiumException when an argument is out of range, such as an
+    opslimit or memlimit below the documented minimum. SodiumException is
+    still thrown for internal libsodium failures.
+
 - SPL:
   . SplObjectStorage::getHash() implementations may no longer mutate any
     SplObjectStorage instance. Attempting to do so now throws an Error.
diff --git a/ext/sodium/libsodium.c b/ext/sodium/libsodium.c
index bd246abb53a..8c85991150b 100644
--- a/ext/sodium/libsodium.c
+++ b/ext/sodium/libsodium.c
@@ -1429,23 +1429,28 @@ PHP_FUNCTION(sodium_crypto_pwhash)
 		RETURN_THROWS();
 	}
 	if (hash_len <= 0) {
-		zend_argument_error(sodium_exception_ce, 1, "must be greater than 0");
+		zend_argument_value_error(1, "must be greater than 0");
+		sodium_remove_param_values_from_backtrace(EG(exception));
 		RETURN_THROWS();
 	}
 	if (hash_len >= 0xffffffff) {
-		zend_argument_error(sodium_exception_ce, 1, "is too large");
+		zend_argument_value_error(1, "must be less than 4294967295 bytes");
+		sodium_remove_param_values_from_backtrace(EG(exception));
 		RETURN_THROWS();
 	}
 	if (passwd_len >= 0xffffffff) {
-		zend_argument_error(sodium_exception_ce, 2, "is too long");
+		zend_argument_value_error(2, "must be less than 4294967295 bytes");
+		sodium_remove_param_values_from_backtrace(EG(exception));
 		RETURN_THROWS();
 	}
 	if (opslimit <= 0) {
-		zend_argument_error(sodium_exception_ce, 4, "must be greater than 0");
+		zend_argument_value_error(4, "must be greater than 0");
+		sodium_remove_param_values_from_backtrace(EG(exception));
 		RETURN_THROWS();
 	}
 	if (memlimit <= 0 || memlimit > SIZE_MAX) {
-		zend_argument_error(sodium_exception_ce, 5, "must be greater than 0");
+		zend_argument_value_error(5, "must be greater than 0");
+		sodium_remove_param_values_from_backtrace(EG(exception));
 		RETURN_THROWS();
 	}
 	if (alg != crypto_pwhash_ALG_ARGON2I13
@@ -1460,15 +1465,18 @@ PHP_FUNCTION(sodium_crypto_pwhash)
 		zend_error(E_WARNING, "empty password");
 	}
 	if (salt_len != crypto_pwhash_SALTBYTES) {
-		zend_argument_error(sodium_exception_ce, 3, "must be SODIUM_CRYPTO_PWHASH_SALTBYTES bytes long");
+		zend_argument_value_error(3, "must be SODIUM_CRYPTO_PWHASH_SALTBYTES bytes long");
+		sodium_remove_param_values_from_backtrace(EG(exception));
 		RETURN_THROWS();
 	}
 	if (opslimit < crypto_pwhash_OPSLIMIT_MIN) {
-		zend_argument_error(sodium_exception_ce, 4, "must be greater than or equal to %d", crypto_pwhash_OPSLIMIT_MIN);
+		zend_argument_value_error(4, "must be greater than or equal to %d", crypto_pwhash_OPSLIMIT_MIN);
+		sodium_remove_param_values_from_backtrace(EG(exception));
 		RETURN_THROWS();
 	}
 	if (memlimit < crypto_pwhash_MEMLIMIT_MIN) {
-		zend_argument_error(sodium_exception_ce, 5, "must be greater than or equal to %d", crypto_pwhash_MEMLIMIT_MIN);
+		zend_argument_value_error(5, "must be greater than or equal to %d", crypto_pwhash_MEMLIMIT_MIN);
+		sodium_remove_param_values_from_backtrace(EG(exception));
 		RETURN_THROWS();
 	}
 	hash = zend_string_alloc((size_t) hash_len, 0);
@@ -1513,26 +1521,31 @@ PHP_FUNCTION(sodium_crypto_pwhash_str)
 		RETURN_THROWS();
 	}
 	if (opslimit <= 0) {
-		zend_argument_error(sodium_exception_ce, 2, "must be greater than 0");
+		zend_argument_value_error(2, "must be greater than 0");
+		sodium_remove_param_values_from_backtrace(EG(exception));
 		RETURN_THROWS();
 	}
 	if (memlimit <= 0 || memlimit > SIZE_MAX) {
-		zend_argument_error(sodium_exception_ce, 3, "must be greater than 0");
+		zend_argument_value_error(3, "must be greater than 0");
+		sodium_remove_param_values_from_backtrace(EG(exception));
 		RETURN_THROWS();
 	}
 	if (passwd_len >= 0xffffffff) {
-		zend_argument_error(sodium_exception_ce, 1, "is too long");
+		zend_argument_value_error(1, "must be less than 4294967295 bytes");
+		sodium_remove_param_values_from_backtrace(EG(exception));
 		RETURN_THROWS();
 	}
 	if (passwd_len <= 0) {
 		zend_error(E_WARNING, "empty password");
 	}
 	if (opslimit < crypto_pwhash_OPSLIMIT_MIN) {
-		zend_argument_error(sodium_exception_ce, 2, "must be greater than or equal to %d", crypto_pwhash_OPSLIMIT_MIN);
+		zend_argument_value_error(2, "must be greater than or equal to %d", crypto_pwhash_OPSLIMIT_MIN);
+		sodium_remove_param_values_from_backtrace(EG(exception));
 		RETURN_THROWS();
 	}
 	if (memlimit < crypto_pwhash_MEMLIMIT_MIN) {
-		zend_argument_error(sodium_exception_ce, 3, "must be greater than or equal to %d", crypto_pwhash_MEMLIMIT_MIN);
+		zend_argument_value_error(3, "must be greater than or equal to %d", crypto_pwhash_MEMLIMIT_MIN);
+		sodium_remove_param_values_from_backtrace(EG(exception));
 		RETURN_THROWS();
 	}
 	hash_str = zend_string_alloc(crypto_pwhash_STRBYTES - 1, 0);
@@ -1619,30 +1632,36 @@ PHP_FUNCTION(sodium_crypto_pwhash_scryptsalsa208sha256)
 		RETURN_THROWS();
 	}
 	if (hash_len <= 0 || hash_len >= ZSTR_MAX_LEN || hash_len > 0x1fffffffe0ULL) {
-		zend_argument_error(sodium_exception_ce, 1, "must be greater than 0");
+		zend_argument_value_error(1, "must be greater than 0");
+		sodium_remove_param_values_from_backtrace(EG(exception));
 		RETURN_THROWS();
 	}
 	if (opslimit <= 0) {
-		zend_argument_error(sodium_exception_ce, 4, "must be greater than 0");
+		zend_argument_value_error(4, "must be greater than 0");
+		sodium_remove_param_values_from_backtrace(EG(exception));
 		RETURN_THROWS();
 	}
 	if (memlimit <= 0 || memlimit > SIZE_MAX) {
-		zend_argument_error(sodium_exception_ce, 5, "must be greater than 0");
+		zend_argument_value_error(5, "must be greater than 0");
+		sodium_remove_param_values_from_backtrace(EG(exception));
 		RETURN_THROWS();
 	}
 	if (passwd_len <= 0) {
 		zend_error(E_WARNING, "empty password");
 	}
 	if (salt_len != crypto_pwhash_scryptsalsa208sha256_SALTBYTES) {
-		zend_argument_error(sodium_exception_ce, 3, "must be SODIUM_CRYPTO_PWHASH_SCRYPTSALSA208SHA256_SALTBYTES bytes long");
+		zend_argument_value_error(3, "must be SODIUM_CRYPTO_PWHASH_SCRYPTSALSA208SHA256_SALTBYTES bytes long");
+		sodium_remove_param_values_from_backtrace(EG(exception));
 		RETURN_THROWS();
 	}
 	if (opslimit < crypto_pwhash_scryptsalsa208sha256_OPSLIMIT_INTERACTIVE) {
-		zend_argument_error(sodium_exception_ce, 4, "must be greater than or equal to %d", crypto_pwhash_scryptsalsa208sha256_OPSLIMIT_INTERACTIVE);
+		zend_argument_value_error(4, "must be greater than or equal to %d", crypto_pwhash_scryptsalsa208sha256_OPSLIMIT_INTERACTIVE);
+		sodium_remove_param_values_from_backtrace(EG(exception));
 		RETURN_THROWS();
 	}
 	if (memlimit < crypto_pwhash_scryptsalsa208sha256_MEMLIMIT_INTERACTIVE) {
-		zend_argument_error(sodium_exception_ce, 5, "must be greater than or equal to %d", crypto_pwhash_scryptsalsa208sha256_MEMLIMIT_INTERACTIVE);
+		zend_argument_value_error(5, "must be greater than or equal to %d", crypto_pwhash_scryptsalsa208sha256_MEMLIMIT_INTERACTIVE);
+		sodium_remove_param_values_from_backtrace(EG(exception));
 		RETURN_THROWS();
 	}
 	hash = zend_string_alloc((size_t) hash_len, 0);
@@ -1674,22 +1693,26 @@ PHP_FUNCTION(sodium_crypto_pwhash_scryptsalsa208sha256_str)
 		RETURN_THROWS();
 	}
 	if (opslimit <= 0) {
-		zend_argument_error(sodium_exception_ce, 2, "must be greater than 0");
+		zend_argument_value_error(2, "must be greater than 0");
+		sodium_remove_param_values_from_backtrace(EG(exception));
 		RETURN_THROWS();
 	}
 	if (memlimit <= 0 || memlimit > SIZE_MAX) {
-		zend_argument_error(sodium_exception_ce, 3, "must be greater than 0");
+		zend_argument_value_error(3, "must be greater than 0");
+		sodium_remove_param_values_from_backtrace(EG(exception));
 		RETURN_THROWS();
 	}
 	if (passwd_len <= 0) {
 		zend_error(E_WARNING, "empty password");
 	}
 	if (opslimit < crypto_pwhash_scryptsalsa208sha256_OPSLIMIT_INTERACTIVE) {
-		zend_argument_error(sodium_exception_ce, 2, "must be greater than or equal to %d", crypto_pwhash_scryptsalsa208sha256_OPSLIMIT_INTERACTIVE);
+		zend_argument_value_error(2, "must be greater than or equal to %d", crypto_pwhash_scryptsalsa208sha256_OPSLIMIT_INTERACTIVE);
+		sodium_remove_param_values_from_backtrace(EG(exception));
 		RETURN_THROWS();
 	}
 	if (memlimit < crypto_pwhash_scryptsalsa208sha256_MEMLIMIT_INTERACTIVE) {
-		zend_argument_error(sodium_exception_ce, 3, "must be greater than or equal to %d", crypto_pwhash_scryptsalsa208sha256_MEMLIMIT_INTERACTIVE);
+		zend_argument_value_error(3, "must be greater than or equal to %d", crypto_pwhash_scryptsalsa208sha256_MEMLIMIT_INTERACTIVE);
+		sodium_remove_param_values_from_backtrace(EG(exception));
 		RETURN_THROWS();
 	}
 	hash_str = zend_string_alloc
diff --git a/ext/sodium/tests/pwhash_memlimit_below_min.phpt b/ext/sodium/tests/pwhash_memlimit_below_min.phpt
index 63bf4443939..8913afb382f 100644
--- a/ext/sodium/tests/pwhash_memlimit_below_min.phpt
+++ b/ext/sodium/tests/pwhash_memlimit_below_min.phpt
@@ -12,13 +12,13 @@

 try {
     sodium_crypto_pwhash(32, "password", $salt, SODIUM_CRYPTO_PWHASH_OPSLIMIT_INTERACTIVE, 1);
-} catch (SodiumException $e) {
+} catch (\ValueError $e) {
     echo $e->getMessage(), "\n";
 }

 try {
     sodium_crypto_pwhash_str("password", SODIUM_CRYPTO_PWHASH_OPSLIMIT_INTERACTIVE, 1);
-} catch (SodiumException $e) {
+} catch (\ValueError $e) {
     echo $e->getMessage(), "\n";
 }
 ?>
diff --git a/ext/sodium/tests/pwhash_valueerror_scrub.phpt b/ext/sodium/tests/pwhash_valueerror_scrub.phpt
new file mode 100644
index 00000000000..1cbd776e183
--- /dev/null
+++ b/ext/sodium/tests/pwhash_valueerror_scrub.phpt
@@ -0,0 +1,25 @@
+--TEST--
+sodium pwhash argument errors throw ValueError and keep the whole backtrace scrubbed
+--EXTENSIONS--
+sodium
+--SKIPIF--
+<?php
+if (!defined('SODIUM_CRYPTO_PWHASH_SALTBYTES')) print "skip libsodium without argon2";
+?>
+--FILE--
+<?php
+function wrap(string $password): void
+{
+    sodium_crypto_pwhash_str($password, SODIUM_CRYPTO_PWHASH_OPSLIMIT_INTERACTIVE, 1);
+}
+
+$secret = "hunter2-secret";
+wrap($secret);
+?>
+--EXPECTF--
+Fatal error: Uncaught ValueError: sodium_crypto_pwhash_str(): Argument #3 ($memlimit) must be greater than or equal to %d in %s:%d
+Stack trace:
+#0 %s(%d): sodium_crypto_pwhash_str()
+#1 %s(%d): wrap()
+#2 {main}
+  thrown in %s on line %d