The fix, mapped to the code

The editor asked for the kind of source-code mapping we did at secrets.sgit.ai/review: the project as layered graphs, the intent on one side and the code derived from the syntax tree on the other, with the join between them. Here the intent is the two flaws and the design of the patch; the code is what the patch changed in php-src, read from the diff by a parser rather than a model. Three layers, 54 nodes and 68 edges; walk them in the navigator, or read the map below.

Commit 3584a98bd · 389 files changed, 37 of them in the engine and bundled extensions traced here · 36 areas

The flaws

CVE-2026-109618: RCE via OOM corruption An out-of-memory condition during request handling could leave request memory in an uninitialised state that later cleanup reads. CVSS v4.0 9.2. GHSA-w8hw-w9fx-j79w. CVE record

CVE-2026-109619: RCE via stale global state Global state that referenced request memory was not always reset, so a later request on the same worker could read freed memory. CVSS v4.0 9.2. GHSA-6wmf-4rv5-p3rp. CVE record

What the fix does, and where

Each card is one thing the patch does, with the functions it uses and the files it changed. The text describes the remediation; how the flaw is reached is on the two flaws and the code, which this layer sits beside.

A soft memory limit that no longer jumps

At the memory limit the allocator stops using the longjmp bailout; it queues a delayed error and lets the allocation proceed, so the fatal is raised later with request state intact.

Functions. zend_mm_check_limit, zend_mm_delayed_error

Addresses: CVE-2026-109618

A hard limit that ends the process cleanly

A new memory_hard_limit_percent sets a ceiling above memory_limit; reaching it ends the worker with _exit(), leaving no cleanup to corrupt. zend_mm_panic also uses _exit now.

Functions. zend_mm_hard_limit_error · new setting memory_hard_limit_percent

Addresses: CVE-2026-109618

An orderly unwind at the next safe point

The queued error is handled at a VM safe point: it throws an UnwindExit that unwinds the PHP frames in order before the fatal, rather than a C longjmp across half-built state.

Functions. zend_handle_delayed_oom, zend_throw_unwind_exit

Addresses: CVE-2026-109618

Bounded, deduplicated request parsing

A new max_multipart_body_parts caps the number of parts the body parser accepts; the multipart header parser keeps only the two headers it uses and ignores duplicates; the session upload-progress callback is changed.

Functions. multipart_buffer_headers, rfc1867_post_handler · new setting max_multipart_body_parts

Addresses: CVE-2026-109619

The change, by area

The patch is 389 files because the new allocation contract touched every place in the engine and the bundled extensions that used to abort on a failed allocation. The twenty largest areas:

AreaFilesAddedRemoved
Zend/962932268
tests/3411091
ext/standard52106185
ext/mbstring3057076
ext/gd1949817
ext/session541663
ext/soap143758
main/734550
ext/zlib1134410
ext/filter83359
ext/date17326122
ext/exif831823
ext/zend_test827842
ext/xmlreader82365
ext/pcre818610
ext/fileinfo316851
ext/iconv714611
ext/json813723
ext/simplexml413426
ext/opcache711311

The full per-file data, with the functions each hunk changed, is data/review.json and review/code/files.json, derived from git show of the commit.

How this was built