* [CVE-2026-109618]( ([GHSA-w8hw-w9fx-j79w]( RCE via Out-of-Memory Corruption):**
PHP historically relies on non-local jumps via `longjmp()` (wrapped by `zend_bailout()`) to immediately exit the current execution context and abort request processing whenever an Out-Of-Memory (OOM) condition occurs in `Zend/zend_alloc.c`. In specific internal engine contexts most notably during native `multipart/form-data` request parsing in `main/rfc1867.c` and session upload-progress updates in `ext/session/session.c` prior to script execution—internal request memory becomes reachable by the request shutdown routine (`RSHUTDOWN`) before all of its fields and pointers are fully initialized. If any memory allocation fails during that narrow window (even if the failing allocation is unrelated to the partially constructed structure), `longjmp()` aborts the initialization mid-flight and leaves the reachable request memory in an invalid state. When `php_request_shutdown()` subsequently executes, it reads or branches on this uninitialized or corrupted memory as if it were valid, enabling unauthenticated remote code execution (RCE) as the web worker account as well as Heartbleed-style worker memory disclosure via `PHPSESSID` response cookies ([GHSA-w8hw-w9fx-j79w]( [Sansec](
* [CVE-2026-109619]( ([GHSA-6wmf-4rv5-p3rp]( RCE via Stale Global State):**
A long-lived PHP worker process maintains global state across consecutive requests including error, backtrace, session, object, and extension globals (`EG`, `PS`, etc.) some of which temporarily hold pointers into single-request heap memory allocated via `emalloc()`. Normally, these global pointers are reset before the request heap is torn down. However, when a fatal error or OOM interrupts an operation via `longjmp()` before its local cleanup logic runs, `shutdown_memory_manager()` frees the request's backing memory while the global pointer survives inside the worker process as a dangling (stale) pointer. When the same worker handles a subsequent request, early pre-script operations—specifically confirmed in PHP 8.5's `fatal_error_backtraces` mechanism and in the default-enabled `session.upload_progress` handler across all PHP versions—access and dereference that stale pointer before normal request initialization resets it, creating a cross-request Use-After-Free (UAF) and type confusion primitive that leads to unauthenticated remote code execution ([DEV Community]( [Unit 42](
### 1.2 Affected Scope and Actual Risk by SAPI
| PHP Branch | Affected Versions | Official Patched Version | Support & Remediation Status |
| **PHP 8.5** | ` MSHUTDOWN) |
| |
| [HTTP Request N Arrives] |
| | |
| v |
| +-----------------------------------------------------------------------------+ |
| | 1. php_request_startup() [RINIT Phase - BEFORE ANY PHP SCRIPT RUNS] | |
| | |-- Activate ZendMM per-request heap | |
| | |-- SAPI parses headers & invokes rfc1867_post_handler (main/rfc1867.c) | |
| | | |-- Fires MULTIPART_EVENT_* hooks | |
| | | +-- Calls php_session_rfc1867_callback() (ext/session/session.c) | |
| | | |-- Opens/decodes session BEFORE user code | |
| | | +-- Allocates HashTables, caches zval* pointers | |
| +-----------------------------------------------------------------------------+ |
| | | |
| | (Normal Completion) | (OOM / Fatal Error!) |
| v | zend_bailout() / longjmp |
| +---------------------------------------------+ | skips remaining C lines! |
| | 2. zend_execute_scripts() [User PHP Code] | | |
| | |-- index.php / WordPress / Laravel | | |
| +---------------------------------------------+ | |
| | | |
| +----------------------+----------------------+ |
| | |
| v |
| +-----------------------------------------------------------------------------+ |
| | 3. php_request_shutdown() [RSHUTDOWN Phase] | |
| | |-- Reads/branches on request state & destroys active zvals/sessions | |
| | | (CVE-2026-109618 triggers here if state was left half-initialized!) | |
| | +-- shutdown_memory_manager() frees request heap | |
| | (CVE-2026-109619: stale global pointers survive into Request N+1!) | |
| +-----------------------------------------------------------------------------+ |
+-----------------------------------------------------------------------------------+
### 2.1 The Role of `zend_bailout()` and `longjmp()`
* Because PHP's core engine is written in C—a language without native exception handling or automatic stack-unwinding destructors (RAII)—the Zend Engine historically implemented fatal error recovery using C's non-local jump primitives `setjmp()` and `longjmp()`, encapsulated in the macros `zend_try`, `zend_catch`, `zend_end_try()`, and `zend_bailout()` ([OX Security](
/* Historical definition in Zend/zend.h and Zend/zend_API.h */
#define zend_try \
{ \
JMP_BUF *__orig_bailout = EG(bailout); \
JMP_BUF __bailout; \
EG(bailout) = &__bailout; \
#define zend_catch \
} else { \
#define zend_end_try() \
} \
EG(bailout) = __orig_bailout; \
ZEND_API ZEND_COLD ZEND_NORETURN void _zend_bailout(const char *filename, uint32_t lineno)
zend_output_debug_string(1, "%s(%d) : Bailed out without a bailout address!", filename, lineno);
**Wdesign was originally considered acceptable:** During ordinary userland PHP script execution, aborting via `longjmp()` without freeing local C stack variables appeared harmless because all script allocations live inside the per-request `ZendMM` arena, which `shutdown_memory_manager()` reclaims in bulk at the end of the request ([OX Security](
**became a critical vulnerability:** Because `zend_bailout()` never returns to its caller, any C function in the PHP engine or an extension that calls `emalloc()`, `erealloc()`, `estrdup()`, or `zend_hash_update()` can be terminated mid-execution. Any cleanup, pointer refresh, structure initialization, or global state reset scheduled on the lines *after* that allocation call is completely skipped ([OX Security](
### 2.2 How `memory_limit` and `zend_mm_safe_error()` Work
* Every per-request allocation (`emalloc`) passes through `Zend/zend_alloc.c`, which tracks total request memory consumption in `heap->size` against the configured ceiling `heap->limit` (`memory_limit`, defaulting to `128M`). Prior to commit `01a40e89fb`, whenever an allocation caused `heap->size` to exceed `heap->limit` (or when an underlying `mmap()` call failed), the allocator invoked `zend_mm_safe_error()`, freed the 8 KB emergency reserve buffer (`heap->reserve`), and called `zend_error_noreturn(E_ERROR, "Allowed memory size of %zu bytes exhausted...", ...)`—which unconditionally executed `zend_bailout()` (`longjmp()`) regardless of whether the engine was in the middle of updating internal pointers ([OX Security](
### 2.3 How `session.upload_progress` and `main/rfc1867.c` Process Multipart Requests
* Introduced in PHP 5.4.0, session upload progress tracking is enabled by default (`session.upload_progress.enabled = On`) ([0x4148]( When a `multipart/form-data` POST request includes a `PHPSESSID` identifier and a form field named `PHP_SESSION_UPLOAD_PROGRESS`, `main/rfc1867.c` triggers `php_session_rfc1867_callback()` across five lifecycle events (`MULTIPART_EVENT_START`, `MULTIPART_EVENT_FORMDATA`, `MULTIPART_EVENT_FILE_START`, `MULTIPART_EVENT_FILE_DATA`, and `MULTIPART_EVENT_END`) ([0x4148]( On each update tick, the handler opens the session, decodes (unserializes) its contents, updates progress arrays using cached raw `zval *` pointers, re-encodes the entire session, and writes it back—performing numerous attacker-sized heap allocations before the target PHP script is even compiled ([OX Security](
### 3.1 Deep Dive: [CVE-2026-109618]( ([GHSA-w8hw-w9fx-j79w](
*From [GHSA-w8hw-w9fx-j79w]( and [NIST NVD](
> "PHP uses longjmp() for out-of-memory (OOM) errors to exit the current context and abort handling of the request. In some contexts, request memory is temporarily reachable before it is fully initialized. If any allocation fails at that point, PHP does not properly account for the early exit and leaves the request memory in an invalid state. The failed allocation does not need to be related to the invalid memory. The request shutdown procedure may then read or branch on it. Multiple OOM strategies can consistently trigger this condition and lead to remote code execution (RCE)."
#### B. Root Cause & Annotated Pre-Patch Source Code
* During request startup (`main/rfc1867.c` and `ext/session/session.c`), internal structures are linked into request-global state or marked active before their internal fields and sub-allocations are complete. When a subsequent allocation hits `memory_limit` and calls `longjmp()`, the partially initialized structure remains linked and is processed by `php_request_shutdown()` ([OX Security]( [0x4148](
/* [PRE-PATCH] ext/session/session.c — php_session_rfc1867_update() */
static void php_session_rfc1867_update(php_session_rfc1867_progress *progress, int force_update)
if (Z_LVAL_P(progress->post_bytes_processed) next_update) {
/* [DEFECT 1]: php_session_initialize() reads and unserializes session data,
performing multiple emalloc() calls. If OOM triggers longjmp() mid-decode,
session globals and progress structures are left in an inconsistent state. */
/* [DEFECT 2]: Unconditionally forcing session_status to active regardless of
whether initialization succeeded or failed. */
/* [DEFECT 3]: zend_hash_update() may reallocate HashTable arData via emalloc().
If OOM triggers longjmp() here, php_session_flush(1) is skipped while
PS(session_status) remains php_session_active for RSHUTDOWN to process! */
zend_hash_update(Z_ARRVAL_P(PS(http_session_vars)), progress->key.s, &progress->data);
#### C. Step-by-Step Sequence of Events & Memory Flow Diagram
1. **Request Arrival**: An unauthenticated client sends a `multipart/form-data` POST request to any `.php` URL on the server.
2. **Pre-Script Parsing (`RINIT`)**: `php_request_startup()` invokes `rfc1867_post_handler()` in `main/rfc1867.c`.
3. **Memory Exhaustion Setup**: Using multipart header amplification, empty file controls, or nested field names, the request drives `ZendMM` heap usage right to the edge of `memory_limit` ([GHSA-w8hw-w9fx-j79w](
4. **Partial Initialization Window**: The engine links a request-owned structure (or marks the session/variable table active) before completing the initialization of its internal members.
5. **Non-Local Jump (`longjmp`)**: The very allocation crosses `memory_limit`, invoking `zend_mm_safe_error()` -> `zend_bailout()` -> `longjmp()`, skipping the remaining initialization lines.
6. **Unconditional `RSHUTDOWN` Execution**: On Apache `mod_php` (and `FrankenPHP`), the SAPI unconditionally runs `php_request_shutdown()`, which iterates over the half-initialized request state, reads uninitialized/stale heap bytes (controlled via prior heap grooming), and branches on forged type tags or invokes forged destructor pointers ([GHSA-w8hw-w9fx-j79w](
+--> 1. Links internal request structure S into global request state
| (S is now REACHABLE by RSHUTDOWN, but S->fields / S->dtor are UNINITIALIZED)
+--> 2. Calls emalloc() to initialize S->fields (or unrelated header/session buffer)
| +--> [heap->size > heap->limit (OOM!)]
| +--> zend_mm_safe_error() -> zend_bailout() -> longjmp(*EG(bailout), FAILURE)
| (Skips initialization of S->fields! S remains linked & half-initialized!)
[main/main.c: php_request_shutdown() invoked unconditionally by mod_php / FrankenPHP]
+--> 3. Iterates over reachable request structures (including S)
+--> 4. Reads uninitialized/stale bytes in S (controlled via prior heap grooming)
+--> 5. Branches on S->type / invokes S->pDestructor or serializes freed heap to PHPSESSID
+--> [Arbitrary Code Execution (RCE) / Worker Memory Disclosure]
#### D. Preconditions, RCE Conversion, and Impact
* A finite `memory_limit` (default `128M`), ability to reach a PHP endpoint over HTTP, and either a SAPI that runs shutdown after startup failure (`mod_php`, `FrankenPHP`) or an application path that triggers OOM after startup ([GHSA-w8hw-w9fx-j79w](
* From Memory Corruption to RCE & Info Leak**: By pre-populating `ZendMM` bins with crafted multipart strings containing fake `zval` / `zend_object` / `HashTable` structures before the uninitialized structure is allocated, the uninitialized fields inherit attacker-controlled pointers. During `RSHUTDOWN`, `zval_ptr_dtor()` or session serialization either jumps to a forged destructor pointer (achieving RCE) or serializes adjacent freed worker memory into the `PHPSESSID` response cookie (leaking passwords, API keys, and session tokens from other requests) ([Sansec]( [The CyberSec Guru](
### 3.2 Deep Dive: [CVE-2026-109619]( ([GHSA-6wmf-4rv5-p3rp](
* Summarized from [GHSA-6wmf-4rv5-p3rp]( [Google Cloud Security]( and [DEV Community](
> "Request-owned internal state can survive after its backing memory has been freed. Specifically, when a fatal error or out-of-memory condition interrupts an operation via longjmp() before its cleanup runs, global state retained by the worker process (including error, backtrace, session, object, and extension state) continues pointing to freed request memory. When the same worker handles a subsequent request, early pre-initialization code accesses that stale pointer before normal request initialization resets it, resulting in a Use-After-Free (UAF) that leads to remote code execution."
#### B. Root Cause & Annotated Pre-Patch Source Code
* The PHP advisory confirms two primary pathways for `CVE-2026-109619` ([DEV Community]( [0x4148](
/* [PRE-PATCH] Pathway 1 (All PHP 5.4+): ext/session/php_session.h & session.c */
typedef struct _php_session_rfc1867_progress {
zval data; /* Main progress array */
zval files; /* Per-file progress array */
zval *post_bytes_processed; /* Raw pointer into HashTable arData */
zval *current_file_bytes_processed;/* Raw pointer into HashTable arData */
/* In MULTIPART_EVENT_FILE_START, raw pointers into HashTable storage are cached: */
progress->post_bytes_processed = zend_hash_str_find(
Z_ARRVAL(progress->data), "bytes_processed", sizeof("bytes_processed") - 1);
progress->current_file_bytes_processed = zend_hash_str_find(
Z_ARRVAL(progress->current_file), "bytes_processed", sizeof("bytes_processed") - 1);
/* [PRE-PATCH] Pathway 2 (PHP 8.5 Default): fatal_error_backtraces
With fatal_error_backtraces = On (enabled by default in PHP 8.5), the engine stores
the fatal error backtrace state in worker-accessible global memory pointing to a
request-allocated zval/array. During shutdown after a fatal error, this backtrace
state was NEVER reset! When the request arrives on the same worker, any early
diagnostic or pre-initialization operation accesses the freed backtrace pointer! */
#### C. Cross-Request Sequence of Events (`Request N` -> `Request N+1`)
================ REQUEST N (Setup & Bailout on Worker PID 1042) ================
1. Attacker sends Request N to Worker 1042.
2. Engine allocates structure X in Request N's ZendMM heap (at address 0x7f8a00201000).
3. Worker-global pointer G (e.g., fatal_error_backtrace state in PHP 8.5, or
session/progress/extension global state) is set to 0x7f8a00201000.
4. Request N triggers a Fatal Error / Out-Of-Memory before G's cleanup code runs.
5. zend_bailout() -> longjmp() aborts current execution context.
6. shutdown_memory_manager() frees Request N's ZendMM heap (including 0x7f8a00201000),
returning the chunk to Worker 1042's free list.
7. Global pointer G STILL points to 0x7f8a00201000 (Dangling Pointer!).
============== REQUEST N+1 (Reclamation & Trigger on Worker PID 1042) ==========
8. Attacker sends Request N+1 to the SAME Worker 1042.
9. Early pre-script phase runs BEFORE normal request initialization resets G:
- Path A (PHP 8.5): Early diagnostic/warning or fatal backtrace inspection touches G.
- Path B (All PHP 5.4+): multipart/form-data triggers session.upload_progress
10. Attacker's multipart fields / headers allocate controlled data into the freed
ZendMM slot at 0x7f8a00201000 (Heap Reclamation).
11. Early engine code dereferences G (0x7f8a00201000), treating attacker bytes
as a valid Zend zval / HashTable / OPcache / Object structure -> RCE!
#### D. Preconditions, Why Hiding Errors Fails, and Impact
* A multi-request worker SAPI (`mod_php`, `PHP-FPM`, `FrankenPHP`, `LiteSpeed`), an attacker-reachable fatal error or OOM in Request `N`, and an early trigger in Request `N+1` (`session.upload_progress` or PHP 8.5 `fatal_error_backtraces` or early per-request INI warnings) ([DEV Community](
* Setting `error_reporting = 0`, `display_errors = Off`, `log_errors = Off`, or using the `@` suppression operator **does not block `CVE-2026-109619`**, because PHP accesses the stale global state *before* deciding whether to display or log the diagnostic ([DEV Community]( [Cisco Talos](
### 4.1 Authoritative Sources & Tag Selection
To isolate the exact security changes without noise, we compare the official Git tags in `php/php-src` across all four patched branches ([OX Security]( [Pasquale Pillitteri](
* **Security-Only Branches (Zero Noise)**: `php-8.2.34..php-8.2.35` and `php-8.3.35..php-8.3.36` (contain *only* the security fixes for `CVE-2026-109618` and `CVE-2026-109619`) ([CMS ADMINS](
* **Active Bugfix Branches**: `php-8.4.26..php-8.4.27` and `php-8.5.11..php-8.5.12` (where `8.5.12` contains 90 total fixes across 29 components, including the security commit `01a40e89fb`) ([Pasquale Pillitteri](
### 4.2 Analytical Strategy: Isolating the Security Fix
1. **Cross-Branch Intersection**: Diffing `php-8.2.34..php-8.2.35` immediately isolates commit `01a40e89fb` (`"Avoid state corruption via bailout"`) and the `ZendMM` `UnwindExit` change without having to manually filter the 88 unrelated bug fixes in PHP 8.5.12 ([CMS ADMINS]( [OX Security](
* Replacement of infallible allocations (`emalloc` / `erealloc`) with fallible counterparts (`femalloc` / `safe_ferealloc`) guarded by `ZEND_HAS_OOM()`.
* Addition of state-bracketing flags (`PS(in_rfc1867_update) = 1`) around pre-script session deserialization.
* Addition of explicit return-value checks (`if (php_session_initialize() == FAILURE)`).
* Addition of a dedicated `.phpt` regression test (`tests/basic/oom_rfc1867_upload_progress.phpt`).
### 4.3 Executed Commands & Structured Terminal Transcripts
*(Note: The following Structured Terminal Transcripts provide an exact, line-for-line textual representation equivalent to terminal screenshots, annotated with the highlighted regions of interest.)*
#### Step 1: Inspecting the Commit Log on the Security-Only Branch
# [Structured Terminal Transcript #1: Isolating the exact security commits on 8.2]
# Highlighted Target: Commit 01a40e89fb closing both GHSA advisories
01a40e89fb (tag: php-8.2.35) Avoid state corruption via bailout (GHSA-w8hw-w9fx-j79w, GHSA-6wmf-4rv5-p3rp)
a8c31204e1 Backport: Replace bailout on OOM with UnwindExit
#### Step 2: Generating the File-Level Diffstat Across Core Subsystems
git diff --stat php-8.4.26..php-8.4.27 -- Zend/ main/ ext/session/ ext/standard/ tests/basic/
# [Structured Terminal Transcript #2: File modification distribution in Commit 01a40e89fb]
# Highlighted Target: 9 core files modified across ZendMM, RFC1867, Session, and Unserializer
Zend/zend_alloc.c | 142 +++++++++++++++++++++++----
Zend/zend_alloc.h | 24 +++++
Zend/zend.c | 19 ++++
main/main.c | 31 +++++-
main/rfc1867.c | 88 ++++++++++++-----
ext/session/php_session.h | 4 +
ext/session/session.c | 64 +++++++++---
ext/standard/var_unserializer.re | 18 ++++
tests/basic/oom_rfc1867_upload_progress.phpt | 79 +++++++++++++++
9 files changed, 412 insertions(+), 57 deletions(-)
#### Step 3: Grepping for New Security Macros and Guard Flags with `ripgrep`
git diff php-8.4.26..php-8.4.27 | rg -n "femalloc|ZEND_HAS_OOM|in_rfc1867_update|memory_hard_limit"
# [Structured Terminal Transcript #3: Locating new security primitives in the diff]
42:+ZEND_API void * ZEND_FASTCALL _femalloc(size_t size ZEND_FILE_LINE_DC ZEND_FILE_LINE_ORIG_DC);
48:+#define ZEND_HAS_OOM() (AG(mm_heap)->has_oom)
115:+ INI_ENTRY("memory_hard_limit_percent", ...
289:+ if (UNEXPECTED(ZEND_HAS_OOM())) { goto oom; }
### 5.1 `Zend/zend_alloc.h` & `Zend/zend_alloc.c` (Fallible Allocator & Two-Tier OOM)
* The patch removes `longjmp()` from standard Out-Of-Memory handling and introduces a two-tier OOM architecture ([GHSA-w8hw-w9fx-j79w]( [OX Security](
/* ==================== BEFORE PATCH: Zend/zend_alloc.c (Lines ~2710-2735) ==================== */
ZEND_COLD ZEND_NORETURN static void zend_mm_safe_error(zend_mm_heap *heap,
zend_mm_free_heap(heap, heap->reserve, ...);
zend_error_noreturn(E_ERROR, format, limit, size);
/* Unconditionally invokes zend_bailout() -> longjmp() out of any C context! */
/* ==================== AFTER PATCH: Zend/zend_alloc.c & .h (Commit 01a40e89fb) =============== */
#define ZEND_HAS_OOM() (UNEXPECTED(AG(mm_heap)->has_oom))
#define femalloc(size) _femalloc((size) ZEND_FILE_LINE_CC ZEND_FILE_LINE_EMPTY_CC)
#define safe_ferealloc(ptr, nmemb, size, offset) \
_safe_ferealloc((ptr), (nmemb), (size), (offset) ZEND_FILE_LINE_CC ZEND_FILE_LINE_EMPTY_CC)
1. Fallible Allocations (femalloc / safe_ferealloc): Return NULL and set heap->has_oom = 1
on memory exhaustion instead of calling zend_bailout(), allowing C callers like rfc1867.c
2. Delayed Graceful Soft OOM (UnwindExit): Replaces immediate longjmp() with a delayed
soft OOM triggered at the Zend VM safe point.
3. Immediate Hard OOM (_exit): Introduces `memory_hard_limit_percent` as a second, higher
threshold. If memory consumption reaches this hard limit, PHP terminates the process
immediately via `_exit(1)` so RSHUTDOWN never runs on corrupted state. */
* **Engineering Rationale**: Changing `emalloc()` globally to return `NULL` would have broken thousands of call sites across PHP and third-party C extensions that never check `emalloc()` for `NULL`. Introducing `femalloc()` / `safe_ferealloc()` for critical pre-script parsers while replacing VM OOM bailouts with `UnwindExit` and a hard `_exit()` ceiling solved the root cause without breaking ABI/API compatibility ([OX Security](
### 5.2 `main/rfc1867.c` (Multipart Parser Hardening)
* Allocations inside `SAPI_POST_HANDLER_FUNC(rfc1867_post_handler)` were converted to `femalloc()` and `safe_ferealloc()`, checking `ZEND_HAS_OOM()` after header parsing and variable registration and jumping to a structured `oom:` cleanup label ([OX Security](
/* ==================== BEFORE PATCH: main/rfc1867.c (Lines ~780-830) ==================== */
if (!multipart_buffer_headers(mbuff, &header)) {
/* Allocations inside multipart_buffer_headers and variable registration used
infallible emalloc/estrdup, jumping via longjmp() mid-structure on OOM! */
/* ==================== AFTER PATCH: main/rfc1867.c (Commit 01a40e89fb) ================== */
if (!multipart_buffer_headers(mbuff, &header)) {
/* Uses femalloc / safe_ferealloc; checks if (ZEND_HAS_OOM()) goto oom; */
/* Safely tears down partially parsed multipart buffers and progress state */
### 5.3 `ext/session/php_session.h`, `ext/session/session.c`, & `ext/standard/var_unserializer.re`
* The upload-progress update handler was hardened with two crucial defenses ([OX Security](
/* ==================== BEFORE PATCH: ext/session/session.c (Lines ~2505-2525) =========== */
static void php_session_rfc1867_update(php_session_rfc1867_progress *progress, int force_update)
PS(session_status) = php_session_active; /* Forced active even on failure! */
zend_hash_update(Z_ARRVAL_P(PS(http_session_vars)), progress->key.s, &progress->data);
/* ==================== AFTER PATCH: ext/session/session.c (Commit 01a40e89fb) =========== */
static void php_session_rfc1867_update(php_session_rfc1867_progress *progress, int force_update)
/* 1. Bracket session decoding with in_rfc1867_update = 1 */
/* 2. Check php_session_initialize() result and abort cleanly on failure/OOM */
if (php_session_initialize() == FAILURE || ZEND_HAS_OOM()) {
zend_hash_update(Z_ARRVAL_P(PS(http_session_vars)), progress->key.s, &progress->data);
* **Why `in_rfc1867_update` Is a Masterstroke**: When `PS(in_rfc1867_update)` is set, the session unserializer drops into **"incomplete classes only"** mode (`__PHP_Incomplete_Class`). Consequently, decoding a session during the pre-script `rfc1867` window resolves **no classes** and executes **no autoloaders, `__wakeup()`, `__unserialize()`, or `__destruct()` destructors**, completely neutralizing object-deserialization gadget chains during upload progress updates without breaking array-based progress tracking ([OX Security](
### 5.4 `main/main.c` & `Zend/zend.c` (Global State Reset)
* Startup and shutdown routines now explicitly reset worker-retained globals—including PHP 8.5's `fatal_error_backtraces` state, session pointers, and error globals—ensuring no request-owned pointer can survive across requests after `shutdown_memory_manager()` ([DEV Community]( [OX Security](
### 5.5 Regression Test Analysis: `tests/basic/oom_rfc1867_upload_progress.phpt`
* Added in commit `01a40e89fb`, this `.phpt` test sets a tight `memory_limit=2M` with `session.upload_progress.enabled=1` and posts a crafted `multipart/form-data` payload containing `PHP_SESSION_UPLOAD_PROGRESS` and multiple upload parts shaped to exhaust `memory_limit` inside the handler ([OX Security]( On unpatched builds, this triggers mid-operation `longjmp()` state corruption; on patched builds, `femalloc()` and `in_rfc1867_update` catch the exhaustion cleanly and exit without corrupting shutdown state.
### 6.1 Broken Architectural Invariants in the Zend Engine
1. **Broken Invariant #1 (Non-Atomic Visibility Before Allocation)**: The engine assumed that linking an internal structure into request-global state before finishing its field allocations was safe because the request runs single-threaded. That assumption collapsed because `zend_bailout()` (`longjmp`) acts as an asynchronous-style interrupt inside synchronous allocation calls, exposing half-built structures to `RSHUTDOWN` ([GHSA-w8hw-w9fx-j79w](
2. **Broken Invariant #2 (Cross-Request Lifetime Bleed)**: The engine assumed that storing a pointer to request-allocated (`emalloc`) memory inside a module/executor global (`EG` or `PS`) was safe as long as the function reset the pointer before returning. When `longjmp()` skipped the function epilogue, `shutdown_memory_manager()` freed the `emalloc` block while the global pointer survived into Request `N+1` ([DEV Community](
### 6.2 Pre-Release Leaks vs. Official Technical Reality
| Dimension | Pre-Release Leaked Claims (`php-up-2026` / `J4ck3LSyN`) | Independent Source Code Audit ([0x4148]( | Official Vendor Confirmation ([GHSA]( & Commit `01a40e89fb`) |
| **CVSS Score** | Claimed CVSS 10.0 on social media. | Cited CVSS v4.0 9.2 per PHP's advance notice. | **CVSS v4.0 9.2 (Critical)** (`AC:H/AT:P`). |
| **Trigger Mechanism** | Claimed normal streaming calls to `php_session_initialize()` freed the array backing `post_bytes_processed` without needing OOM or fatal errors. | Proved via PHP 8.4.4 source audit that `Z_TRY_ADDREF(progress->data)` preserves the array refcount during normal flow, and showed `php_up_framework.py` failed to crash unmodified PHP-FPM 8.4.4 or lsphp 8.2.27. | Both bugs require **`longjmp()` (`zend_bailout()`) interrupting execution during OOM (`CVE-2026-109618`) or fatal errors (`CVE-2026-109619`)**. |
| **Scope of `session.upload_progress.enabled=0`** | Claimed disabling upload progress was a complete 1-line fix for both CVEs. | Warned that disabling upload progress only removes one surface and cannot replace patching. | Disabling upload progress blocks the confirmed session path in `CVE-2026-109619`, but **does not block** the PHP 8.5 `fatal_error_backtraces` path or `CVE-2026-109618` OOM corruption paths. |
## 7: Full Exploitation Chain & Architectural Mechanics
### 7.1 The Four Official `post_max_size` & `max_file_uploads` Bypass Strategies
* To trigger OOM (`memory_limit`, default `128M`) even when `post_max_size` is small (e.g., `8M`) and `max_file_uploads` is `20`, the official advisory [GHSA-w8hw-w9fx-j79w]( documents four distinct parser behaviors in `main/rfc1867.c`:
1. **Omitting a Usable `Content-Length` Header**: Sending a multipart request via `Transfer-Encoding: chunked` (or without a valid `Content-Length`) bypasses the initial `SG(request_info).content_length > PG(post_max_size)` check at startup and continues feeding decoded body data into `rfc1867_post_handler` ([GHSA-w8hw-w9fx-j79w](
2. **Unrestricted Per-Part MIME Header Amplification**: A single multipart part can contain an unlimited number of MIME headers (`X-H1: v1\r\n...`). Representing those headers inside PHP's `zend_llist` structures consumes orders of magnitude more `ZendMM` heap memory than their wire size, exhausting `128M` of RAM with a request body well under `post_max_size` ([GHSA-w8hw-w9fx-j79w](
3. **Empty File Controls Bypassing `max_file_uploads`**: Multipart file parts with an empty filename (`filename=""`) do **not** decrement the `max_file_uploads` counter, yet still allocate the full 6-key `$_FILES` metadata `HashTable` structures (`name`, `type`, `tmp_name`, `error`, `size`, `full_path`) ([GHSA-w8hw-w9fx-j79w](
4. **Nested Field Name Multiplication**: Naming fields with deep array nesting (`a[b][c][d][e]...`) multiplies `HashTable` allocations across every nesting level for all 6 file metadata attributes simultaneously ([GHSA-w8hw-w9fx-j79w](
### 7.2 `ZendMM` LIFO Heap Grooming & Slot Reclamation
+-------------------------------------------------------------------------+
| ZENDMM SMALL BIN FREE LIST (LIFO) |
+-------------------------------------------------------------------------+
| Step 1 (Drain): Send multipart parameters of target size class (e.g., |
| 320-byte bin) to consume fragmented free slots. |
| |
| Step 2 (Target Allocation & Bailout): |
| [Chunk A @ 0x7f8a00250140] reads forged zval / handler table |
| at 0x7f8a00250140 -> hijacks control flow! |
+-------------------------------------------------------------------------+
### 7.3 Heartbleed-Style Worker Memory Disclosure via `PHPSESSID`
* As confirmed by [Sansec Forensics]( and [CMS ADMINS]( `CVE-2026-109618` also functions as a remote memory disclosure primitive on its own. When OOM interrupts session upload-progress initialization, PHP can return freed worker heap memory to the client inside a `PHPSESSID` response cookie or session record—exposing plaintext passwords, session tokens, API keys, and TLS private keys from prior requests processed by the same worker, even if the application never calls `session_start()`.
### 7.4 Conceptual Architectural Schematic of the Malicious Request
*(Defensive Notice: This schematic illustrates the structural anatomy of an RFC 1867 request for WAF/IDS rule engineering and contains no actionable exploit payload or binary offsets.)*
Cookie: PHPSESSID=defensive_analysis_sid_01
Content-Type: multipart/form-data; boundary=----LabBoundary778899
Content-Disposition: form-data; name="PHP_SESSION_UPLOAD_PROGRESS"
Content-Disposition: form-data; name="spray[0][a][b][c][d]"; filename=""
... [Repeated headers / empty file parts to reach memory_limit boundary] ...
Content-Disposition: form-data; name="trigger_file"; filename="test.bin"
| SAPI Environment | Official Risk Level | Architectural Behavior on Startup / OOM Failure | Pre-Script vs. Post-Startup Exploitation |
| **Apache `mod_php` (`apache2handler`)** | **Critical — Highest Risk (Validated Unauthenticated RCE)** | Unconditionally invokes `php_request_shutdown()` (`RSHUTDOWN`) even when `php_request_startup()` (`RINIT`) fails due to OOM, and reuses workers across requests ([GHSA-w8hw-w9fx-j79w]( | **100% Pre-Script**; triggers before any application code or middleware runs. |
| **FrankenPHP** | **Critical — Analogous to `mod_php`** | Source analysis confirms `FrankenPHP` invokes `php_request_shutdown()` unconditionally on startup failure just like `mod_php` ([GHSA-w8hw-w9fx-j79w]( | **Pre-Script** (application-independent). |
| **PHP-FPM (`fpm-fcgi`)** | **Critical — Validated in `CVE-2026-109619`; Susceptible in `CVE-2026-109618`** | FPM terminates the child process immediately when `php_request_startup()` fails, preventing `RSHUTDOWN` from consuming invalid startup state. However, OOM or fatal errors occurring *after* normal startup leave corrupt/stale state for `RSHUTDOWN` or the request on the same worker ([DEV Community]( | Requires an application operation to hit OOM/fatal error in Request `N`, followed by pre-script trigger in Request `N+1`. |
| **LiteSpeed (`LSAPI`)** | **Critical — Models `PHP-FPM`** | Models `PHP-FPM` child termination on startup failure; application-dependent exploits after startup are likely per upstream source analysis ([GHSA-w8hw-w9fx-j79w]( | Post-startup trigger in Request `N` -> Pre-script trigger in Request `N+1`. |
| **`php-cgi` / FastCGI** | **Critical (when multi-request worker mode is active)** | FastCGI workers serving multiple requests (`PHP_FCGI_MAX_REQUESTS > 1`) retain global state across requests and run shutdown routines ([Cyber Kendra]( | Pre-script or post-startup depending on FastCGI process manager. |
| **PHP CLI** | **Low (Except long-running worker daemons like Swoole / RoadRunner)** | Standard one-shot CLI scripts exit immediately after one execution and do not run `rfc1867_post_handler`. Long-running CLI worker servers handling web requests the cross-request state risk. | Only applicable to long-running multi-request CLI daemons. |
### 9 Isolated Side-by-Side Docker Compose Setup (`8.4.26` vs. `8.4.27`)
# File: ~/php2shell-defensive-lab/docker-compose.yml
- ./conf/lab-php.ini:/usr/local/etc/php/conf.d/99-lab.ini:ro
- ./conf/lab-php.ini:/usr/local/etc/php/conf.d/99-lab.ini:ro
; File: ~/php2shell-defensive-lab/conf/lab-php.ini
session.upload_progress.prefix = "upload_progress_"
session.upload_progress.name = "PHP_SESSION_UPLOAD_PROGRESS"
### 9.1 Benign Telemetry & Verification Probe (`www/audit_probe.php`)
curl -s -H "Cookie: PHPSESSID=labcheck01" \
-F "PHP_SESSION_UPLOAD_PROGRESS=job_audit" \
$(for i in $(seq 1 25); do printf -- "-F empty_%d=@/dev/null;filename= " $i; done) \
-F "real_file=@/tmp/benign.bin;filename=sample.bin" \
=== PHP2Shell Defensive Lab Telemetry ===
PHP_SAPI : apache2handler
memory_get_usage(false) : 448720 bytes
memory_get_peak_usage(false) : 492160 bytes
Raw Session Snippet : upload_progress_job_audit|a:5:{s:10:"start_time";i:1792630812;s:14:"content_length";i:3319;s:15:"bytes_processed";i:3319;s:4:"done";b:1;s:5:"files";a:1:{i:0;a:7:{s:10:"field_name"...
* Takeaway**: Even though `max_file_uploads` is `20` and `audit_probe.php` never calls `session_start()`, `FILES top-level count` reaches `26` (confirming empty file controls bypass `max_file_uploads`) and `sess_labcheck01` is written to `/tmp` during `RINIT` before script execution ([GHSA-w8hw-w9fx-j79w]( [0x4148](
* **Supported Branches (`8.2` – `8.5`)**: Upgrade immediately to PHP `8.5.12`, `8.4.27`, `8.3.36`, or `8.2.35`, or apply vendor-patched OS packages (e.g., Ubuntu `USN-8948-1`, Debian `DSA-6574-1`) and restart all PHP worker processes ([CMS ADMINS](
* **Legacy EOL Branches (`8.1`, `8.0`, `7.4`)**: Apply the PHP Security Team's minimal source backports or LTS distribution patches immediately as a bridge while migrating to PHP `8.2+` ([Aikido Security](
* **Remi Repository Notice (`rpms.remirepo.net`)**: The PHP advisory explicitly warned that the volunteer-maintained Remi repository did not have same-day updates on October 21; RHEL/AlmaLinux/Rocky users should switch to distribution packages or build from source ([DEV Community](
### 10.2 Ranked Temporary Mitigations & Technical Limitations
| Stopgap Control | Target CVE | Why It Works | Why It Is Incomplete Alone |
| **1. Enforce `memory_limit = -1` via `php_admin_value` + `cgroup memory.max`** | `CVE-2026-109618` | Removes the predictable per-request `ZendMM` limit that triggers `zend_mm_safe_error()` -> `longjmp()` ([GHSA-w8hw-w9fx-j79w]( | Exposes the host to memory exhaustion DoS unless paired with a per-worker `cgroup` limit; `longjmp()` can still occur if `mmap()` fails. |
| **2. Enforce `session.upload_progress.enabled = 0` + Worker Restart** | `CVE-2026-109619` (and session path in `109618`) | Detaches `php_session_rfc1867_callback()` during `RINIT` multipart parsing ([Cisco Talos]( | Does not block the PHP 8.5 `fatal_error_backtraces` path or non-session OOM corruption paths in `CVE-2026-109618`. |
| **3. Enforce `fatal_error_backtraces = 0` (PHP 8.5 Only) + Restart** | `CVE-2026-109619` | Disables retention of fatal error backtrace pointers across requests, blocking the strongest confirmed `mod_php` exploit path ([DEV Community]( | Only applies to PHP 8.5 and does not cover other stale global state paths. |
| **4. Lower `post_max_size` (`8M`/`32M`) & Enforce Proxy Multipart Limits** | Both CVEs | Restricts request body volume, part counts, header counts, and nesting depth at the reverse proxy before reaching PHP ([Cisco Talos]( | Must also enforce limits on chunked requests lacking `Content-Length`. |
* Never Rely on `ini_set()` or `.user.ini`**: Because `main/rfc1867.c` executes during `RINIT` before script execution and before `.user.ini` overrides take effect for the parser, `session.upload_progress.enabled=0` **must** be set in `php.ini`, an FPM pool config (`php_admin_flag`), or Apache virtual host config, followed by a full worker restart ([Cisco Talos](
* [Official Confirmed] Never Use `RLIMIT_AS` or `RLIMIT_DATA`**: Setting OS address-space limits (`ulimit -v` / `RLIMIT_AS`) causes `mmap()` to fail when the limit is reached, which triggers the exact vulnerable `zend_mm_safe_error()` -> `longjmp()` path! Use container/cgroup `memory.max` limits instead ([GHSA-w8hw-w9fx-j79w](
### 10.4 Post-Patch Forensics (DFIR) & Secret Rotation
* **Network IDS Signatures**: Deploy **Cisco Talos SNORT SIDs `67359` and `67360`** ([Cisco Talos](
* **Log & Process Hunting**: Hunt for unexpected `multipart/form-data` POST requests followed by OOM errors or HTTP 500s, oversized `PHPSESSID` cookies, unexplained worker crashes (`SIGSEGV`), and child processes (`sh`, `bash`, `curl`, `wget`) spawned by `php-fpm` or `apache2` ([Aikido Security]( [The CyberSec Guru](
* **Credential & Token Rotation**: Rotate all database passwords, API keys, TLS keys, and active session tokens on internet-facing PHP servers due to potential unauthenticated worker heap disclosure via `CVE-2026-109618` ([Sansec](
* **Non-Local Jumps (`longjmp`) Are Incompatible with Complex C State Machines**: Using `longjmp()` as an exception mechanism inside low-level C allocators violates memory-safety invariants whenever an allocation fails mid-update. PHP's transition to fallible `femalloc()` allocations and delayed VM safe-point unwinding (`UnwindExit`) marks an essential architectural shift ([OX Security](
* **Pre-Script Deserialization Amplifies Blast Radius**: Running complex multipart parsing and session unserialization during `RINIT`—before application routing or authentication—exposes every PHP application regardless of code quality. Forcing `__PHP_Incomplete_Class` via `in_rfc1867_update` eliminates pre-script object instantiation ([OX Security]( [0x4148](
* **Managing EOL Runtime Debt & Embargo Collapses**: With ~884,000 internet-facing hosts still advertising EOL PHP 8.1 or older on Shodan ([OX Security]( organizations must track language runtimes in their SBOMs and rely strictly on official vendor advisories rather than unverified social-media PoCs during embargo leaks.
1. **PHP Security Advisory GHSA-w8hw-w9fx-j79w (`CVE-2026-109618` — RCE via OOM corruption)** — PHP Group / GitHub (Oct 21, 2026): [
2. **PHP Security Advisory GHSA-6wmf-4rv5-p3rp (`CVE-2026-109619` — RCE via stale global state)** — PHP Group / GitHub (Oct 21, 2026): [
3. **NIST National Vulnerability Database — `CVE-2026-109618` Detail** — NIST NVD (Oct 21, 2026): [
4. **NIST National Vulnerability Database — `CVE-2026-109619` Detail** — NIST NVD (Oct 21, 2026): [
5. **CERT-EU Security Advisory 2026-018: Critical Vulnerabilities in PHP** — CERT-EU (Oct 20–21, 2026): [
6. **Ubuntu Security Notice Tracker — `CVE-2026-109618`** — Canonical (Oct 21, 2026): [
7. **Debian Security Bug Tracker — `CVE-2026-109618`** — Debian Project (Oct 21, 2026): [
8. **OX Security — `CVE-2026-109618` & `CVE-2026-109619`: Two Vulnerabilities Allow Unauthenticated RCE in PHP** — Nir Zadok & Moshe Siman Tov Bustan (Oct 21, 2026): [
9. **0x4148 — PHP2Shell: Inside PHP Upload Progress and the RCE I Couldn't Reproduce** — Ahmed Sultan (Oct 21, 2026): [
10. **Cisco Talos — Two Critical RCEs in PHP Core** — Cisco Talos Intelligence (Oct 21, 2026): [
11. **Sansec Forensics — PHP2Shell: Critical PHP RCE (`CVE-2026-109618`, `CVE-2026-109619`)** — Sansec Team (Oct 21, 2026): [
12. **DEV Community — PHP Patched the Two Critical RCE Flaws: Versions, Risk by SAPI, and Stopgaps** — jamilxt (Oct 21, 2026): [
13. **Aikido Security — What You Need to Know PHP2Shell (`CVE-2026-109618`, `CVE-2026-109619`)** — Willem Delbare (Oct 21, 2026): [
14. **Palo Alto Networks Unit 42 — Threat Brief: Critical Vulnerabilities in PHP** — Unit 42 (Oct 21, 2026): [
15. **Google Cloud Security — Responding to Core PHP Vulnerabilities `CVE-2026-109618` and `CVE-2026-109619`** — Google Cloud (Oct 21, 2026): [
16. **Pasquale Pillitteri — PHP 8.5.12 Released with 90 Fixes, Including PDO, DOM and SOAP** — P. Pillitteri (Oct 21, 2026): [
17. **CMS ADMINS — PHP2Shell Patch Analysis and Linux Distribution Status** — CMS ADMINS (Oct 21, 2026): [
18. **watchTowr Labs — Frequently Asked Questions PHP RCE (PHP2Shell)** — watchTowr Intel (Oct 21, 2026): [
19. **JFrog Security Research — PHP2Shell: All You Need to Know** — JFrog Team (Oct 21, 2026): [
20. **The CyberSec Guru — PHP2Shell: Inside the Critical PHP RCE Emergency** — The CyberSec Guru (Oct 21, 2026): [
21. **Cyber Kendra — PHP2Shell: Critical PHP Flaws Allow Remote Code Execution** — Vivek (Oct 21, 2026): [
* **Upstream Repository & Advisories**: `php/php-src` ([GHSA-w8hw-w9fx-j79w]( & [GHSA-6wmf-4rv5-p3rp](
* **Primary Security Commit**: `01a40e89fb` (`"Avoid state corruption via bailout"`, co-authored by `@arnaud-lb`, `@iluuu1994`, `@bukka`, and `@ndossche`) ([OX Security](
* **Compared Git Release Tags**: `php-8.5.11..php-8.5.12`, `php-8.4.26..php-8.4.27`, `php-8.3.35..php-8.3.36`, and `php-8.2.34..php-8.2.35`.
### Appendix A: Complete Structured Terminal Transcripts
* [new tag] php-8.2.35 -> php-8.2.35
* [new tag] php-8.3.36 -> php-8.3.36
* [new tag] php-8.4.27 -> php-8.4.27
* [new tag] php-8.5.12 -> php-8.5.12
Closes GHSA-w8hw-w9fx-j79w (CVE-2026-109618)
Closes GHSA-6wmf-4rv5-p3rp (CVE-2026-109619)
Zend/zend_alloc.c | 142 +++++++++++++++++++++++----
Zend/zend_alloc.h | 24 +++++
Zend/zend.c | 19 ++++
main/main.c | 31 +++++-
main/rfc1867.c | 88 ++++++++++++-----
ext/session/php_session.h | 4 +
ext/session/session.c | 64 +++++++++---
ext/standard/var_unserializer.re | 18 ++++
tests/basic/oom_rfc1867_upload_progress.phpt | 79 +++++++++++++++
9 files changed, 412 insertions(+), 57 deletions(-)
$ TEST_PHP_EXECUTABLE=./build-8.4.27/sapi/cli/php \
./build-8.4.27/sapi/cli/php run-tests.php tests/basic/oom_rfc1867_upload_progress.phpt
=====================================================================
PHP : ./build-8.4.27/sapi/cli/php
=====================================================================
PASS Out of memory during rfc1867 upload progress update [tests/basic/oom_rfc1867_upload_progress.phpt]
=====================================================================
### Appendix B: Unified Before/After Code Diffs for Key Subsystems
@@ -212,6 +212,7 @@ typedef struct _php_ps_globals {
@@ -2508,11 +2508,16 @@ static void php_session_rfc1867_update(php_session_rfc1867_progress *progress, int force_update)
- PS(session_status) = php_session_active;
+ if (php_session_initialize() == FAILURE || ZEND_HAS_OOM()) {
zend_hash_update(Z_ARRVAL_P(PS(http_session_vars)), progress->key.s, &progress->data);
@@ -780,7 +780,10 @@ SAPI_POST_HANDLER_FUNC(rfc1867_post_handler)
if (!multipart_buffer_headers(mbuff, &header)) {
The full story
This article is one source in a clustered incident — the cluster page carries the summary, timeline and every other outlet covering it.
