🚀 New: chi (χ) — an open-source autoresearch harness for fleets of LLM coding agents. Read the announcement.

PHP's memory_limit Was the Attack Surface: Two Critical RCEs Hide in the Out-of-Memory Path

PHP 8.5.12, 8.4.27, 8.3.36 and 8.2.35 fix two unauthenticated RCEs rooted in longjmp() on out-of-memory. The stopgaps invert normal hardening: memory_limit=-1 helps, RLIMIT_AS hurts.

Contents

On 2026-10-21 the PHP project released 8.5.12, 8.4.27, 8.3.36 and 8.2.35 to fix two unauthenticated remote code execution bugs, CVE-2026-109618 and CVE-2026-109619, both rated CVSS v4 9.2. Patching is the whole answer. The interesting part is the mechanism, because it explains why the interim advice contradicts what most of us configure by habit: the guard that limits memory is what gets exploited.

A limit that works by jumping out

PHP enforces memory_limit with longjmp(). When an allocation crosses the limit, execution leaves the current context at once, skipping whatever was in the middle of being set up. The advisory for CVE-2026-109618 states the consequence directly: in some contexts request memory is reachable before it is fully initialised, PHP does not account for the early exit, and request shutdown “can read or branch on the invalid memory.” The failed allocation does not need to be related to the half-built structure. Any allocation that trips the limit at the wrong moment is enough.

Form-based upload parsing is one such moment. The advisory lists the ways an unauthenticated client can force an out-of-memory condition there:

  • A multipart request without a usable declared length can pass the initial post_max_size check and keep supplying decoded body data.
  • Headers inside a multipart part are unrestricted, and their PHP representation can take far more memory than they do on the wire, so a body under post_max_size can still exhaust memory_limit.
  • Empty file controls do not count toward max_file_uploads but still create file metadata arrays, and nested field names multiply that work.

None of this requires application code to run. The project says the demonstrated exploit fires before any application code executes.

Which servers are exposed

The advisory is specific about how each SAPI handles a failed startup, and that decides how reachable the bug is.

SAPIAdvisory’s assessment
Apache mod_phpCalls request shutdown even when startup fails. Validated unauthenticated RCE with default limits.
PHP-FPMExits the child when startup fails, which limits exploitation. Source analysis supports susceptibility after normal startup; no complete exploit demonstrated.
FrankenPHPLikely susceptible from source analysis, since it calls shutdown unconditionally. Not validated.
LSAPIModels FPM. Application-specific exploits likely, unvalidated.

The wording matters. “Not demonstrated” for FPM is a statement about the current state of the research, not about the code. The PHP announcement lists FPM, mod_php, php-cgi, FrankenPHP and LiteSpeed as all at risk, with mod_php highest. The validated attack ran against prefork configurations with several workers, and the advisory says extra workers or traffic raise the number of attempts needed but do not remove the condition.

The second bug needs two requests

CVE-2026-109619 is the same family seen from a different side. A persistent worker holds global state, in the error, session, object or extension subsystems, that can point at memory owned by a request. If a fatal error interrupts an operation before its reset runs, the request heap is freed while the global still references it. A later operation in the same worker, running before normal request initialisation, reads or frees that stale pointer.

So exploitation needs a producer and a consumer, usually separate HTTP requests served by one worker. The advisory names three confirmed paths:

  • PHP 8.5 with fatal_error_backtraces enabled, where backtrace state is not reset after a fatal error. This is the confirmed RCE path against mod_php.
  • Session upload progress, where an OOM during multipart input leaves decoder state stale and a later upload-progress operation consumes it. Validated on mod_php and PHP-FPM.
  • Warnings or deprecations raised while applying INI settings before request initialisation, confirmed for mod_php .htaccess and php_value or php_admin_* directives.

Note how the two bugs connect. The first can be the trigger that leaves state stale for the second, and the second lists OOM and execution timeouts as ways to interrupt a reset. That is why the advisory says to treat timeouts as fatal errors when assessing exposure.

The stopgaps point the opposite way

The temporary mitigations are worth reading closely, because several invert normal practice. The project is explicit that they reduce exposure to specific paths and do not make an installation safe.

For CVE-2026-109618:

  • Set memory_limit=-1, ideally with php_admin_value in the FPM pool or Apache config so applications cannot override it, and check that no pool, directory or application setting restores a finite value. Removing the controllable limit removes the easy trigger. It is incomplete: the advisory notes that if mmap() or another allocation fails for real, longjmp() can still run.
  • Do not use RLIMIT_AS or RLIMIT_DATA as a substitute. They make mmap() fail, which is exactly the vulnerable allocation failure.
  • A cgroup memory.max is an availability boundary, with a caveat: killing a container can take down the master and every worker unless each worker has its own group.
  • Set post_max_size to 8M if you do not need uploads, or 32M if you do, and keep upload_max_filesize below it. Enforce decoded request size, field count, part count, header count and nesting depth at the proxy, including for requests without a usable Content-Length.

For CVE-2026-109619:

  • session.upload_progress.enabled=0, set in php.ini, the FPM pool or server config, then restart every worker. ini_set() and .user.ini do not apply early enough. The advisory suggests keeping this setting even after patching.
  • On PHP 8.5, fatal_error_backtraces=0 closes only the confirmed backtrace path.
  • Hiding diagnostics with error_reporting, display_errors or @ does not help, because the state goes stale regardless of whether the message is shown.
  • pm.max_requests=1 limits worker reuse if performance allows.

The advisory also warns that clean logs do not prove safety, and recommends log_errors=On, a working error_log and error_reporting=E_ALL for auditing.

What the fix changes

The patch removes longjmp() from the OOM path. It replaces it with two thresholds: a soft OOM, delayed until the next VM safe point so the engine reaches a consistent state first, and a hard OOM that calls _exit() immediately when usage hits a second, higher threshold. Shutdown therefore never sees half-initialised state, and ordinary memory limits still apply. Reading the advisory’s description, memory_limit becomes a request to stop at a safe moment rather than a jump out of whatever was running, with _exit() as the backstop.

For operators the practical consequence is that the old hardening instinct, cap memory as tightly as you can at every layer, was a workaround for a design that is now gone. After upgrading, the memory_limit=-1 stopgap should be reverted to a sensible finite value. That conclusion is my reading of the fix description, not something the advisory states, so check it against your vendor’s release notes.

Patching details that are easy to miss

  • PHP 8.1 and older get no upstream updates. The project published minimal source backports for 7.4, 8.0 and 8.1 on branches named PHP-7.4-CVE-2026-109618-109619, PHP-8.0-CVE-2026-109618-109619 and PHP-8.1-CVE-2026-109618-109619. Check your distribution or LTS vendor first.
  • The project says the volunteer-run rpms.remirepo.net repository would not have updates on release day, and recommends distribution packages or a source build.
  • Products that embed PHP can be affected without exposing PHP directly. CERT-EU points to the NCSC-NL php2shell repository for identifying them.
  • Technical details are public and CERT-EU reports proofs of concept circulating, so assume scanning is under way.

Sources