Commit df7fd972127 for php.net
commit df7fd972127bb4d0070d4297c0ccd45419be966a
Author: Ilia Alshanetsky <ilia@ilia.ws>
Date: Sat Jun 20 21:52:25 2026 -0400
Throw on below-minimum opslimit/memlimit in sodium pwhash
The four sodium pwhash functions queued a zend_argument_error for an
opslimit or memlimit below the documented minimum but fell through to the
KDF instead of returning. When libsodium rejects the value the precise
argument error is clobbered by a generic "internal error"; when it
accepts the value the full KDF runs before the queued error surfaces,
defeating the minimum-cost gate. Add the missing RETURN_THROWS() so each
lower-bound check returns like its sibling upper-bound branches.
Closes GH-22383
diff --git a/ext/sodium/libsodium.c b/ext/sodium/libsodium.c
index 7cabc2b325f..57a34d5ec84 100644
--- a/ext/sodium/libsodium.c
+++ b/ext/sodium/libsodium.c
@@ -1473,6 +1473,7 @@ PHP_FUNCTION(sodium_crypto_pwhash)
}
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);
+ RETURN_THROWS();
}
hash = zend_string_alloc((size_t) hash_len, 0);
ret = -1;
@@ -1532,9 +1533,11 @@ PHP_FUNCTION(sodium_crypto_pwhash_str)
}
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);
+ 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);
+ RETURN_THROWS();
}
hash_str = zend_string_alloc(crypto_pwhash_STRBYTES - 1, 0);
if (crypto_pwhash_str
@@ -1640,9 +1643,11 @@ PHP_FUNCTION(sodium_crypto_pwhash_scryptsalsa208sha256)
}
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);
+ 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);
+ RETURN_THROWS();
}
hash = zend_string_alloc((size_t) hash_len, 0);
if (crypto_pwhash_scryptsalsa208sha256
@@ -1685,9 +1690,11 @@ PHP_FUNCTION(sodium_crypto_pwhash_scryptsalsa208sha256_str)
}
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);
+ 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);
+ RETURN_THROWS();
}
hash_str = zend_string_alloc
(crypto_pwhash_scryptsalsa208sha256_STRBYTES - 1, 0);
diff --git a/ext/sodium/tests/pwhash_memlimit_below_min.phpt b/ext/sodium/tests/pwhash_memlimit_below_min.phpt
new file mode 100644
index 00000000000..63bf4443939
--- /dev/null
+++ b/ext/sodium/tests/pwhash_memlimit_below_min.phpt
@@ -0,0 +1,27 @@
+--TEST--
+sodium_crypto_pwhash(): a below-minimum memlimit reports a precise argument error
+--EXTENSIONS--
+sodium
+--SKIPIF--
+<?php
+if (!defined('SODIUM_CRYPTO_PWHASH_SALTBYTES')) print "skip libsodium without argon2";
+?>
+--FILE--
+<?php
+$salt = str_repeat("a", SODIUM_CRYPTO_PWHASH_SALTBYTES);
+
+try {
+ sodium_crypto_pwhash(32, "password", $salt, SODIUM_CRYPTO_PWHASH_OPSLIMIT_INTERACTIVE, 1);
+} catch (SodiumException $e) {
+ echo $e->getMessage(), "\n";
+}
+
+try {
+ sodium_crypto_pwhash_str("password", SODIUM_CRYPTO_PWHASH_OPSLIMIT_INTERACTIVE, 1);
+} catch (SodiumException $e) {
+ echo $e->getMessage(), "\n";
+}
+?>
+--EXPECTF--
+sodium_crypto_pwhash(): Argument #5 ($memlimit) must be greater than or equal to %d
+sodium_crypto_pwhash_str(): Argument #3 ($memlimit) must be greater than or equal to %d