Why this is a special topic, not a numbered class
Everything in PHP2Shell is a bug class you already know from this course — just inside the runtime behind roughly a fifth of the public web, with about 22.6 million internet-exposed instances potentially vulnerable. To follow the root cause you need two ideas we cover in Week 11:
- Use-after-free — reusing memory after it has been freed (the heart of CVE-2026-109619)
- Heap grooming — arranging allocations so attacker bytes land in a reclaimed slot
The two vulnerabilities
CVE-2026-109618 RCE
RCE via OOM corruption. Out-of-memory handling can interrupt PHP while request data is only partially built. Request cleanup then treats that invalid data as valid.
CVE-2026-109619 RCE
RCE via stale global state. Request-owned state outlives the memory it points to; a later request on the same worker follows the dangling pointer before init resets it.
Root cause 1 — OOM corruption (CVE-2026-109618)
When PHP hits a fatal error — most importantly running out of memory against memory_limit — it does not unwind cleanly. It calls zend_bailout(), which is a longjmp(): a jump across functions straight to request shutdown, skipping any local cleanup on the way out.
- Non-atomic updatePHP bumps a counter or length before the new entry is fully written into a core structure.
- OOM in the gapAn allocation inside that window fails; zend_bailout() longjmps out. The entry is never finished, but the count already says it exists.
- Shutdown trusts itTeardown walks the structure and treats the half-built slot as a valid object — access of an uninitialized pointer.
- Attacker-controlled bytesWith heap grooming the uninitialized slot holds attacker data → a corruption primitive → RCE as the web-server user.
Root cause 2 — use-after-free across requests (CVE-2026-109619)
The advisory frames it as producers and consumers of stale request-owned state. A fatal error creates the dangling pointer; an operation early in a later request dereferences it.
Producers (leave bad state)
A fatal error abandons an operation before its reset runs: parse/compile failure, failed require, uncaught Error, out-of-memory, or "Maximum execution time exceeded." Leftover error / session / object / extension globals now point to freed memory.
Consumers (use it too early)
Anything that runs before normal request init resets those globals: any INI diagnostic (even a suppressed warning), mod_php .htaccess php_value directives, or session upload-progress — which is on by default and needs no diagnostic at all.
Confirmed instances (from the PHP advisories and Wiz Research):
| Internal symbol | What goes wrong | Result |
|---|---|---|
| EG(last_fatal_error_backtrace) | PHP 8.5 keeps the last fatal error's backtrace in an executor global; OOM frees the heap but it is never reset. | basis of the confirmed mod_php RCE |
| PS(id) / upload-progress | RFC 1867 parsing starts session state early and sets PS(id) before module init; OOM frees the heap while it still points in. | leaks memory into the PHPSESSID cookie |
| BG(unserialize) | Upload-progress decoding leaves unserializer bookkeeping published after a failed allocation destroys the heap. | stale decoder state → UAF |
| OPcache: CG(map_ptr_last) | Script persistence advances the process-local pointer-map frontier before publishing the shared one. | type confusion across workers |
A teachable primitive: leaking memory into a cookie
The cleanest demo of CVE-2026-109619 needs no full RCE chain — just an information leak, exactly like the GOT leak we used to defeat ASLR:
- PrimeSend a multipart POST that triggers upload-progress; PS(id) points into the request heap.
- FreeForce OOM in the same request; zend_bailout longjmps out, the heap is freed, PS(id) is left dangling.
- ReuseSend a second multipart request to the same worker; cleanup runs before session init resets the pointer.
- LeakPHP trusts the stale pointer + length as a live string and URL-encodes its bytes into the response cookie.
HTTP/1.1 200 OK
Set-Cookie: PHPSESSID=%00%7f%45%4c%46%02%01...
^ freed heap bytes, served straight back to the attacker
How PHP2Shell maps onto what you already know
| Concept we taught | The idea | Where it shows up here |
|---|---|---|
| Buffer overflow (Lab 1–2) | Corrupt memory you should not be able to write | -109618 writes past a half-built structure |
| Uninitialized / stale data | Trusting memory before it is valid | both CVEs: read before init / after free |
| Use-after-free (Week 11) | Free, then reuse the dangling pointer | -109619 is textbook UAF across requests |
| Info leak → ASLR defeat (ret2PLT/GOT) | Leak an address, then aim the exploit | the PS(id) memory disclosure above |
| ret2libc / PLT–GOT | Reuse code already mapped executable | how you escalate the primitive to code execution |
| Heap grooming (Week 11) | Arrange the allocator, overlap freed objects | how attacker bytes land in the stale slot |
The fix and what to do
The patch stops using longjmp() for OOM (a deferred "soft" OOM at the next safe point, plus an immediate _exit() "hard" OOM at a higher threshold) and resets the wider set of request-owned globals before the request heap is released. Updating is the only complete fix.
Fixed versions: 8.2.35 8.3.36 8.4.27 8.5.12. PHP 8.1 and earlier are end-of-life — no official release (minimal source backports only for 7.4 / 8.0 / 8.1).
php_admin_value[memory_limit] = -1 ; apps can't override; removes the OOM trigger
session.upload_progress.enabled = 0 ; removes the default early consumer
fatal_error_backtraces = 0 ; PHP 8.5 only
How the internet reacted (Oct 20–21)
The same event at four altitudes — the disclosure, a national government, regional panic, and the ops reality. Non-English posts translated to English.
Slides and sources
- PHP official security advisory (Oct 21, 2026)
- GHSA-w8hw-w9fx-j79w — RCE via OOM corruption (CVE-2026-109618)
- GHSA-6wmf-4rv5-p3rp — RCE via stale global state (CVE-2026-109619)
- Wiz Research — PHP2Shell technical breakdown
- Unit 42 (Palo Alto) — threat brief & exposure figures
- Tenable — FAQ, exploitation status, timeline
Last updated 2026-10-22 · CSC 472 home