diff --git a/NEWS b/NEWS index a0bce4ed6f43..9b7d6afdde82 100644 --- a/NEWS +++ b/NEWS @@ -92,6 +92,11 @@ PHP NEWS (ndossche) . Fixed JIT ignoring exceptions from typed property set hooks. (David Carlier) +- PCNTL: + . Added the $rekey_free_lists parameter to pcntl_fork(), pcntl_rfork() and + pcntl_forkx() to optionally avoid re-keying inherited free lists + (GH-25403). (Alessio Attilio) + - PDO: . Fixed PDOStatement::getColumnMeta() reading out of bounds for an invalid column index. (Ilia Alshanetsky) diff --git a/UPGRADING b/UPGRADING index d9242ecb3c17..6ccd786996e8 100644 --- a/UPGRADING +++ b/UPGRADING @@ -121,6 +121,14 @@ PHP 8.7 UPGRADE NOTES 64 pipe or file handles no longer crashes; it fails with a warning, as that path remains bounded by WaitForMultipleObjects(). +- PCNTL: + . pcntl_fork(), pcntl_rfork() and pcntl_forkx() now accept an optional + $rekey_free_lists argument, which defaults to true. Set it to false to avoid + rewriting the child's inherited small free lists. These free blocks will + not be reused by the child, but per-child allocator key isolation is + preserved. Cached chunks are still re-keyed, and the usual child + initialization is performed. + ======================================== 6. New Functions ======================================== diff --git a/Zend/zend_alloc.c b/Zend/zend_alloc.c index 2b52babc98ee..86e62e7ce9e1 100644 --- a/Zend/zend_alloc.c +++ b/Zend/zend_alloc.c @@ -2264,27 +2264,32 @@ static void zend_mm_init_key(zend_mm_heap *heap) zend_mm_refresh_key(heap); } -ZEND_API void zend_mm_refresh_key_child(zend_mm_heap *heap) +static void zend_mm_refresh_key_child_ex(zend_mm_heap *heap, bool rekey_free_lists) { uintptr_t old_key = heap->shadow_key; zend_mm_init_key(heap); - /* Update shadow pointers with new key */ - for (uint32_t i = 0; i < ZEND_MM_BINS; i++) { - zend_mm_free_slot *slot = heap->free_slot[i]; - if (!slot) { - continue; - } - zend_mm_free_slot *next; - while ((next = slot->next_free_slot)) { - zend_mm_free_slot **shadow_addr = ZEND_MM_FREE_SLOT_PTR_SHADOW_ADDR(slot, i); - if (UNEXPECTED(next != zend_mm_decode_free_slot_key(old_key, shadow_addr, *shadow_addr))) { - zend_mm_panic("zend_mm_heap corrupted"); + if (rekey_free_lists) { + /* Update shadow pointers with new key */ + for (uint32_t i = 0; i < ZEND_MM_BINS; i++) { + zend_mm_free_slot *slot = heap->free_slot[i]; + if (!slot) { + continue; + } + zend_mm_free_slot *next; + while ((next = slot->next_free_slot)) { + zend_mm_free_slot **shadow_addr = ZEND_MM_FREE_SLOT_PTR_SHADOW_ADDR(slot, i); + if (UNEXPECTED(next != zend_mm_decode_free_slot_key(old_key, shadow_addr, *shadow_addr))) { + zend_mm_panic("zend_mm_heap corrupted"); + } + zend_mm_set_next_free_slot(heap, i, slot, next); + slot = next; } - zend_mm_set_next_free_slot(heap, i, slot, next); - slot = next; } + } else { + /* Avoid touching inherited free-list pages in the child. */ + memset(heap->free_slot, 0, sizeof(heap->free_slot)); } zend_mm_rekey_cached_chunks(heap, old_key); @@ -2294,6 +2299,11 @@ ZEND_API void zend_mm_refresh_key_child(zend_mm_heap *heap) #endif } +ZEND_API void zend_mm_refresh_key_child(zend_mm_heap *heap) +{ + zend_mm_refresh_key_child_ex(heap, true); +} + static zend_mm_heap *zend_mm_init(void) { zend_mm_chunk *chunk = (zend_mm_chunk*)zend_mm_chunk_alloc_int(ZEND_MM_CHUNK_SIZE, ZEND_MM_CHUNK_SIZE); @@ -3403,9 +3413,14 @@ ZEND_API void shutdown_memory_manager(bool silent, bool full_shutdown) zend_mm_shutdown(AG(mm_heap), full_shutdown, silent); } +ZEND_API void refresh_memory_manager_ex(bool rekey_free_lists) +{ + zend_mm_refresh_key_child_ex(AG(mm_heap), rekey_free_lists); +} + ZEND_API void refresh_memory_manager(void) { - zend_mm_refresh_key_child(AG(mm_heap)); + refresh_memory_manager_ex(true); } ZEND_NORETURN static ZEND_COLD void zend_out_of_memory(void) diff --git a/Zend/zend_alloc.h b/Zend/zend_alloc.h index a8e5c3fa602d..ea0d12beed27 100644 --- a/Zend/zend_alloc.h +++ b/Zend/zend_alloc.h @@ -269,6 +269,7 @@ ZEND_API bool zend_alloc_in_memory_limit_error_reporting(void); 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); +ZEND_API void refresh_memory_manager_ex(bool rekey_free_lists); ZEND_API bool is_zend_mm(void); ZEND_API bool is_zend_ptr(const void *ptr); diff --git a/ext/pcntl/pcntl.c b/ext/pcntl/pcntl.c index 3492444925ca..df09578f4255 100644 --- a/ext/pcntl/pcntl.c +++ b/ext/pcntl/pcntl.c @@ -268,8 +268,12 @@ PHP_MINFO_FUNCTION(pcntl) PHP_FUNCTION(pcntl_fork) { pid_t id; + bool rekey_free_lists = true; - ZEND_PARSE_PARAMETERS_NONE(); + ZEND_PARSE_PARAMETERS_START(0, 1) + Z_PARAM_OPTIONAL + Z_PARAM_BOOL(rekey_free_lists) + ZEND_PARSE_PARAMETERS_END(); id = fork(); if (id == -1) { @@ -295,7 +299,7 @@ PHP_FUNCTION(pcntl_fork) } } else if (id == 0) { - php_child_init(); + php_child_init_ex(rekey_free_lists); } RETURN_LONG((zend_long) id); @@ -1552,11 +1556,13 @@ PHP_FUNCTION(pcntl_rfork) zend_long flags; zend_long csignal = 0; pid_t pid; + bool rekey_free_lists = true; - ZEND_PARSE_PARAMETERS_START(1, 2) + ZEND_PARSE_PARAMETERS_START(1, 3) Z_PARAM_LONG(flags) Z_PARAM_OPTIONAL Z_PARAM_LONG(csignal) + Z_PARAM_BOOL(rekey_free_lists) ZEND_PARSE_PARAMETERS_END(); /* This is a flag to use with great caution in general, preferably not within PHP */ @@ -1599,7 +1605,7 @@ PHP_FUNCTION(pcntl_rfork) php_error_docref(NULL, E_WARNING, "Error %d", errno); } } else if (pid == 0) { - php_child_init(); + php_child_init_ex(rekey_free_lists); } RETURN_LONG((zend_long) pid); @@ -1616,9 +1622,12 @@ PHP_FUNCTION(pcntl_forkx) { zend_long flags; pid_t pid; + bool rekey_free_lists = true; - ZEND_PARSE_PARAMETERS_START(1, 1) + ZEND_PARSE_PARAMETERS_START(1, 2) Z_PARAM_LONG(flags) + Z_PARAM_OPTIONAL + Z_PARAM_BOOL(rekey_free_lists) ZEND_PARSE_PARAMETERS_END(); if (flags < FORK_NOSIGCHLD || flags > FORK_WAITPID) { @@ -1644,7 +1653,7 @@ PHP_FUNCTION(pcntl_forkx) php_error_docref(NULL, E_WARNING, "Error %d", errno); } } else if (pid == 0) { - php_child_init(); + php_child_init_ex(rekey_free_lists); } RETURN_LONG((zend_long) pid); diff --git a/ext/pcntl/pcntl.stub.php b/ext/pcntl/pcntl.stub.php index 1ab52d025036..8affc892895a 100644 --- a/ext/pcntl/pcntl.stub.php +++ b/ext/pcntl/pcntl.stub.php @@ -1000,7 +1000,7 @@ const PCNTL_ECAPMODE = UNKNOWN; #endif - function pcntl_fork(): int {} + function pcntl_fork(bool $rekey_free_lists = true): int {} /** * @param int $status @@ -1085,11 +1085,11 @@ function pcntl_unshare(int $flags): bool {} #endif #ifdef HAVE_RFORK - function pcntl_rfork(int $flags, int $signal = 0): int{} + function pcntl_rfork(int $flags, int $signal = 0, bool $rekey_free_lists = true): int{} #endif #ifdef HAVE_FORKX - function pcntl_forkx(int $flags): int{} + function pcntl_forkx(int $flags, bool $rekey_free_lists = true): int{} #endif #ifdef HAVE_PIDFD_OPEN diff --git a/ext/pcntl/pcntl_arginfo.h b/ext/pcntl/pcntl_arginfo.h index a9d6bb010e29..16fbed158ce4 100644 --- a/ext/pcntl/pcntl_arginfo.h +++ b/ext/pcntl/pcntl_arginfo.h @@ -1,11 +1,12 @@ /* This is a generated file, edit pcntl.stub.php instead. - * Stub hash: ec6306e93fad6d127ff880fc01736ac287619cf7 + * Stub hash: 8c2273c72be8d137c03c46418913ae31c42e716a * Has decl header: yes */ #include "zend_constants.h" #include "zend_enum.h" ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_pcntl_fork, 0, 0, IS_LONG, 0) + ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, rekey_free_lists, _IS_BOOL, 0, "true") ZEND_END_ARG_INFO() ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_pcntl_waitpid, 0, 2, IS_LONG, 0) @@ -98,9 +99,10 @@ ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_pcntl_alarm, 0, 1, IS_LONG, 0) ZEND_ARG_TYPE_INFO(0, seconds, IS_LONG, 0) ZEND_END_ARG_INFO() -#define arginfo_pcntl_get_last_error arginfo_pcntl_fork +ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_pcntl_get_last_error, 0, 0, IS_LONG, 0) +ZEND_END_ARG_INFO() -#define arginfo_pcntl_errno arginfo_pcntl_fork +#define arginfo_pcntl_errno arginfo_pcntl_get_last_error #if defined(HAVE_GETPRIORITY) ZEND_BEGIN_ARG_WITH_RETURN_TYPE_MASK_EX(arginfo_pcntl_getpriority, 0, 0, MAY_BE_LONG|MAY_BE_FALSE) @@ -135,12 +137,14 @@ ZEND_END_ARG_INFO() ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_pcntl_rfork, 0, 1, IS_LONG, 0) ZEND_ARG_TYPE_INFO(0, flags, IS_LONG, 0) ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, signal, IS_LONG, 0, "0") + ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, rekey_free_lists, _IS_BOOL, 0, "true") ZEND_END_ARG_INFO() #endif #if defined(HAVE_FORKX) ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_pcntl_forkx, 0, 1, IS_LONG, 0) ZEND_ARG_TYPE_INFO(0, flags, IS_LONG, 0) + ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, rekey_free_lists, _IS_BOOL, 0, "true") ZEND_END_ARG_INFO() #endif diff --git a/ext/pcntl/pcntl_decl.h b/ext/pcntl/pcntl_decl.h index a0fdb4dde755..74c1ce620774 100644 --- a/ext/pcntl/pcntl_decl.h +++ b/ext/pcntl/pcntl_decl.h @@ -1,8 +1,8 @@ /* This is a generated file, edit pcntl.stub.php instead. - * Stub hash: ec6306e93fad6d127ff880fc01736ac287619cf7 */ + * Stub hash: 8c2273c72be8d137c03c46418913ae31c42e716a */ -#ifndef ZEND_PCNTL_DECL_ec6306e93fad6d127ff880fc01736ac287619cf7_H -#define ZEND_PCNTL_DECL_ec6306e93fad6d127ff880fc01736ac287619cf7_H +#ifndef ZEND_PCNTL_DECL_8c2273c72be8d137c03c46418913ae31c42e716a_H +#define ZEND_PCNTL_DECL_8c2273c72be8d137c03c46418913ae31c42e716a_H typedef enum zend_enum_Pcntl_QosClass { ZEND_ENUM_Pcntl_QosClass_UserInteractive = 1, @@ -12,4 +12,4 @@ typedef enum zend_enum_Pcntl_QosClass { ZEND_ENUM_Pcntl_QosClass_Background = 5, } zend_enum_Pcntl_QosClass; -#endif /* ZEND_PCNTL_DECL_ec6306e93fad6d127ff880fc01736ac287619cf7_H */ +#endif /* ZEND_PCNTL_DECL_8c2273c72be8d137c03c46418913ae31c42e716a_H */ diff --git a/ext/pcntl/tests/gh25403_cow.phpt b/ext/pcntl/tests/gh25403_cow.phpt new file mode 100644 index 000000000000..e96ce2b07582 --- /dev/null +++ b/ext/pcntl/tests/gh25403_cow.phpt @@ -0,0 +1,32 @@ +--TEST-- +GH-25403: opting out of child memory re-keying avoids inherited free-list writes +--EXTENSIONS-- +pcntl +--SKIPIF-- + +--INI-- +memory_limit=128M +--FILE-- + $value) { + if ($i % 2) unset($strings[$i]); +} +$pid = pcntl_fork(rekey_free_lists: false); +if ($pid === -1) throw new Exception('Fork failed'); +if ($pid === 0) { + var_dump(getrusage()['ru_minflt'] < 2048); + exit(0); +} +if (pcntl_waitpid($pid, $status) !== $pid) throw new Exception('Wait failed'); +var_dump(pcntl_wifexited($status) && pcntl_wexitstatus($status) === 0); +?> +--EXPECT-- +bool(true) +bool(true) diff --git a/ext/pcntl/tests/gh25403_fork.phpt b/ext/pcntl/tests/gh25403_fork.phpt new file mode 100644 index 000000000000..fd5253f7e17a --- /dev/null +++ b/ext/pcntl/tests/gh25403_fork.phpt @@ -0,0 +1,48 @@ +--TEST-- +pcntl_fork() preserves child initialization with optional memory re-keying +--EXTENSIONS-- +pcntl +--SKIPIF-- + +--FILE-- + $value) { + if ($i % 2) unset($objects[$i]); + } + realpath(__FILE__); + $pid = pcntl_fork(rekey_free_lists: $rekey); + if ($pid === -1) throw new Exception('Fork failed'); + if ($pid === 0) { + if (array_key_exists(__FILE__, realpath_cache_get())) exit(1); + for ($i = 0; $i < 8192; $i++) { + $objects[] = str_repeat('y', 128 + $i % 64); + } + gc_mem_caches(); + $pid = pcntl_fork(rekey_free_lists: !$rekey); + if ($pid === -1) exit(1); + if ($pid === 0) { + unset($objects); + gc_mem_caches(); + exit(0); + } + if (pcntl_waitpid($pid, $status) !== $pid) exit(1); + exit(pcntl_wifexited($status) ? pcntl_wexitstatus($status) : 1); + } + var_dump(pcntl_waitpid($pid, $status) === $pid); + var_dump(pcntl_wifexited($status) && pcntl_wexitstatus($status) === 0); +} +checkChild(false); +checkChild(true); +?> +--EXPECT-- +bool(true) +bool(true) +bool(true) +bool(true) diff --git a/ext/pcntl/tests/gh25403_forkx.phpt b/ext/pcntl/tests/gh25403_forkx.phpt new file mode 100644 index 000000000000..e3b8b493364a --- /dev/null +++ b/ext/pcntl/tests/gh25403_forkx.phpt @@ -0,0 +1,48 @@ +--TEST-- +pcntl_forkx() preserves child initialization with optional memory re-keying +--EXTENSIONS-- +pcntl +--SKIPIF-- + +--FILE-- + $value) { + if ($i % 2) unset($objects[$i]); + } + realpath(__FILE__); + $pid = pcntl_forkx(FORK_WAITPID, rekey_free_lists: $rekey); + if ($pid === -1) throw new Exception('Fork failed'); + if ($pid === 0) { + if (array_key_exists(__FILE__, realpath_cache_get())) exit(1); + for ($i = 0; $i < 8192; $i++) { + $objects[] = str_repeat('y', 128 + $i % 64); + } + gc_mem_caches(); + $pid = pcntl_fork(rekey_free_lists: !$rekey); + if ($pid === -1) exit(1); + if ($pid === 0) { + unset($objects); + gc_mem_caches(); + exit(0); + } + if (pcntl_waitpid($pid, $status) !== $pid) exit(1); + exit(pcntl_wifexited($status) ? pcntl_wexitstatus($status) : 1); + } + var_dump(pcntl_waitpid($pid, $status) === $pid); + var_dump(pcntl_wifexited($status) && pcntl_wexitstatus($status) === 0); +} +checkChild(false); +checkChild(true); +?> +--EXPECT-- +bool(true) +bool(true) +bool(true) +bool(true) diff --git a/ext/pcntl/tests/gh25403_rfork.phpt b/ext/pcntl/tests/gh25403_rfork.phpt new file mode 100644 index 000000000000..24e59998c150 --- /dev/null +++ b/ext/pcntl/tests/gh25403_rfork.phpt @@ -0,0 +1,48 @@ +--TEST-- +pcntl_rfork() preserves child initialization with optional memory re-keying +--EXTENSIONS-- +pcntl +--SKIPIF-- + +--FILE-- + $value) { + if ($i % 2) unset($objects[$i]); + } + realpath(__FILE__); + $pid = pcntl_rfork(0, 0, rekey_free_lists: $rekey); + if ($pid === -1) throw new Exception('Fork failed'); + if ($pid === 0) { + if (array_key_exists(__FILE__, realpath_cache_get())) exit(1); + for ($i = 0; $i < 8192; $i++) { + $objects[] = str_repeat('y', 128 + $i % 64); + } + gc_mem_caches(); + $pid = pcntl_fork(rekey_free_lists: !$rekey); + if ($pid === -1) exit(1); + if ($pid === 0) { + unset($objects); + gc_mem_caches(); + exit(0); + } + if (pcntl_waitpid($pid, $status) !== $pid) exit(1); + exit(pcntl_wifexited($status) ? pcntl_wexitstatus($status) : 1); + } + var_dump(pcntl_waitpid($pid, $status) === $pid); + var_dump(pcntl_wifexited($status) && pcntl_wexitstatus($status) === 0); +} +checkChild(false); +checkChild(true); +?> +--EXPECT-- +bool(true) +bool(true) +bool(true) +bool(true) diff --git a/main/main.c b/main/main.c index 962739df4219..bd4386f2cb2c 100644 --- a/main/main.c +++ b/main/main.c @@ -1863,9 +1863,9 @@ static void sigchld_handler(int apar) /* }}} */ #endif -PHPAPI void php_child_init(void) +PHPAPI void php_child_init_ex(bool rekey_free_lists) { - refresh_memory_manager(); + refresh_memory_manager_ex(rekey_free_lists); zend_max_execution_timer_init(); php_clear_stat_cache(true, NULL, 0); @@ -1876,6 +1876,11 @@ PHPAPI void php_child_init(void) } ZEND_HASH_FOREACH_END(); } +PHPAPI void php_child_init(void) +{ + php_child_init_ex(true); +} + /* {{{ php_request_startup */ zend_result php_request_startup(void) { diff --git a/main/php_main.h b/main/php_main.h index de2ace1c25ed..008a761c542e 100644 --- a/main/php_main.h +++ b/main/php_main.h @@ -48,6 +48,7 @@ PHPAPI char *php_get_version(sapi_module_struct *sapi_module); PHPAPI void php_print_version(sapi_module_struct *sapi_module); PHPAPI void php_child_init(void); +PHPAPI void php_child_init_ex(bool rekey_free_lists); PHPAPI zend_result php_request_startup(void); PHPAPI void php_request_shutdown(void *dummy); PHPAPI zend_result php_module_startup(sapi_module_struct *sf, zend_module_entry *additional_module);