PHP2Shell Explained: How an Out-of-Memory Error Became Remote Code Execution

On 21 October 2026, the PHP project shipped an emergency release to fix two critical vulnerabilities that live in the PHP engine itself, not in any application running on top of it. Researchers named them PHP2Shell, and the name is a promise: send the right HTTP requests to a vulnerable server, no login required, and you can end up with a shell. The flaws are tracked as CVE-2026-109618 and CVE-2026-109619, each rated 9.2 of 10 on CVSS v4.0.

What makes this one worth your attention is where it sits. PHP runs roughly 70% of the websites whose server-side language is known — WordPress, Drupal, Joomla, Magento, MediaWiki (the software behind Wikipedia), and millions of hand-rolled sites. A bug in the runtime reaches all of them at once, even the ones whose own code is spotless. Wiz, one of the three research teams involved, put it bluntly: a plain “Hello World” page on an affected PHP version can be exploited.

The other reason it matters is how it was found. The underlying weakness has been sitting in the codebase since PHP 5.4.0 — more than a decade. It survived a 2004 discovery by Stefan Esser, a 2020 bug report, and a 2024 professional audit, each time because nobody could prove it was actually exploitable. What changed in 2026 was frontier AI: the research teams used large language models to chain the flaws into working, portable exploits in a matter of days. That shift — from “theoretically a bug” to “here is a working shell” — is as much the story as the vulnerability itself.

This post walks through what the two CVEs actually are, how the exploit chain works at the engine level, who is exposed, and exactly what to do about it.

PHP2Shell at a glance

ItemDetail
NamePHP2Shell (industry nickname)
CVEsCVE-2026-109618 (OOM corruption), CVE-2026-109619 (stale global state)
SeverityCVSS v4.0 9.2 (Critical) for each; vector AV:N/AC:H/AT:P/PR:N/UI:N/VC:H/VI:H/VA:H
ImpactUnauthenticated RCE, process-memory disclosure (Heartbleed-style), denial of service
AffectedEvery PHP release from 5.4.0 onward, across mod_php, PHP-FPM, php-cgi, FrankenPHP and LiteSpeed
Fixed inPHP 8.5.12, 8.4.27, 8.3.36, 8.2.35 (released 21 Oct 2026, 14:00 UTC)
EOL branches7.4–8.1 get minimal source backports only; 5.4–7.3 get nothing from the PHP project
Discovered byCalif, OpenAI and Wiz (with Quarkslab), using frontier AI models
Exploited in the wildYes — attempts seen from 17 Oct (Amazon MadPot), broad scanning after the patch
CISA KEVNot listed as of 22 Oct 2026
FixPatch. There is no complete configuration-only mitigation.

The single most important line in that table: updating is the only complete fix. Everything else on this page is context for why, and what to do while you get there.

How it went public before the patch

This was supposed to be a textbook coordinated disclosure. It wasn’t. On 20 October, the two CVE identifiers and a disclosure date started circulating on Hacker News and social media — initially on nothing more than a “source: trust me, bro” tip. The PHP security team responded the same day with an advance notice of an emergency release set for 21 October at 14:00 UTC, reserving the right to ship early if details leaked further or exploitation appeared.

The embargo then collapsed from several directions at once. A video demonstrating a working proof-of-concept was published ahead of schedule. Mitigation advice the PHP Foundation had shared with critical-infrastructure organizations appears to have leaked too. And six hours before release, Chainguard published container images containing the fix it had received in advance — meaning anyone could diff the patched binary against the old one while every other PHP user was still waiting. The Foundation moved the release forward a day, from the planned 22 October to 21 October.

The original disclosure was a joint effort by Calif, OpenAI and Wiz (with Quarkslab), coordinated with the PHP security team and a wide circle of infrastructure providers — Cloudflare, Google Cloud, AWS, Microsoft, Automattic, Fastly, Akamai, Imperva and Vercel among them — who prepared WAF rules and guidance in advance.

