Commit 16922684742 for php.net

commit 169226847426368c62992af43c2c39b0fdbff4d5
Author: David Carlier <devnexen@gmail.com>
Date:   Fri May 29 21:44:14 2026 +0100

    ext/openssl: openssl_encrypt() zend mm heap overflow on AES-WRAP-PAD mode.

    Fix #22186

    close GH-22187

diff --git a/NEWS b/NEWS
index e0375b21d4f..3c7f104a24d 100644
--- a/NEWS
+++ b/NEWS
@@ -48,6 +48,10 @@ PHP                                                                        NEWS
   . Fixed bug GH-20469 (Unsafe inheritance cache replay with reentrant
     autoloading). (Levi Morrison)

+- OpenSSL:
+  . Fixed bug GH-22187 (Memory corruption (zend_mm_heap corrupted) in
+    openssl_encrypt with AES-WRAP-PAD). (David Carlier)
+
 - Phar:
   . Fixed a bypass of the magic ".phar" directory protection in
     Phar::addEmptyDir() for paths starting with "/.phar", while allowing
diff --git a/ext/openssl/openssl.c b/ext/openssl/openssl.c
index 6d179cebabd..1689662f07a 100644
--- a/ext/openssl/openssl.c
+++ b/ext/openssl/openssl.c
@@ -7932,6 +7932,7 @@ static int php_openssl_cipher_update(const EVP_CIPHER *cipher_type,
 		const char *aad, size_t aad_len, int enc)  /* {{{ */
 {
 	int i = 0;
+	size_t outlen = data_len + EVP_CIPHER_block_size(cipher_type);

 	if (mode->is_single_run_aead && !EVP_CipherUpdate(cipher_ctx, NULL, &i, NULL, (int)data_len)) {
 		php_openssl_store_errors();
@@ -7945,7 +7946,19 @@ static int php_openssl_cipher_update(const EVP_CIPHER *cipher_type,
 		return FAILURE;
 	}

-	*poutbuf = zend_string_alloc((int)data_len + EVP_CIPHER_block_size(cipher_type), 0);
+#ifdef EVP_CIPH_WRAP_MODE
+	if ((EVP_CIPHER_mode(cipher_type)) == EVP_CIPH_WRAP_MODE) {
+		/*
+		 * RFC 5649 wrap-with-padding rounds the input up to the block size
+		 * and prepends an integrity block, we reserve one extra block.
+		 * See EVP_EncryptUpdate(3): wrap mode may write up to
+		 * inl + cipher_block_size bytes.
+		 */
+		outlen += EVP_CIPHER_block_size(cipher_type);
+	}
+#endif
+
+	*poutbuf = zend_string_alloc(outlen, false);

 	if (!EVP_CipherUpdate(cipher_ctx, (unsigned char*)ZSTR_VAL(*poutbuf),
 					&i, (const unsigned char *)data, (int)data_len)) {
@@ -7957,7 +7970,7 @@ static int php_openssl_cipher_update(const EVP_CIPHER *cipher_type,
 		}
 		*/
 		php_openssl_store_errors();
-		zend_string_release_ex(*poutbuf, 0);
+		zend_string_release_ex(*poutbuf, false);
 		return FAILURE;
 	}

diff --git a/ext/openssl/tests/gh22186.phpt b/ext/openssl/tests/gh22186.phpt
new file mode 100644
index 00000000000..8f28e6c45b5
--- /dev/null
+++ b/ext/openssl/tests/gh22186.phpt
@@ -0,0 +1,32 @@
+--TEST--
+GH-22186 (Heap buffer overflow in openssl_encrypt with AES-WRAP-PAD)
+--EXTENSIONS--
+openssl
+--SKIPIF--
+<?php
+/* openssl_get_cipher_methods() enumerates provider ciphers, but openssl_encrypt()
+ * resolves names via the legacy EVP_get_cipherbyname(), so on some builds the
+ * cipher is listed yet not usable. Probe the actual call path instead. */
+if (!@openssl_encrypt("test", "aes-128-wrap-pad", str_repeat("k", 16),
+        OPENSSL_RAW_DATA | OPENSSL_DONT_ZERO_PAD_KEY, str_repeat("\0", 4))) {
+    die('skip aes-128-wrap-pad not usable on this OpenSSL build');
+}
+?>
+--FILE--
+<?php
+$pass = str_repeat("k", 16);
+$iv = str_repeat("\0", 4);
+
+for ($i = 1; $i < 258; $i++) {
+    $data = str_repeat("a", $i);
+    $enc = openssl_encrypt($data, 'aes-128-wrap-pad', $pass, OPENSSL_RAW_DATA | OPENSSL_DONT_ZERO_PAD_KEY, $iv);
+    $dec = openssl_decrypt($enc, 'aes-128-wrap-pad', $pass, OPENSSL_RAW_DATA | OPENSSL_DONT_ZERO_PAD_KEY, $iv);
+    if ($dec !== $data) {
+        die("mismatch at $i\n");
+    }
+}
+
+echo "done\n";
+?>
+--EXPECT--
+done