Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions cgi.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 <php_variables.h>
import "C"
Expand Down Expand Up @@ -180,6 +182,13 @@ 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) {
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)))
}
}

//export go_register_server_variables
func go_register_server_variables(threadIndex C.uintptr_t, trackVarsArray *C.zval) {
thread := phpThreads[threadIndex]
Expand Down Expand Up @@ -298,6 +307,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 {
Expand Down
81 changes: 72 additions & 9 deletions frankenphp.c
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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() */
Expand Down Expand Up @@ -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 */
Expand Down Expand Up @@ -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;
Expand All @@ -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);
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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) {
if (prepared_env == NULL) {
prepared_env = zend_new_array(8);
}
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) {
Expand Down
2 changes: 2 additions & 0 deletions frankenphp.h
Original file line number Diff line number Diff line change
Expand Up @@ -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);

zend_string *frankenphp_init_persistent_string(const char *string, size_t len);
int frankenphp_reset_opcache(void);
Expand Down
19 changes: 19 additions & 0 deletions frankenphp_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
11 changes: 11 additions & 0 deletions testdata/env/prepared-env-getenv.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<?php

require_once __DIR__ . '/../_executor.php';

return function () {
// Variables declared via the env subdirective in the php(_server) directive
// (or via WithRequestEnv) must be exposed to both $_SERVER and getenv().
// 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";
};
Loading