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,22 @@ 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+ typedef struct {
81+ zval ps_open ;
82+ zval ps_close ;
83+ zval ps_read ;
84+ zval ps_write ;
85+ zval ps_destroy ;
86+ zval ps_gc ;
87+ zval ps_create_sid ;
88+ zval ps_validate_sid ;
89+ zval ps_update_timestamp ;
90+ } session_user_handlers ;
91+
92+ __thread session_user_handlers * worker_session_handlers_snapshot = NULL ;
7693
7794void frankenphp_update_local_thread_context (bool is_worker ) {
7895 is_worker_thread = is_worker ;
@@ -174,6 +191,193 @@ static void frankenphp_release_temporary_streams() {
174191 ZEND_HASH_FOREACH_END ();
175192}
176193
194+ /* Destructor for INI snapshot hash table entries */
195+ static void ini_snapshot_dtor (zval * zv ) {
196+ zend_string_release ((zend_string * )Z_PTR_P (zv ));
197+ }
198+
199+ /* Save the current state of modified INI entries.
200+ * This captures INI values set by the framework before the worker loop. */
201+ static void frankenphp_snapshot_ini (void ) {
202+ if (worker_ini_snapshot != NULL ) {
203+ return ; /* Already snapshotted */
204+ }
205+
206+ ALLOC_HASHTABLE (worker_ini_snapshot );
207+ zend_hash_init (worker_ini_snapshot , 8 , NULL , ini_snapshot_dtor , 0 );
208+
209+ if (EG (modified_ini_directives ) == NULL ) {
210+ return ; /* No modifications to snapshot */
211+ }
212+
213+ zend_ini_entry * ini_entry ;
214+ ZEND_HASH_FOREACH_PTR (EG (modified_ini_directives ), ini_entry ) {
215+ if (ini_entry -> value ) {
216+ zend_hash_add_ptr (worker_ini_snapshot , ini_entry -> name ,
217+ zend_string_copy (ini_entry -> value ));
218+ }
219+ }
220+ ZEND_HASH_FOREACH_END ();
221+ }
222+
223+ /* Restore INI values to the state captured by frankenphp_snapshot_ini().
224+ * - Entries in snapshot with changed values: restore to snapshot value
225+ * - Entries not in snapshot: restore to startup default */
226+ static void frankenphp_restore_ini (void ) {
227+ if (worker_ini_snapshot == NULL || EG (modified_ini_directives ) == NULL ) {
228+ return ;
229+ }
230+
231+ zend_ini_entry * ini_entry ;
232+ zend_string * snapshot_value ;
233+ zend_string * entry_name ;
234+
235+ /* Collect entries to restore to default in a separate array.
236+ * We cannot call zend_restore_ini_entry() during iteration because
237+ * it calls zend_hash_del() on EG(modified_ini_directives). */
238+ zend_string * * entries_to_restore = NULL ;
239+ size_t restore_count = 0 ;
240+ size_t restore_capacity = 0 ;
241+
242+ ZEND_HASH_FOREACH_STR_KEY_PTR (EG (modified_ini_directives ), entry_name ,
243+ ini_entry ) {
244+ snapshot_value = zend_hash_find_ptr (worker_ini_snapshot , entry_name );
245+
246+ if (snapshot_value == NULL ) {
247+ /* Entry was not in snapshot: collect for restore to startup default */
248+ if (restore_count >= restore_capacity ) {
249+ restore_capacity = restore_capacity ? restore_capacity * 2 : 8 ;
250+ entries_to_restore =
251+ erealloc (entries_to_restore , restore_capacity * sizeof (zend_string * ));
252+ }
253+ entries_to_restore [restore_count ++ ] = zend_string_copy (entry_name );
254+ } else if (!zend_string_equals (ini_entry -> value , snapshot_value )) {
255+ /* Entry was in snapshot but value changed: restore to snapshot value.
256+ * zend_alter_ini_entry() does not delete from modified_ini_directives. */
257+ zend_alter_ini_entry (entry_name , snapshot_value , PHP_INI_USER ,
258+ PHP_INI_STAGE_RUNTIME );
259+ }
260+ /* else: Entry in snapshot with same value, nothing to do */
261+ }
262+ ZEND_HASH_FOREACH_END ();
263+
264+ /* Now restore entries to default outside of iteration */
265+ for (size_t i = 0 ; i < restore_count ; i ++ ) {
266+ zend_restore_ini_entry (entries_to_restore [i ], PHP_INI_STAGE_RUNTIME );
267+ zend_string_release (entries_to_restore [i ]);
268+ }
269+ if (entries_to_restore ) {
270+ efree (entries_to_restore );
271+ }
272+ }
273+
274+ /* Save session user handlers set before the worker loop.
275+ * This allows frameworks to define custom session handlers that persist. */
276+ static void frankenphp_snapshot_session_handlers (void ) {
277+ if (worker_session_handlers_snapshot != NULL ) {
278+ return ; /* Already snapshotted */
279+ }
280+
281+ /* Check if session module is loaded */
282+ if (zend_hash_str_find_ptr (& module_registry , "session" ,
283+ sizeof ("session" ) - 1 ) == NULL ) {
284+ return ; /* Session module not available */
285+ }
286+
287+ /* Check if user session handlers are defined */
288+ if (Z_ISUNDEF (PS (mod_user_names ).ps_open )) {
289+ return ; /* No user handlers to snapshot */
290+ }
291+
292+ worker_session_handlers_snapshot = malloc (sizeof (session_user_handlers ));
293+ if (worker_session_handlers_snapshot == NULL ) {
294+ return ; /* Memory allocation failed */
295+ }
296+
297+ /* Copy each handler zval with incremented reference count */
298+ #define SNAPSHOT_HANDLER (name ) \
299+ if (!Z_ISUNDEF(PS(mod_user_names).name)) { \
300+ ZVAL_COPY(&worker_session_handlers_snapshot->name, \
301+ &PS(mod_user_names).name); \
302+ } else { \
303+ ZVAL_UNDEF(&worker_session_handlers_snapshot->name); \
304+ }
305+
306+ SNAPSHOT_HANDLER (ps_open );
307+ SNAPSHOT_HANDLER (ps_close );
308+ SNAPSHOT_HANDLER (ps_read );
309+ SNAPSHOT_HANDLER (ps_write );
310+ SNAPSHOT_HANDLER (ps_destroy );
311+ SNAPSHOT_HANDLER (ps_gc );
312+ SNAPSHOT_HANDLER (ps_create_sid );
313+ SNAPSHOT_HANDLER (ps_validate_sid );
314+ SNAPSHOT_HANDLER (ps_update_timestamp );
315+
316+ #undef SNAPSHOT_HANDLER
317+ }
318+
319+ /* Restore session user handlers from snapshot after RSHUTDOWN freed them. */
320+ static void frankenphp_restore_session_handlers (void ) {
321+ if (worker_session_handlers_snapshot == NULL ) {
322+ return ;
323+ }
324+
325+ /* Restore each handler zval */
326+ #define RESTORE_HANDLER (name ) \
327+ if (!Z_ISUNDEF(worker_session_handlers_snapshot->name)) { \
328+ if (!Z_ISUNDEF(PS(mod_user_names).name)) { \
329+ zval_ptr_dtor(&PS(mod_user_names).name); \
330+ } \
331+ ZVAL_COPY(&PS(mod_user_names).name, \
332+ &worker_session_handlers_snapshot->name); \
333+ }
334+
335+ RESTORE_HANDLER (ps_open );
336+ RESTORE_HANDLER (ps_close );
337+ RESTORE_HANDLER (ps_read );
338+ RESTORE_HANDLER (ps_write );
339+ RESTORE_HANDLER (ps_destroy );
340+ RESTORE_HANDLER (ps_gc );
341+ RESTORE_HANDLER (ps_create_sid );
342+ RESTORE_HANDLER (ps_validate_sid );
343+ RESTORE_HANDLER (ps_update_timestamp );
344+
345+ #undef RESTORE_HANDLER
346+ }
347+
348+ /* Free worker state when the worker script terminates. */
349+ static void frankenphp_cleanup_worker_state (void ) {
350+ /* Free INI snapshot */
351+ if (worker_ini_snapshot != NULL ) {
352+ zend_hash_destroy (worker_ini_snapshot );
353+ FREE_HASHTABLE (worker_ini_snapshot );
354+ worker_ini_snapshot = NULL ;
355+ }
356+
357+ /* Free session handlers snapshot */
358+ if (worker_session_handlers_snapshot != NULL ) {
359+ #define FREE_HANDLER (name ) \
360+ if (!Z_ISUNDEF(worker_session_handlers_snapshot->name)) { \
361+ zval_ptr_dtor(&worker_session_handlers_snapshot->name); \
362+ }
363+
364+ FREE_HANDLER (ps_open );
365+ FREE_HANDLER (ps_close );
366+ FREE_HANDLER (ps_read );
367+ FREE_HANDLER (ps_write );
368+ FREE_HANDLER (ps_destroy );
369+ FREE_HANDLER (ps_gc );
370+ FREE_HANDLER (ps_create_sid );
371+ FREE_HANDLER (ps_validate_sid );
372+ FREE_HANDLER (ps_update_timestamp );
373+
374+ #undef FREE_HANDLER
375+
376+ free (worker_session_handlers_snapshot );
377+ worker_session_handlers_snapshot = NULL ;
378+ }
379+ }
380+
177381/* Adapted from php_request_shutdown */
178382static void frankenphp_worker_request_shutdown () {
179383 /* Flush all output buffers */
@@ -208,6 +412,12 @@ bool frankenphp_shutdown_dummy_request(void) {
208412 return false;
209413 }
210414
415+ /* Snapshot INI and session handlers BEFORE shutdown.
416+ * The framework has set these up before the worker loop, and we want
417+ * to preserve them. Session RSHUTDOWN will free the handlers. */
418+ frankenphp_snapshot_ini ();
419+ frankenphp_snapshot_session_handlers ();
420+
211421 frankenphp_worker_request_shutdown ();
212422
213423 return true;
@@ -263,6 +473,12 @@ static int frankenphp_worker_request_startup() {
263473
264474 frankenphp_reset_super_globals ();
265475
476+ /* Restore INI values changed during the previous request back to their
477+ * snapshot state (captured in frankenphp_shutdown_dummy_request).
478+ * This ensures framework settings persist while request-level changes
479+ * are reset. */
480+ frankenphp_restore_ini ();
481+
266482 const char * * module_name ;
267483 zend_module_entry * module ;
268484 for (module_name = MODULES_TO_RELOAD ; * module_name ; module_name ++ ) {
@@ -272,6 +488,12 @@ static int frankenphp_worker_request_startup() {
272488 module -> request_startup_func (module -> type , module -> module_number );
273489 }
274490 }
491+
492+ /* Restore session handlers AFTER session RINIT.
493+ * Session RSHUTDOWN frees mod_user_names callbacks, so we must restore
494+ * them before user code runs. This must happen after RINIT because
495+ * session RINIT may reset some state. */
496+ frankenphp_restore_session_handlers ();
275497 }
276498 zend_catch { retval = FAILURE ; }
277499 zend_end_try ();
@@ -617,6 +839,9 @@ static zend_module_entry frankenphp_module = {
617839 STANDARD_MODULE_PROPERTIES };
618840
619841static void frankenphp_request_shutdown () {
842+ if (is_worker_thread ) {
843+ frankenphp_cleanup_worker_state ();
844+ }
620845 php_request_shutdown ((void * )0 );
621846 frankenphp_free_request_context ();
622847}
0 commit comments