#!/usr/bin/env bash
# Baota PHP82 CVE-2026-109618 / CVE-2026-109619 updater.
# Scope: existing source-built /www/server/php/82 on Linux + existing dependencies.
# No yum/apt upgrade, nginx edits, database edits, or unrelated PHP retirement.
# Default --check is read-only. --apply performs a backup and a bounded same-branch update.
set -Eeuo pipefail
umask 077
TARGET=8.2.35
ARCHIVE_SHA256=5737b98601fe5ab1b8c5db0c82ed3158acb9283ed3081ffe2faffb9ac81455e6
PREFIX=/www/server/php/82
TASK=/root/php-security-20261022
MODE=${1:---check}
case "$MODE" in --help|-h) printf '%s\n' 'Usage: bash baota-php82-security-fix.sh [--check|--apply]' 'Only existing Baota PHP8.2 source installations are supported. Read the linked article first.'; exit 0;; --check|--apply) ;; *) echo 'Unknown option; use --help' >&2; exit 2;; esac
fail(){ echo "STOP: $*" >&2; if [[ ${SWITCHING:-0} == 1 ]]; then SWITCHING=0; trap - ERR; restore_previous; fi; exit 1; }
[[ $(uname -s) == Linux ]] || fail 'Linux only; no changes made.'
for cmd in grep awk sed sha256sum readlink ps pgrep stat head; do command -v "$cmd" >/dev/null || fail "Missing $cmd"; done
[[ -x "$PREFIX/bin/php" && -x "$PREFIX/sbin/php-fpm" && -x "$PREFIX/bin/php-config" ]] || fail 'Expected Baota PHP82 executables not found.'
[[ -x /www/server/nginx/sbin/nginx ]] || fail 'This tested script requires Baota Nginx; Apache and other stacks require a separate procedure.'
BINDINGS=$(/www/server/nginx/sbin/nginx -T 2>&1 | grep -E 'include[[:space:]]+enable-php-82[.]conf|fastcgi_pass.*php-cgi-82[.]sock' || true)
[[ -n "$BINDINGS" ]] || fail 'No PHP82 binding found in active Nginx configuration; manual audit required.'
echo 'PHP82 bindings found in active Nginx config (not printed to avoid private vhost details).'
VERSION=$("$PREFIX/bin/php" -n -r 'echo PHP_VERSION;')
[[ $VERSION == 8.2.* ]] || fail "Expected8.2, got $VERSION. This script does not migrate major/minor branches."
echo "Installed CLI: $VERSION"
"$PREFIX/sbin/php-fpm" -v | head -2
"$PREFIX/sbin/php-fpm" -i | grep -E '^(Loaded Configuration File|memory_limit|post_max_size|session.upload_progress.enabled)' || true
ps -eo pid,args | grep '[p]hp-fpm: master process' || true
if "$PREFIX/bin/php" -n -r "exit(version_compare(PHP_VERSION, '$TARGET', '>=') ? 0 : 1);"; then
 echo "On-disk PHP82 is >=$TARGET; no downgrade/install performed. Verify every running worker separately."
 for pid in $(pgrep -x php-fpm || true); do
  exe=$(readlink "/proc/$pid/exe" || true)
  if [[ "$exe" == "$PREFIX/sbin/php-fpm"* ]]; then
   actual=$(sha256sum "/proc/$pid/exe" | awk '{print $1}')
   expected=$(sha256sum "$PREFIX/sbin/php-fpm" | awk '{print $1}')
   [[ "$actual" == "$expected" ]] || fail "PID$pid still maps an old/different binary; no repair-complete claim."
  elif [[ -n "$exe" ]]; then echo "WARNING: another FPM runtime needs separate audit: $exe"; fi
 done
 exit 0
fi
[[ "$MODE" == --apply ]] || { echo 'Potentially affected old PHP82. --check made no changes; inspect bindings/backups before --apply.'; exit 0; }
[[ $(id -u) == 0 ]] || fail '--apply requires root.'
for cmd in curl tar xz python3 make gcc nice flock install mv cmp df cp chmod ldd find tee dirname getconf sleep cut; do command -v "$cmd" >/dev/null || fail "Missing build prerequisite: $cmd (not auto-installed)."; done
# Fresh source builds are refused on unvalidated old toolchains or low available RAM.
# Staged artifacts still undergo source hash, module, configuration and live-worker checks below.
if [[ ! -f "$TASK/build-success" ]]; then
 GCC_MAJOR=$(gcc -dumpversion | cut -d. -f1)
 AVAIL_KB=$(awk '/MemAvailable:/{print $2}' /proc/meminfo)
 [[ ${GCC_MAJOR:-0} -ge 9 ]] || fail 'Fresh on-host build requires GCC9+; old CentOS7 toolchains need a separately validated isolated build.'
 [[ ${AVAIL_KB:-0} -ge 768000 ]] || fail 'Less than750MiB available RAM. Refusing on-host compilation; use a separately validated isolated build.'
