PHP2Shell is not a parser bug or an injection flaw. It is a family of memory-safety bugs in how PHP cleans up when a request dies badly. The two CVEs, CVE-2026-109618 and CVE-2026-109619, patched on October 21, 2026, share one root: when a request is cut short by an out-of-memory abort or a fatal error, PHP could leave behind data that was half-initialized or that pointed at memory about to be freed. Cleanup, or the next request handled by the same worker, then trusted that data. Researchers at Calif, Wiz and OpenAI turned that into unauthenticated remote code execution against WordPress, Drupal, Joomla and Laravel.
This explainer is for developers and operators who want to understand the mechanism well enough to judge their own exposure. If you only need the timeline and the first-hour checklist, start with what happened and what to do now. For the commands, see the PHP2Shell patch guide.
TL;DR: the mechanism in six rows
| Question | Short answer |
|---|---|
| What is the root cause? | Abnormal request termination (out-of-memory, fatal error, timeout) leaves invalid or stale internal state |
| What does CVE-2026-109618 do? | Out-of-memory longjmp() interrupts setup; shutdown treats half-built data as valid |
| What does CVE-2026-109619 do? | Globals keep pointers to freed request memory; the next request on the same worker uses them |
| Why is it remote? | Attackers trigger out-of-memory with crafted multipart bodies parsed before your code runs |
| Why is mod_php worst? | It runs shutdown after failed startup and reuses the worker |
| What does the fix do? | Removes longjmp() for out-of-memory, adds soft and hard limits, resets state before freeing |
Background: how a PHP worker handles requests
A PHP web worker is a long-lived process. Under Apache with mod_php it is an Apache child; under PHP-FPM it is a pool child; FrankenPHP and LiteSpeed have their own long-lived workers. Each request goes through three phases:
- Startup. PHP initializes request state and, for POST requests, parses the body automatically. Multipart form data becomes
$_POSTand$_FILEShere, before your first line of application code. - Execution. Your script runs, allocating memory from a per-request heap.
- Shutdown. PHP runs cleanup, flushes output, then frees the whole request heap at once.
Two design choices made PHP2Shell possible. First, PHP keeps some state in process globals that outlive a request: error state, session state, object and extension state. Some of these globals point into the per-request heap. Second, when a request exceeds memory_limit, PHP historically aborted with longjmp(), a C mechanism that jumps straight back to a saved point, skipping everything in between.
CVE-2026-109618: out-of-memory corruption
The GitHub advisory GHSA-w8hw-w9fx-j79w explains the first bug. In some code paths, request memory is reachable before it is fully initialized. If an allocation fails right then, the longjmp() skips the rest of the setup. Shutdown later reads or branches on fields that were never written. The advisory adds an important detail: the failed allocation "does not need to be related to the corrupted memory." Any allocation failing at the wrong moment is enough.
How an attacker forces out-of-memory remotely
The trick is to make PHP use far more memory than the request size suggests, while staying under the limits PHP appears to enforce. The advisory lists the shapes:
- Multipart bodies without a usable declared length, which slip past the initial
post_max_sizecheck - An unlimited number of headers in one multipart part, which costs far more memory inside PHP than on the wire
- Empty file controls, which do not count toward
max_file_uploadsbut still build file metadata arrays, multiplied by nested field names such asa[b][c][d] - Application-specific allocations driven by attacker input
All of this happens during startup body parsing. That is why the attack lands before the application, its authentication or its plugins see anything.
Why the result is code execution
Corrupted state during shutdown gives the attacker a use-after-free or an uninitialized pointer access. On its own, that would usually crash the worker. The research teams shaped the heap over many requests so that the freed memory held attacker-chosen data when shutdown used it. heise reports it can take several thousand requests to land; RCE then works reliably within thousands to tens of thousands of requests. That is noisy but trivial for an automated scanner.
CVE-2026-109619: stale global state across requests
The second advisory, GHSA-6wmf-4rv5-p3rp, covers the cross-request half. When a fatal error, an out-of-memory abort or an execution timeout cuts a request short, some globals are not reset before the request heap is freed. They keep pointing at memory that no longer belongs to anyone. Diagnostics emitted during shutdown can even refill error state after it was cleared.
Then a new request arrives on the same worker. In the advisory's words: "Some input, configuration, session and error handling operations run before normal initialization of the next request." That early code can follow the stale pointer.
The confirmed consumers of the stale state are:
- INI diagnostics raised before request initialization, for example from invalid or deprecated per-request settings
- Session upload progress handling (
session.upload_progress.enabled), which reads session data early - Fatal error backtraces on PHP 8.5, where
fatal_error_backtracessaves a backtrace that can survive into the next request; the advisory calls this the strongest confirmed case
The attacker's requests must reach the same worker that holds the stale state. More workers raise the number of attempts needed; they do not stop the attack.
The four primitives researchers built
Wiz Research described four bugs that map onto the two CVEs and combined them into a single script, php2shell.
| Primitive | What it gives | Demonstrated on |
|---|---|---|
| Upload-progress unserializer use-after-free | Fatal allocation failure leaves unserializer state published for a later request | Pre-login RCE on WordPress, Drupal, Joomla |
| Remote process-memory disclosure | Stale session ID pointer read during upload-progress cleanup leaks worker memory into PHPSESSID | Any PHP app, even without session_start() |
| Fatal-error backtrace use-after-free | Global pointing to freed memory survives into the next request | Drupal |
| OPcache type confusion | Mismatch between process-local and shared state gives cross-worker type confusion in shared memory | WordPress |
The memory disclosure deserves attention on its own. It does not need code execution. A web worker's freed memory may contain another user's login request, a database password from a config load, a session token or an API key. Wiz and Sansec both warn that TLS private keys and certificates can be in range when they live in process memory.
mod_php vs PHP-FPM vs FrankenPHP: where the risk differs
The SAPI, the layer that connects PHP to the web server, decides how easy the attack is. Picture one worker serving two requests: request A dies on out-of-memory, request B arrives on the same process. What happens in between depends on the SAPI.
| SAPI | Behavior on failed startup | PHP2Shell status per advisory |
|---|---|---|
| Apache mod_php | Runs shutdown anyway; worker is reused | Full RCE validated over HTTP with default settings |
| PHP-FPM | Exits the child, blocking the startup path | Still affected after normal startup; no complete public exploit |
| FrankenPHP | Runs shutdown unconditionally | Likely susceptible from source analysis |
| LiteSpeed LSAPI | Modeled on FPM | Application-specific exploits likely, unvalidated |
| php-cgi | Listed by PHP as at risk | Update regardless |
The practical reading: mod_php shops are in the most danger and should patch first. FPM shops should not relax. A PHP security team member wrote that the reporters' source analysis puts FPM, FrankenPHP and LSAPI in scope, and that the verified exploit so far is against mod_php. Absence of a public exploit is not absence of an exploit.
What the patch changes
The fix is structural, not a filter on bad input. According to the CVE-2026-109618 advisory:
- No more
longjmp()for out-of-memory. PHP now sets a "soft" out-of-memory condition and stops at the next VM safe point, after setup is complete, so shutdown never sees half-built state. - A hard ceiling with
_exit(). If memory use keeps climbing past a higher threshold, PHP terminates the process immediately. Shutdown functions do not run. - State reset before free. The CVE-2026-109619 fix resets request-owned global state before the request heap is released.
That last point has an operational side effect. Because a hard out-of-memory now kills the worker, code that relied on shutdown functions running after memory exhaustion will behave differently. Restore a real memory_limit after patching; leaving the -1 stopgap in place lets one request consume the whole server.
Why interim settings only narrow the attack
Each stopgap removes one trigger or one consumer, but the bug class remains.
| Setting | Removes | Leaves open |
|---|---|---|
memory_limit=-1 with a cgroup cap | The main out-of-memory trigger | Fatal errors and timeouts; cgroup kills must hit only the worker |
session.upload_progress.enabled=0 | One stale-state consumer and the PHPSESSID leak path | INI diagnostics, backtraces, other variants |
fatal_error_backtraces=0 (8.5) | The strongest stale-state case | Everything else |
Lower pm.max_requests | Shortens how long stale state lives | Does not prevent reuse |
Avoid RLIMIT_AS / RLIMIT_DATA | Prevents making things worse: they make mmap() fail and trigger the bad path | n/a |
Wiz says "other variants likely exist" and that a comprehensive fix was needed. That is why the PHP team keeps repeating that updating is the only safe choice.
What builders should take from this
Three lessons generalize beyond PHP.
- Error paths are attack surface. The happy path of PHP's request handling was fine. The bugs lived in the rarely tested paths that run when something fails. AI-assisted audits are now very good at exploring exactly those paths, as Wiz's use of Gemini 4 Argon and GPT-5.6 Sol shows.
- Process reuse turns one bad request into a cross-request bug. Any long-lived worker model, in any language, has to treat per-request state carefully. If you build servers or agents that keep state between tasks, the same discipline applies; it is a theme in explainx.ai's agent security basics coverage.
- Memory-safe languages remove a class, not all bugs. PHP code is memory safe; the PHP interpreter is C. The bug sat beneath every application that trusted it. That is the same layering problem AI security scanners such as Anthropic's OSS scanner and Cloudflare's security audit skill are aimed at.
If you teach or learn systems programming, PHP2Shell is a near-perfect case study: setjmp and longjmp, arena allocators, use-after-free, and why cleanup code is the hardest code to get right.
Related reading
-
PHP2Shell and AI agents: why the attack window just collapsed
-
CVE-2026-109618 in the AgentBeam CVE database · CVE-2026-109619
Primary sources: GHSA-w8hw-w9fx-j79w (CVE-2026-109618) · GHSA-6wmf-4rv5-p3rp (CVE-2026-109619) · PHP security advisory · Wiz Research · PHP2Shell site · Sansec
Technical details reflect the PHP advisories and researcher write-ups published October 21, 2026.
