Vulnerability Analysis · PHP2Shell
A technical examination of CVE-2026-109618 and CVE-2026-109619: memory ownership, interrupted initialisation and the security consequences of reusing a PHP worker.
Arnab Chattopadhayay, Co-founder, FireCompass Technologies Inc. Technical position checked on 4 November 2026. Based on the supplied research note, corrected and expanded against the PHP project’s advisories and original research. This article examines mechanisms and defensive validation; it does not claim independent reproduction of an exploit.
On 21 October 2026, the PHP project issued emergency security updates for two vulnerabilities collectively known as PHP2Shell. Under affected conditions, an attacker could execute code on a server without logging in, or extract sensitive information from PHP’s process memory. The weaknesses were in PHP itself; the runtime beneath the application, so updating a website’s framework or plugins would not repair them. Researchers demonstrated unauthenticated code execution against WordPress, Drupal and Joomla, alongside memory disclosure that did not depend on those particular applications.[3][6]
For organisations running PHP, this created an awkward situation. A well-maintained application could still sit on a vulnerable engine. Before deciding how exposed they were, administrators needed to establish which PHP builds were serving traffic, how those processes handled requests and whether their hosting provider had delivered a fix.
The discovery had a longer history than the emergency announcement suggested. In 2024, work following an external PHP security audit exposed signs of the underlying out-of-memory problem. Its potential danger was recognised, but a concrete exploitation path had not been established, and competing fixes carried difficult compatibility and availability trade-offs.
The picture changed in 2026. Calif, a cybersecurity research firm specialising in vulnerability discovery and exploit research, began an audit on 30 July; on 11 August, one of their researchers, Ryan Austin, reported remote code execution associated with the same underlying problem. Independent work by OpenAI and additional findings from Wiz expanded the evidence. What had been a difficult runtime-hardening issue now had demonstrated security consequences.[9]
Disclosure then became an operational race. The release was planned for 22 October, but the CVE identifiers began circulating on 20 October, followed by a prematurely published demonstration video. PHP brought the security release forward to 21 October, compressing the time available for distributors and users to prepare.[9]
The impact was serious, although it needs to be described precisely. The research demonstrated a compromise of representative deployments; it did not establish that every PHP website was exploitable in the same way or that every vulnerable server had been attacked. Depending on the path, successful exploitation could expose passwords, tokens and other process-resident secrets, or execute code with the authority of the PHP worker. Memory theft did not require an attacker to obtain a shell first.[6]
That distinction explains why PHP2Shell deserves more than a patch notice. We normally expect a request that exceeds its memory allowance to fail safely. PHP2Shell showed how failure itself could expose a dangerous intermediate state: an object only partly constructed, or a reference surviving after its memory had been released.
To understand how an ordinary HTTP request reaches that point, and why the answer differs between deployments, we need to follow it below the application, into the runtime that parses its input, manages its memory and cleans up after it.
The application is not the first code to touch the request
It is tempting to draw a web application’s security boundary around its routes, middleware and validation functions. That leaves out work performed on its behalf.
Before a PHP script runs, its server integration, the Server Application Programming Interface (SAPI), can arrange request startup and automatic input processing. Multipart input can become arrays, strings, upload metadata and session bookkeeping inside native code. The application receives the results of that work; it does not necessarily supervise it.
PHP urged administrators to update servers running PHP websites. Apache servers using mod_php were a particular concern because researchers demonstrated an attack against PHP itself, before the website’s application code began executing. It lists initial fixed releases for the supported branches as follows. These are minimum fixes for the two original CVEs, not a recommendation to disregard later security updates.[3]
| PHP branch | Initial release fixing the original PHP2Shell pair |
|---|---|
| 8.2 | 8.2.35 |
| 8.3 | 8.3.36 |
| 8.4 | 8.4.27 |
| 8.5 | 8.5.12 |
Older branches need a supported migration or a verified vendor backport. A distribution package’s security status cannot be inferred solely from the upstream version printed in its banner. Conversely, a maintained operating system does not establish that a separately installed PHP binary or container image has been updated.
I would avoid the claim that every PHP installation since 5.4 has an identical remote exploit. Calif traces an early affected path to that generation, but the precise path, deployment conditions and exploit reliability need to be assessed separately.[4]
Two lifetimes in one address space
A long-running PHP worker commonly handles multiple requests without a fresh operating-system process for each one. For each request, it prepares the environment, runs the PHP code and cleans up afterwards. The same worker then handles the next request.
PHP extensions have their own startup and shutdown cycle. In some server configurations, a parent process initialises the extensions before creating worker processes by forking itself. Those workers inherit the initialised state. It is therefore not always correct to say that extensions are initialised separately when each worker starts.
Within that process, request allocations and persistent allocations have different owners. Broadly, emalloc() and efree() manage request memory through Zend’s allocator, while persistent allocations can survive requests. Retaining or recycling underlying allocator storage does not extend the logical lifetime of objects previously stored there.
That last distinction matters. A stale address can still point into mapped memory. The bytes may look plausible. Neither fact makes the former object valid.
Consider a worker-global field referring to a request-owned string. Such a reference is not inherently wrong while the request is active. The obligation is to clear or replace it before the referenced object’s lifetime ends, and to prevent later code from consuming an obsolete value.
On typical modern 64-bit PHP builds, a zval is a compact tagged value containing a value union and metadata. Strings, arrays and objects introduce further referenced structures. If stale storage is reused for a different type, a later operation may interpret those bytes according to the former object’s layout. A value intended as ordinary data can then be treated as a pointer, length, reference count or another internal field.
This is the bridge from a lifecycle mistake to memory corruption. It is not yet a complete exploit. Turning it into reliable disclosure or code execution requires additional control over what is reclaimed, what reads it and how the runtime interprets it.
Why an out-of-memory error is not just an error return
In the vulnerable design, certain fatal exits use zend_bailout() and non-local control transfer based on setjmp()/longjmp(). A saved recovery point allows execution to leave a deeply nested operation without returning through each intervening C function.
That mechanism does not itself perform transactional rollback. Statements following the interrupted allocation do not execute. Local cleanup which depends on reaching those statements does not happen automatically. It is more accurate to describe this as bypassing normal return paths than to imagine C++-style exception unwinding.
The error-handling path must therefore tolerate every intermediate state from which it can be reached. If a function temporarily makes an incomplete object visible, then performs an allocation that can trigger a non-local exit, cleanup needs to know that the object is incomplete.
There are several ways to satisfy that requirement: publish only after successful construction, initialise a destructible placeholder first, provide reliable rollback, or delay failure until a safe boundary. These are general design choices, not interchangeable one-line repairs. Each changes the assumptions made by callers and cleanup code.
The PHP OOM fix changes the failure mechanism itself: it introduces a delayed soft failure at a VM safe point, with a higher-threshold hard failure that terminates the process. That avoids abruptly handing inconsistent state to ordinary shutdown while retaining memory limits.[1]
The first failure: cleanup trusts an unfinished object
Calif’s analysis identifies a particularly clear example in a hash-table insertion path. The following is explanatory pseudocode, not a verbatim patch or a runnable exploit:
allocate storage for the bucket’s key
initialise the remaining bucket fields
finish the insertion
If the allocation exits non-locally, the count already advertises an entry whose fields have not all been established. Later destruction walks the visible entries and treats the unfinished bucket as a real object.[4]
The initial mistake is about publication order. The dangerous consumer is cleanup. No later HTTP request is required to explain this particular failure.
That is an important correction to a common account of PHP2Shell. We should not force every variant into the story of one request poisoning another. A failure during startup followed by unsafe shutdown is already a complete lifecycle problem.
Input limits do not automatically stop it. A request’s wire size and the runtime memory needed to represent it are different quantities. Nested parameters create containers, keys and metadata. Multipart parsing can amplify a relatively compact input into a much larger object graph; empty upload records can also involve bookkeeping without corresponding file data.
For capacity modelling, I would distinguish input bytes from representation cost:
Mrequest = Mbaseline + Minput + Mrepresentation + Mprocessing
There is no universal constant mapping a POST-body limit to safe heap consumption. Shape, nesting, metadata and processing all matter. This is why a server can reject oversized bodies correctly and still need independent limits on structural complexity.
The defensive lesson is broader than PHP. Every parser converting compact input into a richer internal representation needs resource accounting at the representation boundary as well as the transport boundary.
The second failure: a request ends, but its reference survives
CVE-2026-109619 concerns stale worker state. The official description requires a producer that leaves request-owned state reachable and a consumer that acts on it before normal initialisation resets it. The demonstrated cross-request cases require reuse of the same worker.[2]
For this path, the relevant sequence looks like this:
A producer alone is not enough. A dangling reference which is reliably replaced before use does not provide the same attack route. Equally, a suspicious consumer needs a reachable producer and the right lifetime transition.
Session upload progress is relevant because PHP can perform session-related work while processing an upload. The manual documents this facility as progress tracking exposed through session data. Its timing is different from an ordinary application deciding to start a session after routing.[5]
Wiz documents concrete consequences. In one disclosure path, stale session-ID state is interpreted as a live string and reclaimed worker-memory bytes reach a response cookie. Another path involves surviving unserialiser bookkeeping. Its research also identifies fatal-backtrace state and an OPcache synchronisation failure involving process-local and shared pointer-map state. The OPcache case introduces cross-worker type confusion, so even “same worker, next request” is not a complete description of the whole family.[6]
Memory disclosure deserves attention in its own right. An organisation can lose secrets without seeing a shell process or a new executable. Disclosure can also make subsequent corruption more useful by reducing uncertainty about the process layout.
The original note’s references to leaked private keys, credentials and tokens should be read as possible contents of exposed memory, not as a promise that every affected installation holds or reveals every category of secret. The exposure depends on what that process actually handles.
Why the execution model changes the result
PHP-FPM and Apache mod_php should not be treated as equivalent just because both may reuse workers.
For the startup-OOM path, mod_php proceeds into request cleanup, whereas FPM exits a child after a failed startup. The latter breaks that particular route to consuming the malformed state. Red Hat likewise treats deployment architecture as material to risk. That is grounds for differentiated prioritisation, not a declaration that FPM installations are safe.[1][7]
Later-stage failures and suitable application behaviour can expose other paths. The correct assessment is therefore a tuple: deployed build, SAPI, frontend behaviour, effective configuration, extensions, application reachability and worker lifecycle. A version string is one component.
I would also separate concurrency from persistence. Sequential requests in one surviving process can create a cross-request use-after-free without threads. Conversely, a pool introduces an additional practical question: whether the relevant requests reach the worker carrying the stale state. More workers can affect reliability; they do not repair invalid ownership.
Finally, remote code execution means execution with the authority of the compromised process. It does not establish root access, a container escape or compromise of every tenant on a host. Those outcomes require additional permissions or weaknesses. Shared process memory and broadly scoped service credentials can make the initial compromise serious enough without inflating it.
Patching has to reach the process that serves traffic
A Composer update cannot replace a vulnerable PHP engine. Neither can a framework’s validation rule protect a path which runs before that framework starts.
An upstream reverse proxy or WAF sits earlier in the flow, however, and can reject traffic before PHP processes it. Calling all WAF protection ineffective is therefore wrong. A verified rule may interrupt a known path. It still does not establish that every relevant parser shape or lifecycle variant is covered.
For remediation, I would require evidence at three levels:
| Level | Evidence to collect | Typical gap |
|---|---|---|
| Build | Installed package, vendor advisory or image digest covering both CVEs | A banner is treated as patch evidence |
| Runtime | Replacement of the workers actually serving requests | Files updated while old processes remain active |
| Fleet | Every pool, image, region, appliance and deployment accounted for | One healthy instance stands in for the estate |
Containers change the delivery mechanism, not the obligation. Rebuild the image, deploy the replacement and verify that old replicas have gone. Managed hosting requires confirmation about the selected PHP runtime, not just the control-panel software. An appliance needs its vendor’s supported fix.
The source note describes upgrades as inherently disruptive. They can be, especially when obsolete applications require a major-version migration. But a supported-branch security update or a maintained distribution backport need not be the same project as migrating a legacy application. Establish the supported fix path before assuming a wholesale rewrite is necessary.
Configuration changes buy time, with conditions attached
The PHP project is explicit that updating is the complete remedy for the original pair. Configuration measures cover particular paths.[3]
Disabling session upload progress can remove that feature’s demonstrated route, but the change must apply before parsing; doing it inside the target script is too late. Suppressing displayed errors does not establish that the underlying error-state access has disappeared.[2]
The proposed memory_limit=-1 workaround also needs careful handling. It removes a controllable PHP limit but does not make underlying allocation failures impossible. The resource-exhaustion problem moves to the surrounding platform, where containment and kill policy need review. I would not roll it across production simply because it appears in a short mitigation list.
Similarly, reducing FPM’s pm.max_requests shortens worker reuse. The manual defines it as the number of requests a child serves before respawning. At one request per worker, recycling can disrupt cross-request continuity, at a workload-dependent cost. It does not repair corruption consumed within a request or establish universal coverage of shared state.[8]
PHP’s guidance also cautions against treating address-space resource limits as a substitute: inducing allocation failure can reintroduce the vulnerable path. Temporary controls should be validated as a combination, with explicit owners and an expiry when patching is complete.[1]
There is a related session-upload-progress advisory, CVE-2026-115560, involving an unusual serializer configuration and runtime change. It should be tracked separately rather than silently folded into a claim that the two original CVEs are unpatched. Consult the current vendor position on the deployed build.[3]
How I would investigate and validate it
The first task is to distinguish exposure, a triggered failure and compromise. They are different findings.
An affected package establishes a remediation requirement. A memory-limit event establishes a runtime failure. Neither alone proves code execution. A failed remote test also cannot establish safety when its result depends on routing, configuration and runtime state.
For investigation, I would correlate frontend requests with worker identity and process lifetime. Useful evidence includes abnormal input structure, PHP fatal errors, worker exits, unusual response-cookie sizes, unexpected child processes and outbound connections. Wiz specifically calls attention to multipart traffic, memory errors, response cookies and worker churn. Those indicators are leads, not individually conclusive detections.[6]
A request ID tells us which HTTP transaction failed. A worker PID, container identity and timestamps help establish whether a subsequent event used the same process. Without both views, an investigation can separate two events which are causally connected, or wrongly connect unrelated failures.
Application logs may be sparse where execution never reached the application. A rule watching only for command execution will also miss disclosure that stops short of it. Absence of those two signals is weak negative evidence.
For an authorised test programme, I would begin with inventory and runtime configuration. If behavioural reproduction is necessary, use a controlled replica with the relevant SAPI, extensions, frontend and image, synthetic secrets and no route to production data. Establish the behaviour of the vulnerable build, then repeat the same controlled case against the patched build.
The outcome to demonstrate is preservation of runtime invariants under failure: consistent tables, cleared cross-request references and safe worker termination or recovery. A single successful normal request after deployment proves little about any of those properties.
At FireCompass, that distinction is relevant to autonomous testing design. A test hypothesis is not permission to force a production worker into memory exhaustion. The execution layer needs to classify the potential effect, apply the customer’s scope and approvals, and choose between passive evidence, controlled reproduction and an explicitly authorised disruptive test. This is an engineering position, not a claim that FireCompass discovered or independently reproduced PHP2Shell.
What the AI discovery story should actually teach us
There is an AI story here, but it should not obscure the runtime story. The PHP Foundation describes contributions from multiple researchers and AI-assisted workflows, alongside human validation, patch engineering and coordination. It also acknowledges earlier difficulty resolving the OOM issue and the trade-offs involved.[9]
The useful research move is to generalise an observed failure into a pattern worth auditing: make state visible, perform fallible work, then rely on a later statement to complete or restore it. That pattern deserves attention wherever non-local exits can bypass the final step.
An AI system can help search for candidates and trace callers. A candidate still needs a reachable interruption, an actual consumer and evidence of its consequence. Source-code resemblance is not a vulnerability proof; a crash is not automatically a reliable code-execution primitive.
Conclusions: the request boundary is a promise the runtime must keep
What makes PHP2Shell significant is the gap it exposes between a request ending and a process becoming safe to reuse. Those events often occur together during normal operation. Under interrupted initialisation or incomplete cleanup, they can separate.
The two original CVEs approach that gap from different directions. An unfinished structure can become visible to destruction before its fields are valid. Alternatively, a request can release its memory while the worker state still refers to it, leaving an early consumer to use an object whose lifetime has ended. The additional research into session handling, backtraces and OPcache shows why a single upload-specific explanation is too narrow.
There is a useful engineering conclusion here. Error handling is part of the security boundary. Every object published before fallible work creates an obligation: either the object is already safe to inspect and destroy, or the runtime must reliably complete, reverse or abandon the transition. If that obligation depends on reaching a statement after a non-local exit, it has not been met. A successful normal execution does not test it.
For defenders, the immediate priority remains getting a verified fix into every serving runtime. That involves package or image evidence, replacement of old workers and coverage of the estate. A patched development environment, a current framework or a healthy response from one replica cannot stand in for that work. Where a vendor backport is used, the evidence should identify the fixed package rather than rely on a simple comparison with an upstream version.
Deployment distinctions should influence urgency and investigation without becoming excuses for inaction. A path interrupted by FPM’s handling of startup failure may remain reachable elsewhere under different conditions. Equally, demonstrated worker-level execution should not be inflated into an unsupported claim of root access or universal compromise. A defensible response needs that precision.
Temporary measures have a legitimate role when patching cannot happen immediately. Their scope must remain explicit. Disabling a feature, filtering a request shape or shortening worker lifetime can break a known sequence; it does not prove the absence of other producers and consumers. Changing memory limits also changes availability behaviour. The operational cost needs to be understood before an emergency workaround becomes permanent configuration.
Detection and validation require similar discipline. An affected build, a worker crash, a suspicious response and confirmed compromise are different observations. Correlating requests with process identity makes the lifecycle visible; looking only for a shell leaves memory disclosure largely outside the picture. Where reproduction is required, a controlled environment allows the failure to be examined without making the customer’s production system bear the experiment.
For autonomous security testing, this is a concrete safety issue. A model can propose a technically plausible hypothesis while the proposed test remains operationally unacceptable. The execution layer must account for scope, approvals and potential impact before allowing the hypothesis to become traffic. The quality of a finding includes the quality of the method used to establish it.
The AI-assisted discovery deserves recognition for helping researchers follow these patterns across a complicated codebase. The consequential work still includes proving reachability, distinguishing a crash from an exploitation primitive, designing the fix and getting it deployed. Faster candidate generation makes those responsibilities more pressing.
I would judge a PHP2Shell response by whether the organisation can answer three questions with evidence: which serving runtimes are fixed, which temporary controls remain and what they cover, and whether there are signs that the exposed processes were abused. That is a more demanding standard than closing a scanner finding. It is also closer to the actual risk.
A failed request can be an expected event.
An inconsistent runtime must not be.
PHP2Shell brings that distinction into view, in code that many organisations assumed was safely underneath their application security controls.
About FireCompass
FireCompass is an Agentic AI platform for autonomous penetration testing and red teaming across Web, API, and infrastructure. It discovers shadow assets and web applications, safely validates what is exploitable, and connects findings into multi-stage attack paths with near-zero false positives. Unlike traditional scanners, it discovers credential reuse, business-logic flaws, privilege escalation, and app-to-app or app-to-network lateral movement. It can operate autonomously or with expert-in-the-loop validation, with scope, state, and safety enforced outside the model and every action logged. FireCompass has 30+ analyst recognitions across Forrester and IDC, and is trusted by Fortune 100 enterprises.
Find the runtimes still serving traffic unpatched
Run FireCompass against your own environment to discover which internet-facing assets expose a vulnerable runtime, validated with evidence rather than a version banner.
Hack Yourself Before AI Does.
References
- PHP project, RCE via OOM corruption, GHSA-w8hw-w9fx-j79w, CVE-2026-109618. Official mechanism, deployment distinctions and remediation.
- PHP project, RCE via stale global state, GHSA-6wmf-4rv5-p3rp, CVE-2026-109619. Official producer/consumer model and lifecycle conditions.
- PHP project, October 2026 security announcements, including the separate 23 October session advisory. Verify current releases and vendor backports before deployment.
- Ryan Austin / Calif Research, PHP2Shell: A tale of two PHP remote vulnerabilities. Original research on interrupted insertion and related exploitation paths.
- PHP manual, Session Upload Progress.
- Wiz Research, PHP2Shell: AI uncovers critical RCE vulnerabilities. Original research on session disclosure, unserialisation, backtrace state and OPcache.
- Red Hat Product Security, RHSB-2026-013. Vendor-specific impact assessment.
- PHP manual, FPM configuration, particularly worker lifecycle and
pm.max_requests. - The PHP Foundation, Today’s PHP Security Release, and How We’re Strengthening PHP Security. Maintainer account of remediation, research and disclosure.
Source note: the supplied PHP2Shell-Blog.pdf informed the runtime discussion. Specific descriptions were corrected against the primary sources above. In particular, OOM corruption is not universally cross-request; FPM and mod_php differ; upstream WAFs can interrupt paths; code execution is not automatically root compromise; and mitigation costs depend on deployment rather than being universally catastrophic.
