new diag_perm
authorThomas Walker Lynch <eknp9n@reasoningtechnology.com>
Thu, 25 Sep 2025 05:52:48 +0000 (05:52 +0000)
committerThomas Walker Lynch <eknp9n@reasoningtechnology.com>
Thu, 25 Sep 2025 05:52:48 +0000 (05:52 +0000)
developer/source/diag_perm
release/shell/diag_perm

index d40f495..ba10d90 100644 (file)
 #!/usr/bin/env bash
-# Diagnose Man_In_Grey privilege flow: user -> setuid gasket -> python inner
 set -euo pipefail
 
-# --- locate release base from this script's real path ---
-_this="$(readlink -f "$0")"
-_shell_dir="$(cd -- "$(dirname -- "$_this")" && pwd -P)"   # .../release/shell
-_relbase="$(dirname "$_shell_dir")"                        # .../release
-
-# --- arch normalize ---
-_arch_raw=$(uname -m | tr '[:upper:]' '[:lower:]')
-case "$_arch_raw" in
-  amd64|x64)       _arch="x86_64" ;;
-  x86_64)          _arch="x86_64" ;;
-  i386|i486|i586|i686) _arch="i686" ;;
-  arm64|aarch64)   _arch="aarch64" ;;
-  armv7l)          _arch="armv7l" ;;
-  armv6l)          _arch="armv6l" ;;
-  riscv64)         _arch="riscv64" ;;
-  ppc64le|powerpc64le) _arch="ppc64le" ;;
-  s390x)           _arch="s390x" ;;
-  *)               _arch="$_arch_raw" ;;
-esac
+# --- tiny helpers -------------------------------------------------------------
+say(){ printf "%s\n" "$*"; }
+have(){ command -v "$1" >/dev/null 2>&1; }
 
-# Allow explicit overrides via args
-GASKET="${1:-"${_relbase}/${_arch}/man_in_grey_apply"}"
-WRAP="${2:-"${_relbase}/shell/Man_In_Grey"}"
+longest_mount_covering() {
+  # Arg: absolute path
+  # Output: "MOUNTPOINT FSTYPE OPTIONS" (best effort)
+  local target="$1"
+  # Parse /proc/self/mountinfo to find the longest mountpoint prefix
+  # Fields: ... 5:mountpoint 6:opts ... " - " 9:fstype ...
+  awk -v P="$target" '
+    BEGIN{best=""; bestlen=-1}
+    {
+      # split into pre/post " - "
+      line=$0
+      split(line,parts," - ")
+      pre=parts[1]; post=parts[2];
+      n=split(pre,a," ")
+      mp=a[5]; opts=a[6];
+      nf=split(post,b," ")
+      fstype=b[1];
 
-echo "== who/where =="
-echo "user: $(id -un) (uid=$(id -u))  groups: $(id -nG)"
-echo "pwd : $(pwd)"
-echo
+      # mountpoints in mountinfo use escape sequences for spaces; safe for our use
+      if (index(P, mp)==1) {
+        l=length(mp)
+        if (l>bestlen) { best=mp; bestlen=l; BO=opts; FT=fstype; }
+      }
+    }
+    END{
+      if (bestlen>=0) { print best " " FT " " BO; }
+    }' /proc/self/mountinfo
+}
 
-echo "== paths =="
-echo "wrapper: $WRAP"
-echo "gasket : $GASKET"
-echo
+# --- locate repo bits ---------------------------------------------------------
+script_afp="$(realpath "${BASH_SOURCE[0]}")"
+RELEASE_DIR="$(cd "$(dirname "$script_afp")/.." && pwd -P)"
+WRAP="$RELEASE_DIR/shell/Man_In_Grey"
 
