diff --git a/UPGRADING.INTERNALS b/UPGRADING.INTERNALS index 4c5d74b49e9c..7990997ad792 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. Use + STANDARD_MODULE_PROPERTIES_EX_WITH_CHILD_INIT(callback) to initialize it. + Modules without a callback continue to use 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..222295582db5 100644 --- a/Zend/zend_modules.h +++ b/Zend/zend_modules.h @@ -30,7 +30,7 @@ #define ZEND_MODULE_INFO_FUNC_ARGS zend_module_entry *zend_module #define ZEND_MODULE_INFO_FUNC_ARGS_PASSTHRU zend_module -#define ZEND_MODULE_API_NO 20260925 +#define ZEND_MODULE_API_NO 20261024 #ifdef ZTS #define USING_ZTS 1 #else @@ -45,7 +45,11 @@ #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_WITH_CHILD_INIT(child_init) \ + 0, 0, NULL, 0, ZEND_MODULE_BUILD_ID, child_init + +#define STANDARD_MODULE_PROPERTIES_EX \ + STANDARD_MODULE_PROPERTIES_EX_WITH_CHILD_INIT(NULL) #define NO_MODULE_GLOBALS 0, NULL, NULL, NULL @@ -96,6 +100,7 @@ struct _zend_module_entry { void *handle; int module_number; const char *build_id; + void (*child_init_func)(void); }; #define MODULE_DEP_REQUIRED 1 diff --git a/ext/session/php_session.h b/ext/session/php_session.h index e2b7a7e5a7d3..aa20422f8122 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 12c4ae57fdcc..9d7a49a9f984 100644 --- a/ext/session/session.c +++ b/ext/session/session.c @@ -400,18 +400,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); } @@ -3010,7 +2998,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 @@ -3445,6 +3441,13 @@ static zend_result php_session_rfc1867_callback(unsigned int event, void *event_ return retval; } +static void php_session_child_init(void) +{ + /* 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", @@ -3457,7 +3460,7 @@ zend_module_entry session_module_entry = { PHP_GINIT(ps), NULL, NULL, - STANDARD_MODULE_PROPERTIES_EX + STANDARD_MODULE_PROPERTIES_EX_WITH_CHILD_INIT(php_session_child_init) }; #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 7b5180d1a731..e0ff41662814 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 0658f6a3cf43..a67a17f432f7 100644 --- a/ext/zend_test/test.c +++ b/ext/zend_test/test.c @@ -1603,6 +1603,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; @@ -2495,6 +2502,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", @@ -2509,7 +2521,7 @@ zend_module_entry zend_test_module_entry = { PHP_GINIT(zend_test), PHP_GSHUTDOWN(zend_test), NULL, - STANDARD_MODULE_PROPERTIES_EX + STANDARD_MODULE_PROPERTIES_EX_WITH_CHILD_INIT(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 db5ca629ade2..9ac6d7be701e 100644 --- a/ext/zend_test/test.stub.php +++ b/ext/zend_test/test.stub.php @@ -280,6 +280,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 87622c185cff..20616b20dda0 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: 1b25893580205fc2db824a265839da14f5d2f0f0 * Has decl header: yes */ #include "zend_attributes.h" @@ -37,6 +37,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 @@ -457,6 +459,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); @@ -629,6 +632,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 db7652512c83..87d1cc26286d 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: 1b25893580205fc2db824a265839da14f5d2f0f0 */ -#ifndef ZEND_TEST_DECL_ea2674e5d5dbf60aa5efab31132064b52a07bddd_H -#define ZEND_TEST_DECL_ea2674e5d5dbf60aa5efab31132064b52a07bddd_H +#ifndef ZEND_TEST_DECL_1b25893580205fc2db824a265839da14f5d2f0f0_H +#define ZEND_TEST_DECL_1b25893580205fc2db824a265839da14f5d2f0f0_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_1b25893580205fc2db824a265839da14f5d2f0f0_H */ diff --git a/ext/zend_test/test_legacy_arginfo.h b/ext/zend_test/test_legacy_arginfo.h index a673b373142a..7b5efcf05218 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: 1b25893580205fc2db824a265839da14f5d2f0f0 * Has decl header: yes */ #include "zend_constants.h" @@ -32,6 +32,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 @@ -381,6 +383,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); @@ -525,6 +528,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 1e4acfc8aa98..3508df8db788 100644 --- a/main/main.c +++ b/main/main.c @@ -1844,9 +1844,17 @@ static void sigchld_handler(int apar) PHPAPI void php_child_init(void) { + zend_module_entry *module; + refresh_memory_manager(); zend_max_execution_timer_init(); php_clear_stat_cache(true, NULL, 0); + + ZEND_HASH_MAP_FOREACH_PTR(&module_registry, module) { + if (module->module_started && module->child_init_func) { + module->child_init_func(); + } + } ZEND_HASH_FOREACH_END(); } /* {{{ php_request_startup */ 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-- +