PHP2Shell Use-After-Free: What Happened When We Added Runtime Protection

Posted on October 23, 2026
Author: Shane Fry

Key Takeaways

  • PHP2Shell exposes critical memory-safety vulnerabilities in PHP’s core engine, including a use-after-free that can be exploited to achieve remote code execution.
  • RunSafe recreated the vulnerability and developed a working exploit that successfully executed in all 10 tests against an unprotected PHP build.
  • With RunSafe Protect’s Load-time Function Randomization (LFR) applied, the same exploit failed in all 10 tests. The attack failed because the instructions it relied on were no longer at their expected memory addresses.
  • The underlying vulnerability remained present, but the tested exploitation technique was disrupted. This demonstrates how runtime protection can complement patching by making memory-corruption exploits harder to execute.

 

 

On October 21, 2026, the PHP project released versions 8.2.35, 8.3.36, 8.4.27, and 8.5.12 to address two critical memory-safety vulnerabilities, tracked as CVE-2026-109618 and CVE-2026-109619 and rated CVSS 9.2. Together, these flaws expose serious weaknesses in how PHP manages internal state when request processing fails unexpectedly, including a remote, unauthenticated path to code execution in one of the most widely deployed pieces of software on the internet.

RunSafe’s engineering team rebuilt the vulnerable version in a lab, wrote a working exploit (because most of the initial public proofs-of-concept released do not actually work), and then measured what RunSafe Protect does against it. 

Against an unprotected build, the exploit succeeded in all ten runs. Against the same PHP build protected with RunSafe’s Load-time Function Randomization (LFR), the exploit failed in all ten.

In this post, we look at how patching closes this specific bug and how RunSafe Protect breaks the exploitation technique that both this bug and the next one depend on.

What the Bug Actually Is

PHP’s session.upload_progress feature decodes session data while a multipart form upload is still being parsed. That decoder runs during request startup, before RINIT, at a point where no user code is supposed to execute.

If the unserialize operation hits a bailout, which is PHP’s longjmp-based escape used for conditions like out-of-memory or an uncaught exception, the cleanup that would normally reset the unserialize bookkeeping is skipped.

The bookkeeping lives in BG(unserialize), part of the basic globals that persist for the lifetime of the worker process rather than for a single request. After the bailout, its internal data pointer references heap memory that has already been freed.

The worker keeps serving requests with a dangling pointer sitting in a long-lived global. This creates a use-after-free vulnerability, a common class of memory-safety vulnerability in software written in languages such as C and C++.

A later request can reclaim that freed heap chunk with a controlled heap spray. Once the attacker controls the memory referenced by the stale pointer, the unserializer’s own logic can turn it into an arbitrary write.

In our exploit, we used that write to overwrite a function pointer PHP calls during multipart handling, giving us control over the instruction pointer.

The next step is turning it into useful code execution.

Where Exploitation Actually Depends on Knowing the Code

Modern binaries typically mark the stack and heap as non-executable, so an attacker cannot simply jump to malicious instructions placed in uploaded data.

Instead, attackers can use a technique called return-oriented programming (ROP). Rather than introducing new executable code, ROP chains together short instruction sequences, called gadgets, that already exist in the program’s executable memory. Each gadget ends in a return, allowing the attacker to stitch multiple sequences together to perform a desired action.

For this technique to work, the attacker must know where those instructions are located.

Every address in a ROP chain is a fixed offset from the module base, computed ahead of time from the on-disk binary. The attacker reads libphp.so, finds a stack-pivot gadget here, a register-loading gadget there, and the address of a function to call. Those addresses are then written into the exploit payload.

The attack depends on the program’s instructions being where the attacker expects them to be.

We built that chain against the unprotected PHP build. It pivots the stack into attacker-controlled upload data, loads registers through a short sequence of gadgets, and lands a real call to a real PHP function with an argument we chose. At this point, we have a reliable exploit payload that worked against PHP 8.2.32.

What RunSafe Protect Changes

Test Results: PHP With and Without RunSafe Protect

What We Measured Unprotected PHP RunSafe Protect
Successful exploit executions 10/10 0/10
Expected stack-pivot instruction Present Replaced by unrelated code
Code at attacker-computed address Matched every run Differed every run
Exploit outcome ROP chain completed Failed at first gadget

Results from RunSafe’s controlled laboratory testing of the PHP use-after-free exploit.

RunSafe Protect applies Load-time Function Randomization (LFR), which changes the locations of functions every time a binary loads.

With RunSafe Protect, the code layout a process runs is different from the layout on disk and different from the layout used the last time the process started. When instructions are no longer at their expected addresses, an attacker’s prepared ROP chain cannot execute as intended.

RunSafe Protect integrates at build time. For the PHP package, we rebuilt the Debian source package with the same arguments as the official build and prepended a single helper to the build command, producing a protected version of PHP.

We ran the ROP exploit against the protected build, with ASLR disabled and every gadget address re-resolved from the protected build’s own binary, so the attacker started from the most generous possible conditions.

The exploit completed zero times out of ten against the RunSafe-protected build.

We repeated the same test with an exploit chain that works with ASLR enabled and saw the same results. Because of RunSafe Protect’s LFR capability, the address the attacker computed for the stack-pivot gadget no longer contained the pivot instruction. It held unrelated code, and execution derailed within a few bytes and faulted before the chain ever started.

We measured the same effect directly by comparing the bytes at an attacker-computed function address against the intended function on disk. On the unprotected build, they matched on every run. On the protected build, they differed on every run, and they differed again on each restart.

What this Means for Teams Running PHP

Apply the patch. PHP 8.2.35 and the corresponding releases close the disclosed vulnerabilities, and as a security company, we still advocate for patching high-severity vulnerabilities quickly.

However, every operations team already knows that patching is a race, and it is a race you do not always win. The gap between disclosure and a fully deployed fix across every host, container image, and vendored dependency can take days to weeks, and the exploitation window opens the moment details are public. In embedded and long-lived systems that bundle a PHP interpreter, an older version may remain deployed well beyond the release of a security update.

PHP2Shell also illustrates why memory-safety vulnerabilities remain such a difficult problem. A flaw can sit in software for years before someone develops a reliable way to exploit it. When that happens, defenders must respond to the specific vulnerability, even if the underlying exploitation techniques are familiar.

RunSafe Protect addresses the exploitation technique rather than the individual flaw, allowing organizations to stop the exploitation of an entire class of vulnerability rather than chasing individual patches.

Randomizing function locations at load time disrupts the predictability that this technique requires. In this case, we did not remove the vulnerability, but we did prevent the tested exploit from successfully reusing the instructions it needed to execute.

Our advice is to patch to close the door you can see. But also harden the binary so the doors you cannot see yet are far harder to walk through.

Organizations need defenses that do not depend on knowing every vulnerability before an attacker does. Load-time Function Randomization is one of those defenses.

Learn more about RunSafe Protect in our Technical Deep Dive. See examples of CWEs that RunSafe Protect stops the exploitation of.

Guide to Creating and Utilizing SBOMs

Latest Blog Posts

What Is a SBOM? Software Bill of Materials Explained

What Is a SBOM? Software Bill of Materials Explained

A Software Bill of Materials (SBOM) is a machine-readable inventory of every component, library, and dependency inside a software application. Think of it as a detailed ingredient list that reveals exactly what's bundled into your code—including the open source...

read more