#!/bin/bash
set -euo pipefail

ROOT="${PRUVA_ROOT:-$(cd "$(dirname "$0")/.." && pwd)}"
CODING_DIR="$ROOT/coding"
LOGS="$ROOT/logs/coding"
PATCH_FILE="$CODING_DIR/proposed_fix.diff"
WORK="${PRUVA_CODING_WORKDIR:-${TMPDIR:-/tmp}/pruva-coding-cve-2026-61511}"
SOURCE_ROOT="${PRUVA_VB_SOURCE_ROOT:-${TMPDIR:-/tmp}/pruva-vbulletin-cve-2026-61511-variant/vb515-rootfs/var/www/vb5}"
PHP_ROOT="${PRUVA_PHP_ROOT:-${TMPDIR:-/tmp}/pruva-vbulletin-cve-2026-61511-variant/php70-rootfs}"
TARGET_URL="${PRUVA_CODING_TARGET_URL:-http://127.0.0.1:18083/}"
RUN_ID="${PRUVA_CODING_RUN_ID:-$(date -u +%Y%m%dT%H%M%SZ)-$$}"
PROOF_LOG="$LOGS/${RUN_ID}-proof.log"
UNIT_LOG="$LOGS/${RUN_ID}-unit.log"
HTTP_LOG="$LOGS/${RUN_ID}-http.log"
SOURCE_LOG="$LOGS/${RUN_ID}-source-identity.txt"
OBSERVATION="$LOGS/${RUN_ID}-observation.json"
FRONT_ORIGINAL_SHA="cfa52c8bd234e176d9fd994546d8bf0bcb2c0eb47078806717015ea9c6e62284"
CORE_ORIGINAL_SHA="1c576fedaf8daa2b16e4989b8c4979ad52d9716ec870663347a7e1750c9a1fd8"
PHP_MANIFEST="sha256:60cbd13178ed7f629098b1b7a0104ce038d55729f89784739e1bc2eca372f64a"
VB_MANIFEST="sha256:df5a9fa87186c49101db79577a0401c07f1239bf79dd1e5d066328147f6727f8"
INVALID='/* Invalid math expression */'

mkdir -p "$LOGS" "$WORK"
: > "$PROOF_LOG"
: > "$UNIT_LOG"
: > "$HTTP_LOG"

fail() {
    echo "FAIL: $*" | tee -a "$PROOF_LOG" >&2
    exit 1
}

for cmd in python3 patch sha256sum sudo curl grep cmp; do
    command -v "$cmd" >/dev/null || fail "missing required command: $cmd"
done
sudo -n true 2>/dev/null || fail "passwordless sudo is required for the digest-pinned PHP chroot"
[ -s "$PATCH_FILE" ] || fail "missing patch: $PATCH_FILE"
[ -f "$SOURCE_ROOT/includes/vb5/template/runtime.php" ] || fail "affected source tree is unavailable: $SOURCE_ROOT"
[ -f "$SOURCE_ROOT/core/vb/template/runtime.php" ] || fail "affected core runtime is unavailable"
[ -x "$PHP_ROOT/usr/local/bin/php" ] || fail "digest-pinned PHP 7 runtime is unavailable: $PHP_ROOT"
[ "$(cat "$PHP_ROOT/.pruva-image-manifest" 2>/dev/null || true)" = "$PHP_MANIFEST" ] || fail "PHP root identity mismatch"
[ "$(sha256sum "$SOURCE_ROOT/includes/vb5/template/runtime.php" | awk '{print $1}')" = "$FRONT_ORIGINAL_SHA" ] || fail "front runtime source identity mismatch"
[ "$(sha256sum "$SOURCE_ROOT/core/vb/template/runtime.php" | awk '{print $1}')" = "$CORE_ORIGINAL_SHA" ] || fail "core runtime source identity mismatch"

# Always create a fresh patch target. This makes repeated verification independent
# of prior application state and proves patch -p1 behavior against exact old bytes.
TREE="$WORK/tree-$RUN_ID"
rm -rf "$TREE"
mkdir -p "$TREE/includes/vb5/template" "$TREE/core/vb/template"
cp "$SOURCE_ROOT/includes/vb5/template/runtime.php" "$TREE/includes/vb5/template/runtime.php"
cp "$SOURCE_ROOT/core/vb/template/runtime.php" "$TREE/core/vb/template/runtime.php"
patch --batch --forward -d "$TREE" -p1 -i "$PATCH_FILE" | tee -a "$PROOF_LOG"

