PHP2Shell (CVE-2026-109618 and CVE-2026-109619) is a pair of unauthenticated RCEs in PHP, rated CVSS v4.0 9.2. The PHP project fixed 8.2 and newer, published source-only backports for 7.4, 8.0 and 8.1, and nothing for 7.3 and older. CentOS 7 boxes still running Remi's PHP 5.6 or 7.0 had no binary fix anywhere. Here is how we ported it and how we convinced ourselves the port is right.
What the upstream fix actually does
Both bugs share a root cause: PHP's out-of-memory handling is a longjmp (zend_bailout()). When that fires in the middle of request startup, say while rfc1867.c is parsing a multipart body, request state is left half built. Cleanup then trusts it (CVE-2026-109618), or a freed pointer survives into the next request on the same worker (CVE-2026-109619).
The security team's minimal backport for EOL branches (php-src branch PHP-7.4-CVE-2026-109618-109619, commit 712e60f9) does three things:
-
Stop unwinding on allocator failure. A new
zend_mm_safe_error_ex()prints the error and calls_exit(1)instead of bailing out. Every overflow site inzend_alloc,zend_hash, the string-concat VM handlers and friends routes through it. -
Reset request globals on shutdown. NULL out freed request strings in
SAPI.c, clearhttp_globals, reset user error and exception handlers, the handler stacks and the object store at the end ofshutdown_executor(). -
Close the known entry points. Header whitelisting and de-duplication in
rfc1867.c, and the session upload-progress hook disabled outright.
Porting to 7.3 and 7.0
7.3 is close enough to 7.4 that most hunks applied cleanly. The interesting differences:
- 7.3 has no pre-free
!fast_shutdownblock inzend_execute_API.c, so the handler reset has to go where 7.4 runs it on the fast-shutdown path, before the object store is freed. - 7.3's in-place
CONCATextend path had no overflow check at all. We added the upstream check, then regeneratedzend_vm_execute.hwithzend_vm_gen.php(first proving the generator is byte-identical on the unpatched tree). The result has 34 safe-exit sites, the same as upstream 7.4. - Remi's base already carries the CVE-2023-0662
max_multipart_body_partspatch, so thoserfc1867.chunks were dropped instead of duplicated.
7.0 needed real adaptation:
- 7.0 has no
_zend_new_array_0(), soarray_init_singleton()becamearray_init()plusnTableSize = 1, the same table shape upstream creates. - 7.0 has no fast-shutdown branch and destroys the handler stacks inside a
zend_try. The reset runs once at the very end ofshutdown_executor(), after every destroy. - 7.0's
estrdupoverflow sites passed anintto%zu; we used upstream's format. - 7.0 is C89, so the session hook is disabled with
|| 1rather thantrue.
PHP 5.6: start from a vendor port, then fix it
For 5.6 we started from a commercial vendor's published 5.6 adaptation and audited it hunk by hunk against upstream 7.4 and our own 7.0 port. It was right about the allocator, rfc1867.c, SAPI.c and the session hook. It missed part of the shutdown reset: it cleared error_handling and exception, but not the user error and exception handlers, the three handler stacks, or the object store. In 5.6, zend_ptr_stack_destroy() leaves elements dangling, which is exactly the stale-state shape CVE-2026-109619 exploits. We added the missing resets.
A public community patch set for 5.6 touches only the allocator, session and rfc1867.c, and omits the shutdown resets entirely.
Proving it
Each build runs through the same harness in a pristine CentOS 7 container:
- Install the exact Remi NEVRAs the host has today.
- Run both PHP2Shell probes and require them to reproduce. The out-of-memory probe exits 255 and the upload-progress hook runs.
-
yum upgradeto our build. Require both probes to fail: the OOM probe exits 1 and the hook never runs. - Run the full phpt suite against the baseline. The only accepted differences are nine tests whose behaviour changes by design (eight
rfc1867tests andbug45392). - Diff
php -m(including PECL and ionCube), check configuration is preserved and PHP-FPM serves after an OOM request. -
yum downgradeback to Remi and confirm it works.
Builds that pass went to production the next day on PrestaShop 1.6 and Magento 1 servers with nothing more than a PHP-FPM restart.
The trade-off
With the fix, hitting memory_limit kills the worker immediately: no shutdown functions, no flushed output. Upstream made the same call for its EOL backports. If your code relies on a shutdown handler to log memory exhaustion, it will go quiet.
Install
The packages are free in the GetPageSpeed repository and replace Remi's in place:
yum -y install https://extras.getpagespeed.com/release-latest.rpm
yum --enablerepo=getpagespeed-extras-php70 upgrade 'php-*' # or php56, php80
systemctl restart php-fpm
PHP 7.3 (Remi SCL), PHP 8.4.27, and the full per-version details are in the PHP2Shell on CentOS 7 guide.
Top comments (0)