Skip to content

Commit c57f741

Browse files
fix: concurrent env access (#1409)
1 parent 3ba4e25 commit c57f741

File tree

15 files changed

+268
-74
lines changed

15 files changed

+268
-74
lines changed

caddy/caddy_test.go

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -716,3 +716,35 @@ func testSingleIniConfiguration(tester *caddytest.Tester, key string, value stri
716716
)
717717
}
718718
}
719+
720+
func TestOsEnv(t *testing.T) {
721+
os.Setenv("ENV1", "value1")
722+
os.Setenv("ENV2", "value2")
723+
tester := caddytest.NewTester(t)
724+
tester.InitServer(`
725+
{
726+
skip_install_trust
727+
admin localhost:2999
728+
http_port `+testPort+`
729+
730+
frankenphp {
731+
num_threads 2
732+
php_ini variables_order "EGPCS"
733+
worker ../testdata/env/env.php 1
734+
}
735+
}
736+
737+
localhost:`+testPort+` {
738+
route {
739+
root ../testdata
740+
php
741+
}
742+
}
743+
`, "caddyfile")
744+
745+
tester.AssertGetResponse(
746+
"http://localhost:"+testPort+"/env/env.php?keys[]=ENV1&keys[]=ENV2",
747+
http.StatusOK,
748+
"ENV1=value1,ENV2=value2",
749+
)
750+
}

env.go

Lines changed: 79 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
package frankenphp
22

