From 3d5da1aa0b4f85e79250a9e95daaa57ddb9897df Mon Sep 17 00:00:00 2001 From: Alessio Attilio Date: Mon, 9 Nov 2026 13:04:53 +0100 Subject: [PATCH] Add per-extension child initialization hooks Fixes GH-22616. Add zend_module_entry.child_init_func and invoke it from php_child_init() for started modules in dependency order after core child initialization. Expose PHP_CINIT/PHP_CINIT_FUNCTION macros and use named initializers for the touched module properties. In ext/session, seed GC randomness during GINIT and advance the PRNG state in the child init hook so forked children do not share the same GC stream. Keep session GC state in TLS globals. Add zend_test coverage, a session fork regression test, and an FPM worker recycling test. Regenerate zend_test arginfo. --- UPGRADING.INTERNALS | 6 ++ Zend/zend_modules.h | 8 ++- ext/session/php_session.h | 2 - ext/session/session.c | 32 +++++---- ext/session/tests/gh22616.phpt | 99 ++++++++++++++++++++++++++ ext/zend_test/php_test.h | 1 + ext/zend_test/test.c | 15 +++- ext/zend_test/test.stub.php | 2 + ext/zend_test/test_arginfo.h | 6 +- ext/zend_test/test_decl.h | 8 +-- ext/zend_test/test_legacy_arginfo.h | 6 +- ext/zend_test/tests/child_init.phpt | 39 ++++++++++ main/main.c | 6 ++ main/php.h | 2 + sapi/fpm/tests/gh22616-child-init.phpt | 43 +++++++++++ 15 files changed, 251 insertions(+), 24 deletions(-) create mode 100644 ext/session/tests/gh22616.phpt create mode 100644 ext/zend_test/tests/child_init.phpt create mode 100644 sapi/fpm/tests/gh22616-child-init.phpt diff --git a/UPGRADING.INTERNALS b/UPGRADING.INTERNALS index b5f75747ef73..bf38a489139e 100644 --- a/UPGRADING.INTERNALS +++ b/UPGRADING.INTERNALS @@ -14,6 +14,12 @@ PHP 8.7 INTERNALS UPGRADE NOTES 1. Internal API changes ======================== +- Added zend_module_entry.child_init_func, an optional void callback invoked by + php_child_init() in the child process after core child initialization. It runs + in module dependency order for started modules and may run before a request + starts or during an active request. Initialize it with a designated + initializer such as `.child_init_func = callback` after + STANDARD_MODULE_PROPERTIES_EX. - Removed the ZEND_LOG_XOR() macro. - Removed the ZEND_TRUTH() macro. - Removed zend_execute_scripts(). Manually call zend_execute_script() in a loop diff --git a/Zend/zend_modules.h b/Zend/zend_modules.h index bb3050927992..018ad5bbc147 100644 --- a/Zend/zend_modules.h +++ b/Zend/zend_modules.h @@ -45,7 +45,12 @@ #define ZEND_MODULE_BUILD_ID "API" ZEND_TOSTR(ZEND_MODULE_API_NO) ZEND_BUILD_TS ZEND_BUILD_DEBUG ZEND_BUILD_SYSTEM ZEND_BUILD_EXTRA -#define STANDARD_MODULE_PROPERTIES_EX 0, 0, NULL, 0, ZEND_MODULE_BUILD_ID +#define STANDARD_MODULE_PROPERTIES_EX \ + .module_started = 0, \ + .type = 0, \ + .handle = NULL, \ + .module_number = 0, \ + .build_id = ZEND_MODULE_BUILD_ID #define NO_MODULE_GLOBALS 0, NULL, NULL, NULL @@ -91,6 +96,7 @@ struct _zend_module_entry { void (*globals_ctor)(void *global); void (*globals_dtor)(void *global); zend_result (*post_deactivate_func)(void); + void (*child_init_func)(void); int module_started; unsigned char type; void *handle; diff --git a/ext/session/php_session.h b/ext/session/php_session.h index ee36eb291e52..7711d9432dc3 100644 --- a/ext/session/php_session.h +++ b/ext/session/php_session.h @@ -183,8 +183,6 @@ typedef struct _php_ps_globals { bool mod_user_is_open; bool mod_user_uses_object_methods_as_handlers; bool use_trans_sid; /* contains the INI value of whether to use trans-sid */ - bool random_seeded; - bool in_rfc1867_update; /* php_session_rfc1867_update() is running */ } php_ps_globals; diff --git a/ext/session/session.c b/ext/session/session.c index 702b56020ae9..c15941b74523 100644 --- a/ext/session/session.c +++ b/ext/session/session.c @@ -409,18 +409,6 @@ static zend_long php_session_gc(bool immediate) if ((PS(mod_data) || PS(mod_user_implemented))) { /* Use probability-based GC if not forced and probability is configured */ if (!collect && PS(gc_probability) > 0) { - /* Seed lazily on first GC draw per process. */ - if (UNEXPECTED(!PS(random_seeded))) { - php_random_uint128_t seed; - if (php_random_bytes_silent(&seed, sizeof(seed)) == FAILURE) { - seed = php_random_uint128_constant( - php_random_generate_fallback_seed(), - php_random_generate_fallback_seed() - ); - } - php_random_pcgoneseq128xslrr64_seed128(PS(random).state, seed); - PS(random_seeded) = true; - } collect = php_random_range(PS(random), 0, PS(gc_divisor) - 1) < PS(gc_probability); } @@ -3019,7 +3007,15 @@ static PHP_GINIT_FUNCTION(ps) .algo = &php_random_algo_pcgoneseq128xslrr64, .state = &ps_globals->random_state, }; - ps_globals->random_seeded = false; + + php_random_uint128_t seed; + if (php_random_bytes_silent(&seed, sizeof(seed)) == FAILURE) { + seed = php_random_uint128_constant( + php_random_generate_fallback_seed(), + php_random_generate_fallback_seed() + ); + } + php_random_pcgoneseq128xslrr64_seed128(ps_globals->random.state, seed); } /* Interfaces extending the given one are not flattened into ce->interfaces before they are @@ -3454,6 +3450,13 @@ static zend_result php_session_rfc1867_callback(unsigned int event, void *event_ return retval; } +PHP_CINIT_FUNCTION(session) +{ + /* Give each child a distinct, widely separated GC sequence without requesting new entropy. */ + uint64_t advance = (uint64_t) getpid() << 32; + php_random_pcgoneseq128xslrr64_advance(PS(random).state, advance); +} + zend_module_entry session_module_entry = { STANDARD_MODULE_HEADER, "session", @@ -3466,7 +3469,8 @@ zend_module_entry session_module_entry = { PHP_GINIT(ps), NULL, NULL, - STANDARD_MODULE_PROPERTIES_EX + STANDARD_MODULE_PROPERTIES_EX, + .child_init_func = PHP_CINIT(session) }; #ifdef COMPILE_DL_SESSION diff --git a/ext/session/tests/gh22616.phpt b/ext/session/tests/gh22616.phpt new file mode 100644 index 000000000000..13235dede567 --- /dev/null +++ b/ext/session/tests/gh22616.phpt @@ -0,0 +1,99 @@ +--TEST-- +GH-22616: Session GC randomness is independent after a fork +--EXTENSIONS-- +session +pcntl +--INI-- +session.gc_probability=1 +session.gc_divisor=2 +session.use_cookies=0 +session.cache_limiter= +--FILE-- +collected = true; + return 0; + } +}; +session_set_save_handler($handler); +session_id('gh22616'); + +function sequence(): string { + global $handler; + if (session_status() === PHP_SESSION_ACTIVE) { + session_write_close(); + } + $result = ''; + for ($i = 0; $i < 256; $i++) { + $handler->collected = false; + session_start(); + session_write_close(); + $result .= (int) $handler->collected; + } + return $result; +} + +$results = []; +foreach (['unseeded', 'seeded', 'active'] as $state) { + if ($state === 'active') { + session_start(); + } + $file1 = tempnam(sys_get_temp_dir(), 'gh22616'); + $pid1 = pcntl_fork(); + if ($pid1 === -1) { + throw new Exception('fork failed'); + } + if ($pid1 === 0) { + file_put_contents($file1, sequence()); + exit; + } + + $file2 = tempnam(sys_get_temp_dir(), 'gh22616'); + $pid2 = pcntl_fork(); + if ($pid2 === -1) { + throw new Exception('fork failed'); + } + if ($pid2 === 0) { + file_put_contents($file2, sequence()); + exit; + } + + $parent = sequence(); + pcntl_waitpid($pid1, $status1); + pcntl_waitpid($pid2, $status2); + $child1 = file_get_contents($file1); + $child2 = file_get_contents($file2); + unlink($file1); + unlink($file2); + $results[$state] = pcntl_wifexited($status1) + && pcntl_wexitstatus($status1) === 0 + && pcntl_wifexited($status2) + && pcntl_wexitstatus($status2) === 0 + && strlen($child1) === 256 + && strlen($child2) === 256 + && $parent !== $child1 + && $parent !== $child2 + && $child1 !== $child2; +} +var_dump($results); +?> +--EXPECT-- +array(3) { + ["unseeded"]=> + bool(true) + ["seeded"]=> + bool(true) + ["active"]=> + bool(true) +} diff --git a/ext/zend_test/php_test.h b/ext/zend_test/php_test.h index db6feede7caf..34f6d7c30bfe 100644 --- a/ext/zend_test/php_test.h +++ b/ext/zend_test/php_test.h @@ -29,6 +29,7 @@ ZEND_TSRMLS_CACHE_EXTERN() #endif ZEND_BEGIN_MODULE_GLOBALS(zend_test) + zend_long child_init_count; int observer_enabled; int observer_show_output; int observer_observe_all; diff --git a/ext/zend_test/test.c b/ext/zend_test/test.c index f3a8076a456b..307ec19bbe5e 100644 --- a/ext/zend_test/test.c +++ b/ext/zend_test/test.c @@ -1669,6 +1669,13 @@ static PHP_INI_MH(OnUpdateZendTestObserveOplineInZendMM) return OnUpdateBool(entry, new_value, mh_arg1, mh_arg2, mh_arg3, stage); } +static ZEND_FUNCTION(zend_test_child_init_count) +{ + ZEND_PARSE_PARAMETERS_NONE(); + + RETURN_LONG(ZT_G(child_init_count)); +} + static ZEND_FUNCTION(zend_test_fill_packed_array) { HashTable *parameter; @@ -2609,6 +2616,11 @@ PHP_MINFO_FUNCTION(zend_test) DISPLAY_INI_ENTRIES(); } +static void zend_test_child_init(void) +{ + ZT_G(child_init_count)++; +} + zend_module_entry zend_test_module_entry = { STANDARD_MODULE_HEADER, "zend_test", @@ -2623,7 +2635,8 @@ zend_module_entry zend_test_module_entry = { PHP_GINIT(zend_test), PHP_GSHUTDOWN(zend_test), NULL, - STANDARD_MODULE_PROPERTIES_EX + STANDARD_MODULE_PROPERTIES_EX, + .child_init_func = zend_test_child_init }; #ifdef COMPILE_DL_ZEND_TEST diff --git a/ext/zend_test/test.stub.php b/ext/zend_test/test.stub.php index 96f4a599e3d2..87ad8c9b93c8 100644 --- a/ext/zend_test/test.stub.php +++ b/ext/zend_test/test.stub.php @@ -282,6 +282,8 @@ function zend_test_nodiscard(): int {} #[\NoDiscard(message: "custom message 2")] function zend_test_deprecated_nodiscard(): int {} + function zend_test_child_init_count(): int {} + /** @alias zend_test_void_return */ function zend_test_aliased(): void {} diff --git a/ext/zend_test/test_arginfo.h b/ext/zend_test/test_arginfo.h index 9ad59954eae7..314d66a2799a 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: 9d0e693b29e9e11d82404d486c9b2c2dc81ca1c7 + * Stub hash: 64eb27c89a9a5a378385e5f778cd17cfd67caed8 * Has decl header: yes */ #include "zend_attributes.h" @@ -41,6 +41,8 @@ ZEND_END_ARG_INFO() #define arginfo_zend_test_deprecated_nodiscard arginfo_zend_test_nodiscard +#define arginfo_zend_test_child_init_count arginfo_zend_test_nodiscard + #define arginfo_zend_test_aliased arginfo_zend_test_void_return #define arginfo_zend_test_deprecated_aliased arginfo_zend_test_void_return @@ -479,6 +481,7 @@ static ZEND_FUNCTION(zend_test_deprecated); static ZEND_FUNCTION(zend_test_deprecated_attr); static ZEND_FUNCTION(zend_test_nodiscard); static ZEND_FUNCTION(zend_test_deprecated_nodiscard); +static ZEND_FUNCTION(zend_test_child_init_count); static ZEND_FUNCTION(zend_create_unterminated_string); static ZEND_FUNCTION(zend_terminate_string); static ZEND_FUNCTION(zend_leak_variable); @@ -656,6 +659,7 @@ static const zend_function_entry ext_functions[] = { #else ZEND_RAW_FENTRY("zend_test_deprecated_nodiscard", zif_zend_test_deprecated_nodiscard, arginfo_zend_test_deprecated_nodiscard, ZEND_ACC_DEPRECATED) #endif + ZEND_FE(zend_test_child_init_count, arginfo_zend_test_child_init_count) #if (PHP_VERSION_ID >= 80400) ZEND_RAW_FENTRY("zend_test_aliased", zif_zend_test_void_return, arginfo_zend_test_aliased, 0, NULL, NULL) #else diff --git a/ext/zend_test/test_decl.h b/ext/zend_test/test_decl.h index b38ffe98827d..494e097fa576 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: 9d0e693b29e9e11d82404d486c9b2c2dc81ca1c7 */ + * Stub hash: 64eb27c89a9a5a378385e5f778cd17cfd67caed8 */ -#ifndef ZEND_TEST_DECL_9d0e693b29e9e11d82404d486c9b2c2dc81ca1c7_H -#define ZEND_TEST_DECL_9d0e693b29e9e11d82404d486c9b2c2dc81ca1c7_H +#ifndef ZEND_TEST_DECL_64eb27c89a9a5a378385e5f778cd17cfd67caed8_H +#define ZEND_TEST_DECL_64eb27c89a9a5a378385e5f778cd17cfd67caed8_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_9d0e693b29e9e11d82404d486c9b2c2dc81ca1c7_H */ +#endif /* ZEND_TEST_DECL_64eb27c89a9a5a378385e5f778cd17cfd67caed8_H */ diff --git a/ext/zend_test/test_legacy_arginfo.h b/ext/zend_test/test_legacy_arginfo.h index ef7de4d062fd..f941cf44c573 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: 9d0e693b29e9e11d82404d486c9b2c2dc81ca1c7 + * Stub hash: 64eb27c89a9a5a378385e5f778cd17cfd67caed8 * Has decl header: yes */ #include "zend_constants.h" @@ -36,6 +36,8 @@ ZEND_END_ARG_INFO() #define arginfo_zend_test_deprecated_nodiscard arginfo_zend_trigger_bailout +#define arginfo_zend_test_child_init_count arginfo_zend_trigger_bailout + #define arginfo_zend_test_aliased arginfo_zend_trigger_bailout #define arginfo_zend_test_deprecated_aliased arginfo_zend_trigger_bailout @@ -403,6 +405,7 @@ static ZEND_FUNCTION(zend_test_deprecated); static ZEND_FUNCTION(zend_test_deprecated_attr); static ZEND_FUNCTION(zend_test_nodiscard); static ZEND_FUNCTION(zend_test_deprecated_nodiscard); +static ZEND_FUNCTION(zend_test_child_init_count); static ZEND_FUNCTION(zend_create_unterminated_string); static ZEND_FUNCTION(zend_terminate_string); static ZEND_FUNCTION(zend_leak_variable); @@ -552,6 +555,7 @@ static const zend_function_entry ext_functions[] = { ZEND_FE(zend_test_deprecated_attr, arginfo_zend_test_deprecated_attr) ZEND_FE(zend_test_nodiscard, arginfo_zend_test_nodiscard) ZEND_FE(zend_test_deprecated_nodiscard, arginfo_zend_test_deprecated_nodiscard) + ZEND_FE(zend_test_child_init_count, arginfo_zend_test_child_init_count) #if (PHP_VERSION_ID >= 80400) ZEND_RAW_FENTRY("zend_test_aliased", zif_zend_test_void_return, arginfo_zend_test_aliased, 0, NULL, NULL) #else diff --git a/ext/zend_test/tests/child_init.phpt b/ext/zend_test/tests/child_init.phpt new file mode 100644 index 000000000000..60fd93ff8101 --- /dev/null +++ b/ext/zend_test/tests/child_init.phpt @@ -0,0 +1,39 @@ +--TEST-- +GH-22616: Extension child initialization runs once per fork in the child +--EXTENSIONS-- +zend_test +pcntl +--FILE-- + +--EXPECT-- +int(0) +int(1) +int(2) +bool(true) +int(1) +bool(true) +int(0) diff --git a/main/main.c b/main/main.c index a8cdff9ff688..9029aab26e22 100644 --- a/main/main.c +++ b/main/main.c @@ -1847,6 +1847,12 @@ PHPAPI void php_child_init(void) refresh_memory_manager(); zend_max_execution_timer_init(); php_clear_stat_cache(true, NULL, 0); + + ZEND_HASH_MAP_FOREACH_PTR(&module_registry, zend_module_entry *module) { + if (module->module_started && module->child_init_func) { + module->child_init_func(); + } + } ZEND_HASH_FOREACH_END(); } /* {{{ php_request_startup */ diff --git a/main/php.h b/main/php.h index 2b72f04d604f..3195ce77c07e 100644 --- a/main/php.h +++ b/main/php.h @@ -369,6 +369,7 @@ END_EXTERN_C() #define PHP_MINFO ZEND_MODULE_INFO_N #define PHP_GINIT ZEND_GINIT #define PHP_GSHUTDOWN ZEND_GSHUTDOWN +#define PHP_CINIT(module) php_##module##_child_init #define PHP_MINIT_FUNCTION ZEND_MODULE_STARTUP_D #define PHP_MSHUTDOWN_FUNCTION ZEND_MODULE_SHUTDOWN_D @@ -377,6 +378,7 @@ END_EXTERN_C() #define PHP_MINFO_FUNCTION ZEND_MODULE_INFO_D #define PHP_GINIT_FUNCTION ZEND_GINIT_FUNCTION #define PHP_GSHUTDOWN_FUNCTION ZEND_GSHUTDOWN_FUNCTION +#define PHP_CINIT_FUNCTION(module) static void php_##module##_child_init(void) #define PHP_MODULE_GLOBALS ZEND_MODULE_GLOBALS diff --git a/sapi/fpm/tests/gh22616-child-init.phpt b/sapi/fpm/tests/gh22616-child-init.phpt new file mode 100644 index 000000000000..e9acab083950 --- /dev/null +++ b/sapi/fpm/tests/gh22616-child-init.phpt @@ -0,0 +1,43 @@ +--TEST-- +GH-22616: Extension child initialization runs before FPM requests +--EXTENSIONS-- +zend_test +--SKIPIF-- + +--FILE-- +start(extensions: ['zend_test']); +$tester->expectLogStartNotices(); +for ($i = 0; $i < 5; $i++) { + $tester->request()->expectBody('1'); +} +$tester->terminate(); +$tester->expectLogTerminatingNotices(); +$tester->close(); +?> +Done +--EXPECT-- +Done +--CLEAN-- +