#!/bin/bash
# Comprehensive stat sandbox tests to ensure hidden paths cannot be made visible
# via procfs magic, dot/dotdot traversals, symlinks, or combinations thereof.
#
# Copyright (c) 2025 Ali Polatel <alip@chesswob.org>
# SPDX-License-Identifier: GPL-3.0

set -eu

PASS=0 FAIL=0 SKIP=0
ok()   { PASS=$((PASS+1)); printf "[ ok ] %s\n" "$*"; }
fail() { FAIL=$((FAIL+1)); printf "[fail] %s\n" "$*"; }

must_block() {
  # Hidden target must not be observable via stat -L on the constructed path.
  # Any success means canonicalizer+sandbox let the hidden target “pass through”.
  if stat -L -- "$1" >/dev/null 2>&1; then
    fail "$2 :: visible => $1"
  else
    ok "$2 :: blocked"
  fi
}

# Test layout under current working directory
mkdir -p A B C NEST CHAINF CHAIND MIXD
: > A/secret
: > A/other
: > B/other
: > C/visible

ROOT="$(pwd -P)"
SECRET="${ROOT}/A/secret"

# Hide the file we'll try to unhide everywhere
test -c "/dev/syd/deny/stat+${SECRET}" >/dev/null

# Useful links
ln -sf A             LA   # dir symlink (relative)
ln -sf "${ROOT}/A"   AABS # dir symlink (absolute)
ln -sf "A/secret"    SREL # file symlink (relative to secret)
ln -sf "${SECRET}"   SABS # file symlink (absolute to secret)

# fd anchor for /proc/self/fd/N walking (N -> ".")
exec 9<.

PID="$$"
TSCWD="/proc/thread-self/cwd"
SCWD="/proc/self/cwd"
PCWD="/proc/${PID}/cwd"
FD9="/proc/self/fd/9"
SROOT="/proc/self/root"

echo "-- [1] procfs magic symlinks ------------------------------------------------"

# 1.A: cwd magics with varied suffixes
PFX_LIST="${SCWD} ${PCWD} ${TSCWD}"
for PFX in ${PFX_LIST}; do
  for SFX in \
    "A/secret" "./A/secret" "A/./secret" "A//secret" "././A//secret" \
    "B/../A/secret" "A/../A/secret" "./B/../A/./secret" \
    "A/secret/" "A/./secret/" "B/../A/secret/" \
    "LA/secret" "AABS/secret" "LA/./secret" "AABS/./secret" \
    "LA/../A/secret" "AABS/../A/secret" \
    "SREL" "SABS" "./SREL" "./SABS"
  do
    must_block "${PFX}/${SFX}" "PROC.cwds: ${PFX} + ${SFX}"
  done

  # redundant slashes ladder
  i=1
  while [ $i -le 20 ]; do
    SL=""
    j=1; while [ $j -le $i ]; do SL="${SL}/"; j=$((j+1)); done
    must_block "${PFX}/A${SL}secret" "PROC.slashes: ${PFX} + A${SL}secret"
    i=$((i+1))
  done

  # dotdot normalizations
  for MID in "" "A/.." "B/.." "A/./.." "B/./.." "LA/.." "AABS/.."; do
    must_block "${PFX}/${MID}A/secret" "PROC.dotdot: ${PFX} + ${MID}A/secret"
  done
done

# 1.B: /proc/self/root with absolute paths
ABS_CANDS="
${ROOT}/A/secret
${ROOT}/A/./secret
${ROOT}/A//secret
${ROOT}/B/../A/secret
${ROOT}/A/../A/secret
${ROOT}/./A/secret
${ROOT}//A///secret
${ROOT}/A/secret/
"
for P in $ABS_CANDS; do
  must_block "${SROOT}${P}" "PROC.root: ${P}"
done
i=1
while [ $i -le 30 ]; do
  DOTS=""
  k=1; while [ $k -le $i ]; do DOTS="${DOTS}./"; k=$((k+1)); done
  must_block "${SROOT}${ROOT}/${DOTS}A/secret" "PROC.root.dots($i)"
  i=$((i+1))
done

# 1.C: /proc/self/fd/9 anchor
for s in \
  "A/secret" "./A/secret" "A/./secret" "B/../A/secret" "A/../A/secret" \
  "LA/secret" "AABS/secret" "SREL" "SABS" "LA/./secret" "AABS/./secret"
do
  must_block "${FD9}/${s}" "PROC.fd9: ${s}"
done
i=1
while [ $i -le 30 ]; do
  must_block "${FD9}/./B/../A/././secret" "PROC.fd9.dots-cancel-$i"
  i=$((i+1))
done

echo "-- [2] dot & dotdot group ---------------------------------------------------"

# Pure filesystem traversals (no /proc anchors)

# 2.A: canonical + noise
for P in \
  "A/secret" "./A/secret" ".//A///secret" "A/./secret" "A//secret" \
  "B/../A/secret" "A/../A/secret" "./B/../A/./secret" \
  "A/secret/" "A/./secret/" "B/../A/secret/"
