#!/bin/bash
# =============================================================================
# verify_fix.sh — CVE-2026-66066 / GHSA-xr9x-r78c-5hrm
#
# Verifies bundle/coding/proposed_fix.diff against the vendored vulnerable
# activestorage-8.0.5 gem (bundle/repro/work/bundle/.../gems/activestorage-8.0.5).
#
# Checks:
#   1. The patch applies cleanly (patch -p1) to a pristine copy of the gem.
#   2. Static: lib/active_storage/vips.rb calls Vips.block_untrusted(true)
#      and the vips image analyzer requires active_storage/vips (the load
#      path active_storage/engine.rb triggers at boot).
#   3. Equivalence: the two patched security files are byte-identical to the
#      upstream fixed release activestorage-8.0.5.1 (if vendored).
#   4. Dynamic (fresh ruby processes, system libvips >= 8.13):
#      - patched gem:   loading Active Storage the way the engine does, then
#        Vips::Image.new_from_file(<crafted .mat CVE payload>) FAILS
#        ("operation is blocked" on libvips 8.13-8.16, "not a known file
#        format" on libvips >= 8.17 which drops blocked loaders from the
#        loader search) -> matload cannot dereference server-side paths.
#      - unpatched gem: same flow LOADS the payload as an image (99x1),
#        proving the divergence is caused by the patch.
#
# Idempotent: works on a fresh temp copy each run; exits:
#   0 = fix verified   1 = verification failed   2 = environment blocker
# =============================================================================
set -euo pipefail

CODING_DIR="$(cd "$(dirname "$0")" && pwd)"
BUNDLE="$(dirname "$CODING_DIR")"
PATCH_FILE="$CODING_DIR/proposed_fix.diff"
GEMS="$BUNDLE/repro/work/bundle/ruby/3.4.0/gems"
VULN_GEM="$GEMS/activestorage-8.0.5"
FIXED_GEM="$GEMS/activestorage-8.0.5.1"
PAYLOAD="$BUNDLE/vuln_variant/work/payloads/evil.mat"

fail()  { echo "FAIL: $*" >&2; exit 1; }
block() { echo "BLOCKER: $*" >&2; exit 2; }
info()  { echo "[verify] $*"; }

[ -f "$PATCH_FILE" ]            || block "proposed_fix.diff not found at $PATCH_FILE"
command -v patch >/dev/null     || block "patch(1) not available"
command -v ruby  >/dev/null     || block "ruby not available"
[ -d "$VULN_GEM" ]              || block "vendored activestorage-8.0.5 not found at $VULN_GEM"
[ -f "$PAYLOAD" ]               || block "crafted .mat payload not found at $PAYLOAD"
command -v vips >/dev/null \
  && info "libvips: $(vips --version)" \
  || info "libvips CLI not found; relying on ruby-vips FFI"

WORK="$(mktemp -d)"
trap 'rm -rf "$WORK"' EXIT

# --- 1. Patch applies cleanly -------------------------------------------------
info "applying patch to pristine copy of activestorage-8.0.5"
cp -r "$VULN_GEM" "$WORK/patched"
patch -p1 --dry-run -d "$WORK/patched" < "$PATCH_FILE" >/dev/null \
  || fail "patch does not apply cleanly (dry run)"
patch -p1 -d "$WORK/patched" < "$PATCH_FILE" >/dev/null

# --- 2. Static assertions -----------------------------------------------------
info "static assertions"
grep -q 'Vips.block_untrusted(true)' "$WORK/patched/lib/active_storage/vips.rb" \
  || fail "patched vips.rb does not call Vips.block_untrusted(true)"
grep -q 'require "active_storage/vips"' \
  "$WORK/patched/lib/active_storage/analyzer/image_analyzer/vips.rb" \
  || fail "patched analyzer does not require active_storage/vips"