! grep -q '@eval' "$TREE/includes/vb5/template/runtime.php" || fail "eval remains in front runtime"
! grep -q '@eval' "$TREE/core/vb/template/runtime.php" || fail "eval remains in core runtime"
grep -q 'evaluateMathExpression' "$TREE/includes/vb5/template/runtime.php" || fail "strict parser absent from front runtime"
grep -q 'evaluateMathExpression' "$TREE/core/vb/template/runtime.php" || fail "strict parser absent from core runtime"

# Unit harness runs both duplicate implementations in the exact PHP 7.0.32
# runtime used by the exploit. It also uses the full checked-in candidate payload.
cat > "$WORK/test-runtime.php" <<'PHP'
<?php
if ($argc !== 4) { fwrite(STDERR, "usage: test-runtime.php runtime class payload\n"); exit(2); }
define('VB_ENTRY', true);
require $argv[1];
$class = $argv[2];
$payload = trim(file_get_contents($argv[3]));
$invalid = '/* Invalid math expression */';
$tests = array(
    array('1 + 2 * 3', '7'),
    array('(1 + 2) * 3', '9'),
    array('-5 + +2', '-3'),
    array("1\t+\n2", '3'),
    array('.5 + 1.25', '1.75'),
    array('10 / 4', '2.5'),
    array('2px + 3px', '5px'),
    array('', $invalid),
    array('1 + abc', $invalid),
    array('1;phpinfo()', $invalid),
    array('1 ^ 2', $invalid),
    array('1 / 0', $invalid),
    array('1..2', $invalid),
    array($payload, $invalid),
);
$failed = 0;
foreach ($tests as $test)
{
    $actual = call_user_func(array($class, 'runMaths'), $test[0]);
    $ok = ($actual === $test[1]);
    echo ($ok ? 'PASS' : 'FAIL') . ' ' . json_encode(strlen($test[0]) > 120 ? '[full exploit payload]' : $test[0]) . ' => ' . json_encode($actual) . "\n";
    if (!$ok) { $failed++; }
}
$mixed = call_user_func(array($class, 'runMaths'), '1px + 2em');
if (strpos($mixed, 'mixed units') === false) { echo "FAIL mixed units\n"; $failed++; } else { echo "PASS mixed units\n"; }
$long = str_repeat('1+', 600) . '1';
if (call_user_func(array($class, 'runMaths'), $long) !== $invalid) { echo "FAIL length limit\n"; $failed++; } else { echo "PASS length limit\n"; }
$deep = str_repeat('(', 33) . '1' . str_repeat(')', 33);
if (call_user_func(array($class, 'runMaths'), $deep) !== $invalid) { echo "FAIL depth limit\n"; $failed++; } else { echo "PASS depth limit\n"; }
$ops = str_repeat('1+', 257) . '1';
if (call_user_func(array($class, 'runMaths'), $ops) !== $invalid) { echo "FAIL operation limit\n"; $failed++; } else { echo "PASS operation limit\n"; }
if (call_user_func(array($class, 'runMaths'), array('1+1')) !== $invalid) { echo "FAIL non-scalar\n"; $failed++; } else { echo "PASS non-scalar\n"; }
exit($failed ? 1 : 0);
PHP

