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
77114void 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 */
178373static 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
619832static 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