CVE-2026-109618 & CVE-2026-109619 // CORE ENGINE PRE-APP EXPOSURE // The Pre-App SAPI Boundary

The Pre-App SAPI Boundary

Why does the vulnerability strike before index.php even runs?

Philosophical Interrogative

"Why can a server running PHP 5.4–8.5 be compromised before a single line of developer code executes?"

Web security culture has spent thirty years teaching developers to validate input, sanitize parameters, and avoid dangerous functions like eval() or system(). PHP2Shell made every single application-level defense irrelevant overnight.

In October 2026, security researchers at Wiz Research, Calif, and AI-driven static analysis models (Gemini 4 Argon and GPT-5.6 Sol) identified two critical flaws in the C-core of PHP itself. Before the web server hands an HTTP request to application code (e.g. index.php), the Server Application Programming Interface (SAPI) must initialize the interpreter context (PHP_RINIT) and parse incoming request streams. If an unauthenticated attacker triggers memory exhaustion during this internal C-level parsing phase, the Zend Engine aborts execution via a non-local longjmp() jump, corrupting internal structures before the application even wakes up. The global panic prompted emergency shutdowns of governmental identity portals (Dutch DigiD and MijnOverheid), emergency NCSC-NL advisories, and an urgent race to patch ~70% of the public web.

SAPI Request Lifecycle & Pre-App Execution Barrier
1. SAPI Ingress HTTP Connection & Headers
2. PHP_RINIT C-Engine Initialization
3. Multipart Parse Vulnerable Memory Window
4. index.php Never Reached!
ARCHITECTURAL CAUSAL CONCLUSION: ∴ Therefore, the security boundary collapses inside the C runtime of the SAPI layer before userland PHP code (index.php) ever initializes routing, sessions, or authorization logic.
Mitigated at root by: Layer 1 — Official Engine Patching (PHP 8.5.12, 8.4.27, 8.3.36, 8.2.35) Inspect Defense Layer →
CVE-2026-109618 // SAPI MULTIPART OOM HAZARD // The Multipart OOM Trap & SAPI Lifecycle

The Multipart OOM Trap & SAPI Lifecycle

Why does running out of memory before script execution blow open the engine?

Philosophical Interrogative

"Why does running out of memory during HTTP multipart parsing not abort the request safely?"

In standard Unix programming, when an allocation fails, malloc() returns NULL, or an out-of-memory handler terminates the thread cleanly. The Zend Engine implemented an optimization that turned out to be fatal.

When an HTTP client sends an enc-type of multipart/form-data, PHP's SAPI layer parses headers, boundaries, and field names in native C. Attackers discovered that by sending thousands of empty file parts (bypassing max_file_uploads because empty files are not counted as uploaded files), deeply nested field array names, or repeated headers per boundary part, the SAPI parser consumes memory up to the configured memory_limit. Crucially, this happens without exceeding post_max_size! In environments like Apache mod_php and FrankenPHP, where request parsing runs directly in the persistent web server process, hitting an Out-Of-Memory (OOM) error at this precise moment triggers zend_bailout(), which bypasses graceful shutdown and throws the engine into an unrecoverable half-initialized state.

Multipart Parsing Memory Exhaustion Sequence
Unbounded Multipart Parts Empty Files & Nested Keys
→
memory_limit Ceiling post_max_size Not Exceeded
→
zend_bailout() Non-Local longjmp() Trigger
ARCHITECTURAL CAUSAL CONCLUSION: ∴ Therefore, the SAPI parser processes the multipart/form-data stream before script execution, causing memory_limit exhaustion to occur during the volatile C-level header parsing window.
Mitigated at root by: Layer 2 & 3 — Edge WAF Inspection & Aggressive memory_limit Isolation Inspect Defense Layer →
CVE-2026-109618 // UNWINDING BYPASS & HALF-BAKED STRUCTS // longjmp() & Partially Initialized Memory

longjmp() & Partially Initialized Memory

Why does C-level error recovery via longjmp() destroy memory integrity?

Philosophical Interrogative

"Why does C-level error recovery via longjmp() corrupt the interpreter shutdown sequence?"

The C standard library function longjmp() restores the environment saved by the most recent setjmp() invocation. It is non-local: it cuts across function boundaries without unwinding call frames.

