From e812473fe187d75e1a07e36f15f526bd458c4000 Mon Sep 17 00:00:00 2001 From: Rob Landers Date: Fri, 18 Oct 2024 13:47:11 +0200 Subject: [PATCH] implement getenv and putenv in go (#1086) * implement getenv and putenv in go * fix typo * apply formatting * return a bool * prevent ENV= from crashing * optimization * optimization * split env workflows and use go_strings * clean up unused code * update tests * remove useless sprintf * see if this fixes the asan issues * clean up comments * check that VAR= works correctly and use actual php to validate the behavior * move all unpinning to the end of the request * handle the case where php is not installed * fix copy-paste * optimization * use strings.cut * fix lint * override how env is filled * reuse fullenv * use corect function --- frankenphp.c | 121 ++++++++++++++++++++++++++++++++++++++---- frankenphp.go | 78 ++++++++++++++++++++++++--- frankenphp_test.go | 27 ++++++++++ testdata/test-env.php | 50 +++++++++++++++++ worker.go | 2 + 5 files changed, 260 insertions(+), 18 deletions(-) create mode 100644 testdata/test-env.php diff --git a/frankenphp.c b/frankenphp.c index e0f5e83ff..54c149763 100644 --- a/frankenphp.c +++ b/frankenphp.c @@ -150,6 +150,25 @@ static void frankenphp_worker_request_shutdown() { SG(rfc1867_uploaded_files) = NULL; } +PHPAPI void get_full_env(zval *track_vars_array) { + struct go_getfullenv_return full_env = go_getfullenv(thread_index); + + for (int i = 0; i < full_env.r1; i++) { + go_string key = full_env.r0[i * 2]; + go_string val = full_env.r0[i * 2 + 1]; + + // create PHP strings for key and value + zend_string *key_str = zend_string_init(key.data, key.len, 0); + zend_string *val_str = zend_string_init(val.data, val.len, 0); + + // add to the associative array + add_assoc_str(track_vars_array, ZSTR_VAL(key_str), val_str); + + // release the key string + zend_string_release(key_str); + } +} + /* Adapted from php_request_startup() */ static int frankenphp_worker_request_startup() { int retval = SUCCESS; @@ -242,6 +261,60 @@ PHP_FUNCTION(frankenphp_finish_request) { /* {{{ */ RETURN_TRUE; } /* }}} */ +/* {{{ Call go's putenv to prevent race conditions */ +PHP_FUNCTION(frankenphp_putenv) { + char *setting; + size_t setting_len; + + ZEND_PARSE_PARAMETERS_START(1, 1) + Z_PARAM_STRING(setting, setting_len) + ZEND_PARSE_PARAMETERS_END(); + + // Cast str_len to int (ensure it fits in an int) + if (setting_len > INT_MAX) { + php_error(E_WARNING, "String length exceeds maximum integer value"); + RETURN_FALSE; + } + + if (go_putenv(setting, (int)setting_len)) { + RETURN_TRUE; + } else { + RETURN_FALSE; + } +} /* }}} */ + +/* {{{ Call go's getenv to prevent race conditions */ +PHP_FUNCTION(frankenphp_getenv) { + char *name = NULL; + size_t name_len = 0; + bool local_only = 0; + + ZEND_PARSE_PARAMETERS_START(0, 2) + Z_PARAM_OPTIONAL + Z_PARAM_STRING_OR_NULL(name, name_len) + Z_PARAM_BOOL(local_only) + ZEND_PARSE_PARAMETERS_END(); + + if (!name) { + array_init(return_value); + get_full_env(return_value); + + return; + } + + go_string gname = {name_len, name}; + + struct go_getenv_return result = go_getenv(thread_index, &gname); + + if (result.r0) { + // Return the single environment variable as a string + RETVAL_STRINGL(result.r1->data, result.r1->len); + } else { + // Environment variable does not exist + RETVAL_FALSE; + } +} /* }}} */ + /* {{{ Fetch all HTTP request headers */ PHP_FUNCTION(frankenphp_request_headers) { if (zend_parse_parameters_none() == FAILURE) { @@ -260,8 +333,6 @@ PHP_FUNCTION(frankenphp_request_headers) { add_assoc_stringl_ex(return_value, key.data, key.len, val.data, val.len); } - - go_apache_request_cleanup(thread_index); } /* }}} */ @@ -408,15 +479,39 @@ PHP_FUNCTION(headers_send) { RETURN_LONG(sapi_send_headers()); } +PHP_MINIT_FUNCTION(frankenphp) { + zend_function *func; + + // Override putenv + func = zend_hash_str_find_ptr(CG(function_table), "putenv", + sizeof("putenv") - 1); + if (func != NULL && func->type == ZEND_INTERNAL_FUNCTION) { + ((zend_internal_function *)func)->handler = ZEND_FN(frankenphp_putenv); + } else { + php_error(E_WARNING, "Failed to find built-in putenv function"); + } + + // Override getenv + func = zend_hash_str_find_ptr(CG(function_table), "getenv", + sizeof("getenv") - 1); + if (func != NULL && func->type == ZEND_INTERNAL_FUNCTION) { + ((zend_internal_function *)func)->handler = ZEND_FN(frankenphp_getenv); + } else { + php_error(E_WARNING, "Failed to find built-in getenv function"); + } + + return SUCCESS; +} + static zend_module_entry frankenphp_module = { STANDARD_MODULE_HEADER, "frankenphp", - ext_functions, /* function table */ - NULL, /* initialization */ - NULL, /* shutdown */ - NULL, /* request initialization */ - NULL, /* request shutdown */ - NULL, /* information */ + ext_functions, /* function table */ + PHP_MINIT(frankenphp), /* initialization */ + NULL, /* shutdown */ + NULL, /* request initialization */ + NULL, /* request shutdown */ + NULL, /* information */ TOSTRING(FRANKENPHP_VERSION), STANDARD_MODULE_PROPERTIES}; @@ -473,6 +568,8 @@ int frankenphp_update_server_context( } static int frankenphp_startup(sapi_module_struct *sapi_module) { + php_import_environment_variables = get_full_env; + return php_module_startup(sapi_module, &frankenphp_module); } @@ -662,14 +759,15 @@ 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 + * variables. */ frankenphp_server_context *ctx = SG(server_context); /* in non-worker mode we import the os environment regularly */ if (!ctx->has_main_request) { - php_import_environment_variables(track_vars_array); + get_full_env(track_vars_array); + // php_import_environment_variables(track_vars_array); go_register_variables(thread_index, track_vars_array); return; } @@ -678,7 +776,8 @@ static void frankenphp_register_variables(zval *track_vars_array) { if (os_environment == NULL) { os_environment = malloc(sizeof(zval)); array_init(os_environment); - php_import_environment_variables(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); diff --git a/frankenphp.go b/frankenphp.go index 7d8baabf7..8b22ac5b5 100644 --- a/frankenphp.go +++ b/frankenphp.go @@ -507,6 +507,76 @@ func ServeHTTP(responseWriter http.ResponseWriter, request *http.Request) error return nil } +//export go_putenv +func go_putenv(str *C.char, length C.int) C.bool { + // Create a byte slice from C string with a specified length + s := C.GoBytes(unsafe.Pointer(str), length) + + // Convert byte slice to string + envString := string(s) + + // Check if '=' is present in the string + if key, val, found := strings.Cut(envString, "="); found { + if os.Setenv(key, val) != nil { + return false // Failure + } + } else { + // No '=', unset the environment variable + if os.Unsetenv(envString) != nil { + return false // Failure + } + } + + return true // Success +} + +//export go_getfullenv +func go_getfullenv(threadIndex C.uintptr_t) (*C.go_string, C.size_t) { + thread := phpThreads[threadIndex] + + env := os.Environ() + goStrings := make([]C.go_string, len(env)*2) + + for i, envVar := range env { + key, val, _ := strings.Cut(envVar, "=") + k := unsafe.StringData(key) + v := unsafe.StringData(val) + thread.Pin(k) + thread.Pin(v) + + goStrings[i*2] = C.go_string{C.size_t(len(key)), (*C.char)(unsafe.Pointer(k))} + goStrings[i*2+1] = C.go_string{C.size_t(len(val)), (*C.char)(unsafe.Pointer(v))} + } + + value := unsafe.SliceData(goStrings) + thread.Pin(value) + + return value, C.size_t(len(env)) +} + +//export go_getenv +func go_getenv(threadIndex C.uintptr_t, name *C.go_string) (C.bool, *C.go_string) { + thread := phpThreads[threadIndex] + + // Create a byte slice from C string with a specified length + envName := C.GoStringN(name.data, C.int(name.len)) + + // Get the environment variable value + envValue, exists := os.LookupEnv(envName) + if !exists { + // Environment variable does not exist + return false, nil // Return 0 to indicate failure + } + + // Convert Go string to C string + val := unsafe.StringData(envValue) + thread.Pin(val) + value := &C.go_string{C.size_t(len(envValue)), (*C.char)(unsafe.Pointer(val))} + thread.Pin(value) + + return true, value // Return 1 to indicate success +} + //export go_handle_request func go_handle_request(threadIndex C.uintptr_t) bool { select { @@ -524,6 +594,7 @@ func go_handle_request(threadIndex C.uintptr_t) bool { defer func() { maybeCloseContext(fc) thread.mainRequest = nil + thread.Unpin() }() if err := updateServerContext(r, true, false); err != nil { @@ -647,8 +718,6 @@ func go_register_variables(threadIndex C.uintptr_t, trackVarsArray *C.zval) { C.frankenphp_register_bulk_variables(&knownVariables[0], dvsd, C.size_t(l), trackVarsArray) - thread.Unpin() - fc.env = nil } @@ -691,11 +760,6 @@ func go_apache_request_headers(threadIndex C.uintptr_t, hasActiveRequest bool) ( return sd, C.size_t(len(r.Header)) } -//export go_apache_request_cleanup -func go_apache_request_cleanup(threadIndex C.uintptr_t) { - phpThreads[threadIndex].Unpin() -} - func addHeader(fc *FrankenPHPContext, cString *C.char, length C.int) { parts := strings.SplitN(C.GoStringN(cString, length), ": ", 2) if len(parts) != 2 { diff --git a/frankenphp_test.go b/frankenphp_test.go index d6ca541ab..b02ef9e74 100644 --- a/frankenphp_test.go +++ b/frankenphp_test.go @@ -622,6 +622,33 @@ func TestFailingWorker(t *testing.T) { }, &testOptions{workerScript: "failing-worker.php"}) } +func TestEnv(t *testing.T) { + testEnv(t, &testOptions{}) +} +func TestEnvWorker(t *testing.T) { + testEnv(t, &testOptions{workerScript: "test-env.php"}) +} +func testEnv(t *testing.T, opts *testOptions) { + runTest(t, func(handler func(http.ResponseWriter, *http.Request), _ *httptest.Server, i int) { + req := httptest.NewRequest("GET", fmt.Sprintf("http://example.com/test-env.php?var=%d", i), nil) + w := httptest.NewRecorder() + handler(w, req) + + resp := w.Result() + body, _ := io.ReadAll(resp.Body) + + // execute the script as regular php script + cmd := exec.Command("php", "testdata/test-env.php", strconv.Itoa(i)) + 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") + } + + assert.Equal(t, string(stdoutStderr), string(body)) + }, opts) +} + func TestFileUpload_module(t *testing.T) { testFileUpload(t, &testOptions{}) } func TestFileUpload_worker(t *testing.T) { testFileUpload(t, &testOptions{workerScript: "file-upload.php"}) diff --git a/testdata/test-env.php b/testdata/test-env.php new file mode 100644 index 000000000..7588fa646 --- /dev/null +++ b/testdata/test-env.php @@ -0,0 +1,50 @@ +