- What PHP2Shell is
- How the two bugs work
- How it was found, and the leak
- How to patch and protect your servers
Bonus: watch it in 3D
This is part 2 of the PHP2Shell series, for readers who want the mechanism. If you want the simpler version first, read part 1. Everything below is drawn from the two PHP advisories, the PHP security team member who wrote about the fix, and the security vendors' write-ups, all listed at the end. Where something is one source's analysis rather than the advisory, it says so.
Background: three ideas you need
1. A request has a lifecycle
Every web request to PHP goes through startup (read the input, build $_GET, $_POST and friends, apply per-request settings), then your script runs, then shutdown (clean up). The key fact for these bugs: both bugs fire before your application code runs, so application-level logins and checks cannot block them. Datadog makes the same point.
2. PHP limits memory per request
The memory_limit setting (128M by default) caps how much memory one request may use. When a request goes over, the old behavior was to bail out immediately.
3. "Bailing out" used longjmp()
In C, longjmp() is a jump that leaves the current function and everything above it, straight back to a saved point. It skips the remaining lines of whatever was running. That is fine if nothing is in a half-finished state. It is dangerous if something was.
CVE-2026-109618: "RCE via OOM corruption"
The advisory states the root cause: PHP uses longjmp() to exit a request on out-of-memory. In some contexts, request memory is reachable before it is fully initialized. An early exit there leaves that memory invalid, and request shutdown may read it or branch on it. Importantly, the failed allocation does not need to relate to the corrupted memory. Any allocation that fails at the wrong moment can do it.
Datadog gives a concrete example: if allocation fails while PHP builds the $_POST array, PHP has already updated the table's counters for an entry it never finished creating. Cleanup then processes that unfinished entry as valid. Datadog also notes the hash table is one instance of a broader pattern, so defenses should not hinge on that one code path.
How an attacker reaches the out-of-memory moment
The attacker needs to make PHP run out of memory during startup, even though a size cap (post_max_size, 8 MB by default) is supposed to prevent huge requests. The advisory lists these routes:
| Route | Why it works |
|---|---|
| Multipart body with no usable declared length | It can pass the first post_max_size check. |
| Parts with unlimited headers | Headers use far more memory inside PHP than they take on the wire. |
| Empty file fields | They do not count toward max_file_uploads but still create file metadata arrays. Nested field names multiply the work. |
| App-specific inputs | Anything that causes a large allocation can reach the same condition. |
Why Apache mod_php is the confirmed case
The advisory compares the ways PHP is run (called SAPIs):
| How PHP runs | Status in the advisory |
|---|---|
| Apache mod_php | Runs shutdown even when startup fails. Body parsing can hit the memory limit, and shutdown then processes invalid state before any application code runs. Confirmed exploitable over HTTP with default input and memory limits and a normal prefork setup. |
| PHP-FPM | Exits the child process when startup fails, which limits the startup case. An out-of-memory after startup may still hit the flaw. Source analysis suggests it is susceptible, but no complete exploit was demonstrated for this bug. |
| FrankenPHP | Likely susceptible per source analysis, similar to mod_php. Not validated. |
| LSAPI | Modeled on FPM. Application-specific exploits are likely but unvalidated. |
CVE-2026-109619: "RCE via stale global state"
PHP keeps some state at the worker level (error, session, object and extension state) that is supposed to be reset for each request. This bug is about state that outlives the request that created it.
The advisory gives the model: a producer leaves request-owned state published, and a later consumer uses it before initialization resets it. The two must happen in different requests handled by the same worker. Ways state gets left behind include fatal errors of any kind (parse or compile errors, uncaught errors, a failed require, execution timeouts and out-of-memory), and diagnostics raised during cleanup.
The cases the advisory confirms
- Startup diagnostics: a warning or deprecation raised while applying certain settings can consume stale error state. Suppressing errors does not prevent it. The advisory lists settings such as
auto_detect_line_endings,filter.default,assert.*, session options,memory_limitanddate.timezone. - PHP 8.5 fatal error backtraces (confirmed): with
fatal_error_backtraceson, backtrace state from a fatal error is never reset. The advisory calls this the strongest demonstrated case and the basis of the confirmed mod_php exploit. - Session upload progress (confirmed): an out-of-memory during multipart processing can leave session state published, and a later upload-progress operation then uses it. Validated on mod_php and PHP-FPM. This feature is on by default, and Datadog notes it runs even if the app never calls
session_start().
On mod_php, per-directory settings (.htaccess, php_value) are applied before request startup, so they can act as early consumers. On PHP-FPM, exposure is conditional, for example when a frontend passes PHP_VALUE parameters that trigger diagnostics.
How hard is it to exploit?
The scoring vector for both bugs includes AC:H (high attack complexity) and AT:P (attack requirements present). Reasons gathered from the sources:
- Memory layout. Attackers need valid memory addresses, which address randomization (ASLR) hides. Datadog says a memory leak can reduce that difficulty, and failed guesses can crash workers.
- Precise timing. The failure has to land on the right allocation, which depends on the build and earlier allocations.
- Same worker. For 109619, both requests must reach the same worker.
- Build dependence. One analyst reports that the public chain he reviewed was tied to a calibrated PHP 8.4.26 Apache image and offsets taken from the target (Daniele Berardinelli's analysis).
But "hard" is not "safe". Wiz demonstrated pre-authentication code execution against WordPress, Drupal and Joomla deployments, as reported by The Hacker News, and Calif has chained both bugs into a login-free attack per Sansec. Attackers do not need to be universal. They need to be reliable on the most common builds. Treat the complexity rating as a delay, not a defense.
How the fix works
The advisory says the fix removes longjmp() from out-of-memory handling and adds two thresholds. Alexandre Daubois, PHP security team explains the details:
- Soft limit. Going over
memory_limitnow records a pending error instead of jumping out. The C code finishes its structures, PHP throws an unwind at the next safe point, and the multipart parser checks for the pending error between parts and fields. Then the normal fatal error is raised. - Hard limit. A new setting,
memory_hard_limit_percent, defaults to 200 percent ofmemory_limitbelow 1G (256M with the stock 128M). Going past it ends the process immediately. - Size. The author says the patch touches 390 files.
Two caveats. A patched PHP still kills a worker that runs out of memory, but without corruption. And Datadog notes the fix does not change the underlying "counter updated before the allocation" pattern, so similar bugs may appear later, including in older branches.
One more observation from the same author: other runtimes handle this differently. Go's standard runtime treats out-of-memory as unrecoverable, and Rust binaries abort by default. C++ code remains exposed to the same shape of bug, because any new can throw into code that did not expect it.
What this means in practice
If you remember one thing: there is no configuration that fully replaces the update. The PHP advisory for 109618 calls the update the only complete fix, and the advisory for 109619 states there is no complete generic configuration mitigation. Part 4 lists the partial measures, separated by which CVE they cover, because several widely shared tips cover only one of the two.
Sources: PHP advisory for CVE-2026-109618, PHP advisory for CVE-2026-109619, Alexandre Daubois, PHP security team, Datadog Security Labs, Daniele Berardinelli's analysis, Sansec, The Hacker News. Read 23 October 2026