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 ;
@@ -166,6 +183,193 @@ static void frankenphp_release_temporary_streams() {
166183 ZEND_HASH_FOREACH_END ();
167184}
168185
186+ /* Destructor for INI snapshot hash table entries */
187+ static void ini_snapshot_dtor (zval * zv ) {
188+ zend_string_release ((zend_string * )Z_PTR_P (zv ));
189+ }
190+
191+ /* Save the current state of modified INI entries.
192+ * This captures INI values set by the framework before the worker loop. */
193+ static void frankenphp_snapshot_ini (void ) {
194+ if (worker_ini_snapshot != NULL ) {
195+ return ; /* Already snapshotted */
196+ }
197+
198+ ALLOC_HASHTABLE (worker_ini_snapshot );
199+ zend_hash_init (worker_ini_snapshot , 8 , NULL , ini_snapshot_dtor , 0 );
200+
201+ if (EG (modified_ini_directives ) == NULL ) {
202+ return ; /* No modifications to snapshot */
203+ }
204+
205+ zend_ini_entry * ini_entry ;
206+ ZEND_HASH_FOREACH_PTR (EG (modified_ini_directives ), ini_entry ) {
207+ if (ini_entry -> value ) {
208+ zend_hash_add_ptr (worker_ini_snapshot , ini_entry -> name ,
209+ zend_string_copy (ini_entry -> value ));
210+ }
211+ }
212+ ZEND_HASH_FOREACH_END ();
213+ }
214+
215+ /* Restore INI values to the state captured by frankenphp_snapshot_ini().
216+ * - Entries in snapshot with changed values: restore to snapshot value
217+ * - Entries not in snapshot: restore to startup default */
218+ static void frankenphp_restore_ini (void ) {
219+ if (worker_ini_snapshot == NULL || EG (modified_ini_directives ) == NULL ) {
220+ return ;
221+ }
222+
223+ zend_ini_entry * ini_entry ;
224+ zend_string * snapshot_value ;
225+ zend_string * entry_name ;
226+
227+ /* Collect entries to restore to default in a separate array.
228+ * We cannot call zend_restore_ini_entry() during iteration because
229+ * it calls zend_hash_del() on EG(modified_ini_directives). */
230+ zend_string * * entries_to_restore = NULL ;
231+ size_t restore_count = 0 ;
232+ size_t restore_capacity = 0 ;
233+
234+ ZEND_HASH_FOREACH_STR_KEY_PTR (EG (modified_ini_directives ), entry_name ,
235+ ini_entry ) {
236+ snapshot_value = zend_hash_find_ptr (worker_ini_snapshot , entry_name );
237+
238+ if (snapshot_value == NULL ) {
239+ /* Entry was not in snapshot: collect for restore to startup default */
240+ if (restore_count >= restore_capacity ) {
241+ restore_capacity = restore_capacity ? restore_capacity * 2 : 8 ;
242+ entries_to_restore =
243+ erealloc (entries_to_restore , restore_capacity * sizeof (zend_string * ));
244+ }
245+ entries_to_restore [restore_count ++ ] = zend_string_copy (entry_name );
246+ } else if (!zend_string_equals (ini_entry -> value , snapshot_value )) {
247+ /* Entry was in snapshot but value changed: restore to snapshot value.
248+ * zend_alter_ini_entry() does not delete from modified_ini_directives. */
249+ zend_alter_ini_entry (entry_name , snapshot_value , PHP_INI_USER ,
250+ PHP_INI_STAGE_RUNTIME );
251+ }
252+ /* else: Entry in snapshot with same value, nothing to do */
253+ }
254+ ZEND_HASH_FOREACH_END ();
255+
256+ /* Now restore entries to default outside of iteration */
257+ for (size_t i = 0 ; i < restore_count ; i ++ ) {
258+ zend_restore_ini_entry (entries_to_restore [i ], PHP_INI_STAGE_RUNTIME );
259+ zend_string_release (entries_to_restore [i ]);
260+ }
261+ if (entries_to_restore ) {
262+ efree (entries_to_restore );
263+ }
264+ }
265+
266+ /* Save session user handlers set before the worker loop.
267+ * This allows frameworks to define custom session handlers that persist. */
268+ static void frankenphp_snapshot_session_handlers (void ) {
269+ if (worker_session_handlers_snapshot != NULL ) {
270+ return ; /* Already snapshotted */
271+ }
272+
273+ /* Check if session module is loaded */
274+ if (zend_hash_str_find_ptr (& module_registry , "session" ,
275+ sizeof ("session" ) - 1 ) == NULL ) {
276+ return ; /* Session module not available */
277+ }
278+
279+ /* Check if user session handlers are defined */
280+ if (Z_ISUNDEF (PS (mod_user_names ).ps_open )) {
281+ return ; /* No user handlers to snapshot */
282+ }
283+
284+ worker_session_handlers_snapshot = malloc (sizeof (session_user_handlers ));
285+ if (worker_session_handlers_snapshot == NULL ) {
286+ return ; /* Memory allocation failed */
287+ }
288+
289+ /* Copy each handler zval with incremented reference count */
290+ #define SNAPSHOT_HANDLER (name ) \
291+ if (!Z_ISUNDEF(PS(mod_user_names).name)) { \
292+ ZVAL_COPY(&worker_session_handlers_snapshot->name, \
293+ &PS(mod_user_names).name); \
294+ } else { \
295+ ZVAL_UNDEF(&worker_session_handlers_snapshot->name); \
296+ }
297+
298+ SNAPSHOT_HANDLER (ps_open );
299+ SNAPSHOT_HANDLER (ps_close );
300+ SNAPSHOT_HANDLER (ps_read );
301+ SNAPSHOT_HANDLER (ps_write );
302+ SNAPSHOT_HANDLER (ps_destroy );
303+ SNAPSHOT_HANDLER (ps_gc );
304+ SNAPSHOT_HANDLER (ps_create_sid );
305+ SNAPSHOT_HANDLER (ps_validate_sid );
306+ SNAPSHOT_HANDLER (ps_update_timestamp );
307+
308+ #undef SNAPSHOT_HANDLER
309+ }
310+
311+ /* Restore session user handlers from snapshot after RSHUTDOWN freed them. */
312+ static void frankenphp_restore_session_handlers (void ) {
313+ if (worker_session_handlers_snapshot == NULL ) {
314+ return ;
315+ }
316+
317+ /* Restore each handler zval */
318+ #define RESTORE_HANDLER (name ) \
319+ if (!Z_ISUNDEF(worker_session_handlers_snapshot->name)) { \
320+ if (!Z_ISUNDEF(PS(mod_user_names).name)) { \
321+ zval_ptr_dtor(&PS(mod_user_names).name); \
322+ } \
323+ ZVAL_COPY(&PS(mod_user_names).name, \
324+ &worker_session_handlers_snapshot->name); \
325+ }
326+
327+ RESTORE_HANDLER (ps_open );
328+ RESTORE_HANDLER (ps_close );
329+ RESTORE_HANDLER (ps_read );
330+ RESTORE_HANDLER (ps_write );
331+ RESTORE_HANDLER (ps_destroy );
332+ RESTORE_HANDLER (ps_gc );
333+ RESTORE_HANDLER (ps_create_sid );
334+ RESTORE_HANDLER (ps_validate_sid );
335+ RESTORE_HANDLER (ps_update_timestamp );
336+
337+ #undef RESTORE_HANDLER
338+ }
339+
340+ /* Free worker state when the worker script terminates. */
341+ static void frankenphp_cleanup_worker_state (void ) {
342+ /* Free INI snapshot */
343+ if (worker_ini_snapshot != NULL ) {
344+ zend_hash_destroy (worker_ini_snapshot );
345+ FREE_HASHTABLE (worker_ini_snapshot );
346+ worker_ini_snapshot = NULL ;
347+ }
348+
349+ /* Free session handlers snapshot */
350+ if (worker_session_handlers_snapshot != NULL ) {
351+ #define FREE_HANDLER (name ) \
352+ if (!Z_ISUNDEF(worker_session_handlers_snapshot->name)) { \
353+ zval_ptr_dtor(&worker_session_handlers_snapshot->name); \
354+ }
355+
356+ FREE_HANDLER (ps_open );
357+ FREE_HANDLER (ps_close );
358+ FREE_HANDLER (ps_read );
359+ FREE_HANDLER (ps_write );
360+ FREE_HANDLER (ps_destroy );
361+ FREE_HANDLER (ps_gc );
362+ FREE_HANDLER (ps_create_sid );
363+ FREE_HANDLER (ps_validate_sid );
364+ FREE_HANDLER (ps_update_timestamp );
365+
366+ #undef FREE_HANDLER
367+
368+ free (worker_session_handlers_snapshot );
369+ worker_session_handlers_snapshot = NULL ;
370+ }
371+ }
372+
169373/* Adapted from php_request_shutdown */
170374static void frankenphp_worker_request_shutdown () {
171375 /* Flush all output buffers */
@@ -200,6 +404,12 @@ bool frankenphp_shutdown_dummy_request(void) {
200404 return false;
201405 }
202406
407+ /* Snapshot INI and session handlers BEFORE shutdown.
408+ * The framework has set these up before the worker loop, and we want
409+ * to preserve them. Session RSHUTDOWN will free the handlers. */
410+ frankenphp_snapshot_ini ();
411+ frankenphp_snapshot_session_handlers ();
412+
203413 frankenphp_worker_request_shutdown ();
204414
205415 return true;
@@ -255,6 +465,12 @@ static int frankenphp_worker_request_startup() {
255465
256466 frankenphp_reset_super_globals ();
257467
468+ /* Restore INI values changed during the previous request back to their
469+ * snapshot state (captured in frankenphp_shutdown_dummy_request).
470+ * This ensures framework settings persist while request-level changes
471+ * are reset. */
472+ frankenphp_restore_ini ();
473+
258474 const char * * module_name ;
259475 zend_module_entry * module ;
260476 for (module_name = MODULES_TO_RELOAD ; * module_name ; module_name ++ ) {
@@ -264,6 +480,12 @@ static int frankenphp_worker_request_startup() {
264480 module -> request_startup_func (module -> type , module -> module_number );
265481 }
266482 }
483+
484+ /* Restore session handlers AFTER session RINIT.
485+ * Session RSHUTDOWN frees mod_user_names callbacks, so we must restore
486+ * them before user code runs. This must happen after RINIT because
487+ * session RINIT may reset some state. */
488+ frankenphp_restore_session_handlers ();
267489 }
268490 zend_catch { retval = FAILURE ; }
269491 zend_end_try ();
@@ -610,6 +832,9 @@ static zend_module_entry frankenphp_module = {
610832
611833static void frankenphp_request_shutdown () {
612834 frankenphp_free_request_context ();
835+ if (is_worker_thread ) {
836+ frankenphp_cleanup_worker_state ();
837+ }
613838 php_request_shutdown ((void * )0 );
614839}
615840
0 commit comments