Skip to content
ALB Wiki

The Elephant's Glass Jaw: Architecture and Fallout of the PHP2Shell RCE Collapse

This is an old revision of this page — revision 1 (2026-10-22 01:18:16 UTC). It may differ from the current revision.

PHP2Shell is the collective name for two critical remote-code-execution vulnerabilities in the PHP engine — CVE-2026-109618 (out-of-memory corruption) and CVE-2026-109619 (stale global state / use-after-free) — disclosed and patched on 21 October 2026. Both carry a CVSS v4.0 score of 9.2, both permit unauthenticated remote code execution, and both live in code that every PHP web request touches: memory management and fatal-error handling.[1][2]

The blast radius is the story. PHP runs roughly 70% of websites that disclose a server-side language — WordPress, Drupal, Joomla, MediaWiki, and the long tail of everything else — and the vulnerable code path executes before any application code runs. A PHP file that does nothing but print "Hello World" is exploitable.[3] Datadog Security Labs independently reproduced the OOM corruption on PHP 8.5.11 with a single unauthenticated HTTP POST and confirmed that the attacker's command runs during request cleanup — while PHP is trying to tidy up a request that already failed.[4]

The flaws are old. The vulnerable code reached back to PHP 5.4.0, released in 2012 — fourteen years of exposure, sitting in the engine's most-executed paths. What changed on 21 October 2026 is that Wiz Research proved the flaws exploitable, in large part with the help of frontier AI models that compressed months of exploit-development drudgery into hours. Complexity had been the security. It stopped being.

What happened

The disclosure was coordinated and unusually broad. Wiz Research (Sagi Tzadik, Shir Tamari, Nir Ohfeld, Tomer Schwartz) reported the bugs to the PHP security team; the resulting patch touched roughly 300 files across the Zend engine. OpenAI and Calif — both involved in the AI-assisted research — published alongside Wiz, and an industry group including Automattic, Akamai, AWS, Cloudflare, Fastly, Google, Imperva, Microsoft, and Vercel prepared edge mitigations ahead of public release.[5]

PHP shipped four fixed releases the same day — 8.5.12, 8.4.27, 8.3.36, 8.2.35 — and published minimal source backports for the out-of-support 7.4, 8.0, and 8.1 branches.[6] CISA issued an alert within hours, urging inventory, immediate patching of internet-facing systems, and monitoring for PHP workers spawning shells or downloading tools; the UK NCSC issued a parallel warning.[7][8]

Public exploit code appeared the same day — repositories demonstrating multiple attack chains against mod_php and PHP-FPM, including a pre-authentication RCE against Apache's mod_php using a single multipart POST.[9] At the time of the CISA alert, no in-the-wild exploitation had been confirmed, but public working code against 70% of the web is not a situation that stays quiet.

Thirty-one years of PHP

To understand how the web's most-deployed runtime ended up here, you have to look at what PHP is built out of.

Rasmus Lerdorf wrote the first version in 1994–95 as a set of C programs for his personal home page — PHP/FI, the "Personal Home Page / Forms Interpreter," released publicly in June 1995. It was CGI in the literal sense: a C binary the web server invoked fresh for each request. Andi Gutmans and Zeev Suraski rewrote it into PHP 3 (1998) and then built the Zend Engine that has powered every release since — Zend Engine 1 in PHP 4 (2000), Zend Engine 2 in PHP 5 (2004), Zend Engine 3 in PHP 7 (2015). PHP 8.0 (2020) added a JIT; 8.4 and 8.5 continued the line.[10]

The engine has always been C. Hundreds of thousands of lines of reference-counted zval structures, hand-maintained HashTable buckets, per-request memory arenas managed by the Zend Memory Manager — and, at the center of today's CVEs, zend_bailout: a setjmp/longjmp pair that PHP uses to abandon a request on fatal error, including memory-limit exhaustion.

The line was not always smooth. PHP 6 — the ambitious Unicode-native rewrite — collapsed around 2010 and was abandoned outright, which is why version numbers jump from 5.6 to 7.0. Governance stayed thin for years: it took the near-departure of key maintainers and the resulting "bus factor" alarm to produce the PHP Foundation in November 2021 — the first paid, coordinated maintenance structure the language had ever had, twenty-six years in.[11]