PAYLOAD="$ROOT/vuln_variant/candidate_strlen_payload.txt"
[ -s "$PAYLOAD" ] || fail "required current-run regression payload is missing"
CHROOT_DIR="/tmp/pruva-coding-$RUN_ID"
sudo -n rm -rf "$PHP_ROOT$CHROOT_DIR"
sudo -n mkdir -p "$PHP_ROOT$CHROOT_DIR/front" "$PHP_ROOT$CHROOT_DIR/core"
sudo -n cp "$TREE/includes/vb5/template/runtime.php" "$PHP_ROOT$CHROOT_DIR/front/runtime.php"
sudo -n cp "$TREE/core/vb/template/runtime.php" "$PHP_ROOT$CHROOT_DIR/core/runtime.php"
sudo -n cp "$WORK/test-runtime.php" "$PHP_ROOT$CHROOT_DIR/test-runtime.php"
sudo -n cp "$PAYLOAD" "$PHP_ROOT$CHROOT_DIR/payload.txt"
sudo -n chroot "$PHP_ROOT" /usr/local/bin/php -l "$CHROOT_DIR/front/runtime.php" | tee -a "$UNIT_LOG"
sudo -n chroot "$PHP_ROOT" /usr/local/bin/php -l "$CHROOT_DIR/core/runtime.php" | tee -a "$UNIT_LOG"
sudo -n chroot "$PHP_ROOT" /usr/local/bin/php "$CHROOT_DIR/test-runtime.php" "$CHROOT_DIR/front/runtime.php" vB5_Template_Runtime "$CHROOT_DIR/payload.txt" | tee -a "$UNIT_LOG"
sudo -n chroot "$PHP_ROOT" /usr/local/bin/php "$CHROOT_DIR/test-runtime.php" "$CHROOT_DIR/core/runtime.php" vB_Template_Runtime "$CHROOT_DIR/payload.txt" | tee -a "$UNIT_LOG"

# If the real current-run application is available, temporarily install only the
# patched runtime files, test both confirmed unauthenticated routes, and restore
# original bytes even if a request fails. Unit verification remains mandatory;
# this HTTP portion is also mandatory unless explicitly disabled for portability.
HTTP_TESTED=false
PARENT_BLOCKED=false
VARIANT_BLOCKED=false
BENIGN_OK=false
APP_FRONT="$PHP_ROOT/var/www/html/includes/vb5/template/runtime.php"
APP_CORE="$PHP_ROOT/var/www/html/core/vb/template/runtime.php"
BACKUP="$WORK/http-backup-$RUN_ID"
restore_http_files() {
    set +e
    if [ -d "$BACKUP" ]; then
        sudo -n cp "$BACKUP/front.php" "$APP_FRONT" 2>/dev/null || true
        sudo -n cp "$BACKUP/core.php" "$APP_CORE" 2>/dev/null || true
    fi
}
trap restore_http_files EXIT INT TERM

