#!/bin/bash
# cloud-init for the CVE-2026-31431 ("Copy Fail") lab VM.
#
# Responsibilities (all run as root at first boot):
#   1. PREVENT the kernel from being upgraded to a patched build. The AMI ships
#      the affected 6.8 GA kernel; an unattended-upgrade could pull a fixed one.
#   2. GATE on the running kernel being inside the affected range. If it is not,
#      do NOT write the readiness marker -> the environment-builder / verifier
#      treats the substrate as failed instead of silently testing a patched host.
#   3. Confirm AF_ALG / algif_aead is usable (the bug's transport).
#   4. Create the unprivileged actor `lowpriv` (no sudo, no caps).
#   5. Stand up the clean root-owned target baseline the primitive corrupts.
#   6. Leave the verifier a privileged out-of-band channel (root via sudo as
#      the default `ubuntu` user) + a root-readable baseline manifest.
#
# Loud-failure contract: /root/.lab-ready is written ONLY when every gate passes.
# Its presence is the single readiness signal the smoke test checks.
set -uo pipefail
exec > /var/log/lab-init.log 2>&1
echo "[lab-init] start $(date -u)"

FAIL() { echo "[lab-init] SUBSTRATE_FAILED: $*"; rm -f /root/.lab-ready; exit 1; }

# --- 1. Freeze the kernel: hold every installed linux-image/headers/modules,
#         and disable unattended-upgrades so nothing swaps in a patched kernel.
systemctl stop unattended-upgrades.service 2>/dev/null || true
systemctl disable unattended-upgrades.service 2>/dev/null || true
systemctl mask unattended-upgrades.service 2>/dev/null || true
cat > /etc/apt/apt.conf.d/99-lab-no-auto-upgrade <<'EOF'
APT::Periodic::Update-Package-Lists "0";
APT::Periodic::Unattended-Upgrade "0";
Unattended-Upgrade::Allowed-Origins {};
EOF

# Hold all kernel-related packages at their current (vulnerable) version.
HELD=$(dpkg-query -W -f='${Package}\n' 'linux-image-*' 'linux-headers-*' \
       'linux-modules-*' 'linux-generic*' 'linux-image-generic*' 2>/dev/null | sort -u)
if [ -n "$HELD" ]; then
  apt-mark hold $HELD || true
fi
echo "[lab-init] held kernel packages:"; apt-mark showhold

# --- 2. Gate: running kernel must be in the affected range.
#         Affected: 4.14 .. (fixed stable tags). The 6.x line is fixed at the
#         per-series stable tag (6.7..6.12.85, 6.13..6.18.22, 6.19..6.19.12).
#         The AMI's 6.8 GA kernel is well below 6.12.85 -> vulnerable. We accept
#         the broad mainline floor (>=4.14) and reject anything at/above a known
#         fixed stable tag for its series.
KREL=$(uname -r)
KVER=$(echo "$KREL" | grep -oE '^[0-9]+\.[0-9]+\.[0-9]+' | head -1)
echo "[lab-init] running kernel: $KREL (parsed $KVER)"
[ -n "$KVER" ] || FAIL "could not parse kernel version from '$KREL'"

ver_ge() { [ "$(printf '%s\n%s\n' "$1" "$2" | sort -V | tail -1)" = "$1" ]; }   # $1 >= $2
ver_lt() { [ "$1" != "$2" ] && ! ver_ge "$1" "$2"; }                            # $1 <  $2

MAJMIN=$(echo "$KVER" | cut -d. -f1-2)
# Per-series fixed tag (exclusive upper bound). A build is vulnerable iff its
# version is >= 4.14 (the affected floor) AND strictly below the fixed stable
# tag for its own stable series. Series whose every released build predates the
# fix get the lowest fixed tag as a sentinel (>= 4.14, < tag always holds for a
# released build in an unfixed series).
case "$MAJMIN" in
  5.10)                         FIXED="5.10.254" ;;
  5.11|5.12|5.13|5.14|5.15)     FIXED="5.15.204" ;;
  5.16|5.17|5.18|5.19|6.0|6.1)  FIXED="6.1.170" ;;
  6.2|6.3|6.4|6.5|6.6)          FIXED="6.6.137" ;;
  6.7|6.8|6.9|6.10|6.11|6.12)   FIXED="6.12.85" ;;
  6.13|6.14|6.15|6.16|6.17|6.18) FIXED="6.18.22" ;;
  6.19)                         FIXED="6.19.12" ;;
  4.14|4.15|4.16|4.17|4.18|4.19|5.0|5.1|5.2|5.3|5.4|5.5|5.6|5.7|5.8|5.9) FIXED="99.0.0" ;;
  *) FAIL "kernel series $MAJMIN is not a known affected series (likely patched/too new)" ;;