-echo "== wrapper sanity =="
-if [[ -x "$WRAP" ]]; then
-  head -n 1 "$WRAP" | sed 's/^/shebang: /'
-  echo "exec path: $(command -v "$WRAP" || echo '(not in PATH)')"
+# Compute arch dir like the rest of the tools
+arch_raw="$(uname -m | tr '[:upper:]' '[:lower:]')"
+case "$arch_raw" in
+  amd64|x64) arch="x86_64" ;;
+  x86_64)    arch="x86_64" ;;
+  i386|i486|i586|i686) arch="i686" ;;
+  arm64|aarch64) arch="aarch64" ;;
+  armv7l)    arch="armv7l" ;;
+  armv6l)    arch="armv6l" ;;
+  riscv64)   arch="riscv64" ;;
+  ppc64le|powerpc64le) arch="ppc64le" ;;
+  s390x)     arch="s390x" ;;
+  *)         arch="$arch_raw" ;;
+esac
+GASKET="$RELEASE_DIR/$arch/man_in_grey_apply"
+
+# --- who/where ----------------------------------------------------------------
+u="$(id -un)"; uid="$(id -u)"
+groups="$(id -nG 2>/dev/null || true)"
+pwd="$(pwd -P)"
+say "== who/where =="
+say "user: $u (uid=$uid)  groups: ${groups:-unknown}"
+say "pwd : $pwd"
+say
+say "== paths =="
+say "wrapper: $WRAP"
+say "gasket : $GASKET"
+say
+
+# --- wrapper sanity ------------------------------------------------------------
+say "== wrapper sanity =="
+if [[ -f "$WRAP" && -x "$WRAP" ]]; then
+  read -r firstline <"$WRAP" || true
+  say "shebang: ${firstline:-<empty>}"
+  say "exec path: $WRAP"
 else
-  echo "!! wrapper missing/non-exec"
+  say "!! wrapper missing/non-exec"
 fi
-echo
+say
 
-echo "== gasket file sanity =="
+# --- gasket file sanity & mount flags -----------------------------------------
+say "== gasket file sanity =="
 if [[ -e "$GASKET" ]]; then
   ls -l "$GASKET"
-  if [[ -u "$GASKET" ]]; then echo "setuid: YES"; else echo "setuid: NO"; fi
-  mp="$(dirname "$(readlink -f "$GASKET")")"
-  echo "mount: $(findmnt -no TARGET,FSTYPE,OPTIONS "$mp" 2>/dev/null || echo 'findmnt not available')"
+  if [[ -u "$GASKET" ]]; then
+    say "setuid: YES"
+  else
+    say "setuid: NO"
+  fi
+
+  # try to find findmnt robustly
+  FINDMNT_BIN=""
+  for c in "${FINDMNT_BIN:-}" "$(command -v findmnt 2>/dev/null || true)" /usr/bin/findmnt /bin/findmnt /usr/sbin/findmnt; do
+    [[ -n "$c" && -x "$c" ]] && { FINDMNT_BIN="$c"; break; }
+  done
+
+  gasket_abs="$(realpath "$GASKET")"
+  if [[ -n "$FINDMNT_BIN" ]]; then
+    # prefer findmnt -T if supported
+    if "$FINDMNT_BIN" -T "$gasket_abs" >/dev/null 2>&1; then
+      out="$("$FINDMNT_BIN" -no TARGET,FSTYPE,OPTIONS -T "$gasket_abs" || true)"
+    else
+      # fallback: search by path in the tree
+      out="$("$FINDMNT_BIN" -no TARGET,FSTYPE,OPTIONS "$gasket_abs" 2>/dev/null || true)"
+    fi
+    if [[ -n "${out:-}" ]]; then
+      say "mount: $out"
+      mp="$(awk '{print $1}' <<<"$out")"
+      fstype="$(awk '{print $2}' <<<"$out")"
+      opts="$(awk '{$1="";$2=""; sub(/^  */,""); print}' <<<"$out")"
+    else
+      say "mount: findmnt returned no match, falling back to /proc/self/mountinfo"
+    fi
+  else
+    say "mount: findmnt not found, falling back to /proc/self/mountinfo"
+  fi
+
+  if [[ -z "${opts:-}" ]]; then
+    parsed="$(longest_mount_covering "$gasket_abs" || true)"
+    if [[ -n "$parsed" ]]; then
+      mp="$(awk '{print $1}' <<<"$parsed")"
+      fstype="$(awk '{print $2}' <<<"$parsed")"
+      opts="$(awk '{$1="";$2=""; sub(/^  */,""); print}' <<<"$parsed")"
+      say "mount: $mp $fstype $opts  (from /proc/self/mountinfo)"
+    else
+      say "mount: could not determine mount options (even via mountinfo)"
+    fi
+  fi
+
+  if [[ -n "${opts:-}" ]]; then
+    if grep -qw nosuid <<<"$opts"; then
+      say "‼ detected nosuid on mount → setuid will NOT take effect here"
+    else
+      say "✓ nosuid NOT present on mount"
+    fi
+    if ! grep -qw exec <<<"$opts"; then
+      say "‼ mount appears noexec (or exec not set) → binary execution may fail"
+    fi
+  fi
 else
