From 1db4b19aeaf521bf734bd201f4d74bd23860f7ee Mon Sep 17 00:00:00 2001 From: Ilia Alshanetsky Date: Tue, 7 Apr 2026 17:48:43 -0400 Subject: [PATCH] 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. php_hash_alloc_context() used ecalloc() which only guarantees alignof(max_align_t) alignment -- typically 16 bytes on x86_64. When heap layout broke that assumption, xxhash's aligned loads would segfault. 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. --- ext/hash/hash.c | 20 ++++++++++---------- ext/hash/hash_adler32.c | 1 + ext/hash/hash_crc32.c | 3 +++ ext/hash/hash_fnv.c | 4 ++++ ext/hash/hash_gost.c | 6 ++++-- ext/hash/hash_haval.c | 2 +- ext/hash/hash_joaat.c | 1 + ext/hash/hash_md.c | 9 ++++++--- ext/hash/hash_murmur.c | 3 +++ ext/hash/hash_ripemd.c | 12 ++++++++---- ext/hash/hash_sha.c | 21 ++++++++++++++------- ext/hash/hash_sha3.c | 6 ++++-- ext/hash/hash_snefru.c | 3 ++- ext/hash/hash_tiger.c | 3 ++- ext/hash/hash_whirlpool.c | 3 ++- ext/hash/hash_xxhash.c | 8 ++++++-- ext/hash/php_hash.h | 18 ++++++++++++++++++ 17 files changed, 89 insertions(+), 34 deletions(-) diff --git a/ext/hash/hash.c b/ext/hash/hash.c index 6d03bbcca7cea..1c90f4821f1a9 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 3898ea60e8775..e1fdd765b372a 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 a770d0b554167..2e8de00b518db 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 92d4922bd810e..c126de61cfd18 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 2ad6948a9a676..ee2f3c89381ce 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 67bc2b2e4780e..484b9a5829156 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 328f9292c4ca8..6a16ceeeda14c 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 96da7fce82a27..996e71ec9ffb2 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 0117b2e57d361..d69c5c3bb6880 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 4802fdf9a1fcc..188d0095cbc33 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 3129446fcde40..c45947b8584f8 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 07da2cfd2d016..65221593511f6 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 c1dbc3ae57a6c..fd2ee28c80e7c 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 841693a67dd14..745f69582373b 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 db5a0da1236ad..894e2d9f0afe2 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 1c1315afd4b28..b7253b0c99c01 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 3b058ef48bdb2..b77557c5bf78f 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";