Skip to content

Commit ad7e4f1

Browse files
authored
fix(worker): reset ini settinfs and session if changed during worker request
1 parent 0f410a2 commit ad7e4f1

6 files changed

Lines changed: 738 additions & 1 deletion

File tree

frankenphp.c

Lines changed: 216 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
#include <Zend/zend_interfaces.h>
55
#include <Zend/zend_types.h>
66
#include <errno.h>
7+
#include <ext/session/php_session.h>
78
#include <ext/spl/spl_exceptions.h>
89
#include <ext/standard/head.h>
910
#include <inttypes.h>
@@ -73,6 +74,42 @@ bool should_filter_var = 0;
7374
__thread uintptr_t thread_index;
7475
__thread bool is_worker_thread = false;
7576
__thread zval *os_environment = NULL;
77+
__thread HashTable *worker_ini_snapshot = NULL;
78+
79+
/* Session user handler names (same structure as PS(mod_user_names)).
80+
* In PHP 8.2, mod_user_names is a union with .name.ps_* access.
81+
* In PHP 8.3+, mod_user_names is a direct struct with .ps_* access. */
82+
typedef struct {
83+
zval ps_open;
84+
zval ps_close;
85+
zval ps_read;
86+
zval ps_write;
87+
zval ps_destroy;
88+
zval ps_gc;
89+
zval ps_create_sid;
90+
zval ps_validate_sid;
91+
zval ps_update_timestamp;
92+
} session_user_handlers;
93+
94+
/* Macro to access PS(mod_user_names) handlers across PHP versions */
95+
#if PHP_VERSION_ID >= 80300
96+
#define PS_MOD_USER_NAMES(handler) PS(mod_user_names).handler
97+
#else
98+
#define PS_MOD_USER_NAMES(handler) PS(mod_user_names).name.handler
99+
#endif
100+
101+
#define FOR_EACH_SESSION_HANDLER(op) \
102+
op(ps_open); \
103+
op(ps_close); \
104+
op(ps_read); \
105+
op(ps_write); \
106+
op(ps_destroy); \
107+
op(ps_gc); \
108+
op(ps_create_sid); \
109+
op(ps_validate_sid); \
110+
op(ps_update_timestamp)
111+
112+
__thread session_user_handlers *worker_session_handlers_snapshot = NULL;
76113

