Skip to content

Commit 0e8de8f

Browse files
fix(worker): initialize $_RESUEST (#2136)
Hi, This PR fixes #1931, it handles $_REQUEST in worker mode correctly when `auto_globals_jit` is enabled (default configuration for PHP). Some concerns were raised in the comments of the issue regarding performance. This implementation should make sure that request is created only if used. However if a previous execution plan already used `_REQUEST`, all subsequent requests will create it. So the concern is "kindof" mitigated. Let me know if you have any suggestion to improve this. --------- Signed-off-by: Xavier Leune <[email protected]> Co-authored-by: Alexander Stecher <[email protected]>
1 parent ddb11c1 commit 0e8de8f

File tree

5 files changed

+111
-5
lines changed

5 files changed

+111
-5
lines changed

frankenphp.c

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -128,13 +128,19 @@ static void frankenphp_reset_super_globals() {
128128
if (auto_global->name == _env) {
129129
/* skip $_ENV */
130130
} else if (auto_global->name == _server) {
131-
/* always reimport $_SERVER */
131+
/* always reimport $_SERVER */
132132
auto_global->armed = auto_global->auto_global_callback(auto_global->name);
133133
} else if (auto_global->jit) {
134-
/* globals with jit are: $_SERVER, $_ENV, $_REQUEST, $GLOBALS,
135-
* jit will only trigger on script parsing and therefore behaves
136-
* differently in worker mode. We will skip all jit globals
137-
*/
134+
/* JIT globals ($_REQUEST, $GLOBALS) need special handling:
135+
* - $GLOBALS will always be handled by the application, we skip it
136+
* For $_REQUEST:
137+
* - If in symbol_table: re-initialize with current request data
138+
* - If not: do nothing, it may be armed by jit later */
139+
if (auto_global->name == ZSTR_KNOWN(ZEND_STR_AUTOGLOBAL_REQUEST) &&
140+
zend_hash_exists(&EG(symbol_table), auto_global->name)) {
141+
auto_global->armed =
142+
auto_global->auto_global_callback(auto_global->name);
143+
}
138144
} else if (auto_global->auto_global_callback) {
139145
/* $_GET, $_POST, $_COOKIE, $_FILES are reimported here */
140146
auto_global->armed = auto_global->auto_global_callback(auto_global->name);

frankenphp_test.go

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -306,6 +306,56 @@ func testPostSuperGlobals(t *testing.T, opts *testOptions) {
306306
}, opts)
307307
}
308308

309+
func TestRequestSuperGlobal_module(t *testing.T) { testRequestSuperGlobal(t, nil) }
310+
func TestRequestSuperGlobal_worker(t *testing.T) {
311+
phpIni := make(map[string]string)
312+
phpIni["auto_globals_jit"] = "1"
313+
testRequestSuperGlobal(t, &testOptions{workerScript: "request-superglobal.php", phpIni: phpIni})
314+
}
315+
func testRequestSuperGlobal(t *testing.T, opts *testOptions) {
316+
runTest(t, func(handler func(http.ResponseWriter, *http.Request), _ *httptest.Server, i int) {
317+
// Test with both GET and POST parameters
318+
// $_REQUEST should contain merged data from both
319+
formData := url.Values{"post_key": {fmt.Sprintf("post_value_%d", i)}}
320+
req := httptest.NewRequest("POST", fmt.Sprintf("http://example.com/request-superglobal.php?get_key=get_value_%d", i), strings.NewReader(formData.Encode()))
321+
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
322+
body, _ := testRequest(req, handler, t)
323+
324+
// Verify $_REQUEST contains both GET and POST data for the current request
325+
assert.Contains(t, body, fmt.Sprintf("'get_key' => 'get_value_%d'", i))
326+
assert.Contains(t, body, fmt.Sprintf("'post_key' => 'post_value_%d'", i))
327+
}, opts)
328+
}
329+
330+
func TestRequestSuperGlobalConditional_worker(t *testing.T) {
331+
// This test verifies that $_REQUEST works correctly when accessed conditionally
332+
// in worker mode. The first request does NOT access $_REQUEST, but subsequent
333+
// requests do. This tests the "re-arm" mechanism for JIT auto globals.
334+
//
335+
// The bug scenario:
336+
// - Request 1 (i=1): includes file, $_REQUEST initialized with val=1
337+
// - Request 3 (i=3): includes file from cache, $_REQUEST should have val=3
338+
// If the bug exists, $_REQUEST would still have val=1 from request 1.
339+
phpIni := make(map[string]string)
340+
phpIni["auto_globals_jit"] = "1"
341+
runTest(t, func(handler func(http.ResponseWriter, *http.Request), _ *httptest.Server, i int) {
342+
if i%2 == 0 {
343+
// Even requests: don't use $_REQUEST
344+
body, _:= testGet(fmt.Sprintf("http://example.com/request-superglobal-conditional.php?val=%d", i), handler, t)
345+
assert.Contains(t, body, "SKIPPED")
346+
assert.Contains(t, body, fmt.Sprintf("'val' => '%d'", i))
347+
} else {
348+
// Odd requests: use $_REQUEST
349+
body, _ := testGet(fmt.Sprintf("http://example.com/request-superglobal-conditional.php?use_request=1&val=%d", i), handler, t)
350+
assert.Contains(t, body, "REQUEST:")
351+
assert.Contains(t, body, "REQUEST_COUNT:2", "$_REQUEST should have ONLY current request's data (2 keys: use_request and val)")
352+
assert.Contains(t, body, fmt.Sprintf("'val' => '%d'", i), "request data is not present")
353+
assert.Contains(t, body, "'use_request' => '1'")
354+
assert.Contains(t, body, "VAL_CHECK:MATCH", "BUG: $_REQUEST contains stale data from previous request! Body: "+body)
355+
}
356+
}, &testOptions{workerScript: "request-superglobal-conditional.php", phpIni: phpIni})
357+
}
358+
309359
func TestCookies_module(t *testing.T) { testCookies(t, nil) }
310360
func TestCookies_worker(t *testing.T) { testCookies(t, &testOptions{workerScript: "cookies.php"}) }
311361
func testCookies(t *testing.T, opts *testOptions) {
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
<?php
2+
// This file tests if $_REQUEST is properly re-initialized in worker mode
3+
// The key test is: does $_REQUEST contain ONLY the current request's data?
4+
5+
// Static counter to track how many times this file is executed (not compiled)
6+
static $execCount = 0;
7+
$execCount++;
8+
9+
echo "EXEC_COUNT:" . $execCount;
10+
echo "\nREQUEST:";
11+
var_export($_REQUEST);
12+
echo "\nREQUEST_COUNT:" . count($_REQUEST);
13+
14+
// Check if $_REQUEST was properly initialized for this request
15+
// If stale, it might have data from a previous request
16+
if (isset($_GET['val'])) {
17+
$expected_val = $_GET['val'];
18+
$actual_val = $_REQUEST['val'] ?? 'MISSING';
19+
echo "\nVAL_CHECK:" . ($expected_val === $actual_val ? "MATCH" : "MISMATCH(expected=$expected_val,actual=$actual_val)");
20+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
<?php
2+
3+
require_once __DIR__.'/_executor.php';
4+
5+
return function () {
6+
// Only access $_REQUEST on requests where use_request=1 is passed
7+
// This tests the "re-arm" scenario where $_REQUEST might be accessed
8+
// for the first time during a later request
9+
if (isset($_GET['use_request']) && $_GET['use_request'] === '1') {
10+
include 'request-superglobal-conditional-include.php';
11+
} else {
12+
echo "SKIPPED";
13+
}
14+
echo "\nGET:";
15+
var_export($_GET);
16+
};

testdata/request-superglobal.php

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
<?php
2+
3+
require_once __DIR__.'/_executor.php';
4+
5+
return function () {
6+
// Output $_REQUEST to verify it contains current request data
7+
// $_REQUEST should be a merge of $_GET and $_POST (based on request_order)
8+
echo "REQUEST:";
9+
var_export($_REQUEST);
10+
echo "\nGET:";
11+
var_export($_GET);
12+
echo "\nPOST:";
13+
var_export($_POST);
14+
};

0 commit comments

Comments
 (0)