Skip to content
Chinh Hoang
Go back

How to check that PHP is really patched for PHP2Shell when the version number does not change

Markdown
Open in ChatGPTOpen in Claude
Contents
  1. Does this guide apply to you?
  2. 1. Check the binary for the fix marker
  3. 2. Check the processes that are running now
  4. 3. Reload PHP-FPM, restart Apache
  5. Docker images
  6. What a marker does not prove

A backported PHP2Shell fix keeps the old PHP version, so php -v cannot tell you if you are safe. Grep the binary for a string only the fix adds, then check that no running PHP process still uses the old, deleted binary.

On 21 October 2026 PHP disclosed two remote code execution flaws, called PHP2Shell: CVE-2026-109618 and CVE-2026-109619. php.net released fixes for 8.2.35, 8.3.36, 8.4.27 and 8.5.12 only. For 7.4, 8.0 and 8.1 the fix comes as a backport from your distro, and the PHP version stays the same: a fixed Ubuntu 22.04 build still says 8.1.2, with only the package revision after it changed.

The output below comes from a throwaway Ubuntu 22.04 container with php8.1-fpm, run on 28 October 2026, before and after the fix.

Does this guide apply to you?

1. Check the binary for the fix marker

FixMarker strings
Full fix (8.2.35 and newer)memory_hard_limit_percent and Hard memory limit of %zu bytes exhausted
EOL backport for 7.4, 8.0, 8.1 (7.4 commit; 8.0 and 8.1 have matching ones)zend_mm_safe_error_ex or Fatal error: %s in %s on line %d

Save this as check-php-binary.sh:

#!/bin/sh
# check-php-binary.sh FILE — prints full-fix, eol-backport or NONE
FILE=${1:?usage: check-php-binary.sh /path/to/php-fpm-or-libphp}
if grep -aqF 'memory_hard_limit_percent' "$FILE" &&
   grep -aqF 'Hard memory limit of %zu bytes exhausted' "$FILE"; then
  echo "full-fix"
elif grep -aqF 'zend_mm_safe_error_ex' "$FILE" ||
     grep -aqF 'Fatal error: %s in %s on line %d' "$FILE"; then
  echo "eol-backport"
else
  echo "NONE"
fi

Run it on the binary your website uses (php-fpm* or Apache’s libphp*.so), not the CLI. They come from different packages.

Before the fix:

# php-fpm8.1 -v | head -1
PHP 8.1.2 (fpm-fcgi) (built: Apr  7 2022 17:46:26)
# sh check-php-binary.sh /usr/sbin/php-fpm8.1
NONE

After apt-get install --only-upgrade php8.1-fpm:

# dpkg-query -W php8.1-fpm
php8.1-fpm	8.1.2-1ubuntu2.28
# sh check-php-binary.sh /usr/sbin/php-fpm8.1
eol-backport

2. Check the processes that are running now

Upgrading replaces the file on disk. A process that started earlier keeps running the old code, and Linux marks it (deleted) in /proc/PID/exe. This script reads the code each PHP process actually loaded. Run it as root on the host.

#!/bin/bash
# check-php-procs.sh — run as root
set -uo pipefail
markers() {
  if ! head -c 4 "$1" >/dev/null 2>&1; then echo "UNREADABLE(run as root on the host)"; return; fi
  if grep -aqF 'memory_hard_limit_percent' "$1" && grep -aqF 'Hard memory limit of %zu bytes exhausted' "$1"; then echo "full-fix"
  elif grep -aqF 'zend_mm_safe_error_ex' "$1" || grep -aqF 'Fatal error: %s in %s on line %d' "$1"; then echo "eol-backport"
  else echo "NONE"; fi
}
for d in /proc/[0-9]*; do
  pid=${d#/proc/}
  comm=$(cat "$d/comm" 2>/dev/null) || continue
  case "$comm" in php*|httpd*|apache2*|frankenphp*|lsphp*|litespeed*) ;; *) continue ;; esac
  exe=$(readlink "$d/exe" 2>/dev/null) || exe="?"
  mnt=$(readlink "$d/ns/mnt" 2>/dev/null)
  if [ -z "$mnt" ]; then ns="?"; elif [ "$mnt" = "$(readlink /proc/self/ns/mnt)" ]; then ns="host"; else ns="other-mnt-ns(container?)"; fi
  echo "PID $pid comm=$comm ns=$ns"
  case "$comm" in
    httpd*|apache2*|litespeed*) echo "  exe: $exe  (web server; PHP is in the lib lines below)" ;;
    *) echo "  exe: $exe  markers=$(markers "$d/exe")" ;;
  esac
  awk '$6 ~ /libphp|mod_php|php[0-9.]*\.so/ && $2 ~ /x/ {print $1, $6, $7}' "$d/maps" 2>/dev/null | sort -u |
  while read -r range path deleted; do
    echo "  lib: $path ${deleted:-}  markers=$(markers "$d/map_files/$range")"
  done
done

Right after the upgrade, when dpkg already showed the fixed package:

# bash check-php-procs.sh
PID 6920 comm=php-fpm8.1 ns=host
  exe: /usr/sbin/php-fpm8.1 (deleted)  markers=NONE
PID 6921 comm=php-fpm8.1 ns=host
  exe: /usr/sbin/php-fpm8.1 (deleted)  markers=NONE
PID 6922 comm=php-fpm8.1 ns=host
  exe: /usr/sbin/php-fpm8.1 (deleted)  markers=NONE

(deleted) means the site still runs the vulnerable code. For mod_php, read the lib: lines.

3. Reload PHP-FPM, restart Apache

For PHP-FPM a reload is enough: on SIGUSR2 the master reloads its binary (php-fpm(8)), and systemctl reload sends exactly that:

# grep ExecReload /lib/systemd/system/php8.1-fpm.service
ExecReload=/bin/kill -USR2 $MAINPID

After systemctl reload php8.1-fpm (the test sent SIGUSR2 directly):

# bash check-php-procs.sh
PID 13075 comm=php-fpm8.1 ns=host
  exe: /usr/sbin/php-fpm8.1  markers=eol-backport
PID 13076 comm=php-fpm8.1 ns=host
  exe: /usr/sbin/php-fpm8.1  markers=eol-backport
PID 13077 comm=php-fpm8.1 ns=host
  exe: /usr/sbin/php-fpm8.1  markers=eol-backport

No (deleted), and every process has the marker. For Apache with mod_php, run systemctl restart apache2. In a test, apachectl graceful kept the old, deleted libphp. Then check again.

Docker images

The official docker-library/php images for 7.4, 8.0 and 8.1 build PHP from source and are no longer maintained, so apt upgrade cannot fix them. Move to php:8.2-fpm or newer, rebuild, recreate the container, and run check-php-procs.sh on the Docker host. It also sees processes inside containers (ns=other-mnt-ns(container?)).

What a marker does not prove

A marker proves the official fix code is in the binary. The EOL backport is a “minimal mitigation”, not the full fix. The CVE-2026-109619 advisory adds: “It might be prudent to set session.upload_progress.enabled=0 if you don’t need this feature even after applying the patch”. The lasting fix is PHP 8.2 or newer.


Share this post:

Previous Post
How to make Claude Code follow a rule: a PreToolUse hook that refused git commit -a 40 times
Next Post
Does a Simplified Technical English prompt make LLM output shorter? Five runs each