diff --git a/cgi.go b/cgi.go index b105801891..8467b5286b 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_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_prepared_env // #include "frankenphp.h" // #include import "C" @@ -180,6 +182,14 @@ func addPreparedEnvToServer(fc *frankenPHPContext, trackVarsArray *C.zval) { 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 { + C.frankenphp_add_to_prepared_env(toUnsafeChar(k), C.size_t(len(k)-1), toUnsafeChar(v), C.size_t(len(v)), size) + } +} + //export go_register_server_variables func go_register_server_variables(threadIndex C.uintptr_t, trackVarsArray *C.zval) { thread := phpThreads[threadIndex] @@ -298,6 +308,8 @@ func go_update_request_info(threadIndex C.uintptr_t, info *C.sapi_request_info) return nil } + addPreparedEnvToGetenv(fc) + if m, ok := cStringHTTPMethods[request.Method]; ok { info.request_method = m } else { diff --git a/frankenphp.c b/frankenphp.c index 93468afa83..6b5028ff9b 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`, 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 * 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() */ @@ -536,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 */ @@ -571,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; @@ -582,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); @@ -1174,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); @@ -1239,6 +1286,22 @@ 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 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 size) { + if (prepared_env == NULL) { + prepared_env = zend_new_array(size); + } + 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 31df007f18..651fb4a119 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_prepared_env(char *name, size_t name_len, char *val, + size_t val_len, size_t size); 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..c13aba1a93 100644 --- a/frankenphp_test.go +++ b/frankenphp_test.go @@ -767,6 +767,69 @@ 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) { + 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'\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) +} + // 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..b7d02d6730 --- /dev/null +++ b/testdata/env/prepared-env-getenv.php @@ -0,0 +1,12 @@ +