From 015efae50d29de33a5cef6c6e5e377a8545099f6 Mon Sep 17 00:00:00 2001 From: Alessio Attilio Date: Fri, 13 Nov 2026 01:11:29 +0100 Subject: [PATCH] Make PCNTL child memory re-keying optional Closes GH-25403. --- NEWS | 5 ++++ Zend/zend_alloc.c | 22 ++++++++++++-- Zend/zend_alloc.h | 1 + ext/pcntl/README.md | 21 +++++++++++++ ext/pcntl/pcntl.c | 21 +++++++++---- ext/pcntl/pcntl.stub.php | 6 ++-- ext/pcntl/pcntl_arginfo.h | 10 +++++-- ext/pcntl/pcntl_decl.h | 8 ++--- ext/pcntl/tests/gh25403_cow.phpt | 32 ++++++++++++++++++++ ext/pcntl/tests/gh25403_fork.phpt | 48 ++++++++++++++++++++++++++++++ ext/pcntl/tests/gh25403_forkx.phpt | 48 ++++++++++++++++++++++++++++++ ext/pcntl/tests/gh25403_rfork.phpt | 48 ++++++++++++++++++++++++++++++ main/main.c | 9 ++++-- main/php_main.h | 1 + 14 files changed, 260 insertions(+), 20 deletions(-) create mode 100644 ext/pcntl/README.md create mode 100644 ext/pcntl/tests/gh25403_cow.phpt create mode 100644 ext/pcntl/tests/gh25403_fork.phpt create mode 100644 ext/pcntl/tests/gh25403_forkx.phpt create mode 100644 ext/pcntl/tests/gh25403_rfork.phpt diff --git a/NEWS b/NEWS index a0bce4ed6f43..e00ba0609b05 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_memory parameter to pcntl_fork(), pcntl_rfork() and + pcntl_forkx() to optionally avoid re-keying inherited allocator lists + (GH-25403). (Alessio Attilio) + - PDO: . Fixed PDOStatement::getColumnMeta() reading out of bounds for an invalid column index. (Ilia Alshanetsky) diff --git a/Zend/zend_alloc.c b/Zend/zend_alloc.c index 2b52babc98ee..968511abdc92 100644 --- a/Zend/zend_alloc.c +++ b/Zend/zend_alloc.c @@ -2264,8 +2264,16 @@ 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) { + if (!rekey) { + memset(&heap->rand_state, 0, sizeof(heap->rand_state)); +#if ZEND_DEBUG + heap->pid = getpid(); +#endif + return; + } + uintptr_t old_key = heap->shadow_key; zend_mm_init_key(heap); @@ -2294,6 +2302,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 +3416,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) +{ + zend_mm_refresh_key_child_ex(AG(mm_heap), rekey); +} + 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..663d2e0441b2 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); ZEND_API bool is_zend_mm(void); ZEND_API bool is_zend_ptr(const void *ptr); diff --git a/ext/pcntl/README.md b/ext/pcntl/README.md new file mode 100644 index 000000000000..1de462abb302 --- /dev/null +++ b/ext/pcntl/README.md @@ -0,0 +1,21 @@ +# Memory re-keying after fork + +`pcntl_fork(bool $rekey_memory = true)`, +`pcntl_rfork(int $flags, int $signal = 0, bool $rekey_memory = true)` and +`pcntl_forkx(int $flags, bool $rekey_memory = true)` refresh the allocator's +shadow key in the child by default. This requires traversing and writing the +inherited free lists and cached chunk list. A fragmented heap can therefore +cause substantial child startup time and copy-on-write memory overhead. + +Passing `rekey_memory: false` skips that traversal and retains the inherited +shadow key. Allocator links remain protected, but the parent and children share +the key: disclosure of that key in one process compromises this protection in +other processes that retain it. Callers must weigh this reduced isolation +against the startup and memory savings. The default preserves per-child key +isolation. + +Both modes reset the allocator's random generator state so future key refreshes +use fresh randomness. Both also update allocator process bookkeeping and perform +the usual child initialization, including timer initialization, stat and realpath +cache clearing, and extension child initialization hooks. The setting applies to +one fork; subsequent forks use their own argument, defaulting to `true`. diff --git a/ext/pcntl/pcntl.c b/ext/pcntl/pcntl.c index 3492444925ca..4f0d2e961803 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_memory = true; - ZEND_PARSE_PARAMETERS_NONE(); + ZEND_PARSE_PARAMETERS_START(0, 1) + Z_PARAM_OPTIONAL + Z_PARAM_BOOL(rekey_memory) + 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_memory); } 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_memory = 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_memory) 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_memory); } RETURN_LONG((zend_long) pid); @@ -1616,9 +1622,12 @@ PHP_FUNCTION(pcntl_forkx) { zend_long flags; pid_t pid; + bool rekey_memory = true; - ZEND_PARSE_PARAMETERS_START(1, 1) + ZEND_PARSE_PARAMETERS_START(1, 2) Z_PARAM_LONG(flags) + Z_PARAM_OPTIONAL + Z_PARAM_BOOL(rekey_memory) 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_memory); } RETURN_LONG((zend_long) pid); diff --git a/ext/pcntl/pcntl.stub.php b/ext/pcntl/pcntl.stub.php index 1ab52d025036..f7ce0c524490 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_memory = 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_memory = true): int{} #endif #ifdef HAVE_FORKX - function pcntl_forkx(int $flags): int{} + function pcntl_forkx(int $flags, bool $rekey_memory = true): int{} #endif #ifdef HAVE_PIDFD_OPEN diff --git a/ext/pcntl/pcntl_arginfo.h b/ext/pcntl/pcntl_arginfo.h index a9d6bb010e29..4cf6fb257083 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_memory, _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_memory, _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_memory, _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..e6429ce8418a --- /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_memory: 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..1bf657e82948 --- /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_memory: $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_memory: !$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..4557a6a68e3a --- /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_memory: $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_memory: !$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..ba1ef125c753 --- /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_memory: $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_memory: !$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..f2c0a81c846e 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_memory) { - refresh_memory_manager(); + refresh_memory_manager_ex(rekey_memory); 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..8f7d62dc0ff2 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_memory); 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);