do
  must_block "$P" "DOT: $P"
done

# 2.B: deep dotdot collapses
depth=1
while [ $depth -le 60 ]; do
  PATHP="NEST"
  i=1
  while [ $i -le $depth ]; do
    DN="N${i}"
    mkdir -p "${PATHP}/${DN}"
    PATHP="${PATHP}/${DN}"
    i=$((i+1))
  done

  UP=""
  i=1; while [ $i -le $depth ]; do UP="${UP}../"; i=$((i+1)); done

  must_block "${PATHP}/${UP}A/secret"   "DOTDOT: depth ${depth}"
  must_block "${PATHP}/./${UP}./A/./secret" "DOTDOT+: depth ${depth}"
  depth=$((depth+1))
done

echo "-- [3] symlinks group --------------------------------------------------------"

# 3.A: direct symlink probes
for L in SREL SABS; do
  must_block "$L" "SYMLINK.file: $L"
  must_block "./$L" "SYMLINK.file.dot: ./$L"
done
for D in LA AABS; do
  for suf in "secret" "./secret" "//secret" "././secret"; do
    must_block "${D}/${suf}" "SYMLINK.dir: ${D}/${suf}"
  done
done

# 3.B: file symlink chains L1->...->secret
TARGET="$SECRET"
n=1
while [ $n -le 70 ]; do
  L="CHAINF/L${n}"
  ln -sf "$TARGET" "$L"
  TARGET="$L"
  must_block "CHAINF/L1" "CHAINF.len=${n}"
  n=$((n+1))
done

# 3.C: dir symlink chains DL1->...->A then /secret
DTARGET="${ROOT}/A"
m=1
while [ $m -le 60 ]; do
  D="CHAIND/DL${m}"
  ln -sf "$DTARGET" "$D"
  DTARGET="$D"
  for suf in "secret" "./secret" "//secret" "././secret"; do
    must_block "CHAIND/DL1/${suf}" "CHAIND.len=${m} suf=${suf}"
  done
  m=$((m+1))
done

echo "-- [4] mixed (proc + dotdot + symlinks) -------------------------------------"

# Tighten: hide the directory as well, then try many combinations
test -c "/dev/syd/deny/stat+${ROOT}/A" >/dev/null

# 4.A: proc cwd anchors + dir links + dotdots
for PFX in "${SCWD}" "${PCWD}" "${TSCWD}" "${FD9}" ; do
  for PAT in \
    "LA/secret" "LA/./secret" "LA/../A/secret" \
    "AABS/secret" "AABS/./secret" "AABS/../A/secret" \
    "./B/../LA/secret" "./B/../AABS/secret" \
    "CHAINF/L1" "CHAIND/DL1/secret" \
    "B/../CHAIND/DL1/./secret" \
    "LA//secret" "AABS//secret"
  do
    must_block "${PFX}/${PAT}" "MIX.proc+ln: ${PFX} + ${PAT}"
  done

  # ladder of noise
  i=1
  while [ $i -le 30 ]; do
    must_block "${PFX}/./B/../LA/./secret" "MIX.proc+ln+dots i=$i"
    i=$((i+1))
  done
done

# 4.B: /proc/self/root + absolute + symlink hops
for Q in \
  "${ROOT}/LA/secret" "${ROOT}/LA/./secret" "${ROOT}/LA/../A/secret" \
  "${ROOT}/AABS/secret" "${ROOT}/AABS/./secret" "${ROOT}/AABS/../A/secret" \
  "${ROOT}/CHAINF/L1" "${ROOT}/CHAIND/DL1/secret" \
  "${ROOT}/B/../LA/secret" "${ROOT}//LA///secret" \
  "${ROOT}/./CHAIND/../CHAIND/DL1/./secret"
do
  must_block "${SROOT}${Q}" "MIX.root: ${Q}"
done

# 4.C: proc cwd anchors + file symlinks directly
for PFX in "${SCWD}" "${PCWD}" "${TSCWD}" "${FD9}" ; do
  for L in "SREL" "SABS" "./SREL" "./SABS"; do
    must_block "${PFX}/${L}" "MIX.proc+filelink: ${PFX} + ${L}"
  done
done

# 4.D: stress with growing chains after directory hidden
q=1
while [ $q -le 40 ]; do
  must_block "${SCWD}/CHAINF/L1" "MIX.chainF.after-hide q=$q"
  must_block "${SCWD}/CHAIND/DL1/secret" "MIX.chainD.after-hide q=$q"
  q=$((q+1))
done

# Summary
printf -- "--\nTotal: %d  Pass: %d  Fail: %d  Skip: %d\n" $((PASS+FAIL+SKIP)) "$PASS" "$FAIL" "$SKIP"
[ "$FAIL" -eq 0 ] || exit 1
