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
51 changes: 51 additions & 0 deletions Zend/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,57 @@ 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.
OS pages belonging to retired page runs and huge mappings are returned to the
kernel on supported platforms while their virtual addresses remain reserved.
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.

Reserved virtual memory and allocator accounting approach cumulative allocation
volume rather than peak live memory. Resident memory may be lower where the
operating system can discard fully retired pages. Clearing freed storage and
moving reallocations also add memory bandwidth, system-call, 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
Loading
Loading