-  echo "!! gasket not found"
+  say "!! gasket not found"
 fi
-echo
+say
 
-echo "== gasket self-report (--print-flags) =="
+# --- gasket self-report --------------------------------------------------------
+say "== gasket self-report (--print-flags) =="
 if [[ -x "$GASKET" ]]; then
   "$GASKET" --print-flags || true
 else
-  echo "skip (no gasket)"
+  say "skip (no gasket)"
 fi
-echo
+say
 
-echo "== python inner EUID test =="
-PYTEST="/tmp/mig_ids_$$.py"
-cat >"$PYTEST"<<'PY'
+# --- python inner EUID test ----------------------------------------------------
+say "== python inner EUID test =="
+if [[ -x "$GASKET" ]]; then
+  tmp_py="$(mktemp /tmp/mig_ids_XXXXXX.py)"
+  cat >"$tmp_py" <<'PY'
 import os, pwd
-u = os.getuid(); e = os.geteuid()
-name = pwd.getpwuid(u).pw_name if u>=0 else "?"
-print(f"py.real_uid={u} ({name})")
-print(f"py.effective_uid={e}")
-print("py.groups=", os.getgroups())
+uid = os.getuid()
+euid = os.geteuid()
+name = pwd.getpwuid(uid).pw_name
+print(f"py.real_uid={uid} ({name})")
+print(f"py.effective_uid={euid}")
+print("py.groups=", sorted(os.getgroups()))
 PY
-chmod 0644 "$PYTEST"
-
-if [[ -x "$GASKET" ]]; then
-  echo "running: $GASKET --inner $PYTEST"
-  "$GASKET" --inner "$PYTEST" || true
+  say "running: $GASKET --inner $tmp_py"
+  "$GASKET" --inner "$tmp_py" || true
+  rm -f "$tmp_py"
 else
-  echo "skip (no gasket)"
+  say "skip (no gasket)"
 fi
-rm -f "$PYTEST"
-echo
+say
 
-echo "== wrapper execution trace (dry) =="
+# --- wrapper execution trace (dry) --------------------------------------------
+say "== wrapper execution trace (dry) =="
 if [[ -x "$WRAP" ]]; then
-  echo "+ bash -x \"$WRAP\" --phase-2-sanity2-then-stop --stage dummy  (trace only)"
   set +e
-  bash -x "$WRAP" --phase-2-sanity2-then-stop --stage dummy 1>/dev/null 2>&1 | sed 's/^/TRACE: /' || true
+  bash -x "$WRAP" --phase-2-sanity2-then-stop --stage dummy >/dev/null 2>&1 \
+    && say "+ bash -x \"$WRAP\" --phase-2-sanity2-then-stop --stage dummy  (trace only)" \
+    || say "+ bash -x \"$WRAP\" --phase-2-sanity2-then-stop --stage dummy  (trace only)"
   set -e
 else
-  echo "skip (no wrapper)"
+  say "skip (no wrapper)"
 fi
+say
 
-echo
-echo "== verdict (rules of thumb) =="
-cat <<'EOT'
-- If --print-flags shows flag.this_process_privileged=0: setuid isn’t taking effect (check nosuid mount, bit not set, or wrong binary).
-- If that flag is 1 and the Python test shows euid=0: privileges flow is OK; if you still get EPERM to /etc, the wrapper may bypass the gasket sometimes.
-- If the wrapper trace never execs the gasket: replace the wrapper so it always runs the gasket.
-- If root is refused for “not in sudo”, update gasket policy to allow UID 0 (you already have that patch).
-EOT
+# --- verdict -------------------------------------------------------------------
+say "== verdict (rules of thumb) =="
+say "- If --print-flags shows flag.this_process_privileged=0: setuid isn’t taking effect (check nosuid mount, bit not set, or wrong binary)."
+say "- If that flag is 1 and the Python test shows euid=0: privileges flow is OK; if you still get EPERM to /etc, the wrapper may bypass the gasket sometimes."
+say "- If the wrapper trace never execs the gasket: replace the wrapper so it always runs the gasket."
+say "- If root is refused for “not in sudo”, update gasket policy to allow UID 0 (you have that patch)."
index d40f495..ba10d90 100755 (executable)
 #!/usr/bin/env bash