When Zend's memory manager (zend_mm) exhausts its allocation ceiling during pre-app parsing in rfc1867_post_handler(), it calls zend_bailout(). Instead of returning an error code up the call stack, zend_bailout() executes longjmp() directly into the outer zend_try checkpoint, which subsequently enters the request shutdown routine (PHP_RSHUTDOWN). Because the jump happens mid-execution without invoking intermediate stack frame destructors, complex internal C structures (such as hash tables, file descriptor tracking lists, and object descriptors) are left in a partially initialized ("half-baked") state. Crucially, PHP_RSHUTDOWN assumes that every structure registered during request initialization is valid and unconditionally dereferences their pointers to free them! By carefully arranging the heap before the OOM bailout occurs, an attacker causes PHP_RSHUTDOWN to treat arbitrary attacker-controlled memory as valid pointers, turning a memory exhaustion error into type confusion and arbitrary code execution.

Non-Local longjmp() Arc Bypassing C Stack Unwinding
sapi_read_post_data() → rfc1867_post_handler() Active Call Frame
zend_bailout() → longjmp(EG(bailout), 1) Non-Local Leap [BYPASSES FRAME DESTRUCTORS → zend_try]
PHP_RSHUTDOWN → zend_hash_destroy(half_baked_ptr) Wild Pointer Dereference → RIP Control
ARCHITECTURAL CAUSAL CONCLUSION: ∴ Therefore, the non-local longjmp() jump bypasses C stack frame unwinding and abandons data structures in a partially initialized state, which the PHP_RSHUTDOWN routine erroneously dereferences as fully valid.
Mitigated at root by: Layer 1 — Graceful VM-Point OOM Handling in Patched Runtimes Inspect Defense Layer →
CVE-2026-109619 // WORKER GHOSTS & CROSS-REQUEST UAF // Stale Global State & Cross-Request Use-After-Free

Stale Global State & Cross-Request Use-After-Free

Why does a worker process remember the ghosts of previous requests?

Philosophical Interrogative

"Why does a fatal error in Request A poison Request B on the same persistent worker process?"

Modern high-performance web architectures avoid process creation overhead by using persistent worker pools (PHP-FPM, mod_php, FrankenPHP). Each worker process serves hundreds or thousands of sequential HTTP requests.

CVE-2026-109619 exposed a catastrophic flaw in how PHP handles global worker state across sequential requests. When Request #1 terminates abnormally (e.g. via an OOM bailout or fatal error), the Zend memory manager frees all request-scoped heap memory. However, several internal engine components—most notoriously session.upload_progress (which is enabled by default in standard PHP configurations even if the application never uses sessions!) and fatal_error_backtraces in PHP 8.5—store pointers in global worker variables. Because the abnormal bailout aborted the code before it could zero out these global pointers, they remain pointing to memory addresses that were just freed (classic dangling pointers). When Request #2 arrives from a completely different user and lands on that same worker, the engine references this stale global pointer prior to standard initialization, triggering an unauthenticated Use-After-Free (UAF).

Cross-Request Persistent Worker Poisoning Timeline
Request #1 (Aborted)

OOM bailout frees request heap. Global upload_progress variable retains dangling pointer 0x7f8a420a8100...

Request #2 (Attacker Ingress)

Worker dereferences stale pointer before re-init → Use-After-Free → Arbitrary Memory Control

ARCHITECTURAL CAUSAL CONCLUSION: ∴ Therefore, in a persistent multi-request worker model, global pointers retain freed heap addresses from the aborted first request, becoming dangling pointers that trigger a Use-After-Free when the next request arrives.
Mitigated at root by: Layer 2 — Disabling session.upload_progress & Strict Global Pointer Zeroing Inspect Defense Layer →
DEFENSE ARCHITECTURE // CVE-2026-109618 & 109619 REMEDIATION // Engine Patching & Boundary Isolation

Engine Patching, Graceful OOM & Boundary Isolation

Why runtime patching and state isolation are the only structural guarantees

Philosophical Interrogative

"Why do conventional application-level defenses fail while runtime C-level patching is mandatory?"

Because CVE-2026-109618 and CVE-2026-109619 exist inside the C binary of the interpreter itself, no amount of PHP code refactoring, input sanitization, or framework middleware can protect an unpatched server.

