Skip to content

Commit e5c2efb

Browse files
committed
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.
1 parent a11e54e commit e5c2efb

15 files changed

Lines changed: 253 additions & 24 deletions

File tree

‎UPGRADING.INTERNALS‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,12 @@ PHP 8.7 INTERNALS UPGRADE NOTES
1414
1. Internal API changes
1515
========================
1616

17+
- Added zend_module_entry.child_init_func, an optional void callback invoked by
18+
php_child_init() in the child process after core child initialization. It runs
19+
in module dependency order for started modules and may run before a request
20+
starts or during an active request. Initialize it with a designated
21+
initializer such as `.child_init_func = callback` after
22+
STANDARD_MODULE_PROPERTIES_EX.
1723
- Removed the ZEND_LOG_XOR() macro.
1824
- Removed the ZEND_TRUTH() macro.
1925
- Removed zend_execute_scripts(). Manually call zend_execute_script() in a loop

‎Zend/zend_modules.h‎

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,12 @@
4545

4646
#define ZEND_MODULE_BUILD_ID "API" ZEND_TOSTR(ZEND_MODULE_API_NO) ZEND_BUILD_TS ZEND_BUILD_DEBUG ZEND_BUILD_SYSTEM ZEND_BUILD_EXTRA
4747

48-
#define STANDARD_MODULE_PROPERTIES_EX 0, 0, NULL, 0, ZEND_MODULE_BUILD_ID
48+
#define STANDARD_MODULE_PROPERTIES_EX \
49+
.module_started = 0, \
50+
.type = 0, \
51+
.handle = NULL, \
52+
.module_number = 0, \
53+
.build_id = ZEND_MODULE_BUILD_ID
4954

5055
#define NO_MODULE_GLOBALS 0, NULL, NULL, NULL
5156

@@ -91,6 +96,7 @@ struct _zend_module_entry {
9196
void (*globals_ctor)(void *global);
9297
void (*globals_dtor)(void *global);
9398
zend_result (*post_deactivate_func)(void);
99+
void (*child_init_func)(void);
94100
int module_started;
95101
unsigned char type;
96102
void *handle;

‎ext/session/php_session.h‎

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -183,8 +183,6 @@ typedef struct _php_ps_globals {
183183
bool mod_user_is_open;
184184
bool mod_user_uses_object_methods_as_handlers;
185185
bool use_trans_sid; /* contains the INI value of whether to use trans-sid */
186-
bool random_seeded;
187-
188186
bool in_rfc1867_update; /* php_session_rfc1867_update() is running */
189187
} php_ps_globals;
190188

‎ext/session/session.c‎

Lines changed: 18 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -409,18 +409,6 @@ static zend_long php_session_gc(bool immediate)
409409
if ((PS(mod_data) || PS(mod_user_implemented))) {
410410
/* Use probability-based GC if not forced and probability is configured */
411411
if (!collect && PS(gc_probability) > 0) {
412-
/* Seed lazily on first GC draw per process. */
413-
if (UNEXPECTED(!PS(random_seeded))) {
414-
php_random_uint128_t seed;
415-
if (php_random_bytes_silent(&seed, sizeof(seed)) == FAILURE) {
416-
seed = php_random_uint128_constant(
417-
php_random_generate_fallback_seed(),
418-
php_random_generate_fallback_seed()
419-
);
420-
}
421-
php_random_pcgoneseq128xslrr64_seed128(PS(random).state, seed);
422-
PS(random_seeded) = true;
423-
}
424412
collect = php_random_range(PS(random), 0, PS(gc_divisor) - 1) < PS(gc_probability);
425413
}
426414

@@ -3019,7 +3007,15 @@ static PHP_GINIT_FUNCTION(ps)
30193007
.algo = &php_random_algo_pcgoneseq128xslrr64,
30203008
.state = &ps_globals->random_state,
30213009
};
3022-
ps_globals->random_seeded = false;
3010+
3011+
php_random_uint128_t seed;
3012+
if (php_random_bytes_silent(&seed, sizeof(seed)) == FAILURE) {
3013+
seed = php_random_uint128_constant(
3014+
php_random_generate_fallback_seed(),
3015+
php_random_generate_fallback_seed()
3016+
);
3017+
}
3018+
php_random_pcgoneseq128xslrr64_seed128(ps_globals->random.state, seed);
30233019
}
30243020

