mirror of
https://github.com/kuhyx/testsAndMisc.git
synced 2026-11-11 00:47:01 +01:00
A stub that prints nothing makes `size=$(du -sk x)` empty, so every `((size > 0))` is false and the trace walks past every guarded branch. Two such traces compare identical while exercising none of the changed code -- a green check that proves nothing, which is the exact failure mode this harness exists to prevent. --stub now takes `name=output`. Measured on a fixture whose `sudo rm -rf` is guarded by a size test: the silent stub gives `size=[]` / "branch skipped" and never records the rm; `--stub 'du=4096\t/some/dir'` gives `size=[4096]` / "BRANCH TAKEN" and captures it. This weakens the earlier disk_cleanup_check.sh trace, which used a silent du stub: that trace only proved the empty-size path identical. The real run from /tmp is what covered the populated path. Noted in the evidence. Docs also record two limits found while surveying: file-placement installers are not yet traceable (stub cp/chmod/ln and the trace is empty; don't and it rewrites the live system), and baselines for scripts reading relative paths must run from the script's own directory. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011w2Pj4Ek3ZNMQVYcFHgXDY
198 lines
5.7 KiB
Bash
Executable File
198 lines
5.7 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.
|
|
#
|
|
# 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>
|
|
# ============================================================================
|
|
|
|
set -euo pipefail
|
|
|
|
SCRIPT_NAME="$(basename "$0")"
|
|
readonly SCRIPT_NAME
|
|
|
|
# Commands that change the system, the phone, or the package set. Each becomes
|
|
# a stub that records its invocation and exits 0. Extend deliberately: a
|
|
# missing name here means the real binary runs.
|
|
#
|
|
# Deliberately NOT here: `git`, `curl`, `wget`, `makepkg`. Those are stubbed
|
|
# per-run via --stub, because plenty of scripts read git state harmlessly and a
|
|
# blanket git stub would change what they see rather than protect anything.
|
|
# Grep the target for network and build verbs before tracing it.
|
|
readonly DEFAULT_STUBBED_COMMANDS=(
|
|
sudo pacman yay paru systemctl systemd-run
|
|
adb fastboot
|
|
nft iptables ip6tables firewall-cmd
|
|
mount umount swapoff modprobe
|
|
useradd usermod visudo chpasswd
|
|
mkinitcpio grub-mkconfig bootctl
|
|
npm pip pip3 flutter gradle
|
|
reboot shutdown poweroff halt
|
|
)
|
|
|
|
TARGET=""
|
|
OUT=""
|
|
TEMP_DIR=""
|
|
declare -a SCRIPT_ARGS=()
|
|
declare -a EXTRA_STUBS=()
|
|
|
|
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 " -- 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
|
|
}
|
|
|
|
# One stub per mutating command: append the call to the trace, succeed.
|
|
# Stubs report success because the point is to reach later code paths, not to
|
|
# simulate failure. A script whose logic branches on a real exit status needs a
|
|
# hand-written stub instead -- note that in the split's evidence file.
|
|
#
|
|
# A stub may carry an output value as `name=text` (from --stub). This matters
|
|
# more than it looks: a stub that prints nothing makes every `size=$(du ...)`
|
|
# empty, so every `((size > 0))` is false and the trace walks straight past all
|
|
# the branches you wanted to compare. Two such traces are identical and
|
|
# meaningless. Give value-producing commands a plausible non-zero output.
|
|
write_stubs() {
|
|
local bin_dir="$1" entry name value
|
|
mkdir -p "$bin_dir"
|
|
for entry in "${DEFAULT_STUBBED_COMMANDS[@]}" ${EXTRA_STUBS[@]+"${EXTRA_STUBS[@]}"}; do
|
|
name="${entry%%=*}"
|
|
value=""
|
|
if [[ $entry == *=* ]]; then
|
|
value="${entry#*=}"
|
|
fi
|
|
# Quoted heredoc: everything is literal, so $* and $TRACE_FILE reach
|
|
# the generated stub instead of expanding here. Only $name and $value
|
|
# are interpolated, via the unquoted echo lines around it.
|
|
{
|
|
echo "#!/usr/bin/env bash"
|
|
echo "STUB_NAME=$(printf '%q' "$name")"
|
|
echo "STUB_VALUE=$(printf '%q' "$value")"
|
|
cat <<'STUB'
|
|
printf '%s %s\n' "$STUB_NAME" "$*" >>"$TRACE_FILE"
|
|
if [[ -n $STUB_VALUE ]]; then
|
|
printf '%s\n' "$STUB_VALUE"
|
|
fi
|
|
exit 0
|
|
STUB
|
|
} >"$bin_dir/$name"
|
|
chmod +x "$bin_dir/$name"
|
|
done
|
|
}
|
|
|
|
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"
|
|
|
|
# xtrace to a dedicated fd so the script's own stdout stays separate.
|
|
local xtrace="$TEMP_DIR/xtrace.txt"
|
|
|
|
set +e
|
|
(
|
|
export TRACE_FILE="$trace"
|
|
export PATH="$bin_dir:$PATH"
|
|
# A stubbed run must never believe it is root.
|
|
export EUID_OVERRIDE=1000
|
|
exec 9>"$xtrace"
|
|
export BASH_XTRACEFD=9
|
|
set -x
|
|
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"
|
|
echo "=== stdout"
|
|
cat "$TEMP_DIR/stdout.txt"
|
|
echo "=== stderr"
|
|
cat "$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
|
|
;;
|
|
-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 "$@"
|