CVE-2026-109618: we tried to make the PHP RCE fire. It wouldn't.
On 21 October, PHP shipped an emergency patch for a critical remote-code-execution bug in its own interpreter, and a security vendor predicted mass exploitation within about fifteen minutes, because diffing an official fix against the vulnerable source is the easiest exploit lead an attacker ever gets. We spent that same window doing the unglamorous version of the work: building the bug from source, instrumenting it, and trying to make it actually execute code. On everything we tested, it wouldn't. The distance between "this is vulnerable" and "this is owned" turned out to be the whole story, and it is a distance a version-number scanner cannot see.
session.upload_progress feature, patched 2026-10-21, backported even to EOL 7.4/8.0/8.1. The trigger runs before any application code, so an app-layer WAF cannot stop it. The leaked PoCs' stated root cause, a use-after-free inside one request, does not hold on modern PHP; a refcount guard keeps the data alive. The real bug is a harder cross-request use-after-free that needs a php-fpm worker to survive an out-of-memory abort. On stock Docker images (8.4.4 and EOL 7.4.33 alike) the worker always exits instead, and the window never opens. As of this writing nobody has published a working, generic RCE against php-fpm. PHP's own advisory says a working exploit exists, but only for mod_php, not php-fpm. Patch everything anyway.
What the bug actually is
session.upload_progress is a convenience feature: it lets a browser poll how far along a large file upload is, by having PHP write the upload's progress into the user's session while the multipart request body is still streaming in. You switch it on for a request by including a form field named PHP_SESSION_UPLOAD_PROGRESS in a multipart POST. The important part for an attacker is where that code lives: in PHP's native request parser (main/rfc1867.c and ext/session/session.c), which runs before a single line of application code executes. That is why this earns its CVSS 9.2 and why no amount of application-layer input validation or WAF rules helps: the dangerous work happens while PHP is still reading the body, before your app, your framework, or your filters ever see the request.
Because the flaw is in PHP core, not in any one framework, it reaches everything that runs on PHP: Magento, Shopware, WooCommerce, WordPress, Laravel, Symfony. The exposure is wide. Shodan-based figures cited in press coverage put roughly 884,000 internet-facing hosts on potentially affected PHP versions, and the EOL branches (8.1 and below) are the worst off, because most distributions will never ship a packaged fix, only a source backport. That is exactly the small-and-medium-business software stack we see day to day.
The leaked PoCs, and what they actually prove
The CVE was leaked ahead of PHP's own coordinated disclosure, and a cluster of PoC repos went up on GitHub (the most-discussed being Zer061 and 0x4148, with others from junaire, WoYinDao, wakemeiflose, ikow and eness). They all describe the same root cause: a stale, dangling zval* pointer that gets freed between two session.upload_progress updates inside a single request: a textbook use-after-free.
We reviewed those repos directly, line by line, before touching any code. A few things were worth noting. The Zer061 account is not a throwaway: created in 2017, 42 repos, real follower graph. Its writeups (VALIDATION.md, REPORT_VALIDATION.md) are source-line-verified against real php-src 8.4.4 and 8.2.34, and line up with independently published research. We scanned the included PoC script for the usual nasties (exfiltration, hardcoded command-and-control, obfuscation) and it came back clean: raw sockets to a user-supplied host, not a trojan.
One detail did stand out. The repo's REPORT_VALIDATION.md carries an appendix framed as an "ORIGIN-ACCESS RCE RUNBOOK", with <REDACTED> target references and a specific redacted endpoint path. That reads less like an academic writeup built from scratch and more like a sanitized operational log from an engagement of unknown authorization. We did not execute any of that PoC code, even in a sandbox. It wasn't necessary for a defensive reproduction, and we had no interest in operationalizing something of uncertain origin.
So we built it ourselves, and ASAN stayed silent
Everything from here is our own lab work, run in disposable Docker sandboxes with --network none. No live target, no client system, no third party touched: just official PHP images and source builds in a box with no way out.
We compiled PHP 8.4.4 from source with AddressSanitizer: full memory-error instrumentation, system allocator, a 256 MB quarantine so freed memory is not reused quickly enough to hide a use-after-free. Then we ran the public repos' described trigger (the "stale zval* freed across session.upload_progress updates" mechanism) across 12 live attempts, six variants each of what the repos call bug618 and bug619, with a harmless marker payload (echo PWNED_SANDBOX_TEST, never anything destructive).
Every single run reached the trigger code path. Both "armed" pointers got written through, every time. And ASAN reported nothing: zero memory-safety errors, across every variant. So we stopped guessing and read the source. In session.c around lines 3182–3190 there is a Z_TRY_ADDREF refcount bump that keeps the relevant data alive at exactly the moment the public repos claim it gets freed. Conclusion: the publicly described root cause does not hold on modern PHP. The repos prove their trigger reaches the vulnerable area. They do not prove a use-after-free actually fires there.
The real mechanism is harder, and it crosses requests
A silent ASAN is not an all-clear: it means we were looking in the wrong place. Cross-referencing independent researchers and vendors (Sansec, RemiPelloux, Phoenix Security, watchTowr) pointed at a different, nastier mechanism. The real bug is an out-of-memory-triggered cross-request use-after-free:
- Send a request with a huge
upload_progressfilename, big enough to exhaust PHP'smemory_limitduring the serialization that happens before the request properly initializes. - That exhaustion triggers a
zend_bailout, PHP's internal "abort this request now" mechanism, which skips the normal cleanup path. - If, and only if, the php-fpm worker process survives that bailout instead of exiting, the next request served by that same worker can reuse a dangling ~64-byte serialize context left behind by the aborted request.
That is the actual use-after-free: not within one request, but across two requests that happen to share a worker process that lived through the abort. It is a genuinely sound bug. It is also a much more demanding one to land than "send two upload-progress updates," because it hinges entirely on that one word: survives.
We got the OOM every time. The worker died every time.
On the official php:8.4.4-fpm-bookworm image, the out-of-memory event fired exactly as the theory predicts: the fatal "allowed memory size exhausted" error, at the right allocation size, on the right code path. The primitive works. But the worker process always exited after the bailout, with exit code 70, and the master immediately spawned a fresh one. The surviving-worker condition the attack depends on never occurred, so the cross-request reuse window never opened.
That matches the external picture: RemiPelloux's research suggests worker survival sits in a narrow, build-and-memory_limit-specific band, roughly 68–88 MB at a 256 MB limit. In other words, it may well be reachable on some real-world configurations; it just is not the behavior of a stock image. That is the crux of the whole CVE: "vulnerable" and "exploitable" are separated here by a thin, config-dependent margin.
A false positive on EOL 7.4, and how we caught it
PHP 7.4 is end-of-life and gets no packaged distro fix, so it is where the real-world risk concentrates. We pushed harder there, on the official php:7.4.33-fpm image, across three rounds.
Round 1 looked like a breakthrough. The worker appeared to survive the OOM bailout in all 18 test configurations: memory_limit from 16M to 128M, across three php-fpm process-manager modes. The exact opposite of 8.4.4. For a moment it looked like EOL PHP was the soft target.
It was a false positive, and we caught it ourselves. The test harness was running with php-fpm's default post_max_size=8M, which silently rejects oversized multipart bodies before upload-progress parsing ever runs. So the OOM never actually fired: the worker "survived" trivially because the crash path was never reached. We were measuring our own test rig, not the bug. (A good reminder that the thing most likely to fool you in this work is your own instrumentation, not the target.)
Rounds 2 and 3, corrected (post_max_size=0, OOM genuinely firing on every request): first, the same Z_TRY_ADDREF refcount guard is present in 7.4.33 too (session.c line 3151), so the original public-repo mechanism does not hold on 7.4 either. For the real OOM mechanism, we have direct log evidence rather than inference: php-fpm's own stderr shows every worker child exiting with code 70, one to two seconds after an OOM-triggering request, every time, in every config, and the master respawning a clean worker each time. The cross-request reuse never becomes possible. 7.4.33 behaves identically to 8.4.4 for this bug. We did not achieve RCE on either.
Has anyone actually done it? Not publicly, except on mod_php
In parallel we went looking for anyone who had achieved and evidenced real RCE: not a crash, not "the code path is reached," not a probe tool that only detects the vuln, and not a chain that needs a second unrelated bug on top. As of 2026-10-21, we could not find one. Every public repo we examined falls into one of three buckets: an unreproducible UAF claim (the original mechanism, which we disproved), an explicit "this is a vulnerability probe, not an exploit" tool, or a chain that additionally requires an app-side bug (a local file inclusion, or a deserialization gadget class already present in the target app) stacked on top of the CVE. That last kind is not a generic, CVE-alone RCE.
Several vendors (Hadrian, Cloudflare's CTO in press quotes, The Stack) describe attacks "emerging" or happening "at scale," but without evidence: no IOCs, no logs, no samples. That language describes exploitation attempts, which absolutely are happening, not confirmed successful code execution. Huntress stated plainly they have neither built a working PoC nor seen in-the-wild exploitation. One account (ikow) claims a full weaponized exploit behind a subscriber paywall: unverified, could be real, could be vapor.
The one genuinely new and significant fact came from PHP's own advisory: it states there is "a demonstrated exploit where an unauthenticated request can trigger the attack before any application code runs," but scopes it specifically to mod_php, the older Apache-embedded PHP module, not php-fpm. PHP did not publish the exploit or name who built it. And this lines up precisely with our findings: mod_php's process model keeps the Apache child process alive across requests by design, so the "does the worker survive the bailout" question that blocked us on php-fpm simply does not arise. On mod_php, the cross-request reuse window comes for free.
What this means for you
Separate what we verified ourselves from what we are citing. We built the bug under ASAN, confirmed the OOM primitive fires on both 8.4.4 and EOL 7.4.33, and showed with direct exit-code and refcount-source evidence that stock php-fpm workers die on the bailout and the public single-request UAF does not hold. The mod_php exploit, the 884k-host Shodan figure, the fifteen-minute prediction, and the worker-survival band are other people's claims that we are reporting, not things we reproduced.
- This is not an overhyped CVE. The bug is real, the primitives are demonstrated, the mechanism is understood, and the emergency patch was warranted. The trigger running before app code means application-layer defenses genuinely cannot stop it.
- mod_php: treat as confirmed-exploitable now. Per PHP's own advisory. Patch immediately, no caveats.
- php-fpm (most modern hosting): no published generic RCE yet. Turning "worker exits on bailout" into "worker survives and gets reused" appears to depend on specific
memory_limitbands and process-manager tuning that stock images do not hit. That gap is real and meaningful, but narrow, and gaps like this close fast once people start probing production configs instead of stock Docker images. - Patch everything, including EOL 7.4/8.0/8.1. PHP shipped source backports for them. "No one has published php-fpm RCE yet" is not "safe," especially when reconstructing a trigger from the patch diff is trivial.
- Watch CVE-2026-109619 separately. It is a distinct, far less publicly dissected bug: an OPcache type-confusion tied to fatal-error backtraces, with a different mitigation (
fatal_error_backtraces=0, available only from PHP 8.5). We did not test it hands-on. It may be an easier path, and deserves attention.
The thing worth sitting with: this is a bug where a generic scanner will flag "PHP version X, CVE present" and stop, with no way to tell whether a given deployment (mod_php versus php-fpm, process-manager mode, memory_limit) sits inside or outside the narrow exploitable band. Whether you are actually exploitable here is a question about your specific configuration, not your version string. That distinction is the entire difference between a vulnerability list and knowing what an attacker can really do with it.
How we tested it, so a skeptic can check our work
- Environment. Disposable Docker sandboxes,
--network none. No live or client target at any point. Officialphp:8.4.4-fpm-bookwormandphp:7.4.33-fpmimages, plus a from-source PHP 8.4.4 built with AddressSanitizer (system malloc,quarantine_size_mb=256). - Single-request UAF test. 12 runs under ASAN (6 variants each of
bug618/bug619), harmless marker payload. Trigger path reached and armed pointers written on every run; zero ASAN reports. Cross-checked against theZ_TRY_ADDREFguard insession.c(~3182–3190 on 8.4.4, 3151 on 7.4.33). - OOM cross-request test. Oversized
upload_progressfilename to exhaustmemory_limit. Confirmed the fatal exhaustion fires at the expected size and path. Direct evidence, from php-fpm's own stderr, shows worker children exiting with code 70 and the master respawning, every time, across every config. - The 7.4 correction. The initial "worker survives" result was traced to the default
post_max_size=8Mrejecting the body before parsing; re-run withpost_max_size=0so the OOM genuinely fired. We report the false positive because hiding it would misrepresent the result. - We did not execute the leaked PoC code. Not needed for a defensive reproduction, and of uncertain origin. We independently rebuilt and tested the described mechanisms instead.
What this does not prove
A result without its limits is marketing, so here are the boundaries.
- Stock images, not every production config. We tested official Docker images and a source build. The worker-survival band others describe may well be reachable on tuned real-world systems we did not test.
- php-fpm, not mod_php. We did not reproduce the mod_php case. PHP's advisory says a working exploit exists there; we are reporting that, not confirming it ourselves.
- 109618, not 109619. The second CVE was not tested hands-on. Our read of it is from the advisory and public discussion only.
- A point in time. This is the state of public knowledge on 2026-10-21. The gap between vulnerable and weaponized is exactly the kind that narrows as more people look.
Frequently asked questions
What are CVE-2026-109618 and CVE-2026-109619?
Two critical remote-code-execution bugs in the PHP interpreter itself (CVSS 9.2), in the session.upload_progress upload-tracking feature. PHP shipped an emergency patch on 2026-10-21, with source backports even for the EOL 7.4, 8.0 and 8.1 branches. Because the flaw is in PHP core, it reaches any PHP app: Magento, Shopware, WooCommerce, WordPress, Laravel, Symfony and the rest.
Is there a working public exploit for php-fpm?
As of 2026-10-21 we could not find a published, generic, CVE-alone RCE against php-fpm. The public repos are each an unreproducible UAF claim, an explicit vulnerability probe rather than an exploit, or a chain that also needs an app-side bug. PHP's own advisory cites a demonstrated exploit but scopes it to mod_php, not php-fpm.
Why can't a WAF or input validation stop this?
The vulnerable code runs in PHP's native multipart request parser, before any application code executes. The trigger fires while PHP is still reading the body, so app-layer WAF rules and validation never get to see it.
Does the default php-fpm config protect me?
In our testing on stock official images the worker always exited after the OOM abort and the master spawned a fresh one, so the cross-request window never opened. But external research suggests worker survival depends on a narrow memory_limit band and process-manager tuning that stock images do not hit, so some configurations may differ. Patch regardless.
Should I patch EOL PHP like 7.4, 8.0 or 8.1?
Yes. PHP issued source backports for those branches even though most distros will not package a fix. Reconstructing a trigger from the patch diff is trivial, so "no published RCE yet" is not "safe."