From 845bf0703da4765a279d81399d2d71ded0903640 Mon Sep 17 00:00:00 2001 From: Ben Ramsey Date: Sun, 5 Jul 2026 11:43:48 -0500 Subject: [PATCH 01/27] Add zend_memory_limit() and zend_memory_limit_is_unlimited() --- Zend/zend_alloc.c | 14 ++++++++++++++ Zend/zend_alloc.h | 2 ++ 2 files changed, 16 insertions(+) diff --git a/Zend/zend_alloc.c b/Zend/zend_alloc.c index 0b040743abf1..e0e9d9005c9b 100644 --- a/Zend/zend_alloc.c +++ b/Zend/zend_alloc.c @@ -2919,6 +2919,20 @@ ZEND_API zend_result zend_set_memory_limit(size_t memory_limit) return SUCCESS; } +ZEND_API size_t zend_memory_limit(void) +{ +#if ZEND_MM_LIMIT + return AG(mm_heap)->limit; +#else + return (size_t)Z_L(-1) >> 1; +#endif +} + +ZEND_API bool zend_memory_limit_is_unlimited(void) +{ + return zend_memory_limit() == ((size_t)Z_L(-1) >> 1); +} + ZEND_API bool zend_alloc_in_memory_limit_error_reporting(void) { #if ZEND_MM_LIMIT diff --git a/Zend/zend_alloc.h b/Zend/zend_alloc.h index ff51c4fe8652..f946783589b2 100644 --- a/Zend/zend_alloc.h +++ b/Zend/zend_alloc.h @@ -215,6 +215,8 @@ ZEND_API ZEND_ATTRIBUTE_MALLOC char * __zend_strdup(const char *s); #define pestrdup_rel(s, persistent) ((persistent)?strdup(s):estrdup_rel(s)) ZEND_API zend_result zend_set_memory_limit(size_t memory_limit); +ZEND_API size_t zend_memory_limit(void); +ZEND_API bool zend_memory_limit_is_unlimited(void); ZEND_API bool zend_alloc_in_memory_limit_error_reporting(void); ZEND_API void start_memory_manager(void); From 04031a45803ba81a8ba7d0c2664ba22977db5ac8 Mon Sep 17 00:00:00 2001 From: Ben Ramsey Date: Sun, 5 Jul 2026 11:43:49 -0500 Subject: [PATCH 02/27] Add MemoryError exception class --- Zend/tests/memory_error_class.phpt | 22 ++++++++++++++++++++++ Zend/zend_exceptions.c | 4 ++++ Zend/zend_exceptions.h | 1 + Zend/zend_exceptions.stub.php | 4 ++++ Zend/zend_exceptions_arginfo.h | 12 +++++++++++- 5 files changed, 42 insertions(+), 1 deletion(-) create mode 100644 Zend/tests/memory_error_class.phpt diff --git a/Zend/tests/memory_error_class.phpt b/Zend/tests/memory_error_class.phpt new file mode 100644 index 000000000000..8b5587ef0175 --- /dev/null +++ b/Zend/tests/memory_error_class.phpt @@ -0,0 +1,22 @@ +--TEST-- +MemoryError is a catchable Error +--FILE-- +getMessage()); + +try { + throw new MemoryError('caught as Error'); +} catch (Error $e) { + echo $e::class . ': ' . $e->getMessage() . "\n"; +} + +?> +--EXPECT-- +bool(true) +bool(true) +string(4) "boom" +MemoryError: caught as Error diff --git a/Zend/zend_exceptions.c b/Zend/zend_exceptions.c index a1301b8c20b2..6f74a8557555 100644 --- a/Zend/zend_exceptions.c +++ b/Zend/zend_exceptions.c @@ -47,6 +47,7 @@ ZEND_API zend_class_entry *zend_ce_argument_count_error; ZEND_API zend_class_entry *zend_ce_value_error; ZEND_API zend_class_entry *zend_ce_arithmetic_error; ZEND_API zend_class_entry *zend_ce_division_by_zero_error; +ZEND_API zend_class_entry *zend_ce_memory_error; ZEND_API zend_class_entry *zend_ce_unhandled_match_error; ZEND_API zend_class_entry *zend_ce_request_parse_body_exception; @@ -854,6 +855,9 @@ void zend_register_default_exception(void) /* {{{ */ zend_ce_division_by_zero_error = register_class_DivisionByZeroError(zend_ce_arithmetic_error); zend_init_exception_class_entry(zend_ce_division_by_zero_error); + zend_ce_memory_error = register_class_MemoryError(zend_ce_error); + zend_init_exception_class_entry(zend_ce_memory_error); + zend_ce_unhandled_match_error = register_class_UnhandledMatchError(zend_ce_error); zend_init_exception_class_entry(zend_ce_unhandled_match_error); diff --git a/Zend/zend_exceptions.h b/Zend/zend_exceptions.h index 7ef9ef016393..a85b9ca02c02 100644 --- a/Zend/zend_exceptions.h +++ b/Zend/zend_exceptions.h @@ -36,6 +36,7 @@ extern ZEND_API zend_class_entry *zend_ce_argument_count_error; extern ZEND_API zend_class_entry *zend_ce_value_error; extern ZEND_API zend_class_entry *zend_ce_arithmetic_error; extern ZEND_API zend_class_entry *zend_ce_division_by_zero_error; +extern ZEND_API zend_class_entry *zend_ce_memory_error; extern ZEND_API zend_class_entry *zend_ce_unhandled_match_error; extern ZEND_API zend_class_entry *zend_ce_request_parse_body_exception; diff --git a/Zend/zend_exceptions.stub.php b/Zend/zend_exceptions.stub.php index 86f2838ee912..e8946adbf3bb 100644 --- a/Zend/zend_exceptions.stub.php +++ b/Zend/zend_exceptions.stub.php @@ -167,6 +167,10 @@ class DivisionByZeroError extends ArithmeticError { } +class MemoryError extends Error +{ +} + class UnhandledMatchError extends Error { } diff --git a/Zend/zend_exceptions_arginfo.h b/Zend/zend_exceptions_arginfo.h index 4706161a09af..6ec42f5fab5c 100644 --- a/Zend/zend_exceptions_arginfo.h +++ b/Zend/zend_exceptions_arginfo.h @@ -1,5 +1,5 @@ /* This is a generated file, edit zend_exceptions.stub.php instead. - * Stub hash: ba1562ca8fe2fe48c40bc52d10545aa989afd86c */ + * Stub hash: ccb35f370935ff42169311e1ce0503c1106a07f0 */ ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_Throwable_getMessage, 0, 0, IS_STRING, 0) ZEND_END_ARG_INFO() @@ -317,6 +317,16 @@ static zend_class_entry *register_class_DivisionByZeroError(zend_class_entry *cl return class_entry; } +static zend_class_entry *register_class_MemoryError(zend_class_entry *class_entry_Error) +{ + zend_class_entry ce, *class_entry; + + INIT_CLASS_ENTRY(ce, "MemoryError", NULL); + class_entry = zend_register_internal_class_with_flags(&ce, class_entry_Error, 0); + + return class_entry; +} + static zend_class_entry *register_class_UnhandledMatchError(zend_class_entry *class_entry_Error) { zend_class_entry ce, *class_entry; From 5342f72c71f86b52557d742f5dd6fc1476762a2e Mon Sep 17 00:00:00 2001 From: Ben Ramsey Date: Sun, 5 Jul 2026 11:47:44 -0500 Subject: [PATCH 03/27] Add zend_string_alloc_size_exceeds_memory() pre-check helper --- Zend/zend_string.c | 23 +++++++++++++++++++++++ Zend/zend_string.h | 5 +++++ 2 files changed, 28 insertions(+) diff --git a/Zend/zend_string.c b/Zend/zend_string.c index b1b4a0e17a69..cee5b18391c8 100644 --- a/Zend/zend_string.c +++ b/Zend/zend_string.c @@ -18,6 +18,7 @@ #include "zend.h" #include "zend_globals.h" #include "zend_multiply.h" +#include "zend_exceptions.h" #ifdef HAVE_VALGRIND # include "valgrind/callgrind.h" @@ -528,6 +529,28 @@ ZEND_API zend_string *zend_string_concat3( return res; } +ZEND_API bool zend_string_alloc_size_exceeds_memory(size_t nmemb, size_t size, size_t offset) +{ + bool overflow; + size_t needed = zend_safe_address(nmemb, size, offset, &overflow); + + if (!overflow) { + if (zend_memory_limit_is_unlimited()) { + return false; + } + size_t limit = zend_memory_limit(); + size_t usage = zend_memory_usage(true); + size_t available = (limit > usage) ? (limit - usage) : 0; + if (needed <= available) { + return false; + } + } + + zend_throw_error(zend_ce_memory_error, + "The resulting string is too large to fit in the configured memory limit"); + return true; +} + /* strlcpy and strlcat are not always intercepted by msan, so we need to do it * ourselves. Apply a simple heuristic to determine the platforms that need it. * See https://github.com/php/php-src/issues/20002. */ diff --git a/Zend/zend_string.h b/Zend/zend_string.h index fdfff2bbc894..1b862720fb40 100644 --- a/Zend/zend_string.h +++ b/Zend/zend_string.h @@ -54,6 +54,11 @@ ZEND_API zend_string *zend_string_concat3( const char *str2, size_t str2_len, const char *str3, size_t str3_len); +/* Throws a catchable MemoryError and returns true when allocating a string of + * "nmemb * size + offset" bytes would overflow or exceed the configured memory + * limit; returns false (without throwing) otherwise. */ +ZEND_API bool zend_string_alloc_size_exceeds_memory(size_t nmemb, size_t size, size_t offset); + ZEND_API void zend_interned_strings_init(void); ZEND_API void zend_interned_strings_dtor(void); ZEND_API void zend_interned_strings_activate(void); From 933e246d91c147ea4b90294efbeb47de57f3b8fc Mon Sep 17 00:00:00 2001 From: Ben Ramsey Date: Sun, 5 Jul 2026 11:47:44 -0500 Subject: [PATCH 04/27] Throw MemoryError for an over-large string offset assignment --- .../string_offset_write_memory_error.phpt | 33 +++++++++++++++++++ Zend/zend_execute.c | 19 +++++++++++ 2 files changed, 52 insertions(+) create mode 100644 Zend/tests/offsets/string_offset_write_memory_error.phpt diff --git a/Zend/tests/offsets/string_offset_write_memory_error.phpt b/Zend/tests/offsets/string_offset_write_memory_error.phpt new file mode 100644 index 000000000000..dafe38df17af --- /dev/null +++ b/Zend/tests/offsets/string_offset_write_memory_error.phpt @@ -0,0 +1,33 @@ +--TEST-- +Writing to a string at an offset too large for the memory limit throws a catchable MemoryError +--INI-- +memory_limit=64M +opcache.enable_cli=0 +--FILE-- +getMessage() . "\n"; +} +var_dump($s); + +// The throw unwinds the whole assignment, so the outer target keeps its previous value. +$r = 'untouched'; +try { + $r = ($s[PHP_INT_MAX] = 'Z'); +} catch (MemoryError $e) { + echo "caught\n"; +} +var_dump($r); +var_dump($s); + +?> +--EXPECT-- +String offset assignment produces a string too large to fit in the configured memory limit +string(3) "abc" +caught +string(9) "untouched" +string(3) "abc" diff --git a/Zend/zend_execute.c b/Zend/zend_execute.c index 1b28ce25fe37..199197053ac3 100644 --- a/Zend/zend_execute.c +++ b/Zend/zend_execute.c @@ -2037,6 +2037,17 @@ ZEND_API ZEND_COLD void ZEND_FASTCALL zend_false_to_array_deprecated(void) zend_error(E_DEPRECATED, "Automatic conversion of false to array is deprecated"); } +static zend_always_inline bool zend_string_offset_extend_exceeds_memory(size_t new_len) +{ + if (zend_memory_limit_is_unlimited()) { + return false; + } + size_t limit = zend_memory_limit(); + size_t usage = zend_memory_usage(true); + size_t available = (limit > usage) ? (limit - usage) : 0; + return new_len > available; +} + static zend_never_inline void zend_assign_to_string_offset(zval *str, zval *dim, zval *value OPLINE_DC EXECUTE_DATA_DC) { zend_uchar c; @@ -2161,6 +2172,14 @@ static zend_never_inline void zend_assign_to_string_offset(zval *str, zval *dim, if ((size_t)offset >= ZSTR_LEN(s)) { /* Extend string if needed */ zend_long old_len = ZSTR_LEN(s); + if (UNEXPECTED(zend_string_offset_extend_exceeds_memory((size_t)offset + 1))) { + zend_throw_error(zend_ce_memory_error, + "String offset assignment produces a string too large to fit in the configured memory limit"); + if (UNEXPECTED(RETURN_VALUE_USED(opline))) { + ZVAL_UNDEF(EX_VAR(opline->result.var)); + } + return; + } ZVAL_NEW_STR(str, zend_string_extend(s, (size_t)offset + 1, 0)); memset(Z_STRVAL_P(str) + old_len, ' ', offset - old_len); Z_STRVAL_P(str)[offset+1] = 0; From 787186cf3c17fb00cfbe3dcf2c87e4c9a9d1e673 Mon Sep 17 00:00:00 2001 From: Ben Ramsey Date: Sun, 5 Jul 2026 11:52:03 -0500 Subject: [PATCH 05/27] Throw MemoryError in str_repeat(), str_pad(), number_format() --- ext/standard/math.c | 4 ++++ ext/standard/string.c | 7 ++++++ .../math/number_format_memory_error.phpt | 20 ++++++++++++++++ .../tests/strings/str_pad_memory_error.phpt | 20 ++++++++++++++++ .../tests/strings/str_pad_variation1.phpt | 11 +++++---- .../strings/str_repeat_memory_error.phpt | 24 +++++++++++++++++++ 6 files changed, 82 insertions(+), 4 deletions(-) create mode 100644 ext/standard/tests/math/number_format_memory_error.phpt create mode 100644 ext/standard/tests/strings/str_pad_memory_error.phpt create mode 100644 ext/standard/tests/strings/str_repeat_memory_error.phpt diff --git a/ext/standard/math.c b/ext/standard/math.c index 1498a1efc0b2..5a37eef06c64 100644 --- a/ext/standard/math.c +++ b/ext/standard/math.c @@ -1420,6 +1420,10 @@ PHP_FUNCTION(number_format) RETURN_THROWS(); } + if (dec > 0 && UNEXPECTED(zend_string_alloc_size_exceeds_memory((size_t)dec, 1, 0))) { + RETURN_THROWS(); + } + switch (Z_TYPE_P(num)) { case IS_LONG: RETURN_STR(_php_math_number_format_long(Z_LVAL_P(num), dec, dec_point, dec_point_len, thousand_sep, thousand_sep_len)); diff --git a/ext/standard/string.c b/ext/standard/string.c index c6e89dcca2e5..9b0cdf77eea9 100644 --- a/ext/standard/string.c +++ b/ext/standard/string.c @@ -5488,6 +5488,10 @@ PHP_FUNCTION(str_repeat) if (ZSTR_LEN(input_str) == 0 || mult == 0) RETURN_EMPTY_STRING(); + if (UNEXPECTED(zend_string_alloc_size_exceeds_memory(ZSTR_LEN(input_str), mult, 0))) { + RETURN_THROWS(); + } + /* Initialize the result string */ result = zend_string_safe_alloc(ZSTR_LEN(input_str), mult, 0, 0); result_len = ZSTR_LEN(input_str) * mult; @@ -5797,6 +5801,9 @@ PHP_FUNCTION(str_pad) } num_pad_chars = pad_length - ZSTR_LEN(input); + if (UNEXPECTED(zend_string_alloc_size_exceeds_memory(1, ZSTR_LEN(input), num_pad_chars))) { + RETURN_THROWS(); + } result = zend_string_safe_alloc(1, ZSTR_LEN(input), num_pad_chars, 0); ZSTR_LEN(result) = 0; diff --git a/ext/standard/tests/math/number_format_memory_error.phpt b/ext/standard/tests/math/number_format_memory_error.phpt new file mode 100644 index 000000000000..ed6cbf911501 --- /dev/null +++ b/ext/standard/tests/math/number_format_memory_error.phpt @@ -0,0 +1,20 @@ +--TEST-- +number_format() throws a catchable MemoryError when the decimals cannot fit in the memory limit +--INI-- +memory_limit=64M +opcache.enable_cli=0 +--FILE-- +getMessage() . "\n"; +} + +var_dump(number_format(1234.5678, 2)); + +?> +--EXPECT-- +MemoryError: The resulting string is too large to fit in the configured memory limit +string(8) "1,234.57" diff --git a/ext/standard/tests/strings/str_pad_memory_error.phpt b/ext/standard/tests/strings/str_pad_memory_error.phpt new file mode 100644 index 000000000000..a7160a93b965 --- /dev/null +++ b/ext/standard/tests/strings/str_pad_memory_error.phpt @@ -0,0 +1,20 @@ +--TEST-- +str_pad() throws a catchable MemoryError when the result cannot fit in the memory limit +--INI-- +memory_limit=64M +opcache.enable_cli=0 +--FILE-- +getMessage() . "\n"; +} + +var_dump(str_pad('x', 5, '-')); + +?> +--EXPECT-- +MemoryError: The resulting string is too large to fit in the configured memory limit +string(5) "x----" diff --git a/ext/standard/tests/strings/str_pad_variation1.phpt b/ext/standard/tests/strings/str_pad_variation1.phpt index cb71e61156fc..2e4d04c31edc 100644 --- a/ext/standard/tests/strings/str_pad_variation1.phpt +++ b/ext/standard/tests/strings/str_pad_variation1.phpt @@ -25,12 +25,15 @@ try { } $php_int_max_pad_length = PHP_INT_MAX; -var_dump( str_pad($input, $php_int_max_pad_length) ); +try { + var_dump( str_pad($input, $php_int_max_pad_length) ); +} catch (\MemoryError $e) { + echo $e->getMessage() . "\n"; +} ?> ---EXPECTF-- +--EXPECT-- *** Testing str_pad() function: with large value for for 'pad_length' argument *** str_pad(): Argument #2 ($length) must be of type int, float given - -Fatal error: Allowed memory size of %d bytes exhausted%s(tried to allocate %d bytes) in %s on line %d +The resulting string is too large to fit in the configured memory limit diff --git a/ext/standard/tests/strings/str_repeat_memory_error.phpt b/ext/standard/tests/strings/str_repeat_memory_error.phpt new file mode 100644 index 000000000000..4867f0f9946f --- /dev/null +++ b/ext/standard/tests/strings/str_repeat_memory_error.phpt @@ -0,0 +1,24 @@ +--TEST-- +str_repeat() throws a catchable MemoryError when the result cannot fit in the memory limit +--INI-- +memory_limit=64M +opcache.enable_cli=0 +--FILE-- +getMessage() . "\n"; +} + +try { + str_repeat('ab', -1); +} catch (ValueError $e) { + echo $e->getMessage() . "\n"; +} + +?> +--EXPECT-- +MemoryError: The resulting string is too large to fit in the configured memory limit +str_repeat(): Argument #2 ($times) must be greater than or equal to 0 From 0dbbb5e8ec34a06ea04664d15c07663491b4e75b Mon Sep 17 00:00:00 2001 From: Ben Ramsey Date: Sun, 5 Jul 2026 11:52:04 -0500 Subject: [PATCH 06/27] Throw MemoryError in random_bytes() --- ext/random/random.c | 4 ++++ .../random_bytes_memory_error.phpt | 24 +++++++++++++++++++ 2 files changed, 28 insertions(+) create mode 100644 ext/random/tests/01_functions/random_bytes_memory_error.phpt diff --git a/ext/random/random.c b/ext/random/random.c index e7fe2c21801e..572f67f5eaf3 100644 --- a/ext/random/random.c +++ b/ext/random/random.c @@ -590,6 +590,10 @@ PHP_FUNCTION(random_bytes) RETURN_THROWS(); } + if (UNEXPECTED(zend_string_alloc_size_exceeds_memory(size, 1, 0))) { + RETURN_THROWS(); + } + bytes = zend_string_alloc(size, 0); if (php_random_bytes_throw(ZSTR_VAL(bytes), size) == FAILURE) { diff --git a/ext/random/tests/01_functions/random_bytes_memory_error.phpt b/ext/random/tests/01_functions/random_bytes_memory_error.phpt new file mode 100644 index 000000000000..de59da43cdd0 --- /dev/null +++ b/ext/random/tests/01_functions/random_bytes_memory_error.phpt @@ -0,0 +1,24 @@ +--TEST-- +random_bytes() throws a catchable MemoryError when the requested size cannot fit in the memory limit +--INI-- +memory_limit=64M +opcache.enable_cli=0 +--FILE-- +getMessage() . "\n"; +} + +try { + random_bytes(0); +} catch (ValueError $e) { + echo $e->getMessage() . "\n"; +} + +?> +--EXPECT-- +MemoryError: The resulting string is too large to fit in the configured memory limit +random_bytes(): Argument #1 ($length) must be greater than 0 From 9ad6ff428a5e5a5a73d38f7aa3cb946b1f349230 Mon Sep 17 00:00:00 2001 From: Ben Ramsey Date: Sun, 5 Jul 2026 11:52:04 -0500 Subject: [PATCH 07/27] Throw MemoryError in mb_str_pad() --- ext/mbstring/mbstring.c | 7 +++++- .../tests/mb_str_pad_memory_error.phpt | 22 +++++++++++++++++++ 2 files changed, 28 insertions(+), 1 deletion(-) create mode 100644 ext/mbstring/tests/mb_str_pad_memory_error.phpt diff --git a/ext/mbstring/mbstring.c b/ext/mbstring/mbstring.c index 23420f87131f..bb6879ef219d 100644 --- a/ext/mbstring/mbstring.c +++ b/ext/mbstring/mbstring.c @@ -22,6 +22,7 @@ #include "php.h" #include "php_ini.h" #include "php_variables.h" +#include "zend_exceptions.h" #include "mbstring.h" #include "ext/standard/php_string.h" #include "ext/standard/php_mail.h" @@ -5933,6 +5934,10 @@ PHP_FUNCTION(mb_str_pad) size_t num_mb_pad_chars = pad_to_length - input_length; + if (UNEXPECTED(zend_string_alloc_size_exceeds_memory(num_mb_pad_chars, 1, ZSTR_LEN(input)))) { + RETURN_THROWS(); + } + /* We need to figure out the left/right padding lengths. */ size_t left_pad = 0, right_pad = 0; /* Initialize here to silence compiler warnings. */ switch (pad_type_val) { @@ -6013,7 +6018,7 @@ PHP_FUNCTION(mb_str_pad) zend_string_release_ex(remaining_left_pad_str, false); zend_string_release_ex(remaining_right_pad_str, false); overflow_no_release: - zend_throw_error(NULL, "String size overflow"); + zend_throw_error(zend_ce_memory_error, "The resulting string is too large to fit in the configured memory limit"); RETURN_THROWS(); } diff --git a/ext/mbstring/tests/mb_str_pad_memory_error.phpt b/ext/mbstring/tests/mb_str_pad_memory_error.phpt new file mode 100644 index 000000000000..2a2c64a90ca1 --- /dev/null +++ b/ext/mbstring/tests/mb_str_pad_memory_error.phpt @@ -0,0 +1,22 @@ +--TEST-- +mb_str_pad() throws a catchable MemoryError when the result cannot fit in the memory limit +--EXTENSIONS-- +mbstring +--INI-- +memory_limit=64M +opcache.enable_cli=0 +--FILE-- +getMessage() . "\n"; +} + +var_dump(mb_str_pad('x', 5, '-')); + +?> +--EXPECT-- +MemoryError: The resulting string is too large to fit in the configured memory limit +string(5) "x----" From 9afae2a1416661f914ecc7a595ccc82c35e23f1d Mon Sep 17 00:00:00 2001 From: Ben Ramsey Date: Sun, 5 Jul 2026 11:57:17 -0500 Subject: [PATCH 08/27] Adjust OOM bailout tests to reach the memory limit --- Zend/tests/bug40770.phpt | 7 +++---- Zend/tests/fibers/get-return-after-bailout.phpt | 5 ++++- Zend/tests/fibers/out-of-memory-in-fiber.phpt | 4 ++-- Zend/tests/fibers/out-of-memory-in-nested-fiber.phpt | 4 ++-- 4 files changed, 11 insertions(+), 9 deletions(-) diff --git a/Zend/tests/bug40770.phpt b/Zend/tests/bug40770.phpt index bdbae4cf8f1a..88c8fccb3fc8 100644 --- a/Zend/tests/bug40770.phpt +++ b/Zend/tests/bug40770.phpt @@ -15,10 +15,9 @@ if ($zend_mm_enabled === "0") { --FILE-- --EXPECTF-- diff --git a/Zend/tests/fibers/get-return-after-bailout.phpt b/Zend/tests/fibers/get-return-after-bailout.phpt index 79ab70c98baa..aa080adc2948 100644 --- a/Zend/tests/fibers/get-return-after-bailout.phpt +++ b/Zend/tests/fibers/get-return-after-bailout.phpt @@ -16,7 +16,10 @@ register_shutdown_function(static function (): void { }); $fiber = new Fiber(static function (): void { - str_repeat('X', PHP_INT_MAX); + $x = 'X'; + while (true) { + $x .= $x; + } }); $fiber->start(); diff --git a/Zend/tests/fibers/out-of-memory-in-fiber.phpt b/Zend/tests/fibers/out-of-memory-in-fiber.phpt index 4d31aa6f76ad..32696c747ce8 100644 --- a/Zend/tests/fibers/out-of-memory-in-fiber.phpt +++ b/Zend/tests/fibers/out-of-memory-in-fiber.phpt @@ -12,9 +12,9 @@ if (getenv("USE_ZEND_ALLOC") === "0") { Date: Sun, 5 Jul 2026 14:12:49 -0500 Subject: [PATCH 09/27] Base pre-check on used memory rather than reserved chunk size --- Zend/zend_execute.c | 2 +- Zend/zend_string.c | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Zend/zend_execute.c b/Zend/zend_execute.c index 199197053ac3..433dd4daffc2 100644 --- a/Zend/zend_execute.c +++ b/Zend/zend_execute.c @@ -2043,7 +2043,7 @@ static zend_always_inline bool zend_string_offset_extend_exceeds_memory(size_t n return false; } size_t limit = zend_memory_limit(); - size_t usage = zend_memory_usage(true); + size_t usage = zend_memory_usage(false); size_t available = (limit > usage) ? (limit - usage) : 0; return new_len > available; } diff --git a/Zend/zend_string.c b/Zend/zend_string.c index cee5b18391c8..4987a02d532c 100644 --- a/Zend/zend_string.c +++ b/Zend/zend_string.c @@ -539,7 +539,7 @@ ZEND_API bool zend_string_alloc_size_exceeds_memory(size_t nmemb, size_t size, s return false; } size_t limit = zend_memory_limit(); - size_t usage = zend_memory_usage(true); + size_t usage = zend_memory_usage(false); size_t available = (limit > usage) ? (limit - usage) : 0; if (needed <= available) { return false; From eacbd519ced782cd1f744224bbb30a847ba85d19 Mon Sep 17 00:00:00 2001 From: Ben Ramsey Date: Sun, 5 Jul 2026 14:12:50 -0500 Subject: [PATCH 10/27] Throw MemoryError in fread() and fgets() --- ext/standard/file.c | 8 ++++++ .../tests/file/fgets_memory_error.phpt | 26 +++++++++++++++++++ .../tests/file/fread_memory_error.phpt | 25 ++++++++++++++++++ 3 files changed, 59 insertions(+) create mode 100644 ext/standard/tests/file/fgets_memory_error.phpt create mode 100644 ext/standard/tests/file/fread_memory_error.phpt diff --git a/ext/standard/file.c b/ext/standard/file.c index 0bb00355744f..1612bb6fd806 100644 --- a/ext/standard/file.c +++ b/ext/standard/file.c @@ -912,6 +912,10 @@ PHPAPI PHP_FUNCTION(fgets) RETURN_THROWS(); } + if (UNEXPECTED(zend_string_alloc_size_exceeds_memory(len, 1, 0))) { + RETURN_THROWS(); + } + str = zend_string_alloc(len, 0); buf = php_stream_get_line(stream, ZSTR_VAL(str), len, &line_len); php_stream_error_operation_end_for_stream(stream); @@ -1618,6 +1622,10 @@ PHPAPI PHP_FUNCTION(fread) RETURN_THROWS(); } + if (UNEXPECTED(zend_string_alloc_size_exceeds_memory(len, 1, 0))) { + RETURN_THROWS(); + } + php_stream_error_operation_begin(); str = php_stream_read_to_str(stream, len); php_stream_error_operation_end_for_stream(stream); diff --git a/ext/standard/tests/file/fgets_memory_error.phpt b/ext/standard/tests/file/fgets_memory_error.phpt new file mode 100644 index 000000000000..696da90df6f2 --- /dev/null +++ b/ext/standard/tests/file/fgets_memory_error.phpt @@ -0,0 +1,26 @@ +--TEST-- +fgets() throws a catchable MemoryError when the requested length cannot fit in the memory limit +--INI-- +memory_limit=64M +opcache.enable_cli=0 +--FILE-- +getMessage() . "\n"; +} + +var_dump(fgets($fp, 100)); +fclose($fp); + +?> +--EXPECT-- +MemoryError: The resulting string is too large to fit in the configured memory limit +string(6) "hello +" diff --git a/ext/standard/tests/file/fread_memory_error.phpt b/ext/standard/tests/file/fread_memory_error.phpt new file mode 100644 index 000000000000..f4a25218269d --- /dev/null +++ b/ext/standard/tests/file/fread_memory_error.phpt @@ -0,0 +1,25 @@ +--TEST-- +fread() throws a catchable MemoryError when the requested length cannot fit in the memory limit +--INI-- +memory_limit=64M +opcache.enable_cli=0 +--FILE-- +getMessage() . "\n"; +} + +var_dump(fread($fp, 5)); +fclose($fp); + +?> +--EXPECT-- +MemoryError: The resulting string is too large to fit in the configured memory limit +string(5) "hello" From 9c843f87405a542634214b76e8a78bbd6ccd7de1 Mon Sep 17 00:00:00 2001 From: Ben Ramsey Date: Sun, 5 Jul 2026 14:12:50 -0500 Subject: [PATCH 11/27] Throw MemoryError in stream_socket_recvfrom() --- ext/standard/streamsfuncs.c | 4 +++ .../stream_socket_recvfrom_memory_error.phpt | 29 +++++++++++++++++++ 2 files changed, 33 insertions(+) create mode 100644 ext/standard/tests/streams/stream_socket_recvfrom_memory_error.phpt diff --git a/ext/standard/streamsfuncs.c b/ext/standard/streamsfuncs.c index 7962f0f85de3..347e0ad64420 100644 --- a/ext/standard/streamsfuncs.c +++ b/ext/standard/streamsfuncs.c @@ -427,6 +427,10 @@ PHP_FUNCTION(stream_socket_recvfrom) RETURN_THROWS(); } + if (UNEXPECTED(zend_string_alloc_size_exceeds_memory(to_read, 1, 0))) { + RETURN_THROWS(); + } + read_buf = zend_string_alloc(to_read, 0); php_stream_error_operation_begin(); diff --git a/ext/standard/tests/streams/stream_socket_recvfrom_memory_error.phpt b/ext/standard/tests/streams/stream_socket_recvfrom_memory_error.phpt new file mode 100644 index 000000000000..f41fc563865f --- /dev/null +++ b/ext/standard/tests/streams/stream_socket_recvfrom_memory_error.phpt @@ -0,0 +1,29 @@ +--TEST-- +stream_socket_recvfrom() throws a catchable MemoryError when the requested length cannot fit in the memory limit +--SKIPIF-- + +--INI-- +memory_limit=64M +opcache.enable_cli=0 +--FILE-- +getMessage() . "\n"; +} + +var_dump(stream_socket_recvfrom($a, 4)); +fclose($a); +fclose($b); + +?> +--EXPECT-- +MemoryError: The resulting string is too large to fit in the configured memory limit +string(4) "ping" From 4cf9e0164241ff2b38d3f874c7dca3f4745b1ce5 Mon Sep 17 00:00:00 2001 From: Ben Ramsey Date: Sun, 5 Jul 2026 14:12:50 -0500 Subject: [PATCH 12/27] Throw MemoryError in socket_read(), socket_recv(), socket_recvfrom() --- ext/sockets/sockets.c | 12 +++++ .../tests/socket_read_recv_memory_error.phpt | 46 +++++++++++++++++++ 2 files changed, 58 insertions(+) create mode 100644 ext/sockets/tests/socket_read_recv_memory_error.phpt diff --git a/ext/sockets/sockets.c b/ext/sockets/sockets.c index dbf7f4e6ad4b..3555e228e020 100644 --- a/ext/sockets/sockets.c +++ b/ext/sockets/sockets.c @@ -948,6 +948,10 @@ PHP_FUNCTION(socket_read) RETURN_FALSE; } + if (UNEXPECTED(zend_string_alloc_size_exceeds_memory(length, 1, 0))) { + RETURN_THROWS(); + } + tmpbuf = zend_string_alloc(length, 0); if (type == PHP_NORMAL_READ) { @@ -1434,6 +1438,10 @@ PHP_FUNCTION(socket_recv) RETURN_FALSE; } + if (UNEXPECTED(zend_string_alloc_size_exceeds_memory(len, 1, 0))) { + RETURN_THROWS(); + } + recv_buf = zend_string_alloc(len, 0); if ((retval = recv(php_sock->bsd_socket, ZSTR_VAL(recv_buf), len, flags)) < 1) { @@ -1562,6 +1570,10 @@ PHP_FUNCTION(socket_recvfrom) RETURN_FALSE; } + if (UNEXPECTED(zend_string_alloc_size_exceeds_memory(length, 1, 1))) { + RETURN_THROWS(); + } + recv_buf = zend_string_alloc(length + 1, 0); switch (php_sock->type) { diff --git a/ext/sockets/tests/socket_read_recv_memory_error.phpt b/ext/sockets/tests/socket_read_recv_memory_error.phpt new file mode 100644 index 000000000000..2e565b7ef704 --- /dev/null +++ b/ext/sockets/tests/socket_read_recv_memory_error.phpt @@ -0,0 +1,46 @@ +--TEST-- +socket_read(), socket_recv(), and socket_recvfrom() throw a catchable MemoryError +--EXTENSIONS-- +sockets +--SKIPIF-- + +--INI-- +memory_limit=64M +opcache.enable_cli=0 +--FILE-- +getMessage() . "\n"; +} + +try { + socket_recv($a, $buf, PHP_INT_MAX - 1, 0); +} catch (MemoryError $e) { + echo 'socket_recv: ' . $e::class . ': ' . $e->getMessage() . "\n"; +} + +try { + socket_recvfrom($a, $buf, PHP_INT_MAX - 2, 0, $name); +} catch (MemoryError $e) { + echo 'socket_recvfrom: ' . $e::class . ': ' . $e->getMessage() . "\n"; +} + +var_dump(socket_read($a, 4)); +socket_close($a); +socket_close($b); + +?> +--EXPECT-- +socket_read: MemoryError: The resulting string is too large to fit in the configured memory limit +socket_recv: MemoryError: The resulting string is too large to fit in the configured memory limit +socket_recvfrom: MemoryError: The resulting string is too large to fit in the configured memory limit +string(4) "ping" From 5c6e85181a77524975c5e74e12b10c447d62b898 Mon Sep 17 00:00:00 2001 From: Ben Ramsey Date: Sun, 5 Jul 2026 14:12:50 -0500 Subject: [PATCH 13/27] Throw MemoryError in pack() and chunk_split() --- ext/standard/pack.c | 6 +++++ ext/standard/string.c | 7 ++++++ .../strings/chunk_split_memory_error.phpt | 23 +++++++++++++++++++ .../tests/strings/chunk_split_variation3.phpt | 11 +++++---- .../tests/strings/pack_memory_error.phpt | 20 ++++++++++++++++ 5 files changed, 63 insertions(+), 4 deletions(-) create mode 100644 ext/standard/tests/strings/chunk_split_memory_error.phpt create mode 100644 ext/standard/tests/strings/pack_memory_error.phpt diff --git a/ext/standard/pack.c b/ext/standard/pack.c index 1ab5d4858cbe..2e622dd1c4a5 100644 --- a/ext/standard/pack.c +++ b/ext/standard/pack.c @@ -426,6 +426,12 @@ PHP_FUNCTION(pack) } } + if (UNEXPECTED(zend_string_alloc_size_exceeds_memory(outputsize, 1, 0))) { + efree(formatcodes); + efree(formatargs); + RETURN_THROWS(); + } + output = zend_string_alloc(outputsize, 0); outputpos = 0; currentarg = 0; diff --git a/ext/standard/string.c b/ext/standard/string.c index 9b0cdf77eea9..3014cf4f1d96 100644 --- a/ext/standard/string.c +++ b/ext/standard/string.c @@ -2143,6 +2143,10 @@ static zend_string *php_chunk_split(const char *src, size_t srclen, const char * chunks++; } + if (UNEXPECTED(zend_string_alloc_size_exceeds_memory(chunks, endlen, srclen))) { + return NULL; + } + dest = zend_string_safe_alloc(chunks, endlen, srclen, 0); for (p = src, q = ZSTR_VAL(dest); p < (src + srclen - chunklen + 1); ) { @@ -2198,6 +2202,9 @@ PHP_FUNCTION(chunk_split) } result = php_chunk_split(ZSTR_VAL(str), ZSTR_LEN(str), end, endlen, (size_t)chunklen); + if (UNEXPECTED(!result)) { + RETURN_THROWS(); + } RETURN_STR(result); } diff --git a/ext/standard/tests/strings/chunk_split_memory_error.phpt b/ext/standard/tests/strings/chunk_split_memory_error.phpt new file mode 100644 index 000000000000..2f9222ab07b3 --- /dev/null +++ b/ext/standard/tests/strings/chunk_split_memory_error.phpt @@ -0,0 +1,23 @@ +--TEST-- +chunk_split() throws a catchable MemoryError when the result cannot fit in the memory limit +--INI-- +memory_limit=64M +opcache.enable_cli=0 +--FILE-- +getMessage() . "\n"; +} + +var_dump(chunk_split('abcd', 2, '|')); + +?> +--EXPECT-- +MemoryError: The resulting string is too large to fit in the configured memory limit +string(6) "ab|cd|" diff --git a/ext/standard/tests/strings/chunk_split_variation3.phpt b/ext/standard/tests/strings/chunk_split_variation3.phpt index 56c524ef02d5..e3e010aaed3f 100644 --- a/ext/standard/tests/strings/chunk_split_variation3.phpt +++ b/ext/standard/tests/strings/chunk_split_variation3.phpt @@ -14,11 +14,14 @@ echo "Body generation\n"; $body = str_repeat("Hello", 10000000); echo "Using chunk_split()\n"; -var_dump(chunk_split($body, $chunk_length)); +try { + var_dump(chunk_split($body, $chunk_length)); +} catch (MemoryError $e) { + echo $e->getMessage() . "\n"; +} ?> ---EXPECTF-- +--EXPECT-- *** Testing chunk_split() : unexpected large 'end' string argument variation 2 *** Body generation Using chunk_split() - -Fatal error: Allowed memory size of %d bytes exhausted%s(tried to allocate %d bytes) in %s on line %d +The resulting string is too large to fit in the configured memory limit diff --git a/ext/standard/tests/strings/pack_memory_error.phpt b/ext/standard/tests/strings/pack_memory_error.phpt new file mode 100644 index 000000000000..bc57fcaa86cb --- /dev/null +++ b/ext/standard/tests/strings/pack_memory_error.phpt @@ -0,0 +1,20 @@ +--TEST-- +pack() throws a catchable MemoryError when the output size cannot fit in the memory limit +--INI-- +memory_limit=64M +opcache.enable_cli=0 +--FILE-- +getMessage() . "\n"; +} + +var_dump(bin2hex(pack('x4'))); + +?> +--EXPECT-- +MemoryError: The resulting string is too large to fit in the configured memory limit +string(8) "00000000" From 23faae0fc9d683d668c6c975d5546867d48827ff Mon Sep 17 00:00:00 2001 From: Ben Ramsey Date: Sun, 5 Jul 2026 14:12:51 -0500 Subject: [PATCH 14/27] Throw MemoryError for a printf-family too-large width --- ext/standard/formatted_print.c | 4 +++ .../tests/strings/sprintf_memory_error.phpt | 32 +++++++++++++++++++ 2 files changed, 36 insertions(+) create mode 100644 ext/standard/tests/strings/sprintf_memory_error.phpt diff --git a/ext/standard/formatted_print.c b/ext/standard/formatted_print.c index f35d196fb971..d30091dff8bb 100644 --- a/ext/standard/formatted_print.c +++ b/ext/standard/formatted_print.c @@ -547,6 +547,10 @@ php_formatted_print(char *format, size_t format_len, zval *args, int argc, int n } PRINTF_DEBUG(("sprintf: width=%d\n", width)); + if (width > 0 && UNEXPECTED(zend_string_alloc_size_exceeds_memory(width, 1, 0))) { + goto fail; + } + /* after width and argnum comes precision */ if (*format == '.') { format++; diff --git a/ext/standard/tests/strings/sprintf_memory_error.phpt b/ext/standard/tests/strings/sprintf_memory_error.phpt new file mode 100644 index 000000000000..0b7d4497f8b2 --- /dev/null +++ b/ext/standard/tests/strings/sprintf_memory_error.phpt @@ -0,0 +1,32 @@ +--TEST-- +sprintf() throws a catchable MemoryError for a field width too large for the memory limit +--INI-- +memory_limit=64M +opcache.enable_cli=0 +--FILE-- +getMessage() . "\n"; +} + +try { + sprintf('%-2147483000s', 'x'); +} catch (MemoryError $e) { + echo 'width (string pad): ' . $e::class . "\n"; +} + +// Precision is capped for floats and truncates for strings, so it never +// drives a large allocation and keeps its existing behavior. +var_dump(strlen(@sprintf('%.2147483000f', 1.5))); + +var_dump(sprintf('%10d', 42)); + +?> +--EXPECT-- +width: MemoryError: The resulting string is too large to fit in the configured memory limit +width (string pad): MemoryError +int(55) +string(10) " 42" From 26159820418669e12fec8bc6f25c02978a0b7030 Mon Sep 17 00:00:00 2001 From: Ben Ramsey Date: Sun, 5 Jul 2026 15:13:56 -0500 Subject: [PATCH 15/27] Generalize the MemoryError pre-check for non-string allocations --- Zend/zend_alloc.c | 23 +++++++++++++++++++++++ Zend/zend_alloc.h | 5 +++++ Zend/zend_hash.c | 6 ++++++ Zend/zend_hash.h | 5 +++++ Zend/zend_string.c | 20 +------------------- 5 files changed, 40 insertions(+), 19 deletions(-) diff --git a/Zend/zend_alloc.c b/Zend/zend_alloc.c index e0e9d9005c9b..58defc721ebd 100644 --- a/Zend/zend_alloc.c +++ b/Zend/zend_alloc.c @@ -57,6 +57,7 @@ #include "zend_operators.h" #include "zend_multiply.h" #include "zend_bitset.h" +#include "zend_exceptions.h" #include "zend_mmap.h" #include "zend_portability.h" #include @@ -2933,6 +2934,28 @@ ZEND_API bool zend_memory_limit_is_unlimited(void) return zend_memory_limit() == ((size_t)Z_L(-1) >> 1); } +ZEND_API bool zend_alloc_size_exceeds_memory(size_t nmemb, size_t size, size_t offset, const char *kind) +{ + bool overflow; + size_t needed = zend_safe_address(nmemb, size, offset, &overflow); + + if (!overflow) { + if (zend_memory_limit_is_unlimited()) { + return false; + } + size_t limit = zend_memory_limit(); + size_t usage = zend_memory_usage(false); + size_t available = (limit > usage) ? (limit - usage) : 0; + if (needed <= available) { + return false; + } + } + + zend_throw_error(zend_ce_memory_error, + "The resulting %s is too large to fit in the configured memory limit", kind); + return true; +} + ZEND_API bool zend_alloc_in_memory_limit_error_reporting(void) { #if ZEND_MM_LIMIT diff --git a/Zend/zend_alloc.h b/Zend/zend_alloc.h index f946783589b2..84f01620f9c5 100644 --- a/Zend/zend_alloc.h +++ b/Zend/zend_alloc.h @@ -219,6 +219,11 @@ ZEND_API size_t zend_memory_limit(void); ZEND_API bool zend_memory_limit_is_unlimited(void); ZEND_API bool zend_alloc_in_memory_limit_error_reporting(void); +/* Throws a catchable MemoryError and returns true when allocating "nmemb * + * size + offset" bytes would overflow or exceed the configured memory limit; + * returns false (without throwing) otherwise. */ +ZEND_API bool zend_alloc_size_exceeds_memory(size_t nmemb, size_t size, size_t offset, const char *kind); + ZEND_API void start_memory_manager(void); ZEND_API void shutdown_memory_manager(bool silent, bool full_shutdown); ZEND_API void refresh_memory_manager(void); diff --git a/Zend/zend_hash.c b/Zend/zend_hash.c index acc342bc267d..af7cdacf9d8d 100644 --- a/Zend/zend_hash.c +++ b/Zend/zend_hash.c @@ -391,6 +391,12 @@ ZEND_API void ZEND_FASTCALL zend_hash_to_packed(HashTable *ht) pefree(old_data, GC_FLAGS(ht) & IS_ARRAY_PERSISTENT); } +ZEND_API bool zend_array_alloc_size_exceeds_memory(size_t nelems, bool packed) +{ + size_t elem_size = packed ? sizeof(zval) : sizeof(Bucket) + sizeof(uint32_t); + return zend_alloc_size_exceeds_memory(nelems, elem_size, 0, "array"); +} + ZEND_API void ZEND_FASTCALL zend_hash_extend(HashTable *ht, uint32_t nSize, bool packed) { HT_ASSERT_RC1(ht); diff --git a/Zend/zend_hash.h b/Zend/zend_hash.h index 1181bee29fae..887aef82998e 100644 --- a/Zend/zend_hash.h +++ b/Zend/zend_hash.h @@ -119,6 +119,11 @@ ZEND_API void ZEND_FASTCALL zend_hash_extend(HashTable *ht, uint32_t nSize, bool ZEND_API void ZEND_FASTCALL zend_hash_discard(HashTable *ht, uint32_t nNumUsed); ZEND_API void ZEND_FASTCALL zend_hash_packed_grow(HashTable *ht); +/* Throws a catchable MemoryError and returns true when a table of "nelems" + * elements cannot fit in the configured memory limit; returns false (without + * throwing) otherwise. */ +ZEND_API bool zend_array_alloc_size_exceeds_memory(size_t nelems, bool packed); + /* additions/updates/changes */ ZEND_API zval* ZEND_FASTCALL zend_hash_add_or_update(HashTable *ht, zend_string *key, zval *pData, uint32_t flag); ZEND_API zval* ZEND_FASTCALL zend_hash_update(HashTable *ht, zend_string *key,zval *pData); diff --git a/Zend/zend_string.c b/Zend/zend_string.c index 4987a02d532c..2d43e0b45ea4 100644 --- a/Zend/zend_string.c +++ b/Zend/zend_string.c @@ -18,7 +18,6 @@ #include "zend.h" #include "zend_globals.h" #include "zend_multiply.h" -#include "zend_exceptions.h" #ifdef HAVE_VALGRIND # include "valgrind/callgrind.h" @@ -531,24 +530,7 @@ ZEND_API zend_string *zend_string_concat3( ZEND_API bool zend_string_alloc_size_exceeds_memory(size_t nmemb, size_t size, size_t offset) { - bool overflow; - size_t needed = zend_safe_address(nmemb, size, offset, &overflow); - - if (!overflow) { - if (zend_memory_limit_is_unlimited()) { - return false; - } - size_t limit = zend_memory_limit(); - size_t usage = zend_memory_usage(false); - size_t available = (limit > usage) ? (limit - usage) : 0; - if (needed <= available) { - return false; - } - } - - zend_throw_error(zend_ce_memory_error, - "The resulting string is too large to fit in the configured memory limit"); - return true; + return zend_alloc_size_exceeds_memory(nmemb, size, offset, "string"); } /* strlcpy and strlcat are not always intercepted by msan, so we need to do it From 78b68b53331289fa8977169a7d3a761e208e8d20 Mon Sep 17 00:00:00 2001 From: Ben Ramsey Date: Sun, 5 Jul 2026 15:13:57 -0500 Subject: [PATCH 16/27] Throw MemoryError in array_fill(), array_pad(), range(), str_split() --- ext/standard/array.c | 16 +++++++++ ext/standard/string.c | 4 +++ .../tests/array/array_fill_error2.phpt | 11 +++--- .../tests/array/array_fill_memory_error.phpt | 34 ++++++++++++++++++ .../tests/array/array_pad_memory_error.phpt | 36 +++++++++++++++++++ .../tests/array/range_memory_error.phpt | 34 ++++++++++++++++++ .../tests/strings/str_split_memory_error.phpt | 27 ++++++++++++++ 7 files changed, 158 insertions(+), 4 deletions(-) create mode 100644 ext/standard/tests/array/array_fill_memory_error.phpt create mode 100644 ext/standard/tests/array/array_pad_memory_error.phpt create mode 100644 ext/standard/tests/array/range_memory_error.phpt create mode 100644 ext/standard/tests/strings/str_split_memory_error.phpt diff --git a/ext/standard/array.c b/ext/standard/array.c index a233e6c4dcb5..261fe108a859 100644 --- a/ext/standard/array.c +++ b/ext/standard/array.c @@ -2658,6 +2658,10 @@ PHP_FUNCTION(array_fill) /* create packed array */ zval *zv; + if (UNEXPECTED(zend_array_alloc_size_exceeds_memory((size_t)(start_key + num), true))) { + RETURN_THROWS(); + } + array_init_size(return_value, (uint32_t)(start_key + num)); zend_hash_real_init_packed(Z_ARRVAL_P(return_value)); Z_ARRVAL_P(return_value)->nNumUsed = (uint32_t)(start_key + num); @@ -2680,6 +2684,9 @@ PHP_FUNCTION(array_fill) } } else { /* create hash */ + if (UNEXPECTED(zend_array_alloc_size_exceeds_memory((size_t)num, false))) { + RETURN_THROWS(); + } array_init_size(return_value, (uint32_t)num); zend_hash_real_init_mixed(Z_ARRVAL_P(return_value)); if (Z_REFCOUNTED_P(val)) { @@ -2742,6 +2749,9 @@ PHP_FUNCTION(array_fill_keys) RETURN_THROWS(); \ } \ size = (uint32_t)_php_math_round(__calc_size, 0, PHP_ROUND_HALF_UP); \ + if (UNEXPECTED(zend_array_alloc_size_exceeds_memory(size, true))) { \ + RETURN_THROWS(); \ + } \ array_init_size(return_value, size); \ zend_hash_real_init_packed(Z_ARRVAL_P(return_value)); \ } while (0) @@ -2758,6 +2768,9 @@ PHP_FUNCTION(array_fill_keys) RETURN_THROWS(); \ } \ size = (uint32_t)(__calc_size + 1); \ + if (UNEXPECTED(zend_array_alloc_size_exceeds_memory(size, true))) { \ + RETURN_THROWS(); \ + } \ array_init_size(return_value, size); \ zend_hash_real_init_packed(Z_ARRVAL_P(return_value)); \ } while (0) @@ -4713,6 +4726,9 @@ PHP_FUNCTION(array_pad) } num_pads = pad_size_abs - input_size; + if (UNEXPECTED(zend_array_alloc_size_exceeds_memory((size_t)pad_size_abs, HT_IS_PACKED(Z_ARRVAL_P(input))))) { + RETURN_THROWS(); + } if (Z_REFCOUNTED_P(pad_value)) { GC_ADDREF_EX(Z_COUNTED_P(pad_value), num_pads); } diff --git a/ext/standard/string.c b/ext/standard/string.c index 3014cf4f1d96..3d0d29040a4a 100644 --- a/ext/standard/string.c +++ b/ext/standard/string.c @@ -6154,6 +6154,10 @@ PHP_FUNCTION(str_split) return; } + if (UNEXPECTED(zend_array_alloc_size_exceeds_memory(((ZSTR_LEN(str) - 1) / split_length) + 1, true))) { + RETURN_THROWS(); + } + array_init_size(return_value, (uint32_t)(((ZSTR_LEN(str) - 1) / split_length) + 1)); zend_hash_real_init_packed(Z_ARRVAL_P(return_value)); diff --git a/ext/standard/tests/array/array_fill_error2.phpt b/ext/standard/tests/array/array_fill_error2.phpt index 1f8b841c8421..01a1e2f86aff 100644 --- a/ext/standard/tests/array/array_fill_error2.phpt +++ b/ext/standard/tests/array/array_fill_error2.phpt @@ -14,10 +14,13 @@ try { } // calling array_fill() with 'count' equals to INT_MAX -$array = array_fill(0, $intMax, 1); +try { + $array = array_fill(0, $intMax, 1); +} catch (\MemoryError $e) { + echo $e->getMessage() . "\n"; +} ?> ---EXPECTF-- +--EXPECT-- array_fill(): Argument #2 ($count) is too large - -Fatal error: Possible integer overflow in memory allocation (%d * %d + %d) in %s on line %d +The resulting array is too large to fit in the configured memory limit diff --git a/ext/standard/tests/array/array_fill_memory_error.phpt b/ext/standard/tests/array/array_fill_memory_error.phpt new file mode 100644 index 000000000000..78e9d12480cc --- /dev/null +++ b/ext/standard/tests/array/array_fill_memory_error.phpt @@ -0,0 +1,34 @@ +--TEST-- +array_fill() throws a catchable MemoryError when the resulting array cannot fit in the memory limit +--INI-- +memory_limit=64M +opcache.enable_cli=0 +--FILE-- +getMessage() . "\n"; +} + +try { + array_fill(-5, 50000000, 'x'); +} catch (MemoryError $e) { + echo 'hash: ' . $e::class . ': ' . $e->getMessage() . "\n"; +} + +var_dump(array_fill(0, 3, 'x')); + +?> +--EXPECT-- +packed: MemoryError: The resulting array is too large to fit in the configured memory limit +hash: MemoryError: The resulting array is too large to fit in the configured memory limit +array(3) { + [0]=> + string(1) "x" + [1]=> + string(1) "x" + [2]=> + string(1) "x" +} diff --git a/ext/standard/tests/array/array_pad_memory_error.phpt b/ext/standard/tests/array/array_pad_memory_error.phpt new file mode 100644 index 000000000000..18b1e580406c --- /dev/null +++ b/ext/standard/tests/array/array_pad_memory_error.phpt @@ -0,0 +1,36 @@ +--TEST-- +array_pad() throws a catchable MemoryError when the resulting array cannot fit in the memory limit +--INI-- +memory_limit=64M +opcache.enable_cli=0 +--FILE-- +getMessage() . "\n"; +} + +try { + array_pad([1, 2], -50000000, 0); +} catch (MemoryError $e) { + echo 'negative: ' . $e::class . ': ' . $e->getMessage() . "\n"; +} + +var_dump(array_pad([1, 2], 4, 0)); + +?> +--EXPECT-- +MemoryError: The resulting array is too large to fit in the configured memory limit +negative: MemoryError: The resulting array is too large to fit in the configured memory limit +array(4) { + [0]=> + int(1) + [1]=> + int(2) + [2]=> + int(0) + [3]=> + int(0) +} diff --git a/ext/standard/tests/array/range_memory_error.phpt b/ext/standard/tests/array/range_memory_error.phpt new file mode 100644 index 000000000000..2c5579c62f49 --- /dev/null +++ b/ext/standard/tests/array/range_memory_error.phpt @@ -0,0 +1,34 @@ +--TEST-- +range() throws a catchable MemoryError when the resulting array cannot fit in the memory limit +--INI-- +memory_limit=64M +opcache.enable_cli=0 +--FILE-- +getMessage() . "\n"; +} + +try { + range(0.5, 50000000.5); +} catch (MemoryError $e) { + echo 'float: ' . $e::class . ': ' . $e->getMessage() . "\n"; +} + +var_dump(range(1, 3)); + +?> +--EXPECT-- +int: MemoryError: The resulting array is too large to fit in the configured memory limit +float: MemoryError: The resulting array is too large to fit in the configured memory limit +array(3) { + [0]=> + int(1) + [1]=> + int(2) + [2]=> + int(3) +} diff --git a/ext/standard/tests/strings/str_split_memory_error.phpt b/ext/standard/tests/strings/str_split_memory_error.phpt new file mode 100644 index 000000000000..72861e926f79 --- /dev/null +++ b/ext/standard/tests/strings/str_split_memory_error.phpt @@ -0,0 +1,27 @@ +--TEST-- +str_split() throws a catchable MemoryError when the resulting array cannot fit in the memory limit +--INI-- +memory_limit=64M +opcache.enable_cli=0 +--FILE-- +getMessage() . "\n"; +} + +var_dump(str_split('abcd', 2)); + +?> +--EXPECT-- +MemoryError: The resulting array is too large to fit in the configured memory limit +array(2) { + [0]=> + string(2) "ab" + [1]=> + string(2) "cd" +} From c3a23cfa271716d8c10197c20fd8be4116333c70 Mon Sep 17 00:00:00 2001 From: Ben Ramsey Date: Sun, 5 Jul 2026 15:13:58 -0500 Subject: [PATCH 17/27] Throw MemoryError for an oversized SplFixedArray --- ext/spl/spl_fixedarray.c | 8 +++++ ext/spl/tests/SplFixedArray_memory_error.phpt | 30 +++++++++++++++++++ ext/spl/tests/bug67247.phpt | 12 +++++--- 3 files changed, 46 insertions(+), 4 deletions(-) create mode 100644 ext/spl/tests/SplFixedArray_memory_error.phpt diff --git a/ext/spl/spl_fixedarray.c b/ext/spl/spl_fixedarray.c index d2eae52e3c0c..e15f01f3ab9e 100644 --- a/ext/spl/spl_fixedarray.c +++ b/ext/spl/spl_fixedarray.c @@ -101,6 +101,10 @@ static void spl_fixedarray_init_non_empty_struct(spl_fixedarray *array, zend_lon static void spl_fixedarray_init(spl_fixedarray *array, zend_long size) { if (size > 0) { + if (UNEXPECTED(zend_alloc_size_exceeds_memory(size, sizeof(zval), 0, "SplFixedArray"))) { + spl_fixedarray_default_ctor(array); + return; + } spl_fixedarray_init_non_empty_struct(array, size); spl_fixedarray_init_elems(array, 0, size); } else { @@ -179,6 +183,10 @@ static void spl_fixedarray_resize(spl_fixedarray *array, zend_long size) return; } + if (size > array->size && UNEXPECTED(zend_alloc_size_exceeds_memory(size, sizeof(zval), 0, "SplFixedArray"))) { + return; + } + array->cached_resize = size; /* clearing the array */ diff --git a/ext/spl/tests/SplFixedArray_memory_error.phpt b/ext/spl/tests/SplFixedArray_memory_error.phpt new file mode 100644 index 000000000000..2bc2dd3ae1db --- /dev/null +++ b/ext/spl/tests/SplFixedArray_memory_error.phpt @@ -0,0 +1,30 @@ +--TEST-- +SplFixedArray throws a catchable MemoryError when the requested size cannot fit in the memory limit +--INI-- +memory_limit=64M +opcache.enable_cli=0 +--FILE-- +getMessage() . "\n"; +} + +$a = new SplFixedArray(2); +$a[0] = 'kept'; +try { + $a->setSize(2000000000); +} catch (MemoryError $e) { + echo 'setSize: ' . $e::class . ': ' . $e->getMessage() . "\n"; +} +var_dump($a->getSize()); +var_dump($a[0]); + +?> +--EXPECT-- +construct: MemoryError: The resulting SplFixedArray is too large to fit in the configured memory limit +setSize: MemoryError: The resulting SplFixedArray is too large to fit in the configured memory limit +int(2) +string(4) "kept" diff --git a/ext/spl/tests/bug67247.phpt b/ext/spl/tests/bug67247.phpt index cb71445d7b79..2c53a9cb75dd 100644 --- a/ext/spl/tests/bug67247.phpt +++ b/ext/spl/tests/bug67247.phpt @@ -4,10 +4,14 @@ Bug #67247 (spl_fixedarray_resize integer overflow) getSize()."\n"; -$ar->setSize((PHP_INT_SIZE==8)?0x2000000000000001:0x40000001); +try { + $ar->setSize((PHP_INT_SIZE==8)?0x2000000000000001:0x40000001); +} catch (MemoryError $e) { + echo $e->getMessage() . "\n"; +} echo "size: ".$ar->getSize()."\n"; ?> ---EXPECTF-- +--EXPECT-- +size: 1 +The resulting SplFixedArray is too large to fit in the configured memory limit size: 1 - -Fatal error: Possible integer overflow in memory allocation (%d * %d + 0) in %s on line %d From 137881bb2f0c7de9156daed3f809d81b943855a0 Mon Sep 17 00:00:00 2001 From: Ben Ramsey Date: Sun, 5 Jul 2026 15:13:58 -0500 Subject: [PATCH 18/27] Throw MemoryError in msg_receive() --- ext/sysvmsg/sysvmsg.c | 4 +++ .../tests/msg_receive_memory_error.phpt | 29 +++++++++++++++++++ 2 files changed, 33 insertions(+) create mode 100644 ext/sysvmsg/tests/msg_receive_memory_error.phpt diff --git a/ext/sysvmsg/sysvmsg.c b/ext/sysvmsg/sysvmsg.c index 0c2b3dcf183b..399c72a43564 100644 --- a/ext/sysvmsg/sysvmsg.c +++ b/ext/sysvmsg/sysvmsg.c @@ -287,6 +287,10 @@ PHP_FUNCTION(msg_receive) mq = Z_SYSVMSG_QUEUE_P(queue); + if (UNEXPECTED(zend_string_alloc_size_exceeds_memory(maxsize, 1, sizeof(struct php_msgbuf)))) { + RETURN_THROWS(); + } + messagebuffer = (struct php_msgbuf *) safe_emalloc(maxsize, 1, sizeof(struct php_msgbuf)); result = msgrcv(mq->id, messagebuffer, maxsize, desiredmsgtype, realflags); diff --git a/ext/sysvmsg/tests/msg_receive_memory_error.phpt b/ext/sysvmsg/tests/msg_receive_memory_error.phpt new file mode 100644 index 000000000000..c7ab37999f0f --- /dev/null +++ b/ext/sysvmsg/tests/msg_receive_memory_error.phpt @@ -0,0 +1,29 @@ +--TEST-- +msg_receive() throws a catchable MemoryError when the maximum size cannot fit in the memory limit +--EXTENSIONS-- +sysvmsg +--INI-- +memory_limit=64M +opcache.enable_cli=0 +--FILE-- +getMessage() . "\n"; +} + +msg_send($queue, 1, 'ping'); +var_dump(msg_receive($queue, 0, $msgtype, 1024, $message, true, MSG_IPC_NOWAIT)); +var_dump($message); + +msg_remove_queue($queue); + +?> +--EXPECT-- +MemoryError: The resulting string is too large to fit in the configured memory limit +bool(true) +string(4) "ping" From f141a6eaf54ce4e120590395def986626e6c34ba Mon Sep 17 00:00:00 2001 From: Ben Ramsey Date: Sun, 5 Jul 2026 15:13:58 -0500 Subject: [PATCH 19/27] Throw MemoryError in openssl_random_pseudo_bytes() --- ext/openssl/openssl_backend_common.c | 3 +++ ...nssl_random_pseudo_bytes_memory_error.phpt | 22 +++++++++++++++++++ 2 files changed, 25 insertions(+) create mode 100644 ext/openssl/tests/openssl_random_pseudo_bytes_memory_error.phpt diff --git a/ext/openssl/openssl_backend_common.c b/ext/openssl/openssl_backend_common.c index 7261a617af32..522292b615bc 100644 --- a/ext/openssl/openssl_backend_common.c +++ b/ext/openssl/openssl_backend_common.c @@ -2115,6 +2115,9 @@ PHP_OPENSSL_API zend_string* php_openssl_random_pseudo_bytes(zend_long buffer_le zend_argument_value_error(1, "must be less than or equal to %d", INT_MAX); return NULL; } + if (UNEXPECTED(zend_string_alloc_size_exceeds_memory(buffer_length, 1, 0))) { + return NULL; + } buffer = zend_string_alloc(buffer_length, 0); PHP_OPENSSL_CHECK_LONG_TO_INT_NULL_RETURN(buffer_length, length); diff --git a/ext/openssl/tests/openssl_random_pseudo_bytes_memory_error.phpt b/ext/openssl/tests/openssl_random_pseudo_bytes_memory_error.phpt new file mode 100644 index 000000000000..29e05b2553d4 --- /dev/null +++ b/ext/openssl/tests/openssl_random_pseudo_bytes_memory_error.phpt @@ -0,0 +1,22 @@ +--TEST-- +openssl_random_pseudo_bytes() throws a catchable MemoryError when the length cannot fit in the memory limit +--EXTENSIONS-- +openssl +--INI-- +memory_limit=64M +opcache.enable_cli=0 +--FILE-- +getMessage() . "\n"; +} + +var_dump(strlen(openssl_random_pseudo_bytes(16))); + +?> +--EXPECT-- +MemoryError: The resulting string is too large to fit in the configured memory limit +int(16) From 7510d788d4ea7a8f2f2a2950b6cf5a5b60e54ba7 Mon Sep 17 00:00:00 2001 From: Ben Ramsey Date: Sun, 5 Jul 2026 15:13:58 -0500 Subject: [PATCH 20/27] Throw MemoryError in sodium length-taking functions --- ext/sodium/libsodium.c | 9 ++++ ext/sodium/tests/sodium_memory_error.phpt | 50 +++++++++++++++++++++++ 2 files changed, 59 insertions(+) create mode 100644 ext/sodium/tests/sodium_memory_error.phpt diff --git a/ext/sodium/libsodium.c b/ext/sodium/libsodium.c index 8c85991150b5..bc998656bef2 100644 --- a/ext/sodium/libsodium.c +++ b/ext/sodium/libsodium.c @@ -1230,6 +1230,9 @@ PHP_FUNCTION(sodium_crypto_stream) zend_argument_error(sodium_exception_ce, 3, "must be SODIUM_CRYPTO_STREAM_KEYBYTES bytes long"); RETURN_THROWS(); } + if (UNEXPECTED(zend_string_alloc_size_exceeds_memory((size_t) ciphertext_len, 1, 0))) { + RETURN_THROWS(); + } ciphertext = zend_string_alloc((size_t) ciphertext_len, 0); if (crypto_stream((unsigned char *) ZSTR_VAL(ciphertext), (unsigned long long) ciphertext_len, nonce, key) != 0) { @@ -1310,6 +1313,9 @@ PHP_FUNCTION(sodium_crypto_stream_xchacha20) zend_argument_error(sodium_exception_ce, 3, "must be SODIUM_CRYPTO_STREAM_XCHACHA20_KEYBYTES bytes long"); RETURN_THROWS(); } + if (UNEXPECTED(zend_string_alloc_size_exceeds_memory((size_t) ciphertext_len, 1, 0))) { + RETURN_THROWS(); + } ciphertext = zend_string_checked_alloc((size_t) ciphertext_len, 0); if (crypto_stream_xchacha20((unsigned char *) ZSTR_VAL(ciphertext), (unsigned long long) ciphertext_len, nonce, key) != 0) { @@ -1438,6 +1444,9 @@ PHP_FUNCTION(sodium_crypto_pwhash) sodium_remove_param_values_from_backtrace(EG(exception)); RETURN_THROWS(); } + if (UNEXPECTED(zend_string_alloc_size_exceeds_memory((size_t) hash_len, 1, 0))) { + RETURN_THROWS(); + } if (passwd_len >= 0xffffffff) { zend_argument_value_error(2, "must be less than 4294967295 bytes"); sodium_remove_param_values_from_backtrace(EG(exception)); diff --git a/ext/sodium/tests/sodium_memory_error.phpt b/ext/sodium/tests/sodium_memory_error.phpt new file mode 100644 index 000000000000..405f91b0b2f1 --- /dev/null +++ b/ext/sodium/tests/sodium_memory_error.phpt @@ -0,0 +1,50 @@ +--TEST-- +Sodium length-taking functions throw a catchable MemoryError for a length too large for the memory limit +--EXTENSIONS-- +sodium +--INI-- +memory_limit=64M +opcache.enable_cli=0 +--FILE-- +getMessage() . "\n"; +} + +$xnonce = str_repeat("\0", SODIUM_CRYPTO_STREAM_XCHACHA20_NONCEBYTES); +$xkey = str_repeat("\0", SODIUM_CRYPTO_STREAM_XCHACHA20_KEYBYTES); + +try { + sodium_crypto_stream_xchacha20(2147483000, $xnonce, $xkey); +} catch (MemoryError $e) { + echo 'stream_xchacha20: ' . $e::class . ': ' . $e->getMessage() . "\n"; +} + +$salt = str_repeat("\0", SODIUM_CRYPTO_PWHASH_SALTBYTES); + +try { + sodium_crypto_pwhash( + 2147483000, + 'password', + $salt, + SODIUM_CRYPTO_PWHASH_OPSLIMIT_INTERACTIVE, + SODIUM_CRYPTO_PWHASH_MEMLIMIT_INTERACTIVE, + ); +} catch (MemoryError $e) { + echo 'pwhash: ' . $e::class . ': ' . $e->getMessage() . "\n"; +} + +var_dump(strlen(sodium_crypto_stream(32, $nonce, $key))); + +?> +--EXPECT-- +stream: MemoryError: The resulting string is too large to fit in the configured memory limit +stream_xchacha20: MemoryError: The resulting string is too large to fit in the configured memory limit +pwhash: MemoryError: The resulting string is too large to fit in the configured memory limit +int(32) From cbfdbfe5c663bff84e141e01708d287d6c0e9bf7 Mon Sep 17 00:00:00 2001 From: Ben Ramsey Date: Sun, 5 Jul 2026 17:06:00 -0500 Subject: [PATCH 21/27] Throw MemoryError in Randomizer::getBytes() and getBytesFromString() --- ext/random/randomizer.c | 6 ++++ .../randomizer_memory_error.phpt | 31 +++++++++++++++++++ 2 files changed, 37 insertions(+) create mode 100644 ext/random/tests/03_randomizer/randomizer_memory_error.phpt diff --git a/ext/random/randomizer.c b/ext/random/randomizer.c index 0738380ca253..5b2234d66089 100644 --- a/ext/random/randomizer.c +++ b/ext/random/randomizer.c @@ -272,6 +272,9 @@ PHP_METHOD(Random_Randomizer, getBytes) } size_t length = (size_t)user_length; + if (UNEXPECTED(zend_string_alloc_size_exceeds_memory(length, 1, 0))) { + RETURN_THROWS(); + } retval = zend_string_alloc(length, 0); php_random_result result; @@ -425,6 +428,9 @@ PHP_METHOD(Random_Randomizer, getBytesFromString) } size_t length = (size_t)user_length; + if (UNEXPECTED(zend_string_alloc_size_exceeds_memory(length, 1, 0))) { + RETURN_THROWS(); + } retval = zend_string_alloc(length, 0); if (max_offset > 0xff) { diff --git a/ext/random/tests/03_randomizer/randomizer_memory_error.phpt b/ext/random/tests/03_randomizer/randomizer_memory_error.phpt new file mode 100644 index 000000000000..2afe97d23fb7 --- /dev/null +++ b/ext/random/tests/03_randomizer/randomizer_memory_error.phpt @@ -0,0 +1,31 @@ +--TEST-- +Random\Randomizer::getBytes() and getBytesFromString() throw a catchable MemoryError +--INI-- +memory_limit=64M +opcache.enable_cli=0 +--FILE-- +getBytes(PHP_INT_MAX); +} catch (MemoryError $e) { + echo 'getBytes: ' . $e::class . ': ' . $e->getMessage() . "\n"; +} + +try { + $randomizer->getBytesFromString('abc', PHP_INT_MAX); +} catch (MemoryError $e) { + echo 'getBytesFromString: ' . $e::class . ': ' . $e->getMessage() . "\n"; +} + +var_dump(strlen($randomizer->getBytes(16))); +var_dump(strlen($randomizer->getBytesFromString('abc', 16))); + +?> +--EXPECT-- +getBytes: MemoryError: The resulting string is too large to fit in the configured memory limit +getBytesFromString: MemoryError: The resulting string is too large to fit in the configured memory limit +int(16) +int(16) From a88ecfd91a4a073cc8ab386f77aedcb9b6f65cc3 Mon Sep 17 00:00:00 2001 From: Ben Ramsey Date: Sun, 5 Jul 2026 17:06:02 -0500 Subject: [PATCH 22/27] Throw MemoryError in mb_str_split() --- ext/mbstring/mbstring.c | 9 +++++ .../tests/mb_str_split_memory_error.phpt | 36 +++++++++++++++++++ 2 files changed, 45 insertions(+) create mode 100644 ext/mbstring/tests/mb_str_split_memory_error.phpt diff --git a/ext/mbstring/mbstring.c b/ext/mbstring/mbstring.c index bb6879ef219d..8f91b78fb396 100644 --- a/ext/mbstring/mbstring.c +++ b/ext/mbstring/mbstring.c @@ -1679,6 +1679,9 @@ PHP_FUNCTION(mb_str_split) if (char_len) { unsigned int chunk_len = char_len * split_len; unsigned int chunks = ((ZSTR_LEN(str) / chunk_len) + split_len - 1) / split_len; /* round up */ + if (UNEXPECTED(zend_array_alloc_size_exceeds_memory(chunks, true))) { + RETURN_THROWS(); + } array_init_size(return_value, chunks); while (p < e) { add_next_index_stringl(return_value, (const char*)p, MIN(chunk_len, e - p)); @@ -1688,6 +1691,9 @@ PHP_FUNCTION(mb_str_split) unsigned char const *mbtab = enc->mblen_table; /* Assume that we have 1-byte characters */ + if (UNEXPECTED(zend_array_alloc_size_exceeds_memory((ZSTR_LEN(str) + split_len - 1) / split_len, true))) { + RETURN_THROWS(); + } array_init_size(return_value, (ZSTR_LEN(str) + split_len - 1) / split_len); while (p < e) { @@ -1703,6 +1709,9 @@ PHP_FUNCTION(mb_str_split) } } else { /* Assume that we have 1-byte characters */ + if (UNEXPECTED(zend_array_alloc_size_exceeds_memory((ZSTR_LEN(str) + split_len - 1) / split_len, true))) { + RETURN_THROWS(); + } array_init_size(return_value, (ZSTR_LEN(str) + split_len - 1) / split_len); uint32_t wchar_buf[128]; diff --git a/ext/mbstring/tests/mb_str_split_memory_error.phpt b/ext/mbstring/tests/mb_str_split_memory_error.phpt new file mode 100644 index 000000000000..f644cdf32070 --- /dev/null +++ b/ext/mbstring/tests/mb_str_split_memory_error.phpt @@ -0,0 +1,36 @@ +--TEST-- +mb_str_split() throws a catchable MemoryError when the resulting array cannot fit in the memory limit +--EXTENSIONS-- +mbstring +--INI-- +memory_limit=64M +opcache.enable_cli=0 +--FILE-- +getMessage() . "\n"; +} + +try { + mb_str_split($input, 1, '8bit'); +} catch (MemoryError $e) { + echo '8bit: ' . $e::class . ': ' . $e->getMessage() . "\n"; +} + +var_dump(mb_str_split('abcd', 2)); + +?> +--EXPECT-- +UTF-8: MemoryError: The resulting array is too large to fit in the configured memory limit +8bit: MemoryError: The resulting array is too large to fit in the configured memory limit +array(2) { + [0]=> + string(2) "ab" + [1]=> + string(2) "cd" +} From 51b138a9eb7ff50cbc9a83678605e3e094019304 Mon Sep 17 00:00:00 2001 From: Ben Ramsey Date: Sun, 5 Jul 2026 17:06:02 -0500 Subject: [PATCH 23/27] Throw MemoryError in fgetcsv() --- ext/standard/file.c | 4 +++ .../tests/file/fgetcsv_memory_error.phpt | 32 +++++++++++++++++++ 2 files changed, 36 insertions(+) create mode 100644 ext/standard/tests/file/fgetcsv_memory_error.phpt diff --git a/ext/standard/file.c b/ext/standard/file.c index 1612bb6fd806..f3dac46c23e4 100644 --- a/ext/standard/file.c +++ b/ext/standard/file.c @@ -1889,6 +1889,10 @@ PHP_FUNCTION(fgetcsv) RETURN_FALSE; } } else { + if (UNEXPECTED(zend_string_alloc_size_exceeds_memory(len, 1, 1))) { + php_stream_error_operation_end_for_stream(stream); + RETURN_THROWS(); + } buf = emalloc(len + 1); if (php_stream_get_line(stream, buf, len + 1, &buf_len) == NULL) { efree(buf); diff --git a/ext/standard/tests/file/fgetcsv_memory_error.phpt b/ext/standard/tests/file/fgetcsv_memory_error.phpt new file mode 100644 index 000000000000..83a8061ba9d7 --- /dev/null +++ b/ext/standard/tests/file/fgetcsv_memory_error.phpt @@ -0,0 +1,32 @@ +--TEST-- +fgetcsv() throws a catchable MemoryError when the requested length cannot fit in the memory limit +--INI-- +memory_limit=64M +opcache.enable_cli=0 +--FILE-- +getMessage() . "\n"; +} + +var_dump(fgetcsv($fp, 1024, ',', '"', '')); +fclose($fp); + +?> +--EXPECT-- +MemoryError: The resulting string is too large to fit in the configured memory limit +array(3) { + [0]=> + string(1) "a" + [1]=> + string(1) "b" + [2]=> + string(1) "c" +} From 22023f826ac8c9e3ce02d36fba1015d04d1932f4 Mon Sep 17 00:00:00 2001 From: Ben Ramsey Date: Sun, 5 Jul 2026 17:06:02 -0500 Subject: [PATCH 24/27] Throw MemoryError in shmop_read() --- ext/shmop/shmop.c | 4 +++ ext/shmop/tests/shmop_read_memory_error.phpt | 26 ++++++++++++++++++++ 2 files changed, 30 insertions(+) create mode 100644 ext/shmop/tests/shmop_read_memory_error.phpt diff --git a/ext/shmop/shmop.c b/ext/shmop/shmop.c index 7903a294a037..d5c90031df7c 100644 --- a/ext/shmop/shmop.c +++ b/ext/shmop/shmop.c @@ -245,6 +245,10 @@ PHP_FUNCTION(shmop_read) startaddr = shmop->addr + start; zend_long bytes = count ? count : shmop->size - start; + if (UNEXPECTED(zend_string_alloc_size_exceeds_memory(bytes, 1, 0))) { + RETURN_THROWS(); + } + return_string = zend_string_init(startaddr, bytes, 0); RETURN_NEW_STR(return_string); diff --git a/ext/shmop/tests/shmop_read_memory_error.phpt b/ext/shmop/tests/shmop_read_memory_error.phpt new file mode 100644 index 000000000000..de9135a0f1e0 --- /dev/null +++ b/ext/shmop/tests/shmop_read_memory_error.phpt @@ -0,0 +1,26 @@ +--TEST-- +shmop_read() throws a catchable MemoryError when the read size cannot fit in the memory limit +--EXTENSIONS-- +shmop +--INI-- +memory_limit=32M +opcache.enable_cli=0 +--FILE-- +getMessage() . "\n"; +} + +var_dump(strlen(shmop_read($shm, 0, 4))); + +shmop_delete($shm); + +?> +--EXPECT-- +MemoryError: The resulting string is too large to fit in the configured memory limit +int(4) From 9770294c4c41d6ae41c648b4b3ef5f171bab16e4 Mon Sep 17 00:00:00 2001 From: Ben Ramsey Date: Sun, 5 Jul 2026 17:06:02 -0500 Subject: [PATCH 25/27] Throw MemoryError in pg_lo_read() --- ext/pgsql/pgsql.c | 4 ++ ext/pgsql/tests/pg_lo_read_memory_error.phpt | 40 ++++++++++++++++++++ 2 files changed, 44 insertions(+) create mode 100644 ext/pgsql/tests/pg_lo_read_memory_error.phpt diff --git a/ext/pgsql/pgsql.c b/ext/pgsql/pgsql.c index 0ea766c12b42..b096162a9126 100644 --- a/ext/pgsql/pgsql.c +++ b/ext/pgsql/pgsql.c @@ -2811,6 +2811,10 @@ PHP_FUNCTION(pg_lo_read) RETURN_THROWS(); } + if (UNEXPECTED(zend_string_alloc_size_exceeds_memory(buffer_length, 1, 0))) { + RETURN_THROWS(); + } + buf = zend_string_alloc(buffer_length, 0); if ((nbytes = lo_read((PGconn *)pgsql->conn, pgsql->lofd, ZSTR_VAL(buf), ZSTR_LEN(buf)))<0) { zend_string_efree(buf); diff --git a/ext/pgsql/tests/pg_lo_read_memory_error.phpt b/ext/pgsql/tests/pg_lo_read_memory_error.phpt new file mode 100644 index 000000000000..a2ca193534b6 --- /dev/null +++ b/ext/pgsql/tests/pg_lo_read_memory_error.phpt @@ -0,0 +1,40 @@ +--TEST-- +pg_lo_read() throws a catchable MemoryError when the buffer length cannot fit in the memory limit +--EXTENSIONS-- +pgsql +--SKIPIF-- + +--INI-- +memory_limit=64M +opcache.enable_cli=0 +--FILE-- +getMessage() . "\n"; +} + +var_dump(pg_lo_read($lo, 4)); + +pg_lo_close($lo); +pg_query($db, 'COMMIT'); +pg_lo_unlink($db, $oid); +pg_close($db); + +?> +--EXPECT-- +MemoryError: The resulting string is too large to fit in the configured memory limit +string(4) "ping" From cb720b8a648a7721bd6e9b6aeaa8bd24f1c5ce8a Mon Sep 17 00:00:00 2001 From: Ben Ramsey Date: Sun, 5 Jul 2026 17:06:03 -0500 Subject: [PATCH 26/27] Throw MemoryError in GMP string rendering and gmp_export() --- ext/gmp/gmp.c | 9 +++++++ ext/gmp/tests/gmp_memory_error.phpt | 38 +++++++++++++++++++++++++++++ 2 files changed, 47 insertions(+) create mode 100644 ext/gmp/tests/gmp_memory_error.phpt diff --git a/ext/gmp/gmp.c b/ext/gmp/gmp.c index 59209c4e1be3..55105878f0bc 100644 --- a/ext/gmp/gmp.c +++ b/ext/gmp/gmp.c @@ -672,6 +672,11 @@ static void gmp_strval(zval *result, mpz_t gmpnum, int base) /* {{{ */ num_len++; } + if (UNEXPECTED(zend_string_alloc_size_exceeds_memory(num_len, 1, 0))) { + ZVAL_EMPTY_STRING(result); + return; + } + str = zend_string_alloc(num_len, 0); mpz_get_str(ZSTR_VAL(str), base, gmpnum); @@ -841,6 +846,10 @@ ZEND_FUNCTION(gmp_export) size_t bits_per_word = (size_t) size * 8; size_t count = (size_in_base_2 + bits_per_word - 1) / bits_per_word; + if (UNEXPECTED(zend_string_alloc_size_exceeds_memory(count, size, 0))) { + RETURN_THROWS(); + } + zend_string *out_string = zend_string_safe_alloc(count, size, 0, 0); mpz_export(ZSTR_VAL(out_string), NULL, order, size, endian, 0, gmpnumber); ZSTR_VAL(out_string)[ZSTR_LEN(out_string)] = '\0'; diff --git a/ext/gmp/tests/gmp_memory_error.phpt b/ext/gmp/tests/gmp_memory_error.phpt new file mode 100644 index 000000000000..a859438056cf --- /dev/null +++ b/ext/gmp/tests/gmp_memory_error.phpt @@ -0,0 +1,38 @@ +--TEST-- +GMP string rendering throws a catchable MemoryError when the result cannot fit in the memory limit +--EXTENSIONS-- +gmp +--INI-- +memory_limit=64M +opcache.enable_cli=0 +--FILE-- +getMessage() . "\n"; +} + +try { + (string) $huge; +} catch (MemoryError $e) { + echo 'cast: ' . $e::class . ': ' . $e->getMessage() . "\n"; +} + +try { + gmp_export($huge); +} catch (MemoryError $e) { + echo 'gmp_export: ' . $e::class . ': ' . $e->getMessage() . "\n"; +} + +var_dump(gmp_strval(gmp_init(255))); + +?> +--EXPECT-- +gmp_strval: MemoryError: The resulting string is too large to fit in the configured memory limit +cast: MemoryError: The resulting string is too large to fit in the configured memory limit +gmp_export: MemoryError: The resulting string is too large to fit in the configured memory limit +string(3) "255" From e164219e8d9873d5134e9c74f92f50ba8009671e Mon Sep 17 00:00:00 2001 From: Ben Ramsey Date: Sat, 11 Jul 2026 19:23:17 -0500 Subject: [PATCH 27/27] Update tests for MemoryError on 32-bit and BSD platforms --- Zend/tests/bug55509.phpt | 29 +++++++++-------- ext/shmop/tests/shmop_read_memory_error.phpt | 2 ++ .../tests/socket_read_recv_memory_error.phpt | 4 ++- .../tests/array/array_pad_memory_error.phpt | 6 ++-- .../tests/array/range_memory_error.phpt | 6 ++-- .../stream_socket_recvfrom_memory_error.phpt | 4 ++- .../strings/chunk_split_variation1_32bit.phpt | 10 ++++-- .../strings/chunk_split_variation2_32bit.phpt | 10 ++++-- .../tests/strings/str_pad_variation5.phpt | 10 ++++-- ext/zend_test/tests/gh11078.phpt | 14 ++++++--- ext/zend_test/tests/observer_bug81430_2.phpt | 22 +++++++++++-- ext/zend_test/tests/observer_bug81435.phpt | 14 +++++++-- ext/zend_test/tests/observer_error_01.phpt | 24 ++++++++++++-- .../tests/observer_fiber_functions_03.phpt | 31 +++++++++++++++++-- 14 files changed, 144 insertions(+), 42 deletions(-) diff --git a/Zend/tests/bug55509.phpt b/Zend/tests/bug55509.phpt index bdb7da966a5b..c1fefe900345 100644 --- a/Zend/tests/bug55509.phpt +++ b/Zend/tests/bug55509.phpt @@ -65,21 +65,24 @@ elseif (PHP_OS == 'FreeBSD') { memory_limit=2100M --FILE-- getMessage() . "\n"; +} ?> ---EXPECTF-- +--EXPECT-- 1 2 3 4 - -Fatal error: Allowed memory size of %d bytes exhausted%s(tried to allocate %d bytes) in %sbug55509.php on line %d +MemoryError: The resulting string is too large to fit in the configured memory limit diff --git a/ext/shmop/tests/shmop_read_memory_error.phpt b/ext/shmop/tests/shmop_read_memory_error.phpt index de9135a0f1e0..8a54685a1385 100644 --- a/ext/shmop/tests/shmop_read_memory_error.phpt +++ b/ext/shmop/tests/shmop_read_memory_error.phpt @@ -2,6 +2,8 @@ shmop_read() throws a catchable MemoryError when the read size cannot fit in the memory limit --EXTENSIONS-- shmop +--SKIPIF-- + --INI-- memory_limit=32M opcache.enable_cli=0 diff --git a/ext/sockets/tests/socket_read_recv_memory_error.phpt b/ext/sockets/tests/socket_read_recv_memory_error.phpt index 2e565b7ef704..9bc53c2c8866 100644 --- a/ext/sockets/tests/socket_read_recv_memory_error.phpt +++ b/ext/sockets/tests/socket_read_recv_memory_error.phpt @@ -12,7 +12,9 @@ opcache.enable_cli=0 --FILE-- 4 ? 50_000_000 : 32_500_000; + try { - array_pad([1, 2], 50000000, 0); + array_pad([1, 2], $size, 0); } catch (MemoryError $e) { echo $e::class . ': ' . $e->getMessage() . "\n"; } try { - array_pad([1, 2], -50000000, 0); + array_pad([1, 2], -$size, 0); } catch (MemoryError $e) { echo 'negative: ' . $e::class . ': ' . $e->getMessage() . "\n"; } diff --git a/ext/standard/tests/array/range_memory_error.phpt b/ext/standard/tests/array/range_memory_error.phpt index 2c5579c62f49..8c898c1459a9 100644 --- a/ext/standard/tests/array/range_memory_error.phpt +++ b/ext/standard/tests/array/range_memory_error.phpt @@ -6,14 +6,16 @@ opcache.enable_cli=0 --FILE-- 4 ? 50_000_000 : 32_500_000; + try { - range(0, 50000000); + range(0, $size); } catch (MemoryError $e) { echo 'int: ' . $e::class . ': ' . $e->getMessage() . "\n"; } try { - range(0.5, 50000000.5); + range(0.5, $size + 0.5); } catch (MemoryError $e) { echo 'float: ' . $e::class . ': ' . $e->getMessage() . "\n"; } diff --git a/ext/standard/tests/streams/stream_socket_recvfrom_memory_error.phpt b/ext/standard/tests/streams/stream_socket_recvfrom_memory_error.phpt index f41fc563865f..8106a96c03c2 100644 --- a/ext/standard/tests/streams/stream_socket_recvfrom_memory_error.phpt +++ b/ext/standard/tests/streams/stream_socket_recvfrom_memory_error.phpt @@ -10,7 +10,9 @@ opcache.enable_cli=0 --FILE-- getMessage() . "\n"; +} ?> --EXPECTF-- *** Testing chunk_split() : unexpected large 'end' string argument variation 1 *** - -Fatal error: %rAllowed memory size of %d bytes exhausted%s\(tried to allocate %d bytes\)|Possible integer overflow in memory allocation \(4294901777 \+ %d\)%r in %s on line %d +MemoryError: The resulting string is too large to fit in the configured memory limit diff --git a/ext/standard/tests/strings/chunk_split_variation2_32bit.phpt b/ext/standard/tests/strings/chunk_split_variation2_32bit.phpt index c83a37f00edb..22826710c4f0 100644 --- a/ext/standard/tests/strings/chunk_split_variation2_32bit.phpt +++ b/ext/standard/tests/strings/chunk_split_variation2_32bit.phpt @@ -11,9 +11,13 @@ echo "*** Testing chunk_split() : unexpected large 'end' string argument variati $a=str_repeat("B", 65537); $b=1; $c=str_repeat("B", 65537); -var_dump(chunk_split($a,$b,$c)); + +try { + var_dump(chunk_split($a,$b,$c)); +} catch (Throwable $t) { + echo $t::class . ': ' . $t->getMessage() . "\n"; +} ?> --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 +MemoryError: The resulting string is too large to fit in the configured memory limit diff --git a/ext/standard/tests/strings/str_pad_variation5.phpt b/ext/standard/tests/strings/str_pad_variation5.phpt index 1a65366c823c..e9a3ddedfc40 100644 --- a/ext/standard/tests/strings/str_pad_variation5.phpt +++ b/ext/standard/tests/strings/str_pad_variation5.phpt @@ -20,10 +20,14 @@ echo "*** Testing str_pad() function: with large value for for 'pad_length' argu //defining '$input' argument $input = "Test string"; $pad_length = PHP_INT_MAX - 16; /* zend_string header is 16 bytes */ -var_dump( str_pad($input, $pad_length) ); + +try { + var_dump( str_pad($input, $pad_length) ); +} catch (Throwable $t) { + echo $t::class . ': ' . $t->getMessage() . "\n"; +} ?> --EXPECTF-- *** Testing str_pad() function: with large value for for 'pad_length' argument *** - -Fatal error: Allowed memory size of %d bytes exhausted%s(tried to allocate %d bytes) in %s on line %d +MemoryError: The resulting string is too large to fit in the configured memory limit diff --git a/ext/zend_test/tests/gh11078.phpt b/ext/zend_test/tests/gh11078.phpt index 2aed49480b02..740734abedd4 100644 --- a/ext/zend_test/tests/gh11078.phpt +++ b/ext/zend_test/tests/gh11078.phpt @@ -26,10 +26,14 @@ class CrashingFifo { } } -stream_register_wrapper('fifo', CrashingFifo::class); -$readStream = fopen('fifo://1', 'r'); -zend_test_cast_fread($readStream); +try { + stream_register_wrapper('fifo', CrashingFifo::class); + $readStream = fopen('fifo://1', 'r'); + zend_test_cast_fread($readStream); +} catch (Throwable $t) { + echo $t::class . ': ' . $t->getMessage() . "\n"; +} ?> ---EXPECTF-- -Fatal error: Allowed memory size of %d bytes exhausted %s +--EXPECT-- +MemoryError: The resulting string is too large to fit in the configured memory limit diff --git a/ext/zend_test/tests/observer_bug81430_2.phpt b/ext/zend_test/tests/observer_bug81430_2.phpt index 11a9f061b3d4..f2d9e7ee0298 100644 --- a/ext/zend_test/tests/observer_bug81430_2.phpt +++ b/ext/zend_test/tests/observer_bug81430_2.phpt @@ -44,10 +44,28 @@ call_user_func([$r->getAttributes(A::class)[0], 'newInstance']); - -Fatal error: Allowed memory size of %d bytes exhausted %s in %s on line %d + + + + + + + + + + + + +Fatal error: Uncaught MemoryError: The resulting string is too large to fit in the configured memory limit in %s:%d +Stack trace: +#0 [internal function]: str_repeat('\xFF', 100000000) +#1 %s(%d): array_map('str_repeat', Array, Array) +#2 %s(%d): A->__construct() +#3 %s(%d): ReflectionAttribute->newInstance() +#4 {main} + thrown in %s on line %d diff --git a/ext/zend_test/tests/observer_bug81435.phpt b/ext/zend_test/tests/observer_bug81435.phpt index da9bdd01db12..f9045cad0a09 100644 --- a/ext/zend_test/tests/observer_bug81435.phpt +++ b/ext/zend_test/tests/observer_bug81435.phpt @@ -49,6 +49,16 @@ a(); - -Fatal error: Allowed memory size of 20971520 bytes exhausted %s in %s on line %d + + + + +Fatal error: Uncaught MemoryError: The resulting string is too large to fit in the configured memory limit in %s:%d +Stack trace: +#0 [internal function]: str_repeat('\xFF', 100000000) +#1 %s(%d): array_map('str_repeat', Array, Array) +#2 %s(%d): bailout(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15) +#3 %s(%d): a() +#4 {main} + thrown in %s on line %d diff --git a/ext/zend_test/tests/observer_error_01.phpt b/ext/zend_test/tests/observer_error_01.phpt index d20a23b00b29..080d2b493830 100644 --- a/ext/zend_test/tests/observer_error_01.phpt +++ b/ext/zend_test/tests/observer_error_01.phpt @@ -30,8 +30,28 @@ echo 'You should not see this.'; - -Fatal error: Allowed memory size of 2097152 bytes exhausted%s(tried to allocate %d bytes) in %s on line %d + + + + + + + + + + +Fatal error: Uncaught MemoryError: The resulting string is too large to fit in the configured memory limit in %s:%d +Stack trace: +#0 %s(%d): str_repeat('.', 2097152) +#1 %s(%d): foo() +#2 {main} + thrown in %s on line %d diff --git a/ext/zend_test/tests/observer_fiber_functions_03.phpt b/ext/zend_test/tests/observer_fiber_functions_03.phpt index 8e267ea65a03..d28a90ce57ec 100644 --- a/ext/zend_test/tests/observer_fiber_functions_03.phpt +++ b/ext/zend_test/tests/observer_fiber_functions_03.phpt @@ -78,12 +78,37 @@ int(2) - -Fatal error: Allowed memory size of 104857600 bytes exhausted %s on line %d + + - + + + + + + + + + + +Fatal error: Uncaught MemoryError: The resulting string is too large to fit in the configured memory limit in %s:%d +Stack trace: +#0 %s(%d): str_repeat('A', 200000000) +#1 [internal function]: {closure:%s:%d}() +#2 %s(%d): Fiber->resume() +#3 {main} + thrown in %s on line %d + + + + + + + + +