Date (2026, UTC)Event
Aug 1Calif discovers multiple RCE paths while auditing PHP for OpenAI’s “Patch the Planet”
Aug 11OOM-to-RCE reported to PHP by Calif and OpenAI; coordinated disclosure begins
Aug 19–21Wiz independently discovers the first bug; confirms unauthenticated RCE on Drupal and WordPress
Aug 24Wiz reports the vulnerabilities to the PHP security team
Sep 21PHP acknowledges and begins developing a coordinated fix
Oct 17Amazon MadPot honeypots log the first exploitation attempt — four days before disclosure
Oct 20CVE IDs and date leak online; PHP posts advance notice; CERT-EU issues advisory 2026-018
Oct 21, ~03:00Cloudflare WAF protections active; early attack traffic observed
Oct 21, 14:00PHP releases 8.5.12 / 8.4.27 / 8.3.36 / 8.2.35 and the advisories; public disclosure
Oct 22Originally planned disclosure date

The root cause: bailouts leave stale state

Both CVEs are memory-safety bugs, and both grow from the same soil: what PHP does when a request dies unexpectedly, especially when it runs out of memory.

When PHP hits a fatal condition — exceeding the configured memory_limit, a parse error, a timeout — it doesn’t unwind the stack gracefully the way a C++ exception would. It uses a mechanism called a bailout, implemented with longjmp(), which jumps straight to an error-handling boundary and abandons whatever it was doing. That is fast and simple, but it has a dangerous property: if the jump happens after PHP has published a pointer or allocated a cache slot, but before the matching cleanup or bookkeeping runs, the engine is left holding a partially initialized, or stale, piece of state.

In a modern PHP deployment this is a problem because workers are reused. Apache in prefork mode, or any process manager, hands one worker process many requests in a row. So the sequence that matters is:

  1. Request A publishes some request-owned state into a global — a session pointer, an unserializer’s bookkeeping, an error backtrace, an OPcache slot.
  2. A fatal error (usually an attacker-induced out-of-memory) interrupts A before the state is cleaned up.
  3. PHP frees Request A’s heap memory, but the surviving worker’s global still points into that freed region.
  4. Request B lands on the same worker and touches that global before normal initialization resets it.
  5. Request B reads, frees, or branches on a dangling pointer — a classic use-after-free. With a carefully shaped heap, that becomes memory disclosure or full code execution.

The PHP security team’s own framing is that this state must be reset before request memory is released, and in a number of input, configuration, session and error-handling paths, it isn’t. CVE-2026-109618 is the producer side turned into a weapon — forcing the fatal interruption at the right moment. CVE-2026-109619 is the consumer side — the operations that trust stale state on the next request. Below, the engine-level diagram shows the shared pattern; the two sections after it break down each CVE.

The producer and consumer run in different requests that happen to hit the same worker. Multiple workers and worker recycling lower the odds per attempt but don’t remove the condition — an attacker just sends more requests.

CVE-2026-109618: forcing the crash at the right moment

The first CVE (GHSA-w8hw-w9fx-j79w, “RCE via OOM corruption”) is the engine-level flaw, and the clever part is how an attacker reaches an out-of-memory condition on demand without tripping PHP’s own size limits.

PHP compares post_max_size against the request’s declared Content-Length before it parses the body. But it doesn’t re-check that limit against the bytes it actually consumes. Calif found two ways to exploit that gap:

  • No usable declared length. A chunked HTTP/1.1 request, or a lengthless HTTP/2 multipart request, sails past the initial post_max_size check and keeps feeding the parser. A configured input-decompression filter does the same thing, since the compressed length and the decoded stream don’t match.
  • Cheap memory amplification. An RFC 1867 upload part with filename="" carries no file data and doesn’t count toward max_file_uploads, yet PHP still builds all six $_FILES entries for it (name, full_path, type, tmp_name, error, size). Nest the field name — files[a][b][c]... — and PHP rebuilds that whole hierarchy six times over, turning one tiny empty-upload record into a pile of native HashTable, Bucket and string allocations. A few hundred of these blow past the default 128 MiB heap while the request body stays under the default 8 MiB post_max_size.

With a controllable crash in hand, the exploit targets a subtle ordering bug deep in the engine’s hash-table code (_zend_hash_str_add_or_update_i). PHP increments the “number of elements in use” counter before it finishes initializing a new bucket. If the key allocation fatals at exactly that point, the counter stays bumped but the bucket is never filled in — and the teardown code trusts the counter, walking into the half-built bucket and consuming stale allocation bytes as if they were a live entry. On Apache mod_php, request shutdown runs even after a startup failure, so it hits that malformed table; that is the path to RCE against arbitrary PHP scripts.

