@@ -101,6 +101,10 @@ HashTable *main_thread_env = NULL;
101101__thread uintptr_t thread_index ;
102102__thread bool is_worker_thread = false;
103103__thread HashTable * sandboxed_env = NULL ;
104+ /* prepared_env holds entries from php(_server)'s `env KEY VAL`, exposed to
105+ * getenv() and merged into $_ENV when 'E' is in variables_order. Separate from
106+ * putenv() so those don't leak into $_ENV. */
107+ __thread HashTable * prepared_env = NULL ;
104108
105109/* Published via SG(server_context) so ext-parallel children, which inherit
106110 * the parent's SG(server_context), can route SAPI callbacks back to the
@@ -519,9 +523,17 @@ bool frankenphp_shutdown_dummy_request(void) {
519523}
520524
521525void get_full_env (zval * track_vars_array ) {
522- zend_hash_extend (Z_ARR_P (track_vars_array ),
523- zend_hash_num_elements (main_thread_env ), 0 );
526+ size_t total = zend_hash_num_elements (main_thread_env );
527+ if (prepared_env != NULL ) {
528+ // perf: doesn't matter if we get the exact count, just >= needed
529+ total += zend_hash_num_elements (prepared_env );
530+ }
531+ zend_hash_extend (Z_ARR_P (track_vars_array ), total , 0 );
524532 zend_hash_copy (Z_ARR_P (track_vars_array ), main_thread_env , NULL );
533+ if (prepared_env != NULL ) {
534+ zend_hash_copy (Z_ARR_P (track_vars_array ), prepared_env ,
535+ (copy_ctor_func_t )zval_add_ref );
536+ }
525537}
526538
527539/* Adapted from php_request_startup() */
@@ -626,6 +638,13 @@ PHP_FUNCTION(frankenphp_putenv) {
626638
627639 if (sandboxed_env == NULL ) {
628640 sandboxed_env = zend_array_dup (main_thread_env );
641+ /* prepared_env overrides the OS env and putenv() overrides both, so layer
642+ * the prepared vars onto the dup before sandboxed_env starts shadowing the
643+ * other two layers in getenv(). */
644+ if (prepared_env != NULL ) {
645+ zend_hash_copy (sandboxed_env , prepared_env ,
646+ (copy_ctor_func_t )zval_add_ref );
647+ }
629648 }
630649
631650 /* cut at null byte to stay consistent with regular putenv */
@@ -661,6 +680,38 @@ PHP_FUNCTION(frankenphp_putenv) {
661680 RETURN_BOOL (success );
662681} /* }}} */
663682
683+ /* getenv() lookup: sandboxed_env if present (it already holds prepared + OS),
684+ * otherwise prepared_env then main_thread_env. */
685+ static inline zval * frankenphp_lookup_env (const char * name , size_t name_len ) {
686+ if (sandboxed_env != NULL ) {
687+ return zend_hash_str_find (sandboxed_env , name , name_len );
688+ }
689+
690+ zval * env_val = NULL ;
691+ if (prepared_env != NULL ) {
692+ env_val = zend_hash_str_find (prepared_env , name , name_len );
693+ }
694+ if (env_val == NULL ) {
695+ env_val = zend_hash_str_find (main_thread_env , name , name_len );
696+ }
697+
698+ return env_val ;
699+ }
700+
701+ /* Returns a fresh copy of the full environment, merging the layers above. */
702+ static inline HashTable * frankenphp_dup_env (void ) {
703+ if (sandboxed_env != NULL ) {
704+ return zend_array_dup (sandboxed_env );
705+ }
706+
707+ HashTable * env = zend_array_dup (main_thread_env );
708+ if (prepared_env != NULL ) {
709+ zend_hash_copy (env , prepared_env , (copy_ctor_func_t )zval_add_ref );
710+ }
711+
712+ return env ;
713+ }
714+
664715/* {{{ Get the env from the sandboxed environment */
665716PHP_FUNCTION (frankenphp_getenv ) {
666717 zend_string * name = NULL ;
@@ -672,14 +723,12 @@ PHP_FUNCTION(frankenphp_getenv) {
672723 Z_PARAM_BOOL (local_only )
673724 ZEND_PARSE_PARAMETERS_END ();
674725
675- HashTable * ht = sandboxed_env ? sandboxed_env : main_thread_env ;
676-
677726 if (!name ) {
678- RETURN_ARR (zend_array_dup ( ht ));
727+ RETURN_ARR (frankenphp_dup_env ( ));
679728 return ;
680729 }
681730
682- zval * env_val = zend_hash_find ( ht , name );
731+ zval * env_val = frankenphp_lookup_env ( ZSTR_VAL ( name ), ZSTR_LEN ( name ) );
683732 if (env_val && Z_TYPE_P (env_val ) == IS_STRING ) {
684733 zend_string * str = Z_STR_P (env_val );
685734 zend_string_addref (str );
@@ -1203,6 +1252,13 @@ void frankenphp_register_server_vars(zval *track_vars_array,
12031252 zend_hash_update_ind (ht , frankenphp_strings .remote_ident , & zv );
12041253}
12051254
1255+ void frankenphp_merge_with_prepared_env (zval * track_vars_array ) {
1256+ if (prepared_env != NULL ) {
1257+ HashTable * ht = Z_ARRVAL_P (track_vars_array );
1258+ zend_hash_copy (ht , prepared_env , (copy_ctor_func_t )zval_add_ref );
1259+ }
1260+ }
1261+
12061262/** Create an immutable zend_string that lasts for the whole process **/
12071263zend_string * frankenphp_init_persistent_string (const char * string , size_t len ) {
12081264 /* persistent strings will be ignored by the GC at the end of a request */
@@ -1316,9 +1372,7 @@ static void frankenphp_log_message(const char *message, int syslog_type_int) {
13161372}
13171373
13181374static char * frankenphp_getenv (const char * name , size_t name_len ) {
1319- HashTable * ht = sandboxed_env ? sandboxed_env : main_thread_env ;
1320-
1321- zval * env_val = zend_hash_str_find (ht , name , name_len );
1375+ zval * env_val = frankenphp_lookup_env (name , name_len );
13221376 if (env_val && Z_TYPE_P (env_val ) == IS_STRING ) {
13231377 zend_string * str = Z_STR_P (env_val );
13241378 return ZSTR_VAL (str );
@@ -1381,6 +1435,22 @@ static inline void reset_sandboxed_environment() {
13811435 zend_hash_release (sandboxed_env );
13821436 sandboxed_env = NULL ;
13831437 }
1438+ if (prepared_env != NULL ) {
1439+ zend_hash_release (prepared_env );
1440+ prepared_env = NULL ;
1441+ }
1442+ }
1443+
1444+ /* Adds a key/value pair to the per-thread prepared environment, exposing
1445+ * env vars from the php(_server) directive to getenv() and $_ENV. */
1446+ void frankenphp_add_to_prepared_env (char * name , size_t name_len , char * val ,
1447+ size_t val_len , size_t size ) {
1448+ if (prepared_env == NULL ) {
1449+ prepared_env = zend_new_array (size );
1450+ }
1451+ zval zv = {0 };
1452+ ZVAL_STRINGL (& zv , val , val_len );
1453+ zend_hash_str_update (prepared_env , name , name_len , & zv );
13841454}
13851455
13861456static void * php_thread (void * arg ) {
0 commit comments