Two performance decisions made three decades ago define the blast radius today:

  • Persistent workers. PHP abandoned the CGI model — one fresh process per request, total isolation — because it was too slow. mod_php embeds the interpreter inside Apache; PHP-FPM keeps workers alive across hundreds of requests. That is exactly what makes a cross-request use-after-free possible: there has to be a next request on the same worker for the stale state to poison.[12]
  • Shared engine globals. The engine tracks per-request and per-process state through macro-globals — EG() (executor globals), BG() (basic globals) — which the public exploit chains corrupt to reach function pointers like zif_system.[13]

The security record has been thin for most of that history. PHP underwent its first external security audit only in 2024, funded by the Sovereign Tech Agency through OSTIF and performed by QuarksLab — twenty-nine years after the project's founding.[14] And the specific feature at the center of the exploit chains — session.upload_progress, which fires during request body parsing before application code — was added in PHP 5.4.0 (2012), enabled by default, and barely used by anyone. A forgotten feature in the pre-authentication path is the classic place a bug goes to sleep for a decade.

This is not PHP's first engine-level escape. In June 2024, CVE-2024-4577 — an argument-injection flaw in PHP-CGI on Windows — was exploited in the wild within days of disclosure, including by ransomware operators. The pattern recurs because the architecture that produces it hasn't changed: a C codebase with manual memory discipline, non-local error handling, and a process model that reuses workers indefinitely.[15]

Anatomy of the two flaws

Both CVEs are the same shape of bug: a fatal error interrupts PHP mid-operation, and the engine later treats the incomplete state as valid. They differ in where the interrupted operation happens.

CVE-2026-109618 — the out-of-memory corruption

PHP turns a form submission into $_POST by inserting each field into a HashTable. In the vulnerable versions, the insertion helper _zend_hash_str_add_or_update_i() ran its steps in a fatal order — incrementing the table's bucket counters before allocating the key:

idx = ht->nNumUsed++;
ht->nNumOfElements++;
p = ht->arData + idx;
p->key = key = zend_string_init(str, len, GC_FLAGS(ht) & IS_ARRAY_PERSISTENT);

If that zend_string_init allocation pushes the request over memory_limit, PHP raises a fatal error and zend_bailout's longjmp() abandons the request — leaving the counters already advanced over a bucket whose key and value were never initialized.[16]

The request is dead, but the engine still has to clean it up. When request shutdown destroys the array, it walks every counted bucket — including the phantom — and interprets whatever stale heap bytes it finds there as a PHP value. Datadog demonstrated the full chain: an attacker times the allocation failure, sprays the heap so the unfinished bucket lands on controlled data, and the cleanup routine calls a fabricated destructor pointer — pDestructor overwritten to reach zif_system — executing a command as the web worker.[17]

On Apache with mod_php the OOM trigger is reliable with default settings — an unauthenticated request, no application bug required. On PHP-FPM, RCE was confirmed through application-level code paths. The same bugs also yield unauthenticated memory disclosure — reading passwords, session tokens, API keys, and certificates out of the worker's heap, including credentials belonging to other requests. A PHPSESSID-based technique supplies the memory addresses needed to defeat ASLR.[14][18]

CVE-2026-109619 — the stale global state

The second flaw crosses the request boundary. A fatal error mid-request can leave module-level global pointers — engine state expected to be per-process — still aimed at per-request heap that request shutdown has already freed. The next request handled by the same PHP-FPM worker touches those stale references before normal initialization resets them: a textbook use-after-free, unauthenticated, carrying over from one anonymous visitor to the next.[19][20]

Wiz grouped four exploitable bug instances under the two CVEs: an upload-progress unserializer use-after-free and a remote memory-disclosure under 109618; a fatal-error backtrace use-after-free and an OPcache type-confusion under 109619. Demonstrated results: pre-authentication RCE on current WordPress, Drupal, and Joomla container images, plus application-agnostic remote memory read. Wiz also noted there are other tracked and likely untracked variants of the pattern in the codebase — the patch addresses the root mechanism, not just the four instances.[21]

