mirror of
https://github.com/kuhyx/testsAndMisc.git
synced 2026-11-10 22:47:02 +01:00
Shell redirections are builtins, so no PATH stub can intercept `cat > /etc/modprobe.d/x.conf`. That left installer scripts unverifiable: stub cp/chmod/ln and the trace is nearly empty, don't and the run rewrites the live system. --prefix redirects HOME and every XDG_* base into a temp tree. XDG_DATA_HOME is exported explicitly, not left to default off HOME: install_leechblock.sh reads it directly and then runs `rsync -a --delete` at it. --bind-abs binds hardcoded absolutes via bwrap --unshare-user --uid 0, leaf directories only. uid 0 is load-bearing beyond permissions: common.sh's require_root does `exec sudo "$0"`, and under the sudo stub that truncated the whole run to one recorded call -- a trace that diffs clean against any split. Binding /etc wholesale was measured to shadow /etc/passwd, collapse $SUDO_USER to /home//, and mask a nested bind, so the scanner never emits it. The trace gains a manifest of every file written, with content hashes. That section is the point: with it, a dropped write in the fixture pair is a one-line diff; with it stripped, the two traces are identical, i.e. the old harness passed that broken split. Hashes normalize the prefix to @PREFIX@ because install_usage_monitoring.sh writes an unquoted heredoc that bakes $HOME into the artifact. Verified: ~/.config/systemd/user, ~/.local/bin, ~/.local/share and /etc/modprobe.d byte-identical before and after; usage-report units still 2. The three prior regressions are now committed fixtures rather than ad-hoc -- one was silently neutered mid-change when a trailing echo added for SC2034 became the function's last statement and turned exit 1 into exit 0. Stub generation moved to lib/ because --prefix pushed the harness to 252 lines, over the cap it exists to serve. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011w2Pj4Ek3ZNMQVYcFHgXDY
198 lines
5.8 KiB
Bash
Executable File
198 lines
5.8 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
|
|
# ============================================================================
|
|
# Capture an execution trace of a shell script with every mutating command
|
|
# stubbed, so a split can be verified by RUNNING it rather than by sourcing it.
|
|
#
|
|
# Why this exists: verify_shell_split.sh sources the libs and never calls them,
|
|
# which proves `source` lines resolve and nothing more. The analyze_repo.sh
|
|
# split passed that, plus bash -n, shellcheck and a function-set diff, and
|
|
# still shipped a script that aborted after language detection -- a `set -e`
|
|
# function-tail return and a self-referencing nameref, neither reachable
|
|
# without execution. See docs/shell-split-verification.md.
|
|
#
|
|
# The trace is the artifact: run this at the pre-split commit and again after
|
|
# the split, then diff. An identical trace means the same commands ran in the
|
|
# same order with the same arguments.
|
|
#
|
|
# Mutating binaries are shadowed by stubs earlier on PATH, so the live system
|
|
# is never touched. That is what makes this safe for the enforcement and
|
|
# installer scripts Decision 6 forbids executing for real.
|
|
#
|
|
# Scripts whose job is PLACING FILES need --prefix: shell redirections cannot
|
|
# be stubbed via PATH, so without it the choice is an empty trace or a rewritten
|
|
# live system. See lib/trace_prefix.sh for the two layers that solves.
|
|
#
|
|
# Usage:
|
|
# trace_shell_split.sh <script> [-- <script args>]
|
|
# trace_shell_split.sh <script> --out <file> [-- <script args>]
|
|
# trace_shell_split.sh <script> --stub git,curl,makepkg --out <file>
|
|
# trace_shell_split.sh <script> --prefix <dir> [--bind-abs /etc/modprobe.d]
|
|
# ============================================================================
|
|
|
|
set -euo pipefail
|
|
|
|
SCRIPT_NAME="$(basename "$0")"
|
|
readonly SCRIPT_NAME
|
|
|
|
# readlink -f, not dirname alone: this repo's root-level entry points are
|
|
# symlinks into meta/, and an unresolved SCRIPT_DIR turns a source line into an
|
|
# instant exit under set -e. See docs/shell-split-verification.md.
|
|
SCRIPT_DIR="$(dirname "$(readlink -f "${BASH_SOURCE[0]}")")"
|
|
readonly SCRIPT_DIR
|
|
# shellcheck source=lib/trace_prefix.sh
|
|
source "$SCRIPT_DIR/lib/trace_prefix.sh"
|
|
# shellcheck source=lib/trace_stubs.sh
|
|
source "$SCRIPT_DIR/lib/trace_stubs.sh"
|
|
|
|
TARGET=""
|
|
OUT=""
|
|
TEMP_DIR=""
|
|
PREFIX=""
|
|
declare -a SCRIPT_ARGS=()
|
|
declare -a EXTRA_STUBS=()
|
|
declare -a BIND_ABS=()
|
|
|
|
cleanup() {
|
|
if [[ -n "${TEMP_DIR:-}" && -d "$TEMP_DIR" ]]; then
|
|
rm -rf "$TEMP_DIR"
|
|
fi
|
|
}
|
|
|
|
trap cleanup EXIT
|
|
|
|
usage() {
|
|
echo "Usage: $SCRIPT_NAME <script> [--out <file>] [--stub a,b] [-- <script args>]"
|
|
echo " <script> script to trace (not modified)"
|
|
echo " --out <file> write the trace here (default: stdout)"
|
|
echo " --stub a,b also shadow these binaries (e.g. git,curl,makepkg)"
|
|
echo " --prefix <d> redirect HOME/XDG writes into <d> and list them"
|
|
echo " --bind-abs <p> bind absolute path <p> into the prefix (repeatable)"
|
|
echo " -- everything after this is passed to the script"
|
|
exit 0
|
|
}
|
|
|
|
validate_requirements() {
|
|
if [[ -z "$TARGET" ]]; then
|
|
echo "Error: no script given" >&2
|
|
exit 1
|
|
fi
|
|
if [[ ! -f "$TARGET" ]]; then
|
|
echo "Error: no such script: $TARGET" >&2
|
|
exit 1
|
|
fi
|
|
# Refuse rather than quietly writing to the real path: a script with
|
|
# hardcoded absolute destinations and no --bind-abs would either mutate the
|
|
# live system or take a different branch on EPERM, and both look like a
|
|
# clean trace.
|
|
if [[ -n "$PREFIX" && ${#BIND_ABS[@]} -eq 0 ]]; then
|
|
local -a found=()
|
|
mapfile -t found < <(trace_prefix_scan_absolute "$TARGET")
|
|
if [[ ${#found[@]} -gt 0 ]]; then
|
|
echo "Error: $TARGET writes to absolute paths; pass --bind-abs for each:" >&2
|
|
printf ' --bind-abs %s\n' "${found[@]}" >&2
|
|
exit 1
|
|
fi
|
|
fi
|
|
}
|
|
|
|
main() {
|
|
validate_requirements
|
|
|
|
TEMP_DIR="$(mktemp -d)"
|
|
if [[ ! -d "$TEMP_DIR" ]]; then
|
|
echo "Error: failed to create temporary directory" >&2
|
|
exit 1
|
|
fi
|
|
|
|
local bin_dir="$TEMP_DIR/bin"
|
|
local trace="$TEMP_DIR/trace.txt"
|
|
: >"$trace"
|
|
write_stubs "$bin_dir" ${EXTRA_STUBS[@]+"${EXTRA_STUBS[@]}"}
|
|
|
|
# xtrace to a dedicated fd so the script's own stdout stays separate.
|
|
local xtrace="$TEMP_DIR/xtrace.txt"
|
|
|
|
# Under --prefix the run happens inside bwrap when absolute destinations
|
|
# were bound; otherwise it is a plain subshell with a redirected HOME.
|
|
local -a runner=()
|
|
if [[ -n "$PREFIX" && ${#BIND_ABS[@]} -gt 0 ]]; then
|
|
mapfile -t runner < <(trace_prefix_bwrap_argv "$PREFIX" "${BIND_ABS[@]}" --)
|
|
fi
|
|
|
|
set +e
|
|
(
|
|
export TRACE_FILE="$trace"
|
|
export PATH="$bin_dir:$PATH"
|
|
if [[ -n "$PREFIX" ]]; then
|
|
local assignment
|
|
while IFS= read -r assignment; do
|
|
export "${assignment?}"
|
|
done < <(trace_prefix_env "$PREFIX")
|
|
fi
|
|
exec 9>"$xtrace"
|
|
export BASH_XTRACEFD=9
|
|
set -x
|
|
"${runner[@]+"${runner[@]}"}" bash "$TARGET" "${SCRIPT_ARGS[@]+"${SCRIPT_ARGS[@]}"}"
|
|
) >"$TEMP_DIR/stdout.txt" 2>"$TEMP_DIR/stderr.txt"
|
|
local status=$?
|
|
set -e
|
|
|
|
{
|
|
echo "=== exit status: $status"
|
|
echo "=== mutating calls (stubbed)"
|
|
cat "$trace"
|
|
# Emitted only under --prefix, so traces taken without the flag stay
|
|
# byte-identical to the ones already captured.
|
|
if [[ -n "$PREFIX" ]]; then
|
|
echo "=== files written (prefix)"
|
|
trace_prefix_manifest "$PREFIX"
|
|
fi
|
|
echo "=== stdout"
|
|
sed "s#${PREFIX:-__no_prefix__}#@PREFIX@#g" "$TEMP_DIR/stdout.txt"
|
|
echo "=== stderr"
|
|
sed "s#${PREFIX:-__no_prefix__}#@PREFIX@#g" "$TEMP_DIR/stderr.txt"
|
|
} >"${OUT:-/dev/stdout}"
|
|
}
|
|
|
|
while [[ $# -gt 0 ]]; do
|
|
case $1 in
|
|
--out)
|
|
OUT="$2"
|
|
shift 2
|
|
;;
|
|
--stub)
|
|
# Comma-separated extra binaries to shadow for this run only.
|
|
IFS=',' read -r -a EXTRA_STUBS <<<"$2"
|
|
shift 2
|
|
;;
|
|
--prefix)
|
|
PREFIX="$2"
|
|
shift 2
|
|
;;
|
|
--bind-abs)
|
|
BIND_ABS+=("$2")
|
|
shift 2
|
|
;;
|
|
-h | --help)
|
|
usage
|
|
;;
|
|
--)
|
|
shift
|
|
SCRIPT_ARGS=("$@")
|
|
break
|
|
;;
|
|
*)
|
|
if [[ -z "$TARGET" ]]; then
|
|
TARGET="$1"
|
|
shift
|
|
else
|
|
echo "Unknown option: $1" >&2
|
|
exit 1
|
|
fi
|
|
;;
|
|
esac
|
|
done
|
|
|
|
main "$@"
|