PHP 8.1 stopped getting patches from php.net on 31 December 2025. In the 301 days since, 22 CVE identifiers it had never seen were fixed into it anyway, by somebody else.
Nothing about a runtime changes on its end-of-life date. The vulnerability feed keeps producing entries and the patch feed stops, so risk accrues as a function of time rather than arriving as an event. PHP 8.1 collected CVE identifiers at 1.11 a month while it had security support, and at 2.22 a month after it lost it.
What changes on the day a runtime reaches end of life
The patch feed stops, the vulnerability feed doesn’t, and the binary is byte-for-byte what it was the day before. php.net’s end-of-life page, read 29 October 2026, lists branch 8.1 under Unsupported Branches against 31 Dec 2025, last release 8.1.34. The supported-versions page has dropped 8.1, but the rule is still there, on a render headed “Today: 28 Oct 2026”:
Once the four years of support are completed, the branch reaches its end of life and is no longer supported.
Four years, split in two, further down that page:
After this two year period of active support, each branch is then supported for two additional years for critical security issues only. Releases during this period are made on an as-needed basis: there may be multiple point releases, or none, depending on the number of reports.
HeroDevs calls what the fourth year ends in the forever-day problem, in its .NET Cliff whitepaper. Good name — and no rate, which is what decides anything.
The PHP Release Process RFC is better on mechanism. Its Security Management section tells release managers to “Be sure that the security team of each major distributions have access to the security reports, before public release”. That sentence starts 9,812 characters into a 10,495-character raw export, after ten headings, immediately before a Changelog last amended 2011/6/20.
How many CVEs an end-of-life PHP version collects in a year
Thirteen if you project PHP 8.1’s own security-phase rate forward, thirty if you count what its live siblings produced that was old enough to be in it.
Each branch “is fully supported for two years from its initial stable release”, php.net says, and 8.1.0 shipped on 25 November 2021, so active support ended 25 November 2023 — the date a snapshot headed “Today: 31 May 2024” still prints. To 31 December 2025 is 767 days, 767 ÷ 30.4375 = 25.20 months. Eight releases land inside, one with no CVE, and the php.net PHP 8 changelog gives them 28 distinct identifiers: 28 ÷ 25.20 = 1.11 a month, so twelve months is 13.3.
Now count what happened after. Canonical published six jammy-security uploads of php8.1 after 31 December 2025, from 8.1.2-1ubuntu2.23 on 12 January to .28 on 21 October 2026, with 2, 7, 1, 2, 10 and 2 CVEs in turn: 24 distinct identifiers. Two, CVE-2025-14178 and CVE-2025-14180, php.net had fixed in 8.1.34 before the branch died. That leaves 22 over 301 days, or 9.89 months, so 22 ÷ 9.89 = 2.22 against 1.11, and 2.22 ÷ 1.11 = 2.00.
The rate doubled.
Upstream counts the same thing by an independent route. php.net’s changelog carries 32 distinct identifiers across the live branches in 2026 to 28 October, seven of which never appear below 8.3, so 25 were old enough for the dead branch: 25 ÷ 9.89 = 2.53 a month. Two archives, two counting operations, three identifiers apart: 3 ÷ 9.89 = 0.30 a month.
CVE identifiers accruing against PHP 8.1 at three horizons past 31 December 2025. No column counts unpatched issues; the middle one is what Canonical patched.
| Months past end of life | Projected at 1.11/month | Backported at 2.22/month | Eligible feed at 2.53/month |
|---|---|---|---|
| 6 | 6.7 | 13.3 | 15.2 |
| 12 | 13.3 | 26.6 | 30.4 |
| 24 | 26.6 | 53.3 | 60.7 |
Why a doubled CVE rate is not evidence that end of life caused it
Because PHP’s live branches did better than double over the same window, and a branch rising more slowly than its siblings has caught nothing from being dead. The changelog carries 31 distinct identifiers across all live 8.x branches in 2024 and 2025, 31 ÷ 24 = 1.29 a month, against 32 in 2026 to 28 October, 32 ÷ 9.89 = 3.24. The project ran 3.24 ÷ 1.29 = 2.51 times hotter, the dead branch 2.00, so 2.00 ÷ 2.51 = 0.80.
A control that counts the thing it controls for is worth nothing, so I checked. Strike every 8.1.x release from the 2024–2025 set and the count is still 31: each identifier fixed in an 8.1 release was fixed in a sibling the same day.
What the control rules out is the flat baseline the 13.3 in the table assumes. What it leaves open is why, and I don’t know: more reporters, a funded audit, one researcher with a fuzzer and a good month. I haven’t looked. The feed might keep climbing and it might fall back.
What share of a dead branch’s CVE feed still gets backported
88.0%, for PHP 8.1 on Ubuntu 22.04 across 2026, once code the branch never had leaves the denominator. Twenty-two of the 32 identifiers php.net fixed across its live branches that window landed in 8.1 anyway, which reads as 22 ÷ 32 = 68.8% until you look at the ten that didn’t.
Seven of the ten were fixed only in 8.3 and above: CVE-2026-12184 first appears in 8.3.32, three more in 8.4, three in 8.5. A lowest fixed branch is evidence the code postdates 8.1, not proof, and I didn’t open the seven advisories. Strike them and 32 − 7 = 25 were old enough, so 22 ÷ 25 = 88.0%.
Call that the carry-over fraction: the share of a runtime’s post-end-of-life vulnerability feed that still lands in the dead branch, once code postdating the branch leaves the denominator. Seven in eight is the house default we plan with. Three identifiers reached 8.2 and missed 8.1: CVE-2026-17545, CVE-2026-7258 and CVE-2026-9672.
endoflife.ai got there first with a looser denominator. Scott Bissett’s “The Backport Myth”, 21 August 2026, hand-checked about 98 exploited-CVE pairs across 700-odd products and found “roughly 88% failed — most often because the vendor had quietly patched the ‘dead’ version anyway”. Its first wrong lesson reads “Courtesy backports are charity, and charity has no trend you can plan on”, and I plan on it anyway, because Launchpad puts php8.1 on jammy in component main, and main is what Canonical’s published security commitment attaches to. Charity has no trend. A commitment has a clock, and I cost it below.
Check whether your distro is still patching your end-of-life runtime
Open the distro’s changelog for the package, not the runtime’s. Canonical published a security update to php8.1 on Ubuntu 22.04 on 21 October 2026 — 294 days after PHP 8.1 reached end of life — and the package is still called 8.1.2. Dates here are Launchpad publication timestamps, one to eight days after the Debian changelog trailer. The top stanza, patched paths cut, from Canonical’s php8.1 changelog:
text
php8.1 (8.1.2-1ubuntu2.28) jammy-security; urgency=medium
* SECURITY UPDATE: Remote RCE via state corruption issue
- debian/patches/CVE-2026-109618_109619.patch: Avoid state corruption via
bailout in Zend/zend_API.h, Zend/zend_alloc.c, Zend/zend_alloc.h,
[... nine further lines of patched paths elided ...]
- CVE-2026-109618
- CVE-2026-109619 Now open the php-src advisory GHSA-w8hw-w9fx-j79w, CVE-2026-109618, “RCE via OOM corruption”, critical. Its affected-version table has four rows: < 8.2.35, < 8.3.36, < 8.4.27 and < 8.5.12. No 8.1 row, and not one marked “not affected” either. An advisory names the version that fixes each supported branch; a dead branch has none, so it drops out. Absence is scope, not safety.
Canonical shipped that patch within the hour, which is the RFC’s distro-notification line working: Launchpad puts the upload at 13:33:49Z created and 13:41:10Z published against the advisory’s 2026-10-21T13:34:25Z, read off the page’s <relative-time datetime> attribute because GitHub’s advisories API returns 404 for this GHSA. The advisory carries one severity vector, a CVSS 4.0 string at 9.2, and no v3 vector — so a queue sorted on a v3 score has nothing to sort the critical RCE on, which is where the EPSS article picks up.
The version string will bite a scanner too, and harder than an upstream 8.1.34 would, because the Ubuntu package reports 8.1.2 rather than the newest 8.1. Upstream PHP 8.1.2 shipped on 20 January 2022, 1,735 days before this upload, or 1,735 ÷ 365.25 = 4.75 years, because every Canonical revision since lives after the hyphen. Debian builds print the whole version, revision included, so the hazard is a scanner keyed on the upstream triple, or one truncating at the hyphen.
Is it safe to run an end-of-life PHP version?
For an internal tool with no untrusted input it can be. Oxbow is where I’d say so out loud, and Oxbow is invented: a PHP 8.1 application on Ubuntu 22.04, reachable only over the company VPN, on a database replica, used by eleven people. Not a client, not a past employer, not a composite of either. Every number below is chosen, not measured.
Four things move it: months past end of life (m), the eligible-feed rate (r), the carry-over fraction (a), and untrusted input paths (x). The first three give a number. With the distro patching, what is left is the miss rate, 12 × 2.53 × (1 − 0.88) = 3.6 a year — and the three observed misses give 3 ÷ 9.89 × 12 = 3.6 from the other end. Without it, the whole feed: 12 × 2.53 = 30.4.
The fourth decides whether either means anything. My tolerance is zero reachable issues, not few, and at x = 0 the expected count is exactly zero whatever m, r and a are: the product has a zero in it. The function jumps at x = 1 and is flat either side, so the threshold sits on exposure, not on a CVE count.
So: at zero untrusted input paths I’d leave Oxbow on PHP 8.1 and buy the subscription, and the moment one form accepts an upload that stops being true.
Upgrade, extended support, or isolate: the three options costed
One engineer-month buys eight server-years of Ubuntu Pro. Our embedded software rate is $4,000 a month at 160 hours, so $4,000 ÷ 160 = $25 an hour and the 20-hour minimum is $500; Ubuntu Pro pricing prints “Server with unlimited VMs” at “$500” “per machine per year”, so $4,000 ÷ $500 = 8.0 server-years.
The obvious estimate bills you from the day the runtime dies, and that is what it gets wrong. Ubuntu 22.04’s standard security maintenance runs to May 2027 and php8.1 sits in main, so all six uploads cost nothing; the release cycle page gives expanded maintenance to May 2032, and five years at $500 a server a year is 5 × $500 = $2,500. You buy the distro’s clock, not the runtime’s.
An upgrade to PHP 8.4 is what a competent engineer picks by default: it ends the question until 31 December 2028 and stops the exposure argument resting on a policy nobody owns. One engineer-month, $4,000, at our software development rate, and it wins the moment anything untrusted reaches the app.
Commercial extended support means somebody else reads the advisories and backports the patch, $500 a server a year from May 2027. It wins when the runtime is in Ubuntu main and exposure is low, and it ends on a date rather than a judgement.
Isolation removes the class instead of the bugs one at a time, and it has a cost nobody writes down: twenty hours to write the policy, $500, then somebody’s attention forever. It fails the day somebody adds an ingress for a reporting feed, and nobody rings a bell.
For Oxbow I would not pay for the upgrade. $2,500 to Canonical beats $4,000 of our time, and we’d tell a client to spend that $4,000 where it earns. We’d decline the work, and that is the sentence that costs us the sale.
Double the servers and it flips: 2 × $2,500 = $5,000 against $4,000 to upgrade once, so the subscription loses above $4,000 ÷ $2,500 = 1.6 servers. I wouldn’t have guessed it was that low.
Derate the upgrade 4x, because extension lists fight back: $16,000 ÷ $500 = 32 server-years, so on one box the decision hardens rather than flips. Derate the other side and 2.53 ÷ 4 = 0.63 a month gives 7.6 a year rather than 30.4 — which doesn’t flip it either. Behind Oxbow’s VPN 30.4 is as survivable as 7.6. The CVE count isn’t the decision variable. Exposure is.
Does .NET end of support work the same way as PHP end of life?
The clock is shorter and has no security-only half: three years for a .NET LTS release against PHP’s two plus two. Under Release types, the .NET support policy page says “LTS releases are supported for three years after the initial release.” The section matters, because the page gives three years “after general availability” further down and a search lands on whichever comes first.
.NET 6 is the clean case because its record is closed. dotnet/core’s releases.json for 6.0 lists 44 releases from 8 November 2021 to 12 November 2024, 1,100 days or 36.14 months. They carry 49 distinct CVEs: 49 ÷ 36.14 = 1.36 a month. PHP 8.1’s 35 releases over 49.18 months carry 43, and 43 ÷ 49.18 = 0.87, so .NET 6 ran 1.36 ÷ 0.87 = 1.56 times hotter in a shorter life. Both are whole-life figures, and that matters — .NET 6’s last twelve months alone give 14 CVEs over 11.96 months, 14 ÷ 11.96 = 1.17, near enough PHP’s security-phase 1.11 to look like a law. That agreement is an artefact of a window I chose.
And the patch train doesn’t taper at end of support. It stops. The last .NET 6 patch, 6.0.36, shipped on 12 November 2024, the date that same JSON gives as the branch’s eol-date.
Check the date itself, because Microsoft’s pages disagree by a day: the support policy page gives .NET 8 and .NET 9 end of support as “November 10, 2026” and both releases.json files carry "eol-date": "2026-11-10", while the Microsoft Lifecycle page gives both as 11/11/2026 6:59:59 AM.
Why counting CVE identifiers is not counting flaws
Because a changelog can name a CVE without fixing it and fix one without naming it, and both happened above. The under-count bit hardest. php.net records some security fixes with a GHSA identifier and no CVE identifier: PHP 8.2.35, 8.3.36, 8.4.27 and 8.5.12, all dated 22 October 2026, each read “Fixed GHSA-w8hw-w9fx-j79w, GHSA-6wmf-4rv5-p3rp (Avoid state corruption via bailout).” and nothing more. A CVE-identifier scan misses both. They are in the counts above only because I resolved the GHSAs by hand, and my sweep of 2024 and 2025 found no others, which isn’t proof there are none.
The over-count is better known, and larger than one. Two of the security phase’s 28 are provable: CVE-2012-1823 in 8.1.29, there because CVE-2024-4577 is a bypass of it and which the soft hyphen article covers, and CVE-2022-31629 in 8.1.28, the partial fix CVE-2024-2756 got round. The branch’s arithmetic catches that one: 16 identifiers in its first two years plus 28 in its security phase is 44 against a whole-life count of 43, and CVE-2022-31629 is the discrepancy. Strip both and the security phase holds 26 fixes, 26 ÷ 25.20 = 1.03 a month, so the ratio is 2.22 ÷ 1.03 = 2.16. The doubling survives. The clean 2.00 does not.
One assumption would make this article wrong: that an upstream advisory feed is the right instrument for a dead branch’s risk. It is not. Past end of life the measurement that counts is the distro’s backport record, the only feed still written against the code you are running, and it has its own blind spot. It tells you what somebody fixed. Never what somebody found.