Commit 8da53b5d31c for php.net

commit 8da53b5d31cbf4b50c779440afc34f75e79a0afb
Author: Ilia Alshanetsky <ilia@ilia.ws>
Date:   Tue Apr 7 17:48:43 2026 -0400

    Fix GH-18173: ext/hash relies on implementation-defined malloc alignment

    XXH3_state_t requires 64-byte alignment for its acc, customSecret, and
    buffer members, but php_hash_alloc_context() used ecalloc(), which only
    guarantees alignof(max_align_t) (typically 16 bytes on x86_64). When heap
    layout broke that assumption, xxhash's aligned loads segfaulted. Add a
    context_align field to php_hash_ops; when set, php_hash_alloc_context()
    over-allocates and manually aligns the returned pointer, storing the offset
    for php_hash_free_context() to recover the original allocation.

    Fixes GH-18173
    Closes GH-21668

diff --git a/NEWS b/NEWS
index 531d6a334c0..86ef47098f7 100644
--- a/NEWS
+++ b/NEWS
@@ -2,6 +2,10 @@ PHP                                                                        NEWS
 |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
 ?? ??? ????, PHP 8.4.24

+- Hash:
+  . Fixed bug GH-18173 (ext/hash relies on implementation-defined malloc
+    alignment). (iliaal)
+
 - Intl:
   . Fixed Locale::lookup() and locale_lookup() to return NULL instead of the
     fallback locale when a language tag cannot be canonicalized. (Weilin Du)