-# Diagnose Man_In_Grey privilege flow: user -> setuid gasket -> python inner
 set -euo pipefail
 
-# --- locate release base from this script's real path ---
-_this="$(readlink -f "$0")"
-_shell_dir="$(cd -- "$(dirname -- "$_this")" && pwd -P)"   # .../release/shell
-_relbase="$(dirname "$_shell_dir")"                        # .../release
-
-# --- arch normalize ---
-_arch_raw=$(uname -m | tr '[:upper:]' '[:lower:]')
-case "$_arch_raw" in
-  amd64|x64)       _arch="x86_64" ;;
-  x86_64)          _arch="x86_64" ;;
-  i386|i486|i586|i686) _arch="i686" ;;
-  arm64|aarch64)   _arch="aarch64" ;;
-  armv7l)          _arch="armv7l" ;;
-  armv6l)          _arch="armv6l" ;;
-  riscv64)         _arch="riscv64" ;;
-  ppc64le|powerpc64le) _arch="ppc64le" ;;
-  s390x)           _arch="s390x" ;;
-  *)               _arch="$_arch_raw" ;;
-esac
+# --- tiny helpers -------------------------------------------------------------
+say(){ printf "%s\n" "$*"; }
+have(){ command -v "$1" >/dev/null 2>&1; }
 
-# Allow explicit overrides via args
-GASKET="${1:-"${_relbase}/${_arch}/man_in_grey_apply"}"
-WRAP="${2:-"${_relbase}/shell/Man_In_Grey"}"
+longest_mount_covering() {
+  # Arg: absolute path
+  # Output: "MOUNTPOINT FSTYPE OPTIONS" (best effort)
+  local target="$1"
+  # Parse /proc/self/mountinfo to find the longest mountpoint prefix
+  # Fields: ... 5:mountpoint 6:opts ... " - " 9:fstype ...
+  awk -v P="$target" '
+    BEGIN{best=""; bestlen=-1}
+    {
+      # split into pre/post " - "
+      line=$0
+      split(line,parts," - ")
+      pre=parts[1]; post=parts[2];
+      n=split(pre,a," ")
+      mp=a[5]; opts=a[6];
+      nf=split(post,b," ")
+      fstype=b[1];
 
-echo "== who/where =="
-echo "user: $(id -un) (uid=$(id -u))  groups: $(id -nG)"
-echo "pwd : $(pwd)"
-echo
+      # mountpoints in mountinfo use escape sequences for spaces; safe for our use
+      if (index(P, mp)==1) {
+        l=length(mp)
+        if (l>bestlen) { best=mp; bestlen=l; BO=opts; FT=fstype; }
+      }
+    }
+    END{
+      if (bestlen>=0) { print best " " FT " " BO; }
+    }' /proc/self/mountinfo
+}
 
-echo "== paths =="
-echo "wrapper: $WRAP"
-echo "gasket : $GASKET"
-echo
+# --- locate repo bits ---------------------------------------------------------
+script_afp="$(realpath "${BASH_SOURCE[0]}")"
+RELEASE_DIR="$(cd "$(dirname "$script_afp")/.." && pwd -P)"
+WRAP="$RELEASE_DIR/shell/Man_In_Grey"
 
