The PHP2Shell fix shipped on October 21. We went through the php-src issues filed since then, ran one regression on patched and unpatched PHP, and counted the public crash backlog.
This explains reporting by
IEEE Spectrum, "PHP2Shell Is a Wakeup Call for AI in Cybersecurity" (November 2, 2026). Read the original first:
https://spectrum.ieee.org/ai-in-cybersecurity-php2shell-vulnerability
In one minute #
- The PHP2Shell bug came from a fatal error at memory_limit jumping out of C code that was halfway through setting up internal state. IEEE Spectrum describes it as memory too small to hold the data, which is a buffer overflow and a different bug.
- The fix (commit 31c0ea7d, +11,556 and -1,017 lines) makes memory_limit soft and adds a hard limit, 200% of memory_limit by default. Crossing the hard limit ends the process with _exit(137).
- Eight php-src issues filed since the release name the fix commit. Four are behavior regressions, and none of their fixes is merged yet.
- A sprintf call that gave a normal fatal error (exit 255) on PHP 8.5.0 and 8.3.30 ends the process with exit code 137 on 8.5.12. We ran it on all three.
- Public sanitizer crash reports in php-src mostly close within days (median 1.8 days across 362). The PHP2Shell root cause was never in that queue.
Why we looked #
IEEE Spectrum's piece on PHP2Shell is on the front page today under the headline "AI is forcing us to pay off 40 years of technical debt." Most of it is about finding bugs. One of the researchers it quotes makes the opposite point partway through: fixing is the hard half, because you have to close every path and keep everything that already works working.
That seemed worth checking. The fix shipped on October 21, so there are two weeks of public bug reports to look at. We went through them.
First, what the bug was #
The Spectrum article describes the root cause as an out-of-memory error, "in which the amount of memory allocated is too small to hold the data." That definition describes a buffer overflow, which is a different bug.
The PHP Foundation's write-up is more specific. If a request hit memory_limit at exactly the wrong moment, the fatal error jumped out of C code that was halfway through setting up internal state (PHP does this with a longjmp, zend_bailout()). The half-built state stayed behind, and later code treated it as valid. Both CVEs, CVE-2026-109618 and CVE-2026-109619, come from that one pattern.
What the fix changed, briefly #
Mattias Geniar wrote up the mechanics on release day, with runs on patched and unpatched builds in containers (ma.ttias.be/php-memory-hard-limit). Read that for detail. The short version, which we also checked against Zend/zend_alloc.c on the PHP-8.5 branch:
- memory_limit is now soft. An allocation that crosses it still succeeds, and the fatal error is raised at the next safe point.
- A new hard limit sits above it, 200% of memory_limit by default (or memory_limit plus 1 GB once the limit is above 1 GB). The 8.5 source sets it through a new ini, memory_hard_limit_percent. We read that in the code and did not test the setting itself.
- An allocation that is not allowed to fail, and would cross the hard limit, prints one line to stderr and calls _exit(137). Shutdown functions do not run.
The commit is 31c0ea7d, "Avoid state corruption via bailout," +11,556 and -1,017 lines. It shipped in 8.5.12, 8.4.27, 8.3.36 and 8.2.35. One small thing we noticed: on the PHP-8.5 branch, the new setting is not in UPGRADING, php.ini-production or php.ini-development.
What broke #
A GitHub search for the fix commit's hash (31c0ea7d on 8.5, 3584a98bda on 8.2) turns up eight issues filed since the release. Four of them are behavior regressions, covered below in the order they were filed. The other four: a pending out-of-memory error that gets dropped when compilation fails (GH-24631), a stack-limit test that has segfaulted on x32 every night since October 22 (GH-24799), an older FPM settings-leak report that a commenter tied to the fix (GH-24689), and a use-after-free that was closed as a duplicate of an earlier report (GH-24672).
GH-24492 (filed on release day). Some code paths still use the allocator that is not allowed to fail, with a size the caller picks. Those used to give the normal fatal error. Now they cross the hard limit and the process exits. The reporter's simplest case:
<?php
sprintf("%400000000s", "");
echo "survived\n";
We ran it with php -n -d memory_limit=128M on three builds. On the two unpatched ones we had on hand, 8.5.0 and 8.3.30, you get the old behavior:
Fatal error: Allowed memory size of 134217728 bytes exhausted (tried to allocate 503316512 bytes) in repro.php on line 2
exit=255
On the official php:8.5.12-cli Docker image (built Oct 28), the same script never prints "survived":
Hard memory limit of 268435456 bytes exhausted (tried to allocate 503320576 bytes) in /x/repro.php on line 2
exit=137
That matches the bug report and the source. About 480 MiB is far past a 256 MiB hard limit, so the process exits with no fatal error and no shutdown functions. The report also lists pack(), number_format(), bcadd(), bin2hex(), base64_encode(), urlencode() and var_export(), plus a crafted phar:// manifest of about 100 bytes. We have not reproduced the phar case. If it holds, a small uploaded file can take down a worker.
Geniar found that a different large allocation gave him a normal fatal error, so this is per call site. Some were converted to fail softly. These were missed.
There are four fix PRs for GH-24492: #24548, #24596, #24618, and #24516 for the Expat side of ext/xml. As of tonight all four are open and none is merged. The 8.5.13 section of NEWS, dated November 19, does not list GH-24492 yet.
GH-24493. The session decoder that runs for upload progress now rejects three reserved property names, such as __PHP_Incomplete_Class_Name. A rejection fails the whole decode, so PHP deletes the session. The reporter shows that session_encode() itself can write such a session, for example when $_SESSION holds the result of json_decode() on user input. In 8.5.11 the same session survives.
GH-24787. If the limit is crossed during request startup, in a gap where nothing checks for the pending error, an extension's RINIT can fail. The worker then calls exit(1) before sending any response, and uploaded temp files stay in upload_tmp_dir. The PHP team has labeled this one Verified.
GH-24563. Its author calls it informational: a few allocation-failure paths on master still use the old fatal-error route. Three days later the same author posted a way to reach one of them, through a huge serialize_precision.
The Foundation's post says every candidate fix came with trade-offs, and names compatibility breaks and new denial-of-service risk among them. It also says plainly that the months of delay before a fix were a structural failure on its side. The reports above fall into those first two buckets.
Was this bug sitting in a crash queue? #
The Spectrum piece suggests crash reports that nobody takes seriously are where this kind of bug waits. The public tracker does not fit that story well. We pulled every php-src issue with AddressSanitizer output in the body, 362 of them since January 2022:
- Closed: 313
- Median time to close: 1.8 days
- Closed within 7 days: 68%
- Open now: 49
- Median age of the open ones: 102 days
- Filed since August 1, 2026: 67 (24 still open)
Sanitizer crash reports in php-src mostly get closed within days. And the PHP2Shell root cause was never in that queue. Per the Foundation, a core developer found the first signs while fixing an earlier audit finding, and a possible exploit was theorized in 2024. Work on a fix stalled and restarted for months, until Calif reported a working remote code execution on August 11. All of that happened in the private security process.
What has changed since the disclosure is what people file in public. Since August 1, 15 issues mention "bailout" and 11 are still open. Four of the open ones describe a bailout leaving state behind somewhere the fix did not touch: libxml, the OPcache JIT, an fcc destructor, and opcache_compile_file(). Four more are the regressions above. The rest are two feature requests and a test that segfaults on x32.
So the 40-year debt framing is a bit off for this case, at least going by the tracker. The debt was one early design choice: on a fatal error, longjmp out from wherever you are. Paying it off meant more than 11,000 added lines in how memory errors work, and the tracker shows that change still settling.
How we got these numbers #
GitHub search, run 2026-11-04 at 9:42 PM UTC. Counts move as issues open and close.
gh api -X GET search/issues -f q='repo:php/php-src is:issue AddressSanitizer in:body' -f per_page=100 -f page=1
gh api -X GET search/issues -f q='repo:php/php-src is:issue bailout created:>=2026-08-01'
gh api repos/php/php-src/commits/31c0ea7d
gh api 'repos/php/php-src/contents/Zend/zend_alloc.c?ref=PHP-8.5'
Who is affected #
| Case | Status |
|---|---|
| PHP 8.5.12, 8.4.27, 8.3.36 and 8.2.35 | Have the fix, the new hard limit, and the regressions in GH-24492, GH-24493 and GH-24787. |
| Code that builds big strings from a caller-chosen size (sprintf padding, pack, number_format and others in GH-24492) | Can end the process with exit 137 where it used to give a fatal error. Fix PRs open, none merged. |
| Sites using upload progress with sessions that hold decoded user input | A session with a reserved property name is deleted (GH-24493). |
| PHP-FPM and mod_php workers | A worker can use up to the hard limit before it stops, and a hard-limit exit skips shutdown functions. |
What to do #
- Search stderr and FPM logs for "Hard memory limit of", and for workers exiting with code 137. Geniar's post covers why the stderr line often never reaches a log.
- If you use upload progress and your sessions hold decoded user input, read GH-24493 before you plan around 8.5.13.
- If your code counts on shutdown functions running after an out-of-memory fatal, and it builds big strings (sprintf padding, pack, number_format), watch GH-24492.
What is still unknown #
- Whether the crafted phar:// manifest in GH-24492 can end a worker. We have not reproduced it.
- How memory_hard_limit_percent behaves when you set it. We read it in the source but did not test it.
- When the GH-24492 fix PRs will merge. All four were open as of November 4, and the 8.5.13 notes do not list it yet.
- The GitHub counts move as issues open and close. Ours are from 9:42 PM UTC on November 4.
Sources #
AI News Report· every headline, every morning.