The confirmed trigger vector for the stale-state path is session.upload_progress — the 2012 feature nobody uses, firing inside the RFC 1867 body parser, before any PHP application code has run.[22]

A note on exploitability, because precision matters: these are not point-and-shoot bugs. The OOM chain requires the attacker to calibrate request size against the target's memory_limit, keep the worker alive through the failed request, and defeat ASLR with a memory-disclosure step — the PHPSESSID technique — before the destructor call lands. Datadog documented each requirement separately, and the public PoC's own documentation is frank that the remote chain is version- and configuration-specific. "Exploitable" and "automatically exploitable everywhere" are different claims; the first is proven, the second is not — which is the difference between "patch today" and "the web is already down."[23][24]

Why this was allowed to exist

The architectural critique writes itself once you list the preconditions. Every element of the bug is a design decision:

longjmp() across initialization boundaries. C's non-local jump abandons a stack mid-construction. Everything between "operation started" and "operation complete" — half-incremented counters, half-copied strings, half-built structures — is left exactly where the jump found it, for the cleanup path to trip over. PHP uses this mechanism for every fatal error and every OOM event in every request.

Count-before-allocate ordering. The nNumUsed++ before zend_string_init is a two-line ordering bug that survived fourteen years of code review because the failure mode it creates is invisible until you make allocation fail at that line — which is precisely what an attacker controlling request size can arrange on demand.

Destructor chains over partially-initialized state. Cleanup iterates counted buckets and trusts the count. The count is a claim about memory that was never allocated. The destructor call that follows is the RCE.

The persistent-worker boundary. A per-request heap living inside a process that outlives the request is the shared-state boundary both CVEs cross. In 1995, PHP ran one process per request and this bug class could not exist — there was no "next request" to inherit stale state, and a corrupted worker died with the request that corrupted it. Every performance optimization since has widened exactly that boundary.

The pre-authentication parser. session.upload_progress executes during RFC 1867 body parsing — before the application, before authentication, before any defensive code a developer might write. Application-level security cannot reach the vulnerable path; Datadog confirmed this independently.[25]

The audit gap. A codebase serving 70% of the web received its first external security audit in 2024. The bugs survived not because they were hidden — they were in the most-executed code in the engine — but because turning them into an exploit required chaining heap-spray timing, ASLR defeat, and destructor fabrication, and nobody had done the work.

The AI inflection point

That last point is where 2026 differs from every PHP CVE before it. Wiz is explicit about the method: the vulnerable code sat there for over a decade, but demonstrating exploitability required "an exceptionally complex chain of steps, at the edge of what human researchers could practically achieve." Frontier AI models — with the work coordinated across Wiz, OpenAI, and Calif — reduced exploit development from a months-long manual grind to hours per adaptation.[26]

This is the first widely-documented case of LLM-assisted research producing a critical, internet-scale memory-corruption exploit — and both sides of the coin landed on the same day. The good news version: AI found a fourteen-year-old flaw in code humans had audited and missed, and a coordinated industry patched it before confirmed exploitation. The other version, which Wiz states plainly: "complexity may no longer provide a meaningful barrier." Every latent memory-corruption flaw in every large C codebase — and the web runs on several of them — just moved closer to the practical-exploit threshold. The same capability that found this bug is available to people who would not report it.[27]

The honest reading of October 21 is not "PHP got unlucky." It is that the economic model of vulnerability research inverted, on a codebase whose defense was partly that nobody could afford to attack it this way.

Could it have been avoided?

Three honest answers, at three different levels.

At the code level — yes, trivially. Swap two lines: allocate the key, then count the bucket. The 109618 variant dies. Initialize-on-failure or explicit "dirty bucket" flags kill the cleanup path. These are the kinds of bugs a rigorous static-analysis or bounded-model check flags mechanically — which is why the 2024 QuarksLab audit and today's strengthened pipeline matter more than any single patch.[28]

