Skip to content

Commit 1f216dd

Browse files
henderkesAlliBalliBabaCopilot
authored
fix: feat: add php_server's env vars to sandboxed environment (#2451)
closes #1674 was looking through old issues and found this one that already had a plan to solve it --------- Signed-off-by: Marc <m@pyc.ac> Co-authored-by: Alliballibaba <alliballibaba@gmail.com> Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
1 parent 39b9692 commit 1f216dd

7 files changed

Lines changed: 206 additions & 14 deletions

File tree

cgi.go

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,12 @@ package frankenphp
44
// #cgo nocallback frankenphp_register_variable_safe
55
// #cgo nocallback frankenphp_register_known_variable
66
// #cgo nocallback frankenphp_init_persistent_string
7+
// #cgo nocallback frankenphp_add_to_prepared_env
78
// #cgo noescape frankenphp_register_server_vars
89
// #cgo noescape frankenphp_register_variable_safe
910
// #cgo noescape frankenphp_register_known_variable
1011
// #cgo noescape frankenphp_init_persistent_string
12+
// #cgo noescape frankenphp_add_to_prepared_env
1113
// #include "frankenphp.h"
1214
// #include <php_variables.h>
1315
import "C"
@@ -162,11 +164,12 @@ func addHeadersToServer(ctx context.Context, request *http.Request, trackVarsArr
162164
}
163165
}
164166

165-
func addPreparedEnvToServer(fc *frankenPHPContext, trackVarsArray *C.zval) {
166-
for k, v := range fc.env {
167-
C.frankenphp_register_variable_safe(toUnsafeChar(k), toUnsafeChar(v), C.size_t(len(v)), trackVarsArray)
167+
// registerPreparedEnv exposes fc.env to getenv() before any PHP code runs.
168+
func registerPreparedEnv(env PreparedEnv) {
169+
size := C.size_t(len(env))
170+
for k, v := range env {
171+
C.frankenphp_add_to_prepared_env(toUnsafeChar(k), C.size_t(len(k)-1), toUnsafeChar(v), C.size_t(len(v)), size)
168172
}
169-
fc.env = nil
170173
}
171174

172175
//export go_register_server_variables
@@ -180,7 +183,9 @@ func go_register_server_variables(threadIndex C.uintptr_t, trackVarsArray *C.zva
180183
}
181184

182185
// The Prepared Environment is registered last and can overwrite any previous values
183-
addPreparedEnvToServer(fc, trackVarsArray)
186+
if len(fc.env) != 0 {
187+
C.frankenphp_merge_with_prepared_env(trackVarsArray)
188+
}
184189
}
185190

186191
// splitCgiPath splits the request path into SCRIPT_NAME, SCRIPT_FILENAME, PATH_INFO, DOCUMENT_URI
@@ -287,6 +292,10 @@ func go_update_request_info(threadIndex C.uintptr_t, info *C.sapi_request_info)
287292
return nil
288293
}
289294

