From a8ccf4f036a43b7c4679b8a7bc0462b910b44c75 Mon Sep 17 00:00:00 2001 From: Florian Engelhardt Date: Wed, 23 Sep 2026 14:25:42 +0200 Subject: [PATCH 1/4] Fix alignment in `zend_string_safe_alloc()`/`zend_string_safe_realloc()` --- Zend/zend_string.h | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/Zend/zend_string.h b/Zend/zend_string.h index d5674f3ff1c4..d4cdeb5d1fa7 100644 --- a/Zend/zend_string.h +++ b/Zend/zend_string.h @@ -128,6 +128,12 @@ static zend_always_inline zend_string *ZSTR_KNOWN(size_t idx) { #define _ZSTR_STRUCT_SIZE(len) (_ZSTR_HEADER_SIZE + len + 1) +/* ALIGN(n * m + header + l + 1). The "+ ALIGNMENT - 1" of the rounding is + * done inside the overflow check, so the final "& MASK" can only shrink + * the value and can not wrap. */ +#define _ZSTR_SAFE_STRUCT_SIZE(n, m, l) \ + (zend_safe_address_guarded(n, m, _ZSTR_STRUCT_SIZE(l) + ZEND_MM_ALIGNMENT - 1) & ZEND_MM_ALIGNMENT_MASK) + #define ZSTR_MAX_OVERHEAD (ZEND_MM_ALIGNED_SIZE(_ZSTR_HEADER_SIZE + 1)) #define ZSTR_MAX_LEN (SIZE_MAX - ZSTR_MAX_OVERHEAD) @@ -209,7 +215,7 @@ static zend_always_inline zend_string *zend_string_alloc(size_t len, bool persis /* Fallible variant of zend_string_safe_alloc */ static zend_always_inline zend_string *zend_string_safe_falloc(size_t n, size_t m, size_t l, bool persistent, bool fallible) { - zend_string *ret = (zend_string *)safe_fpemalloc(n, m, ZEND_MM_ALIGNED_SIZE(_ZSTR_STRUCT_SIZE(l)), persistent, fallible); + zend_string *ret = (zend_string *)fpemalloc(_ZSTR_SAFE_STRUCT_SIZE(n, m, l), persistent, fallible); if (ZEND_CONST_COND(fallible, true) && UNEXPECTED(ret == NULL)) { return ret; } @@ -394,7 +400,7 @@ static zend_always_inline zend_string *zend_string_safe_frealloc(zend_string *s, if (!ZSTR_IS_INTERNED(s)) { if (GC_REFCOUNT(s) == 1) { - ret = (zend_string *)safe_fperealloc(s, n, m, ZEND_MM_ALIGNED_SIZE(_ZSTR_STRUCT_SIZE(l)), persistent, fallible); + ret = (zend_string *)fperealloc(s, _ZSTR_SAFE_STRUCT_SIZE(n, m, l), persistent, fallible); if (ZEND_CONST_COND(fallible, true) && !ret) { return NULL; } From e59cc531496ad0a52365b9f441a7f0a73c4498ea Mon Sep 17 00:00:00 2001 From: Florian Engelhardt Date: Thu, 24 Sep 2026 08:26:59 +0200 Subject: [PATCH 2/4] Update chunk_split 32bit test for new overflow offset --- ext/standard/tests/strings/chunk_split_variation2_32bit.phpt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ext/standard/tests/strings/chunk_split_variation2_32bit.phpt b/ext/standard/tests/strings/chunk_split_variation2_32bit.phpt index c83a37f00edb..8f56cd96fbc3 100644 --- a/ext/standard/tests/strings/chunk_split_variation2_32bit.phpt +++ b/ext/standard/tests/strings/chunk_split_variation2_32bit.phpt @@ -16,4 +16,4 @@ var_dump(chunk_split($a,$b,$c)); --EXPECTF-- *** Testing chunk_split() : unexpected large 'end' string argument variation 2 *** -Fatal error: Possible integer overflow in memory allocation (65537 * 65537 + %r65556|65560%r) in %s on line %d +Fatal error: Possible integer overflow in memory allocation (65537 * 65537 + %r65557|65561%r) in %s on line %d From 083e6d6ed0812e078c8429e567ca85843756c27f Mon Sep 17 00:00:00 2001 From: Florian Engelhardt Date: Mon, 5 Oct 2026 12:10:53 +0200 Subject: [PATCH 3/4] Fix allignement in mbstring functions --- ext/mbstring/libmbfl/mbfl/mbfl_encoding.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ext/mbstring/libmbfl/mbfl/mbfl_encoding.h b/ext/mbstring/libmbfl/mbfl/mbfl_encoding.h index 6abc042ff09b..e8f24bfe0eb6 100644 --- a/ext/mbstring/libmbfl/mbfl/mbfl_encoding.h +++ b/ext/mbstring/libmbfl/mbfl/mbfl_encoding.h @@ -155,7 +155,7 @@ typedef zend_string* (*mb_cut_fn)(unsigned char *str, size_t from, size_t len, u static inline void mb_convert_buf_init(mb_convert_buf *buf, size_t initsize, uint32_t repl_char, unsigned int err_mode) { buf->state = buf->errors = 0; - buf->str = emalloc(_ZSTR_STRUCT_SIZE(initsize)); + buf->str = emalloc(ZEND_MM_ALIGNED_SIZE(_ZSTR_STRUCT_SIZE(initsize))); buf->out = (unsigned char*)ZSTR_VAL(buf->str); buf->limit = buf->out + initsize; buf->replacement_char = repl_char; @@ -168,7 +168,7 @@ static inline zend_result mb_convert_buf_ensure(mb_convert_buf *buf, unsigned ch if ((size_t)(*limit - *out) < needed) { size_t oldsize = *limit - (unsigned char*)ZSTR_VAL((buf)->str); size_t newsize = oldsize + MAX(oldsize >> 1, needed); - zend_string *newstr = safe_ferealloc((buf)->str, 1, newsize, _ZSTR_STRUCT_SIZE(0), /* fallible */ true); + zend_string *newstr = ferealloc((buf)->str, _ZSTR_SAFE_STRUCT_SIZE(1, newsize, 0), /* fallible */ true); if (!newstr) { return FAILURE; } From 639a53c3059436a7c9256f4d69919c117a926473 Mon Sep 17 00:00:00 2001 From: Florian Engelhardt Date: Tue, 6 Oct 2026 14:29:34 +0200 Subject: [PATCH 4/4] fix overflow --- Zend/zend_string.h | 45 +++++++++++-------- ext/mbstring/libmbfl/mbfl/mbfl_encoding.h | 7 ++- .../strings/chunk_split_variation2_32bit.phpt | 2 +- 3 files changed, 32 insertions(+), 22 deletions(-) diff --git a/Zend/zend_string.h b/Zend/zend_string.h index d4cdeb5d1fa7..573677e77fec 100644 --- a/Zend/zend_string.h +++ b/Zend/zend_string.h @@ -128,15 +128,23 @@ static zend_always_inline zend_string *ZSTR_KNOWN(size_t idx) { #define _ZSTR_STRUCT_SIZE(len) (_ZSTR_HEADER_SIZE + len + 1) -/* ALIGN(n * m + header + l + 1). The "+ ALIGNMENT - 1" of the rounding is - * done inside the overflow check, so the final "& MASK" can only shrink - * the value and can not wrap. */ -#define _ZSTR_SAFE_STRUCT_SIZE(n, m, l) \ - (zend_safe_address_guarded(n, m, _ZSTR_STRUCT_SIZE(l) + ZEND_MM_ALIGNMENT - 1) & ZEND_MM_ALIGNMENT_MASK) - #define ZSTR_MAX_OVERHEAD (ZEND_MM_ALIGNED_SIZE(_ZSTR_HEADER_SIZE + 1)) #define ZSTR_MAX_LEN (SIZE_MAX - ZSTR_MAX_OVERHEAD) +/* Returns n * m + l, or SIZE_MAX on fallible overflow. Rejects lengths above + * ZSTR_MAX_LEN so the aligned allocation size of the string can not wrap. */ +static zend_always_inline size_t zend_string_safe_len(size_t n, size_t m, size_t l, bool fallible) +{ + bool overflow; + size_t len = zend_safe_address(n, m, l, &overflow); + + if (UNEXPECTED(overflow || len > ZSTR_MAX_LEN)) { + zend_mm_delayed_error_or_panic(fallible, "Possible integer overflow in memory allocation (%zu * %zu + %zu)", n, m, l); + return SIZE_MAX; + } + return len; +} + #define ZSTR_ALLOCA_ALLOC(str, _len, use_heap) do { \ (str) = (zend_string *)do_alloca(ZEND_MM_ALIGNED_SIZE_EX(_ZSTR_STRUCT_SIZE(_len), 8), (use_heap)); \ GC_SET_REFCOUNT(str, 1); \ @@ -215,16 +223,11 @@ static zend_always_inline zend_string *zend_string_alloc(size_t len, bool persis /* Fallible variant of zend_string_safe_alloc */ static zend_always_inline zend_string *zend_string_safe_falloc(size_t n, size_t m, size_t l, bool persistent, bool fallible) { - zend_string *ret = (zend_string *)fpemalloc(_ZSTR_SAFE_STRUCT_SIZE(n, m, l), persistent, fallible); - if (ZEND_CONST_COND(fallible, true) && UNEXPECTED(ret == NULL)) { - return ret; + size_t len = zend_string_safe_len(n, m, l, fallible && !persistent); + if (UNEXPECTED(len == SIZE_MAX)) { + return NULL; } - - GC_SET_REFCOUNT(ret, 1); - GC_TYPE_INFO(ret) = GC_STRING | ((persistent ? IS_STR_PERSISTENT : 0) << GC_FLAGS_SHIFT); - ZSTR_H(ret) = 0; - ZSTR_LEN(ret) = (n * m) + l; - return ret; + return zend_string_falloc(len, persistent, fallible); } static zend_always_inline zend_string *zend_string_safe_alloc(size_t n, size_t m, size_t l, bool persistent) @@ -397,23 +400,27 @@ static zend_always_inline zend_string *zend_string_truncate(zend_string *s, size static zend_always_inline zend_string *zend_string_safe_frealloc(zend_string *s, size_t n, size_t m, size_t l, bool persistent, bool fallible) { zend_string *ret; + size_t len = zend_string_safe_len(n, m, l, fallible && !persistent); + if (UNEXPECTED(len == SIZE_MAX)) { + return NULL; + } if (!ZSTR_IS_INTERNED(s)) { if (GC_REFCOUNT(s) == 1) { - ret = (zend_string *)fperealloc(s, _ZSTR_SAFE_STRUCT_SIZE(n, m, l), persistent, fallible); + ret = (zend_string *)fperealloc(s, ZEND_MM_ALIGNED_SIZE(_ZSTR_STRUCT_SIZE(len)), persistent, fallible); if (ZEND_CONST_COND(fallible, true) && !ret) { return NULL; } - ZSTR_LEN(ret) = (n * m) + l; + ZSTR_LEN(ret) = len; zend_string_forget_hash_val(ret); return ret; } } - ret = zend_string_safe_falloc(n, m, l, persistent, fallible); + ret = zend_string_falloc(len, persistent, fallible); if (ZEND_CONST_COND(fallible, true) && !ret) { return NULL; } - memcpy(ZSTR_VAL(ret), ZSTR_VAL(s), MIN((n * m) + l, ZSTR_LEN(s)) + 1); + memcpy(ZSTR_VAL(ret), ZSTR_VAL(s), MIN(len, ZSTR_LEN(s)) + 1); if (!ZSTR_IS_INTERNED(s)) { GC_DELREF(s); } diff --git a/ext/mbstring/libmbfl/mbfl/mbfl_encoding.h b/ext/mbstring/libmbfl/mbfl/mbfl_encoding.h index e8f24bfe0eb6..ac21fbbe491a 100644 --- a/ext/mbstring/libmbfl/mbfl/mbfl_encoding.h +++ b/ext/mbstring/libmbfl/mbfl/mbfl_encoding.h @@ -167,8 +167,11 @@ static inline zend_result mb_convert_buf_ensure(mb_convert_buf *buf, unsigned ch ZEND_ASSERT(*out <= *limit); if ((size_t)(*limit - *out) < needed) { size_t oldsize = *limit - (unsigned char*)ZSTR_VAL((buf)->str); - size_t newsize = oldsize + MAX(oldsize >> 1, needed); - zend_string *newstr = ferealloc((buf)->str, _ZSTR_SAFE_STRUCT_SIZE(1, newsize, 0), /* fallible */ true); + size_t newsize = zend_string_safe_len(1, oldsize, MAX(oldsize >> 1, needed), /* fallible */ true); + if (UNEXPECTED(newsize == SIZE_MAX)) { + return FAILURE; + } + zend_string *newstr = ferealloc((buf)->str, ZEND_MM_ALIGNED_SIZE(_ZSTR_STRUCT_SIZE(newsize)), /* fallible */ true); if (!newstr) { return FAILURE; } diff --git a/ext/standard/tests/strings/chunk_split_variation2_32bit.phpt b/ext/standard/tests/strings/chunk_split_variation2_32bit.phpt index 8f56cd96fbc3..3c227c9273da 100644 --- a/ext/standard/tests/strings/chunk_split_variation2_32bit.phpt +++ b/ext/standard/tests/strings/chunk_split_variation2_32bit.phpt @@ -16,4 +16,4 @@ var_dump(chunk_split($a,$b,$c)); --EXPECTF-- *** Testing chunk_split() : unexpected large 'end' string argument variation 2 *** -Fatal error: Possible integer overflow in memory allocation (65537 * 65537 + %r65557|65561%r) in %s on line %d +Fatal error: Possible integer overflow in memory allocation (65537 * 65537 + 65537) in %s on line %d