-echo "== wrapper sanity =="
-if [[ -x "$WRAP" ]]; then
-  head -n 1 "$WRAP" | sed 's/^/shebang: /'
-  echo "exec path: $(command -v "$WRAP" || echo '(not in PATH)')"
+# Compute arch dir like the rest of the tools
+arch_raw="$(uname -m | tr '[:upper:]' '[:lower:]')"
+case "$arch_raw" in
+  amd64|x64) arch="x86_64" ;;
+  x86_64)    arch="x86_64" ;;
+  i386|i486|i586|i686) arch="i686" ;;
+  arm64|aarch64) arch="aarch64" ;;
+  armv7l)    arch="armv7l" ;;
+  armv6l)    arch="armv6l" ;;
+  riscv64)   arch="riscv64" ;;
+  ppc64le|powerpc64le) arch="ppc64le" ;;
+  s390x)     arch="s390x" ;;
+  *)         arch="$arch_raw" ;;
+esac
+GASKET="$RELEASE_DIR/$arch/man_in_grey_apply"
+
+# --- who/where ----------------------------------------------------------------
+u="$(id -un)"; uid="$(id -u)"
+groups="$(id -nG 2>/dev/null || true)"
+pwd="$(pwd -P)"
+say "== who/where =="
+say "user: $u (uid=$uid)  groups: ${groups:-unknown}"
+say "pwd : $pwd"
+say
+say "== paths =="
+say "wrapper: $WRAP"
+say "gasket : $GASKET"
+say
+
+# --- wrapper sanity ------------------------------------------------------------
+say "== wrapper sanity =="
+if [[ -f "$WRAP" && -x "$WRAP" ]]; then
+  read -r firstline <"$WRAP" || true
+  say "shebang: ${firstline:-<empty>}"
+  say "exec path: $WRAP"
 else
-  echo "!! wrapper missing/non-exec"
+  say "!! wrapper missing/non-exec"
 fi
-echo
+say
 
-echo "== gasket file sanity =="
+# --- gasket file sanity & mount flags -----------------------------------------
+say "== gasket file sanity =="
 if [[ -e "$GASKET" ]]; then
   ls -l "$GASKET"
-  if [[ -u "$GASKET" ]]; then echo "setuid: YES"; else echo "setuid: NO"; fi
-  mp="$(dirname "$(readlink -f "$GASKET")")"
-  echo "mount: $(findmnt -no TARGET,FSTYPE,OPTIONS "$mp" 2>/dev/null || echo 'findmnt not available')"
+  if [[ -u "$GASKET" ]]; then
+    say "setuid: YES"
+  else
+    say "setuid: NO"
+  fi
+
+  # try to find findmnt robustly
+  FINDMNT_BIN=""
+  for c in "${FINDMNT_BIN:-}" "$(command -v findmnt 2>/dev/null || true)" /usr/bin/findmnt /bin/findmnt /usr/sbin/findmnt; do
+    [[ -n "$c" && -x "$c" ]] && { FINDMNT_BIN="$c"; break; }
+  done
+
+  gasket_abs="$(realpath "$GASKET")"
+  if [[ -n "$FINDMNT_BIN" ]]; then
+    # prefer findmnt -T if supported
+    if "$FINDMNT_BIN" -T "$gasket_abs" >/dev/null 2>&1; then
+      out="$("$FINDMNT_BIN" -no TARGET,FSTYPE,OPTIONS -T "$gasket_abs" || true)"
+    else
+      # fallback: search by path in the tree
+      out="$("$FINDMNT_BIN" -no TARGET,FSTYPE,OPTIONS "$gasket_abs" 2>/dev/null || true)"
+    fi
+    if [[ -n "${out:-}" ]]; then
+      say "mount: $out"
+      mp="$(awk '{print $1}' <<<"$out")"
+      fstype="$(awk '{print $2}' <<<"$out")"
+      opts="$(awk '{$1="";$2=""; sub(/^  */,""); print}' <<<"$out")"
+    else
+      say "mount: findmnt returned no match, falling back to /proc/self/mountinfo"
+    fi
+  else
+    say "mount: findmnt not found, falling back to /proc/self/mountinfo"
+  fi
+
+  if [[ -z "${opts:-}" ]]; then
+    parsed="$(longest_mount_covering "$gasket_abs" || true)"
+    if [[ -n "$parsed" ]]; then
+      mp="$(awk '{print $1}' <<<"$parsed")"
+      fstype="$(awk '{print $2}' <<<"$parsed")"
+      opts="$(awk '{$1="";$2=""; sub(/^  */,""); print}' <<<"$parsed")"
+      say "mount: $mp $fstype $opts  (from /proc/self/mountinfo)"
+    else
+      say "mount: could not determine mount options (even via mountinfo)"
+    fi
+  fi
+
+  if [[ -n "${opts:-}" ]]; then
+    if grep -qw nosuid <<<"$opts"; then
+      say "‼ detected nosuid on mount → setuid will NOT take effect here"
+    else
+      say "✓ nosuid NOT present on mount"
+    fi
+    if ! grep -qw exec <<<"$opts"; then
+      say "‼ mount appears noexec (or exec not set) → binary execution may fail"
+    fi
+  fi
 else