295+
if len(fc.env) != 0 {
296+
registerPreparedEnv(fc.env)
297+
}
298+
290299
if m, ok := cStringHTTPMethods[request.Method]; ok {
291300
info.request_method = m
292301
} else {

frankenphp.c

Lines changed: 79 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,10 @@ HashTable *main_thread_env = NULL;
101101
__thread uintptr_t thread_index;
102102
__thread bool is_worker_thread = false;
103103
__thread HashTable *sandboxed_env = NULL;
104+
/* prepared_env holds entries from php(_server)'s `env KEY VAL`, exposed to
105+
* getenv() and merged into $_ENV when 'E' is in variables_order. Separate from
106+
* putenv() so those don't leak into $_ENV. */
107+
__thread HashTable *prepared_env = NULL;
104108

105109
/* Published via SG(server_context) so ext-parallel children, which inherit
106110
* the parent's SG(server_context), can route SAPI callbacks back to the
@@ -519,9 +523,17 @@ bool frankenphp_shutdown_dummy_request(void) {
519523
}
520524

521525
void get_full_env(zval *track_vars_array) {
522-
zend_hash_extend(Z_ARR_P(track_vars_array),
523-
zend_hash_num_elements(main_thread_env), 0);
526+
size_t total = zend_hash_num_elements(main_thread_env);
527+
if (prepared_env != NULL) {
528+
// perf: doesn't matter if we get the exact count, just >= needed
529+
total += zend_hash_num_elements(prepared_env);
530+
}
531+
zend_hash_extend(Z_ARR_P(track_vars_array), total, 0);
524532
zend_hash_copy(Z_ARR_P(track_vars_array), main_thread_env, NULL);
533+
if (prepared_env != NULL) {
534+
zend_hash_copy(Z_ARR_P(track_vars_array), prepared_env,
535+
(copy_ctor_func_t)zval_add_ref);
536+
}
525537
}
526538

527539
/* Adapted from php_request_startup() */
@@ -626,6 +638,13 @@ PHP_FUNCTION(frankenphp_putenv) {
626638

627639
if (sandboxed_env == NULL) {
628640
sandboxed_env = zend_array_dup(main_thread_env);
641+
/* prepared_env overrides the OS env and putenv() overrides both, so layer
642+
* the prepared vars onto the dup before sandboxed_env starts shadowing the
643+
* other two layers in getenv(). */
644+
if (prepared_env != NULL) {
645+
zend_hash_copy(sandboxed_env, prepared_env,
646+
(copy_ctor_func_t)zval_add_ref);
647+
}
629648
}
630649

631650
/* cut at null byte to stay consistent with regular putenv */
@@ -661,6 +680,38 @@ PHP_FUNCTION(frankenphp_putenv) {
661680
RETURN_BOOL(success);
662681
} /* }}} */
663682

683+
/* getenv() lookup: sandboxed_env if present (it already holds prepared + OS),
684+
* otherwise prepared_env then main_thread_env. */
685+
static inline zval *frankenphp_lookup_env(const char *name, size_t name_len) {
686+
if (sandboxed_env != NULL) {
687+
return zend_hash_str_find(sandboxed_env, name, name_len);
688+
}
689+
690+
zval *env_val = NULL;
691+
if (prepared_env != NULL) {
692+
env_val = zend_hash_str_find(prepared_env, name, name_len);
693+
}
694+
if (env_val == NULL) {
695+
env_val = zend_hash_str_find(main_thread_env, name, name_len);
696+
}
697+
698+
return env_val;
699+
}
700+
701+
/* Returns a fresh copy of the full environment, merging the layers above. */
702+
static inline HashTable *frankenphp_dup_env(void) {
703+
if (sandboxed_env != NULL) {
704+
return zend_array_dup(sandboxed_env);
705+
}
706+
707+
HashTable *env = zend_array_dup(main_thread_env);
708+
if (prepared_env != NULL) {
709+
zend_hash_copy(env, prepared_env, (copy_ctor_func_t)zval_add_ref);
710+
}
711+
712+
return env;
713+
}
714+
664715
/* {{{ Get the env from the sandboxed environment */
665716
PHP_FUNCTION(frankenphp_getenv) {
666717
zend_string *name = NULL;
@@ -672,14 +723,12 @@ PHP_FUNCTION(frankenphp_getenv) {
672723
Z_PARAM_BOOL(local_only)
673724
ZEND_PARSE_PARAMETERS_END();
674725

675-
HashTable *ht = sandboxed_env ? sandboxed_env : main_thread_env;
676-
677726
if (!name) {
678-
RETURN_ARR(zend_array_dup(ht));
727+
RETURN_ARR(frankenphp_dup_env());
679728
return;
680729
}
681730

682-
zval *env_val = zend_hash_find(ht, name);
731+
zval *env_val = frankenphp_lookup_env(ZSTR_VAL(name), ZSTR_LEN(name));
683732
if (env_val && Z_TYPE_P(env_val) == IS_STRING) {
684733
zend_string *str = Z_STR_P(env_val);
685734
zend_string_addref(str);
@@ -1203,6 +1252,13 @@ void frankenphp_register_server_vars(zval *track_vars_array,
12031252
zend_hash_update_ind(ht, frankenphp_strings.remote_ident, &zv);
12041253
}
12051254

1255+
void frankenphp_merge_with_prepared_env(zval *track_vars_array) {
1256+
if (prepared_env != NULL) {
1257+
HashTable *ht = Z_ARRVAL_P(track_vars_array);
1258+
zend_hash_copy(ht, prepared_env, (copy_ctor_func_t)zval_add_ref);
1259+
}
1260+
}
1261+
12061262
/** Create an immutable zend_string that lasts for the whole process **/
12071263
zend_string *frankenphp_init_persistent_string(const char *string, size_t len) {
12081264
/* persistent strings will be ignored by the GC at the end of a request */
@@ -1316,9 +1372,7 @@ static void frankenphp_log_message(const char *message, int syslog_type_int) {
13161372
}
13171373

13181374
static char *frankenphp_getenv(const char *name, size_t name_len) {
1319-
HashTable *ht = sandboxed_env ? sandboxed_env : main_thread_env;
1320-
1321-
zval *env_val = zend_hash_str_find(ht, name, name_len);
1375+
zval *env_val = frankenphp_lookup_env(name, name_len);
13221376
if (env_val && Z_TYPE_P(env_val) == IS_STRING) {
13231377
zend_string *str = Z_STR_P(env_val);
13241378
return ZSTR_VAL(str);
@@ -1381,6 +1435,22 @@ static inline void reset_sandboxed_environment() {
13811435
zend_hash_release(sandboxed_env);
13821436
sandboxed_env = NULL;
13831437
}
1438+
if (prepared_env != NULL) {
1439+
zend_hash_release(prepared_env);
1440+
prepared_env = NULL;
1441+
}
1442+
}
1443+
1444+
/* Adds a key/value pair to the per-thread prepared environment, exposing
1445+
* env vars from the php(_server) directive to getenv() and $_ENV. */
1446+
void frankenphp_add_to_prepared_env(char *name, size_t name_len, char *val,
1447+
size_t val_len, size_t size) {
1448+
if (prepared_env == NULL) {
1449+
prepared_env = zend_new_array(size);
1450+
}
1451+
zval zv = {0};
1452+
ZVAL_STRINGL(&zv, val, val_len);
1453+
zend_hash_str_update(prepared_env, name, name_len, &zv);
13841454
}
13851455

13861456
static void *php_thread(void *arg) {

frankenphp.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -202,6 +202,9 @@ void frankenphp_register_variable_safe(char *key, char *var, size_t val_len,
202202
zval *track_vars_array);
203203
void frankenphp_register_server_vars(zval *track_vars_array,
204204
frankenphp_server_vars vars);
205+
void frankenphp_add_to_prepared_env(char *name, size_t name_len, char *val,
206+
size_t val_len, size_t size);
207+
void frankenphp_merge_with_prepared_env(zval *track_vars_array);
205208

206209
zend_string *frankenphp_init_persistent_string(const char *string, size_t len);
207210
int frankenphp_get_current_memory_limit();

frankenphp_test.go

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -767,6 +767,69 @@ func TestEnvIsNotResetInWorkerMode(t *testing.T) {
767767
}, &testOptions{workerScript: "env/remember-env.php"})
768768
}
769769

770+
// reproduction of https://github.com/php/frankenphp/issues/1674
771+
func TestPreparedEnvIsVisibleToGetenv_module(t *testing.T) {
772+
testPreparedEnvIsVisibleToGetenv(t, &testOptions{nbParallelRequests: 1})
773+
}
774+
func TestPreparedEnvIsVisibleToGetenv_worker(t *testing.T) {
775+
testPreparedEnvIsVisibleToGetenv(t, &testOptions{
776+
workerScript: "env/prepared-env-getenv.php",
777+
})
778+
}
779+
func testPreparedEnvIsVisibleToGetenv(t *testing.T, opts *testOptions) {
780+
if opts.phpIni == nil {
781+
opts.phpIni = map[string]string{}
782+
}
783+
opts.phpIni["variables_order"] = "EGPCS"
784+
opts.requestOpts = append(opts.requestOpts,
785+
frankenphp.WithRequestEnv(map[string]string{"FRANKENPHP_TEST_PHP_SERVER_ENV_IN_GETENV": "hello"}),
786+
)
787+
788+
expectedEnv := "'hello'"
789+
if opts.workerScript != "" {
790+
// workers don't populate $_ENV regardless of variables_order
791+
expectedEnv = "NULL"
792+
}
793+
794+
runTest(t, func(handler func(http.ResponseWriter, *http.Request), _ *httptest.Server, _ int) {
795+
body, _ := testGet("http://example.com/env/prepared-env-getenv.php", handler, t)
796+
assert.Equal(t, fmt.Sprintf("getenv='hello'\nserver='hello'\nenv=%s\n", expectedEnv), body)
797+
}, opts)
798+
}
799+
800+
// $_ENV mustn't be filled with prepared_env without E in variables_order
801+
func TestPreparedEnvIsNotInEnvWithoutVariablesOrderE(t *testing.T) {
802+
opts := &testOptions{
803+
nbParallelRequests: 1,
804+
phpIni: map[string]string{"variables_order": "GPCS"},
805+
}
806+
opts.requestOpts = append(opts.requestOpts,
807+
frankenphp.WithRequestEnv(map[string]string{"FRANKENPHP_TEST_PHP_SERVER_ENV_IN_GETENV": "hello"}),
808+
)
809+
runTest(t, func(handler func(http.ResponseWriter, *http.Request), _ *httptest.Server, _ int) {
810+
body, _ := testGet("http://example.com/env/prepared-env-getenv.php", handler, t)
811+
assert.Equal(t, "getenv='hello'\nserver='hello'\nenv=NULL\n", body)
812+
}, opts)
813+
}
814+
815+
func TestPreparedEnvSurvivesPutenv_module(t *testing.T) {
816+
testPreparedEnvSurvivesPutenv(t, &testOptions{nbParallelRequests: 1})
817+
}
818+
func TestPreparedEnvSurvivesPutenv_worker(t *testing.T) {
819+
testPreparedEnvSurvivesPutenv(t, &testOptions{
820+
workerScript: "env/prepared-env-survives-putenv.php",
821+
})
822+
}
823+
func testPreparedEnvSurvivesPutenv(t *testing.T, opts *testOptions) {
824+
opts.requestOpts = append(opts.requestOpts,
825+
frankenphp.WithRequestEnv(map[string]string{"FRANKENPHP_PREPARED": "prepared_value"}),
826+
)
827+
runTest(t, func(handler func(http.ResponseWriter, *http.Request), _ *httptest.Server, _ int) {
828+
body, _ := testGet("http://example.com/env/prepared-env-survives-putenv.php", handler, t)
829+
assert.Equal(t, "before='prepared_value'\nprepared='prepared_value'\nput='put_value'\n", body)
830+
}, opts)
831+
}
832+
770833
// reproduction of https://github.com/php/frankenphp/issues/1061
771834
func TestModificationsToEnvPersistAcrossRequests(t *testing.T) {
772835
runTest(t, func(handler func(http.ResponseWriter, *http.Request), _ *httptest.Server, i int) {

requestoptions.go

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,13 +130,38 @@ func WithRequestEnv(env map[string]string) RequestOption {
130130
}
131131

132132
func WithRequestPreparedEnv(env PreparedEnv) RequestOption {
133+
env = ensurePreparedEnv(env)
134+
133135
return func(o *frankenPHPContext) error {
134136
o.env = env
135137

136138
return nil
137139
}
138140
}
139141

142+
// ensurePreparedEnv ensures every key is NUL-terminated
143+
// Empty keys are dropped
144+
func ensurePreparedEnv(env PreparedEnv) PreparedEnv {
145+
for k := range env {
146+
if k == "" || k[len(k)-1] != '\x00' {
147+
fixed := make(PreparedEnv, len(env))
148+
for k, v := range env {
149+
if k == "" {
150+
continue
151+
}
152+
if k[len(k)-1] != '\x00' {
153+
k += "\x00"
154+
}
155+
fixed[k] = v
156+
}
157+
158+
return fixed
159+
}
160+
}
161+
162+
return env
163+
}
164+
140165
func WithOriginalRequest(r *http.Request) RequestOption {
141166
return func(o *frankenPHPContext) error {
142167
o.originalRequest = r
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
<?php
2+
3+
require_once __DIR__ . '/../_executor.php';
4+
5+
return function () {
6+
// Variables declared via the env subdirective in the php(_server) directive
7+
// (or via WithRequestEnv) must be exposed to both $_SERVER and getenv().
8+
// See https://github.com/php/frankenphp/issues/1674
9+
echo "getenv=" . var_export(getenv('FRANKENPHP_TEST_PHP_SERVER_ENV_IN_GETENV'), true) . "\n";
10+
echo "server=" . var_export($_SERVER['FRANKENPHP_TEST_PHP_SERVER_ENV_IN_GETENV'] ?? null, true) . "\n";
11+
echo "env=" . var_export($_ENV['FRANKENPHP_TEST_PHP_SERVER_ENV_IN_GETENV'] ?? null, true) . "\n";
12+
};
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
<?php
2+
3+
require_once __DIR__ . '/../_executor.php';
4+
5+
return function () {
6+
echo "before=" . var_export(getenv('FRANKENPHP_PREPARED'), true) . "\n";
7+
putenv('FRANKENPHP_PUT=put_value');
8+
echo "prepared=" . var_export(getenv('FRANKENPHP_PREPARED'), true) . "\n";
9+
echo "put=" . var_export(getenv('FRANKENPHP_PUT'), true) . "\n";
10+
};

0 commit comments

Comments
 (0)