# --- 3. Equivalence with upstream fixed release --------------------------------
if [ -d "$FIXED_GEM" ]; then
  info "equivalence vs activestorage-8.0.5.1"
  diff -q "$WORK/patched/lib/active_storage/vips.rb" \
          "$FIXED_GEM/lib/active_storage/vips.rb" >/dev/null \
    || fail "patched vips.rb differs from upstream 8.0.5.1"
  diff -q "$WORK/patched/lib/active_storage/analyzer/image_analyzer/vips.rb" \
          "$FIXED_GEM/lib/active_storage/analyzer/image_analyzer/vips.rb" >/dev/null \
    || fail "patched analyzer differs from upstream 8.0.5.1"
  info "patched security files are byte-identical to upstream 8.0.5.1"
else
  info "activestorage-8.0.5.1 not vendored; skipping equivalence check"
fi

# --- 4. Dynamic assertions ----------------------------------------------------
cat > "$WORK/check_block.rb" <<'RUBY'
# Usage: ruby check_block.rb <activestorage_lib_dir> <payload.mat> <blocked|unblocked>
lib, payload, expect = ARGV
$LOAD_PATH.unshift lib

require "openssl"
# Mirror active_storage/engine.rb: the engine eagerly requires the vips image
# analyzer at boot, which (after the fix) pulls in active_storage/vips and
# calls Vips.block_untrusted(true).
require "active_storage"
require "active_storage/analyzer/image_analyzer"
require "active_storage/analyzer/image_analyzer/vips"

# The vips transformer/analyzer loads ruby-vips lazily at variant-processing
# time (via image_processing); mirror that for the unpatched control run.
require "ruby-vips" unless defined?(Vips)

result = begin
  im = Vips::Image.new_from_file(payload)
  "loaded #{im.width}x#{im.height}"
rescue Vips::Error => e
  "error: #{e.message.strip}"
end

puts "matload of crafted payload: #{result}"
blocked = result.start_with?("error:") &&
          result.match?(/operation is blocked|not a known file format/)
ok = (expect == "blocked") ? blocked : !blocked
puts(ok ? "CHECK PASS (#{expect})" : "CHECK FAIL (expected #{expect})")
exit(ok ? 0 : 1)
RUBY

# Pure-ruby dependencies of activestorage, vendored by the repro stage.
RL=""
for g in activesupport-8.0.5 activemodel-8.0.5 activerecord-8.0.5 i18n-1.15.2 \
         tzinfo-2.0.6 connection_pool-3.0.2 timeout-0.6.1 marcel-1.2.1 \
         image_processing-1.14.0 mini_magick-5.3.2; do
  [ -d "$GEMS/$g/lib" ] || block "vendored dependency $g missing"
  RL="$RL:$GEMS/$g/lib"
done
RL="$RL:$GEMS/concurrent-ruby-1.3.8/lib/concurrent-ruby"
export RUBYLIB="${RL#:}${RUBYLIB:+:$RUBYLIB}"
# ruby-vips is activated via `gem "ruby-vips"` inside the fix; expose the
# vendored gem repository so activation succeeds (native extensions such as
# ffi resolve from the system gem home first).
export GEM_PATH="$BUNDLE/repro/work/bundle/ruby/3.4.0${GEM_PATH:+:$GEM_PATH}"

info "dynamic check: patched gem must REFUSE the crafted .mat (blocked)"
ruby "$WORK/check_block.rb" "$WORK/patched/lib" "$PAYLOAD" blocked \
  || fail "patched gem still processes the untrusted matload payload"

info "dynamic check: unpatched gem must LOAD the crafted .mat (control)"
ruby "$WORK/check_block.rb" "$VULN_GEM/lib" "$PAYLOAD" unblocked \
  || fail "control run failed: unpatched gem did not load the payload; divergence unproven"

echo "============================================================================="
echo "RESULT: FIX VERIFIED — patched activestorage blocks untrusted libvips"
echo "        operations at boot (matload refused), unpatched control loads them."
echo "============================================================================="
exit 0