At the design level — partially. Process-per-request (the CGI model PHP was born with) prevents the cross-request half of the bug class by construction; php-fpm's pm.max_requests worker recycling bounds the damage window but doesn't close it. Failing a whole worker on fatal error rather than attempting surgical cleanup would trade availability for state cleanliness. Each of these was a known option; each cost performance or compatibility, and the ecosystem chose performance every time for twenty years.

At the language level — structurally. Both CVEs are use-after-free / uninitialized-read patterns (CWE-416 family). In Rust, the borrow checker rejects the code shapes at compile time; in Ada/SPARK, access-type discipline plus proof obligations eliminate the dangling-reference class; in any managed runtime with a GC, the "freed memory reinterpreted as live object" primitive simply isn't available to an attacker. You cannot write _zend_hash_str_add_or_update_i's bug in a language where the count and the allocation are one indivisible operation — which, in a memory-safe language, they are.[29]

The fair counterpoint is the one PHP cannot escape: seventy percent of the web is written in it, the C engine is what it is, and "rewrite it in Rust" is a 30-year project, not a patch. What PHP can do — what it did on October 21 — is fix the mechanism, harden the lifecycle boundary, and fund the audits it skipped for three decades.

The zero-heap counterpoint

This site maintains AdaLogic BASIC, and the comparison is worth one honest paragraph — not because anyone is migrating their WordPress fleet to ALB, but because these CVEs are a clean illustration of what "memory safety" actually costs and buys at the language-design level.

ALB's runtime allocates no per-request heap. State lives in statically-sized structures allocated at initialization; a request — or a tick, or a BBS session — cannot trigger a mid-operation allocation failure because there is no mid-operation allocation. There is no longjmp analogue: error paths are structured, local, and cannot leave counters desynchronized from backing memory, because the two are the same object. A "use-after-free across worker requests" like 109619 isn't a bug ALB has patched; it's a bug the design cannot express — the same property SPARK Ada proves formally and Rust enforces through ownership. See Ada vs Rust Memory Safety for the deeper comparison.

None of that makes ALB a web-scale PHP replacement — the ecosystems aren't comparable and pretending otherwise would be dishonest. The point is narrower and more useful: the classes of failure in today's CVEs are choices, not laws of nature. They were chosen once in 1995 for performance, chosen again every time the audit didn't happen, and re-chosen — correctly, this time, and for the last time in this instance — on October 21, 2026.

What to do right now

Patch. Everything else is a stopgap.

PHP branchAffectedFirst fixed release
8.5≤ 8.5.118.5.12
8.4≤ 8.4.268.4.27
8.3≤ 8.3.358.3.36
8.2≤ 8.2.348.2.35
7.4 / 8.0 / 8.1allsource backports only — check your vendor

All web SAPIs are in scope: mod_php, PHP-FPM, php-cgi, FrankenPHP, LiteSpeed. When building the inventory, count containers and products that bundle their own PHP — the CLI binary's version tells you nothing about what serves the site. After patching, restart or replace every worker still running the old runtime.[30]

If you cannot patch today, the partial mitigations — partial, not protective:

  • For 109618: enforce memory_limit=-1 at the administrator level, paired with request-body limits at the frontend and memory caps on the container. Do not substitute RLIMIT_AS/RLIMIT_DATA — process-level limits trigger exactly the allocation failures this bug feeds on. An attacker who exhausts real system memory can still destabilize the host.[31]
  • For 109619: set session.upload_progress.enabled=0 and, on PHP 8.5, fatal_error_backtraces=0, in configuration loaded before request parsing — then restart every worker. Hiding the errors does not prevent access to stale state; it only removes the known trigger.[32]
  • Put a reverse proxy or WAF in front of PHP — boundary controls that block oversized or malformed request bodies before they reach the vulnerable parser. The coordinated industry partners shipped out-of-box rules today.[33]
  • On PHP 8.1 and older: CISA's guidance is blunt — take PHP-based systems offline until patched or upgraded. Source backports exist for 7.4/8.0/8.1, but they land in vendor packages on vendor time.[7]

Detection: hunt for memory-exhaustion errors during request initialization (before app code), unusually large form submissions, and — the tell — PHP worker processes spawning shells or downloading tools. Successful exploitation doesn't necessarily crash the worker.[34][35]