diff --git a/ext/hash/hash.c b/ext/hash/hash.c
index 6d03bbcca7c..1c90f4821f1 100644
--- a/ext/hash/hash.c
+++ b/ext/hash/hash.c
@@ -392,7 +392,7 @@ static void php_hash_do_hash(
 		}
 		php_stream_close(stream);
 		if (n < 0) {
-			efree(context);
+			php_hash_free_context(ops, context);
 			RETURN_FALSE;
 		}
 	} else {
@@ -401,7 +401,7 @@ static void php_hash_do_hash(

 	digest = zend_string_alloc(ops->digest_size, 0);
 	ops->hash_final((unsigned char *) ZSTR_VAL(digest), context);
-	efree(context);
+	php_hash_free_context(ops, context);

 	if (raw_output) {
 		ZSTR_VAL(digest)[ops->digest_size] = 0;
@@ -540,7 +540,7 @@ static void php_hash_do_hash_hmac(
 		}
 		php_stream_close(stream);
 		if (n < 0) {
-			efree(context);
+			php_hash_free_context(ops, context);
 			efree(K);
 			zend_string_release(digest);
 			RETURN_FALSE;
@@ -558,7 +558,7 @@ static void php_hash_do_hash_hmac(
 	/* Zero the key */
 	ZEND_SECURE_ZERO(K, ops->block_size);
 	efree(K);
-	efree(context);
+	php_hash_free_context(ops, context);

 	if (raw_output) {
 		ZSTR_VAL(digest)[ops->digest_size] = 0;
@@ -817,7 +817,7 @@ PHP_FUNCTION(hash_final)
 	ZSTR_VAL(digest)[digest_len] = 0;

 	/* Invalidate the object from further use */
-	efree(hash->context);
+	php_hash_free_context(hash->ops, hash->context);
 	hash->context = NULL;

 	if (raw_output) {
@@ -975,7 +975,7 @@ PHP_FUNCTION(hash_hkdf)
 	ZEND_SECURE_ZERO(digest, ops->digest_size);
 	ZEND_SECURE_ZERO(prk, ops->digest_size);
 	efree(K);
-	efree(context);
+	php_hash_free_context(ops, context);
 	efree(prk);
 	efree(digest);
 	ZSTR_VAL(returnval)[length] = 0;
@@ -1091,7 +1091,7 @@ PHP_FUNCTION(hash_pbkdf2)
 	efree(K1);
 	efree(K2);
 	efree(computed_salt);
-	efree(context);
+	php_hash_free_context(ops, context);
 	efree(digest);
 	efree(temp);

@@ -1347,7 +1347,7 @@ PHP_FUNCTION(mhash_keygen_s2k)
 				RETVAL_STRINGL(key, bytes);
 				ZEND_SECURE_ZERO(key, bytes);
 				efree(digest);
-				efree(context);
+				php_hash_free_context(ops, context);
 				efree(key);
 			}
 		}
@@ -1377,7 +1377,7 @@ static void php_hashcontext_dtor(zend_object *obj) {
 	php_hashcontext_object *hash = php_hashcontext_from_object(obj);

 	if (hash->context) {
-		efree(hash->context);
+		php_hash_free_context(hash->ops, hash->context);
 		hash->context = NULL;
 	}

@@ -1413,7 +1413,7 @@ static zend_object *php_hashcontext_clone(zend_object *zobj) {
 	newobj->ops->hash_init(newobj->context, NULL);

 	if (SUCCESS != newobj->ops->hash_copy(newobj->ops, oldobj->context, newobj->context)) {
-		efree(newobj->context);
+		php_hash_free_context(newobj->ops, newobj->context);
 		newobj->context = NULL;
 		return znew;
 	}
diff --git a/ext/hash/hash_adler32.c b/ext/hash/hash_adler32.c
index 3898ea60e87..e1fdd765b37 100644
--- a/ext/hash/hash_adler32.c
+++ b/ext/hash/hash_adler32.c
@@ -70,5 +70,6 @@ const php_hash_ops php_hash_adler32_ops = {
 	4, /* what to say here? */
 	4,
 	sizeof(PHP_ADLER32_CTX),
+	0,
 	0
 };
diff --git a/ext/hash/hash_crc32.c b/ext/hash/hash_crc32.c
index a770d0b5541..2e8de00b518 100644
--- a/ext/hash/hash_crc32.c
+++ b/ext/hash/hash_crc32.c
@@ -102,6 +102,7 @@ const php_hash_ops php_hash_crc32_ops = {
 	4, /* what to say here? */
 	4,
 	sizeof(PHP_CRC32_CTX),
+	0,
 	0
 };

@@ -117,6 +118,7 @@ const php_hash_ops php_hash_crc32b_ops = {
 	4, /* what to say here? */
 	4,
 	sizeof(PHP_CRC32_CTX),
+	0,
 	0
 };

@@ -132,5 +134,6 @@ const php_hash_ops php_hash_crc32c_ops = {
 	4, /* what to say here? */
 	4,
 	sizeof(PHP_CRC32_CTX),
+	0,
 	0
 };
diff --git a/ext/hash/hash_fnv.c b/ext/hash/hash_fnv.c
index 92d4922bd81..c126de61cfd 100644
--- a/ext/hash/hash_fnv.c
+++ b/ext/hash/hash_fnv.c
@@ -32,6 +32,7 @@ const php_hash_ops php_hash_fnv132_ops = {
 	4,
 	4,
 	sizeof(PHP_FNV132_CTX),
+	0,
 	0
 };

@@ -47,6 +48,7 @@ const php_hash_ops php_hash_fnv1a32_ops = {
 	4,
 	4,
 	sizeof(PHP_FNV132_CTX),
+	0,
 	0
 };

@@ -62,6 +64,7 @@ const php_hash_ops php_hash_fnv164_ops = {
 	8,
 	4,
 	sizeof(PHP_FNV164_CTX),
+	0,
 	0
 };

@@ -77,6 +80,7 @@ const php_hash_ops php_hash_fnv1a64_ops = {
 	8,
 	4,
 	sizeof(PHP_FNV164_CTX),
+	0,
 	0
 };

diff --git a/ext/hash/hash_gost.c b/ext/hash/hash_gost.c
index 2ad6948a9a6..ee2f3c89381 100644
--- a/ext/hash/hash_gost.c
+++ b/ext/hash/hash_gost.c
@@ -329,7 +329,8 @@ const php_hash_ops php_hash_gost_ops = {
 	32,
 	32,
 	sizeof(PHP_GOST_CTX),
-	1
+	1,
+	0
 };

 const php_hash_ops php_hash_gost_crypto_ops = {
@@ -344,5 +345,6 @@ const php_hash_ops php_hash_gost_crypto_ops = {
 	32,
 	32,
 	sizeof(PHP_GOST_CTX),
-	1
+	1,
+	0
 };
diff --git a/ext/hash/hash_haval.c b/ext/hash/hash_haval.c
index 67bc2b2e478..484b9a58291 100644
--- a/ext/hash/hash_haval.c
+++ b/ext/hash/hash_haval.c
@@ -252,7 +252,7 @@ const php_hash_ops php_hash_##p##haval##b##_ops = { \
 	php_hash_serialize, \
 	php_hash_unserialize, \
 	PHP_HAVAL_SPEC, \
-	((b) / 8), 128, sizeof(PHP_HAVAL_CTX), 1 }; \
+	((b) / 8), 128, sizeof(PHP_HAVAL_CTX), 1, 0 }; \
 PHP_HASH_API void PHP_##p##HAVAL##b##Init(PHP_HAVAL_CTX *context, ZEND_ATTRIBUTE_UNUSED HashTable *args) \
 {	int i; context->count[0] = 	context->count[1] = 	0; \
 	for(i = 0; i < 8; i++) context->state[i] = D0[i]; \
diff --git a/ext/hash/hash_joaat.c b/ext/hash/hash_joaat.c
index 328f9292c4c..6a16ceeeda1 100644
--- a/ext/hash/hash_joaat.c
+++ b/ext/hash/hash_joaat.c
@@ -33,6 +33,7 @@ const php_hash_ops php_hash_joaat_ops = {
 	4,
 	4,
 	sizeof(PHP_JOAAT_CTX),
+	0,
 	0
 };

diff --git a/ext/hash/hash_md.c b/ext/hash/hash_md.c
index 96da7fce82a..996e71ec9ff 100644
--- a/ext/hash/hash_md.c
+++ b/ext/hash/hash_md.c
@@ -29,7 +29,8 @@ const php_hash_ops php_hash_md5_ops = {
 	16,
 	64,
 	sizeof(PHP_MD5_CTX),
-	1
+	1,
+	0
 };

 const php_hash_ops php_hash_md4_ops = {
@@ -44,7 +45,8 @@ const php_hash_ops php_hash_md4_ops = {
 	16,
 	64,
 	sizeof(PHP_MD4_CTX),
-	1
+	1,
+	0
 };

 static int php_md2_unserialize(php_hashcontext_object *hash, zend_long magic, const zval *zv);
@@ -61,7 +63,8 @@ const php_hash_ops php_hash_md2_ops = {
 	16,
 	16,
 	sizeof(PHP_MD2_CTX),
-	1
+	1,
+	0
 };

 /* MD common stuff */
diff --git a/ext/hash/hash_murmur.c b/ext/hash/hash_murmur.c
index 0117b2e57d3..d69c5c3bb68 100644
--- a/ext/hash/hash_murmur.c
+++ b/ext/hash/hash_murmur.c
@@ -33,6 +33,7 @@ const php_hash_ops php_hash_murmur3a_ops = {
 	4,
 	4,
 	sizeof(PHP_MURMUR3A_CTX),
+	0,
 	0
 };

@@ -95,6 +96,7 @@ const php_hash_ops php_hash_murmur3c_ops = {
 	16,
 	4,
 	sizeof(PHP_MURMUR3C_CTX),
+	0,
 	0
 };

@@ -174,6 +176,7 @@ const php_hash_ops php_hash_murmur3f_ops = {
 	16,
 	8,
 	sizeof(PHP_MURMUR3F_CTX),
+	0,
 	0
 };

diff --git a/ext/hash/hash_ripemd.c b/ext/hash/hash_ripemd.c
index 4802fdf9a1f..188d0095cbc 100644
--- a/ext/hash/hash_ripemd.c
+++ b/ext/hash/hash_ripemd.c
@@ -33,7 +33,8 @@ const php_hash_ops php_hash_ripemd128_ops = {
 	16,
 	64,
 	sizeof(PHP_RIPEMD128_CTX),
-	1
+	1,
+	0
 };

 const php_hash_ops php_hash_ripemd160_ops = {
@@ -48,7 +49,8 @@ const php_hash_ops php_hash_ripemd160_ops = {
 	20,
 	64,
 	sizeof(PHP_RIPEMD160_CTX),
-	1
+	1,
+	0
 };

 const php_hash_ops php_hash_ripemd256_ops = {
@@ -63,7 +65,8 @@ const php_hash_ops php_hash_ripemd256_ops = {
 	32,
 	64,
 	sizeof(PHP_RIPEMD256_CTX),
-	1
+	1,
+	0
 };

 const php_hash_ops php_hash_ripemd320_ops = {
@@ -78,7 +81,8 @@ const php_hash_ops php_hash_ripemd320_ops = {
 	40,
 	64,
 	sizeof(PHP_RIPEMD320_CTX),
-	1
+	1,
+	0
 };

 /* {{{ PHP_RIPEMD128Init
diff --git a/ext/hash/hash_sha.c b/ext/hash/hash_sha.c
index 3129446fcde..c45947b8584 100644
--- a/ext/hash/hash_sha.c
+++ b/ext/hash/hash_sha.c
@@ -75,7 +75,8 @@ const php_hash_ops php_hash_sha1_ops = {
 	20,
 	64,
 	sizeof(PHP_SHA1_CTX),
-	1
+	1,
+	0
 };

 /* sha224/sha256 */
@@ -92,7 +93,8 @@ const php_hash_ops php_hash_sha256_ops = {
 	32,
 	64,
 	sizeof(PHP_SHA256_CTX),
-	1
+	1,
+	0
 };

 const php_hash_ops php_hash_sha224_ops = {
@@ -107,7 +109,8 @@ const php_hash_ops php_hash_sha224_ops = {
 	28,
 	64,
 	sizeof(PHP_SHA224_CTX),
-	1
+	1,
+	0
 };

 #define ROTR32(b,x)		((x >> b) | (x << (32 - b)))
@@ -624,7 +627,8 @@ const php_hash_ops php_hash_sha384_ops = {
 	48,
 	128,
 	sizeof(PHP_SHA384_CTX),
-	1
+	1,
+	0
 };

 /* {{{ PHP_SHA512InitArgs
@@ -803,7 +807,8 @@ const php_hash_ops php_hash_sha512_ops = {
 	64,
 	128,
 	sizeof(PHP_SHA512_CTX),
-	1
+	1,
+	0
 };

 const php_hash_ops php_hash_sha512_256_ops = {
@@ -818,7 +823,8 @@ const php_hash_ops php_hash_sha512_256_ops = {
 	32,
 	128,
 	sizeof(PHP_SHA512_CTX),
-	1
+	1,
+	0
 };

 const php_hash_ops php_hash_sha512_224_ops = {
@@ -833,5 +839,6 @@ const php_hash_ops php_hash_sha512_224_ops = {
 	28,
 	128,
 	sizeof(PHP_SHA512_CTX),
-	1
+	1,
+	0
 };
diff --git a/ext/hash/hash_sha3.c b/ext/hash/hash_sha3.c
index 07da2cfd2d0..65221593511 100644
--- a/ext/hash/hash_sha3.c
+++ b/ext/hash/hash_sha3.c
@@ -251,7 +251,8 @@ const php_hash_ops php_hash_sha3_##bits##_ops = { \
 	bits >> 3, \
 	(1600 - (2 * bits)) >> 3, \
 	sizeof(PHP_SHA3_##bits##_CTX), \
-	1 \
+	1, \
+	0 \
 }

 #else
@@ -339,7 +340,8 @@ const php_hash_ops php_hash_sha3_##bits##_ops = { \
 	bits >> 3, \
 	(1600 - (2 * bits)) >> 3, \
 	sizeof(PHP_SHA3_CTX), \
-	1 \
+	1, \
+	0 \
 }

 #endif
diff --git a/ext/hash/hash_snefru.c b/ext/hash/hash_snefru.c
index c1dbc3ae57a..fd2ee28c80e 100644
--- a/ext/hash/hash_snefru.c
+++ b/ext/hash/hash_snefru.c
@@ -214,5 +214,6 @@ const php_hash_ops php_hash_snefru_ops = {
 	32,
 	32,
 	sizeof(PHP_SNEFRU_CTX),
-	1
+	1,
+	0
 };
diff --git a/ext/hash/hash_tiger.c b/ext/hash/hash_tiger.c
index 841693a67dd..745f6958237 100644
--- a/ext/hash/hash_tiger.c
+++ b/ext/hash/hash_tiger.c
@@ -265,7 +265,8 @@ static int php_tiger_unserialize(php_hashcontext_object *hash, zend_long magic,
 		b/8, \
 		64, \
 		sizeof(PHP_TIGER_CTX), \
-		1 \
+		1, \
+		0 \
 	}

 PHP_HASH_TIGER_OPS(3, 128);
diff --git a/ext/hash/hash_whirlpool.c b/ext/hash/hash_whirlpool.c
index db5a0da1236..894e2d9f0af 100644
--- a/ext/hash/hash_whirlpool.c
+++ b/ext/hash/hash_whirlpool.c
@@ -457,5 +457,6 @@ const php_hash_ops php_hash_whirlpool_ops = {
 	64,
 	64,
 	sizeof(PHP_WHIRLPOOL_CTX),
-	1
+	1,
+	0
 };
diff --git a/ext/hash/hash_xxhash.c b/ext/hash/hash_xxhash.c
index 1c1315afd4b..b7253b0c99c 100644
--- a/ext/hash/hash_xxhash.c
+++ b/ext/hash/hash_xxhash.c
@@ -34,6 +34,7 @@ const php_hash_ops php_hash_xxh32_ops = {
 	4,
 	4,
 	sizeof(PHP_XXH32_CTX),
+	0,
 	0
 };

@@ -101,6 +102,7 @@ const php_hash_ops php_hash_xxh64_ops = {
 	8,
 	8,
 	sizeof(PHP_XXH64_CTX),
+	0,
 	0
 };

@@ -152,7 +154,8 @@ const php_hash_ops php_hash_xxh3_64_ops = {
 	8,
 	8,
 	sizeof(PHP_XXH3_64_CTX),
-	0
+	0,
+	64
 };

 typedef XXH_errorcode (*xxh3_reset_with_secret_func_t)(XXH3_state_t*, const void*, size_t);
@@ -257,7 +260,8 @@ const php_hash_ops php_hash_xxh3_128_ops = {
 	16,
 	8,
 	sizeof(PHP_XXH3_128_CTX),
-	0
+	0,
+	64
 };

 PHP_HASH_API void PHP_XXH3_128_Init(PHP_XXH3_128_CTX *ctx, HashTable *args)
diff --git a/ext/hash/php_hash.h b/ext/hash/php_hash.h
index 3b058ef48bd..b77557c5bf7 100644
--- a/ext/hash/php_hash.h
+++ b/ext/hash/php_hash.h
@@ -52,6 +52,7 @@ typedef struct _php_hash_ops {
 	size_t block_size;
 	size_t context_size;
 	unsigned is_crypto: 1;
+	size_t context_align;
 } php_hash_ops;

 struct _php_hashcontext_object {
@@ -155,9 +156,26 @@ PHP_HASH_API int php_hash_unserialize_spec(php_hashcontext_object *hash, const z

 static inline void *php_hash_alloc_context(const php_hash_ops *ops) {
 	/* Zero out context memory so serialization doesn't expose internals */
+	if (ops->context_align > 0) {
+		size_t align = ops->context_align;
+		char *base = ecalloc(1, ops->context_size + align);
+		size_t offset = align - ((uintptr_t)base & (align - 1));
+		char *ptr = base + offset;
+		ptr[-1] = (char)offset;
+		return ptr;
+	}
 	return ecalloc(1, ops->context_size);
 }

+static inline void php_hash_free_context(const php_hash_ops *ops, void *ctx) {
+	if (ops->context_align > 0) {
+		unsigned char offset = ((unsigned char *)ctx)[-1];
+		efree((char *)ctx - offset);
+		return;
+	}
+	efree(ctx);
+}
+
 static inline void php_hash_bin2hex(char *out, const unsigned char *in, size_t in_len)
 {
 	static const char hexits[17] = "0123456789abcdef";