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
845bf07
Add zend_memory_limit() and zend_memory_limit_is_unlimited()
ramsey Jul 5, 2026
04031a4
Add MemoryError exception class
ramsey Jul 5, 2026
5342f72
Add zend_string_alloc_size_exceeds_memory() pre-check helper
ramsey Jul 5, 2026
933e246
Throw MemoryError for an over-large string offset assignment
ramsey Jul 5, 2026
787186c
Throw MemoryError in str_repeat(), str_pad(), number_format()
ramsey Jul 5, 2026
0dbbb5e
Throw MemoryError in random_bytes()
ramsey Jul 5, 2026
9ad6ff4
Throw MemoryError in mb_str_pad()
ramsey Jul 5, 2026
9afae2a
Adjust OOM bailout tests to reach the memory limit
ramsey Jul 5, 2026
26627db
Base pre-check on used memory rather than reserved chunk size
ramsey Jul 5, 2026
eacbd51
Throw MemoryError in fread() and fgets()
ramsey Jul 5, 2026
9c843f8
Throw MemoryError in stream_socket_recvfrom()
ramsey Jul 5, 2026
4cf9e01
Throw MemoryError in socket_read(), socket_recv(), socket_recvfrom()
ramsey Jul 5, 2026
5c6e851
Throw MemoryError in pack() and chunk_split()
ramsey Jul 5, 2026
23faae0
Throw MemoryError for a printf-family too-large width
ramsey Jul 5, 2026
2615982
Generalize the MemoryError pre-check for non-string allocations
ramsey Jul 5, 2026
78b68b5
Throw MemoryError in array_fill(), array_pad(), range(), str_split()
ramsey Jul 5, 2026
c3a23cf
Throw MemoryError for an oversized SplFixedArray
ramsey Jul 5, 2026
137881b
Throw MemoryError in msg_receive()
ramsey Jul 5, 2026
f141a6e
Throw MemoryError in openssl_random_pseudo_bytes()
ramsey Jul 5, 2026
7510d78
Throw MemoryError in sodium length-taking functions
ramsey Jul 5, 2026
cbfdbfe
Throw MemoryError in Randomizer::getBytes() and getBytesFromString()
ramsey Jul 5, 2026
a88ecfd
Throw MemoryError in mb_str_split()
ramsey Jul 5, 2026
51b138a
Throw MemoryError in fgetcsv()
ramsey Jul 5, 2026
22023f8
Throw MemoryError in shmop_read()
ramsey Jul 5, 2026
9770294
Throw MemoryError in pg_lo_read()
ramsey Jul 5, 2026
cb720b8
Throw MemoryError in GMP string rendering and gmp_export()
ramsey Jul 5, 2026
e164219
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 @@ -2919,6 +2920,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 @@ -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(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 @@ -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;
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 @@ -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);

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