DEV Community

Danila Vershinin for GetPageSpeed

Posted on Originally published at getpagespeed.com Fully Autonomous

Backporting the PHP2Shell fix to PHP 5.6 and 7.0 on CentOS 7

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:

  1. 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 in zend_alloc, zend_hash, the string-concat VM handlers and friends routes through it.
  2. Reset request globals on shutdown. NULL out freed request strings in SAPI.c, clear http_globals, reset user error and exception handlers, the handler stacks and the object store at the end of shutdown_executor().
  3. 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_shutdown block in zend_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 CONCAT extend path had no overflow check at all. We added the upstream check, then regenerated zend_vm_execute.h with zend_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_parts patch, so those rfc1867.c hunks were dropped instead of duplicated.

7.0 needed real adaptation:

  • 7.0 has no _zend_new_array_0(), so array_init_singleton() became array_init() plus nTableSize = 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 of shutdown_executor(), after every destroy.
  • 7.0's estrdup overflow sites passed an int to %zu; we used upstream's format.
  • 7.0 is C89, so the session hook is disabled with || 1 rather than true.

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:

  1. Install the exact Remi NEVRAs the host has today.
  2. Run both PHP2Shell probes and require them to reproduce. The out-of-memory probe exits 255 and the upload-progress hook runs.
  3. yum upgrade to our build. Require both probes to fail: the OOM probe exits 1 and the hook never runs.
  4. Run the full phpt suite against the baseline. The only accepted differences are nine tests whose behaviour changes by design (eight rfc1867 tests and bug45392).
  5. Diff php -m (including PECL and ionCube), check configuration is preserved and PHP-FPM serves after an OOM request.
  6. yum downgrade back 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
Enter fullscreen mode Exit fullscreen mode

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)