fi
[[ -x /etc/init.d/php-fpm-82 ]] || fail 'Missing Baota SysV PHP82 service script; do not guess a restart method.'
[[ -f "$PREFIX/etc/php.ini" && -f "$PREFIX/etc/php-cli.ini" && -f "$PREFIX/etc/php-fpm.conf" ]] || fail 'Expected INI/FPM configuration missing.'
# Avoid applying a single-version fix while another live version remains silently exposed.
for pid in $(pgrep -f '^php-fpm: master process' || true); do
 exe=$(readlink "/proc/$pid/exe" || true)
 [[ "$exe" == "$PREFIX/sbin/php-fpm" ]] || fail "Another PHP-FPM installation is running ($exe). Audit it separately; no production files changed."
done
[[ ! -L "$TASK" ]] || fail 'Task directory must not be a symlink.'
mkdir -p "$TASK"; [[ $(stat -c %u "$TASK") == 0 ]] || fail 'Task directory is not root-owned.'
chmod 700 "$TASK"
exec 9>"$TASK/repair.lock"
flock -n 9 || fail 'Another repair instance is active; do not run twice.'
exec > >(tee -a "$TASK/repair.log") 2>&1
printf 'Started: '; date -Is
AVAILABLE_KB=$(df -Pk "$TASK" | awk 'NR==2{print $4}')
[[ "$AVAILABLE_KB" -ge 2097152 ]] || fail 'Need at least2GiB free for source, staging and backup.'
"$PREFIX/sbin/php-fpm" -t -y "$PREFIX/etc/php-fpm.conf"
if [[ ! -f "$TASK/backup-complete" ]]; then
 tar -czf "$TASK/php82-before.tar.gz" -C /www/server/php 82
 cp -a /etc/init.d/php-fpm-82 "$TASK/php-fpm-82"
 cp -a /www/server/panel/vhost/nginx "$TASK/nginx-vhosts-before"
 sha256sum "$PREFIX/bin/php" "$PREFIX/sbin/php-fpm" > "$TASK/binaries-before.sha256"
 "$PREFIX/bin/php" -m > "$TASK/modules-before.txt"
 "$PREFIX/bin/php-config" --configure-options > "$TASK/configure-options.before.txt"
 date -Is > "$TASK/backup-complete"
fi
[[ -s "$TASK/php82-before.tar.gz" && -s "$TASK/modules-before.txt" ]] || fail 'Backup manifest incomplete.'
tar -tzf "$TASK/php82-before.tar.gz" >/dev/null || fail 'Backup archive integrity check failed.' 
cd "$TASK"
if [[ ! -f php-8.2.35.tar.xz ]]; then
 curl -fL --retry 2 --connect-timeout 15 --max-time 300 https://www.php.net/distributions/php-8.2.35.tar.xz -o php-8.2.35.tar.xz
fi
printf '%s  %s\n' "$ARCHIVE_SHA256" php-8.2.35.tar.xz | sha256sum -c -
STAGE="$TASK/stage$PREFIX"
if [[ ! -f "$TASK/build-success" ]]; then
 [[ -d php-8.2.35 ]] || tar -xf php-8.2.35.tar.xz
 cd php-8.2.35
 export PKG_CONFIG_PATH=/usr/local/onig/lib/pkgconfig:/usr/local/libsodium/lib/pkgconfig:${PKG_CONFIG_PATH:-}
 python3 - <<'PY'
import shlex, subprocess
args=shlex.split(subprocess.check_output(['/www/server/php/82/bin/php-config','--configure-options']).decode())
subprocess.check_call(['./configure']+args)
PY
 # At most2 jobs. Lower CPU priority and fall back to1 when available RAM is low.
 JOBS=1
 AVAIL=$(awk '/MemAvailable:/{print $2}' /proc/meminfo)
 if [[ ${AVAIL:-0} -ge 700000 && $(getconf _NPROCESSORS_ONLN) -ge 2 ]]; then JOBS=2; fi
 echo "Compiling in private directory with jobs=$JOBS, nice10; production not overwritten."
 nice -n 10 make -j"$JOBS"
 make install INSTALL_ROOT="$TASK/stage"
 date -Is > "$TASK/build-success"
fi
[[ $("$STAGE/bin/php" -n -r 'echo PHP_VERSION;') == "$TARGET" ]] || fail 'Staged CLI is not the pinned target version.'
"$STAGE/sbin/php-fpm" -v | grep -F "PHP $TARGET" >/dev/null || fail 'Staged FPM is not target version.'
for binary in "$STAGE/bin/php" "$STAGE/sbin/php-fpm"; do
 if ldd "$binary" | grep -q 'not found'; then fail "Missing shared library for $binary"; fi
