PHP2Shell: when PHP's error exit leaves a mess behind
A PHP worker has been serving requests for six hours without a restart. Request 41,872 runs out of memory halfway through and dies. The worker shrugs and picks up request 41,873. That’s on purpose: it’s how PHP keeps its “fresh process per request” feel inside long-lived workers.
On 21 October 2026 the PHP project shipped fixes for two critical flaws that live in exactly that gap, between “request died” and “next request starts”. They’re CVE-2026-109618 and CVE-2026-109619, and researchers have nicknamed the pair PHP2Shell [1][3][4]. If you only read one line: update to PHP 8.5.12, 8.4.27, 8.3.36 or 8.2.35, or to your vendor’s backport [1][3].
Why now
The fixed releases and both CVEs went public on 21 October 2026, each scored 9.2 on CVSS v4.0 [1][3]. One vendor reports that details leaked first and PHP moved its disclosure forward [4]. As of 30 October the Dutch NCSC tracks 929 products, 217 of them marked vulnerable and 165 fixed [3]. That’s a lot of web infrastructure in an unfinished state, so it’s worth understanding the bug class and not just the patch number.


Leaving through the window
When a request hits an out-of-memory error or a fatal error, PHP aborts it by calling longjmp() to jump back out of whatever C code was running [1]. If you haven’t written C in a while: longjmp() unwinds the stack without running the cleanup code of the functions it skips. Whatever those functions were going to free, reset or initialise just doesn’t happen. It’s pulling the fire alarm and climbing out of the window. Quick, but nobody locks the filing cabinet.
The vendor write-ups describe two different ways that shortcut goes wrong [1][4]:
| CVE-2026-109618 | CVE-2026-109619 | |
|---|---|---|
| Trigger | Out-of-memory (OOM) error | Fatal error |
| What is left behind | Request memory in an uninitialised state, because some contexts did not account for the OOM jump | Global memory in a bad state, still pointing at memory that is freed at request end |
| Who trips over it | Cleanup of a later request reads or branches on that memory | A later request reads or branches on the freed memory |
| Stated outcome | Remote code execution; several OOM strategies can trigger it repeatedly | Remote code execution; multiple attack techniques possible |
Both come down to the same thing: state owned by a request outlives that request. The first request isn’t the one that gets exploited; it’s the setup. The damage lands in a neighbouring request on the same worker.


