# -*- mode: ruby -*-
# vi: set ft=ruby :
#
# tools/verify-vm/Vagrantfile — parameterized verifier VM.
#
# Driven by env vars set by tools/verify-vm/verify.sh:
#
#   SKK_VM_BOX            generic/<box> name (e.g. generic/debian11)
#   SKK_VM_KERNEL_PKG     optional apt package for the vulnerable kernel
#                         (e.g. linux-image-5.13.0-19-generic). Empty = use stock.
#   SKK_VM_KERNEL_VERSION expected kernel version after install
#   SKK_VM_HOSTNAME       hostname for this VM (used in vagrant box name)
#
# The Vagrantfile mounts the repo root at /vagrant (Vagrant default) so the
# in-VM `make` builds against your live source — no rebuild loop.

require "yaml"

REPO_ROOT = File.expand_path("../..", __dir__)

box      = ENV["SKK_VM_BOX"]              || "generic/debian12"
pkg      = ENV["SKK_VM_KERNEL_PKG"]       || ""
mainline = ENV["SKK_VM_MAINLINE_VERSION"] || ""
kver     = ENV["SKK_VM_KERNEL_VERSION"]   || ""
host     = ENV["SKK_VM_HOSTNAME"]         || "skk-verify"

Vagrant.configure("2") do |c|
  # Define ONE Vagrant machine named after SKK_VM_HOSTNAME. Per-module
  # isolation: each module gets its own `skk-<module>` machine that
  # vagrant tracks in .vagrant/machines/skk-<module>/parallels/.
  c.vm.define host do |m|
    m.vm.box = box
    # Guest hostnames forbid underscores per RFC 952. Vagrant machine
    # names allow them (we keep skk-cgroup_release_agent so per-module
    # state stays isolated in .vagrant/machines/), but inside the VM
    # we translate to hyphens so the hostname is RFC-valid.
    m.vm.hostname = host.gsub("_", "-")

    m.vm.synced_folder REPO_ROOT, "/vagrant",
      type: "rsync", rsync__exclude: ["build/", ".git/", "*.o", "skeletonkey-test*"]

    m.vm.provider "parallels" do |p|
      p.memory = 2048
      p.cpus   = 2
      p.name   = host
      # Don't auto-update Parallels Tools: the installer fails on older
      # guest kernels (e.g. Ubuntu 20.04's 5.4.0-169 is "outdated and
      # not supported" by latest tools). We use rsync over SSH for
      # sync_folder, which doesn't need the guest tools at all.
      p.update_guest_tools = false
      p.check_guest_tools  = false
    end

    # 1. Always install build deps + sudo (needed for module verification).
    m.vm.provision "shell", inline: <<-SHELL
      set -e
      if command -v apt-get >/dev/null 2>&1; then
          export DEBIAN_FRONTEND=noninteractive
          apt-get update -qq
          apt-get install -y -qq build-essential libglib2.0-dev pkg-config sudo curl ca-certificates
      elif command -v dnf >/dev/null 2>&1; then
          dnf install -y -q gcc make glib2-devel pkgconfig sudo curl
      fi
    SHELL

    # 2a. Pin via apt if requested. Reboot needed afterward.
    if !pkg.empty?
      m.vm.provision "shell", name: "pin-kernel-#{pkg}", inline: <<-SHELL
        set -e
        if dpkg-query -W -f='${Status}' #{pkg} 2>/dev/null | grep -q 'install ok installed'; then
            echo "[=] #{pkg} already installed"
        else
            echo "[+] installing #{pkg} (kernel target #{kver})"
            export DEBIAN_FRONTEND=noninteractive
            apt-get install -y -qq #{pkg}
            echo "[i] kernel #{pkg} installed; reboot via 'vagrant reload'"
        fi
      SHELL
    end

    # 2b. Pin via kernel.ubuntu.com/mainline/ if mainline_version is set.
    # Fetches the four .debs (linux-headers _all, linux-headers _amd64
    # generic, linux-image-unsigned generic, linux-modules generic),
    # dpkg -i's them, regenerates grub, and prints a reboot hint.
    # Mainline kernel package version like "5.15.5-051505" sorts ABOVE
    # Ubuntu's stock "5.15.0-91" in debian-version-compare (numeric
    # 51505 > 91), so update-grub puts it at boot index 0 and the next
    # boot lands on it automatically.
    if !mainline.empty?
      m.vm.provision "shell", name: "pin-mainline-#{mainline}", inline: <<-SHELL
        set -e
        KVER="#{mainline}"
        # already booted into it?
        if uname -r | grep -q "^${KVER}-[0-9]\\+-generic"; then
            echo "[=] mainline ${KVER} already booted ($(uname -r))"
            exit 0
        fi
        # already installed on disk (waiting on reboot)?
        if ls /boot/vmlinuz-${KVER}-* >/dev/null 2>&1; then
            echo "[=] mainline ${KVER} already installed; needs reboot"
            exit 0
        fi
        echo "[+] fetching kernel.ubuntu.com mainline v${KVER}"
        # Newer mainline kernels live under /v${KVER}/amd64/; older ones
        # (≤ ~4.15) put debs at /v${KVER}/ directly. Try /amd64/ first;
        # fall back to bare. linux-image-unsigned was renamed from
        # linux-image- around 4.18 — old kernels use the plain name.
        BASE="https://kernel.ubuntu.com/mainline/v${KVER}"
        for URL in "${BASE}/amd64/" "${BASE}/"; do
            INDEX=$(curl -sL "$URL")
            if echo "$INDEX" | grep -q '\\.deb"'; then
                break
            fi
        done
        TMP=$(mktemp -d)
        cd "$TMP"
        # Pick the 4 canonical generic-kernel .debs by pattern match against
        # the directory index. Skip lowlatency variants. Accept both
        # 'linux-image-unsigned-' (newer) and 'linux-image-' (older).
        DEBS=$(echo "$INDEX" | \\
            grep -oE 'href="[^"]+\\.deb"' | sed 's/href="//; s/"$//' | \\
            grep -E '(linux-image(-unsigned)?|linux-modules|linux-headers)-[0-9.]+-[0-9]+-generic_|linux-headers-[0-9.]+-[0-9]+_[^_]+_all\\.deb' | \\
            grep -v lowlatency)
        if [ -z "$DEBS" ]; then
            echo "[-] no .debs found at ${BASE}/ (tried /amd64/ and bare)" >&2
            exit 2
        fi
        for f in $DEBS; do
            echo "[+]   $f"
            curl -fsSL -O "${URL}${f}"
        done
        export DEBIAN_FRONTEND=noninteractive
        dpkg -i *.deb || apt-get install -f -y -qq
        update-grub 2>&1 | tail -3
        echo "[i] mainline ${KVER} installed; reboot via 'vagrant reload'"
      SHELL
    end

    # 2c. Optional per-module provisioner. If
    # tools/verify-vm/provisioners/<module>.sh exists, run it as root
    # before build-and-verify. Used for things only meaningful per-module:
    # build sudo 1.9.16 from source (sudo_chwoot), drop a polkit allow
    # rule (udisks_libblockdev), add a sudoers grant (sudo_runas_neg1).
    skk_mod = ENV["SKK_MODULE"] || ""
    if !skk_mod.empty?
      prov_path = File.join(__dir__, "provisioners", "#{skk_mod}.sh")
      if File.exist?(prov_path)
        m.vm.provision "shell", name: "module-provision-#{skk_mod}",
          path: prov_path
      end
    end

    # 3. Build SKELETONKEY in-VM and run --explain --active for the target
    #    module. Runs as the unprivileged 'vagrant' user (NOT root) — most
    #    detect()s gate on "are you already root?" and short-circuit if so,
    #    which would invalidate every verification (pack2theroot was the
    #    motivating case). 'privileged: false' is how vagrant downshifts.
    #    SKK_MODULE is set by verify.sh on the second-pass `vagrant
    #    provision` call (post-reboot if kernel was pinned).
    m.vm.provision "shell", name: "build-and-verify", run: "never",
      privileged: false,
      env: { "SKK_MODULE" => ENV["SKK_MODULE"] || "" },
      inline: <<-SHELL
      set -e
      cd /vagrant
      echo "[*] running as $(id)"
      echo "[*] kernel: $(uname -r)"
      echo "[*] building skeletonkey..."
      make clean >/dev/null 2>&1 || true
      make 2>&1 | tail -3
      echo
      echo "[*] running: skeletonkey --explain ${SKK_MODULE} --active"
      echo
      ./skeletonkey --explain "${SKK_MODULE}" --active 2>&1 || true
    SHELL
  end
end