done
"$STAGE/bin/php" -c "$PREFIX/etc/php-cli.ini" -m > "$TASK/modules-staged.txt" 2>"$TASK/modules-staged.stderr"
[[ ! -s "$TASK/modules-staged.stderr" ]] || fail 'Staged PHP emitted extension/startup warnings; inspect private log.'
python3 - "$TASK/modules-before.txt" "$TASK/modules-staged.txt" <<'PY'
import sys
from pathlib import Path
def mods(p):return {x.strip() for x in Path(p).read_text().splitlines() if x.strip() and not x.startswith('[')}
missing=mods(sys.argv[1])-mods(sys.argv[2])
if missing:raise SystemExit('Missing old modules: '+', '.join(sorted(missing)))
PY
"$STAGE/sbin/php-fpm" -t -y "$PREFIX/etc/php-fpm.conf"
mkdir -p "$TASK/replace-before"
# Replace the runtime and its development metadata, plus the matching bundled OPcache.
# Preserve etc/, external extensions (e.g.zip), pool/socket configuration and application files.
FILES=(bin/php bin/php-cgi sbin/php-fpm bin/php-config bin/phpize)
[[ ! -f "$STAGE/bin/phpdbg" ]] || FILES+=(bin/phpdbg)
while IFS= read -r file; do FILES+=("${file#$STAGE/}"); done < <(find "$STAGE/lib/php/extensions" -type f -name opcache.so 2>/dev/null)
for rel in "${FILES[@]}"; do
 [[ -f "$STAGE/$rel" ]] || fail "Missing staged file $rel"
 mkdir -p "$TASK/replace-before/$(dirname "$rel")"
 if [[ -f "$PREFIX/$rel" ]]; then cp -a "$PREFIX/$rel" "$TASK/replace-before/$rel"; fi
done
# Rollback is for availability only: the previous runtime remains vulnerable.
restore_previous(){
 echo 'ROLLBACK: preserving availability; original vulnerable runtime is restored. Repair is NOT complete.' >&2
 /etc/init.d/php-fpm-82 stop || true
 for rel in "${FILES[@]}"; do
  if [[ -f "$TASK/replace-before/$rel" ]]; then cp -a "$TASK/replace-before/$rel" "$PREFIX/$rel.rollback-new"; mv -f "$PREFIX/$rel.rollback-new" "$PREFIX/$rel"; fi
 done
 /etc/init.d/php-fpm-82 start || true
 date -Is > "$TASK/ROLLED-BACK-NOT-FIXED"
}
SWITCHING=1
on_error(){ rc=$?; trap - ERR; if [[ ${SWITCHING:-0} == 1 ]]; then restore_previous; fi; exit "$rc"; }
on_interrupt(){
 trap - ERR INT TERM HUP
 if [[ ${SWITCHING:-0} == 1 ]]; then restore_previous; fi
 exit 130
}
trap on_error ERR
trap on_interrupt INT TERM HUP
for rel in "${FILES[@]}"; do
 install -m 0755 "$STAGE/$rel" "$PREFIX/$rel.security-new"
 mv -f "$PREFIX/$rel.security-new" "$PREFIX/$rel"
done
"$PREFIX/sbin/php-fpm" -t -y "$PREFIX/etc/php-fpm.conf"
/etc/init.d/php-fpm-82 restart
sleep 2
LIVE_HASH=$(sha256sum "$PREFIX/sbin/php-fpm" | awk '{print $1}')
SEEN=0
for pid in $(pgrep -x php-fpm || true); do
 exe=$(readlink "/proc/$pid/exe" || true)
 if [[ "$exe" == "$PREFIX/sbin/php-fpm" || "$exe" == "$PREFIX/sbin/php-fpm (deleted)" ]]; then
  hash=$(sha256sum "/proc/$pid/exe" | awk '{print $1}')
  [[ "$hash" == "$LIVE_HASH" ]] || fail "PID$pid still maps a different/old runtime."
  SEEN=$((SEEN+1))
 fi
done
[[ "$SEEN" -gt 0 ]] || fail 'No running updated PHP82 process found.'
"$PREFIX/bin/php" -v
"$PREFIX/bin/php" -m > "$TASK/modules-after.txt"
"$PREFIX/sbin/php-fpm" -v
[[ -S /tmp/php-cgi-82.sock ]] || fail 'Expected FPM socket missing.'
SWITCHING=0
trap - ERR INT TERM HUP
# Align the panel display only after binaries and workers pass; never substitute this for an update.
if [[ -f "$PREFIX/version.pl" ]] && grep -qx '8\.2\.[0-9]*' "$PREFIX/version.pl"; then
 cp -a "$PREFIX/version.pl" "$TASK/version.pl.before"
 printf '%s' "$TARGET" > "$PREFIX/version.pl"
fi
printf 'RUNTIME PATCH VERIFIED: version=%s, live_processes=%s\n' "$TARGET" "$SEEN"
echo 'Next: test each bound site directly at origin (dynamic login/API), public URLs and application workflows. A static/CDN200 is insufficient.'
echo 'This script does not test database writes, customer forms, file uploads, or prove absence of prior compromise.'
echo "Private backup and logs: $TASK"
date -Is > "$TASK/runtime-patch-verified"
