[php-src] master: Fix GH-18173: ext/hash relies on implementation-defined malloc alignment

Ilia Alshanetsky <[email protected]>
Newsgroups gmane.comp.php.cvs.general
Message-ID <[email protected]>
Author: Ilia Alshanetsky (iliaal)
Date: 2026-06-29T10:37:14-04:00

Commit: https://github.com/php/php-src/commit/8da53b5d31cbf4b50c779440afc34f75e79a0afb
Raw diff: https://github.com/php/php-src/commit/8da53b5d31cbf4b50c779440afc34f75e79a0afb.diff

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

Changed paths:
  M  NEWS
  M  ext/hash/hash.c
  M  ext/hash/hash_adler32.c
  M  ext/hash/hash_crc32.c
  M  ext/hash/hash_fnv.c
  M  ext/hash/hash_gost.c
  M  ext/hash/hash_haval.c
  M  ext/hash/hash_joaat.c
  M  ext/hash/hash_md.c
  M  ext/hash/hash_murmur.c
  M  ext/hash/hash_ripemd.c
  M  ext/hash/hash_sha.c
  M  ext/hash/hash_sha3.c
  M  ext/hash/hash_snefru.c
  M  ext/hash/hash_tiger.c
  M  ext/hash/hash_whirlpool.c
  M  ext/hash/hash_xxhash.c
  M  ext/hash/php_hash.h


Diff:

diff --git a/NEWS b/NEWS
index 531d6a334c05..86ef47098f77 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 6d03bbcca7ce..1c90f4821f1a 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 3898ea60e877..e1fdd765b372 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 a770d0b55416..2e8de00b518d 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 92d4922bd810..c126de61cfd1 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 2ad6948a9a67..ee2f3c89381c 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 67bc2b2e4780..484b9a582915 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 328f9292c4ca..6a16ceeeda14 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 96da7fce82a2..996e71ec9ffb 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 0117b2e57d36..d69c5c3bb688 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 4802fdf9a1fc..188d0095cbc3 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 3129446fcde4..c45947b8584f 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 07da2cfd2d01..65221593511f 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 c1dbc3ae57a6..fd2ee28c80e7 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 841693a67dd1..745f69582373 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 db5a0da1236a..894e2d9f0afe 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 1c1315afd4b2..b7253b0c99c0 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 3b058ef48bdb..b77557c5bf78 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";
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.