From f5b448c968ab8e6b8efbed1a38b3d3006cdd9d9f Mon Sep 17 00:00:00 2001 From: Florian Engelhardt Date: Wed, 23 Sep 2026 14:25:42 +0200 Subject: [PATCH 1/7] 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 5bd805aa8ddb..d8eb82a610f2 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 8d1a1ad2bb2d6fc54735a1fce73c8a66cfd2e905 Mon Sep 17 00:00:00 2001 From: Florian Engelhardt Date: Thu, 24 Sep 2026 08:26:59 +0200 Subject: [PATCH 2/7] 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 b21f6253f33e56c24569e44579f5a03469c4d8d9 Mon Sep 17 00:00:00 2001 From: Florian Engelhardt Date: Mon, 5 Oct 2026 12:10:53 +0200 Subject: [PATCH 3/7] 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 e71737318bf7..a5cd4413859d 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) { /* An earlier growth may have moved the buffer. */ buf->out = *out; From fd78c1f629ed35b248dd93e48a39c35695a676a1 Mon Sep 17 00:00:00 2001 From: Florian Engelhardt Date: Tue, 6 Oct 2026 14:29:34 +0200 Subject: [PATCH 4/7] 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 d8eb82a610f2..1c6c208023b9 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 a5cd4413859d..8a277d4c324c 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) { /* An earlier growth may have moved the buffer. */ buf->out = *out; 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 From 00be4ebed3012bba1075f715469cb9975e0312cc Mon Sep 17 00:00:00 2001 From: Florian Engelhardt Date: Tue, 3 Nov 2026 08:06:12 +0100 Subject: [PATCH 5/7] Mark fatal string allocation overflow as unreachable Let GCC see that non-fallible overflow handling aborts, avoiding false array-bounds errors in callers that copy into the allocated string. --- Zend/zend_string.h | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Zend/zend_string.h b/Zend/zend_string.h index 1c6c208023b9..b1dd3f329082 100644 --- a/Zend/zend_string.h +++ b/Zend/zend_string.h @@ -140,6 +140,12 @@ static zend_always_inline size_t zend_string_safe_len(size_t n, size_t m, size_t 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); + if (!fallible) { + /* zend_mm_delayed_error_or_panic() aborts for !fallible, but its body + * is in zend_alloc.c and is not visible when compiling other source files. + * Its declaration cannot use ZEND_NORETURN because the fallible path returns. */ + ZEND_UNREACHABLE(); + } return SIZE_MAX; } return len; From f0fa718c830cc900cde16fe4097425af03f74e75 Mon Sep 17 00:00:00 2001 From: Florian Engelhardt Date: Wed, 11 Nov 2026 13:44:58 +0100 Subject: [PATCH 6/7] Reuse safe allocators for aligned strings Apply ndossche's suggestion and check realloc alignment overflow. https://github.com/php/php-src/pull/23869#issuecomment-6795117595 --- Zend/zend_alloc.c | 17 ++++++- Zend/zend_string.h | 49 ++++++++----------- ext/mbstring/libmbfl/mbfl/mbfl_encoding.h | 9 ++-- .../strings/chunk_split_variation2_32bit.phpt | 2 +- 4 files changed, 39 insertions(+), 38 deletions(-) diff --git a/Zend/zend_alloc.c b/Zend/zend_alloc.c index 2b52babc98ee..d0c6f751e518 100644 --- a/Zend/zend_alloc.c +++ b/Zend/zend_alloc.c @@ -1825,6 +1825,10 @@ static zend_never_inline void *zend_mm_realloc_huge(zend_mm_heap *heap, void *pt #if ZEND_DEBUG real_size = size; size = ZEND_MM_ALIGNED_SIZE(size) + ZEND_MM_ALIGNED_SIZE(sizeof(zend_mm_debug_info)); + if (UNEXPECTED(size < real_size)) { + zend_mm_delayed_error_or_panic(heap->fallible, "Possible integer overflow in memory allocation (%zu + %zu)", ZEND_MM_ALIGNED_SIZE(real_size), ZEND_MM_ALIGNED_SIZE(sizeof(zend_mm_debug_info))); + return NULL; + } #endif if (size > ZEND_MM_MAX_LARGE_SIZE) { #if ZEND_DEBUG @@ -1835,10 +1839,15 @@ static zend_never_inline void *zend_mm_realloc_huge(zend_mm_heap *heap, void *pt * We allocate them with 2MB size granularity, to avoid many * reallocations when they are extended by small pieces */ - new_size = ZEND_MM_ALIGNED_SIZE_EX(size, MAX(REAL_PAGE_SIZE, ZEND_MM_CHUNK_SIZE)); + size_t alignment = MAX(REAL_PAGE_SIZE, ZEND_MM_CHUNK_SIZE); #else - new_size = ZEND_MM_ALIGNED_SIZE_EX(size, REAL_PAGE_SIZE); + size_t alignment = REAL_PAGE_SIZE; #endif + new_size = ZEND_MM_ALIGNED_SIZE_EX(size, alignment); + if (UNEXPECTED(new_size < size)) { + zend_mm_delayed_error_or_panic(heap->fallible, "Possible integer overflow in memory allocation (%zu + %zu)", size, alignment); + return NULL; + } if (new_size == old_size) { #if ZEND_DEBUG zend_mm_change_huge_block_size(heap, ptr, new_size, real_size ZEND_FILE_LINE_RELAY_CC ZEND_FILE_LINE_ORIG_RELAY_CC); @@ -1920,6 +1929,10 @@ static zend_always_inline void *zend_mm_realloc_heap(zend_mm_heap *heap, void *p size_t real_size = size; size = ZEND_MM_ALIGNED_SIZE(size) + ZEND_MM_ALIGNED_SIZE(sizeof(zend_mm_debug_info)); + if (UNEXPECTED(size < real_size)) { + zend_mm_delayed_error_or_panic(heap->fallible, "Possible integer overflow in memory allocation (%zu + %zu)", ZEND_MM_ALIGNED_SIZE(real_size), ZEND_MM_ALIGNED_SIZE(sizeof(zend_mm_debug_info))); + return NULL; + } #endif ZEND_MM_CHECK(chunk->heap == heap, "zend_mm_heap corrupted"); diff --git a/Zend/zend_string.h b/Zend/zend_string.h index b1dd3f329082..bb7da1d5410d 100644 --- a/Zend/zend_string.h +++ b/Zend/zend_string.h @@ -131,24 +131,14 @@ static zend_always_inline zend_string *ZSTR_KNOWN(size_t idx) { #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); - if (!fallible) { - /* zend_mm_delayed_error_or_panic() aborts for !fallible, but its body - * is in zend_alloc.c and is not visible when compiling other source files. - * Its declaration cannot use ZEND_NORETURN because the fallible path returns. */ - ZEND_UNREACHABLE(); - } - return SIZE_MAX; +/* Offset for safe_*alloc(n, m, offset) to size the string like zend_string_alloc(n * m + l). */ +static zend_always_inline size_t zend_string_safe_offset(size_t n, size_t m, size_t l) +{ + if (UNEXPECTED(l > ZSTR_MAX_LEN)) { + return SIZE_MAX; /* Rejected by the allocator. */ } - return len; + /* Works even if n * m wraps, allocator detects that overflow. */ + return ZEND_MM_ALIGNED_SIZE(_ZSTR_STRUCT_SIZE(n * m + l)) - n * m; } #define ZSTR_ALLOCA_ALLOC(str, _len, use_heap) do { \ @@ -229,11 +219,16 @@ 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) { - size_t len = zend_string_safe_len(n, m, l, fallible && !persistent); - if (UNEXPECTED(len == SIZE_MAX)) { - return NULL; + zend_string *ret = (zend_string *)safe_fpemalloc(n, m, zend_string_safe_offset(n, m, l), persistent, fallible); + if (ZEND_CONST_COND(fallible, true) && UNEXPECTED(ret == NULL)) { + return ret; } - return zend_string_falloc(len, persistent, fallible); + + 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; } static zend_always_inline zend_string *zend_string_safe_alloc(size_t n, size_t m, size_t l, bool persistent) @@ -406,27 +401,23 @@ 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, ZEND_MM_ALIGNED_SIZE(_ZSTR_STRUCT_SIZE(len)), persistent, fallible); + ret = (zend_string *)safe_fperealloc(s, n, m, zend_string_safe_offset(n, m, l), persistent, fallible); if (ZEND_CONST_COND(fallible, true) && !ret) { return NULL; } - ZSTR_LEN(ret) = len; + ZSTR_LEN(ret) = (n * m) + l; zend_string_forget_hash_val(ret); return ret; } } - ret = zend_string_falloc(len, persistent, fallible); + ret = zend_string_safe_falloc(n, m, l, persistent, fallible); if (ZEND_CONST_COND(fallible, true) && !ret) { return NULL; } - memcpy(ZSTR_VAL(ret), ZSTR_VAL(s), MIN(len, ZSTR_LEN(s)) + 1); + memcpy(ZSTR_VAL(ret), ZSTR_VAL(s), MIN((n * m) + l, 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 8a277d4c324c..7d2aab7cf338 100644 --- a/ext/mbstring/libmbfl/mbfl/mbfl_encoding.h +++ b/ext/mbstring/libmbfl/mbfl/mbfl_encoding.h @@ -167,11 +167,8 @@ 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 = 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); + size_t growth = MAX(oldsize >> 1, needed); + zend_string *newstr = safe_ferealloc((buf)->str, 1, oldsize, zend_string_safe_offset(1, oldsize, growth), /* fallible */ true); if (!newstr) { /* An earlier growth may have moved the buffer. */ buf->out = *out; @@ -179,7 +176,7 @@ static inline zend_result mb_convert_buf_ensure(mb_convert_buf *buf, unsigned ch return FAILURE; } *out = (unsigned char*)ZSTR_VAL(newstr) + (*out - (unsigned char*)ZSTR_VAL((buf)->str)); - *limit = (unsigned char*)ZSTR_VAL(newstr) + newsize; + *limit = (unsigned char*)ZSTR_VAL(newstr) + oldsize + growth; (buf)->str = newstr; } return SUCCESS; diff --git a/ext/standard/tests/strings/chunk_split_variation2_32bit.phpt b/ext/standard/tests/strings/chunk_split_variation2_32bit.phpt index 3c227c9273da..1f378a739d53 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 + 65537) in %s on line %d +Fatal error: Possible integer overflow in memory allocation (65537 * 65537 + %d) in %s on line %d From 75e4f2b6a6f5833c16fd7ae77898f9c571340f5a Mon Sep 17 00:00:00 2001 From: Florian Engelhardt Date: Wed, 11 Nov 2026 15:57:00 +0100 Subject: [PATCH 7/7] Make string allocation bounds explicit to GCC Assert bounds after successful allocation to avoid stringop-overflow warnings. --- Zend/zend_string.h | 4 ++++ ext/mbstring/libmbfl/mbfl/mbfl_encoding.h | 2 ++ 2 files changed, 6 insertions(+) diff --git a/Zend/zend_string.h b/Zend/zend_string.h index bb7da1d5410d..91edd5b5ba15 100644 --- a/Zend/zend_string.h +++ b/Zend/zend_string.h @@ -223,6 +223,8 @@ static zend_always_inline zend_string *zend_string_safe_falloc(size_t n, size_t if (ZEND_CONST_COND(fallible, true) && UNEXPECTED(ret == NULL)) { return ret; } + /* GCC can't see that allocation fails if l is too large, so make it explicit. */ + ZEND_ASSERT(l <= ZSTR_MAX_LEN); GC_SET_REFCOUNT(ret, 1); GC_TYPE_INFO(ret) = GC_STRING | ((persistent ? IS_STR_PERSISTENT : 0) << GC_FLAGS_SHIFT); @@ -408,6 +410,8 @@ static zend_always_inline zend_string *zend_string_safe_frealloc(zend_string *s, if (ZEND_CONST_COND(fallible, true) && !ret) { return NULL; } + /* GCC can't see that allocation fails if l is too large, so make it explicit. */ + ZEND_ASSERT(l <= ZSTR_MAX_LEN); ZSTR_LEN(ret) = (n * m) + l; zend_string_forget_hash_val(ret); return ret; diff --git a/ext/mbstring/libmbfl/mbfl/mbfl_encoding.h b/ext/mbstring/libmbfl/mbfl/mbfl_encoding.h index 7d2aab7cf338..594e2f3ebd2a 100644 --- a/ext/mbstring/libmbfl/mbfl/mbfl_encoding.h +++ b/ext/mbstring/libmbfl/mbfl/mbfl_encoding.h @@ -175,6 +175,8 @@ static inline zend_result mb_convert_buf_ensure(mb_convert_buf *buf, unsigned ch buf->limit = *limit; return FAILURE; } + /* GCC can't see that allocation fails if growth is too large, so make it explicit. */ + ZEND_ASSERT(growth <= ZSTR_MAX_LEN); *out = (unsigned char*)ZSTR_VAL(newstr) + (*out - (unsigned char*)ZSTR_VAL((buf)->str)); *limit = (unsigned char*)ZSTR_VAL(newstr) + oldsize + growth; (buf)->str = newstr;