PHP2Shell Explained: How a PHP Memory Error Became Remote Code Execution
An illustrated, beginner-friendly case study of CVE-2026–109618 and CVE-2026–109619, with the small browser experiments that helped me understand it.
A security research article led me down a rabbit hole: How can making a web server run out of memory possibly allow someone to execute code on it?
I understood that servers have RAM and applications can crash. What I didn’t understand was the connection between an ordinary out-of-memory error and remote code execution (RCE). So I started at the beginning. I ran a few harmless PHP snippets in a browser-based sandbox, observed memory usage, simulated interrupted operations, and then mapped those concepts back to the PHP2Shell research. Later, a security advisory circulated at work and made the topic feel much less theoretical.
This post is a learning case study, not an original vulnerability discovery or penetration test. I did not attempt the PHP2Shell exploit or probe any employer or third-party infrastructure. The shopping scenario below is fictional, and the PHP sandbox experiments demonstrate concepts rather than the vulnerabilities themselves.
First question: What actually happens when I visit a PHP website?
Imagine buying a T-shirt from a fictional online clothing store that uses PHP on the backend. Starting from the moment I open my laptop:
- Open the laptop and browser. That alone doesn’t necessarily contact the store.
- Enter the store’s URL. The browser sends an HTTP GET request for the homepage. The returned HTML may cause additional requests for images, CSS, fonts, JavaScript and APIs.
- Open a product page. This may generate another GET request.
- Select a size. This might happen entirely in JavaScript or trigger an API request to check inventory.
- Add the product to cart. The browser might send a POST request containing the product ID, size and quantity.
- Open checkout. Another request may fetch a checkout view.
- Place the order. A POST request typically submits the order; the backend validates it, works with storage and may contact a payment processor.
- See confirmation. The response may render confirmation directly or redirect the browser to another page.
Not every click or keystroke makes an HTTP request. Some interactions are entirely local to the browser. A real website may also issue many more background requests than this simple walkthrough.
Where does PHP run? On the company’s server, VM or container — not in the shopper’s browser. PHP executes the server-side application logic and can generate HTML or JSON. The browser renders HTML, runs JavaScript, and uses my laptop’s RAM. The PHP worker separately uses the server’s RAM. HTML does not execute PHP.
If the customer submits name=Sam, PHP might read that data via $_POST['name']. PHP variables use memory, and PHP arrays use internal hash-table structures. But not every variable is a hash-table entry, and a customer's purchase record is not the same thing as PHP's temporary internal memory entry.
The sandbox: I wanted to see PHP use memory
I used an online PHP sandbox because I wasn’t going to install tools or run security experiments on a company-managed laptop.
First, I generated a webpage:
<?php
$name = "Sam";
echo "<h1>Welcome, $name!</h1>";
?>That established the basic boundary: PHP generates output on the server; the browser displays it.
Next, I measured memory while creating arrays:
<?php
$before = memory_get_usage();
$numbers = range(1, 10000);
$after = memory_get_usage();
echo "Extra memory: " . round(($after - $before) / 1024) . " KB";
unset($numbers);
?>Here are the real measurements I observed in my sandbox:
ExperimentBeforeAfterAfter unset()10,000 numbers393 KB653 KB393 KB20,000 numbers393 KB909 KB393 KB
The larger array used more memory. The unset() released the reference to the array; the reported usage returned to the starting value in this run. This did not mean all process memory was returned to the operating system. PHP's memory allocator can retain memory for reuse.
I then checked the sandbox configuration with ini_get('memory_limit'): 32 MB. That was a limit inside the remote sandbox's PHP environment, not on my laptop.
“If I set a 1 MB limit, why did it reach 1,422 KB?”
This was my first big misunderstanding. I created a variable called $practiceLimit and checked it after every array allocation. That did not change PHP’s actual 32 MB limit.
$practiceLimit = 1024 * 1024; // just our threshold
$numbers = range(1, $i); // allocate first
$memory = memory_get_usage(); // measure second
if ($memory >= $practiceLimit) {
break; // stop third
}My output climbed to 1,422 KB at 35,000 numbers; the sandbox also reported a 1,938 KB peak during the run. Why did it exceed 1,024 KB? Because the program checked after allocating. It then stopped by running break.
No genuine out-of-memory fatal error occurred in this experiment. Our program stopped voluntarily; PHP’s real configured limit remained 32 MB.
This matters because a code-level threshold check and PHP’s runtime-enforced memory limit are two different things.
What if PHP is interrupted halfway through a task?
Next, I used a handled exception, not a genuine memory failure:
$record = ["status" => "in_progress"];
try {
throw new Exception("Interrupted!");
$record["status"] = "complete";
} catch (Exception $e) {
echo $record["status"]; // in_progress
} finally {
unset($record);
}The record never became complete because the exception interrupted the normal path. The finally block still cleaned up our variable. In a second experiment I intentionally omitted unset() and observed that the variable remained available to later code within that same execution. It was not proof of a memory leak or of data surviving into another web request.
We also simulated an unfinished $1,000 order. One branch of our toy code printed a payment message without checking order status; the other blocked processing because the order wasn’t complete. No payment was actually made. That was a demonstration of an application logic mistake, not the underlying PHP2Shell bug.
A critical caution: A catchable PHP exception is not the same as an out-of-memory fatal bailout inside the PHP interpreter. The experiments helped me think about interrupted work, but the real vulnerability lives inside PHP’s engine.
Where PHP2Shell actually enters the picture
The October 2026 research from Calif, with OpenAI and Wiz, describes two distinct core PHP vulnerabilities:
- CVE-2026–109618: Memory exhaustion can interrupt internal request-data creation after PHP has counted an entry but before the entry is fully initialized.
- CVE-2026–109619: Request-owned internal state can continue pointing at memory that has already been freed, enabling unsafe use by later operations or requests.
The PHP project rated both issues critical, and issued fixed releases on October 21, 2026. The researchers demonstrated unauthenticated RCE in affected configurations. Exploitability depends on the PHP runtime and deployment path; this does not mean every PHP application was compromised.
Vulnerability one: “Count Jake before creating Jake”
My easiest mental model was a fictional list of customers: Sam, Alex and Jake. But the actual records aren’t people or orders — they are internal PHP hash-table entries called buckets.
Imagine an internal table with two valid buckets. PHP begins adding a third:
- It increments counters indicating that the third slot is in use.
- It tries to allocate the memory needed to finish that bucket’s key and value.
- The allocation fails at the memory limit, and the operation is interrupted.
- Cleanup later sees the incremented count and tries to process the unfinished third slot as if it were a valid bucket.
This is not PHP thinking “Jake is a real customer.” It is PHP’s native cleanup code interpreting uninitialized or stale bytes as legitimate internal type information or pointers. An invalid pointer could crash the process; with sufficient attacker control over the memory layout, researchers demonstrated paths toward RCE.
For some affected Apache mod_php setups, this could be triggered while PHP parsed the incoming HTTP request — before the application’s PHP script had run. In the specific early-startup path described by the researchers, PHP-FPM could instead exit its worker, which is why the server execution model matters.
Vulnerability two: “The paper was thrown away, but the address was kept”
This one answered my repeated question: Does the broken information get carried to the next page refresh?
Get Sanketh Subhas’s stories in your inbox
Join Medium for free to get updates from this writer.
In some vulnerable paths, request-specific memory was freed, but PHP worker state still contained a reference to it. If the same worker processed a subsequent request before that state had been correctly reset, PHP could follow the invalid reference. That is a use-after-free, also called a dangling-pointer problem.
A refresh could create another HTTP request, but it is not inherently the trigger; a different visitor’s request could also be the next one handled by that worker. The broken customer order is not copied into the next order. The issue is retained interpreter state, not persistent shopping-cart data.
Why would an attacker do this? How does an error turn into RCE?
The researchers investigated specially structured HTTP form and upload input that could make PHP allocate far more internal memory than the incoming request’s size suggests. They didn’t need visitors to type PHP source code into a website. The request parser itself could build large nested data structures.
However, forcing a fatal error is only stage one. Usually a crash is just a crash.
A possible exploitation chain is:
- Trigger a vulnerable interruption.
- Cause PHP to process a partially initialized bucket or stale memory reference.
- Influence the memory contents PHP sees when it follows that invalid state.
- Turn the resulting unsafe read/write or object handling into control over program execution.
- Execute attacker-chosen behavior in the context of the PHP worker, if the exploit succeeds.
That final step is remote code execution (RCE). RCE is not automatically root access. It initially provides the privileges of the compromised PHP/web-server process, and further access depends on system permissions and other weaknesses.
How did the researchers discover the issue?
The discovery had a history. Calif’s write-up traces its motivation through a 2004 PHP memory-limit RCE associated with Stefan Esser, a 2020 bug report about out-of-memory failures and temporary-file cleanup, and a 2024 OSTIF/Quarkslab audit that identified related behavior without proving exploitation.
In 2026, researchers revisited the hypothesis with AI-assisted source-code review. They looked at how PHP’s memory allocator, fatal-error handling, and request shutdown interact. One especially revealing code sequence incremented a hash-table entry count before an allocation that could fail. From there, they investigated additional “publish state → perform fallible work → skip cleanup/rollback” patterns, including session and deserialization paths.
According to the original timeline, initial discoveries were made in August 2026, the first issue was reported to PHP on August 11, a working RCE proof of concept was confirmed by PHP on August 15, the second issue was reported on August 21, and fixes were released on October 21.
I found the research approach more interesting than a generic “AI found a bug” headline: historical domain knowledge shaped the question; AI assisted the investigation; researchers validated the actual exploit and coordinated disclosure.
How did PHP patch it?
The fix was not simply “kill the server whenever something goes wrong.” That was one of my questions too.
In the patched versions, PHP’s configured memory_limit becomes a soft limit for many allocations: an allocation may complete so that internal state is left consistent, and PHP raises the fatal error later at a safer execution point. A separate hard limit bounds how far memory may grow; if it would be exceeded, the process is terminated immediately rather than running potentially unsafe cleanup.
A request can still fail. The security improvement is that failure should not provide the original memory-corruption path. The release also addresses the stale-state vulnerability.
Fixed upstream releases issued October 21, 2026:
PHP branchFixed release8.28.2.358.38.3.368.48.4.278.58.5.12
An additional nuance for readers following the story in real time: on October 23, 2026, PHP published a separate informational advisory about a rare session-upload-progress configuration and recommended disabling that feature temporarily in affected setups. That follow-up should not be conflated with the two CVEs discussed here; it is another reason to check current vendor advisories rather than treat a version number as the end of a security review.
PHP 8.1 and earlier are end-of-life upstream. Enterprise Linux and long-term-support vendors may backport security fixes, so administrators must check their distributor’s advisories rather than conclude that an older version string necessarily means unpatched.
Did patching RCE eliminate DoS and DDoS?
No — and it doesn’t have to for the RCE patch to be real. This was another question I had.
Memory-intensive or high-volume requests can still create availability problems if enough workers, CPU time, RAM or connections are exhausted. A DoS is a loss of service availability; DDoS involves distributed attack sources. One expensive request might fail in isolation, while many expensive requests could degrade a service.
Defense in depth includes vendor patching, reverse-proxy request limits, rate limiting, limits on input nesting and count, PHP worker limits, resource isolation, monitoring and capacity planning. A small request body may expand into a much larger in-memory representation, so capping only raw HTTP body size is not enough.
Importantly, temporarily removing PHP’s memory limit was documented as a partial pre-patch mitigation for one attack path, not as a safe universal anti-DoS recommendation; it can increase resource-exhaustion risk. The supported fix is patching.
Where do organizations patch PHP — and can an ordinary user check?
On the servers or containers where PHP runs, not on a customer’s browser. An authorized administrator may inspect a PHP CLI version with php -v, but a web server can run a different PHP version; package provenance and backported vendor fixes also matter.
A browser user cannot reliably verify a remote website’s PHP patch level. Some sites reveal a version header, but that can be absent, outdated or insufficient. Real confirmation comes from the system owner: their inventory, package advisories, deployment records and verification.
A related question came up when I saw an advisory circulated to service providers at work. Why did I receive a PHP warning if PHP is server-side? Because provider organizations may manage their own web applications and infrastructure. The advisory asked their IT teams to check affected installations. Receiving that message does not prove the sender’s platform or my employer was vulnerable or compromised.
The questions that changed my understanding
Does HTML execute PHP? No. PHP runs on the backend and may generate HTML; the browser renders it.
Does PHP use my laptop’s RAM? Server-side PHP uses server RAM. The browser uses my device RAM for its own work.
Did my 1 MB sandbox experiment produce a real PHP fatal error? No. I set an artificial threshold and stopped the loop myself.
Was an unfinished purchase treated as another customer? No. The real bug involved PHP-internal hash-table buckets, not a database of orders.
Was cleanup skipped, or did it actually happen? Both patterns appeared in different bugs: unsafe cleanup of a partial bucket in one, and skipped state reset / dangling references in another.
Does a later HTTP request always inherit the previous request’s memory? No. The cross-request issue required vulnerable worker state surviving in the same process.
Does invalid memory automatically grant RCE? No. Exploitation requires additional control; many invalid accesses simply crash.
Will an attacker see the fatal error in their browser? The failure occurs on the server. The browser might receive an HTTP error or a dropped connection.
Can a patched server still suffer DoS? Yes. Preventing the particular RCE and preventing resource exhaustion are different security objectives.
Can I tell from a website visit whether PHP is patched? Not reliably; confirmation generally requires authorized server-side evidence.
My takeaway
The bug wasn’t simply “PHP ran out of RAM.” It was what PHP believed about its own state after an error. Counting an entry before finishing it, or keeping a pointer after freeing its target, can turn ordinary error handling into a security boundary failure.
The most useful lesson I took from this case is about failure paths: safe software needs correct behavior not only when requests succeed, but also when allocations, initialization, callbacks and cleanup fail halfway through.
And for defenders, the practical conclusion is clear: patch the affected PHP runtime, verify the deployed web-facing version, and separately maintain resource-exhaustion protections.
Sources and attribution
- Original research and technical discovery: Calif — PHP2Shell: A tale of two PHP remote vulnerabilities (research by Ryan Austin, with OpenAI and Wiz referenced by the project).
- Official PHP security advisory and October 21, 2026 release information.
- Official PHP manual:
memory_limitandmemory_hard_limit_percent. - PHP October 23, 2026 informational advisory about session upload progress.
Diagrams are original educational illustrations. They simplify implementation details; consult the original research for the actual source-code paths. No live systems were tested for this article.





