Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
ca19712
Cleanup.
AlliBalliBaba Nov 1, 2025
5b7fbab
Formatting.
AlliBalliBaba Nov 1, 2025
0cebb74
Moves state to own module.
AlliBalliBaba Nov 1, 2025
6dc3432
Refactoring.
AlliBalliBaba Nov 1, 2025
7c8813e
Also moves php_headers test.
AlliBalliBaba Nov 1, 2025
1436802
tests with -vet=off
AlliBalliBaba Nov 6, 2025
7c79d7a
tests with ./go.sh vet before.
AlliBalliBaba Nov 6, 2025
f5bb4e0
Moves env logic to the C side.
AlliBalliBaba Nov 8, 2025
06a329b
Cleanup.
AlliBalliBaba Nov 8, 2025
87123bd
import C test.
AlliBalliBaba Nov 10, 2025
4161623
adds turns state into string
AlliBalliBaba Nov 11, 2025
a36547b
suggestion: simplify exponential backoff (#1970)
AlliBalliBaba Nov 13, 2025
b05964f
Merge branch 'main' into refator/cleanup-c
AlliBalliBaba Nov 13, 2025
42b2ffa
changes log to the documented version.
AlliBalliBaba Nov 13, 2025
26e1408
go linting.
AlliBalliBaba Nov 14, 2025
99e3b99
Merge branch 'main' into refactor/cleanup-env
AlliBalliBaba Nov 21, 2025
a116591
Merge branch 'main' into refator/cleanup-c
AlliBalliBaba Nov 21, 2025
bcd482a
Resolve merge conflicts.
AlliBalliBaba Nov 21, 2025
69f3d8d
Merge branch 'main' into refactor/cleanup-env
AlliBalliBaba Nov 30, 2025
c4e6605
better success checks.
AlliBalliBaba Nov 30, 2025
b8288b9
Merge branch 'main' into refator/cleanup-c
AlliBalliBaba Dec 2, 2025
2e1811c
Merge conflict fix.
AlliBalliBaba Dec 2, 2025
0199038
Merge branch 'refator/cleanup-c' into refactor/cleanup-env
AlliBalliBaba Dec 2, 2025
0e5964d
go fmt
AlliBalliBaba Dec 2, 2025
c116953
Merge branch 'main' into refactor/cleanup-env
AlliBalliBaba Dec 2, 2025
1d52c10
go fmt
AlliBalliBaba Dec 2, 2025
dde7906
Fixes tests.
AlliBalliBaba Dec 2, 2025
45a86af
clang-format
AlliBalliBaba Dec 4, 2025
c55dc34
makes wrong format even wronger.
AlliBalliBaba Dec 4, 2025
016b63d
Adds clarification.
AlliBalliBaba Dec 4, 2025
981b801
Removes test again as asan/msan will make insertions succeed.
AlliBalliBaba Dec 4, 2025
d3c5501
Adds test.
AlliBalliBaba Dec 11, 2025
3aa35f9
Adds custom env to start of tests.
AlliBalliBaba Dec 11, 2025
c803d7c
Never clears main thread env.
AlliBalliBaba Dec 11, 2025
3fc0d92
Adds putenv syntax check.
AlliBalliBaba Dec 11, 2025
e00a70e
Adds more tests.
AlliBalliBaba Dec 11, 2025
c578745
Fixes os env tests.
AlliBalliBaba Dec 12, 2025
75a07f8
cleanup.
AlliBalliBaba Dec 12, 2025
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
12 changes: 10 additions & 2 deletions caddy/caddy_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,16 @@ import (

var testPort = "9080"

func TestMain(m *testing.M) {
// setup custom environment vars for TestOsEnv
if os.Setenv("ENV1", "value1") != nil || os.Setenv("ENV2", "value2") != nil {
fmt.Println("Failed to set environment variables for tests")
os.Exit(1)
}

os.Exit(m.Run())
}

func TestPHP(t *testing.T) {
var wg sync.WaitGroup
tester := caddytest.NewTester(t)
Expand Down Expand Up @@ -904,8 +914,6 @@ func testSingleIniConfiguration(tester *caddytest.Tester, key string, value stri
}

func TestOsEnv(t *testing.T) {
os.Setenv("ENV1", "value1")
os.Setenv("ENV2", "value2")
tester := caddytest.NewTester(t)
tester.InitServer(`
{
Expand Down
109 changes: 16 additions & 93 deletions env.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,106 +11,29 @@ import (
"unsafe"
)

func initializeEnv() map[string]*C.zend_string {
env := os.Environ()
envMap := make(map[string]*C.zend_string, len(env))

for _, envVar := range env {
//export go_init_os_env
func go_init_os_env(mainThreadEnv *C.zend_array) {
for _, envVar := range os.Environ() {
key, val, _ := strings.Cut(envVar, "=")
envMap[key] = C.frankenphp_init_persistent_string(toUnsafeChar(val), C.size_t(len(val)))
}

return envMap
}

// get the main thread env or the thread specific env
func getSandboxedEnv(thread *phpThread) map[string]*C.zend_string {
if thread.sandboxedEnv != nil {
return thread.sandboxedEnv
}

return mainThread.sandboxedEnv
}

func clearSandboxedEnv(thread *phpThread) {
if thread.sandboxedEnv == nil {
return
}

for key, val := range thread.sandboxedEnv {
valInMainThread, ok := mainThread.sandboxedEnv[key]
if !ok || val != valInMainThread {
C.free(unsafe.Pointer(val))
}
}

thread.sandboxedEnv = nil
}

// if an env var already exists, it needs to be freed
func removeEnvFromThread(thread *phpThread, key string) {
valueInThread, existsInThread := thread.sandboxedEnv[key]
if !existsInThread {
return
}

valueInMainThread, ok := mainThread.sandboxedEnv[key]
if !ok || valueInThread != valueInMainThread {
C.free(unsafe.Pointer(valueInThread))
}

delete(thread.sandboxedEnv, key)
}
zkey := C.frankenphp_init_persistent_string(toUnsafeChar(key), C.size_t(len(key)))
zvalStr := C.frankenphp_init_persistent_string(toUnsafeChar(val), C.size_t(len(val)))

// copy the main thread env to the thread specific env
func cloneSandboxedEnv(thread *phpThread) {
if thread.sandboxedEnv != nil {
return
}
thread.sandboxedEnv = make(map[string]*C.zend_string, len(mainThread.sandboxedEnv))
for key, value := range mainThread.sandboxedEnv {
thread.sandboxedEnv[key] = value
var zval C.zval
*(*uint32)(unsafe.Pointer(&zval.u1)) = C.IS_INTERNED_STRING_EX
*(**C.zend_string)(unsafe.Pointer(&zval.value)) = zvalStr
C.zend_hash_update(mainThreadEnv, zkey, &zval)
}
}

//export go_putenv
func go_putenv(threadIndex C.uintptr_t, str *C.char, length C.int) C.bool {
thread := phpThreads[threadIndex]
envString := C.GoStringN(str, length)
cloneSandboxedEnv(thread)

// Check if '=' is present in the string
if key, val, found := strings.Cut(envString, "="); found {
removeEnvFromThread(thread, key)
thread.sandboxedEnv[key] = C.frankenphp_init_persistent_string(toUnsafeChar(val), C.size_t(len(val)))
return os.Setenv(key, val) == nil
}

// No '=', unset the environment variable
removeEnvFromThread(thread, envString)
return os.Unsetenv(envString) == nil
}

//export go_getfullenv
func go_getfullenv(threadIndex C.uintptr_t, trackVarsArray *C.zval) {
thread := phpThreads[threadIndex]
env := getSandboxedEnv(thread)

for key, val := range env {
C.add_assoc_str_ex(trackVarsArray, toUnsafeChar(key), C.size_t(len(key)), val)
}
}

//export go_getenv
func go_getenv(threadIndex C.uintptr_t, name *C.char) (C.bool, *C.zend_string) {
thread := phpThreads[threadIndex]
func go_putenv(name *C.char, nameLen C.int, val *C.char, valLen C.int) C.bool {
goName := C.GoStringN(name, nameLen)

// Get the environment variable value
envValue, exists := getSandboxedEnv(thread)[C.GoString(name)]
if !exists {
// Environment variable does not exist
return false, nil // Return 0 to indicate failure
if val == nil {
// If no "=" is present, unset the environment variable
return C.bool(os.Unsetenv(goName) == nil)
}

return true, envValue // Return 1 to indicate success
goVal := C.GoStringN(val, valLen)
return C.bool(os.Setenv(goName, goVal) == nil)
}
128 changes: 69 additions & 59 deletions frankenphp.c
Original file line number Diff line number Diff line change
Expand Up @@ -70,9 +70,11 @@ frankenphp_config frankenphp_get_config() {
}

bool should_filter_var = 0;
HashTable *main_thread_env = NULL;

__thread uintptr_t thread_index;
__thread bool is_worker_thread = false;
__thread zval *os_environment = NULL;
__thread HashTable *sandboxed_env = NULL;

void frankenphp_update_local_thread_context(bool is_worker) {
is_worker_thread = is_worker;
Expand Down Expand Up @@ -206,7 +208,7 @@ bool frankenphp_shutdown_dummy_request(void) {
}

PHPAPI void get_full_env(zval *track_vars_array) {
go_getfullenv(thread_index, track_vars_array);
zend_hash_copy(Z_ARR_P(track_vars_array), main_thread_env, NULL);
}

/* Adapted from php_request_startup() */
Expand Down Expand Up @@ -303,39 +305,64 @@ PHP_FUNCTION(frankenphp_putenv) {
RETURN_FALSE;
}

if (go_putenv(thread_index, setting, (int)setting_len)) {
RETURN_TRUE;
if (setting_len == 0 || setting[0] == '=') {
zend_argument_value_error(1, "must have a valid syntax");
RETURN_THROWS();
}

if (sandboxed_env == NULL) {
sandboxed_env = zend_array_dup(main_thread_env);
}

// cut the string at the first '='
char *eq_pos = memchr(setting, '=', setting_len);
bool put_env_success = true;
if (eq_pos != NULL) {
size_t name_len = eq_pos - setting;
size_t value_len =
(setting_len > name_len + 1) ? (setting_len - name_len - 1) : 0;
put_env_success =
go_putenv(setting, (int)name_len, eq_pos + 1, (int)value_len);
if (put_env_success) {
zval val = {0};
ZVAL_STRINGL(&val, eq_pos + 1, value_len);
zend_hash_str_update(sandboxed_env, setting, name_len, &val);
}
Comment on lines +317 to +330
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Important to note that doing string cutting like this is binary safe, in contrast to the php-src implementation (null bytes will be forwarded and rejected by go's putenv)

} else {
RETURN_FALSE;
// no '=' found, delete the variable
put_env_success = go_putenv(setting, (int)setting_len, NULL, 0);
if (put_env_success) {
zend_hash_str_del(sandboxed_env, setting, setting_len);
}
}

RETURN_BOOL(put_env_success);
} /* }}} */

/* {{{ Call go's getenv to prevent race conditions */
/* {{{ Get the env from the sandboxed environment */
PHP_FUNCTION(frankenphp_getenv) {
char *name = NULL;
size_t name_len = 0;
zend_string *name = NULL;
bool local_only = 0;

ZEND_PARSE_PARAMETERS_START(0, 2)
Z_PARAM_OPTIONAL
Z_PARAM_STRING_OR_NULL(name, name_len)
Z_PARAM_STR_OR_NULL(name)
Z_PARAM_BOOL(local_only)
ZEND_PARSE_PARAMETERS_END();

if (!name) {
array_init(return_value);
get_full_env(return_value);
HashTable *ht = sandboxed_env ? sandboxed_env : main_thread_env;

if (!name) {
RETURN_ARR(zend_array_dup(ht));
return;
}

struct go_getenv_return result = go_getenv(thread_index, name);

if (result.r0) {
// Return the single environment variable as a string
RETVAL_STR(result.r1);
zval *env_val = zend_hash_find(ht, name);
if (env_val && Z_TYPE_P(env_val) == IS_STRING) {
zend_string *str = Z_STR_P(env_val);
zend_string_addref(str);
RETVAL_STR(str);
} else {
// Environment variable does not exist
RETVAL_FALSE;
}
} /* }}} */
Expand Down Expand Up @@ -585,11 +612,6 @@ static zend_module_entry frankenphp_module = {
TOSTRING(FRANKENPHP_VERSION),
STANDARD_MODULE_PROPERTIES};

static void frankenphp_request_shutdown() {
frankenphp_free_request_context();
php_request_shutdown((void *)0);
}

static int frankenphp_startup(sapi_module_struct *sapi_module) {
php_import_environment_variables = get_full_env;

Expand Down Expand Up @@ -812,32 +834,11 @@ static inline void register_server_variable_filtered(const char *key,
static void frankenphp_register_variables(zval *track_vars_array) {
/* https://www.php.net/manual/en/reserved.variables.server.php */

/* In CGI mode, we consider the environment to be a part of the server
* variables.
/* In CGI mode, the environment is part of the $_SERVER variables.
* $_SERVER and $_ENV should only contain values from the original
* environment, not values added though putenv
*/

/* in non-worker mode we import the os environment regularly */
if (!is_worker_thread) {
get_full_env(track_vars_array);
// php_import_environment_variables(track_vars_array);
go_register_variables(thread_index, track_vars_array);
return;
}

/* In worker mode we cache the os environment */
if (os_environment == NULL) {
os_environment = malloc(sizeof(zval));
if (os_environment == NULL) {
php_error(E_ERROR, "Failed to allocate memory for os_environment");

return;
}
array_init(os_environment);
get_full_env(os_environment);
// php_import_environment_variables(os_environment);
}
zend_hash_copy(Z_ARR_P(track_vars_array), Z_ARR_P(os_environment),
(copy_ctor_func_t)zval_add_ref);
zend_hash_copy(Z_ARR_P(track_vars_array), main_thread_env, NULL);

go_register_variables(thread_index, track_vars_array);
}
Expand All @@ -847,10 +848,12 @@ static void frankenphp_log_message(const char *message, int syslog_type_int) {
}

static char *frankenphp_getenv(const char *name, size_t name_len) {
struct go_getenv_return result = go_getenv(thread_index, (char *)name);
HashTable *ht = sandboxed_env ? sandboxed_env : main_thread_env;

if (result.r0) {
return result.r1->val;
zval *env_val = zend_hash_str_find(ht, 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);
}

return NULL;
Expand Down Expand Up @@ -986,6 +989,13 @@ static void *php_main(void *arg) {
cfg_get_string("filter.default", &default_filter);
should_filter_var = default_filter != NULL;

/* take a snapshot of the environment for sandboxing */
if (main_thread_env == NULL) {
main_thread_env = malloc(sizeof(HashTable));
zend_hash_init(main_thread_env, 8, NULL, NULL, 1);
go_init_os_env(main_thread_env);
}

go_frankenphp_main_thread_is_ready();

/* channel closed, shutdown gracefully */
Expand Down Expand Up @@ -1059,16 +1069,16 @@ int frankenphp_execute_script(char *file_name) {
zend_catch { status = EG(exit_status); }
zend_end_try();

// free the cached os environment before shutting down the script
if (os_environment != NULL) {
zval_ptr_dtor(os_environment);
free(os_environment);
os_environment = NULL;
}

zend_destroy_file_handle(&file_handle);

frankenphp_request_shutdown();
/* Reset values the sandboxed environment */
if (sandboxed_env != NULL) {
zend_hash_release(sandboxed_env);
sandboxed_env = NULL;
}

frankenphp_free_request_context();
php_request_shutdown((void *)0);

return status;
}
Expand Down
8 changes: 7 additions & 1 deletion frankenphp_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,12 @@ func TestMain(m *testing.M) {
slog.SetDefault(slog.New(slog.DiscardHandler))
}

// setup custom environment var for TestWorkerHasOSEnvironmentVariableInSERVER
if os.Setenv("CUSTOM_OS_ENV_VARIABLE", "custom_env_variable_value") != nil {
fmt.Println("Failed to set environment variable for tests")
os.Exit(1)
}

os.Exit(m.Run())
}

Expand Down Expand Up @@ -645,7 +651,7 @@ func testEnv(t *testing.T, opts *testOptions) {
stdoutStderr, err := cmd.CombinedOutput()
if err != nil {
// php is not installed or other issue, use the hardcoded output below:
stdoutStderr = []byte("Set MY_VAR successfully.\nMY_VAR = HelloWorld\nUnset MY_VAR successfully.\nMY_VAR is unset.\nMY_VAR set to empty successfully.\nMY_VAR = \nUnset NON_EXISTING_VAR successfully.\n")
stdoutStderr = []byte("Set MY_VAR successfully.\nMY_VAR = HelloWorld\nMY_VAR not found in $_SERVER.\nUnset MY_VAR successfully.\nMY_VAR is unset.\nMY_VAR set to empty successfully.\nMY_VAR = \nUnset NON_EXISTING_VAR successfully.\nInvalid value was not inserted.\n")
}

assert.Equal(t, string(stdoutStderr), body)
Expand Down
Loading