Could Early Have Flagged the PHP2Shell Bug in January 2025?
A historical PHP change, an unsafe memory lifetime, and a Fix PR engineers can inspect.

What if Early had been running against PHP changes in January 2025, when a new diagnostic feature introduced a dangerous memory lifetime? Would an engineer have received a useful finding before that defect became part of a major security disclosure?
We used today’s Early Release Guard to analyze the historical change. In the run examined here, Early flagged the backtrace lifetime defect associated with one of the PHP2Shell vulnerabilities. Bug Hunter also identified a separate initialization defect in the same feature.
This was a targeted historical replay performed after disclosure, using today’s Early against the January 2025 change. It demonstrates detection in this selected case, not reliable nightly detection in 2025. A follow-up created a Fix PR on the vulnerable revision.
For the high-level result and engineering takeaway, see the PHP2Shell Case File.
Why we looked at PHP2Shell
On October 21, 2026, Wiz published its PHP2Shell research, describing several vulnerabilities in PHP itself. Its fatal-error backtrace finding falls under CVE-2026-109619. Wiz demonstrated unauthenticated remote code execution against Drupal using that primitive.
Our experiment focused on that backtrace defect. It did not evaluate every PHP2Shell variant, and the other applications discussed in the disclosure were not our analysis targets. We analyzed PHP’s Zend engine.
Discovery and exploitation credit belongs to the researchers behind the PHP2Shell disclosure. Early’s replay examined whether the earlier code change contained enough evidence to flag the defect.
A pointer outlives its request
The feature added a way to retain a fatal error’s backtrace. That trace contains information useful for diagnosing a failure, and PHP stores it in an executor-global value named EG(last_fatal_error_backtrace).
The critical distinction is between the lifetime of a request and the lifetime of the worker handling it. A worker can serve another request after the previous request’s memory has been released. Any surviving pointer into that old memory must be cleared before it can be used again.
In the historical change, backtrace cleanup was inside the non-fast shutdown path. Fast shutdown could release the request heap while leaving the global value referring to it. An error early in a subsequent request could reach that value before normal executor initialization reset it.
The failure crosses a request boundary. Resetting state during normal startup is too late if an earlier diagnostic can already access it.
This is the important connection to the disclosure. Wiz describes a fatal-error backtrace surviving request-heap destruction, then being consumed during early processing of another request. Its exploitation work establishes the security consequence. Our analysis identified the unsafe lifetime in the introducing change.
The change we analyzed
We selected the January 29, 2025 Error Backtraces v2 commit and compared it with its immediate parent. The primary project was /Zend in our fork of php/php-src.
| Replay input | Selected value |
|---|---|
| Baseline commit | 5bf6e2e3785f1f20046f4663e25985d85413eefd |
| Target commit | 0a14ab18d2b3bcf1358aa9f25efc1ad72c18d000 |
| Comparison | One introducing commit against its parent |
| Primary scope | PHP Zend engine, /Zend |
| Analysis | Regression analysis with Bug Hunter enabled |
We compared the introducing commit with its immediate parent, rather than two published PHP releases.
What Early flagged
The central finding was titled “Backtrace global not cleared on fast shutdown.” It came from regression analysis and was surfaced as an actionable bug.


Actual product capture supplied on October 25. The finding is now shown as P1 Security. The verification steps are suggested checks, not evidence that an exploit was reproduced. Open the full-size finding.
Its reasoning identified the relevant chain: cleanup guarded by the shutdown mode, request memory released on the fast path, and a global value still pointing to that memory. It also identified the dangerous interval before the next executor initialization.
That is materially more useful than a generic warning about pointers or error handling. It tells an engineer which lifetime assumption to challenge and where to inspect the cleanup path.
The complete run produced seven findings. Three were surfaced for action. Four remained below the actionable threshold. The three surfaced findings were:
| Finding | What it identified | Evidence boundary |
|---|---|---|
| Backtrace global not cleared on fast shutdown | Request-owned memory remains referenced after shutdown | Matches the targeted PHP2Shell backtrace mechanism |
| Uninitialized EG backtrace zval destroyed | An early error can access a new field before thread initialization makes it safe | Separate historical ZTS defect with an upstream fix |
| Handled user errors retain stale trace | A handled error can retain trace references and disturb diagnostic consistency | Candidate requiring independent runtime validation |

Later product view with “Show all findings” unchecked. Its five visible rows are not the original seven-finding run inventory summarized above. Open the full-size list.
Finding a memory defect and assessing its security consequences are separate tasks. The earlier follow-up investigation treated the backtrace issue as a reliability problem at P2. It also incorrectly described the feature as requiring opt-in, although the analyzed revision enabled fatal-error backtraces by default. The supplied product screenshots now show this same finding as P1 Security. That current classification should not be confused with the original investigation result.
How the later fixes line up
The October 2026 upstream state-corruption fix includes an unconditional reset of the fatal-error backtrace global. The reset sits outside the non-fast shutdown cleanup, addressing the lifetime gap highlighted by the finding. The commit also contains broader hardening, so this one reset should not be presented as the complete PHP2Shell remediation.
Bug Hunter’s additional finding involved a different lifecycle boundary. In thread-safe PHP builds, the new backtrace value was missing initialization in the executor-globals constructor. An error before normal executor initialization could therefore attempt to destroy an indeterminate value. A separate May 2026 fix initialized that field in the constructor.
These are two distinct memory-safety defects. One concerns freed request memory. The other concerns uninitialized thread state. We have not established remote exploitability for the second defect, and neither is a new vulnerability discovery by Early.
PHP’s existing fixes corroborate the mechanisms Early flagged. The original run evaluated detection at the change that introduced the defect.
From finding to Fix PR
After the detection run, we asked Early to create a Fix PR on a branch containing the vulnerable January 2025 revision. Early created a Fix PR with one executable line added to Zend/zend_execute_API.c:
ZVAL_UNDEF(&EG(last_fatal_error_backtrace));
The reset runs at the end of zend_shutdown_executor_values, after object-store cleanup and outside the slow-shutdown condition. It clears the stale global on fast shutdown while preserving the existing destructor on the slow path. The line matches the corresponding cleanup in PHP’s later upstream hardening. It addresses this lifetime gap, not the complete PHP2Shell remediation.
For this follow-up, we prepared a branch at the vulnerable revision; the investigation included PHP’s upstream fix. Early created the linked Fix PR. Compilation and runtime validation remain pending, and no regression test was added.
What the January question can answer
Early flagged the underlying memory-lifetime defect and a separate initialization problem. The finding gave an engineer a specific cleanup path to investigate: a pointer whose owner is gone, a shutdown path that skips it, and another request that can reach it too soon.
The value is earlier intervention: giving engineers a specific bug to investigate when a change introduces it, rather than after its consequences emerge in production.
PHP had already fixed both defects before this replay. Early flagged the backtrace defect in the change that introduced it, then created a Fix PR against the historical revision—giving engineers a specific finding and code change to review.