A toy version
This isn’t PHP source. It’s a toy that shows why skipping cleanup is dangerous:
static char *g_buf; /* global, request-scoped by convention */
void handle_request(void) {
g_buf = request_alloc(4096); /* owned by the request arena */
if (do_work() == OOM)
longjmp(bailout, 1); /* skips the line below */
g_buf = NULL; /* never runs on the error path */
}
/* arena freed at request end; g_buf still holds the old address */Once the arena is released, g_buf points into memory that can be handed out again. If a later request, or its cleanup, reads through that pointer or decides what to do based on what’s there, an attacker who can shape what lives at that address gets a say in control flow. Classic use-after-free. The vendor write-ups describe the read-and-branch step but not the exact exploit chain, and I won’t guess at it [1][4].
What makes this unusual for a PHP bug: there’s no unserialize() of attacker data, no eval, nothing in your application at all. The flaw is in the engine’s request lifecycle. That’s why the advisory says every PHP installation serving web requests is at risk, whatever the SAPI [3].
Who’s exposed
PHP names PHP-FPM, Apache with mod_php, php-cgi, FrankenPHP and LiteSpeed, and rates mod_php highest, because an unauthenticated request can trigger exploitation before any application code runs [3]. Persistent workers are the interesting case: the stale state outlives a single request, so clean application code doesn’t save you.
PHP also lives inside appliances, network devices and management interfaces where you can’t just run apt upgrade. Those need vendor updates, and the NCSC product list is where to look [3].
Patching
Find every PHP runtime you have: containers, base images, appliances, CI runners that serve anything. Check the version, update to a fixed release or vendor backport, restart the workers so old binaries aren’t still loaded, and check again with the same command:
php -v # CLI binary
php-fpm -v # the FPM binary can differ from the CLI
apache2ctl -M 2>/dev/null | grep php # mod_php present?
docker run --rm your-image php -v # do not trust the tag nameFixed upstream releases:
| Branch | Fixed in |
|---|---|
| 8.5 | 8.5.12 |
| 8.4 | 8.4.27 |
| 8.3 | 8.3.36 |
| 8.2 | 8.2.35 |
Upstream no longer supports PHP 8.1 or older. Distributions and LTS vendors may ship backports, and PHP published minimal source backports for 7.4, 8.0 and 8.1 for people who build from source [1][3]. Read your distribution’s changelog: a package version that looks old can still carry the fix.
Do it today for your container images too. An image pinned to a patch tag from before 21 October is exactly the thing that stays vulnerable for months because nobody owns it.
On an end-of-life branch
Two practitioner write-ups, from Intelvue and ComputingForGeeks, cover the branches upstream no longer supports [5][6]. Both come from people who sell or run this work, so check the details against your distribution’s own notes.
First, what the backport is. For 7.4, 8.0 and 8.1, PHP published “minimal mitigation for EOL version” commits. They carry the core fix (“Avoid state corruption via bailout”) but differ from the full fix in three ways [6]. Session upload progress is switched off in the code whatever php.ini says, so the setting can still read On while the feature is dead. The full fix’s memory_hard_limit_percent setting doesn’t exist. And every memory_limit overrun ends the process immediately, so shutdown functions never run; behind PHP-FPM the worker dies and visitors get a 502 or 503 [6].
Intelvue’s five steps [5], with the ComputingForGeeks details folded in [6]:
- Find every PHP version and how it runs.
php -v, but alsols /etc/php/ /opt/remi/andsystemctl list-units '*php*fpm*'. The CLI version is often not the one serving traffic, so check each site’s FPM pool or Apachemod_phpmodule [5]. If an application is encoded with ionCube or its vendor only certifies PHP 8.1, you may have to patch that branch instead of moving up [6]. - Install the patched package for your branch:
apt install --only-upgrade 'php8.1*',dnf upgrade --refresh 'php*',dnf upgrade -y 'php81-*'for a Remi collection, or EasyApache 4 and CustomBuild on cPanel and DirectAdmin. Then restart every pool, and Apache if you usemod_php[5]. Stay with the packager you already use [6]. Red Hat retired the RHEL 9php:8.1and RHEL 8php:8.0streams, so RHEL and Rocky servers on those get no vendor fix and have to move to Remi’s matching stream [6]. Verify by changelog, not version number. Patched builds keep the same version string, and the ini checks answer the same as on a vulnerable build. For examplezgrep CVE-2026-109618 /usr/share/doc/php8.1-fpm/changelog.Debian.gzorrpm -q --changelog php81-php-fpm | head -3[5][6]. On AlmaLinux the changelog is the only proof [6]. - Turn off session upload progress:
session.upload_progress.enabled = Offinphp.ini, and asphp_admin_flagin each FPM pool, then restart. PHP recommends keeping it off until the next release [3][5]. Only progress bars that read from the PHP session stop working; uploads themselves are fine [5]. - Don’t remove the memory limit. Early advice to set
memory_limit = -1to dodge the out-of-memory trigger lets one request eat all the server’s memory. Keep a sensible limit [5]. After patching, setcatch_workers_output = yesin the pool and countexited with code 1lines in the FPM master log for a few days. A pool where that keeps climbing needs a highermemory_limit[5][6]. - If there’s no patched package, shield the server. For PHP 7.3 and older PHP published nothing, though a few packagers patched on their own (DirectAdmin, Freexian ELTS for Debian, Ubuntu Pro); Remi’s 7.2 and 7.3 builds have no fix [6]. Without a package: put the server behind a WAF with PHP2Shell rules in blocking mode, block
multipart/form-dataon routes that don’t take uploads, restrict admin panels to a VPN or allow-listed addresses, and take it offline if none of that is possible [5].
ComputingForGeeks adds that the backport closes PHP2Shell and nothing else, and the next PHP security release may come with no backport at all. Plan the application upgrade now. It also reports that CISA’s alert told organizations on PHP 8.1 or earlier to take PHP systems offline until patched or updated [6].
Upload progress, and stopgaps
On 23 October PHP published a separate informational advisory suggesting session.upload_progress.enabled=0 until the next regular bugfix release, to guard against an unlikely RCE vector tracked as CVE-2026-115560. A full advisory will come with that release. The NCSC repository doesn’t track it separately, so patching alone doesn’t retire it [3].
Where you really can’t patch yet, PHP’s interim advice is to disable session.upload_progress, restart workers, and review the guidance on memory_limit, post_max_size and, on PHP 8.5, fatal_error_backtraces [3]. Both CVEs need an OOM or a fatal error to fire, so limits like these plausibly take away some of the attacker’s options. They’re a speed bump, not a fix. So is a WAF rule: Fastly describes its signatures for structural oddities in request bodies as virtual patching until the real patch lands [1].
What we don’t know yet
I’d rather be honest here than loud.
The affected version range is disputed. Fastly says all of PHP 5 through 8 is exposed [1]. An early leak claimed something similar alongside CVSS 10.0 scores; SOC Prime points out that the official 9.2 contradicts that, and that no official vendor advisory had confirmed the range when they wrote [4]. Upstream fixed releases exist only for 8.2 to 8.5, with minimal backports for 7.4, 8.0 and 8.1 [3]. Treat “every version ever” as a vendor-blog claim, not a fact, and let your distribution’s tracker settle it for your package.
Exploitation is reported, narrowly. Unit 42 says the flaws are being actively exploited against internet-exposed PAN-OS management interfaces. It cites Dream Research, which reports attackers chaining PHP2Shell with a Linux kernel privilege-escalation bug to get root, and says it independently verified the same attacker’s tooling [2]. The same brief retracts an earlier claim: post-compromise activity at a European government agency was first attributed to PHP2Shell, but Unit 42 now can’t confirm that initial access involved these CVEs [2]. So exploitation is reported for one product family, not established for PHP deployments in general. The NCSC tells operators to watch for public exploit activity [3].
And there’s no public exploit chain. The advisories describe the bug class but not a reproducible chain. Anything past that is speculation, mine included.
The Unit 42 brief was last updated on 29 October [2]. Read it together with the NCSC list for the current status; this article can’t see what changed after it was written.
Detection, while signatures are thin
With no public indicators, behaviour is the best signal you have. SOC Prime suggests watching for PHP or web server processes unexpectedly spawning shells, interpreters or download tools [4]. That’s ordinary web-shell hygiene, worth having anyway: php-fpm, apache2 or httpd as the parent of sh, bash, curl or wget. Writes by the web user into the webroot or temp directories, followed by execution.
Also look for bursts of worker crashes, OOM kills or fatal errors in the PHP log from a single client shortly before an odd process spawn. That order of events would fit the bug class (fail once to prime the state, then follow up), but to be clear, no source describes it as an observed attack pattern.
“Fresh state per request” sounds like a property PHP just has. It isn’t. Someone has to maintain it by hand, on every error path, forever, and this time two paths slipped.
Share this article
Sources
- Fastly: Critical RCE vulnerabilities in Core PHP, CVE-2026-109618 and CVE-2026-109619123456789
- Unit 42: Threat Brief on CVE-2026-109618 and CVE-2026-109619123
- NCSC-NL php2shell: operational information12345678910111213
- SOC Prime: CVE-2026-109618 and CVE-2026-109619 analysis123456
- Intelvue: PHP2Shell fix for end-of-life PHP, step by step12345678910
- ComputingForGeeks: PHP2Shell on end-of-life PHP 8.1, 8.0 and 7.4 (WHMCS, ionCube)123456789101112