PHP's php2shell RCEs Break the Runtime Before Your Code Runs: Patch, Restart, and Don't Trust the Copy-Paste PoCs
Don't Blindly Copy Online php2shell Reproduction Scripts: Where the PHP Kernel Actually Broke This Time — 4 Truths (2026)
📖 Summary: On October 21, 2026, PHP officially issued urgent releases 8.2.35 / 8.3.36 / 8.4.27 / 8.5.12 to fix the CVSS 9.2 php2shell vulnerabilities (CVE-2026-109618 / 109619). This article clarifies the memory corruption root cause from longjmp, teaches you how to determine real reachability in 30 minutes, how to upgrade and detect, and corrects the false reproduction conclusions circulating online.
🏷️ Keywords: php2shell, PHP security, remote code execution, memory corruption, incident response
Table of Contents
- 48 Hours: A Fire Lit Early
- Root cause: longjmp skips C stack unwinding
- Who is most at risk: SAPI and configuration reachability
- Hands-on: self-check and remediation in 30 minutes
- Detection: how to tell if you have been hit
- Debunking: where the online reproduction conclusions are wrong
- Three long-term lessons from this incident
- Summary
48 Hours: A Fire Lit Early
On October 20, 2026, the php.net homepage displayed four lines: Upcoming Critical PHP Security Release, saying that fixes for two critical vulnerabilities would be released the next day at 14:00 UTC. A four-line announcement could not sustain a weekend of hype; what really made it explode was: details leaked before the announcement.
The vulnerability is codenamed php2shell, with two CVEs: CVE-2026-109618 (OOM memory corruption) and CVE-2026-109619 (cross-request stale global state), both rated CVSS 4.0 9.2. They require no account, no session, and no vulnerable application code anywhere on the target site.
From leak to million-level attacks: the timeline
Putting the public disclosure timeline together shows a very tight curve:
| Time (UTC) | Event |
|---|---|
| 2026-08-11 | Calif (Ryan) and OpenAI independently report the OOM → RCE chain; Wiz and OpenAI later add new attack vectors |
| 2026-10-17 | Amazon MadPot honeypot records the first exploitation attempt, sourced from commercial anonymized exit IPs (4 days before disclosure) |
| 2026-10-20 | PHP publishes the advance notice; the same day CVE numbers, session.upload_progress, and fragments spread on social media and HN, forcing the official release to accelerate |
| around 2026-10-21 02:00 | The earliest public PoC appears, about 12 hours before the embargo lifted |
| 2026-10-21 13:30–14:00 | Official releases 8.5.12 / 8.4.27 / 8.3.36 / 8.2.35, plus minimal source backports for 7.4 / 8.0 / 8.1 |
| 2026-10-21 daytime | UK NCSC issues an alert; Dutch NCSC and the Port of Rotterdam, Overheid.nl, KNVB, multiple hospitals, and several political party websites temporarily go offline; Finland shuts down some government public services; the UK Office for Budget Responsibility website goes down |
| 2026-10-21 evening | Cloudflare CEO publishes attack volume: tens of thousands within 45 minutes after the announcement, hundreds of thousands within 2 hours, then millions |
| 2026-10-22 | Singapore CSA announces "threat is imminent"; South Korea's Ministry of Science and ICT raises the national cyber alert to "caution"; China's CNVD, MIIT NVDB, Tencent Cloud, Alibaba Cloud, and Qi'anxin issue advisories one after another |
A few orders of magnitude for context, to explain why governments chose "take it offline first":
- In W3Techs' metrics, PHP covers about 69%–77% of known server-side language sites;
- VirusTotal's metric is 5.1 million+ PHP hosts exposed to the public internet, of which about 3.8 million have at least one port open;
- What is affected is not just "PHP code you wrote", but the entire runtime foundation underneath WordPress, Drupal, Joomla, MediaWiki, Magento, Laravel, Symfony, WHMCS, Nextcloud, phpMyAdmin, Roundcube, and OpenCart.
⚠️ Key point: this is not "a plugin has a hole"; the host language itself has a hole. Upgrading all your WordPress plugins, auditing all themes, and using strong admin passwords adds zero score against these two CVEs.
Why this is not the same magnitude as "some CMS has a vulnerability"
The difference is in three "unrelated to business" properties:
- Unrelated to code quality. Request parsing happens before extension initialization and before script compilation. That is, the vulnerability is triggered before the first line of your
index.phpexecutes. No matter how clean your code is, its exposure is exactly the same as a messy legacy site. - Unrelated to framework. Laravel middleware, Symfony firewalls, and WordPress's
wp_die()all take effect only in the script phase, while this bug is in the SAPI layer. - Unrelated to authentication. A single anonymous POST can hit it; there is no "requires login" downgrade condition.
CrowdStrike's public test conclusion: one anonymous POST request can make an Apache mod_php worker spawn a shell process and execute arbitrary commands, reliably consistent across x86-64 / ARM, still reliable after worker restart, and on ARM hosts it can bypass ASLR using only HTTP requests.
The worst part is how "quiet" it is: the worker does not crash, HTTP returns 200, and the application logs contain nothing, because the application code never ran.
Root cause: longjmp skips C stack unwinding
To understand these two bugs, you first have to accept a counterintuitive fact: PHP's way of handling "out of memory" is not returning an error; it is jumping away directly.
What PHP actually does when it hits a fatal error
The terminal path for PHP fatal errors is zend_bailout():
// Zend/zend.c (official public source, excerpt)
ZEND_API ZEND_COLD ZEND_NORETURN void _zend_bailout(const char *filename, uint32_t lineno)
{
...
CG(unclean_shutdown) = 1; // only leaves a "this shutdown was not clean" marker
...
LONGJMP(*EG(bailout), FAILURE); // jump directly back to the outer setjmp point
}
LONGJMP is C's "non-local jump". It does not unwind the stack: all C function frames between the error point and zend_catch are discarded directly; destructors do not run, refcounts are not rolled back, cleanup functions are never called, and nothing records "this data structure was only half-built".
After jumping back, PHP continues its normal request cleanup flow (php_request_shutdown()), which assumes all structures are in a consistent state. The two CVEs are about the two places where that assumption breaks.
Also remember two components:
HashTable: the underlying structure for all arrays in PHP, including the$_POSTsuperglobal;- Zend MM: PHP's per-request allocator, sitting between PHP and the OS. Small allocations go through free lists bucketed by block size, strictly LIFO, and reclaimed memory blocks are never zeroed; this is the physical basis for "stale bytes can be controlled by the attacker".
CVE-2026-109618: accounting first, allocation second
The problem is in _zend_hash_str_add_or_update_i(), the low-level implementation of all string-key insertions:
// Zend/zend_hash.c (official public source, excerpt)
add_to_hash:
idx = ht->nNumUsed++; // ① accounting first: the table "has one more element"
ht->nNumOfElements++;
p = ht->arData + idx;
p->key = key = zend_string_init(str, len, ...); // ② allocation second: this step can OOM
The order is key. ① has already incremented nNumUsed, and only then ② allocates a copy of the key. If ② happens to hit memory_limit, zend_bailout() capsizes the request, and this bucket (ht->arData[idx]) is never filled in: its value, hash, and key pointer are all stale heap data left by the allocator.
Then the cleanup flow steps in: zend_array_destroy() faithfully iterates over nNumUsed buckets and treats this "ghost entry" as a normal zval; i_zval_ptr_dtor() dereferences the pointers inside it, decrements reference counts at the pointed-to addresses, and even when the count reaches zero, indexes into a function pointer table using the type tag in the stale bytes and dispatches a call.
The attacker needs to control two things:
- Control "where OOM happens". Send a URL-encoded POST with field names in the form
ZZZZ...ZZZZ[x]=1as an extra-long nested structure;php_register_variable_ex()inserts the outer name as a new array key; the key length is directly controlled by the attacker, tuned to just crossmemory_limit, so the bailout lands exactly on the key-allocation line. - Control "what the stale bytes are". Zend MM's size-class + LIFO reuse behavior is deterministic enough. An earlier field in the same request has its value copied, decoded, and immediately freed by PHP, so the freed content happens to occupy the block that is later "claimed". The ghost entry then becomes a forged structure completely defined by the attacker.
💡 One-sentence summary of 109618: the table says "I have one more element", and the bytes of that element are garbage left over from a previous allocation, and the cleanup flow believes it.
CVE-2026-109619: cross-request zombie globals
109619 is the same mechanism, but the damage lands on process-level global variables, pushing the exploitation window to the next request.
The interrupted operation caches state in module-level globals (error state, session identity and handler state, serialization context, upload progress state, etc.), and relies on reaching its own cleanup to clear them. When a fatal error jumps away, teardown is skipped, the globals still point at per-request heap, and nested counters still claim "an operation is in progress".
When the request closes, the entire per-request heap is freed, but the globals belong to the process, not the request, so they survive. PHP is now holding module-level pointers to freed memory.
Two factors make this zombie state both reachable and controllable:
- Worker reuse: in resident-worker deployments like Apache mod_php, the same process handles the next request and reads the stale globals as-is;
- Startup order:
php_request_startup()parses request variables before running extensions' request-init callbacks. So multipart upload progress processing consumes session / serialization code before the extensions that are supposed to reset those globals get a chance to act.
So 109619 is two-phase: request one acts as "producer" and poisons the worker; request two acts as "consumer" and triggers reads/writes of freed memory. The trigger entry point is not limited to memory exhaustion; execution timeout and client disconnect also go through the same zend_bailout() path.
Who is most at risk: SAPI and configuration reachability
Worker reuse is the amplifier
The official statement that mod_php is the highest risk is not casual:
| Dimension | Apache mod_php |
PHP-FPM |
|---|---|---|
| Worker reuse | The same process handles requests consecutively; cross-request exploitation is naturally feasible (the core exposure surface of 109619) | Workers that fail during startup are killed; but workers interrupted after entering the script phase survive and are equally exposed |
| Memory layout stability | In prefork, all workers fork from the same parent process, so the layout remains stable after restart and payloads can be refined repeatedly at zero cost | Relatively unstable |
| Official judgment | Highest risk; a single anonymous request can bypass all application code | Partially mitigated, by no means immune |
Full list of affected SAPIs: PHP-FPM, Apache mod_php, php-cgi, FrankenPHP, LiteSpeed. Any PHP installation that "provides external web request handling" is in scope, including the image you pulled with docker pull php:8.1-apache.
The memory_limit / post_max_size ratio
This is the easiest and most overlooked check. For the attacker to cause "exactly exhaust memory while parsing the request", the request body must be able to chew through memory_limit without exceeding post_max_size. The empirical criterion is:
Reachable condition ≈ memory_limit < 4 × post_max_size
| Configuration combination | Reachable? | Explanation |
|---|---|---|
post_max_size=8M / memory_limit=128M |
Default combination, URL-encoded path not reachable | But the multipart path still depends on field count and nesting depth |
post_max_size=64M / memory_limit=128M |
Reachable | Common in businesses that "accept large attachments" |
post_max_size=100M / memory_limit=256M |
Reachable | Common in backend imports and media sites |
post_max_size=2M / memory_limit=32M |
Not reachable | Common on small sites, risk significantly reduced |
⚠️ Note:
post_max_sizecan be set per directory in.htaccessor in virtual hosts. On shared hosting, a single site can turn itself into a vulnerable configuration, so "our default image is safe" is not true.
Hands-on: self-check and remediation in 30 minutes
Inventory: the "invisible PHP" instances
Do not rush into apt upgrade first. The real pitfalls are not the PHP you installed yourself, but the PHP someone else installed for you.
# 1) Find all PHP executables (including ones that bypassed the package manager)
for p in /usr/bin/php* /usr/local/bin/php* /opt/*/bin/php*; do
[ -x "$p" ] && printf "%-40s %s\n" "$p" "$("$p" -r 'echo PHP_VERSION;' 2>/dev/null)"
done
# 2) Among processes listening / serving externally, which are PHP-family
sudo lsof -nP -iTCP -sTCP:LISTEN | grep -Ei 'php|httpd|apache|nginx|litespeed|frankenphp'
# 3) Container images (for Podman, replace docker with podman)
docker ps --format '{{.Names}}\t{{.Image}}' | grep -Ei 'php|wordpress|nextcloud|matomo|whmcs'
# 4) Products with panels / bundled runtimes, list separately for manual review
# Common forms: control panels, ticket systems, e-commerce SaaS, monitoring probes, router/NAS web management pages
Write the inventory result directly into a table, to avoid "I thought there were only two machines":
| Asset type | Examples | Common misses |
|---|---|---|
| Self-hosted sites | Business backends, official sites | post_max_size changed per directory in .htaccess |
| CMS | WordPress / Drupal / Joomla | Plugin-bundled composer dependencies pin PHP versions and cannot be upgraded |
| Panels / tools | Various ops panels, ticketing, monitoring | Bundled PHP runtime that does not go through the system package manager |
| Containers | php:8.x-fpm, php:8.x-apache |
Image built long ago, base layer not rebuilt |
| Devices / appliances | Firewalls, NAS, print servers | Vendor firmware embeds PHP and never tells you |
Determine whether your instance is actually reachable
A one-liner on a single machine gets the key four items:
php -v | head -1
php -i | grep -E '^(memory_limit|post_max_size|upload_max_filesize|session\.upload_progress\.enabled|Loaded Configuration File)'
Check against a one-line table:
| PHP branch | Highest affected version | Fixed version |
|---|---|---|
| 8.5.x | ≤ 8.5.11 | 8.5.12 |
| 8.4.x | ≤ 8.4.26 | 8.4.27 |
| 8.3.x | ≤ 8.3.35 | 8.3.36 |
| 8.2.x | ≤ 8.2.34 | 8.2.35 |
| 8.1 and earlier | All | No new official releases; only source-level backports for 7.4 / 8.0 / 8.1 |
💡 Side note: the
8.2branch reaches EOL on 2026-12-31. Projects still on 8.2 should put "upgrade to 8.3/8.4" into their December plan after this fix, rather than being hit again next year.
Upgrade: why workers must be restarted
The official announcement includes a sentence many people skim over: after updating, restart services so running processes load the fixed code.
The reason is straightforward: PHP-FPM master processes, Apache prefork workers, and OPcache shared memory all "keep running old code while the process is alive". After you apt install, the new version is on disk, but the old Zend is still what is running in memory.
# Debian / Ubuntu family
sudo apt update && sudo apt install --only-upgrade php8.4-fpm php8.4-cli php8.4-common
# RHEL / Rocky / Alma family (the third-party remi repo had no update on release day; official advice is to switch back to distro packages or compile yourself)
sudo dnf upgrade --refresh 'php*'
# Key: restart, not just reload (reload does not rebuild worker memory layout)
sudo systemctl restart php8.4-fpm
sudo systemctl restart apache2 # machines using mod_php must restart httpd/apache2
# Containers: recreate, not restart
docker compose up -d --force-recreate php
# Verify: confirm the running version, not the on-disk version
php -v | head -1
sudo lsof -p "$(pgrep -o php-fpm)" | grep -E 'libphp|php$' | head
⚠️ Machines using
mod_phphave the highest priority. If one machine runs bothphp-fpmandmod_php(common in legacy leftovers), restart both; do not restart only the one you remember.
Mitigations: temporary measures when you cannot patch
The official wording is restrained and honest: these measures "only reduce the exposure of specific attack paths; they cannot make an installation safe". In other words, they buy time; they do not fix the problem.
For CVE-2026-109618 (OOM corruption):
; php.ini or FPM pool configuration
; 1) Turn off the controllable memory-limit failure (must prevent the app from overriding it)
; FPM: php_admin_value[memory_limit] = -1
; Apache: php_admin_value memory_limit -1
; And check every pool / <Directory> / .htaccess to see whether a finite value has been set back
memory_limit = -1
; 2) Lower the request body limit (8M if no uploads needed, 32M if uploads are needed)
post_max_size = 8M
upload_max_filesize = 6M ; must be less than post_max_size
At the LiteSpeed / Nginx / reverse proxy layer, add limits on "decoded request size, field count, multipart segment count, header count, nesting depth", and cover requests without an available Content-Length (chunked transfer gets missed otherwise):
# nginx-side example: limit request body and buffers, reducing per-request disposable memory
client_max_body_size 8m;
client_body_buffer_size 128k;
large_client_header_buffers 4 8k;
For CVE-2026-109619 (stale globals):
; Must be configured in a place that takes effect "before request parsing":
; php.ini / FPM pool / server configuration
; Do not rely on ini_set() or FPM's .user.ini; their effective timing is too late
session.upload_progress.enabled = 0
; PHP 8.5 only: turn off fatal error backtraces to block the confirmed backtrace path
fatal_error_backtraces = 0
After changing these, every worker must be restarted, otherwise you only changed a config file. Two more things that "configuration cannot solve":
- Clear out deprecated per-request configs that "spit diagnostic information before initialization"; using
error_reporting/display_errors/@to hide messages does not help, because what needs blocking is the act of producing diagnostics itself; - Fix fatal errors and execution timeouts that an attacker can reach (the spots in callbacks, uploads, imports, and cleanup logic).
⚠️ One more counterintuitive trap: do not use
RLIMIT_AS/RLIMIT_DATAas memory guards. They make allocations fail directly, which is exactly what pushes execution back onto the vulnerable allocation path. Use container / cgroup memory limits instead, configured as "only kill the affected worker".
Detection: how to tell if you have been hit
The only high-fidelity signal
A successful exploit leaves almost no trace: the worker does not crash, HTTP returns 200, and application logs are quiet. The only stable product is a generic fatal error in the web server error log:
PHP Fatal error: Allowed memory size of 16777216 bytes exhausted
(tried to allocate 3670048 bytes) in Unknown on line 0
The distinguishing point is all in the last half of the line: in Unknown on line 0.
It means no PHP file was executing at the time: the error occurred before script startup. That separates it cleanly from the large amount of noise from "in-script arrays too large causing OOM". Grep for this:
# Search back from 10-17 (the earliest exploitation attempt recorded by the honeypot)
sudo zgrep -hE 'Allowed memory size of .* exhausted' /var/log/apache2/*error*.gz \
/var/log/nginx/*error*.gz 2>/dev/null \
| grep 'in Unknown on line 0' | sort | uniq -c | sort -rn | head -50
# FPM side
sudo journalctl -u php8.4-fpm --since '2026-10-17' \
| grep -E 'memory size of .* exhausted' | grep 'Unknown on line 0' | wc -l
If the hit count for this rule jumps from 0 to dozens or hundreds after October 17, treat it as "being scanned / exploit attempts".
Post-exploitation traces at process and file level
Logs can only prove "someone is attacking"; proving "they got through" depends on results:
| Layer | What to look for | Command / location |
|---|---|---|
| Process tree | sh, bash, curl, wget, python child processes under httpd / php-fpm / lshttpd |
ps -eo pid,ppid,user,lstart,cmd --forest |
| Files | New .php files under the web root, especially random filenames with content starting with eval/assert/base64_decode |
find /var/www -name '*.php' -newermt '2026-10-17' |
| Scheduled tasks | cron / systemd timers written as www-data |
crontab -u www-data -l, /etc/cron.d/ |
| Credentials | atime changes from .env, wp-config.php, config/database.php being read |
find /var/www -name '.env' -newerat '2026-10-17' |
| Outbound network | Abnormal external connections from web processes | Egress firewall / NetFlow; watch www-data established connections |
💡 Note: criteria like
find -newerare noisy on machines with frequent releases. Use them only on static sites and WordPress sites; for business repositories, a file integrity baseline comparison is more reliable.
Response order after a confirmed compromise
Getting the order wrong loses evidence and misses backdoors:
- Isolate first, then forensics: take down the public entry or put up a maintenance page; do not shut the machine down directly (memory evidence will be lost);
- Keep copies: disk snapshot + the whole
/var/log+ the process list at that time; - Patch and restart workers; close the entry first, otherwise you will be re-compromised after cleanup;
- Rotate all credentials: database passwords, object storage AK/SK, third-party API keys, everything in
.env, and the salts inwp-config.php. This class of vulnerability can read process memory, and those plaintext values are in memory; - Clean backdoors before returning to service: webshells, cron, SSH
authorized_keys, modified.htaccess, injected mu-plugins; - Do the retrospective last: why was this machine's PHP version not on the list.
Debunking: where the online reproduction conclusions are wrong
This section may be the most valuable part of this article. The vulnerability is real, but among the secondhand information around it, the wrong parts spread faster than the right ones.
Claim 1: the root cause is a session.upload_progress zval UAF
This is the most widely circulated claim. Several public PoC repositories locate the root cause as: the rfc1867 upload progress handler caches a zval* into the session HashTable; after an upload stalls longer than session.upload_progress.min_freq, the next FILE_DATA event reinitializes and frees the table, the cached pointer becomes dangling, and the next write to bytes_processed breaks through.
It sounds self-consistent, and it does match the two line numbers in ext/session/session.c for CVE-2026-109618 / 109619. But someone verified the public PoCs one by one on ASAN builds (PHP 8.4.4, 8.5.4, 5.6.40; tried disabling ASLR, poisoning call addresses), and the conclusion was: the UAF/RCE for these two CVEs did not reproduce. Comparing with the official patch diff, the patch actually does three things:
- Rework the OOM bailout path: remove
longjmp()in OOM scenarios and instead trigger a delayed "soft OOM" at the next VM safe point; at a higher threshold, use_exit()for a "hard OOM". This way the shutdown flow no longer sees half-finished state, while preservingmemory_limit's normal capability; - Initialize session state before RINIT;
- Reset state left over from the previous request.
In other words: the root-cause claim is wrong; the attack-surface claim is half right. session.upload_progress is indeed a confirmed consumer path for 109619 (the official announcement itself says so), but it is an amplifier, not the root cause. Treating the amplifier as the root cause has a real consequence: you will think "turning off session.upload_progress.enabled makes me safe", while the official statement says this only blocks the confirmed session paths and does not block other variants.
💡 The simplest way to judge whether a vulnerability analysis is credible: check whether it includes a diff of the official patch. Analyses that only reverse-engineer from a PoC are probably guesses.
Claim 2: CVSS 10.0, only PHP 8 is affected
Both numbers were distorted:
| Rumor | Actual situation |
|---|---|
| CVSS 10.0 | Official CVSS 4.0 is 9.2; some organizations rate it 9.8 under CVSS 3.1; neither is 10.0 |
| Only PHP 8.x is affected | The defective code can be traced back to the PHP 5.4 era; 7.x / 8.x are all in scope; official releases were only made for 8.2–8.5 |
| 8.1 and earlier are also fixed by someone | The official project no longer releases new versions for 8.1 and earlier; only minimal source backports for 7.4 / 8.0 / 8.1; distros and LTS vendors may provide their own backports |
The "only 8.x affected" rumor is the most damaging: it makes a bunch of legacy 7.4 sites give up on checking, and those are precisely the least likely to have a WAF, the least maintained, and the most likely to be used as a springboard.
Claim 3: adding a WAF or changing config equals being fixed
The PHP Foundation's original words deserve to be copied and pasted into your group chat:
Mitigations "reduce the exposure of specific attack paths, but cannot make an installation safe".
Breaking it down:
- WAF is probabilistic. Cloudflare itself added rules about 9 hours before public disclosure, and attack volume still went from tens of thousands to millions after the announcement, which shows rules can block a batch of known patterns but not mutations. Self-hosted WAFs often only cover the HTTP layer, while this chain's payload is "a legitimate POST structure + carefully tuned field lengths", which looks perfectly normal.
memory_limit=-1is a double-edged sword. It does remove the "controllable memory-limit failure", but your machine's available memory becomes the attacker's denial-of-service budget. You must add a cgroup limit as a safety net.session.upload_progress.enabled=0only blocks one path. The official statement says it "blocks the confirmed session paths but not other variants".- Changing config without restarting = not changing it. This cannot be repeated three times too often.
Claim 4: upgrading the kernel is all you need
This is the most insidious one. The kernel patch fixes PHP's own memory corruption, but application-level attack chains do not disappear because the kernel is upgraded.
A verified chain that still works: session.upload_progress writes the uploaded filename verbatim into the session file (no sanitization), and the multipart handler accepts PHPSESSID from the POST body, bypassing session.use_only_cookies. If the target application itself has an LFI, the session file can be included to execute injected code. This combination succeeds with cleanup=0; with the default cleanup=1 race it succeeded 3/3 times; with enabled=0 it still succeeds by switching to "include the uploaded temporary file".
The conclusion needs to be split into two layers:
- The kernel CVEs are real, and upgrading is the only complete fix;
- But "the application layer has an LFI" is itself an independent high-severity problem, and it does not disappear because you upgraded PHP to 8.5.12.
So the correct action is two tracks in parallel: upgrade the kernel today, and this week, audit sinks like include($_GET[...]).
Three long-term lessons from this incident
A shadow PHP asset inventory
What made governments scramble this time was not "our main site uses PHP", but no one could answer within an hour "how many PHP instances do we actually have?". Panels, ticket systems, monitoring probes, NAS management pages, printer web consoles, vendor appliances; they all run PHP and are not in the CMDB.
One workable practice: treat the "runtime inventory" as an independent asset, not as an attachment under an application. The minimum fields are four: runtime + exact version + external reachability + owner. Teams that could filter out all php < 8.2.35 within an hour this time will get to lose one less all-nighter next time.
In the age of AI vulnerability discovery, the patch window is closing
This timeline is worth recording separately: this OOM flaw was found by someone in 2020, rediscovered in a 2024 PHP audit, and was not fixed because exploitability had not been demonstrated.
For more than ten years it just lay there, because "from OOM to RCE" requires a chain that human researchers found very hard to walk by hand. Frontier models walked that chain. Wiz's statement is that AI "significantly lowers the bar for turning a long-standing flaw into a working exploit"; an American cloud security company CTO's comment is more direct: this will not be the last time; AI will keep digging out similar problems in widely used software.
For those of us writing business code, the practical meaning is one thing: "this bug is too complex, nobody should be able to exploit it" is no longer an acceptable scheduling reason. The moat of complexity is being filled in by compute.
Put "restart required" into the change process
This time a number of teams will go through a very unpleasant scenario: the patch is installed, the version number is correct, and they are still compromised, because the old Zend was still running in memory.
This is not a new problem, but it has the most severe consequences in "language runtime-level" security updates. It is recommended to fix one line in the change checklist as:
[ ] Security update applied
[ ] Running processes confirmed to be loading the new version (not just the version number on disk)
[ ] No leftover old workers / old containers / old master
Summary
Compress the whole thing into one table:
| Dimension | Conclusion |
|---|---|
| What it is | Two unauthenticated RCEs in the PHP kernel, codenamed php2shell, CVSS 4.0 = 9.2 |
| IDs | CVE-2026-109618 (OOM memory corruption), CVE-2026-109619 (cross-request stale globals) |
| Root cause | zend_bailout() jumps out with longjmp(), without C stack unwinding; cleanup trusts a structure that was "only half built" |
| Fixed versions | 8.5.12 / 8.4.27 / 8.3.36 / 8.2.35; 7.4 / 8.0 / 8.1 source backports only |
| Most dangerous config | Apache mod_php resident workers + memory_limit < 4 × post_max_size |
| Only complete fix | Upgrade + restart workers |
| Detection signal | Allowed memory size ... exhausted with in Unknown on line 0 |
| Biggest misconception | Treating session.upload_progress as the root cause; thinking WAF / config change / kernel-only upgrade is enough |
If you want to leave one sentence for colleagues still scheduling:
Running
php -vis more important than reading any analysis. These two bugs do not care about frameworks, code quality, or whether you have a login state; they only care about "when you restart".
If this article helped you, feel free to like and bookmark. How did your team inventory this time (did you find hidden PHP in panels or devices)? Tell us in the comments, and leave a clue for people doing post-mortems later.