One honest caveat for after the patch: public PoC documentation describes additional exploitation paths — application-dependent chains through unserialize() sinks, serializer-handler mismatches, and session-injection LFI — reported as still functional on 8.5.12. These are different bug classes needing application cooperation, not the CVEs themselves — but the same codebase, the same architecture, and Wiz's warning about untracked variants should temper any sense that today's patch is the end of it.[36]

The read

Heartbleed (2014) showed what a memory bug in shared infrastructure looks like. Log4Shell (2021) showed what a dependency nobody inventories looks like. PHP2Shell is both at once — a memory-safety failure in infrastructure so shared that a "Hello World" page inherits it, disclosed by the first AI-assisted exploit campaign to hit internet scale.

The elephant's jaw was always glass. The engine is C, the error model is longjmp, the worker is shared, the parser runs before authentication, and the first external audit arrived 29 years in. None of this was secret; all of it was accepted, because the alternative — the rewrite, the migration, the discipline — always cost more than the risk seemed to. Today an AI model repriced that risk for everyone, defenders included.

Patch tonight. Then keep the uncomfortable fact somewhere you'll see it: the language holding up most of the web is written in the one language family where these bugs are always possible, and the difference between "theoretical" and "pre-auth RCE on WordPress" turned out to be a question of who could afford to look hard enough. Now anyone can.

See also

Footnotes

  1. ↑ PHP Foundation, "Today's PHP Security Release, and How We're Strengthening PHP Security", 21 October 2026
  2. ↑ CISA, "Critical Vulnerabilities in PHP Require Immediate Patching", 21 October 2026
  3. ↑ Wiz Research, "PHP2Shell: AI uncovers critical RCE vulnerabilities affecting 70% of the Internet", 21 October 2026
  4. ↑ Datadog Security Labs, "PHP2Shell — Critical vulnerabilities in PHP: detection, analysis, and remediation", 21 October 2026
  5. ↑ Wiz Research, PHP2Shell disclosure
  6. ↑ Datadog Security Labs, remediation and backport details
  7. ↑ CISA alert
  8. ↑ NCSC, "Critical security issue affecting PHP", 21 October 2026
  9. ↑ php2shell public proof-of-concept repository, released 21 October 2026
  10. ↑ PHP: History of PHP and related projects
  11. ↑ PHP: History of PHP
  12. ↑ Datadog Security Labs, CVE-2026-109619 mechanics
  13. ↑ php2shell PoC — attack chain documentation
  14. ↑ PHP Foundation, security release post
  15. ↑ CVE-2024-4577 — PHP-CGI argument injection
  16. ↑ Datadog Security Labs, exploitation walkthrough, stage 1
  17. ↑ Datadog Security Labs, stages 2–3: heap spray to function pointer
  18. ↑ Datadog Security Labs, memory disclosure and PHPSESSID technique
  19. ↑ NCSC, vulnerability descriptions
  20. ↑ Datadog Security Labs, stale-state mechanics
  21. ↑ Wiz Research, vulnerability table and variant note
  22. ↑ php2shell PoC — rfc1867 pre-auth note
  23. ↑ Datadog Security Labs, exploit requirements and limitations
  24. ↑ php2shell PoC — universal? no
  25. ↑ Datadog Security Labs, "application-level authentication can't block an attack"
  26. ↑ Wiz Research, "The AI inflection point"
  27. ↑ Wiz Research, AI inflection point framing
  28. ↑ PHP Foundation, post-release strengthening measures
  29. ↑ Tenable — CVE-2026-109618, CWE-416 classification
  30. ↑ Datadog Security Labs, fixed versions and remediation steps
  31. ↑ PHP / Datadog, temporary mitigation guidance
  32. ↑ Datadog Security Labs, mitigation caveats
  33. ↑ Wiz Research, industry mitigation coordination
  34. ↑ Datadog Security Labs, detection guidance
  35. ↑ CISA monitoring guidance
  36. ↑ php2shell PoC — post-patch path status
Categories: Languages · News · Reference
This page was last edited 2026-10-22 01:18:16 UTC by ALB Team. What links here