Skip to content

Commit e812473

Browse files
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
1 parent 5ec0308 commit e812473

5 files changed

Lines changed: 260 additions & 18 deletions

File tree

frankenphp.c

Lines changed: 110 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,25 @@ static void frankenphp_worker_request_shutdown() {
150150
SG(rfc1867_uploaded_files) = NULL;
151151
}
152152

153+
PHPAPI void get_full_env(zval *track_vars_array) {
154+
struct go_getfullenv_return full_env = go_getfullenv(thread_index);
155+
156+
for (int i = 0; i < full_env.r1; i++) {
157+
go_string key = full_env.r0[i * 2];
158+
go_string val = full_env.r0[i * 2 + 1];
159+
160+
// create PHP strings for key and value
161+
zend_string *key_str = zend_string_init(key.data, key.len, 0);
162+
zend_string *val_str = zend_string_init(val.data, val.len, 0);
163+
164+
// add to the associative array
165+
add_assoc_str(track_vars_array, ZSTR_VAL(key_str), val_str);
166+
167+
// release the key string
168+
zend_string_release(key_str);
169+
}
170+
}
171+
153172
/* Adapted from php_request_startup() */
154173
static int frankenphp_worker_request_startup() {
155174
int retval = SUCCESS;
@@ -242,6 +261,60 @@ PHP_FUNCTION(frankenphp_finish_request) { /* {{{ */
242261
RETURN_TRUE;
243262
} /* }}} */
244263

264+
/* {{{ Call go's putenv to prevent race conditions */
265+
PHP_FUNCTION(frankenphp_putenv) {
266+
char *setting;
267+
size_t setting_len;
268+
269+
ZEND_PARSE_PARAMETERS_START(1, 1)
270+
Z_PARAM_STRING(setting, setting_len)
271+
ZEND_PARSE_PARAMETERS_END();
272+
273+
// Cast str_len to int (ensure it fits in an int)
274+
if (setting_len > INT_MAX) {
275+
php_error(E_WARNING, "String length exceeds maximum integer value");
276+
RETURN_FALSE;
277+
}
278+
279+
if (go_putenv(setting, (int)setting_len)) {
280+
RETURN_TRUE;
281+
} else {
282+
RETURN_FALSE;
283+
}
284+
} /* }}} */
285+
286+
/* {{{ Call go's getenv to prevent race conditions */
287+
PHP_FUNCTION(frankenphp_getenv) {
288+
char *name = NULL;
289+
size_t name_len = 0;
290+
bool local_only = 0;
291+
292+
ZEND_PARSE_PARAMETERS_START(0, 2)
293+
Z_PARAM_OPTIONAL
294+
Z_PARAM_STRING_OR_NULL(name, name_len)
295+
Z_PARAM_BOOL(local_only)
296+
ZEND_PARSE_PARAMETERS_END();
297+
298+
if (!name) {
299+
array_init(return_value);
300+
get_full_env(return_value);
301+
302+
return;
303+
}
304+
305+
go_string gname = {name_len, name};
306+
307+
struct go_getenv_return result = go_getenv(thread_index, &gname);
308+
309+
if (result.r0) {
310+
// Return the single environment variable as a string
311+
RETVAL_STRINGL(result.r1->data, result.r1->len);
312+
} else {
313+
// Environment variable does not exist
314+
RETVAL_FALSE;
315+
}
316+
} /* }}} */
317+
245318
/* {{{ Fetch all HTTP request headers */
246319
PHP_FUNCTION(frankenphp_request_headers) {
247320
if (zend_parse_parameters_none() == FAILURE) {
@@ -260,8 +333,6 @@ PHP_FUNCTION(frankenphp_request_headers) {
260333

261334
add_assoc_stringl_ex(return_value, key.data, key.len, val.data, val.len);
262335
}
263-
264-
go_apache_request_cleanup(thread_index);
265336
}
266337
/* }}} */
267338

@@ -408,15 +479,39 @@ PHP_FUNCTION(headers_send) {
408479
RETURN_LONG(sapi_send_headers());
409480
}
410481

482+
PHP_MINIT_FUNCTION(frankenphp) {
483+
zend_function *func;
484+
485+
// Override putenv
486+
func = zend_hash_str_find_ptr(CG(function_table), "putenv",
487+
sizeof("putenv") - 1);
488+
if (func != NULL && func->type == ZEND_INTERNAL_FUNCTION) {
489+
((zend_internal_function *)func)->handler = ZEND_FN(frankenphp_putenv);
490+
} else {
491+
php_error(E_WARNING, "Failed to find built-in putenv function");
492+
}
493+
494+
// Override getenv
495+
func = zend_hash_str_find_ptr(CG(function_table), "getenv",
496+
sizeof("getenv") - 1);
497+
if (func != NULL && func->type == ZEND_INTERNAL_FUNCTION) {
498+
((zend_internal_function *)func)->handler = ZEND_FN(frankenphp_getenv);
499+
} else {
500+
php_error(E_WARNING, "Failed to find built-in getenv function");
501+
}
502+
503+
return SUCCESS;
504+
}
505+
411506
static zend_module_entry frankenphp_module = {
412507
STANDARD_MODULE_HEADER,
413508
"frankenphp",
414-
ext_functions, /* function table */
415-
NULL, /* initialization */
416-
NULL, /* shutdown */
417-
NULL, /* request initialization */
418-
NULL, /* request shutdown */
419-
NULL, /* information */
509+
ext_functions, /* function table */
510+
PHP_MINIT(frankenphp), /* initialization */
511+
NULL, /* shutdown */
512+
NULL, /* request initialization */
513+
NULL, /* request shutdown */
514+
NULL, /* information */
420515
TOSTRING(FRANKENPHP_VERSION),
421516
STANDARD_MODULE_PROPERTIES};
422517

@@ -473,6 +568,8 @@ int frankenphp_update_server_context(
473568
}
474569

475570
static int frankenphp_startup(sapi_module_struct *sapi_module) {
571+
php_import_environment_variables = get_full_env;
572+
476573
return php_module_startup(sapi_module, &frankenphp_module);
477574
}
478575

@@ -662,14 +759,15 @@ static void frankenphp_register_variables(zval *track_vars_array) {
662759
/* https://www.php.net/manual/en/reserved.variables.server.php */
663760

664761
/* In CGI mode, we consider the environment to be a part of the server
665-
* variables
762+
* variables.
666763
*/
667764

668765
frankenphp_server_context *ctx = SG(server_context);
669766

670767
/* in non-worker mode we import the os environment regularly */
671768
if (!ctx->has_main_request) {
672-
php_import_environment_variables(track_vars_array);
769+
get_full_env(track_vars_array);
770+
// php_import_environment_variables(track_vars_array);
673771
go_register_variables(thread_index, track_vars_array);
674772
return;
675773
}
@@ -678,7 +776,8 @@ static void frankenphp_register_variables(zval *track_vars_array) {
678776
if (os_environment == NULL) {
679777
os_environment = malloc(sizeof(zval));
680778
array_init(os_environment);
681-
php_import_environment_variables(os_environment);
779+
get_full_env(os_environment);
780+
// php_import_environment_variables(os_environment);
682781
}
683782
zend_hash_copy(Z_ARR_P(track_vars_array), Z_ARR_P(os_environment),
684783
(copy_ctor_func_t)zval_add_ref);

frankenphp.go

Lines changed: 71 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -507,6 +507,76 @@ func ServeHTTP(responseWriter http.ResponseWriter, request *http.Request) error
507507
return nil
508508
}
509509

510+
//export go_putenv
511+
func go_putenv(str *C.char, length C.int) C.bool {
512+
// Create a byte slice from C string with a specified length
513+
s := C.GoBytes(unsafe.Pointer(str), length)
514+
515+
// Convert byte slice to string
516+
envString := string(s)
517+
518+
// Check if '=' is present in the string
519+
if key, val, found := strings.Cut(envString, "="); found {
520+
if os.Setenv(key, val) != nil {
521+
return false // Failure
522+
}
523+
} else {
524+
// No '=', unset the environment variable
525+
if os.Unsetenv(envString) != nil {
526+
return false // Failure
527+
}
528+
}
529+
530+
return true // Success
531+
}
532+
533+
//export go_getfullenv
534+
func go_getfullenv(threadIndex C.uintptr_t) (*C.go_string, C.size_t) {
535+
thread := phpThreads[threadIndex]
536+
537+
env := os.Environ()
538+
goStrings := make([]C.go_string, len(env)*2)
539+
540+
for i, envVar := range env {
541+
key, val, _ := strings.Cut(envVar, "=")
542+
k := unsafe.StringData(key)
543+
v := unsafe.StringData(val)
544+
thread.Pin(k)
545+
thread.Pin(v)
546+
547+
goStrings[i*2] = C.go_string{C.size_t(len(key)), (*C.char)(unsafe.Pointer(k))}
548+
goStrings[i*2+1] = C.go_string{C.size_t(len(val)), (*C.char)(unsafe.Pointer(v))}
549+
}
550+
551+
value := unsafe.SliceData(goStrings)
552+
thread.Pin(value)
553+
554+
return value, C.size_t(len(env))
555+
}
556+
557+
//export go_getenv
558+
func go_getenv(threadIndex C.uintptr_t, name *C.go_string) (C.bool, *C.go_string) {
559+
thread := phpThreads[threadIndex]
560+
561+
// Create a byte slice from C string with a specified length
562+
envName := C.GoStringN(name.data, C.int(name.len))
563+
564+
// Get the environment variable value
565+
envValue, exists := os.LookupEnv(envName)
566+
if !exists {
567+
// Environment variable does not exist
568+
return false, nil // Return 0 to indicate failure
569+
}
570+
571+
// Convert Go string to C string
572+
val := unsafe.StringData(envValue)
573+
thread.Pin(val)
574+
value := &C.go_string{C.size_t(len(envValue)), (*C.char)(unsafe.Pointer(val))}
575+
thread.Pin(value)
576+
577+
return true, value // Return 1 to indicate success
578+
}
579+
510580
//export go_handle_request
511581
func go_handle_request(threadIndex C.uintptr_t) bool {
512582
select {
@@ -524,6 +594,7 @@ func go_handle_request(threadIndex C.uintptr_t) bool {
524594
defer func() {
525595
maybeCloseContext(fc)
526596
thread.mainRequest = nil
597+
thread.Unpin()
527598
}()
528599

529600
if err := updateServerContext(r, true, false); err != nil {
@@ -647,8 +718,6 @@ func go_register_variables(threadIndex C.uintptr_t, trackVarsArray *C.zval) {
647718

648719
C.frankenphp_register_bulk_variables(&knownVariables[0], dvsd, C.size_t(l), trackVarsArray)
649720

650-
thread.Unpin()
651-
652721
fc.env = nil
653722
}
654723

@@ -691,11 +760,6 @@ func go_apache_request_headers(threadIndex C.uintptr_t, hasActiveRequest bool) (
691760
return sd, C.size_t(len(r.Header))
692761
}
693762

694-
//export go_apache_request_cleanup
695-
func go_apache_request_cleanup(threadIndex C.uintptr_t) {
696-
phpThreads[threadIndex].Unpin()
697-
}
698-
699763
func addHeader(fc *FrankenPHPContext, cString *C.char, length C.int) {
700764
parts := strings.SplitN(C.GoStringN(cString, length), ": ", 2)
701765
if len(parts) != 2 {

frankenphp_test.go

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -622,6 +622,33 @@ func TestFailingWorker(t *testing.T) {
622622
}, &testOptions{workerScript: "failing-worker.php"})
623623
}
624624

625+
func TestEnv(t *testing.T) {
626+
testEnv(t, &testOptions{})
627+
}
628+
func TestEnvWorker(t *testing.T) {
629+
testEnv(t, &testOptions{workerScript: "test-env.php"})
630+
}
631+
func testEnv(t *testing.T, opts *testOptions) {
632+
runTest(t, func(handler func(http.ResponseWriter, *http.Request), _ *httptest.Server, i int) {
633+
req := httptest.NewRequest("GET", fmt.Sprintf("http://example.com/test-env.php?var=%d", i), nil)
634+
w := httptest.NewRecorder()
635+
handler(w, req)
636+
637+
resp := w.Result()
638+
body, _ := io.ReadAll(resp.Body)
639+
640+
// execute the script as regular php script
641+
cmd := exec.Command("php", "testdata/test-env.php", strconv.Itoa(i))
642+
stdoutStderr, err := cmd.CombinedOutput()
643+
if err != nil {
644+
// php is not installed or other issue, use the hardcoded output below:
645+
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")
646+
}
647+
648+
assert.Equal(t, string(stdoutStderr), string(body))
649+
}, opts)
650+
}
651+
625652
func TestFileUpload_module(t *testing.T) { testFileUpload(t, &testOptions{}) }
626653
func TestFileUpload_worker(t *testing.T) {
627654
testFileUpload(t, &testOptions{workerScript: "file-upload.php"})

testdata/test-env.php

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
<?php
2+
3+
require_once __DIR__.'/_executor.php';
4+
5+
return function() {
6+
$var = 'MY_VAR_' . ($_GET['var'] ?? '');
7+
// Setting an environment variable
8+
$result = putenv("$var=HelloWorld");
9+
if ($result) {
10+
echo "Set MY_VAR successfully.\n";
11+
echo "MY_VAR = " . getenv($var) . "\n";
12+
} else {
13+
echo "Failed to set MY_VAR.\n";
14+
}
15+
16+
// Unsetting the environment variable
17+
$result = putenv($var);
18+
if ($result) {
19+
echo "Unset MY_VAR successfully.\n";
20+
$value = getenv($var);
21+
if ($value === false) {
22+
echo "MY_VAR is unset.\n";
23+
} else {
24+
echo "MY_VAR = " . $value . "\n";
25+
}
26+
} else {
27+
echo "Failed to unset MY_VAR.\n";
28+
}
29+
30+
$result = putenv("$var=");
31+
if ($result) {
32+
echo "MY_VAR set to empty successfully.\n";
33+
$value = getenv($var);
34+
if ($value === false) {
35+
echo "MY_VAR is unset.\n";
36+
} else {
37+
echo "MY_VAR = " . $value . "\n";
38+
}
39+
} else {
40+
echo "Failed to set MY_VAR.\n";
41+
}
42+
43+
// Attempt to unset a non-existing variable
44+
$result = putenv('NON_EXISTING_VAR' . ($_GET['var'] ?? ''));
45+
if ($result) {
46+
echo "Unset NON_EXISTING_VAR successfully.\n";
47+
} else {
48+
echo "Failed to unset NON_EXISTING_VAR.\n";
49+
}
50+
};

worker.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -310,4 +310,6 @@ func go_frankenphp_finish_request(threadIndex C.uintptr_t, isWorkerRequest bool)
310310

311311
c.Write(fields...)
312312
}
313+
314+
thread.Unpin()
313315
}

0 commit comments

Comments
 (0)