Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
4f05cbc
Add zend_memory_limit() and zend_memory_limit_is_unlimited()
ramsey Jul 5, 2026
4edbe83
Add MemoryError exception class
ramsey Jul 5, 2026
4756591
Add zend_string_alloc_size_exceeds_memory() pre-check helper
ramsey Jul 5, 2026
a389682
Throw MemoryError for an over-large string offset assignment
ramsey Jul 5, 2026
db950ed
Throw MemoryError in str_repeat(), str_pad(), number_format()
ramsey Jul 5, 2026
3599dc7
Throw MemoryError in random_bytes()
ramsey Jul 5, 2026
8ea3ac9
Throw MemoryError in mb_str_pad()
ramsey Jul 5, 2026
2ce4e9a
Adjust OOM bailout tests to reach the memory limit
ramsey Jul 5, 2026
0fee9fb
Base pre-check on used memory rather than reserved chunk size
ramsey Jul 5, 2026
17ace0f
Throw MemoryError in fread() and fgets()
ramsey Jul 5, 2026
dc4731d
Throw MemoryError in stream_socket_recvfrom()
ramsey Jul 5, 2026
9403a8a
Throw MemoryError in socket_read(), socket_recv(), socket_recvfrom()
ramsey Jul 5, 2026
6b017cb
Throw MemoryError in pack() and chunk_split()
ramsey Jul 5, 2026
874b034
Throw MemoryError for a printf-family too-large width
ramsey Jul 5, 2026
c7b1aae
Generalize the MemoryError pre-check for non-string allocations
ramsey Jul 5, 2026
076419c
Throw MemoryError in array_fill(), array_pad(), range(), str_split()
ramsey Jul 5, 2026
8fc51da
Throw MemoryError for an oversized SplFixedArray
ramsey Jul 5, 2026
a902e06
Throw MemoryError in msg_receive()
ramsey Jul 5, 2026
2efdeda
Throw MemoryError in openssl_random_pseudo_bytes()
ramsey Jul 5, 2026
4f99503
Throw MemoryError in sodium length-taking functions
ramsey Jul 5, 2026
68427c6
Throw MemoryError in Randomizer::getBytes() and getBytesFromString()
ramsey Jul 5, 2026
109d88f
Throw MemoryError in mb_str_split()
ramsey Jul 5, 2026
ff11ca7
Throw MemoryError in fgetcsv()
ramsey Jul 5, 2026
ebda2b0
Throw MemoryError in shmop_read()
ramsey Jul 5, 2026
02f273d
Throw MemoryError in pg_lo_read()
ramsey Jul 5, 2026
f03f82d
Throw MemoryError in GMP string rendering and gmp_export()
ramsey Jul 5, 2026
cea28bd
Update tests for MemoryError on 32-bit and BSD platforms
ramsey Jul 12, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 3 additions & 4 deletions Zend/tests/bug40770.phpt
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,9 @@ if ($zend_mm_enabled === "0") {
--FILE--
<?php
ini_set('display_errors',true);
$mb=148;
$var = '';
for ($i=0; $i<=$mb; $i++) {
$var.= str_repeat('a',1*1024*1024);
$var = 'a';
while (true) {
$var .= $var;
}
?>
--EXPECTF--
Expand Down
29 changes: 16 additions & 13 deletions Zend/tests/bug55509.phpt
Original file line number Diff line number Diff line change
Expand Up @@ -65,21 +65,24 @@ elseif (PHP_OS == 'FreeBSD') {
memory_limit=2100M
--FILE--
<?php
$a1 = str_repeat("1", 1024 * 1024 * 1024 * 0.5);
echo "1\n";
$a2 = str_repeat("2", 1024 * 1024 * 1024 * 0.5);
echo "2\n";
$a3 = str_repeat("3", 1024 * 1024 * 1024 * 0.5);
echo "3\n";
$a4 = str_repeat("4", 1024 * 1024 * 1024 * 0.5);
echo "4\n";
$a5 = str_repeat("5", 1024 * 1024 * 1024 * 0.5);
echo "5\n";
try {
$a1 = str_repeat("1", 1024 * 1024 * 1024 * 0.5);
echo "1\n";
$a2 = str_repeat("2", 1024 * 1024 * 1024 * 0.5);
echo "2\n";
$a3 = str_repeat("3", 1024 * 1024 * 1024 * 0.5);
echo "3\n";
$a4 = str_repeat("4", 1024 * 1024 * 1024 * 0.5);
echo "4\n";
$a5 = str_repeat("5", 1024 * 1024 * 1024 * 0.5);
echo "5\n";
} catch (Throwable $t) {
echo $t::class . ': ' . $t->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
5 changes: 4 additions & 1 deletion Zend/tests/fibers/get-return-after-bailout.phpt
Original file line number Diff line number Diff line change
Expand Up @@ -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();

Expand Down
4 changes: 2 additions & 2 deletions Zend/tests/fibers/out-of-memory-in-fiber.phpt
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,9 @@ if (getenv("USE_ZEND_ALLOC") === "0") {
<?php

$fiber = new Fiber(function (): void {
$buffer = '';
$buffer = '.';
while (true) {
$buffer .= str_repeat('.', 1 << 10);
$buffer .= $buffer;
}
});

Expand Down
4 changes: 2 additions & 2 deletions Zend/tests/fibers/out-of-memory-in-nested-fiber.phpt
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,9 @@ if (getenv("USE_ZEND_ALLOC") === "0") {

$fiber = new Fiber(function (): void {
$fiber = new Fiber(function (): void {
$buffer = '';
$buffer = '.';
while (true) {
$buffer .= str_repeat('.', 1 << 10);
$buffer .= $buffer;
}
});

Expand Down
22 changes: 22 additions & 0 deletions Zend/tests/memory_error_class.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
--TEST--
MemoryError is a catchable Error
--FILE--
<?php

$e = new MemoryError('boom');
var_dump($e instanceof Error);
var_dump($e instanceof Throwable);
var_dump($e->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
33 changes: 33 additions & 0 deletions Zend/tests/offsets/string_offset_write_memory_error.phpt
Original file line number Diff line number Diff line change
@@ -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--
<?php

$s = 'abc';
try {
$s[PHP_INT_MAX] = 'Z';
} catch (MemoryError $e) {
echo $e->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"
37 changes: 37 additions & 0 deletions Zend/zend_alloc.c
Original file line number Diff line number Diff line change
Expand Up @@ -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 <signal.h>
Expand Down Expand Up @@ -2918,6 +2919,42 @@ 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_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
Expand Down
7 changes: 7 additions & 0 deletions Zend/zend_alloc.h
Original file line number Diff line number Diff line change
Expand Up @@ -215,8 +215,15 @@ 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);

/* 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);
Expand Down
4 changes: 4 additions & 0 deletions Zend/zend_exceptions.c
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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);

Expand Down
1 change: 1 addition & 0 deletions Zend/zend_exceptions.h
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down
4 changes: 4 additions & 0 deletions Zend/zend_exceptions.stub.php
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,10 @@ class DivisionByZeroError extends ArithmeticError
{
}

class MemoryError extends Error
{
}

class UnhandledMatchError extends Error
{
}
Expand Down
12 changes: 11 additions & 1 deletion Zend/zend_exceptions_arginfo.h

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

19 changes: 19 additions & 0 deletions Zend/zend_execute.c
Original file line number Diff line number Diff line change
Expand Up @@ -2044,6 +2044,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(false);
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;
Expand Down Expand Up @@ -2168,6 +2179,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;
Expand Down
6 changes: 6 additions & 0 deletions Zend/zend_hash.c
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
5 changes: 5 additions & 0 deletions Zend/zend_hash.h
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
5 changes: 5 additions & 0 deletions Zend/zend_string.c
Original file line number Diff line number Diff line change
Expand Up @@ -528,6 +528,11 @@ 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)
{
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
* ourselves. Apply a simple heuristic to determine the platforms that need it.
* See https://github.com/php/php-src/issues/20002. */
Expand Down
5 changes: 5 additions & 0 deletions Zend/zend_string.h
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
9 changes: 9 additions & 0 deletions ext/gmp/gmp.c
Original file line number Diff line number Diff line change
Expand Up @@ -686,6 +686,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);

Expand Down Expand Up @@ -855,6 +860,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';
Expand Down
Loading
Loading