30253021
/* 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_
34543450
return retval;
34553451
}
34563452

3453+
PHP_CINIT_FUNCTION(session)
3454+
{
3455+
/* Give each child a distinct, widely separated GC sequence without requesting new entropy. */
3456+
uint64_t advance = (uint64_t) getpid() << 32;
3457+
php_random_pcgoneseq128xslrr64_advance(PS(random).state, advance);
3458+
}
3459+
34573460
zend_module_entry session_module_entry = {
34583461
STANDARD_MODULE_HEADER,
34593462
"session",
@@ -3466,7 +3469,8 @@ zend_module_entry session_module_entry = {
34663469
PHP_GINIT(ps),
34673470
NULL,
34683471
NULL,
3469-
STANDARD_MODULE_PROPERTIES_EX
3472+
STANDARD_MODULE_PROPERTIES_EX,
3473+
.child_init_func = PHP_CINIT(session)
34703474
};
34713475

34723476
#ifdef COMPILE_DL_SESSION

‎ext/session/tests/gh22616.phpt‎

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
--TEST--
2+
GH-22616: Session GC randomness is independent after a fork
3+
--EXTENSIONS--
4+
session
5+
pcntl
6+
--INI--
7+
session.gc_probability=1
8+
session.gc_divisor=2
9+
session.use_cookies=0
10+
session.cache_limiter=
11+
--FILE--
12+
<?php
13+
$handler = new class implements SessionHandlerInterface, SessionIdInterface, SessionUpdateTimestampHandlerInterface {
14+
public bool $collected = false;
15+
16+
public function create_sid(): string { return 'gh22616'; }
17+
public function validateId(string $id): bool { return true; }
18+
public function updateTimestamp(string $id, string $data): bool { return true; }
19+
public function open(string $path, string $name): bool { return true; }
20+
public function close(): bool { return true; }
21+
public function read(string $id): string|false { return ''; }
22+
public function write(string $id, string $data): bool { return true; }
23+
public function destroy(string $id): bool { return true; }
24+
public function gc(int $max_lifetime): int|false {
25+
$this->collected = true;
26+
return 0;
27+
}
28+
};
29+
session_set_save_handler($handler);
30+
session_id('gh22616');
31+
32+
function sequence(): string {
33+
global $handler;
34+
if (session_status() === PHP_SESSION_ACTIVE) {
35+
session_write_close();
36+
}
37+
$result = '';
38+
for ($i = 0; $i < 256; $i++) {
39+
$handler->collected = false;
40+
session_start();
41+
session_write_close();
42+
$result .= (int) $handler->collected;
43+
}
44+
return $result;
45+
}
46+
47+
$results = [];
48+
foreach (['unseeded', 'seeded', 'active'] as $state) {
49+
if ($state === 'active') {
50+
session_start();
51+
}
52+
$file1 = tempnam(sys_get_temp_dir(), 'gh22616');
53+
$pid1 = pcntl_fork();
54+
if ($pid1 === -1) {
55+
throw new Exception('fork failed');
56+
}
57+
if ($pid1 === 0) {
58+
file_put_contents($file1, sequence());
59+
exit;
60+
}
61+
62+
$file2 = tempnam(sys_get_temp_dir(), 'gh22616');
63+
$pid2 = pcntl_fork();
64+
if ($pid2 === -1) {
65+
throw new Exception('fork failed');
66+
}
67+
if ($pid2 === 0) {
68+
file_put_contents($file2, sequence());
69+
exit;
70+
}
71+
72+
$parent = sequence();
73+
pcntl_waitpid($pid1, $status1);
74+
pcntl_waitpid($pid2, $status2);
75+
$child1 = file_get_contents($file1);
76+
$child2 = file_get_contents($file2);
77+
unlink($file1);
78+
unlink($file2);
79+
$results[$state] = pcntl_wifexited($status1)
80+
&& pcntl_wexitstatus($status1) === 0
81+
&& pcntl_wifexited($status2)
82+
&& pcntl_wexitstatus($status2) === 0
83+
&& strlen($child1) === 256
84+
&& strlen($child2) === 256
85+
&& $parent !== $child1
86+
&& $parent !== $child2
87+
&& $child1 !== $child2;
88+
}
89+
var_dump($results);
90+
?>
91+
--EXPECT--
92+
array(3) {
93+
["unseeded"]=>
94+
bool(true)
95+
["seeded"]=>
96+
bool(true)
97+
["active"]=>
98+
bool(true)
99+
}

‎ext/zend_test/php_test.h‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ ZEND_TSRMLS_CACHE_EXTERN()
2929
#endif
3030

3131
ZEND_BEGIN_MODULE_GLOBALS(zend_test)
32+
zend_long child_init_count;
3233
int observer_enabled;
3334
int observer_show_output;
3435
int observer_observe_all;

‎ext/zend_test/test.c‎

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1669,6 +1669,13 @@ static PHP_INI_MH(OnUpdateZendTestObserveOplineInZendMM)
16691669
return OnUpdateBool(entry, new_value, mh_arg1, mh_arg2, mh_arg3, stage);
16701670
}
16711671

1672+
static ZEND_FUNCTION(zend_test_child_init_count)
1673+
{
1674+
ZEND_PARSE_PARAMETERS_NONE();
1675+
1676+
RETURN_LONG(ZT_G(child_init_count));
1677+
}
1678+
16721679
static ZEND_FUNCTION(zend_test_fill_packed_array)
16731680
{
16741681
HashTable *parameter;
@@ -2609,6 +2616,11 @@ PHP_MINFO_FUNCTION(zend_test)
26092616
DISPLAY_INI_ENTRIES();
26102617
}
26112618

2619+
static void zend_test_child_init(void)
2620+
{
2621+
ZT_G(child_init_count)++;
2622+
}
2623+
26122624
zend_module_entry zend_test_module_entry = {
26132625
STANDARD_MODULE_HEADER,
26142626
"zend_test",
@@ -2623,7 +2635,8 @@ zend_module_entry zend_test_module_entry = {
26232635
PHP_GINIT(zend_test),
26242636
PHP_GSHUTDOWN(zend_test),
26252637
NULL,
2626-
STANDARD_MODULE_PROPERTIES_EX
2638+
STANDARD_MODULE_PROPERTIES_EX,
2639+
.child_init_func = zend_test_child_init
26272640
};
26282641

26292642
#ifdef COMPILE_DL_ZEND_TEST

‎ext/zend_test/test.stub.php‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -282,6 +282,8 @@ function zend_test_nodiscard(): int {}
282282
#[\NoDiscard(message: "custom message 2")]
283283
function zend_test_deprecated_nodiscard(): int {}
284284

285+
function zend_test_child_init_count(): int {}
286+
285287
/** @alias zend_test_void_return */
286288
function zend_test_aliased(): void {}
287289

‎ext/zend_test/test_arginfo.h‎

Lines changed: 5 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎ext/zend_test/test_decl.h‎

Lines changed: 4 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)