The fix is structural. PHP removed longjmp() for OOM entirely. Instead of jumping out of a half-finished operation, it now defers to a graceful “soft” OOM at the next safe point in the virtual machine, with a hard _exit() fallback if memory climbs past a second, higher threshold. Shutdown never sees invalid state again, and PHP keeps its ability to enforce a memory limit.

CVE-2026-109619: the stale state the next request trusts

The second CVE (GHSA-6wmf-4rv5-p3rp, “RCE via stale global state”) covers the consumer side — the places where PHP touches request-owned globals before a new request has reinitialized them. Four concrete variants were grouped under this CVE, and together they explain how a crash becomes a shell or a data leak.

A recurring character here is PHP’s session upload-progress feature. It opens and updates a session while PHP is still parsing the multipart request body — before any application code runs, and before PHP even considers its modules fully activated. No call to session_start() is needed; the feature is on by default. That early execution window is what lets an attacker reach session internals from an unauthenticated request against a “Hello World” endpoint.

The four demonstrated techniques:

  • Session decoder use-after-free. PHP’s generic unserializer keeps bookkeeping (reference and destructor lists) in a global. A fatal error mid-decode skips the cleanup that would clear it, so the next request’s upload-progress handler reuses a dangling decoder. Crafted serialized input then steers a native value-copy to attacker-chosen memory — application-dependent RCE that works under both mod_php and PHP-FPM.
  • Stale session-ID pointer — memory disclosure. After a crash leaves PS(id) pointing at freed memory, a follow-up multipart request makes PHP treat that freed region as a live string and URL-encode it straight into the Set-Cookie: PHPSESSID response header. It leaks roughly 67 KB of live worker memory per request — other users’ passwords, tokens, API keys, certificates. This is the Heartbleed-style primitive, and it works even when the app never calls session_start().
  • Fatal-error backtrace use-after-free. On PHP 8.5 with fatal_error_backtraces enabled, the backtrace of the last fatal error is kept in a global that shutdown never resets. A diagnostic early in the next request reaches the dangling pointer. This is the strongest demonstrated case and the basis of the confirmed mod_php RCE.
  • OPcache type confusion. OPcache persists compiled classes into shared memory and publishes an index only after persistence completes. A memory bailout between those two steps leaves a half-persisted slot; a later worker reusing that slot resolves stale metadata as the wrong pointer type, corrupting shared cache state across workers — used for unauthenticated RCE against WordPress.

There’s a second consumer worth noting: certain invalid or deprecated INI settings emit a diagnostic before request initialization, and that diagnostic can consume stale error state. Crucially, hiding the warning with error_reporting, @, or display_errors=Off does not help — the dangerous access happens before PHP decides whether to report it at all. Wiz and the PHP team both stress that these four are not the whole story: other tracked and untracked variants of the same pattern exist, which is why the fix resets the entire family of request-owned state rather than patching one path.

Why the way you run PHP matters

The single biggest factor in how exposed you are is your SAPI — the interface between PHP and your web server. The difference comes down to one decision: what does PHP do when request startup fails?

Apache with mod_php is the worst case. It runs request shutdown unconditionally, even after a startup failure. So an attacker can force the crash during the engine’s own startup — before a single line of application code executes — and still have shutdown walk into the corrupted state. This is the demonstrated, application-independent, pre-authentication RCE. The PHP team explicitly calls mod_php the highest-risk configuration. FrankenPHP calls shutdown unconditionally too, so source analysis flags it as likely susceptible in the same way.

PHP-FPM is harder, but not safe. When request startup fails, FPM kills the worker and removes it from the pool, which breaks the startup-crash path. But that only closes one door. Calif demonstrated a working FPM exploit by publishing stale state after startup succeeds — during normal application execution — then reaching it through upload-progress on the next request. It needs a suitable application operation (a session_start(), unserialize(), or WordPress’s maybe_unserialize()), but it works against a normal dynamic FPM pool. PHP confirmed RCE against PHP-FPM via CVE-2026-109619.

