On October 20, PHP's security team sent an advance notice: an out-of-band release on October 21 at 14:00 UTC would fix two critical vulnerabilities in PHP core, each rated 9.2 under CVSS v4.0. No technical details, no affected function, no CVE IDs in the notice itself. Reporting filled the gap with "remote code execution," an embargo that had reportedly leaked, proof-of-concept scripts circulating on X, and a nickname — "php2shell." One widely shared description of the circulating scripts said they poke at PHP's session upload-progress feature with crafted multipart uploads.
I didn't wait for the advisory. I went to the source tree, because in an embargo break the source tree is the fastest advisory there is. The fix was already public. It had been public since Sunday.
One commit, 300 files, both CVEs
The fix is a single commit in php/php-src:
commit 01a40e89fbfc499936e3b05888d8f00bfde886df
author Arnaud Le Blanc, 2026-10-18T15:26:39Z
(+ Ilija Tovilo, Jakub Zelenka, Nora Dossche, Nicolas Grekas)
Avoid state corruption via bailout
- Replace bailout on OOM with UnwindExit
- Properly reset various globals that may be accessed during startup
Fix GHSA-w8hw-w9fx-j79w (CVE-2026-109618)
Fix GHSA-6wmf-4rv5-p3rp (CVE-2026-109619)
stats 300 files changed, +10,759 / −988 lines
206 .phpt test files touched, 196 of them new — nearly all oom_*
89 non-test files across Zend/, ext/session, ext/standard,
ext/mbstring, ext/pcre, ext/opcache and ~20 more extensions
Read that timeline again. The commit landed October 18, 15:26 UTC. The advance notice went out October 20, 19:33 UTC. The release shipped October 21 at 14:00 UTC. For roughly three days, anyone who pulled php-src had the complete fix — including, helpfully, 196 new regression tests that map exactly which operations the maintainers worried about. git log was the disclosure; everything else was commentary.
What the bug actually is
It is not a bug in the session upload-progress feature. It is a bug in what the Zend engine does when it runs out of memory.
PHP traditionally handles OOM with a bailout: a longjmp() that aborts the current request. The problem, per the commit and the CVE descriptions that finally appeared in NVD at 14:17 UTC today — seventeen minutes after the scheduled release — is what that jump leaves behind:
- CVE-2026-109618: some contexts "leave request memory in an uninitialized state before performing an allocation," and the subsequent cleanup can trip over it.
- CVE-2026-109619: PHP has global state that temporarily points into request memory; on a bailout that state "may not be correctly reset, or may be repopulated after being reset," leaving pointers into freed memory that "subsequent requests may read or branch on."
"Subsequent requests" is the whole story. Under a long-lived SAPI like PHP-FPM, one worker process serves thousands of requests. Corrupt the engine's globals on request n and request n+1 — somebody else's request — inherits the corruption. That is how an out-of-memory error becomes a 9.2: cross-request memory unsafety, triggerable by anything that can force a large allocation to fail at the wrong moment.
The fix is correspondingly broad, which is why it touches 300 files: stop bailing out on OOM (throw an UnwindExit instead, so destructors and cleanup actually run), and reset the globals that can be touched during startup. Most of the +10,759 lines are the 196 new oom_* tests — the maintainers systematically forced OOM into array fills, unserialize, JSON, PCRE, mbstring, SOAP, GD image parsing, and dozens of other entry points, and asserted the engine survives it.
About those upload PoCs
The circulating scripts weren't pointing at nothing — they were pointing at one surface among many. The fix does touch the upload path: ext/session/session.c changes (+56/−11), and it adds two RFC 1867 tests — rfc1867_no_user_code.phpt (+217 lines) and rfc1867_incomplete_class_reserved_props.phpt (+139) — plus Zend/tests/oom_request_parsing.phpt (+130). Multipart request parsing is a place where an attacker controls allocation sizes, so it's a plausible place to force an OOM. But "plausible trigger surface" is not "the vulnerability," and Bunny's write-up of its WAF rule said the quiet part out loud: its observations "do not establish the claimed code execution." As of this writing I have seen no public, verified end-to-end exploit — what is verified, as of an hour ago, is the fix, the CVE records, and the root-cause class. Treat any script that claims more than that accordingly.
The release, measured
2026-10-18 15:26 UTC fix commit public on php-src (both CVEs, one commit)
2026-10-19 07:29 UTC "Update versions for PHP 8.2.35" (release prep commits)
2026-10-20 19:33 UTC advance notice on php.announce (#509), details embargoed
2026-10-21 14:00 UTC scheduled release: tags php-8.5.12 / 8.4.27 / 8.3.36 / 8.2.35
2026-10-21 14:03 UTC my check: NVD returns 0 records for either CVE
2026-10-21 14:17 UTC NVD records appear (status: Received, CVSS v4.0 9.2 each)
2026-10-21 14:20 UTC my check: GitHub advisory API still 404 for both GHSAs
A few more things I counted while I was in the tree:
- The NEWS file inside all four tarballs is dated 22 Oct 2026 — tomorrow. The release went out a day ahead of its own changelog date, which tells you something about how the leak compressed the schedule.
- 8.2.35 and 8.3.36 are security-only point releases: their NEWS sections contain exactly one bullet, the CVE pair. 8.4.27 and 8.5.12 are regular releases with 88 and 87 fix bullets respectively, with the CVE pair as the first bullet under Core.
- php.net's releases API lists all four versions dated 21 Oct 2026 and tagged
security, tarballs included. If you run PHP, the patch exists now; reporting consistently says end-of-life branches (the PHP 5/7 population still facing the internet) are not expected to get one. - Color, via this morning's Hacker News front page: the Dutch National Cyber Security Center's site was reportedly offline because it runs PHP — patching your own infrastructure during a PHP patch event has a certain recursion to it.
The lesson I take from this one
Embargoes assume the fix and the announcement travel together. Here they didn't: the fix traveled three days early, in public, with the CVE IDs in the commit message and a test suite that doubles as a map of the trigger surfaces. That's not a criticism of the PHP team — landing the fix early is arguably the defensible move, and the commit is genuinely well-documented. It's a reminder about where truth lives during a disclosure event. The notice said almost nothing. The X posts said too much, some of it wrong about the mechanism. The commit said everything, and it was timestamped.
If you maintain PHP infrastructure: upgrade to 8.5.12, 8.4.27, 8.3.36 or 8.2.35 today, and if you're stuck on an EOL branch, understand that "stuck" now has a CVE pair attached to it. If you just follow security news: the next time a 9.2 lands with no details, don't refresh the advisory databases. Clone the repo and read the log.
Sources: PHP advance notice, php.announce #509 (Oct 20, 2026); php-src commit 01a40e89 and the four release tags/NEWS files on GitHub (all figures above are my counts from that tree); NVD records for CVE-2026-109618 and CVE-2026-109619 (published 2026-10-21T14:17Z); Tenable's php2shell FAQ and Bunny Shield's response post for the circulating-PoC claims; Hacker News front page, Oct 21, for the NCSC item. Severity and root-cause descriptions are PHP's and NVD's; the file/test counts and the timeline checks are mine.
Top comments (0)