if [ "${PRUVA_SKIP_HTTP:-0}" != 1 ]; then
    [ -f "$APP_FRONT" ] && [ -f "$APP_CORE" ] || fail "real application files unavailable for HTTP verification"
    curl -fsS --connect-timeout 3 --max-time 15 "$TARGET_URL" -o /dev/null || fail "real vBulletin target is not reachable at $TARGET_URL"
    mkdir -p "$BACKUP"
    sudo -n cp "$APP_FRONT" "$BACKUP/front.php"
    sudo -n cp "$APP_CORE" "$BACKUP/core.php"
    sudo -n cp "$TREE/includes/vb5/template/runtime.php" "$APP_FRONT"
    sudo -n cp "$TREE/core/vb/template/runtime.php" "$APP_CORE"
    # PHP OPcache checks timestamps on a revalidation interval. Wait beyond the
    # stock two-second interval so every Apache worker loads patched bytes.
    sleep 4
    HTTP_TESTED=true

    PARENT_BENIGN="$LOGS/${RUN_ID}-parent-benign.json"
    VARIANT_BENIGN="$LOGS/${RUN_ID}-variant-benign.json"
    PARENT_ATTACK="$LOGS/${RUN_ID}-parent-attack.json"
    VARIANT_ATTACK="$LOGS/${RUN_ID}-variant-attack.json"
    PAYLOAD_VALUE=$(tr -d '\r\n' < "$PAYLOAD")

    curl -fsS --connect-timeout 5 --max-time 30 \
        --data-urlencode 'routestring=ajax/render/pagenav' \
        --data-urlencode 'pagenav[pagenumber]=2' "$TARGET_URL" > "$PARENT_BENIGN"
    curl -fsS --connect-timeout 5 --max-time 30 \
        --data-urlencode 'routestring=ajax/render/pagenavnew' \
        --data-urlencode 'pagenav[currentpage]=2' \
        --data-urlencode 'pagenav[totalpages]=100' "$TARGET_URL" > "$VARIANT_BENIGN"
    if grep -Fq 'data-page=\"1\"' "$PARENT_BENIGN" && grep -Fq 'data-page=\"3\"' "$PARENT_BENIGN" && \
       grep -Fq 'data-page=\"1\"' "$VARIANT_BENIGN" && grep -Fq 'data-page=\"3\"' "$VARIANT_BENIGN"; then
        BENIGN_OK=true
    else
        fail "ordinary paging arithmetic regressed"
    fi

    curl -fsS --connect-timeout 5 --max-time 90 \
        --data-urlencode 'routestring=ajax/render/pagenav' \
        --data-urlencode "pagenav[pagenumber]=$PAYLOAD_VALUE" "$TARGET_URL" > "$PARENT_ATTACK"
    curl -fsS --connect-timeout 5 --max-time 90 \
        --data-urlencode 'routestring=ajax/render/pagenavnew' \
        --data-urlencode "pagenav[currentpage]=$PAYLOAD_VALUE" \
        --data-urlencode 'pagenav[totalpages]=100' "$TARGET_URL" > "$VARIANT_ATTACK"

    if grep -Fq 'Invalid math expression' "$PARENT_ATTACK" && ! grep -Fq 'data-page=\"36\"' "$PARENT_ATTACK"; then PARENT_BLOCKED=true; fi
    if grep -Fq 'Invalid math expression' "$VARIANT_ATTACK" && ! grep -Fq 'data-page=\"36\"' "$VARIANT_ATTACK"; then VARIANT_BLOCKED=true; fi
    [ "$PARENT_BLOCKED" = true ] || fail "published pagenav trigger was not blocked"
    [ "$VARIANT_BLOCKED" = true ] || fail "pagenavnew alternate trigger was not blocked"
    {
        echo "target_url=$TARGET_URL"
        echo "benign_parent_and_variant_arithmetic=$BENIGN_OK"
        echo "parent_pagenav_payload_blocked=$PARENT_BLOCKED"
        echo "variant_pagenavnew_payload_blocked=$VARIANT_BLOCKED"
    } | tee -a "$HTTP_LOG"
fi

restore_http_files
trap - EXIT INT TERM
sudo -n rm -rf "$PHP_ROOT$CHROOT_DIR"

{
    echo "run_id=$RUN_ID"
    echo "vbulletin_manifest=$VB_MANIFEST"
    echo "php_manifest=$PHP_MANIFEST"
    echo "front_original_sha256=$FRONT_ORIGINAL_SHA"
    echo "core_original_sha256=$CORE_ORIGINAL_SHA"
    echo "patch_sha256=$(sha256sum "$PATCH_FILE" | awk '{print $1}')"
    echo "php_version=$(sudo -n chroot "$PHP_ROOT" /usr/local/bin/php -r 'echo PHP_VERSION;')"
} > "$SOURCE_LOG"

python3 - "$OBSERVATION" "$HTTP_TESTED" "$BENIGN_OK" "$PARENT_BLOCKED" "$VARIANT_BLOCKED" <<'PY'
import json, sys
path, http, benign, parent, variant = sys.argv[1:]
with open(path, 'w', encoding='utf-8') as f:
    json.dump({
        'schema_version': 1,
        'patch_applied_cleanly': True,
        'both_runtime_unit_suites_passed': True,
        'eval_removed_from_both_runtimes': True,
        'http_tested': http == 'true',
        'benign_arithmetic_preserved': benign == 'true',
        'parent_trigger_blocked': parent == 'true',
        'alternate_trigger_blocked': variant == 'true',
    }, f, indent=2)
    f.write('\n')
PY

{
    echo "patch_applied_cleanly=true"
    echo "both_runtime_unit_suites_passed=true"
    echo "eval_removed_from_both_runtimes=true"
    echo "http_tested=$HTTP_TESTED"
    echo "benign_arithmetic_preserved=$BENIGN_OK"
    echo "parent_trigger_blocked=$PARENT_BLOCKED"
    echo "alternate_trigger_blocked=$VARIANT_BLOCKED"
    echo "VERIFIED: strict arithmetic parsing blocks both CVE-2026-61511 request paths without breaking normal paging."
} | tee -a "$PROOF_LOG"
