memory_limit < 4 × post_max_sizeThe two CVEs differ in scope and timing in ways that matter operationally. CVE-2026-109618 is self-contained: A single request corrupts its own data, and the consequence executes during that same request’s cleanup, which makes it fast, atomic, and difficult to reconstruct after the fact. CVE-2026-109619 spans requests, requiring a poison step and a trigger step against the same worker, but it reaches a broader set of internal state and can be triggered through timeouts and client disconnects as well as memory exhaustion. Persistent-worker deployments are the material exposure for this second class.
Both vulnerabilities are present in every PHP release from the 7.x and 8.x series. The specific HashTable ordering behind CVE-2026-109618 was introduced by a performance optimization in October 2017 and first shipped in PHP 7.3.0 (December 2018). The serialization and session paths behind CVE-2026-109619 date back further, to PHP 7.1 and PHP 5.4, respectively. In other words, the exploitable window has existed in released PHP for more than seven years. No supported branch is unaffected.
During teardown, php_request_shutdown() destroys the superglobals, and zend_array_destroy() iterates nNumUsed buckets. The stale entry is passed to i_zval_ptr_dtor(), which treats the leftover bytes as a reference-counted value: It dereferences the pointer they contain, decrements the reference count at that address, and (if it reaches zero) dispatches through a function-pointer table indexed by a type tag that also came from the leftover bytes.

  • CVE-2026-109618 (RCE via out-of-memory) covers contexts that leave request memory uninitialized. A data structure is marked as containing an entry before the memory backing that entry is allocated; when the allocation is the one that crosses the memory limit, the entry is left holding stale heap data. The failing request’s own cleanup routine then reads and branches on those bytes.
  • CVE-2026-109619 (RCE via fatal errors) covers contexts that leave global state invalid. A module-level global still points into the per-request heap that shutdown has already freed, and still reports its operation as active. A subsequent request handled by the same worker process reads or branches on that freed memory.

In our testing, we developed an exploit code that operated consistently across x86-64 and ARM architectures, remained reliable across worker restarts, and on ARM-based hosts, which are increasingly common in cloud fleets, defeated Address Space Layout Randomization (ASLR) using only HTTP requests.
CrowdStrike employs a layered approach to detecting exploitation and post-exploitation activity, using machine learning and behavior-based detections, including indicators of attack (IOAs). The Falcon platform provides out-of-the-box visibility and detection coverage for post-exploitation activities of the PHP vulnerabilities. Existing IOAs detect and prevent common post-exploitation activity, including web shell deployment, suspicious child processes spawned by PHP or web server processes, and malicious command execution.
The canonical instance is _zend_hash_str_add_or_update_i() in Zend/zend_hash.c, the helper behind every string-keyed insert into a PHP array. On the insert path, it increments both element counters, takes a pointer to the new bucket, and only then allocates a copy of the key:

Technical Details

The Shared Root Cause: longjmp() Without Unwinding

The two issues share a single root cause. When PHP encounters a fatal condition (exhausting its configured memory_limit, exceeding the maximum execution time, or losing the client connection), it does not return an error up the call stack. It raises a fatal error and uses longjmp() to abandon the current execution context outright, aborting the request. Any C function caught midway through building a data structure is simply discarded: no destructors run, no bookkeeping is rolled back, and nothing records that the structure is incomplete. PHP then proceeds into an otherwise ordinary cleanup sequence that assumes every structure is in a consistent state.
What elevates these vulnerabilities is their placement in the execution stack. PHP parses incoming request data before extensions are initialized and before a single line of application code is compiled, so exploitation is independent of what the server hosts. A meticulously maintained application is exposed to precisely the same degree as a neglected one. The attack is also remarkably quiet: The worker process does not crash, the server returns a standard HTTP 200 response, and application logs record nothing unusual.
Triggering the condition requires nothing more than a URL-encoded POST body, because PHP populates $_POST inside php_hash_environment(), which php_request_startup() calls before zend_activate_modules() and before any script is compiled. A field with a nested name such as ZZZZ…[x]=1 causes php_register_variable_ex() to insert the outer name as a new array key, so the attacker directly controls the length of the key that must be allocated. Sizing that name to cross memory_limit places the bailout precisely on the key allocation.

CVE-2026-109618: Uninitialized Request Memory, Consumed During the Same Request

The two CVEs describe where that assumption breaks:
Control over the leftover bytes comes from the allocator’s determinism. Zend MM serves small allocations from per-size free lists in strict last-in-first-out order and never zeroes reclaimed blocks. An earlier field in the same request, one whose value PHP copies, decodes, and immediately releases, can therefore be sized so that its freed contents occupy exactly the block the corrupted array subsequently claims. The stale entry becomes a fully attacker-defined value, which we used to synthesize a counterfeit array whose destructor pointer resolves to libc’s system(), with the command string resident in the same block.
Successful exploitation of either issue yields command execution in the web server user context, the standard foothold for deploying web shells, harvesting database and API credentials from configuration files, and pivoting into internal networks. Patching is the only complete remediation. Where immediate patching is not feasible, endpoint behavioral monitoring is the primary line of defense, because the exploitation is invisible at the HTTP layer while its outcome is not.
Two pieces of core Zend machinery are central to the issues described here: the HashTable, which backs every PHP array including the $_POST superglobal, and the Zend Memory Manager (Zend MM), PHP’s own per-request allocator that sits between PHP and the operating system. PHP’s fatal-error path terminates in zend_bailout(), which performs a LONGJMP out of the current C call stack:
CrowdStrike continues to research exploitation techniques associated with this vulnerability.
When a fatal error interrupts one of these operations, the matching teardown is skipped. The global retains its pointer, and the nesting counter still asserts that the operation is active. Request shutdown then frees the entire per-request heap, but the global survives because it belongs to the process rather than the request. PHP is left holding a module-level pointer into memory it has already released.

