#!/usr/bin/env bash
# Community-reported temporary mitigation; not a verified CVE fix.
set -uo pipefail
export PATH=/usr/sbin:/usr/bin:/sbin:/bin
umask 077

MODE=apply
INSTALL_DEPS=1
TARGET=
WORK=
LOCK=
LOG=
ACTIVE_INI=
ACTIVE_BACKUP=
ACTIVE_NEXT=
CHANGED=0
MASTER_PID=
MASTER_START=
CHECK_REASON=
ACTIVE_FPM=
ACTIVE_CONF=
ACTIVE_SERVICE=
RELOAD_ATTEMPTED=0
HEALTH_URL=
HEALTH_CODE=
REPORT_CAPTURE=0
INSTANCE_RESULT=
INSTANCE_FAILURE=
NAMES=() INIS=() FPMS=() CONFS=() SERVICES=()

say() {
    if [[ $REPORT_CAPTURE == 1 ]]; then
        case $* in
            CRITICAL：*) INSTANCE_FAILURE=$* ;;
            FAILED：*|UNKNOWN：*) [[ $INSTANCE_FAILURE == CRITICAL：* ]] || INSTANCE_FAILURE=$* ;;
            CONFIG_OFF：*) INSTANCE_RESULT='已关闭，无需修改（网站运行值未验证）' ;;
            APPLIED_UNVERIFIED：*) INSTANCE_RESULT='已关闭并重载（网站运行值未验证）' ;;
        esac
    fi
    printf '%s\n' "$*"
    if [[ -n $LOG && $REPORT_CAPTURE != 1 ]]; then printf '%s\n' "$*" >> "$LOG"; fi
}
warning() {
    say '------------------------------------------------------------'
    say '说明：仅为临时缓解，未确认覆盖全部漏洞。'
    say '      请验证网站并及时更新 PHP。'
    if [[ -n $LOG ]]; then
        printf '\n'
        say "日志：${LOG}"
    fi
}
print_instance_result() {
    say "[$1]"
    say "  结果：$2"
    printf '\n'
}
usage() {
    printf '%s\n' 'Usage: bash php-session-mitigate.sh [--scan | --apply] [--target ID] [--yes]' \
        '默认自动应用：无需交互，枚举全部实例、备份、修改、检查并重载对应 FPM。' \
        '--scan：仅只读枚举；--apply：显式指定自动应用（与默认相同）。' \
        '--target：仅处理指定实例，例如 bt-84；省略则处理全部已识别实例。' \
        '--health-url URL：可选 HTTP(S) 业务检查；多实例时必须同时指定 --target。' \
        '--no-install-deps：禁用默认的缺失依赖自动安装。--scan 始终不安装。' \
        '--yes：兼容旧版参数；现在始终免交互，不会取消警告输出。'
}

