From b52bf388419122d977568bf07c59ebd2322618a6 Mon Sep 17 00:00:00 2001 From: henderkes Date: Thu, 28 May 2026 09:07:59 +0700 Subject: [PATCH 1/8] add php_server's env vars to sandboxed environment keep track of separate prepared_env set for a php_server correctly exposes env vars into $_ENV when E is in `variables_order` also fixes the problem of lazy server eval I didn't think about last commit --- cgi.go | 11 ++++++++ frankenphp.c | 40 ++++++++++++++++++++++++++-- frankenphp.h | 2 ++ frankenphp_test.go | 19 +++++++++++++ testdata/env/prepared-env-getenv.php | 11 ++++++++ 5 files changed, 81 insertions(+), 2 deletions(-) create mode 100644 testdata/env/prepared-env-getenv.php diff --git a/cgi.go b/cgi.go index b105801891..c833629ebd 100644 --- a/cgi.go +++ b/cgi.go @@ -4,10 +4,12 @@ package frankenphp // #cgo nocallback frankenphp_register_variable_safe // #cgo nocallback frankenphp_register_known_variable // #cgo nocallback frankenphp_init_persistent_string +// #cgo nocallback frankenphp_add_to_sandboxed_env // #cgo noescape frankenphp_register_server_vars // #cgo noescape frankenphp_register_variable_safe // #cgo noescape frankenphp_register_known_variable // #cgo noescape frankenphp_init_persistent_string +// #cgo noescape frankenphp_add_to_sandboxed_env // #include "frankenphp.h" // #include import "C" @@ -180,6 +182,13 @@ func addPreparedEnvToServer(fc *frankenPHPContext, trackVarsArray *C.zval) { fc.env = nil } +// addPreparedEnvToSandbox exposes fc.env to getenv() before any PHP code runs. +func addPreparedEnvToSandbox(fc *frankenPHPContext) { + for k, v := range fc.env { + C.frankenphp_add_to_sandboxed_env(toUnsafeChar(k), C.size_t(len(k)-1), toUnsafeChar(v), C.size_t(len(v))) + } +} + //export go_register_server_variables func go_register_server_variables(threadIndex C.uintptr_t, trackVarsArray *C.zval) { thread := phpThreads[threadIndex] @@ -298,6 +307,8 @@ func go_update_request_info(threadIndex C.uintptr_t, info *C.sapi_request_info) return nil } + addPreparedEnvToSandbox(fc) + if m, ok := cStringHTTPMethods[request.Method]; ok { info.request_method = m } else { diff --git a/frankenphp.c b/frankenphp.c index 93468afa83..91a666f0f8 100644 --- a/frankenphp.c +++ b/frankenphp.c @@ -92,6 +92,10 @@ HashTable *main_thread_env = NULL; __thread uintptr_t thread_index; __thread bool is_worker_thread = false; __thread HashTable *sandboxed_env = NULL; +/* prepared_env holds entries from php(_server)'s `env KEY VAL`, + * so they can be merged into $_ENV when 'E' is in + * variables_order. Separate from putenv() so we don't leak into $_ENV */ +__thread HashTable *prepared_env = NULL; /* Published via SG(server_context) so ext-parallel children, which inherit * the parent's SG(server_context), can route SAPI callbacks back to the @@ -429,9 +433,17 @@ bool frankenphp_shutdown_dummy_request(void) { } void get_full_env(zval *track_vars_array) { - zend_hash_extend(Z_ARR_P(track_vars_array), - zend_hash_num_elements(main_thread_env), 0); + size_t total = zend_hash_num_elements(main_thread_env); + if (prepared_env != NULL) { + // perf: doesn't matter if we get the exact count, just >= needed + total += zend_hash_num_elements(prepared_env); + } + zend_hash_extend(Z_ARR_P(track_vars_array), total, 0); zend_hash_copy(Z_ARR_P(track_vars_array), main_thread_env, NULL); + if (prepared_env != NULL) { + zend_hash_copy(Z_ARR_P(track_vars_array), prepared_env, + (copy_ctor_func_t)zval_add_ref); + } } /* Adapted from php_request_startup() */ @@ -1239,6 +1251,30 @@ static inline void reset_sandboxed_environment() { zend_hash_release(sandboxed_env); sandboxed_env = NULL; } + if (prepared_env != NULL) { + zend_hash_release(prepared_env); + prepared_env = NULL; + } +} + +/* Adds a key/value pair to the per-thread sandboxed environment so it becomes + * visible to getenv()/$_ENV. Used to expose env vars declared in the + * php(_server) directive, which would otherwise only appear in $_SERVER. */ +void frankenphp_add_to_sandboxed_env(char *name, size_t name_len, char *val, + size_t val_len) { + if (sandboxed_env == NULL) { + sandboxed_env = zend_array_dup(main_thread_env); + } + zval zv = {0}; + ZVAL_STRINGL(&zv, val, val_len); + zend_hash_str_update(sandboxed_env, name, name_len, &zv); + + if (prepared_env == NULL) { + prepared_env = zend_new_array(8); + } + zval zv2 = {0}; + ZVAL_STRINGL(&zv2, val, val_len); + zend_hash_str_update(prepared_env, name, name_len, &zv2); } static void *php_thread(void *arg) { diff --git a/frankenphp.h b/frankenphp.h index 31df007f18..b19437d6ec 100644 --- a/frankenphp.h +++ b/frankenphp.h @@ -202,6 +202,8 @@ void frankenphp_register_variable_safe(char *key, char *var, size_t val_len, zval *track_vars_array); void frankenphp_register_server_vars(zval *track_vars_array, frankenphp_server_vars vars); +void frankenphp_add_to_sandboxed_env(char *name, size_t name_len, char *val, + size_t val_len); zend_string *frankenphp_init_persistent_string(const char *string, size_t len); int frankenphp_reset_opcache(void); diff --git a/frankenphp_test.go b/frankenphp_test.go index f5355784cf..cccb58984c 100644 --- a/frankenphp_test.go +++ b/frankenphp_test.go @@ -767,6 +767,25 @@ func TestEnvIsNotResetInWorkerMode(t *testing.T) { }, &testOptions{workerScript: "env/remember-env.php"}) } +// reproduction of https://github.com/php/frankenphp/issues/1674 +func TestPreparedEnvIsVisibleToGetenv_module(t *testing.T) { + testPreparedEnvIsVisibleToGetenv(t, &testOptions{nbParallelRequests: 1}) +} +func TestPreparedEnvIsVisibleToGetenv_worker(t *testing.T) { + testPreparedEnvIsVisibleToGetenv(t, &testOptions{ + workerScript: "env/prepared-env-getenv.php", + }) +} +func testPreparedEnvIsVisibleToGetenv(t *testing.T, opts *testOptions) { + opts.requestOpts = append(opts.requestOpts, + frankenphp.WithRequestEnv(map[string]string{"FRANKENPHP_TEST_PHP_SERVER_ENV_IN_GETENV": "hello"}), + ) + runTest(t, func(handler func(http.ResponseWriter, *http.Request), _ *httptest.Server, _ int) { + body, _ := testGet("http://example.com/env/prepared-env-getenv.php", handler, t) + assert.Equal(t, "getenv='hello'\nserver='hello'\n", body) + }, opts) +} + // reproduction of https://github.com/php/frankenphp/issues/1061 func TestModificationsToEnvPersistAcrossRequests(t *testing.T) { runTest(t, func(handler func(http.ResponseWriter, *http.Request), _ *httptest.Server, i int) { diff --git a/testdata/env/prepared-env-getenv.php b/testdata/env/prepared-env-getenv.php new file mode 100644 index 0000000000..840d6a8031 --- /dev/null +++ b/testdata/env/prepared-env-getenv.php @@ -0,0 +1,11 @@ + Date: Fri, 29 May 2026 18:55:22 +0700 Subject: [PATCH 2/8] check sandboxed_env -> prepared_env -> main_thread_env --- cgi.go | 12 ++++---- frankenphp.c | 77 +++++++++++++++++++++++++++++++++++----------------- frankenphp.h | 4 +-- 3 files changed, 60 insertions(+), 33 deletions(-) diff --git a/cgi.go b/cgi.go index c833629ebd..5c914456da 100644 --- a/cgi.go +++ b/cgi.go @@ -4,12 +4,12 @@ package frankenphp // #cgo nocallback frankenphp_register_variable_safe // #cgo nocallback frankenphp_register_known_variable // #cgo nocallback frankenphp_init_persistent_string -// #cgo nocallback frankenphp_add_to_sandboxed_env +// #cgo nocallback frankenphp_add_to_prepared_env // #cgo noescape frankenphp_register_server_vars // #cgo noescape frankenphp_register_variable_safe // #cgo noescape frankenphp_register_known_variable // #cgo noescape frankenphp_init_persistent_string -// #cgo noescape frankenphp_add_to_sandboxed_env +// #cgo noescape frankenphp_add_to_prepared_env // #include "frankenphp.h" // #include import "C" @@ -182,10 +182,10 @@ func addPreparedEnvToServer(fc *frankenPHPContext, trackVarsArray *C.zval) { fc.env = nil } -// addPreparedEnvToSandbox exposes fc.env to getenv() before any PHP code runs. -func addPreparedEnvToSandbox(fc *frankenPHPContext) { +// addPreparedEnvToGetenv exposes fc.env to getenv() before any PHP code runs. +func addPreparedEnvToGetenv(fc *frankenPHPContext) { for k, v := range fc.env { - C.frankenphp_add_to_sandboxed_env(toUnsafeChar(k), C.size_t(len(k)-1), toUnsafeChar(v), C.size_t(len(v))) + C.frankenphp_add_to_prepared_env(toUnsafeChar(k), C.size_t(len(k)-1), toUnsafeChar(v), C.size_t(len(v))) } } @@ -307,7 +307,7 @@ func go_update_request_info(threadIndex C.uintptr_t, info *C.sapi_request_info) return nil } - addPreparedEnvToSandbox(fc) + addPreparedEnvToGetenv(fc) if m, ok := cStringHTTPMethods[request.Method]; ok { info.request_method = m diff --git a/frankenphp.c b/frankenphp.c index 91a666f0f8..cadc3ec499 100644 --- a/frankenphp.c +++ b/frankenphp.c @@ -92,9 +92,9 @@ HashTable *main_thread_env = NULL; __thread uintptr_t thread_index; __thread bool is_worker_thread = false; __thread HashTable *sandboxed_env = NULL; -/* prepared_env holds entries from php(_server)'s `env KEY VAL`, - * so they can be merged into $_ENV when 'E' is in - * variables_order. Separate from putenv() so we don't leak into $_ENV */ +/* prepared_env holds entries from php(_server)'s `env KEY VAL`, exposed to + * getenv() and merged into $_ENV when 'E' is in variables_order. Separate from + * putenv() so those don't leak into $_ENV. */ __thread HashTable *prepared_env = NULL; /* Published via SG(server_context) so ext-parallel children, which inherit @@ -548,6 +548,13 @@ PHP_FUNCTION(frankenphp_putenv) { if (sandboxed_env == NULL) { sandboxed_env = zend_array_dup(main_thread_env); + /* prepared_env overrides the OS env and putenv() overrides both, so layer + * the prepared vars onto the dup before sandboxed_env starts shadowing the + * other two layers in getenv(). */ + if (prepared_env != NULL) { + zend_hash_copy(sandboxed_env, prepared_env, + (copy_ctor_func_t)zval_add_ref); + } } /* cut at null byte to stay consistent with regular putenv */ @@ -583,6 +590,38 @@ PHP_FUNCTION(frankenphp_putenv) { RETURN_BOOL(success); } /* }}} */ +/* getenv() lookup: sandboxed_env if present (it already holds prepared + OS), + * otherwise prepared_env then main_thread_env. */ +static zval *frankenphp_lookup_env(const char *name, size_t name_len) { + if (sandboxed_env != NULL) { + return zend_hash_str_find(sandboxed_env, name, name_len); + } + + zval *env_val = NULL; + if (prepared_env != NULL) { + env_val = zend_hash_str_find(prepared_env, name, name_len); + } + if (env_val == NULL) { + env_val = zend_hash_str_find(main_thread_env, name, name_len); + } + + return env_val; +} + +/* Returns a fresh copy of the full environment, merging the layers above. */ +static HashTable *frankenphp_dup_env(void) { + if (sandboxed_env != NULL) { + return zend_array_dup(sandboxed_env); + } + + HashTable *env = zend_array_dup(main_thread_env); + if (prepared_env != NULL) { + zend_hash_copy(env, prepared_env, (copy_ctor_func_t)zval_add_ref); + } + + return env; +} + /* {{{ Get the env from the sandboxed environment */ PHP_FUNCTION(frankenphp_getenv) { zend_string *name = NULL; @@ -594,14 +633,12 @@ PHP_FUNCTION(frankenphp_getenv) { Z_PARAM_BOOL(local_only) ZEND_PARSE_PARAMETERS_END(); - HashTable *ht = sandboxed_env ? sandboxed_env : main_thread_env; - if (!name) { - RETURN_ARR(zend_array_dup(ht)); + RETURN_ARR(frankenphp_dup_env()); return; } - zval *env_val = zend_hash_find(ht, name); + zval *env_val = frankenphp_lookup_env(ZSTR_VAL(name), ZSTR_LEN(name)); if (env_val && Z_TYPE_P(env_val) == IS_STRING) { zend_string *str = Z_STR_P(env_val); zend_string_addref(str); @@ -1186,9 +1223,7 @@ static void frankenphp_log_message(const char *message, int syslog_type_int) { } static char *frankenphp_getenv(const char *name, size_t name_len) { - HashTable *ht = sandboxed_env ? sandboxed_env : main_thread_env; - - zval *env_val = zend_hash_str_find(ht, name, name_len); + zval *env_val = frankenphp_lookup_env(name, name_len); if (env_val && Z_TYPE_P(env_val) == IS_STRING) { zend_string *str = Z_STR_P(env_val); return ZSTR_VAL(str); @@ -1257,24 +1292,16 @@ static inline void reset_sandboxed_environment() { } } -/* Adds a key/value pair to the per-thread sandboxed environment so it becomes - * visible to getenv()/$_ENV. Used to expose env vars declared in the - * php(_server) directive, which would otherwise only appear in $_SERVER. */ -void frankenphp_add_to_sandboxed_env(char *name, size_t name_len, char *val, - size_t val_len) { - if (sandboxed_env == NULL) { - sandboxed_env = zend_array_dup(main_thread_env); - } - zval zv = {0}; - ZVAL_STRINGL(&zv, val, val_len); - zend_hash_str_update(sandboxed_env, name, name_len, &zv); - +/* Adds a key/value pair to the per-thread prepared environment, exposing + * env vars from the php(_server) directive to getenv() and $_ENV. */ +void frankenphp_add_to_prepared_env(char *name, size_t name_len, char *val, + size_t val_len) { if (prepared_env == NULL) { prepared_env = zend_new_array(8); } - zval zv2 = {0}; - ZVAL_STRINGL(&zv2, val, val_len); - zend_hash_str_update(prepared_env, name, name_len, &zv2); + zval zv = {0}; + ZVAL_STRINGL(&zv, val, val_len); + zend_hash_str_update(prepared_env, name, name_len, &zv); } static void *php_thread(void *arg) { diff --git a/frankenphp.h b/frankenphp.h index b19437d6ec..c985c22985 100644 --- a/frankenphp.h +++ b/frankenphp.h @@ -202,8 +202,8 @@ void frankenphp_register_variable_safe(char *key, char *var, size_t val_len, zval *track_vars_array); void frankenphp_register_server_vars(zval *track_vars_array, frankenphp_server_vars vars); -void frankenphp_add_to_sandboxed_env(char *name, size_t name_len, char *val, - size_t val_len); +void frankenphp_add_to_prepared_env(char *name, size_t name_len, char *val, + size_t val_len); zend_string *frankenphp_init_persistent_string(const char *string, size_t len); int frankenphp_reset_opcache(void); From 28defe0c06a24eebfa6f2771858b58c41ec19b17 Mon Sep 17 00:00:00 2001 From: henderkes Date: Sat, 30 May 2026 19:10:20 +0700 Subject: [PATCH 3/8] prefill size of zend_new_array --- cgi.go | 3 ++- frankenphp.c | 4 ++-- frankenphp.h | 2 +- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/cgi.go b/cgi.go index 5c914456da..8467b5286b 100644 --- a/cgi.go +++ b/cgi.go @@ -184,8 +184,9 @@ func addPreparedEnvToServer(fc *frankenPHPContext, trackVarsArray *C.zval) { // addPreparedEnvToGetenv exposes fc.env to getenv() before any PHP code runs. func addPreparedEnvToGetenv(fc *frankenPHPContext) { + size := C.size_t(len(fc.env)) for k, v := range fc.env { - C.frankenphp_add_to_prepared_env(toUnsafeChar(k), C.size_t(len(k)-1), toUnsafeChar(v), C.size_t(len(v))) + C.frankenphp_add_to_prepared_env(toUnsafeChar(k), C.size_t(len(k)-1), toUnsafeChar(v), C.size_t(len(v)), size) } } diff --git a/frankenphp.c b/frankenphp.c index cadc3ec499..6b5028ff9b 100644 --- a/frankenphp.c +++ b/frankenphp.c @@ -1295,9 +1295,9 @@ static inline void reset_sandboxed_environment() { /* Adds a key/value pair to the per-thread prepared environment, exposing * env vars from the php(_server) directive to getenv() and $_ENV. */ void frankenphp_add_to_prepared_env(char *name, size_t name_len, char *val, - size_t val_len) { + size_t val_len, size_t size) { if (prepared_env == NULL) { - prepared_env = zend_new_array(8); + prepared_env = zend_new_array(size); } zval zv = {0}; ZVAL_STRINGL(&zv, val, val_len); diff --git a/frankenphp.h b/frankenphp.h index c985c22985..651fb4a119 100644 --- a/frankenphp.h +++ b/frankenphp.h @@ -203,7 +203,7 @@ void frankenphp_register_variable_safe(char *key, char *var, size_t val_len, void frankenphp_register_server_vars(zval *track_vars_array, frankenphp_server_vars vars); void frankenphp_add_to_prepared_env(char *name, size_t name_len, char *val, - size_t val_len); + size_t val_len, size_t size); zend_string *frankenphp_init_persistent_string(const char *string, size_t len); int frankenphp_reset_opcache(void); From e913a15dc811f4bba6b8d6e0b6ec7345e82c7b63 Mon Sep 17 00:00:00 2001 From: henderkes Date: Sat, 30 May 2026 19:10:35 +0700 Subject: [PATCH 4/8] extra tests as requested --- frankenphp_test.go | 46 ++++++++++++++++++- testdata/env/prepared-env-getenv.php | 1 + testdata/env/prepared-env-survives-putenv.php | 10 ++++ 3 files changed, 56 insertions(+), 1 deletion(-) create mode 100644 testdata/env/prepared-env-survives-putenv.php diff --git a/frankenphp_test.go b/frankenphp_test.go index cccb58984c..c13aba1a93 100644 --- a/frankenphp_test.go +++ b/frankenphp_test.go @@ -777,12 +777,56 @@ func TestPreparedEnvIsVisibleToGetenv_worker(t *testing.T) { }) } func testPreparedEnvIsVisibleToGetenv(t *testing.T, opts *testOptions) { + if opts.phpIni == nil { + opts.phpIni = map[string]string{} + } + opts.phpIni["variables_order"] = "EGPCS" + opts.requestOpts = append(opts.requestOpts, + frankenphp.WithRequestEnv(map[string]string{"FRANKENPHP_TEST_PHP_SERVER_ENV_IN_GETENV": "hello"}), + ) + + expectedEnv := "'hello'" + if opts.workerScript != "" { + // workers don't populate $_ENV regardless or variables_order + expectedEnv = "NULL" + } + + runTest(t, func(handler func(http.ResponseWriter, *http.Request), _ *httptest.Server, _ int) { + body, _ := testGet("http://example.com/env/prepared-env-getenv.php", handler, t) + assert.Equal(t, fmt.Sprintf("getenv='hello'\nserver='hello'\nenv=%s\n", expectedEnv), body) + }, opts) +} + +// $_ENV mustn't be filled with prepared_env without E in variables_order +func TestPreparedEnvIsNotInEnvWithoutVariablesOrderE(t *testing.T) { + opts := &testOptions{ + nbParallelRequests: 1, + phpIni: map[string]string{"variables_order": "GPCS"}, + } opts.requestOpts = append(opts.requestOpts, frankenphp.WithRequestEnv(map[string]string{"FRANKENPHP_TEST_PHP_SERVER_ENV_IN_GETENV": "hello"}), ) runTest(t, func(handler func(http.ResponseWriter, *http.Request), _ *httptest.Server, _ int) { body, _ := testGet("http://example.com/env/prepared-env-getenv.php", handler, t) - assert.Equal(t, "getenv='hello'\nserver='hello'\n", body) + assert.Equal(t, "getenv='hello'\nserver='hello'\nenv=NULL\n", body) + }, opts) +} + +func TestPreparedEnvSurvivesPutenv_module(t *testing.T) { + testPreparedEnvSurvivesPutenv(t, &testOptions{nbParallelRequests: 1}) +} +func TestPreparedEnvSurvivesPutenv_worker(t *testing.T) { + testPreparedEnvSurvivesPutenv(t, &testOptions{ + workerScript: "env/prepared-env-survives-putenv.php", + }) +} +func testPreparedEnvSurvivesPutenv(t *testing.T, opts *testOptions) { + opts.requestOpts = append(opts.requestOpts, + frankenphp.WithRequestEnv(map[string]string{"FRANKENPHP_PREPARED": "prepared_value"}), + ) + runTest(t, func(handler func(http.ResponseWriter, *http.Request), _ *httptest.Server, _ int) { + body, _ := testGet("http://example.com/env/prepared-env-survives-putenv.php", handler, t) + assert.Equal(t, "before='prepared_value'\nprepared='prepared_value'\nput='put_value'\n", body) }, opts) } diff --git a/testdata/env/prepared-env-getenv.php b/testdata/env/prepared-env-getenv.php index 840d6a8031..b7d02d6730 100644 --- a/testdata/env/prepared-env-getenv.php +++ b/testdata/env/prepared-env-getenv.php @@ -8,4 +8,5 @@ // See https://github.com/php/frankenphp/issues/1674 echo "getenv=" . var_export(getenv('FRANKENPHP_TEST_PHP_SERVER_ENV_IN_GETENV'), true) . "\n"; echo "server=" . var_export($_SERVER['FRANKENPHP_TEST_PHP_SERVER_ENV_IN_GETENV'] ?? null, true) . "\n"; + echo "env=" . var_export($_ENV['FRANKENPHP_TEST_PHP_SERVER_ENV_IN_GETENV'] ?? null, true) . "\n"; }; diff --git a/testdata/env/prepared-env-survives-putenv.php b/testdata/env/prepared-env-survives-putenv.php new file mode 100644 index 0000000000..bd7bdaf0cf --- /dev/null +++ b/testdata/env/prepared-env-survives-putenv.php @@ -0,0 +1,10 @@ + Date: Sun, 31 May 2026 14:01:53 +0200 Subject: [PATCH 5/8] merges prepared env from the C side --- cgi.go | 21 +++++++++------------ frankenphp.c | 11 +++++++++-- frankenphp.h | 1 + 3 files changed, 19 insertions(+), 14 deletions(-) diff --git a/cgi.go b/cgi.go index 8467b5286b..954a198e62 100644 --- a/cgi.go +++ b/cgi.go @@ -175,17 +175,10 @@ func addHeadersToServer(ctx context.Context, request *http.Request, trackVarsArr } } -func addPreparedEnvToServer(fc *frankenPHPContext, trackVarsArray *C.zval) { - for k, v := range fc.env { - C.frankenphp_register_variable_safe(toUnsafeChar(k), toUnsafeChar(v), C.size_t(len(v)), trackVarsArray) - } - fc.env = nil -} - // addPreparedEnvToGetenv exposes fc.env to getenv() before any PHP code runs. -func addPreparedEnvToGetenv(fc *frankenPHPContext) { - size := C.size_t(len(fc.env)) - for k, v := range fc.env { +func addPreparedEnvToGetenv(env PreparedEnv) { + size := C.size_t(len(env)) + for k, v := range env { C.frankenphp_add_to_prepared_env(toUnsafeChar(k), C.size_t(len(k)-1), toUnsafeChar(v), C.size_t(len(v)), size) } } @@ -201,7 +194,9 @@ func go_register_server_variables(threadIndex C.uintptr_t, trackVarsArray *C.zva } // The Prepared Environment is registered last and can overwrite any previous values - addPreparedEnvToServer(fc, trackVarsArray) + if fc.env != nil { + C.frankenphp_merge_with_prepared_env(trackVarsArray) + } } // splitCgiPath splits the request path into SCRIPT_NAME, SCRIPT_FILENAME, PATH_INFO, DOCUMENT_URI @@ -308,7 +303,9 @@ func go_update_request_info(threadIndex C.uintptr_t, info *C.sapi_request_info) return nil } - addPreparedEnvToGetenv(fc) + if fc.env != nil { + addPreparedEnvToGetenv(fc.env) + } if m, ok := cStringHTTPMethods[request.Method]; ok { info.request_method = m diff --git a/frankenphp.c b/frankenphp.c index 6b5028ff9b..183cd93fe4 100644 --- a/frankenphp.c +++ b/frankenphp.c @@ -592,7 +592,7 @@ PHP_FUNCTION(frankenphp_putenv) { /* getenv() lookup: sandboxed_env if present (it already holds prepared + OS), * otherwise prepared_env then main_thread_env. */ -static zval *frankenphp_lookup_env(const char *name, size_t name_len) { +static inline zval *frankenphp_lookup_env(const char *name, size_t name_len) { if (sandboxed_env != NULL) { return zend_hash_str_find(sandboxed_env, name, name_len); } @@ -609,7 +609,7 @@ static zval *frankenphp_lookup_env(const char *name, size_t name_len) { } /* Returns a fresh copy of the full environment, merging the layers above. */ -static HashTable *frankenphp_dup_env(void) { +static inline HashTable *frankenphp_dup_env(void) { if (sandboxed_env != NULL) { return zend_array_dup(sandboxed_env); } @@ -1110,6 +1110,13 @@ void frankenphp_register_server_vars(zval *track_vars_array, zend_hash_update_ind(ht, frankenphp_strings.remote_ident, &zv); } +void frankenphp_merge_with_prepared_env(zval *track_vars_array) { + if (prepared_env != NULL) { + HashTable *ht = Z_ARRVAL_P(track_vars_array); + zend_hash_copy(ht, prepared_env, (copy_ctor_func_t)zval_add_ref); + } +} + /** Create an immutable zend_string that lasts for the whole process **/ zend_string *frankenphp_init_persistent_string(const char *string, size_t len) { /* persistent strings will be ignored by the GC at the end of a request */ diff --git a/frankenphp.h b/frankenphp.h index 651fb4a119..6923e9f71e 100644 --- a/frankenphp.h +++ b/frankenphp.h @@ -204,6 +204,7 @@ void frankenphp_register_server_vars(zval *track_vars_array, frankenphp_server_vars vars); void frankenphp_add_to_prepared_env(char *name, size_t name_len, char *val, size_t val_len, size_t size); +void frankenphp_merge_with_prepared_env(zval *track_vars_array); zend_string *frankenphp_init_persistent_string(const char *string, size_t len); int frankenphp_reset_opcache(void); From 19cd490673b7ca4b0b0faa00869ef5965c6192ab Mon Sep 17 00:00:00 2001 From: Alliballibaba Date: Sun, 31 May 2026 14:05:56 +0200 Subject: [PATCH 6/8] uses length checks instead. --- cgi.go | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/cgi.go b/cgi.go index 954a198e62..1747da4395 100644 --- a/cgi.go +++ b/cgi.go @@ -175,8 +175,8 @@ func addHeadersToServer(ctx context.Context, request *http.Request, trackVarsArr } } -// addPreparedEnvToGetenv exposes fc.env to getenv() before any PHP code runs. -func addPreparedEnvToGetenv(env PreparedEnv) { +// registerPreparedEnv exposes fc.env to getenv() before any PHP code runs. +func registerPreparedEnv(env PreparedEnv) { size := C.size_t(len(env)) for k, v := range env { C.frankenphp_add_to_prepared_env(toUnsafeChar(k), C.size_t(len(k)-1), toUnsafeChar(v), C.size_t(len(v)), size) @@ -194,7 +194,7 @@ func go_register_server_variables(threadIndex C.uintptr_t, trackVarsArray *C.zva } // The Prepared Environment is registered last and can overwrite any previous values - if fc.env != nil { + if len(fc.env) != 0 { C.frankenphp_merge_with_prepared_env(trackVarsArray) } } @@ -303,8 +303,8 @@ func go_update_request_info(threadIndex C.uintptr_t, info *C.sapi_request_info) return nil } - if fc.env != nil { - addPreparedEnvToGetenv(fc.env) + if len(fc.env) != 0 { + registerPreparedEnv(fc.env) } if m, ok := cStringHTTPMethods[request.Method]; ok { From a3fe9b925fcc59a7ca909ab7344ecaa50fdd908d Mon Sep 17 00:00:00 2001 From: Alliballibaba Date: Sun, 31 May 2026 14:58:13 +0200 Subject: [PATCH 7/8] finishes all scaling before shutdown --- frankenphp.go | 1 - phpmainthread.go | 4 ++++ scaling.go | 10 ---------- 3 files changed, 4 insertions(+), 11 deletions(-) diff --git a/frankenphp.go b/frankenphp.go index 52246d01c7..b034dc25d2 100644 --- a/frankenphp.go +++ b/frankenphp.go @@ -370,7 +370,6 @@ func Shutdown() { } drainWatchers() - drainAutoScaling() drainPHPThreads() metrics.Shutdown() diff --git a/phpmainthread.go b/phpmainthread.go index b892d52f19..d377bf0403 100644 --- a/phpmainthread.go +++ b/phpmainthread.go @@ -78,6 +78,10 @@ func initPHPThreads(numThreads int, numMaxThreads int, phpIni map[string]string) } func drainPHPThreads() { + // disallow scaling during the shutdown process + scalingMu.Lock() + defer scalingMu.Unlock() + if mainThread == nil { return // mainThread was never initialized } diff --git a/scaling.go b/scaling.go index c606925fdf..1489d6b570 100644 --- a/scaling.go +++ b/scaling.go @@ -53,16 +53,6 @@ func initAutoScaling(mainThread *phpMainThread) { go startDownScalingThreads(done) } -func drainAutoScaling() { - scalingMu.Lock() - - if globalLogger.Enabled(globalCtx, slog.LevelDebug) { - globalLogger.LogAttrs(globalCtx, slog.LevelDebug, "shutting down autoscaling", slog.Int("autoScaledThreads", len(autoScaledThreads))) - } - - scalingMu.Unlock() -} - func addRegularThread() (*phpThread, error) { thread := getInactivePHPThread() if thread == nil { From 34f736b4243b0550b6e9e0e34a0790611816482e Mon Sep 17 00:00:00 2001 From: Alliballibaba Date: Sun, 31 May 2026 15:16:49 +0200 Subject: [PATCH 8/8] Revert "finishes all scaling before shutdown" This reverts commit a3fe9b925fcc59a7ca909ab7344ecaa50fdd908d. --- frankenphp.go | 1 + phpmainthread.go | 4 ---- scaling.go | 10 ++++++++++ 3 files changed, 11 insertions(+), 4 deletions(-) diff --git a/frankenphp.go b/frankenphp.go index b034dc25d2..52246d01c7 100644 --- a/frankenphp.go +++ b/frankenphp.go @@ -370,6 +370,7 @@ func Shutdown() { } drainWatchers() + drainAutoScaling() drainPHPThreads() metrics.Shutdown() diff --git a/phpmainthread.go b/phpmainthread.go index d377bf0403..b892d52f19 100644 --- a/phpmainthread.go +++ b/phpmainthread.go @@ -78,10 +78,6 @@ func initPHPThreads(numThreads int, numMaxThreads int, phpIni map[string]string) } func drainPHPThreads() { - // disallow scaling during the shutdown process - scalingMu.Lock() - defer scalingMu.Unlock() - if mainThread == nil { return // mainThread was never initialized } diff --git a/scaling.go b/scaling.go index 1489d6b570..c606925fdf 100644 --- a/scaling.go +++ b/scaling.go @@ -53,6 +53,16 @@ func initAutoScaling(mainThread *phpMainThread) { go startDownScalingThreads(done) } +func drainAutoScaling() { + scalingMu.Lock() + + if globalLogger.Enabled(globalCtx, slog.LevelDebug) { + globalLogger.LogAttrs(globalCtx, slog.LevelDebug, "shutting down autoscaling", slog.Int("autoScaledThreads", len(autoScaledThreads))) + } + + scalingMu.Unlock() +} + func addRegularThread() (*phpThread, error) { thread := getInactivePHPThread() if thread == nil {