php-cgi, LiteSpeed/LSAPI are listed as at risk; LSAPI models FPM and is a conditional candidate. In short: if your server handles web requests with PHP, assume you’re affected. The SAPI changes how hard the exploit is to build, not whether the bug is there.

Who is affected, and what to upgrade to

The confirmed exposed range is every PHP release before the fixed versions — wider than the leaked tool’s “5.4 and up” banner, which is itself alarming enough. The earliest affected code path Calif found dates to PHP 5.4.0.

BranchStatusFixed / action
PHP 8.5.xAffected before 8.5.12Update to 8.5.12
PHP 8.4.xAffected before 8.4.27Update to 8.4.27
PHP 8.3.xAffected before 8.3.36Update to 8.3.36
PHP 8.2.xAffected before 8.2.35Update to 8.2.35 (branch EOL 31 Dec 2026)
PHP 8.0 – 8.1, 7.4Affected, end-of-lifeNo official release; minimal source backports published for those building from source
PHP 5.4 – 7.3Affected, end-of-lifeNothing from the PHP project; upgrade, or rely on a distro/LTS vendor backport

A few practical notes. If you install PHP from your Linux distribution (Debian, Ubuntu, RHEL, AlmaLinux, SUSE and others pushed updates), take the fix from the distro rather than from source — they received the patches in advance. If your host manages PHP for you, ask them to confirm the update. One sharp edge: the volunteer-run Remi repository (rpms.remirepo.net, common on RHEL-family servers) did not have the fix on release day; the PHP Foundation advised switching to distribution packages or building from source. Note that only the four supported branches get an official release — if you’re on 8.1 or older, patching means an upgrade or a vendor backport, not a drop-in.

Exploitation in the wild

The uncomfortable headline: exploitation attempts predate the public disclosure. Amazon Threat Intelligence found a PHP2Shell attempt against its MadPot honeypot network on 17 October — four days before the patch — from an IP tied to a commercial anonymization service. It’s unclear how that actor got the exploit information while it was still under embargo. From roughly 02:00 UTC on release day, Amazon saw multiple proof-of-concept exploits and further attempts from varied infrastructure, suggesting several distinct actors.

Through the day of and after the patch, the picture was consistent across vendors: lots of probing, no confirmed successful compromise reported yet, and a clear warning that it won’t stay that way. Cloudflare’s CTO said they were “seeing early attacks emerging” before the fix shipped. Hadrian reported exploitation “at scale,” mostly abusing the session upload-progress path. watchTowr said it reproduced both bugs and was watching exploitation attempts; Patchstack, Aikido and others reported attempts too. As of 22 October, neither CVE was in CISA’s Known Exploited Vulnerabilities catalog.

Amazon’s guidance is the key takeaway for defenders: start your investigations from at least 17 October, not from the disclosure date. If you ran a vulnerable PHP version on an internet-facing server in that window, assume you may have been probed, and check. Amazon published a short list of observed source IPs — all shared anonymization infrastructure, so investigate sightings rather than blindly blocking:

IP addressFirst observed
103.216.220.9217 Oct 2026
31.223.184.16621 Oct 2026
37.19.200.521 Oct 2026
134.209.159.6421 Oct 2026
103.114.163.18821 Oct 2026
95.217.98.3721 Oct 2026

For scale: researchers put the exposed population somewhere between a rough 1.3 million and a Shodan-counted 1.9 million internet-facing PHP hosts. Sansec’s pre-release estimate was that mass exploitation would begin within about 15 minutes of the patch landing.

What to do right now

1. Patch. This is the whole answer. The PHP team is unambiguous: there is no complete configuration-only mitigation for either CVE. Update to 8.5.12, 8.4.27, 8.3.36 or 8.2.35 and restart your workers. On most servers the fix comes through your package manager:

# Debian / Ubuntu
sudo apt update && sudo apt upgrade php8.3 php8.3-fpm
sudo systemctl restart php8.3-fpm   # or: systemctl restart apache2

# RHEL / AlmaLinux / Rocky (distro packages, not Remi)
sudo dnf update php php-fpm php-cli php-common
sudo systemctl restart php-fpm

# then confirm the running version
php -v

