From c1cf1c5087e4679b2dd9d04ce4ff703f35ce69b0 Mon Sep 17 00:00:00 2001 From: Ilia Alshanetsky Date: Sun, 21 Jun 2026 13:43:27 -0400 Subject: [PATCH] ext/sodium: throw ValueError for pwhash argument errors 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. --- NEWS | 2 + UPGRADING | 9 +++ ext/sodium/libsodium.c | 69 ++++++++++++------- .../tests/pwhash_memlimit_below_min.phpt | 4 +- ext/sodium/tests/pwhash_valueerror_scrub.phpt | 25 +++++++ 5 files changed, 84 insertions(+), 25 deletions(-) create mode 100644 ext/sodium/tests/pwhash_valueerror_scrub.phpt diff --git a/NEWS b/NEWS index c7645ca27b8a..8aa9440bdb16 100644 --- a/NEWS +++ b/NEWS @@ -220,6 +220,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 c77bfbfa4e02..275ea07ec129 100644 --- a/UPGRADING +++ b/UPGRADING @@ -128,6 +128,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 bd246abb53a5..8c85991150b5 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 63bf4443939b..8913afb382f0 100644 --- a/ext/sodium/tests/pwhash_memlimit_below_min.phpt +++ b/ext/sodium/tests/pwhash_memlimit_below_min.phpt @@ -12,13 +12,13 @@ $salt = str_repeat("a", SODIUM_CRYPTO_PWHASH_SALTBYTES); 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 000000000000..1cbd776e1839 --- /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-- + +--FILE-- + +--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