77114
void frankenphp_update_local_thread_context(bool is_worker) {
78115
is_worker_thread = is_worker;
@@ -174,6 +211,164 @@ static void frankenphp_release_temporary_streams() {
174211
ZEND_HASH_FOREACH_END();
175212
}
176213

214+
/* Destructor for INI snapshot hash table entries */
215+
static void frankenphp_ini_snapshot_dtor(zval *zv) {
216+
zend_string_release((zend_string *)Z_PTR_P(zv));
217+
}
218+
219+
/* Save the current state of modified INI entries.
220+
* This captures INI values set by the framework before the worker loop. */
221+
static void frankenphp_snapshot_ini(void) {
222+
if (worker_ini_snapshot != NULL) {
223+
return; /* Already snapshotted */
224+
}
225+
226+
if (EG(modified_ini_directives) == NULL) {
227+
/* Allocate empty table to mark as snapshotted */
228+
ALLOC_HASHTABLE(worker_ini_snapshot);
229+
zend_hash_init(worker_ini_snapshot, 0, NULL, frankenphp_ini_snapshot_dtor, 0);
230+
return;
231+
}
232+
233+
uint32_t num_modified = zend_hash_num_elements(EG(modified_ini_directives));
234+
ALLOC_HASHTABLE(worker_ini_snapshot);
235+
zend_hash_init(worker_ini_snapshot, num_modified, NULL, frankenphp_ini_snapshot_dtor, 0);
236+
237+
zend_ini_entry *ini_entry;
238+
ZEND_HASH_FOREACH_PTR(EG(modified_ini_directives), ini_entry) {
239+
if (ini_entry->value) {
240+
zend_hash_add_ptr(worker_ini_snapshot, ini_entry->name,
241+
zend_string_copy(ini_entry->value));
242+
}
243+
}
244+
ZEND_HASH_FOREACH_END();
245+
}
246+
247+
/* Restore INI values to the state captured by frankenphp_snapshot_ini().
248+
* - Entries in snapshot with changed values: restore to snapshot value
249+
* - Entries not in snapshot: restore to startup default */
250+
static void frankenphp_restore_ini(void) {
251+
if (worker_ini_snapshot == NULL || EG(modified_ini_directives) == NULL) {
252+
return;
253+
}
254+
255+
zend_ini_entry *ini_entry;
256+
zend_string *snapshot_value;
257+
zend_string *entry_name;
258+
259+
/* Collect entries to restore to default in a separate array.
260+
* We cannot call zend_restore_ini_entry() during iteration because
261+
* it calls zend_hash_del() on EG(modified_ini_directives). */
262+
uint32_t max_entries = zend_hash_num_elements(EG(modified_ini_directives));
263+
zend_string **entries_to_restore =
264+
max_entries ? emalloc(max_entries * sizeof(zend_string *)) : NULL;
265+
size_t restore_count = 0;
266+
267+
ZEND_HASH_FOREACH_STR_KEY_PTR(EG(modified_ini_directives), entry_name,
268+
ini_entry) {
269+
snapshot_value = zend_hash_find_ptr(worker_ini_snapshot, entry_name);
270+
271+
if (snapshot_value == NULL) {
272+
/* Entry was not in snapshot: collect for restore to startup default */
273+
entries_to_restore[restore_count++] = zend_string_copy(entry_name);
274+
} else if (!zend_string_equals(ini_entry->value, snapshot_value)) {
275+
/* Entry was in snapshot but value changed: restore to snapshot value.
276+
* zend_alter_ini_entry() does not delete from modified_ini_directives. */
277+
zend_alter_ini_entry(entry_name, snapshot_value, PHP_INI_USER,
278+
PHP_INI_STAGE_RUNTIME);
279+
}
280+
/* else: Entry in snapshot with same value, nothing to do */
281+
}
282+
ZEND_HASH_FOREACH_END();
283+
284+
/* Now restore entries to default outside of iteration */
285+
for (size_t i = 0; i < restore_count; i++) {
286+
zend_restore_ini_entry(entries_to_restore[i], PHP_INI_STAGE_RUNTIME);
287+
zend_string_release(entries_to_restore[i]);
288+
}
289+
if (entries_to_restore) {
290+
efree(entries_to_restore);
291+
}
292+
}
293+
294+
/* Save session user handlers set before the worker loop.
295+
* This allows frameworks to define custom session handlers that persist. */
296+
static void frankenphp_snapshot_session_handlers(void) {
297+
if (worker_session_handlers_snapshot != NULL) {
298+
return; /* Already snapshotted */
299+
}
300+
301+
/* Check if session module is loaded */
302+
if (zend_hash_str_find_ptr(&module_registry, "session",
303+
sizeof("session") - 1) == NULL) {
304+
return; /* Session module not available */
305+
}
306+
307+
/* Check if user session handlers are defined */
308+
if (Z_ISUNDEF(PS_MOD_USER_NAMES(ps_open))) {
309+
return; /* No user handlers to snapshot */
310+
}
311+
312+
worker_session_handlers_snapshot = emalloc(sizeof(session_user_handlers));
313+
314+
/* Copy each handler zval with incremented reference count */
315+
#define SNAPSHOT_HANDLER(h) \
316+
if (!Z_ISUNDEF(PS_MOD_USER_NAMES(h))) { \
317+
ZVAL_COPY(&worker_session_handlers_snapshot->h, &PS_MOD_USER_NAMES(h)); \
318+
} else { \
319+
ZVAL_UNDEF(&worker_session_handlers_snapshot->h); \
320+
}
321+
322+
FOR_EACH_SESSION_HANDLER(SNAPSHOT_HANDLER);
323+
324+
#undef SNAPSHOT_HANDLER
325+
}
326+
327+
/* Restore session user handlers from snapshot after RSHUTDOWN freed them. */
328+
static void frankenphp_restore_session_handlers(void) {
329+
if (worker_session_handlers_snapshot == NULL) {
330+
return;
331+
}
332+
333+
/* Restore each handler zval.
334+
* Session RSHUTDOWN already freed the handlers via zval_ptr_dtor and set
335+
* them to UNDEF, so we don't need to destroy them again. We simply copy
336+
* from the snapshot (which holds its own reference). */
337+
#define RESTORE_HANDLER(h) \
338+
if (!Z_ISUNDEF(worker_session_handlers_snapshot->h)) { \
339+
ZVAL_COPY(&PS_MOD_USER_NAMES(h), &worker_session_handlers_snapshot->h); \
340+
}
341+
342+
FOR_EACH_SESSION_HANDLER(RESTORE_HANDLER);
343+
344+
#undef RESTORE_HANDLER
345+
}
346+
347+
/* Free worker state when the worker script terminates. */
348+
static void frankenphp_cleanup_worker_state(void) {
349+
/* Free INI snapshot */
350+
if (worker_ini_snapshot != NULL) {
351+
zend_hash_destroy(worker_ini_snapshot);
352+
FREE_HASHTABLE(worker_ini_snapshot);
353+
worker_ini_snapshot = NULL;
354+
}
355+
356+
/* Free session handlers snapshot */
357+
if (worker_session_handlers_snapshot != NULL) {
358+
#define FREE_HANDLER(h) \
359+
if (!Z_ISUNDEF(worker_session_handlers_snapshot->h)) { \
360+
zval_ptr_dtor(&worker_session_handlers_snapshot->h); \
361+
}
362+
363+
FOR_EACH_SESSION_HANDLER(FREE_HANDLER);
364+
365+
#undef FREE_HANDLER
366+
367+
efree(worker_session_handlers_snapshot);
368+
worker_session_handlers_snapshot = NULL;
369+
}
370+
}
371+
177372
/* Adapted from php_request_shutdown */
178373
static void frankenphp_worker_request_shutdown() {
179374
/* Flush all output buffers */
@@ -208,6 +403,12 @@ bool frankenphp_shutdown_dummy_request(void) {
208403
return false;
209404
}
210405

406+
/* Snapshot INI and session handlers BEFORE shutdown.
407+
* The framework has set these up before the worker loop, and we want
408+
* to preserve them. Session RSHUTDOWN will free the handlers. */
409+
frankenphp_snapshot_ini();
410+
frankenphp_snapshot_session_handlers();
411+
211412
frankenphp_worker_request_shutdown();
212413

213414
return true;
@@ -263,6 +464,12 @@ static int frankenphp_worker_request_startup() {
263464

264465
frankenphp_reset_super_globals();
265466

467+
/* Restore INI values changed during the previous request back to their
468+
* snapshot state (captured in frankenphp_shutdown_dummy_request).
469+
* This ensures framework settings persist while request-level changes
470+
* are reset. */
471+
frankenphp_restore_ini();
472+
266473
const char **module_name;
267474
zend_module_entry *module;
268475
for (module_name = MODULES_TO_RELOAD; *module_name; module_name++) {
@@ -272,6 +479,12 @@ static int frankenphp_worker_request_startup() {
272479
module->request_startup_func(module->type, module->module_number);
273480
}
274481
}
482+
483+
/* Restore session handlers AFTER session RINIT.
484+
* Session RSHUTDOWN frees mod_user_names callbacks, so we must restore
485+
* them before user code runs. This must happen after RINIT because
486+
* session RINIT may reset some state. */
487+
frankenphp_restore_session_handlers();
275488
}
276489
zend_catch { retval = FAILURE; }
277490
zend_end_try();
@@ -617,6 +830,9 @@ static zend_module_entry frankenphp_module = {
617830
STANDARD_MODULE_PROPERTIES};
618831

619832
static void frankenphp_request_shutdown() {
833+
if (is_worker_thread) {
834+
frankenphp_cleanup_worker_state();
835+
}
620836
php_request_shutdown((void *)0);
621837
frankenphp_free_request_context();
622838
}

0 commit comments

Comments
 (0)