-  echo "!! gasket not found"
+  say "!! gasket not found"
 fi
-echo
+say
 
-echo "== gasket self-report (--print-flags) =="
+# --- gasket self-report --------------------------------------------------------
+say "== gasket self-report (--print-flags) =="
 if [[ -x "$GASKET" ]]; then
   "$GASKET" --print-flags || true
 else
-  echo "skip (no gasket)"
+  say "skip (no gasket)"
 fi
-echo
+say
 
-echo "== python inner EUID test =="
-PYTEST="/tmp/mig_ids_$$.py"
-cat >"$PYTEST"<<'PY'
+# --- python inner EUID test ----------------------------------------------------
+say "== python inner EUID test =="
+if [[ -x "$GASKET" ]]; then
+  tmp_py="$(mktemp /tmp/mig_ids_XXXXXX.py)"
+  cat >"$tmp_py" <<'PY'
 import os, pwd
-u = os.getuid(); e = os.geteuid()
-name = pwd.getpwuid(u).pw_name if u>=0 else "?"
-print(f"py.real_uid={u} ({name})")
-print(f"py.effective_uid={e}")
-print("py.groups=", os.getgroups())
+uid = os.getuid()
+euid = os.geteuid()
+name = pwd.getpwuid(uid).pw_name
+print(f"py.real_uid={uid} ({name})")
+print(f"py.effective_uid={euid}")
+print("py.groups=", sorted(os.getgroups()))
 PY
-chmod 0644 "$PYTEST"
-
-if [[ -x "$GASKET" ]]; then
-  echo "running: $GASKET --inner $PYTEST"
-  "$GASKET" --inner "$PYTEST" || true
+  say "running: $GASKET --inner $tmp_py"
+  "$GASKET" --inner "$tmp_py" || true
+  rm -f "$tmp_py"
 else
-  echo "skip (no gasket)"
+  say "skip (no gasket)"
 fi
-rm -f "$PYTEST"
-echo
+say
 
-echo "== wrapper execution trace (dry) =="
+# --- wrapper execution trace (dry) --------------------------------------------
+say "== wrapper execution trace (dry) =="
 if [[ -x "$WRAP" ]]; then
-  echo "+ bash -x \"$WRAP\" --phase-2-sanity2-then-stop --stage dummy  (trace only)"
   set +e
-  bash -x "$WRAP" --phase-2-sanity2-then-stop --stage dummy 1>/dev/null 2>&1 | sed 's/^/TRACE: /' || true
+  bash -x "$WRAP" --phase-2-sanity2-then-stop --stage dummy >/dev/null 2>&1 \
+    && say "+ bash -x \"$WRAP\" --phase-2-sanity2-then-stop --stage dummy  (trace only)" \
+    || say "+ bash -x \"$WRAP\" --phase-2-sanity2-then-stop --stage dummy  (trace only)"
   set -e
 else
-  echo "skip (no wrapper)"
+  say "skip (no wrapper)"
 fi
+say
 
-echo
-echo "== verdict (rules of thumb) =="
-cat <<'EOT'
-- If --print-flags shows flag.this_process_privileged=0: setuid isn’t taking effect (check nosuid mount, bit not set, or wrong binary).
-- If that flag is 1 and the Python test shows euid=0: privileges flow is OK; if you still get EPERM to /etc, the wrapper may bypass the gasket sometimes.
-- If the wrapper trace never execs the gasket: replace the wrapper so it always runs the gasket.
-- If root is refused for “not in sudo”, update gasket policy to allow UID 0 (you already have that patch).
-EOT
+# --- verdict -------------------------------------------------------------------
+say "== verdict (rules of thumb) =="
+say "- If --print-flags shows flag.this_process_privileged=0: setuid isn’t taking effect (check nosuid mount, bit not set, or wrong binary)."
+say "- If that flag is 1 and the Python test shows euid=0: privileges flow is OK; if you still get EPERM to /etc, the wrapper may bypass the gasket sometimes."
+say "- If the wrapper trace never execs the gasket: replace the wrapper so it always runs the gasket."
+say "- If root is refused for “not in sudo”, update gasket policy to allow UID 0 (you have that patch)."