diff --git a/Zend/README.md b/Zend/README.md index 26f3613c34bb..f2c860f37c75 100644 --- a/Zend/README.md +++ b/Zend/README.md @@ -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 diff --git a/Zend/tests/zend_mm_no_reuse_accounting.phpt b/Zend/tests/zend_mm_no_reuse_accounting.phpt new file mode 100644 index 000000000000..0b54a27ec62a --- /dev/null +++ b/Zend/tests/zend_mm_no_reuse_accounting.phpt @@ -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-- += 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) diff --git a/Zend/tests/zend_mm_no_reuse_configuration.phpt b/Zend/tests/zend_mm_no_reuse_configuration.phpt new file mode 100644 index 000000000000..c67bc4ea0657 --- /dev/null +++ b/Zend/tests/zend_mm_no_reuse_configuration.phpt @@ -0,0 +1,44 @@ +--TEST-- +ZendMM no-reuse mode rejects invalid settings and incompatible allocators +--SKIPIF-- + +--FILE-- + "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 diff --git a/Zend/tests/zend_mm_no_reuse_memory_limit.phpt b/Zend/tests/zend_mm_no_reuse_memory_limit.phpt new file mode 100644 index 000000000000..328752218440 --- /dev/null +++ b/Zend/tests/zend_mm_no_reuse_memory_limit.phpt @@ -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-- + +--EXPECTF-- +Fatal error: Allowed memory size of 16777216 bytes exhausted %s diff --git a/Zend/zend_alloc.c b/Zend/zend_alloc.c index b293ee1a51dc..315a48c55593 100644 --- a/Zend/zend_alloc.c +++ b/Zend/zend_alloc.c @@ -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 @@ -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. @@ -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 */ @@ -880,6 +883,45 @@ static zend_always_inline void zend_mm_hugepage(void* ptr, size_t size) #endif } +static zend_always_inline void zend_mm_no_reuse_clear(void *ptr, size_t size) +{ +#if defined(MADV_DONTNEED) + char *p = (char *) ptr; + size_t offset = ZEND_MM_ALIGNED_OFFSET(p, REAL_PAGE_SIZE); + size_t prefix = offset != 0 ? REAL_PAGE_SIZE - offset : 0; + + if (prefix > size) { + prefix = size; + } + if (prefix != 0) { + memset(p, 0, prefix); + p += prefix; + size -= prefix; + } + + size_t discard_size = size - ZEND_MM_ALIGNED_OFFSET(size, REAL_PAGE_SIZE); + if (discard_size != 0) { +#ifdef __linux__ + /* Private anonymous pages fault back as zeroes while their addresses stay reserved. */ + if (madvise(p, discard_size, MADV_DONTNEED) != 0) { + memset(p, 0, discard_size); + } +#else + /* MADV_DONTNEED does not guarantee zero-fill on every supported platform. */ + memset(p, 0, discard_size); + (void)madvise(p, discard_size, MADV_DONTNEED); +#endif + p += discard_size; + size -= discard_size; + } + if (size != 0) { + memset(p, 0, size); + } +#else + memset(ptr, 0, size); +#endif +} + static void *zend_mm_chunk_alloc_int(size_t size, size_t alignment) { void *ptr = zend_mm_mmap(size); @@ -1392,6 +1434,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; + zend_mm_no_reuse_clear(ZEND_MM_PAGE_ADDR(chunk, page_num), pages_count * ZEND_MM_PAGE_SIZE); + return; + } zend_mm_free_pages(heap, chunk, page_num, pages_count); } @@ -1632,6 +1680,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. */ @@ -1892,6 +1945,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)) { @@ -2118,6 +2176,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; } @@ -2229,10 +2293,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)) { + zend_mm_no_reuse_clear(ptr, 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 @@ -2334,6 +2402,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 @@ -2351,6 +2420,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; @@ -2515,6 +2588,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) { @@ -2634,6 +2711,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); @@ -2722,6 +2803,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"); @@ -3736,6 +3832,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)); @@ -3787,6 +3886,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); @@ -3854,6 +3959,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 { @@ -3933,6 +4042,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 = { @@ -3985,6 +4097,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) { diff --git a/docs/source/miscellaneous/running-tests.rst b/docs/source/miscellaneous/running-tests.rst index bb2c56dcecd2..035161164ba3 100644 --- a/docs/source/miscellaneous/running-tests.rst +++ b/docs/source/miscellaneous/running-tests.rst @@ -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 +`__. 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 ************** diff --git a/ext/zend_test/test.c b/ext/zend_test/test.c index 4867f2f41c69..7c16daad578f 100644 --- a/ext/zend_test/test.c +++ b/ext/zend_test/test.c @@ -269,6 +269,128 @@ static ZEND_FUNCTION(zend_test_callback) free(p); } +typedef struct { + uintptr_t start; + size_t size; +} zend_test_mm_range; + +static bool zend_test_mm_is_fresh(const zend_test_mm_range *retired, size_t count, void *ptr, size_t size) +{ + uintptr_t start = (uintptr_t) ptr; + for (size_t i = 0; i < count; i++) { + if (start < retired[i].start + retired[i].size && retired[i].start < start + size) { + return false; + } + } + return true; +} + +static ZEND_FUNCTION(zend_test_mm_no_reuse) +{ + ZEND_PARSE_PARAMETERS_NONE(); + + const char *enabled = getenv("ZEND_MM_NO_REUSE"); + if (!enabled || strcmp(enabled, "1") != 0) { + zend_throw_error(NULL, "Start PHP with ZEND_MM_NO_REUSE=1"); + RETURN_THROWS(); + } + +#define ZEND_TEST_MM_SIZE(num, size, elements, pages, x, y) size, + const size_t sizes[] = { + ZEND_MM_BINS_INFO(ZEND_TEST_MM_SIZE, x, y) + 4096, 16384, 1024 * 1024, ZEND_MM_MAX_LARGE_SIZE, 3 * 1024 * 1024 + }; +#undef ZEND_TEST_MM_SIZE + const size_t realloc_sizes[] = {1, 64, 3072, 4096, 16384, 3 * 1024 * 1024}; + zend_test_mm_range retired[1024]; + size_t count = 0; + bool fresh = true, collected = true, moved = true, preserved = true, cleared = true; + zend_mm_heap *heap = zend_mm_startup(); + + for (size_t i = 0; i < 512; i++) { + void *ptr = zend_mm_alloc(heap, 64); + size_t capacity = zend_mm_block_size(heap, ptr); + fresh &= zend_test_mm_is_fresh(retired, count, ptr, capacity); + memset(ptr, 0x5a, 64); + zend_mm_free(heap, ptr); + retired[count++] = (zend_test_mm_range) {(uintptr_t) ptr, capacity}; + if (i % 32 == 0) { + collected &= zend_mm_gc(heap) == 0; + } + } + for (size_t pass = 0; pass < 2; pass++) { + for (size_t i = 0; i < sizeof(sizes) / sizeof(*sizes); i++) { + void *ptr = zend_mm_alloc(heap, sizes[i]); + size_t capacity = zend_mm_block_size(heap, ptr); + fresh &= zend_test_mm_is_fresh(retired, count, ptr, capacity); + memset(ptr, 0x5a, sizes[i]); + zend_mm_free(heap, ptr); + retired[count++] = (zend_test_mm_range) {(uintptr_t) ptr, capacity}; + collected &= zend_mm_gc(heap) == 0; + } + } + for (size_t i = 0; i < sizeof(realloc_sizes) / sizeof(*realloc_sizes); i++) { + for (size_t j = 0; j < sizeof(realloc_sizes) / sizeof(*realloc_sizes); j++) { + size_t old_size = realloc_sizes[i], new_size = realloc_sizes[j]; + void *ptr = zend_mm_realloc2(heap, NULL, old_size, 0); + size_t capacity = zend_mm_block_size(heap, ptr); + fresh &= zend_test_mm_is_fresh(retired, count, ptr, capacity); + memset(ptr, 0x5a, old_size); + retired[count++] = (zend_test_mm_range) {(uintptr_t) ptr, capacity}; + if (j % 2 == 0) { + ptr = zend_mm_realloc(heap, ptr, new_size); + } else { + ptr = zend_mm_realloc2(heap, ptr, new_size, MIN(old_size, new_size)); + } + capacity = zend_mm_block_size(heap, ptr); + moved &= zend_test_mm_is_fresh(retired, count, ptr, capacity); + for (size_t k = 0; k < MIN(old_size, new_size); k++) { + preserved &= ((unsigned char *) ptr)[k] == 0x5a; + } + zend_mm_free(heap, ptr); + retired[count++] = (zend_test_mm_range) {(uintptr_t) ptr, capacity}; + } + } + + zend_mm_heap *orig_heap = zend_mm_set_heap(heap); + void *ptr = emalloc(1); + size_t capacity = zend_mem_block_size(ptr); + fresh &= zend_test_mm_is_fresh(retired, count, ptr, capacity); + efree_size(ptr, 1); + retired[count++] = (zend_test_mm_range) {(uintptr_t) ptr, capacity}; + ptr = emalloc(8192); + capacity = zend_mem_block_size(ptr); + fresh &= zend_test_mm_is_fresh(retired, count, ptr, capacity); + efree_size(ptr, 8192); + retired[count++] = (zend_test_mm_range) {(uintptr_t) ptr, capacity}; + zend_mm_set_heap(orig_heap); + + ZEND_ASSERT(count <= sizeof(retired) / sizeof(*retired)); + zend_mm_refresh_key_child(heap); + for (size_t i = 0; i < count; i++) { + const unsigned char *bytes = (const unsigned char *) retired[i].start; + for (size_t j = 0; j < retired[i].size; j++) { + cleared &= bytes[j] == 0; + } + } + zend_mm_shutdown(heap, false, false); + ptr = zend_mm_alloc(heap, 64); + bool reset = (uintptr_t) ptr == retired[0].start; + zend_mm_free(heap, ptr); + zend_mm_shutdown(heap, false, false); + ptr = zend_mm_alloc(heap, 3 * 1024 * 1024); + zend_mm_free(heap, ptr); + zend_mm_shutdown(heap, true, false); + + array_init(return_value); + add_assoc_bool(return_value, "non-overlap", fresh); + add_assoc_bool(return_value, "gc", collected); + add_assoc_bool(return_value, "realloc", moved); + add_assoc_bool(return_value, "contents", preserved); + add_assoc_bool(return_value, "cleared", cleared); + add_assoc_bool(return_value, "shutdown", reset); +} + /* Leak a refcounted variable */ static ZEND_FUNCTION(zend_leak_variable) { diff --git a/ext/zend_test/test.stub.php b/ext/zend_test/test.stub.php index db5ca629ade2..31c4a5bb530d 100644 --- a/ext/zend_test/test.stub.php +++ b/ext/zend_test/test.stub.php @@ -301,6 +301,8 @@ function zend_delref(mixed $variable): void {} function zend_test_erealloc_block_size(int $old_size, int $new_size): array {} + function zend_test_mm_no_reuse(): array {} + function zend_bool(bool $param): bool {} function zend_bool_or_null(bool|null $param): bool|null {} function zend_bool_slow_zpp(bool $param): bool {} diff --git a/ext/zend_test/test_arginfo.h b/ext/zend_test/test_arginfo.h index 87622c185cff..fd328fcb478c 100644 --- a/ext/zend_test/test_arginfo.h +++ b/ext/zend_test/test_arginfo.h @@ -1,5 +1,5 @@ /* This is a generated file, edit test.stub.php instead. - * Stub hash: ea2674e5d5dbf60aa5efab31132064b52a07bddd + * Stub hash: c9809fbf1f24e193d0d9a68600ca3a2f74a59981 * Has decl header: yes */ #include "zend_attributes.h" @@ -64,6 +64,8 @@ ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_zend_test_erealloc_block_size, 0 ZEND_ARG_TYPE_INFO(0, new_size, IS_LONG, 0) ZEND_END_ARG_INFO() +#define arginfo_zend_test_mm_no_reuse arginfo_zend_test_array_return + ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_zend_bool, 0, 1, _IS_BOOL, 0) ZEND_ARG_TYPE_INFO(0, param, _IS_BOOL, 0) ZEND_END_ARG_INFO() @@ -463,6 +465,7 @@ static ZEND_FUNCTION(zend_leak_variable); static ZEND_FUNCTION(zend_leak_bytes); static ZEND_FUNCTION(zend_delref); static ZEND_FUNCTION(zend_test_erealloc_block_size); +static ZEND_FUNCTION(zend_test_mm_no_reuse); static ZEND_FUNCTION(zend_bool); static ZEND_FUNCTION(zend_bool_or_null); static ZEND_FUNCTION(zend_bool_slow_zpp); @@ -645,6 +648,7 @@ static const zend_function_entry ext_functions[] = { ZEND_FE(zend_leak_bytes, arginfo_zend_leak_bytes) ZEND_FE(zend_delref, arginfo_zend_delref) ZEND_FE(zend_test_erealloc_block_size, arginfo_zend_test_erealloc_block_size) + ZEND_FE(zend_test_mm_no_reuse, arginfo_zend_test_mm_no_reuse) ZEND_FE(zend_bool, arginfo_zend_bool) ZEND_FE(zend_bool_or_null, arginfo_zend_bool_or_null) ZEND_FE(zend_bool_slow_zpp, arginfo_zend_bool_slow_zpp) diff --git a/ext/zend_test/test_decl.h b/ext/zend_test/test_decl.h index db7652512c83..ba6044980dbb 100644 --- a/ext/zend_test/test_decl.h +++ b/ext/zend_test/test_decl.h @@ -1,8 +1,8 @@ /* This is a generated file, edit test.stub.php instead. - * Stub hash: ea2674e5d5dbf60aa5efab31132064b52a07bddd */ + * Stub hash: c9809fbf1f24e193d0d9a68600ca3a2f74a59981 */ -#ifndef ZEND_TEST_DECL_ea2674e5d5dbf60aa5efab31132064b52a07bddd_H -#define ZEND_TEST_DECL_ea2674e5d5dbf60aa5efab31132064b52a07bddd_H +#ifndef ZEND_TEST_DECL_c9809fbf1f24e193d0d9a68600ca3a2f74a59981_H +#define ZEND_TEST_DECL_c9809fbf1f24e193d0d9a68600ca3a2f74a59981_H typedef enum zend_enum_ZendTestUnitEnum { ZEND_ENUM_ZendTestUnitEnum_Foo = 1, @@ -27,4 +27,4 @@ typedef enum zend_enum_ZendTestEnumWithInterface { ZEND_ENUM_ZendTestEnumWithInterface_Bar = 2, } zend_enum_ZendTestEnumWithInterface; -#endif /* ZEND_TEST_DECL_ea2674e5d5dbf60aa5efab31132064b52a07bddd_H */ +#endif /* ZEND_TEST_DECL_c9809fbf1f24e193d0d9a68600ca3a2f74a59981_H */ diff --git a/ext/zend_test/test_legacy_arginfo.h b/ext/zend_test/test_legacy_arginfo.h index a673b373142a..01635b099ab5 100644 --- a/ext/zend_test/test_legacy_arginfo.h +++ b/ext/zend_test/test_legacy_arginfo.h @@ -1,5 +1,5 @@ /* This is a generated file, edit test.stub.php instead. - * Stub hash: ea2674e5d5dbf60aa5efab31132064b52a07bddd + * Stub hash: c9809fbf1f24e193d0d9a68600ca3a2f74a59981 * Has decl header: yes */ #include "zend_constants.h" @@ -59,6 +59,8 @@ ZEND_BEGIN_ARG_INFO_EX(arginfo_zend_test_erealloc_block_size, 0, 0, 2) ZEND_ARG_INFO(0, new_size) ZEND_END_ARG_INFO() +#define arginfo_zend_test_mm_no_reuse arginfo_zend_trigger_bailout + ZEND_BEGIN_ARG_INFO_EX(arginfo_zend_bool, 0, 0, 1) ZEND_ARG_INFO(0, param) ZEND_END_ARG_INFO() @@ -387,6 +389,7 @@ static ZEND_FUNCTION(zend_leak_variable); static ZEND_FUNCTION(zend_leak_bytes); static ZEND_FUNCTION(zend_delref); static ZEND_FUNCTION(zend_test_erealloc_block_size); +static ZEND_FUNCTION(zend_test_mm_no_reuse); static ZEND_FUNCTION(zend_bool); static ZEND_FUNCTION(zend_bool_or_null); static ZEND_FUNCTION(zend_bool_slow_zpp); @@ -541,6 +544,7 @@ static const zend_function_entry ext_functions[] = { ZEND_FE(zend_leak_bytes, arginfo_zend_leak_bytes) ZEND_FE(zend_delref, arginfo_zend_delref) ZEND_FE(zend_test_erealloc_block_size, arginfo_zend_test_erealloc_block_size) + ZEND_FE(zend_test_mm_no_reuse, arginfo_zend_test_mm_no_reuse) ZEND_FE(zend_bool, arginfo_zend_bool) ZEND_FE(zend_bool_or_null, arginfo_zend_bool_or_null) ZEND_FE(zend_bool_slow_zpp, arginfo_zend_bool_slow_zpp) diff --git a/ext/zend_test/tests/zend_mm_no_reuse.phpt b/ext/zend_test/tests/zend_mm_no_reuse.phpt new file mode 100644 index 000000000000..fa8486502091 --- /dev/null +++ b/ext/zend_test/tests/zend_mm_no_reuse.phpt @@ -0,0 +1,20 @@ +--TEST-- +ZendMM no-reuse mode retains and clears freed storage until heap shutdown +--EXTENSIONS-- +zend_test +--ENV-- +USE_ZEND_ALLOC=1 +ZEND_MM_NO_REUSE=1 +--FILE-- + $passed) { + echo "$check: ", $passed ? "ok" : "failed", "\n"; +} +?> +--EXPECT-- +non-overlap: ok +gc: ok +realloc: ok +contents: ok +cleared: ok +shutdown: ok