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