Keep the slot filter_input(INPUT_ENV) reads for the whole worker - #302
Merged
Conversation
Fix: - filter_input(INPUT_ENV, …), filter_input_array(INPUT_ENV) and filter_has_var(INPUT_ENV, …) answer again in worker mode. The filter extension does not read $_ENV for INPUT_ENV — it reads an array the engine keeps for itself, and reports "no such variable" for anything else it finds there. That array is the only source it has: the one it would prefer is filled by an input filter nothing ever invokes for ENV. Worker mode pins $_ENV so a .env loader's values survive a bootstrap that runs once per worker, and pinning means the callback is disarmed, so nothing rebuilds the array once the per-request cleanup destroys it — from the request after the one that first built $_ENV, all three functions answered NULL, NULL and false for every name the environment plainly had. The cleanup now leaves that one slot standing, as the end of a request already did. - A request that pauses no longer takes it with it, which is the same hole seen from the other side. The suspend snapshot moved all six slots out and the resume put all six back, so the request served inside a paused window found the array missing exactly as before — and a second request pausing while it was missing would save that emptiness and hand it back over the array the first one restored, taking the API away from every request the worker served afterwards. That is why this and the cleanup change together or not at all: either alone is worse than the defect, and the failure of the pair is concurrency-only. - What the three functions report is the process environment, as PHP reports it everywhere: values the application wrote into $_ENV are not among them, because writing to $_ENV gives that array its own copy while the filter extension goes on reading the engine's. One worker-mode difference stays, the one $_ENV already has — the environment reported is the one captured when $_ENV was first built, so a later putenv() is not in it, while getenv() reads live. Docs: - The superglobals reference stated that these three functions report nothing in worker mode, and the release notes published the same thing as a deliberate consequence of pinning $_ENV. Both now describe what the functions read and what they do not, folded into the entry about the pinning itself rather than added beside it, since the two would otherwise ship in one release saying opposite things. Both also say the note covers INPUT_ENV alone and claims nothing about INPUT_GET, INPUT_POST or INPUT_COOKIE, which read storage of their own. Tests: - Three lines in the runtime-hooks profile, one worker: read the environment through all three entry points on a later request, and again from inside a paused window and after resuming. Each line covers one of the three places the array can be lost, and a first line materialises $_ENV before them — until it exists the callback is still armed and rebuilds the array on demand, which would make the rest pass on a build with the defect. - The paused-window line carries the discriminating check: its inner self-request asserts the environment from inside the window rather than only reporting that it ran, so a snapshot that carried the array off fails there instead of passing quietly. - The first line checks its own precondition against a key the environment has rather than against a non-empty $_ENV. An earlier test in the profile seeds a key onto the same worker, so a non-empty array is true whatever variables_order says — and a build importing no environment at all would then pass that line and fail the reads below it, which reads as a server fault rather than as configuration. - It also writes a key into $_ENV that the two reading lines assert is not visible through INPUT_ENV, which is what pins the documented half of the contract that rests on copy-on-write. Paired with a positive check that the key is still in $_ENV, without which it would pass on a build where the write never arrived. 3 integration tests.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
In worker mode
filter_input(INPUT_ENV, …),filter_input_array(INPUT_ENV)andfilter_has_var(INPUT_ENV, …)reported that no environment variables exist, for every name, on every request after the one that first built$_ENVon that worker. They answer again, and report the process environment — the same thing they report in every other SAPI.Why it happened
ext/filterdoes not read$_ENVforINPUT_ENV.php_filter_get_storage()readsIF_G(env_array)and falls back toPG(http_globals)[TRACK_VARS_ENV], treating a non-array there as "storage not initialized". The fallback is the only path that exists:IF_G(env_array)is written only by the SAPI input filter withPARSE_ENV, and nothing ever calls the input filter with that argument —_php_import_environment_variables()registers throughphp_register_variable_quick(), which bypasses it.Worker mode pins
$_ENVso a.envloader's values survive a bootstrap that runs once per worker, and pinning works by disarming the auto-global. With the callback disarmed nothing rebuilds that slot, while the per-request cleanup destroyed it along with the other five. So the slot sat undefined for the rest of the worker's life.The change
PG(http_globals)[TRACK_VARS_ENV]is worker state, not request state. Three loops that walk the six slots now skip that index:oxphp_reset_request_context_globals()oxphp_fiber_save_php_state()oxphp_fiber_restore_php_state()The fourth such loop, in the request-finalize path, already skipped it.
These three go together or not at all. With only the first, a pausing request carries the array off and the request served inside that window finds it missing; a second request pausing while it is missing saves that emptiness and hands it back over the array the first one restored, taking the API away from every request the worker serves afterwards — permanently, and only under concurrency.
Contract
What the three functions report is the process environment. Values the application wrote into
$_ENVare not among them: writing to$_ENVseparates that array from the engine's copy by copy-on-write, and the filter extension goes on reading the engine's. That is true of PHP everywhere, not something worker mode introduces.One worker-mode difference remains, and it is the one
$_ENValready had: the environment reported is the one captured when$_ENVwas first built on that worker, so aputenv()call made afterwards is not in it.getenv()reads the live environment and is unaffected.This covers
INPUT_ENVonly and claims nothing aboutINPUT_GET,INPUT_POSTorINPUT_COOKIE, which read storage of their own.Traditional, framework and SPA serving were never affected — the reset that disarms
_ENVruns only on worker-mode dispatch paths.Tests
tests/php/hooks/test_env_filter_input.phpwithtests/php/hooks/fixture_inner_env_filter.php, three lines in thehooksprofile (PHP_WORKERS=1, so every request lands on one worker):?mode=warm$_ENV, checks its own precondition, seeds a$_ENVwrite?mode=expect?mode=suspendTwo details that keep the test honest. The precondition in
?mode=warmcompares$_ENV['PATH']againstgetenv('PATH')rather than asserting a non-empty array — an earlier test in the profile seeds a key onto the same worker, so "non-empty" holds whatevervariables_ordersays, and a build importing no environment would pass that line and fail the reads below it, which reads as a server fault instead of configuration. And the$_ENVwrite seeded there is asserted to be invisible throughINPUT_ENV, paired with a positive check that it is still in$_ENV— without the positive half the negative one passes on a build where the write never arrived.Verification
Two images from the same Dockerfile, differing only by the
ext/change (oxphp_sapi.sosha2562b1d72d0…vs05cc8b1e…, so the green run is not a stale image):RED fails on exactly the reported defect:
Other profiles on the green image:
fibers32/32,async36/36,worker53 passed with 3 errors — the knownshared/*stress flakes, which give the same three on the red image.Host:
cargo fmt -- --check,cargo clippy --no-default-features -- -D warnings,cargo test --no-default-featuresall clean. The diff touches no Rust.