Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
48 changes: 48 additions & 0 deletions Zend/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,54 @@ Zend MM disabled:
USE_ZEND_ALLOC=0 valgrind --leak-check=full sapi/cli/php -r 'leak();'
```

### Request-scoped no-reuse mode

Set `ZEND_MM_NO_REUSE=1` in the process environment before starting PHP to
prevent native Zend MM from reusing freed allocation ranges within a request:

```bash
ZEND_MM_NO_REUSE=1 sapi/cli/php script.php
```

The default is disabled: leave the variable unset or set it to `0`. Only the
literal values `0` and `1` are accepted; other values cause an allocator panic.
This is not a `php.ini` option. It is read at engine startup, so changing it with
`putenv()` has no effect on the running engine. For web SAPIs, set it in the
launcher or service environment before PHP starts, and restart PHP processes
after changing it.

With the mode enabled:

- Freed small allocations, page runs, and huge mappings are cleared and retained
until their heap shuts down, after request shutdown callbacks have finished.
Retired payloads are not used to store allocator free-list links.
- Reallocations of existing blocks always move to fresh storage, including
shrinking and unchanged-size reallocations. The original storage is retired.
- Allocator garbage collection does not reclaim retired storage. Addresses may
be reused after heap shutdown, normally in the next request.
- Retained mappings remain charged against `memory_limit`. Reaching the limit
fails through the normal allocation-error path; it never enables reuse.
`memory_get_usage(false)` can decrease after freeing an allocation even though
the underlying storage remains retained.

Memory consumption approaches cumulative allocation volume rather than peak
live memory. Clearing freed storage and moving reallocations also add memory
bandwidth and copying costs. The mode is intended for selected short-lived
requests, not long-running scripts or application workers that do not perform
normal PHP request shutdown.

The mode requires native Zend MM. Combining it with `USE_ZEND_ALLOC=0`, custom
heap or storage handlers, or `ZEND_MM_DEBUG` allocator wrappers causes an
allocator panic rather than silently bypassing the protection.

This is exploit mitigation, not memory safety or a sandbox. Invalid accesses
to retired storage may still occur without trapping. Persistent allocations and
extension-owned allocations outside native Zend MM are not covered, nor are
dangling references that survive heap shutdown.

See [Testing ZendMM No-Reuse Mode](../docs/source/miscellaneous/running-tests.rst#testing-zendmm-no-reuse-mode)
for regression-test instructions.

### Shared extensions

Since PHP 5.3.11 it is possible to prevent shared extensions from unloading so
Expand Down
38 changes: 38 additions & 0 deletions Zend/tests/zend_mm_no_reuse_accounting.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
--TEST--
ZendMM no-reuse mode keeps retired huge mappings accounted for
--ENV--
USE_ZEND_ALLOC=1
ZEND_MM_NO_REUSE=1
--FILE--
<?php
putenv("ZEND_MM_NO_REUSE=0");
$initial = memory_get_usage(true);
$buffer = str_repeat("x", 3 * 1024 * 1024);
$allocated = memory_get_usage();
$committed = memory_get_usage(true);
unset($buffer);
$released = memory_get_usage();
$retained = memory_get_usage(true);
$collected = gc_mem_caches();

var_dump($committed - $initial >= 3 * 1024 * 1024);
var_dump($allocated - $released >= 3 * 1024 * 1024);
var_dump($retained === $committed);
var_dump($collected === 0);
var_dump(memory_get_usage(true) === $retained);

register_shutdown_function(function () use ($retained) {
var_dump(memory_get_usage(true) >= $retained);
$buffer = str_repeat("y", 3 * 1024 * 1024);
unset($buffer);
var_dump(memory_get_usage(true) > $retained);
});
?>
--EXPECT--
bool(true)
bool(true)
bool(true)
bool(true)
bool(true)
bool(true)
bool(true)
44 changes: 44 additions & 0 deletions Zend/tests/zend_mm_no_reuse_configuration.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
--TEST--
ZendMM no-reuse mode rejects invalid settings and incompatible allocators
--SKIPIF--
<?php
if (!function_exists("proc_open")) {
die("skip proc_open is required");
}
?>
--FILE--
<?php
$env = getenv();
$cases = [
["ZEND_MM_NO_REUSE" => "invalid", "USE_ZEND_ALLOC" => "1"],
["ZEND_MM_NO_REUSE" => "1", "USE_ZEND_ALLOC" => "0"],
["ZEND_MM_NO_REUSE" => "1", "USE_ZEND_ALLOC" => "1", "ZEND_MM_DEBUG" => "poison_free=0"],
];
foreach ($cases as $case) {
$process = proc_open(
[PHP_BINARY, "-n", "-r", "echo 'Unexpected startup';"],
[0 => ["pipe", "r"], 1 => ["pipe", "w"], 2 => ["pipe", "w"]],
$pipes,
null,
array_replace($env, $case)
);
fclose($pipes[0]);
$output = stream_get_contents($pipes[1]);
$error = stream_get_contents($pipes[2]);
fclose($pipes[1]);
fclose($pipes[2]);
var_dump(proc_close($process) !== 0);
var_dump($output === "");
echo strtok($error, "\r\n"), "\n";
}
?>
--EXPECT--
bool(true)
bool(true)
ZEND_MM_NO_REUSE must be 0 or 1
bool(true)
bool(true)
ZEND_MM_NO_REUSE requires native ZendMM (USE_ZEND_ALLOC=1)
bool(true)
bool(true)
ZEND_MM_NO_REUSE does not support custom heap handlers
18 changes: 18 additions & 0 deletions Zend/tests/zend_mm_no_reuse_memory_limit.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
--TEST--
ZendMM no-reuse mode charges retired allocations against the memory limit
--ENV--
USE_ZEND_ALLOC=1
ZEND_MM_NO_REUSE=1
--INI--
memory_limit=16M
--FILE--
<?php
for ($i = 0; $i < 100; $i++) {
$buffer = str_repeat("x", 1024 * 1024);
unset($buffer);
gc_mem_caches();
}
echo "Unexpected reuse\n";
?>
--EXPECTF--
Fatal error: Allowed memory size of 16777216 bytes exhausted %s
78 changes: 76 additions & 2 deletions Zend/zend_alloc.c
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,7 @@ typedef zend_ulong zend_mm_bitset; /* 4-byte or 8-byte integer */
typedef zend_mm_bitset zend_mm_page_map[ZEND_MM_PAGE_MAP_LEN]; /* 64B */

#define ZEND_MM_IS_FRUN 0x00000000
#define ZEND_MM_IS_RETIRED 0x20000000
#define ZEND_MM_IS_LRUN 0x40000000
#define ZEND_MM_IS_SRUN 0x80000000

Expand Down Expand Up @@ -237,6 +238,7 @@ typedef struct _zend_mm_huge_list zend_mm_huge_list;

static bool zend_mm_use_huge_pages = false;
static bool zend_mm_max_allocs_reached = false;
static bool zend_mm_no_reuse = false;

/*
* Memory is retrieved from OS by chunks of fixed size 2MB.
Expand Down Expand Up @@ -298,6 +300,7 @@ struct _zend_mm_heap {
bool fallible;

zend_mm_huge_list *huge_list; /* list of huge allocated blocks */
zend_mm_huge_list *retired_huge_list;

zend_mm_chunk *main_chunk;
zend_mm_chunk *cached_chunks; /* list of unused chunks */
Expand Down Expand Up @@ -1392,6 +1395,12 @@ static zend_always_inline void zend_mm_free_large(zend_mm_heap *heap, zend_mm_ch
#if ZEND_MM_STAT
heap->size -= pages_count * ZEND_MM_PAGE_SIZE;
#endif
if (UNEXPECTED(zend_mm_no_reuse)) {
ZEND_MM_CHECK(!(chunk->map[page_num] & ZEND_MM_IS_RETIRED), "zend_mm_heap corrupted (double free)");
chunk->map[page_num] |= ZEND_MM_IS_RETIRED;
memset(ZEND_MM_PAGE_ADDR(chunk, page_num), 0, pages_count * ZEND_MM_PAGE_SIZE);
return;
}
zend_mm_free_pages(heap, chunk, page_num, pages_count);
}

Expand Down Expand Up @@ -1632,6 +1641,11 @@ static zend_always_inline void zend_mm_free_small(zend_mm_heap *heap, void *ptr,
} while (0);
#endif

if (UNEXPECTED(zend_mm_no_reuse)) {
memset(ptr, 0, bin_data_size[bin_num]);
return;
}

p = (zend_mm_free_slot*)ptr;
#if ZEND_MM_HEAP_PROTECTION
/* Catch the most common double-free pattern for free. */
Expand Down Expand Up @@ -1892,6 +1906,11 @@ static zend_always_inline void *zend_mm_realloc_heap(zend_mm_heap *heap, void *p
zend_mm_debug_info *dbg;
#endif

if (UNEXPECTED(zend_mm_no_reuse) && ptr != NULL) {
old_size = zend_mm_size(heap, ptr ZEND_FILE_LINE_RELAY_CC ZEND_FILE_LINE_ORIG_RELAY_CC);
return zend_mm_realloc_slow(heap, ptr, size, MIN(old_size, MIN(size, copy_size)) ZEND_FILE_LINE_RELAY_CC ZEND_FILE_LINE_ORIG_RELAY_CC);
}

page_offset = ZEND_MM_ALIGNED_OFFSET(ptr, ZEND_MM_CHUNK_SIZE);
if (UNEXPECTED(page_offset == 0)) {
if (EXPECTED(ptr == NULL)) {
Expand Down Expand Up @@ -2118,6 +2137,12 @@ static size_t zend_mm_del_huge_block(zend_mm_heap *heap, void *ptr ZEND_FILE_LIN
list->next->prev = list->prev;
}
size = list->size;
if (UNEXPECTED(zend_mm_no_reuse)) {
list->next = heap->retired_huge_list;
list->prev = NULL;
heap->retired_huge_list = list;
return size;
}
zend_mm_free_heap(heap, list ZEND_FILE_LINE_RELAY_CC ZEND_FILE_LINE_ORIG_RELAY_CC);
return size;
}
Expand Down Expand Up @@ -2229,10 +2254,14 @@ static void zend_mm_free_huge(zend_mm_heap *heap, void *ptr ZEND_FILE_LINE_DC ZE
ZEND_MM_CHECK(ZEND_MM_ALIGNED_OFFSET(ptr, ZEND_MM_CHUNK_SIZE) == 0, "zend_mm_heap corrupted");
size = zend_mm_del_huge_block(heap, ptr ZEND_FILE_LINE_RELAY_CC ZEND_FILE_LINE_ORIG_RELAY_CC);
zend_mm_check_huge_block_size(heap, size);
zend_mm_chunk_free(heap, ptr, size);
if (UNEXPECTED(zend_mm_no_reuse)) {
memset(ptr, 0, size);
} else {
zend_mm_chunk_free(heap, ptr, size);
#if ZEND_MM_STAT || ZEND_MM_LIMIT
heap->real_size -= size;
heap->real_size -= size;
#endif
}
#if ZEND_MM_STAT
heap->size -= size;
#endif
Expand Down Expand Up @@ -2334,6 +2363,7 @@ static zend_mm_heap *zend_mm_init(void)
heap->storage = NULL;
#endif
heap->huge_list = NULL;
heap->retired_huge_list = NULL;
#if ZEND_DEBUG
heap->pid = getpid();
#endif
Expand All @@ -2351,6 +2381,10 @@ ZEND_API size_t zend_mm_gc(zend_mm_heap *heap)
bool has_free_pages;
size_t collected = 0;

if (UNEXPECTED(zend_mm_no_reuse)) {
return 0;
}

#if ZEND_MM_CUSTOM
if (heap->use_custom_heap) {
size_t (*gc)(void) = heap->custom_heap._gc;
Expand Down Expand Up @@ -2515,6 +2549,10 @@ static zend_long zend_mm_find_leaks(zend_mm_heap *heap, zend_mm_chunk *p, uint32
i += bin_pages[bin_num];
} else /* if (p->map[i] & ZEND_MM_IS_LRUN) */ {
int pages_count = ZEND_MM_LRUN_PAGES(p->map[i]);
if (p->map[i] & ZEND_MM_IS_RETIRED) {
i += pages_count;
continue;
}
zend_mm_debug_info *dbg = (zend_mm_debug_info*)((char*)p + ZEND_MM_PAGE_SIZE * (i + pages_count) - ZEND_MM_ALIGNED_SIZE(sizeof(zend_mm_debug_info)));

if (dbg->filename == leak->filename && dbg->lineno == leak->lineno) {
Expand Down Expand Up @@ -2634,6 +2672,10 @@ static void zend_mm_check_leaks(zend_mm_heap *heap)
i += bin_pages[bin_num];
} else /* if (p->map[i] & ZEND_MM_IS_LRUN) */ {
int pages_count = ZEND_MM_LRUN_PAGES(p->map[i]);
if (p->map[i] & ZEND_MM_IS_RETIRED) {
i += pages_count;
continue;
}
zend_mm_debug_info *dbg = (zend_mm_debug_info*)((char*)p + ZEND_MM_PAGE_SIZE * (i + pages_count) - ZEND_MM_ALIGNED_SIZE(sizeof(zend_mm_debug_info)));

leak.addr = (void*)((char*)p + ZEND_MM_PAGE_SIZE * i);
Expand Down Expand Up @@ -2722,6 +2764,21 @@ ZEND_API void zend_mm_shutdown(zend_mm_heap *heap, bool full, bool silent)
}
#endif

/* Retired mappings stay reserved until all request callbacks have finished. */
list = heap->retired_huge_list;
heap->retired_huge_list = NULL;
while (list != NULL) {
zend_mm_huge_list *next = list->next;
size_t size = list->size;
zend_mm_check_huge_block_size(heap, size);
zend_mm_chunk_free(heap, list->ptr, size);
#if ZEND_MM_STAT || ZEND_MM_LIMIT
heap->real_size -= size;
#endif
zend_mm_free_heap(heap, list ZEND_FILE_LINE_CC ZEND_FILE_LINE_EMPTY_CC);
list = next;
}

#if ZEND_DEBUG
if (!silent) {
char *tmp = getenv("ZEND_ALLOC_PRINT_LEAKS");
Expand Down Expand Up @@ -3736,6 +3793,9 @@ static void alloc_globals_ctor(zend_alloc_globals *alloc_globals)
#if ZEND_MM_CUSTOM
tmp = getenv("USE_ZEND_ALLOC");
if (tmp && !ZEND_ATOL(tmp)) {
if (zend_mm_no_reuse) {
zend_mm_panic("ZEND_MM_NO_REUSE requires native ZendMM (USE_ZEND_ALLOC=1)");
}
bool tracked = (tmp = getenv("USE_TRACKED_ALLOC")) && ZEND_ATOL(tmp);
zend_mm_heap *mm_heap = alloc_globals->mm_heap = malloc(sizeof(zend_mm_heap));
memset(mm_heap, 0, sizeof(zend_mm_heap));
Expand Down Expand Up @@ -3787,6 +3847,12 @@ static void alloc_globals_dtor(zend_alloc_globals *alloc_globals)

ZEND_API void start_memory_manager(void)
{
const char *no_reuse = getenv("ZEND_MM_NO_REUSE");
if (no_reuse && strcmp(no_reuse, "0") != 0 && strcmp(no_reuse, "1") != 0) {
zend_mm_panic("ZEND_MM_NO_REUSE must be 0 or 1");
}
zend_mm_no_reuse = no_reuse && strcmp(no_reuse, "1") == 0;

#ifndef _WIN32
# if defined(_SC_PAGESIZE)
REAL_PAGE_SIZE = sysconf(_SC_PAGESIZE);
Expand Down Expand Up @@ -3854,6 +3920,10 @@ ZEND_API void zend_mm_set_custom_handlers_ex(zend_mm_heap *heap,
#if ZEND_MM_CUSTOM
zend_mm_heap *_heap = (zend_mm_heap*)heap;

if (UNEXPECTED(zend_mm_no_reuse) && (_malloc || _free || _realloc || _gc || _shutdown)) {
zend_mm_panic("ZEND_MM_NO_REUSE does not support custom heap handlers");
}

if (!_malloc && !_free && !_realloc) {
_heap->use_custom_heap = ZEND_MM_CUSTOM_HEAP_NONE;
} else {
Expand Down Expand Up @@ -3933,6 +4003,9 @@ ZEND_API zend_mm_heap *zend_mm_startup(void)

ZEND_API zend_mm_heap *zend_mm_startup_ex(const zend_mm_handlers *handlers, void *data, size_t data_size)
{
if (UNEXPECTED(zend_mm_no_reuse)) {
zend_mm_panic("ZEND_MM_NO_REUSE does not support custom storage handlers");
}
#if ZEND_MM_STORAGE
zend_mm_storage *storage;
zend_mm_storage tmp_storage = {
Expand Down Expand Up @@ -3985,6 +4058,7 @@ ZEND_API zend_mm_heap *zend_mm_startup_ex(const zend_mm_handlers *handlers, void
#endif
heap->storage = &tmp_storage;
heap->huge_list = NULL;
heap->retired_huge_list = NULL;
memset(heap->free_slot, 0, sizeof(heap->free_slot));
storage = _zend_mm_alloc(heap, sizeof(zend_mm_storage) + data_size ZEND_FILE_LINE_CC ZEND_FILE_LINE_CC);
if (!storage) {
Expand Down
19 changes: 19 additions & 0 deletions docs/source/miscellaneous/running-tests.rst
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,25 @@ The test runner can run tests in parallel, by using the ``-j`` option:

php run-tests.php -j24 ext/date/*.phpt

Testing ZendMM No-Reuse Mode
============================

The ``ZEND_MM_NO_REUSE`` environment option is documented in the `Zend memory-manager README
<https://github.com/php/php-src/blob/master/Zend/README.md#request-scoped-no-reuse-mode>`__. Build
with ``--enable-zend-test`` and run its dedicated regression tests:

.. code:: shell

sapi/cli/php run-tests.php Zend/tests/zend_mm_no_reuse_*.phpt \
ext/zend_test/tests/zend_mm_no_reuse.phpt

These tests enable the mode in their own test processes. They check address-range non-overlap,
clearing, reallocations, GC, repeated heap shutdown, memory accounting, and incompatible
configurations.

Most ordinary tests should still be run in the default allocator mode; tests relying on allocation
recycling or bounded memory usage can behave differently with no-reuse enabled.

**************
Test results
**************
Expand Down
Loading
Loading