# GNU stat, required only in Linux apply mode. Reject symlinks in every ancestor.
trusted_path() {
    local p=$1 uid mode
    [[ $p == /* && -e $p ]] || return 1
    while :; do
        [[ ! -L $p ]] || return 1
        uid=$(stat -c %u -- "$p") || return 1
        mode=$(stat -c %a -- "$p") || return 1
        [[ $uid == 0 && $mode =~ ^[0-7]+$ ]] || return 1
        (( (8#$mode & 0022) == 0 )) || return 1
        [[ $p == / ]] && break
        p=${p%/*}; [[ -n $p ]] || p=/
    done
}

add_target() {
    [[ -f $2 && -x $3 && -f $4 ]] || return 0
    local n=${#NAMES[@]}
    NAMES[n]=$1 INIS[n]=$2 FPMS[n]=$3 CONFS[n]=$4 SERVICES[n]=$5
}
discover() {
    local base v conf
    for base in /www/server/php/*; do
        [[ -d $base ]] || continue
        v=${base##*/}; [[ $v =~ ^[0-9]+$ ]] || continue
        add_target "bt-$v" "$base/etc/php.ini" "$base/sbin/php-fpm" \
            "$base/etc/php-fpm.conf" "init:/etc/init.d/php-fpm-$v"
    done
    for base in /etc/php/*/fpm; do
        [[ -d $base ]] || continue
        v=${base%/fpm}; v=${v##*/}; [[ $v =~ ^[0-9]+\.[0-9]+$ ]] || continue
        add_target "deb-$v" "$base/php.ini" "/usr/sbin/php-fpm$v" \
            "$base/php-fpm.conf" "unit:php$v-fpm"
    done
    add_target system /etc/php.ini /usr/sbin/php-fpm /etc/php-fpm.conf unit:php-fpm
    for conf in /etc/php[0-9]*/php-fpm.conf; do
        [[ -f $conf ]] || continue
        base=${conf%/*}; v=${base#/etc/php}; [[ $v =~ ^[0-9]+$ ]] || continue
        add_target "alpine-$v" "$base/php.ini" "/usr/sbin/php-fpm$v" \
            "$conf" "openrc:php-fpm$v"
    done
}

bounded() {
    timeout -k 5 30 "$@"
}
install_dependencies() {
    local tool manager= packages=() need_core=0
    for tool in timeout cp stat readlink; do
        command -v "$tool" >/dev/null || need_core=1
    done
    if command -v cp >/dev/null; then
        cp --version 2>/dev/null | grep -q 'GNU coreutils' || need_core=1
    fi
    (( ! need_core )) || packages+=(coreutils)
    command -v getfacl >/dev/null || packages+=(acl)
    command -v getfattr >/dev/null || packages+=(attr)
    command -v diff >/dev/null || packages+=(diffutils)
    if [[ -n $HEALTH_URL ]]; then command -v curl >/dev/null || packages+=(curl); fi
    (( ${#packages[@]} > 0 )) || return 0
    if (( ! INSTALL_DEPS )); then
        say "UNKNOWN：缺少依赖包 ${packages[*]}；已禁用自动安装，未修改 PHP。"
        return 1
    fi
    # Select only known package managers. Never add repositories or run upgrades.
    for tool in apt-get dnf yum zypper apk; do
        if command -v "$tool" >/dev/null; then manager=$tool; break; fi
    done
    [[ -n $manager ]] || { say "UNKNOWN：无法自动安装 ${packages[*]}，请手工安装（Arch 不做局部升级）。"; return 1; }
    say "自动安装缺失依赖：${packages[*]}（使用 ${manager}；可能安装必要依赖，不安装 PHP）。"
    say '使用现有可信软件源，不执行整机升级、不删包、不删除锁；软件包安装可能需要一些时间。'
    case $manager in
        apt-get)
            # No wall-clock kill around dpkg: terminating an active transaction
            # could leave packages half configured. Only lock/network waits bounded.
            DEBIAN_FRONTEND=noninteractive NEEDRESTART_MODE=l apt-get \
                -o DPkg::Lock::Timeout=120 -o Acquire::Retries=1 \
                -o Acquire::http::Timeout=30 -o Acquire::https::Timeout=30 update || return 1
            DEBIAN_FRONTEND=noninteractive NEEDRESTART_MODE=l apt-get \
                -o DPkg::Lock::Timeout=120 -o Acquire::Retries=1 \
                -o Acquire::http::Timeout=30 -o Acquire::https::Timeout=30 \
                install -y --no-install-recommends --no-remove --no-upgrade "${packages[@]}" || return 1
            ;;
        dnf|yum)
            "$manager" -y --setopt=timeout=30 --setopt=retries=1 \
                --setopt=exit_on_lock=True install "${packages[@]}" || return 1
            ;;
        zypper)
            ZYPP_LOCK_TIMEOUT=120 zypper --non-interactive install --no-recommends "${packages[@]}" || return 1
            ;;
        apk)
            apk --wait 120 add "${packages[@]}" || return 1
            ;;
    esac
    say '依赖安装命令完成，继续核验工具；安装的软件包不会在 PHP 配置回滚时卸载。'
}
process_start() {
    local value fields
    value=$(cat "/proc/$1/stat") || return 1
    value=${value##*) }
    read -r -a fields <<< "$value"
    [[ ${fields[19]:-} =~ ^[0-9]+$ ]] || return 1
    printf '%s\n' "${fields[19]}"
}
master_matches() {
    local pid=$1 fpm=$2 conf=$3 title exe
    [[ $pid =~ ^[0-9]+$ && -r /proc/$pid/cmdline ]] || { CHECK_REASON='master 不存在或 cmdline 不可读'; return 1; }
    exe=$(readlink -f "/proc/$pid/exe") || { CHECK_REASON='master 可执行路径不可读'; return 1; }
    [[ $exe == "$(readlink -f "$fpm")" ]] || { CHECK_REASON='master 可执行路径与目标不匹配'; return 1; }
    title=$(tr '\000' ' ' < "/proc/$pid/cmdline") || { CHECK_REASON='无法读取 master 标题'; return 1; }
    # Unknown titles, alternate namespaces and noncanonical paths fail closed.
    [[ $title == "php-fpm: master process ($conf)"* ]] || { CHECK_REASON='master 标题/配置路径暂不匹配（可能正在 re-exec）'; return 1; }
    if [[ -n $MASTER_START && $pid == "$MASTER_PID" ]]; then
        [[ $(process_start "$pid") == "$MASTER_START" ]] || { CHECK_REASON='master 启动时间改变或不可读，拒绝沿用 PID'; return 1; }
    fi
    [[ $(stat -c %u "/proc/$pid") == 0 ]] || { CHECK_REASON='master 所有者不是 root 或无法核实'; return 1; }
    CHECK_REASON=
    return 0
}
identify_master() {
    local fpm=$1 conf=$2 service=$3 entry pid candidate= count=0 candidate_start
    for entry in /proc/[0-9]*/cmdline; do
        pid=${entry%/cmdline}; pid=${pid##*/}
        if master_matches "$pid" "$fpm" "$conf"; then candidate=$pid; count=$((count + 1)); fi
    done
    [[ $count == 1 ]] || return 1
    if [[ $service == unit:* ]]; then
        [[ $(bounded systemctl show -p MainPID --value "${service#unit:}") == "$candidate" ]] || return 1
    fi
    # Standard BT launches use this PID file. A stale/mismatched file is not
    # sufficient evidence to adopt a process, even if its title looks right.
    if [[ $service == init:/etc/init.d/php-fpm-* && $fpm == /www/server/php/*/sbin/php-fpm ]]; then
        local pidfile="${fpm%/sbin/php-fpm}/var/run/php-fpm.pid"
        trusted_path "$pidfile" || return 1
        [[ $(cat "$pidfile") == "$candidate" ]] || return 1
    fi
    candidate_start=$(process_start "$candidate") || return 1
    # Master environments cannot be reproduced safely in the checker. Refuse
    # custom PHPRC/PHP_INI_SCAN_DIR and injected startup settings.
    tr '\000' '\n' < "/proc/$candidate/environ" > "$WORK/master-env" || return 1
    if grep -Eq '^(PHPRC|PHP_INI_SCAN_DIR|PHP_VALUE|PHP_ADMIN_VALUE|LD_PRELOAD|LD_LIBRARY_PATH)=' "$WORK/master-env"; then return 1; fi
    # Publish identity only after every check succeeded. Failure preserves the
    # previous identity so recovery cannot accidentally signal a rejected PID.
    MASTER_PID=$candidate
    MASTER_START=$candidate_start
    CHECK_REASON=
    return 0
}
master_pid_exists() {
    [[ -d /proc/$1 ]]
}
refresh_after_reload() {
    local old=$MASTER_PID
    [[ $RELOAD_ATTEMPTED == 1 && -n $old && -n $ACTIVE_SERVICE ]] || return 1
    # An alive old PID with changed identity must not be silently replaced.
    master_pid_exists "$old" && return 1
    if ! identify_master "$ACTIVE_FPM" "$ACTIVE_CONF" "$ACTIVE_SERVICE"; then
        CHECK_REASON='旧 master 已退出，尚未找到通过唯一性/路径/服务归属检查的新 master'
        return 1
    fi
    [[ $MASTER_PID != "$old" ]] || { CHECK_REASON='未确认新的 master PID'; return 1; }
    say "已核实 FPM 重载换 PID：${old} -> ${MASTER_PID}（相同二进制与配置）。"
    return 0
}
normalize_init_checks() {
    # Remove only known shell builtin/test options, not the rest of a line.
    # Thus `if [ -n "$x" ]; then "$php_fpm_BIN" -n; fi` still rejects PHP -n.
    sed -E \
        -e 's/(^|[;[:space:]])echo[[:space:]]+-n([[:space:]]|$)/\1echo \2/g' \
        -e 's/\[\[?[[:space:]]+-[cnd]([[:space:]])/[ SHELL_TEST \1/g' \
        -e 's/(^|[;[:space:]])test[[:space:]]+-[cnd]([[:space:]])/\1test SHELL_TEST \2/g' "$1"
}
launch_config_safe() {
    local fpm=$1 ini=$2 service=$3 launcher check
    case $service in
        unit:*)
            bounded systemctl show -p ExecStart --value "${service#unit:}" > "$WORK/launch" || return 1
            grep -Fq "path=$fpm ;" "$WORK/launch" || return 1
            bounded systemctl show -p Environment --value "${service#unit:}" > "$WORK/launch-env" || return 1
            if grep -Eq '(PHPRC|PHP_INI_SCAN_DIR|PHP_VALUE|PHP_ADMIN_VALUE|LD_PRELOAD|LD_LIBRARY_PATH)=' "$WORK/launch-env"; then return 1; fi
            check=$WORK/launch
            ;;
        init:*|openrc:*)
            launcher=${service#*:}
            [[ $service != openrc:* ]] || launcher="/etc/init.d/$launcher"
            trusted_path "$launcher" || return 1
            cp -- "$launcher" "$WORK/launch" || return 1
            # OpenRC conf.d can supply custom options; reject nonempty files.
            if [[ $service == openrc:* && -s /etc/conf.d/${service#*:} ]]; then return 1; fi
            normalize_init_checks "$WORK/launch" > "$WORK/launch-check" || return 1
            check=$WORK/launch-check
            ;;
        *) return 1 ;;
    esac
    # Conservative textual check; refuse launch-time INI overrides/wrappers.
    if grep -Eq '(^|[[:space:]"\x27])(-[cnd]|--(php-ini|no-php-ini|define))|PHPRC|PHP_INI_SCAN_DIR|PHP_ADMIN_VALUE|PHP_VALUE' "$check"; then
        say '检查未通过：启动入口疑似存在 PHP INI 覆盖参数/环境（未打印参数值，避免泄漏）。'
        return 1
    fi
    bounded "$fpm" -i > "$WORK/info.txt" 2>&1 || return 1
    grep -Fxq "Loaded Configuration File => $ini" "$WORK/info.txt" || {
        say "检查未通过：FPM 默认加载的 INI 不是预期路径 ${ini}。"
        return 1
    }
}
reload_master() {
    if ! master_matches "$MASTER_PID" "$ACTIVE_FPM" "$ACTIVE_CONF"; then
        refresh_after_reload || {
        say "未发送重载信号：${CHECK_REASON}"; return 1;
        }
    fi
    # Recheck immediately before signalling, including the candidate start time.
    master_matches "$MASTER_PID" "$ACTIVE_FPM" "$ACTIVE_CONF" || return 1
    # Signal the verified FPM master directly: no arbitrary init reload/restart.
    kill -USR2 "$MASTER_PID" || { say '向核实的 master 发送 USR2 失败。'; return 1; }
}
health_check() {
    local code
    [[ -n $HEALTH_URL ]] || return 0
    code=$(bounded curl --silent --show-error --fail --noproxy '*' \
        --connect-timeout 3 --max-time 10 --proto '=http,https' \
        --output /dev/null --write-out '%{http_code}' -- "$HEALTH_URL") || { CHECK_REASON="业务请求失败（HTTP ${code:-unknown}）"; return 1; }
    [[ $code =~ ^2[0-9][0-9]$ ]] || { CHECK_REASON="业务返回非 2xx（HTTP ${code}）"; return 1; }
    if [[ -z $HEALTH_CODE ]]; then HEALTH_CODE=$code; fi
    [[ $code == "$HEALTH_CODE" ]] || { CHECK_REASON='业务状态码与修改前不一致'; return 1; }
}
wait_healthy() {
    local i stable=0 deadline=$((SECONDS + 30)) previous= observed_pid=$MASTER_PID
    # USR2 is asynchronous: re-exec can temporarily replace the process title.
    # Require five consecutive good samples, with a bounded recovery window.
    # Do not require /proc/.../children: some kernels omit that optional file.
    for ((i=0; i<30 && SECONDS<deadline; i++)); do
        sleep 1
        if { master_matches "$MASTER_PID" "$ACTIVE_FPM" "$ACTIVE_CONF" || refresh_after_reload; } && health_check; then
            if [[ $MASTER_PID != "$observed_pid" ]]; then stable=0; observed_pid=$MASTER_PID; fi
            stable=$((stable + 1))
            (( stable < 5 )) || return 0
        else
            stable=0
            if [[ $CHECK_REASON != "$previous" ]]; then
                say "等待 FPM 稳定：${CHECK_REASON:-未知检查失败}"
                previous=$CHECK_REASON
            fi
        fi
    done
    say "FAILED：FPM 稳定检查超时，未获得连续 5 次正常结果；最后原因：${CHECK_REASON:-稳定观察时间不足}"
    return 1
}
copy_metadata() {
    bounded cp --preserve=all -- "$1" "$2" || return 1
    metadata_equal "$1" "$2"
}
metadata_equal() {
    local a=$1 b=$2 attrs_a attrs_b
    attrs_a=$(stat -c '%u:%g:%a' "$a") || return 1
    attrs_b=$(stat -c '%u:%g:%a' "$b") || return 1
    [[ $attrs_a == "$attrs_b" ]] || return 1
    getfacl -cp -- "$a" > "$WORK/acl-a" || return 1
    getfacl -cp -- "$b" > "$WORK/acl-b" || return 1
    diff "$WORK/acl-a" "$WORK/acl-b" >/dev/null || return 1
    # security.selinux is covered by xattrs when present. Do not use stat %C:
    # systems without SELinux legitimately report ENODATA for that formatter.
    getfattr --absolute-names -d -m - -- "$a" > "$WORK/attrs-a" 2>/dev/null || return 1
    getfattr --absolute-names -d -m - -- "$b" > "$WORK/attrs-b" 2>/dev/null || return 1
    diff <(sed '/^# file:/d' "$WORK/attrs-a") <(sed '/^# file:/d' "$WORK/attrs-b") >/dev/null
}
startup_off() {
    bounded "$1" -c "$2" -i > "$WORK/info.txt" 2>&1 || return 1
    grep -Fq "Loaded Configuration File => $2" "$WORK/info.txt" || return 1
    grep -Eiq '^[[:space:]]*session[.]upload_progress[.]enabled[[:space:]]*=>[[:space:]]*(Off|0)[[:space:]]*=>' "$WORK/info.txt"
}

# Replace an existing section-independent setting safely by appending a [PHP]
# section; an arbitrary final INI section (e.g. [PATH=...]) must not capture it.
write_ini() {
    cat -- "$1" || return 1
    printf '\n[PHP]\n; Community-reported temporary mitigation; upgrade PHP promptly.\nsession.upload_progress.enabled = Off\n'
}
restore_active() {
    [[ $CHANGED == 1 && -n $ACTIVE_BACKUP ]] || return 0
    # Do not overwrite edits made by another tool after our replacement.
    if ! cmp -s -- "$ACTIVE_INI" "$WORK/applied.ini" || ! metadata_equal "$ACTIVE_INI" "$WORK/applied.ini"; then
        say "FAILED：配置在操作中发生变化，未覆盖。请人工恢复：$ACTIVE_BACKUP"
        return 1
    fi
    ACTIVE_NEXT=$(mktemp "$ACTIVE_INI.upload-progress-restore.XXXXXXXX") || return 1
    copy_metadata "$ACTIVE_BACKUP" "$ACTIVE_NEXT" && mv -f -- "$ACTIVE_NEXT" "$ACTIVE_INI" || {
        say "FAILED：恢复失败，备份：$ACTIVE_BACKUP"; return 1;
    }
    CHANGED=0
    if [[ $RELOAD_ATTEMPTED == 1 ]]; then
        if ! bounded "$ACTIVE_FPM" -c "$ACTIVE_INI" -y "$ACTIVE_CONF" -t >> "$LOG" 2>&1 ||
            ! reload_master || ! wait_healthy; then
            say 'CRITICAL：文件已恢复，但运行服务恢复未验证！请立即人工检查；不自动 restart。'
            return 1
        fi
    fi
    RELOAD_ATTEMPTED=0
    say '已恢复原配置文件；如曾重载，已重新加载并完成有限状态检查，仍需业务确认。'
}
cleanup() {
    local code=$?
    trap - EXIT INT TERM HUP
    if [[ $CHANGED == 1 && -n $LOG ]]; then
        REPORT_CAPTURE=1
        if ! restore_active >> "$LOG" 2>&1; then
            REPORT_CAPTURE=0
            say "严重错误：自动恢复未确认，请立即检查服务。日志：$LOG"
            code=1
        fi
        REPORT_CAPTURE=0
    else
        restore_active || code=1
    fi
    [[ -z $ACTIVE_NEXT ]] || rm -f -- "$ACTIVE_NEXT"
    if [[ -n $WORK ]]; then
        rm -f -- "$WORK/info.txt" "$WORK/test.txt" "$WORK/service.txt" "$WORK/applied.ini" "$WORK/attrs-a" "$WORK/attrs-b" "$WORK/acl-a" "$WORK/acl-b" "$WORK/master-env" "$WORK/launch" "$WORK/launch-env" "$WORK/launch-check"
        rmdir -- "$WORK" 2>/dev/null || :
    fi
    [[ -z $LOCK ]] || rmdir -- "$LOCK" 2>/dev/null || :
    if [[ $MODE == apply ]]; then warning; fi
    exit "$code"
}

run_reported_instance() {
    local rc
    INSTANCE_RESULT= INSTANCE_FAILURE=
    REPORT_CAPTURE=1
    # Redirection preserves shell state (PID adoption/rollback flags), unlike
    # command substitution. All detailed diagnostics stay in the private log.
    apply_one "$@" >> "$LOG" 2>&1
    rc=$?
    REPORT_CAPTURE=0
    if (( rc != 0 )); then
        INSTANCE_RESULT=${INSTANCE_FAILURE:-失败：文件操作或检查失败，详见日志}
        INSTANCE_RESULT=${INSTANCE_RESULT/#UNKNOWN：/未处理：}
        INSTANCE_RESULT=${INSTANCE_RESULT/#FAILED：/失败：}
        INSTANCE_RESULT=${INSTANCE_RESULT/#CRITICAL：/严重错误：}
        if (( CHANGED == 1 )); then INSTANCE_RESULT='严重错误：配置回滚未完成，需立即人工检查'; fi
    else
        INSTANCE_RESULT=${INSTANCE_RESULT:-已完成（实际网站值未验证）}
    fi
    return "$rc"
}

apply_one() {
    local name=$1 ini=$2 fpm=$3 conf=$4 service=$5 path stamp
    say "[$name] ini=$ini; FPM=$fpm; config=$conf; service=$service"
    ACTIVE_INI= ACTIVE_BACKUP= ACTIVE_NEXT= CHANGED=0 RELOAD_ATTEMPTED=0
    ACTIVE_FPM=$fpm ACTIVE_CONF=$conf ACTIVE_SERVICE=$service MASTER_PID= MASTER_START= HEALTH_CODE=
    for path in "$ini" "$fpm" "$conf"; do
        trusted_path "$path" || { say 'UNKNOWN：路径不可信、可写或包含符号链接，未修改。'; return 1; }
    done
    identify_master "$fpm" "$conf" "$service" || { say 'UNKNOWN：无法唯一核实实际 FPM master/配置/环境，未修改。'; return 1; }
    launch_config_safe "$fpm" "$ini" "$service" || { say 'UNKNOWN：启动入口/默认 INI 不匹配或包含自定义覆盖，未修改。'; return 1; }
    health_check || { say 'FAILED：修改前业务健康检查失败，未修改。'; return 1; }
    bounded "$fpm" -c "$ini" -y "$conf" -t > "$WORK/test.txt" 2>&1 || {
        say 'FAILED：原 FPM 配置检查失败，未修改。'; return 1;
    }
    if startup_off "$fpm" "$ini"; then
        say 'CONFIG_OFF：新启动 FPM 配置已为 Off；未修改，未确认运行中网站值。'
        return 0
    fi
    stamp=$(date -u +%Y%m%dT%H%M%SZ)
    ACTIVE_INI=$ini
    ACTIVE_BACKUP="$ini.upload-progress-backup.$stamp.$$"
    # mktemp creates a non-predictable sibling, on the same filesystem.
    ACTIVE_NEXT=$(mktemp "$ini.upload-progress-next.XXXXXXXX") || return 1
    [[ ! -e $ACTIVE_BACKUP && ! -L $ACTIVE_BACKUP ]] || return 1
    copy_metadata "$ini" "$ACTIVE_BACKUP" || return 1
    say "备份：$ACTIVE_BACKUP"
    copy_metadata "$ini" "$ACTIVE_NEXT" && write_ini "$ACTIVE_BACKUP" > "$ACTIVE_NEXT" || return 1
    metadata_equal "$ACTIVE_BACKUP" "$ACTIVE_NEXT" || return 1
    copy_metadata "$ACTIVE_NEXT" "$WORK/applied.ini" || return 1
    if ! cmp -s -- "$ini" "$ACTIVE_BACKUP" || ! metadata_equal "$ini" "$ACTIVE_BACKUP"; then
        say 'FAILED：备份后原配置发生变化，未覆盖。'; return 1
    fi
    # Mark before rename so interrupts are handled conservatively.
    CHANGED=1
    mv -f -- "$ACTIVE_NEXT" "$ini" || { CHANGED=0; return 1; }
    if ! bounded "$fpm" -c "$ini" -y "$conf" -t > "$WORK/test.txt" 2>&1 || ! startup_off "$fpm" "$ini"; then
        say 'FAILED：修改后配置检查失败或被其他启动 INI 覆盖。'
        restore_active || return 2
        return 1
    fi
    RELOAD_ATTEMPTED=1
    if ! reload_master >> "$LOG" 2>&1 || ! wait_healthy; then
        say 'FAILED：重载或服务检查失败。恢复文件不能保证运行状态一致，必须人工检查。'
        restore_active || return 2
        return 1
    fi
    CHANGED=0 RELOAD_ATTEMPTED=0
    say 'APPLIED_UNVERIFIED：启动配置为 Off，已向核实的 FPM master 发 USR2 并观察状态。'
}

main() {
    local i count=0 failures=0 rc stopped=0
    while (( $# )); do
        case $1 in
            --scan) MODE=scan ;;
            --apply) MODE=apply ;;
            --yes) : ;; # Compatibility: apply is always non-interactive.
            --no-install-deps) INSTALL_DEPS=0 ;;
            --target) shift; [[ $# -gt 0 && $1 != --* ]] || { usage; return 2; }; TARGET=$1 ;;
            --health-url) shift; [[ $# -gt 0 && $1 =~ ^https?:// ]] || { usage; return 2; }; HEALTH_URL=$1 ;;
            -h|--help) usage; return 0 ;;
            *) usage; return 2 ;;
        esac
        shift
    done
    trap cleanup EXIT
    trap 'exit 130' INT
    trap 'exit 143' TERM
    trap 'exit 129' HUP
    [[ -z $HEALTH_URL || -n $TARGET ]] || { say '--health-url 必须配合 --target，避免检查错实例。'; return 2; }
    [[ $(uname -s) == Linux ]] || { say 'UNKNOWN：仅支持 Linux，不作修改。'; return 1; }
    discover
    for ((i=0; i<${#NAMES[@]}; i++)); do
        [[ -z $TARGET || $TARGET == "${NAMES[i]}" ]] || continue
        count=$((count + 1))
    done
    say "识别到 ${count} 个 PHP。"
    say '------------------------------------------------------------'
    printf '\n'
    if (( count == 0 )); then
        say '未匹配支持的实例。'
        return 1
    fi
    if [[ $MODE == apply ]]; then
        [[ $EUID == 0 ]] || { say '应用模式需要 root；请使用 sudo 或由 root 执行，不会自动提权。'; return 2; }
        trusted_path /run || { say 'FAILED：/run 不可信或不可用。'; return 1; }
        if ! mkdir /run/php-session-mitigate.lock 2>/dev/null; then
            say 'FAILED：锁已存在；检查并行进程，勿盲目删除锁。'; return 1
        fi
        LOCK=/run/php-session-mitigate.lock
        WORK=$(mktemp -d /run/php-session-mitigate.XXXXXXXX) || return 1
        LOG=$(mktemp /run/php-session-mitigate-log.XXXXXXXX) || return 1
        REPORT_CAPTURE=1
        install_dependencies >> "$LOG" 2>&1
        rc=$?
        REPORT_CAPTURE=0
        if (( rc != 0 )); then
            for ((i=0; i<${#NAMES[@]}; i++)); do
                [[ -z $TARGET || $TARGET == "${NAMES[i]}" ]] || continue
                print_instance_result "${INIS[i]}" '未处理：依赖准备失败，详见日志'
            done
            return 1
        fi
        local tool
        for tool in timeout cp stat getfacl getfattr diff readlink; do
            command -v "$tool" >/dev/null || { say "UNKNOWN：依赖准备后仍缺少工具 ${tool}，未修改 PHP，请手工检查。"; return 1; }
        done
        cp --version 2>/dev/null | grep -q 'GNU coreutils' || { say 'UNKNOWN：应用模式需要 GNU cp，未修改。'; return 1; }
        [[ -z $HEALTH_URL ]] || command -v curl >/dev/null || { say '缺少 curl，未修改。'; return 1; }
        unset PHPRC PHP_INI_SCAN_DIR PHP_VALUE PHP_ADMIN_VALUE LD_PRELOAD LD_LIBRARY_PATH
    fi
    for ((i=0; i<${#NAMES[@]}; i++)); do
        [[ -z $TARGET || $TARGET == "${NAMES[i]}" ]] || continue
        if [[ $MODE == scan ]]; then
            print_instance_result "${INIS[i]}" '仅扫描，未修改'
        elif (( stopped )); then
            print_instance_result "${INIS[i]}" '已跳过：前一实例失败，已停止后续操作'
        else
            run_reported_instance "${NAMES[i]}" "${INIS[i]}" "${FPMS[i]}" "${CONFS[i]}" "${SERVICES[i]}"
            rc=$?
            print_instance_result "${INIS[i]}" "$INSTANCE_RESULT"
            if (( rc != 0 || CHANGED == 1 )); then
                failures=$((failures + 1))
                stopped=1
                continue
            fi
            [[ -z $ACTIVE_NEXT ]] || rm -f -- "$ACTIVE_NEXT"
            ACTIVE_INI= ACTIVE_BACKUP= ACTIVE_NEXT=
        fi
    done
    (( failures == 0 ))
}

if [[ -z ${BASH_SOURCE[0]:-} || ${BASH_SOURCE[0]:-} == "$0" ]]; then main "$@"; fi