esac

# Vulnerable iff >= 4.14 AND < FIXED for its series.
ver_ge "$KVER" "4.14"  || FAIL "kernel $KVER is older than the 4.14 affected floor"
ver_lt "$KVER" "$FIXED" || FAIL "kernel $KVER is at/above fixed tag $FIXED for series $MAJMIN (PATCHED)"
echo "[lab-init] kernel $KVER is within the affected range (series $MAJMIN, fixed at $FIXED)"

# --- 3. Confirm AF_ALG / algif_aead transport is usable.
modprobe algif_aead 2>/dev/null || true
modprobe authencesn 2>/dev/null || true
python3 - <<'PY' || FAIL "AF_ALG / algif_aead bind failed (transport unavailable)"
import socket, sys
try:
    s = socket.socket(socket.AF_ALG, socket.SOCK_SEQPACKET, 0)
    s.bind(("aead", "authencesn(hmac(sha256),cbc(aes))"))
    s.close()
except Exception as e:
    print("AF_ALG check failed:", e); sys.exit(1)
print("AF_ALG authencesn bind OK")
PY
echo "[lab-init] AF_ALG/authencesn transport OK"

# --- 4. Unprivileged actor: no sudo, no caps, no special groups.
if ! id lowpriv >/dev/null 2>&1; then
  useradd -m -s /bin/bash lowpriv
fi
# Make sure lowpriv is NOT in sudo/admin and has a usable home for the exploit.
deluser lowpriv sudo 2>/dev/null || true
deluser lowpriv admin 2>/dev/null || true
passwd -d lowpriv >/dev/null 2>&1 || true
# Toolchain for compiling/running an exploit offline (baked now, not at exploit time).
DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
  build-essential gcc make python3 >/dev/null 2>&1 || true

# --- 5. Clean root-owned target baseline.
#   (a) setuid-root binary image target: /usr/bin/su is present by default and
#       root-owned setuid -- the canonical Strategy-1 target. Record its baseline.
#   (b) credential-database target: /etc/passwd with lowpriv at its normal
#       unprivileged UID -- the canonical Strategy-2 target.
#   (c) a generic root-only marker location for the "privileged side effect"
#       observable: a dir only root can write, that lowpriv can read.
install -d -m 0755 -o root -g root /opt/lab-target
# Root-only-writable marker dir; the verifier reads ownership of whatever the
# exploit drops here through the OOB root channel.
install -d -m 0755 -o root -g root /var/lib/lab-marker
chmod 0755 /var/lib/lab-marker

SU_PATH=/usr/bin/su
[ -u "$SU_PATH" ] || FAIL "$SU_PATH is not setuid-root (baseline target missing)"
SU_SHA=$(sha256sum "$SU_PATH" | awk '{print $1}')
LOWPRIV_PWLINE=$(getent passwd lowpriv)
LOWPRIV_UID=$(id -u lowpriv)

# --- 6. Root-readable baseline manifest + readiness marker (the OOB channel
#         reads these). NB: no secrets here -- this is state-class, not read-class.
cat > /root/lab-baseline.json <<EOF
{
  "cve": "CVE-2026-31431",
  "kernel": "$KREL",
  "kernel_version": "$KVER",
  "fixed_tag_for_series": "$FIXED",
  "vulnerable": true,
  "unprivileged_actor": "lowpriv",
  "lowpriv_uid": $LOWPRIV_UID,
  "lowpriv_passwd_line": "$LOWPRIV_PWLINE",
  "setuid_target": "$SU_PATH",
  "setuid_target_sha256_baseline": "$SU_SHA",
  "marker_dir": "/var/lib/lab-marker",
  "marker_dir_owner": "root"
}
EOF
chmod 0600 /root/lab-baseline.json

date -u +%FT%TZ > /root/.lab-ready
chmod 0644 /root/.lab-ready   # world-readable readiness flag (no secret content)
echo "[lab-init] READY: baseline staged, kernel vulnerable, AF_ALG OK"
echo "[lab-init] done $(date -u)"
