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-- +