[php-src] PHP-8.6: zend_alloc: move a small block shrunk to the size of the bin below

From: Date: Thu, 08 Oct 2026 18:42:01 +0000
Subject: [php-src] PHP-8.6: zend_alloc: move a small block shrunk to the size of the bin below
Groups: php.cvs 
Request: Send a blank email to php-cvs+get-142104@lists.php.net to get a copy of this message
Author: Marc Bennewitz (marc-mabe) Committer: ndossche (ndossche) Date: 2026-10-08T20:39:44+02:00 Commit: https://github.com/php/php-src/commit/630406c89566c3ffd19623456b0516302c747a85 Raw diff: https://github.com/php/php-src/commit/630406c89566c3ffd19623456b0516302c747a85.diff zend_alloc: move a small block shrunk to the size of the bin below Reallocating a small block to exactly the size of the bin below kept it in place, in its bin. efree_size() with that size then frees it into the bin below: heap->size is decreased by too little, and the slot of a run of the larger bin ends up on the free list of the smaller one. The next zend_mm_gc() counts that slot for the smaller bin and stores the run in the page map with the number of the smaller bin, so the run's remaining blocks are then freed and its pages released with the wrong layout. Truncate when the size fits the bin below, so the block lives in the bin its size maps to. This costs no instruction; it only turns a realloc to exactly the smaller bin size from in place into a copy. The test uses a new zend_test function, zend_test_erealloc_block_size(), to compare the block size after erealloc() with that of a fresh allocation of the same size. Closes GH-24014. Changed paths: A Zend/tests/zend_alloc_realloc_shrink_boundary.phpt M NEWS M Zend/zend_alloc.c M ext/zend_test/test.c M ext/zend_test/test.stub.php M ext/zend_test/test_arginfo.h Diff: diff --git a/NEWS b/NEWS index 4d15d50f6436..f2c2a126aa6d 100644 --- a/NEWS +++ b/NEWS @@ -7,6 +7,9 @@ PHP NEWS in its previous chain. (Edmond) . Fixed bug GH-23979 (Nullsafe operator must not flush delayed oplines of an enclosing function). (ndossche) + . Fixed memory manager keeping a block reallocated to exactly the size of + the next smaller bin in its larger bin, which efree_size() then freed + into the wrong one. (Marc Bennewitz) - DOM: . Fixed bug GH-23352 (UAF reading an attribute value node retained across diff --git a/Zend/tests/zend_alloc_realloc_shrink_boundary.phpt b/Zend/tests/zend_alloc_realloc_shrink_boundary.phpt new file mode 100644 index 000000000000..0fae7c2acda0 --- /dev/null +++ b/Zend/tests/zend_alloc_realloc_shrink_boundary.phpt @@ -0,0 +1,40 @@ +--TEST-- +erealloc() of a small block keeps it in the bin its new size maps to +--EXTENSIONS-- +zend_test +--SKIPIF-- +<?php +if (getenv("USE_ZEND_ALLOC") === "0") die("skip requires zmm"); +?> +--FILE-- +<?php + +// The largest request of each small bin, found from fresh allocations. +$largest = []; +for ($n = 1; $n <= 3072; $n++) { + [, $block] = zend_test_erealloc_block_size($n, $n); + if ($block > 3072) { + break; + } + $largest[$block] = $n; +} +var_dump(count($largest) > 20); + +// Shrink a block of each bin to every smaller size, down to the size of the +// bin below, which efree_size() maps to that bin. +$mismatches = []; +foreach ($largest as $old_size) { + for ($new_size = 1; $new_size < $old_size; $new_size++) { + [$block, $fresh] = zend_test_erealloc_block_size($old_size, $new_size); + if ($block !== $fresh) { + $mismatches[] = "$old_size -> $new_size: block of $block, expected $fresh"; + } + } +} +var_dump(array_slice($mismatches, 0, 5)); + +?> +--EXPECT-- +bool(true) +array(0) { +} diff --git a/Zend/zend_alloc.c b/Zend/zend_alloc.c index 7768d4e3af3d..0b0ecb44e236 100644 --- a/Zend/zend_alloc.c +++ b/Zend/zend_alloc.c @@ -1713,8 +1713,9 @@ static zend_always_inline void *zend_mm_realloc_heap(zend_mm_heap *heap, void *p /* Check if requested size fits into current bin */ if (size <= old_size) { - /* Check if truncation is necessary */ - if (old_bin_num > 0 && size < bin_data_size[old_bin_num - 1]) { + /* Check if truncation is necessary. A size that fits the + * bin below moves there, as efree_size() maps it to that bin. */ + if (old_bin_num > 0 && size <= bin_data_size[old_bin_num - 1]) { /* truncation */ ret = zend_mm_alloc_small(heap, ZEND_MM_SMALL_SIZE_TO_BIN(size) ZEND_FILE_LINE_RELAY_CC ZEND_FILE_LINE_ORIG_RELAY_CC); copy_size = use_copy_size ? MIN(size, copy_size) : size; diff --git a/ext/zend_test/test.c b/ext/zend_test/test.c index c4be7ff42fb6..c5ae9a71c203 100644 --- a/ext/zend_test/test.c +++ b/ext/zend_test/test.c @@ -174,6 +174,37 @@ static ZEND_FUNCTION(zend_leak_bytes) emalloc(leakbytes); } +/* Reallocate a block of old_size bytes to new_size bytes and return its block + * size, along with the block size of a fresh allocation of new_size bytes. */ +static ZEND_FUNCTION(zend_test_erealloc_block_size) +{ + zend_long old_size, new_size; + + ZEND_PARSE_PARAMETERS_START(2, 2) + Z_PARAM_LONG(old_size) + Z_PARAM_LONG(new_size) + ZEND_PARSE_PARAMETERS_END(); + + if (old_size < 1) { + zend_argument_value_error(1, "must be greater than 0"); + RETURN_THROWS(); + } + if (new_size < 1) { + zend_argument_value_error(2, "must be greater than 0"); + RETURN_THROWS(); + } + + void *ptr = erealloc(emalloc(old_size), new_size); + void *fresh = emalloc(new_size); + + array_init(return_value); + add_next_index_long(return_value, zend_mem_block_size(ptr)); + add_next_index_long(return_value, zend_mem_block_size(fresh)); + + efree(fresh); + efree(ptr); +} + /* 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 dfe04caabbb4..c595677fc4a8 100644 --- a/ext/zend_test/test.stub.php +++ b/ext/zend_test/test.stub.php @@ -246,6 +246,8 @@ function zend_leak_variable(mixed $variable): void {} function zend_leak_bytes(int $bytes = 3): void {} + function zend_test_erealloc_block_size(int $old_size, int $new_size): array {} + function zend_string_or_object(object|string $param): object|string {} function zend_string_or_object_or_null(object|string|null $param): object|string|null {} diff --git a/ext/zend_test/test_arginfo.h b/ext/zend_test/test_arginfo.h index c08feb900466..93792ac7c664 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 the .stub.php file instead. - * Stub hash: d87db0f02d40732749355ee65828221c3fd14cdb */ + * Stub hash: b1c1701fbea434d422c0f8e6b04582ca231901e2 */ ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_zend_test_array_return, 0, 0, IS_ARRAY, 0) ZEND_END_ARG_INFO() @@ -42,6 +42,11 @@ ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_zend_leak_bytes, 0, 0, IS_VOID, ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, bytes, IS_LONG, 0, "3") ZEND_END_ARG_INFO() +ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_zend_test_erealloc_block_size, 0, 2, IS_ARRAY, 0) + ZEND_ARG_TYPE_INFO(0, old_size, IS_LONG, 0) + ZEND_ARG_TYPE_INFO(0, new_size, IS_LONG, 0) +ZEND_END_ARG_INFO() + ZEND_BEGIN_ARG_WITH_RETURN_TYPE_MASK_EX(arginfo_zend_string_or_object, 0, 1, MAY_BE_OBJECT|MAY_BE_STRING) ZEND_ARG_TYPE_MASK(0, param, MAY_BE_OBJECT|MAY_BE_STRING, NULL) ZEND_END_ARG_INFO() @@ -272,6 +277,7 @@ static ZEND_FUNCTION(zend_create_unterminated_string); static ZEND_FUNCTION(zend_terminate_string); static ZEND_FUNCTION(zend_leak_variable); static ZEND_FUNCTION(zend_leak_bytes); +static ZEND_FUNCTION(zend_test_erealloc_block_size); static ZEND_FUNCTION(zend_string_or_object); static ZEND_FUNCTION(zend_string_or_object_or_null); static ZEND_FUNCTION(zend_string_or_stdclass); @@ -380,6 +386,7 @@ static const zend_function_entry ext_functions[] = { ZEND_FE(zend_terminate_string, arginfo_zend_terminate_string) ZEND_FE(zend_leak_variable, arginfo_zend_leak_variable) ZEND_FE(zend_leak_bytes, arginfo_zend_leak_bytes) + ZEND_FE(zend_test_erealloc_block_size, arginfo_zend_test_erealloc_block_size) ZEND_FE(zend_string_or_object, arginfo_zend_string_or_object) ZEND_FE(zend_string_or_object_or_null, arginfo_zend_string_or_object_or_null) ZEND_FE(zend_string_or_stdclass, arginfo_zend_string_or_stdclass)

« previous php.cvs (#142104) next »