// Zend/zend.c
ZEND_API ZEND_COLD ZEND_NORETURN void _zend_bailout(const char *filename, uint32_t lineno)
{
...
CG(unclean_shutdown) = 1;
...
LONGJMP(*EG(bailout), FAILURE);
}

The severity of both issues stems from their position early in request handling. Because the corruption occurs while PHP is still assembling internal request variables, before extension initialization and before any application code runs, every reachable PHP endpoint on an affected server constitutes an active attack surface, including static forms, login routes, and health checks. There is no authentication to bypass, no session to steal, and no vulnerable endpoint to discover. The exposure is independent of the CMS, framework, or plug-ins in use.
The second CVE follows the same mechanism but the damage lands in process-scoped state, which shifts the exploitation window to a later request. Here, the interrupted operation is one that caches state in a global variable and depends on reaching its own cleanup call to clear it. PHP caches the active serializer and unserializer contexts in module globals and reuses the cached pointer whenever the nesting level is non-zero. Only the matching destroy call decrements that level and clears the pointer, and only an extension’s request-initialization routine resets it. Session handling behaves comparably, holding the session identifier, handler state, session variables, and upload-progress state in globals cleared by dedicated shutdown routines.

PHP Fatal error: Allowed memory size of 16777216 bytes exhausted (tried to allocate 3670048 bytes) in Unknown on line 0

In multi-process web servers such as Apache prefork, every worker is derived from a single parent process, so the memory layout an attacker depends on remains consistent across worker restarts. That stability allows an attacker to refine payloads at no cost; a failed attempt is absorbed by a worker respawn that monitoring treats as routine.

Vulnerability Impact

The attack also leaves virtually no digital footprint. Worker processes remain stable, responses return HTTP 200, and application logs record no anomalies, because no application code ever ran. The only server-side trace is a single memory-limit fatal in the web server’s error log, which is routinely dismissed as noise, leaving defenders without actionable web telemetry to identify compromised endpoints. 
PHP’s shipped defaults (post_max_size = 8M, memory_limit = 128M) do not meet this condition, so a stock installation is not reachable through the URL-encoded path we demonstrated. However, many production deployments are tuned to accept large uploads, and common pairings such as post_max_size = 64M with memory_limit = 128M, or post_max_size = 100M with memory_limit = 256M, satisfy it. Note that post_max_size can be set per directory in .htaccess or a virtual host, so on shared hosting, a single site can place itself into the vulnerable configuration without any server-wide change. CrowdStrike reproduced the issue with memory_limit lowered to 16M and post_max_size raised to 64M purely to keep the test payload small; the same bug is reached with larger payloads against the larger limits above.
Tracked as CVE-2026-109618 and CVE-2026-109619, both vulnerabilities carry a CVSS 4.0 score of 9.2 (Critical) and are classified as CWE-824: Access of Uninitialized Pointer. Both allow an unauthenticated remote attacker to execute arbitrary system commands on an affected web server over HTTP.
The only server-side artifact of a successful attempt is a generic fatal error in the web server log:
Beyond memory exhaustion, this condition can be reached through execution timeouts and aborted client connections, each of which routes through the same zend_bailout() path. Exploitation is consequently a two-stage operation: One request poisons the worker, a second triggers the use of the freed memory.

Am I Impacted?

// Zend/zend_hash.c
add_to_hash:
idx = ht->nNumUsed++;
ht->nNumOfElements++;
p = ht->arData + idx;
p->key = key = zend_string_init(str, len, GC_FLAGS(ht) & IS_ARRAY_PERSISTENT);

Version alone does not determine exposure. The out-of-memory trigger must be reachable while PHP is parsing request data, which requires that a request body be allowed to grow large enough to exhaust the memory limit before any script runs. In practice, this means the configuration satisfies roughly:
In both cases, the attacker controls the trigger and, critically, the data that PHP ultimately misinterprets. CrowdStrike has verified end-to-end that a single anonymous POST request can cause an Apache mod_php worker to launch a shell process and execute an attacker-specified command, with no authentication, no session, and no vulnerable application code involved.
Both vulnerabilities live in Zend, the engine at the heart of PHP. Zend is the C implementation that compiles PHP source into bytecode, executes it in a virtual machine, and manages everything the language needs at runtime: variables, arrays, objects, reference counting, and memory allocation. Originally written by Zeev Suraski and Andi Gutmans (the name is a contraction of their first names) and introduced with PHP 4, it is not an optional component or a third-party add-on; it ships inside every PHP build, which is why its source lives in the Zend/ directory of php-src. In practice, this means a defect in Zend is a defect in the PHP engine itself, affecting every application, framework, and extension running on the interpreter, regardless of how that code is written.
Two critical memory-corruption remote code execution (RCE) vulnerabilities, collectively dubbed PHP2Shell, have been disclosed in core PHP, the underlying engine powering a vast portion of the web. The CrowdStrike Falcon® platform provides out-of-the-box visibility and detection coverage for post-exploitation activities of both vulnerabilities.

How CrowdStrike Falcon Protects Customers

Two factors make this reachable and controllable:
To see which version of PHP and configuration is running on your server, either run php --version. In order to see the values set on your server for options listed above, run the following command:
php -i | grep -E "^(memory_limit|post_max_size|upload_max_filesize|Loaded Configuration File)”.

Similar Posts