After restarting, check php -v reports a fixed build, and remember OPcache and FPM workers need to actually recycle for the new binary to take effect.

2. If you genuinely cannot patch yet, reduce exposure — but don’t mistake this for safety. These settings must go in php.ini, an FPM pool, or server config (applied at startup), enforced so applications can’t override them with ini_set() or .user.ini. Restart every worker after changing them.

  • For CVE-2026-109618: set memory_limit=-1 (enforced via php_admin_value in the FPM pool or Apache config). This removes the controllable per-request limit, but an underlying allocation failure can still reach the bug. Keep post_max_size conservative (e.g. 8M, or 32M if you need uploads), bound multipart part counts, header counts and nesting, and enforce decoded-size limits at the proxy. Do not use RLIMIT_AS/RLIMIT_DATA — they trigger the vulnerable path.
  • For CVE-2026-109619: set session.upload_progress.enabled=0, and on PHP 8.5 also fatal_error_backtraces=0. These close the confirmed session and backtrace paths, not the others.
  • If the app doesn’t use multipart form data at all, block such requests at the frontend before they reach PHP.
  • A WAF (Cloudflare pushed default rules to all plans, free included) is a stopgap, not a patch — attackers were already mutating payloads to slip past rules.

3. Hunt for signs you were hit. Because the bug lives below the application layer, your app logs may show little. Look for:

  • POST requests containing the PHP_SESSION_UPLOAD_PROGRESS field, especially unusual or repeated multipart uploads.
  • Unusually large or malformed multipart payloads, high field/part counts, followed by memory-limit errors or repeated HTTP 500s.
  • Unexpectedly large or malformed PHPSESSID values in Set-Cookie response headers (the memory-disclosure tell).
  • Unexplained PHP worker crashes or churn.
  • On the host: unexpected child processes, new files in the web root, modified application files, new cron jobs, and unusual outbound connections.

And because memory disclosure was in scope, assume secrets may have leaked: rotate credentials, tokens, API keys and certificates that the PHP process could reach if you find any sign of exploitation.

The bigger picture: when exploitation gets cheap

Strip away the engine internals and PHP2Shell is a story about economics. The bug was always there. Stefan Esser circled the memory-limit RCE idea back in 2004. A 2020 report (#78876) flagged the out-of-memory behavior. A 2024 OSTIF/Quarkslab audit rediscovered it. Each time it went unpatched for the same reason: triggering the crash was easy, but turning it into reliable code execution demanded chaining multiple bugs and adapting to each target’s exact memory layout — the kind of painstaking, expert, non-portable work that doesn’t scale. “Theoretically exploitable” was a comfortable place to leave it.

Frontier AI dissolved that comfort. The research teams used models — Wiz names Gemini 4 Argon and GPT-5.6 Sol; Calif worked with an LLM companion — to analyze allocator behavior, generalize the bug pattern into a reusable hunting heuristic, and adapt working exploits across different PHP versions, configurations and memory layouts. Wiz says a single researcher did in about a week what would previously have taken a world-class team months. Calif’s author is careful to add the caveat that matters: the models didn’t find the bug from a cold prompt. Deep target knowledge — knowing PHP’s vulnerability history and where to look — did the steering; the AI made the grinding, scaling work of exploitation cheap.

That cuts both ways, and it’s the real lesson. Amazon’s write-up spells out the uncomfortable half: the window between a patch and a working exploit is collapsing, because turning a patch diff into an exploit is now something many more actors can do in hours, not something a handful of specialists do over weeks. A 7- or 30-day patch SLA assumes a delay that no longer exists for critical internet-facing bugs. The hopeful half is that the same capability lets defenders find and kill decade-old flaws that survived years of human review. PHP2Shell is both of those at once — and a preview of a lot more to come.

The PHP Foundation notes AI has already surfaced an unusual number of PHP vulnerabilities, with 15 new CVEs since 1 July 2026. If you maintain anything built on a widely used runtime, the takeaway isn’t “panic” — it’s “shorten your patch cycle and assume the attackers have the same tools you do.”

Leave a Comment

Collaborations & press enquiries: s-foot-eb5843@nomadscrolls.net · s-foot-6abde7@nomadscrolls.net · s-foot-99d666@nomadscrolls.net · full contact directory