# PHP2Shell: two memory bugs at the boundary between PHP requests

> How multipart parsing, abrupt out-of-memory exits and stale worker state can lead to PHP code execution, with confirmed scope and fixed versions.

Published: 2026-11-04  
Canonical: https://rootzero.eu/en/research/php2shell-deux-bugs-cycle-requete/

**Disclosure and fixes: 21 October 2026. Analysis written on 4 November 2026.** PHP2Shell groups two distinct runtime flaws: [CVE-2026-109618](https://github.com/php/php-src/security/advisories/GHSA-w8hw-w9fx-j79w) concerns an abrupt exit while request data is only partly initialized; [CVE-2026-109619](https://github.com/php/php-src/security/advisories/GHSA-6wmf-4rv5-p3rp) concerns global worker state that outlives the request memory it references. PHP confirms unauthenticated remote code execution in demonstrated configurations, running with the web worker's privileges. Both advisories have a 9.2 CVSS 4.0 score.

## A broken startup followed by cleanup

For CVE-2026-109618, PHP's out-of-memory handling can use `longjmp()` to leave the current call chain. A pointer to a request structure may already be visible even though initialization is incomplete. If an allocation fails at that point, later cleanup can read, free or branch on invalid state. The [PHP advisory](https://github.com/php/php-src/security/advisories/GHSA-w8hw-w9fx-j79w) confirms a full HTTP RCE against Apache `mod_php` with ordinary limits and multiple workers. The vulnerable sequence can happen during automatic request parsing, before application code starts.

The remote trigger is more subtle than a request whose wire size exceeds a simple limit. Multipart part headers can consume disproportionate PHP memory; empty file controls still generate metadata; nested field names amplify allocations; and a body without a usable declared length could pass an early `post_max_size` check. These paths can cause an out-of-memory exit while incomplete state is reachable. A crash alone is not a code-execution proof: reliability depends on allocator layout, build, extensions and the surrounding request flow.

PHP-FPM terminates its worker when startup fails, removing that specific startup-cleanup path. An out-of-memory error after successful startup can still reach susceptible code, although the advisory does not claim a complete FPM exploit for this CVE alone. FrankenPHP is considered likely susceptible by source analysis for the startup case, with less validation published.

## A pointer surviving into the next request

For CVE-2026-109619, a persistent worker retains global error, session or extension fields that may point into request-owned memory. The safety rule is to reset each field before releasing the request heap. A fatal error can skip the reset; a later cleanup stage can repopulate a field after its normal reset. Another request on the same worker may then use a dangling pointer **before** initialization replaces it. The [advisory](https://github.com/php/php-src/security/advisories/GHSA-6wmf-4rv5-p3rp) describes a *producer* request that leaves stale state and a *consumer* request that reads it.

Confirmed consumers include early configuration diagnostics and PHP session upload progress. On PHP 8.5, `fatal_error_backtraces` is a confirmed producer. A later upload-progress operation can consume state without an application-level `session_start()`. PHP validated RCE for session lifecycle cases on `mod_php` and FPM, with an appropriate application operation needed for the FPM sequence. Hiding diagnostics with `display_errors=Off` does not help: the stale pointer is accessed before PHP decides what to display.

## How to investigate and fix it

In an authorized lab, trace operations that publish request pointers to worker globals, force each error and cleanup branch, and determine whether the worker survives to serve another request. Compare old and fixed builds under memory instrumentation. A 502, process crash or leaked bytes provides a lead; a claim of RCE requires a demonstrated path from controlled input to control flow. Operational logs can reveal bursts of multipart input, OOMs, fatal errors and worker recycling, but clean logs are not proof of safety.

PHP released [8.2.35, 8.3.36, 8.4.27 and 8.5.12](https://www.php.net/archive/2026.php#2026-10-21-5). Older unsupported branches rely on vendor backports or an upgrade; verify the actual package revision and restart every worker. Temporary measures include administratively disabling `session.upload_progress.enabled`, limiting decoded multipart complexity, and carefully managing memory limits. The PHP team stresses that these measures each address particular paths and cannot replace the runtime fix. On 23 October it also recommended disabling upload progress pending a regular fix for a separate, unusual serializer configuration, CVE-2026-115560.

The common design lesson is a lifetime invariant: a reused worker must never carry a pointer into an already released request heap.

Comparing these lifetime invariants with [Ruby compilation RCE in GitLab](/en/research/gitlab-ruby-regexp-rce-2026/) helps distinguish the mechanisms behind the same “RCE” label. Our [vulnerability validation note](/en/research/proof-before-alert/) explains the difference between a reproducible observation, a confirmed vulnerability and demonstrated business impact.

**Primary sources:** [PHP OOM advisory](https://github.com/php/php-src/security/advisories/GHSA-w8hw-w9fx-j79w), [PHP stale-state advisory](https://github.com/php/php-src/security/advisories/GHSA-6wmf-4rv5-p3rp), [PHP release announcement](https://www.php.net/archive/2026.php#2026-10-21-5).