3+
// #cgo nocallback frankenphp_init_persistent_string
4+
// #cgo nocallback frankenphp_add_assoc_str_ex
5+
// #cgo noescape frankenphp_init_persistent_string
6+
// #cgo noescape frankenphp_add_assoc_str_ex
37
// #include "frankenphp.h"
48
import "C"
59
import (
@@ -8,67 +12,106 @@ import (
812
"unsafe"
913
)
1014

15+
func initializeEnv() map[string]*C.zend_string {
16+
env := os.Environ()
17+
envMap := make(map[string]*C.zend_string, len(env))
18+
19+
for _, envVar := range env {
20+
key, val, _ := strings.Cut(envVar, "=")
21+
envMap[key] = C.frankenphp_init_persistent_string(toUnsafeChar(val), C.size_t(len(val)))
22+
}
23+
24+
return envMap
25+
}
26+
27+
// get the main thread env or the thread specific env
28+
func getSandboxedEnv(thread *phpThread) map[string]*C.zend_string {
29+
if thread.sandboxedEnv != nil {
30+
return thread.sandboxedEnv
31+
}
32+
33+
return mainThread.sandboxedEnv
34+
}
35+
36+
func clearSandboxedEnv(thread *phpThread) {
37+
if thread.sandboxedEnv == nil {
38+
return
39+
}
40+
41+
for key, val := range thread.sandboxedEnv {
42+
valInMainThread, ok := mainThread.sandboxedEnv[key]
43+
if !ok || val != valInMainThread {
44+
C.free(unsafe.Pointer(val))
45+
}
46+
}
47+
48+
thread.sandboxedEnv = nil
49+
}
50+
51+
// if an env var already exists, it needs to be freed
52+
func removeEnvFromThread(thread *phpThread, key string) {
53+
valueInThread, existsInThread := thread.sandboxedEnv[key]
54+
if !existsInThread {
55+
return
56+
}
57+
58+
valueInMainThread, ok := mainThread.sandboxedEnv[key]
59+
if !ok || valueInThread != valueInMainThread {
60+
C.free(unsafe.Pointer(valueInThread))
61+
}
62+
63+
delete(thread.sandboxedEnv, key)
64+
}
65+
66+
// copy the main thread env to the thread specific env
67+
func cloneSandboxedEnv(thread *phpThread) {
68+
if thread.sandboxedEnv != nil {
69+
return
70+
}
71+
thread.sandboxedEnv = make(map[string]*C.zend_string, len(mainThread.sandboxedEnv))
72+
for key, value := range mainThread.sandboxedEnv {
73+
thread.sandboxedEnv[key] = value
74+
}
75+
}
76+
1177
//export go_putenv
12-
func go_putenv(str *C.char, length C.int) C.bool {
78+
func go_putenv(threadIndex C.uintptr_t, str *C.char, length C.int) C.bool {
79+
thread := phpThreads[threadIndex]
1380
envString := C.GoStringN(str, length)
81+
cloneSandboxedEnv(thread)
1482

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

2090
// No '=', unset the environment variable
91+
removeEnvFromThread(thread, envString)
2192
return os.Unsetenv(envString) == nil
2293
}
2394

2495
//export go_getfullenv
25-
func go_getfullenv(threadIndex C.uintptr_t) (*C.go_string, C.size_t) {
96+
func go_getfullenv(threadIndex C.uintptr_t, trackVarsArray *C.zval) {
2697
thread := phpThreads[threadIndex]
98+
env := getSandboxedEnv(thread)
2799

28-
env := os.Environ()
29-
goStrings := make([]C.go_string, len(env)*2)
30-
31-
for i, envVar := range env {
32-
key, val, _ := strings.Cut(envVar, "=")
33-
goStrings[i*2] = C.go_string{C.size_t(len(key)), thread.pinString(key)}
34-
goStrings[i*2+1] = C.go_string{C.size_t(len(val)), thread.pinString(val)}
100+
for key, val := range env {
101+
C.frankenphp_add_assoc_str_ex(trackVarsArray, toUnsafeChar(key), C.size_t(len(key)), val)
35102
}
36-
37-
value := unsafe.SliceData(goStrings)
38-
thread.Pin(value)
39-
40-
return value, C.size_t(len(env))
41103
}
42104

43105
//export go_getenv
44-
func go_getenv(threadIndex C.uintptr_t, name *C.go_string) (C.bool, *C.go_string) {
106+
func go_getenv(threadIndex C.uintptr_t, name *C.char) (C.bool, *C.zend_string) {
45107
thread := phpThreads[threadIndex]
46108

47-
// Create a byte slice from C string with a specified length
48-
envName := C.GoStringN(name.data, C.int(name.len))
49-
50109
// Get the environment variable value
51-
envValue, exists := os.LookupEnv(envName)
110+
envValue, exists := getSandboxedEnv(thread)[C.GoString(name)]
52111
if !exists {
53112
// Environment variable does not exist
54113
return false, nil // Return 0 to indicate failure
55114
}
56115

57-
// Convert Go string to C string
58-
value := &C.go_string{C.size_t(len(envValue)), thread.pinString(envValue)}
59-
thread.Pin(value)
60-
61-
return true, value // Return 1 to indicate success
62-
}
63-
64-
//export go_sapi_getenv
65-
func go_sapi_getenv(threadIndex C.uintptr_t, name *C.go_string) *C.char {
66-
envName := C.GoStringN(name.data, C.int(name.len))
67-
68-
envValue, exists := os.LookupEnv(envName)
69-
if !exists {
70-
return nil
71-
}
72-
73-
return phpThreads[threadIndex].pinCString(envValue)
116+
return true, envValue // Return 1 to indicate success
74117
}

frankenphp.c

Lines changed: 27 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -160,18 +160,12 @@ static void frankenphp_worker_request_shutdown() {
160160
}
161161

162162
PHPAPI void get_full_env(zval *track_vars_array) {
163-
struct go_getfullenv_return full_env = go_getfullenv(thread_index);
164-
165-
for (int i = 0; i < full_env.r1; i++) {
166-
go_string key = full_env.r0[i * 2];
167-
go_string val = full_env.r0[i * 2 + 1];
168-
169-
// create PHP string for the value
170-
zend_string *val_str = zend_string_init(val.data, val.len, 0);
163+
go_getfullenv(thread_index, track_vars_array);
164+
}
171165

172-
// add to the associative array
173-
add_assoc_str_ex(track_vars_array, key.data, key.len, val_str);
174-
}
166+
void frankenphp_add_assoc_str_ex(zval *track_vars_array, char *key,
167+
size_t keylen, zend_string *val) {
168+
add_assoc_str_ex(track_vars_array, key, keylen, val);
175169
}
176170

177171
/* Adapted from php_request_startup() */
@@ -219,8 +213,20 @@ static int frankenphp_worker_request_startup() {
219213

220214
php_hash_environment();
221215

216+
/* zend_is_auto_global will force a re-import of the $_SERVER global */
222217
zend_is_auto_global(ZSTR_KNOWN(ZEND_STR_AUTOGLOBAL_SERVER));
223218

219+
/* disarm the $_ENV auto_global to prevent it from being reloaded in worker
220+
* mode */
221+
if (zend_hash_str_exists(&EG(symbol_table), "_ENV", 4)) {
222+
zend_auto_global *env_global;
223+
if ((env_global = zend_hash_find_ptr(
224+
CG(auto_globals), ZSTR_KNOWN(ZEND_STR_AUTOGLOBAL_ENV))) !=
225+
NULL) {
226+
env_global->armed = 0;
227+
}
228+
}
229+
224230
/* Unfinish the request */
225231
frankenphp_server_context *ctx = SG(server_context);
226232
ctx->finished = false;
@@ -282,7 +288,7 @@ PHP_FUNCTION(frankenphp_putenv) {
282288
RETURN_FALSE;
283289
}
284290

285-
if (go_putenv(setting, (int)setting_len)) {
291+
if (go_putenv(thread_index, setting, (int)setting_len)) {
286292
RETURN_TRUE;
287293
} else {
288294
RETURN_FALSE;
@@ -308,13 +314,11 @@ PHP_FUNCTION(frankenphp_getenv) {
308314
return;
309315
}
310316

311-
go_string gname = {name_len, name};
312-
313-
struct go_getenv_return result = go_getenv(thread_index, &gname);
317+
struct go_getenv_return result = go_getenv(thread_index, name);
314318

315319
if (result.r0) {
316320
// Return the single environment variable as a string
317-
RETVAL_STRINGL(result.r1->data, result.r1->len);
321+
RETVAL_STR(result.r1);
318322
} else {
319323
// Environment variable does not exist
320324
RETVAL_FALSE;
@@ -748,17 +752,14 @@ void frankenphp_register_bulk(
748752
zend_string *frankenphp_init_persistent_string(const char *string, size_t len) {
749753
/* persistent strings will be ignored by the GC at the end of a request */
750754
zend_string *z_string = zend_string_init(string, len, 1);
755+
zend_string_hash_val(z_string);
751756

752757
/* interned strings will not be ref counted by the GC */
753758
GC_ADD_FLAGS(z_string, IS_STR_INTERNED);
754759

755760
return z_string;
756761
}
757762

758-
void frankenphp_release_zend_string(zend_string *z_string) {
759-
zend_string_release(z_string);
760-
}
761-
762763
static void
763764
frankenphp_register_variable_from_request_info(zend_string *zKey, char *value,
764765
bool must_be_present,
@@ -844,9 +845,13 @@ static void frankenphp_log_message(const char *message, int syslog_type_int) {
844845
}
845846

846847
static char *frankenphp_getenv(const char *name, size_t name_len) {
847-
go_string gname = {name_len, (char *)name};
848+
struct go_getenv_return result = go_getenv(thread_index, (char *)name);
848849

849-
return go_sapi_getenv(thread_index, &gname);
850+
if (result.r0) {
851+
return result.r1->val;
852+
}
853+
854+
return NULL;
850855
}
851856

852857
sapi_module_struct frankenphp_sapi_module = {

frankenphp.h

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,9 +69,10 @@ void frankenphp_register_variables_from_request_info(
6969
void frankenphp_register_variable_safe(char *key, char *var, size_t val_len,
7070
zval *track_vars_array);
7171
zend_string *frankenphp_init_persistent_string(const char *string, size_t len);
72-
void frankenphp_release_zend_string(zend_string *z_string);
7372
int frankenphp_reset_opcache(void);
7473
int frankenphp_get_current_memory_limit();
74+
void frankenphp_add_assoc_str_ex(zval *track_vars_array, char *key,
75+
size_t keylen, zend_string *val);
7576

7677
void frankenphp_register_single(zend_string *z_key, char *value, size_t val_len,
7778
zval *track_vars_array);

frankenphp_test.go

Lines changed: 47 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ type testOptions struct {
4545
realServer bool
4646
logger *zap.Logger
4747
initOpts []frankenphp.Option
48+
phpIni map[string]string
4849
}
4950

5051
func runTest(t *testing.T, test func(func(http.ResponseWriter, *http.Request), *httptest.Server, int), opts *testOptions) {
@@ -67,6 +68,9 @@ func runTest(t *testing.T, test func(func(http.ResponseWriter, *http.Request), *
6768
initOpts = append(initOpts, frankenphp.WithWorkers(testDataDir+opts.workerScript, opts.nbWorkers, opts.env, opts.watch))
6869
}
6970
initOpts = append(initOpts, opts.initOpts...)
71+
if opts.phpIni != nil {
72+
initOpts = append(initOpts, frankenphp.WithPhpIni(opts.phpIni))
73+
}
7074

7175
err := frankenphp.Init(initOpts...)
7276
require.Nil(t, err)
@@ -671,21 +675,21 @@ func TestEnv(t *testing.T) {
671675
testEnv(t, &testOptions{})
672676
}
673677
func TestEnvWorker(t *testing.T) {
674-
testEnv(t, &testOptions{workerScript: "test-env.php"})
678+
testEnv(t, &testOptions{workerScript: "env/test-env.php"})
675679
}
676680
func testEnv(t *testing.T, opts *testOptions) {
677681
assert.NoError(t, os.Setenv("EMPTY", ""))
678682

679683
runTest(t, func(handler func(http.ResponseWriter, *http.Request), _ *httptest.Server, i int) {
680-
req := httptest.NewRequest("GET", fmt.Sprintf("http://example.com/test-env.php?var=%d", i), nil)
684+
req := httptest.NewRequest("GET", fmt.Sprintf("http://example.com/env/test-env.php?var=%d", i), nil)
681685
w := httptest.NewRecorder()
682686
handler(w, req)
683687

684688
resp := w.Result()
685689
body, _ := io.ReadAll(resp.Body)
686690

687691
// execute the script as regular php script
688-
cmd := exec.Command("php", "testdata/test-env.php", strconv.Itoa(i))
692+
cmd := exec.Command("php", "testdata/env/test-env.php", strconv.Itoa(i))
689693
stdoutStderr, err := cmd.CombinedOutput()
690694
if err != nil {
691695
// php is not installed or other issue, use the hardcoded output below:
@@ -696,6 +700,46 @@ func testEnv(t *testing.T, opts *testOptions) {
696700
}, opts)
697701
}
698702

703+
func TestEnvIsResetInNonWorkerMode(t *testing.T) {
704+
assert.NoError(t, os.Setenv("test", ""))
705+
runTest(t, func(handler func(http.ResponseWriter, *http.Request), _ *httptest.Server, i int) {
706+
putResult := fetchBody("GET", fmt.Sprintf("http://example.com/env/putenv.php?key=test&put=%d", i), handler)
707+
708+
assert.Equal(t, fmt.Sprintf("test=%d", i), putResult, "putenv and then echo getenv")
709+
710+
getResult := fetchBody("GET", "http://example.com/env/putenv.php?key=test", handler)
711+
712+
assert.Equal(t, "test=", getResult, "putenv should be reset across requests")
713+
}, &testOptions{})
714+
}
715+
716+
// TODO: should it actually get reset in worker mode?
717+
func TestEnvIsNotResetInWorkerMode(t *testing.T) {
718+
assert.NoError(t, os.Setenv("index", ""))
719+
runTest(t, func(handler func(http.ResponseWriter, *http.Request), _ *httptest.Server, i int) {
720+
putResult := fetchBody("GET", fmt.Sprintf("http://example.com/env/remember-env.php?index=%d", i), handler)
721+
722+
assert.Equal(t, "success", putResult, "putenv and then echo getenv")
723+
724+
getResult := fetchBody("GET", "http://example.com/env/remember-env.php", handler)
725+
726+
assert.Equal(t, "success", getResult, "putenv should not be reset across worker requests")
727+
}, &testOptions{workerScript: "env/remember-env.php"})
728+
}
729+
730+
// reproduction of https://github.com/dunglas/frankenphp/issues/1061
731+
func TestModificationsToEnvPersistAcrossRequests(t *testing.T) {
732+
runTest(t, func(handler func(http.ResponseWriter, *http.Request), _ *httptest.Server, i int) {
733+
for j := 0; j < 3; j++ {
734+
result := fetchBody("GET", "http://example.com/env/overwrite-env.php", handler)
735+
assert.Equal(t, "custom_value", result, "a var directly added to $_ENV should persist")
736+
}
737+
}, &testOptions{
738+
workerScript: "env/overwrite-env.php",
739+
phpIni: map[string]string{"variables_order": "EGPCS"},
740+
})
741+
}
742+
699743
func TestFileUpload_module(t *testing.T) { testFileUpload(t, &testOptions{}) }
700744
func TestFileUpload_worker(t *testing.T) {
701745
testFileUpload(t, &testOptions{workerScript: "file-upload.php"})

0 commit comments

Comments
 (0)