True immunity requires structural guarantees at the runtime engine, web server SAPI, and operating system layers. The PHP development team solved CVE-2026-109618 by replacing the dangerous non-local longjmp() bailout with graceful OOM handling at safe VM checkpoints, coupled with an unconditional _exit() termination if memory exceeds a hard panic threshold. For CVE-2026-109619, the engine now strictly zeroes and invalidates all global worker pointers during shutdown. Organizations unable to update binaries immediately must apply emergency temporary mitigations: disabling session.upload_progress.enabled in php.ini, disabling fatal_error_backtraces, tuning memory_limit comfortably above post_max_size, migrating from mod_php to isolated PHP-FPM pools with aggressive worker recycling (pm.max_requests = 500), and filtering anomalous multipart headers at the Cloudflare / WAF edge.

Layer 01 // Official Engine Patch

Upgrade to PHP 8.5.12, 8.4.27, 8.3.36, or 8.2.35. Replaces longjmp() bails with graceful VM checkpoints and zeroes global pointers.

Layer 02 // SAPI & php.ini Hardening

Disable session.upload_progress.enabled and fatal_error_backtraces. Migrate mod_php to PHP-FPM (pm.max_requests = 500).

Layer 03 // Edge WAF Multipart Inspection

Filter anomalous multipart payloads (>10 headers per part, array depth > 5) at Cloudflare / Nginx before reaching SAPI parser.

Layer 04 // OS & Container Confinement

Read-only rootfs, Seccomp-BPF profiles blocking SYS_execve and SYS_fork, and zero ambient capabilities.

ARCHITECTURAL CAUSAL CONCLUSION: ∴ Therefore, complete threat neutralization requires eliminating non-local longjmp() jumps in the Zend C-runtime, strictly zeroing global worker pointers, and enforcing multi-layered process containment.
Active Protection: 4 Concentric Defense Shields Online
Architectural Principles

Frequently Asked Architectural Questions (FAQ)

What is PHP2Shell (CVE-2026-109618 & CVE-2026-109619)? ↓

PHP2Shell is the collective name for two critical remote code execution vulnerabilities (CVSS 9.2) in the core C engine of PHP (versions 5.4 through 8.5) disclosed in October 2026. CVE-2026-109618 involves memory corruption triggered by non-local longjmp() jumps during pre-app multipart Out-Of-Memory (OOM) bailouts. CVE-2026-109619 involves a cross-request Use-After-Free (UAF) caused by stale global worker pointers in features like session.upload_progress.

Why are Apache mod_php and FrankenPHP more exposed than PHP-FPM? ↓

In Apache mod_php and FrankenPHP, HTTP request parsing occurs directly within the long-running web server process thread. If an OOM bailout occurs during pre-app parsing, mod_php unconditionally runs the shutdown sequence within that same context. PHP-FPM, while still vulnerable across sequential requests via stale globals, separates worker pools and can recycle failed child workers more aggressively.

Why did the Dutch government and Wikimedia take down systems during the disclosure? ↓

The disclosure of PHP2Shell in October 2026 leaked ahead of the coordinated release schedule, revealing active proof-of-concept exploits. Because PHP powers ~70% of the public web and the vulnerabilities require no authentication or application-level interaction, critical government portals (such as the Dutch DigiD and MijnOverheid identity platforms) and major organizations (such as the Wikimedia Foundation, which isolated Phabricator and related infrastructure) took services offline to protect sensitive infrastructure until emergency patches could be deployed.

Why does longjmp() cause memory corruption during an OOM error? ↓

In C, longjmp() restores the CPU execution context saved by setjmp(), instantly jumping across stack frames without executing intermediate cleanup routines or destructors. When an allocation fails during multipart parsing, zend_bailout() uses longjmp() to leap straight to the outer zend_try checkpoint, which then invokes PHP_RSHUTDOWN. Because the parsing was interrupted mid-flight, internal Zend structures are left partially initialized. PHP_RSHUTDOWN erroneously treats these uninitialized pointers as valid and attempts to free or traverse them, corrupting the heap.

How does session.upload_progress cause a Use-After-Free even if the app never uses sessions? ↓

session.upload_progress is enabled by default in standard PHP installations. The SAPI parser monitors incoming multipart streams for a progress key before any PHP script runs, storing tracking data in a global worker variable. If a request aborts, the request memory pool is freed, but the global variable continues pointing to that freed memory. When the same worker handles a subsequent request, it dereferences that stale pointer, creating a classic Use-After-Free vulnerability.