mirror of
https://github.com/kuhyx/testsAndMisc.git
synced 2026-11-11 15:34:06 +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
167 lines
7.4 KiB
Bash
Executable File
167 lines
7.4 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
|
|
# ============================================================================
|
|
# Prefix redirection for trace_shell_split.sh: run a script whose job is
|
|
# PLACING FILES without letting it touch the live system, and make every write
|
|
# it performs visible in the trace.
|
|
#
|
|
# Why a separate mechanism from the PATH stubs: `cat > /etc/modprobe.d/x.conf`
|
|
# is a bash builtin plus a redirection. No PATH entry can intercept it. The
|
|
# stubs handle mutating *binaries*; this file handles mutating *redirections*.
|
|
#
|
|
# Two layers, applied together by build_prefix_argv:
|
|
#
|
|
# 1. Environment redirect. HOME and every XDG_* base are pointed into the
|
|
# prefix. This covers the majority of real destinations, which are
|
|
# $HOME-rooted variables (`unit_dir="$HOME/.config/systemd/user"`), with
|
|
# NO change to the traced script. XDG_DATA_HOME is exported explicitly
|
|
# rather than left to default off HOME: install_leechblock.sh reads it
|
|
# directly and then runs `rsync -a --delete` at it, so inheriting a real
|
|
# XDG_DATA_HOME from the caller's environment would delete live data.
|
|
#
|
|
# 2. bwrap bind mounts for the hardcoded-absolute residue (/etc/modprobe.d,
|
|
# /usr/local/bin, /etc/systemd/system). Only the LEAF directories named on
|
|
# the command line are bound -- binding /etc wholesale would shadow
|
|
# /etc/passwd and /etc/os-release and produce fake failures that look like
|
|
# a broken split. `--unshare-user --uid 0` additionally makes the run
|
|
# believe it is root, which matters more than it sounds: the shared
|
|
# require_root() does `exec sudo "$0"`, and under the sudo stub that
|
|
# truncates the whole run to a single recorded call -- a near-empty trace
|
|
# that diffs clean against any split at all.
|
|
#
|
|
# Anything absolute that was NOT bound fails EPERM / read-only rather than
|
|
# silently succeeding, so Decision 6 (never mutate the real system to verify a
|
|
# split) holds by construction rather than by discipline.
|
|
# ============================================================================
|
|
|
|
# XDG bases redirected into the prefix. Each is exported explicitly so a value
|
|
# already present in the caller's environment cannot leak a live path through.
|
|
readonly TRACE_XDG_VARS=(
|
|
XDG_DATA_HOME:.local/share
|
|
XDG_CONFIG_HOME:.config
|
|
XDG_CACHE_HOME:.cache
|
|
XDG_STATE_HOME:.local/state
|
|
)
|
|
|
|
# Populate the prefix with the directory skeleton and echo the env assignments
|
|
# needed to redirect a run into it. Caller exports them.
|
|
trace_prefix_env() {
|
|
local prefix="$1" entry var rel
|
|
mkdir -p "$prefix"
|
|
printf 'HOME=%s\n' "$prefix"
|
|
for entry in "${TRACE_XDG_VARS[@]}"; do
|
|
var="${entry%%:*}"
|
|
rel="${entry#*:}"
|
|
mkdir -p "$prefix/$rel"
|
|
printf '%s=%s\n' "$var" "$prefix/$rel"
|
|
done
|
|
}
|
|
|
|
# Absolute destinations a script writes to, discovered statically. Used to tell
|
|
# the user which --bind-abs arguments a target needs, and to refuse clearly
|
|
# when bwrap cannot provide them.
|
|
#
|
|
# Both spellings must be caught, and the second is the one that bites:
|
|
# nvidia_troubleshoot.sh writes `cat >"$CONFIG_FILE"`, where CONFIG_FILE is
|
|
# built from MODPROBE_DIR="/etc/modprobe.d" many lines earlier. A scan that
|
|
# only looked for a literal path next to the `>` found nothing, the refusal
|
|
# never fired, and the run silently produced an empty manifest -- a false pass
|
|
# of exactly the kind this harness exists to prevent. So we also collect any
|
|
# variable ASSIGNED an absolute path under a system root, which over-reports
|
|
# (a path may only ever be read) but never under-reports. Over-reporting costs
|
|
# a redundant --bind-abs; under-reporting costs a live-system write.
|
|
trace_prefix_scan_absolute() {
|
|
local target="$1"
|
|
# `|| true` on each grep: a target with only one spelling of destination
|
|
# makes the other grep exit 1, and under the caller's `set -e` that aborted
|
|
# validate_requirements silently -- the refusal never fired and the run
|
|
# proceeded with an empty manifest. Exactly the false pass being guarded
|
|
# against, so the status is neutralised rather than relied upon.
|
|
{
|
|
# Literal destination directly after a redirection.
|
|
{ grep -oE '>[[:space:]]*"?(/etc|/usr/local|/usr/share|/var/lib|/opt)[^"[:space:]]*' "$target" || true; } |
|
|
sed -E 's/^>[[:space:]]*"?//' |
|
|
xargs -r -n1 dirname
|
|
# Absolute system paths assigned to a variable, which redirections and
|
|
# `tee`/`mkdir` then reach indirectly.
|
|
{ grep -oE '^[[:space:]]*(local[[:space:]]+)?[A-Za-z_][A-Za-z0-9_]*="?(/etc|/usr/local|/usr/share|/var/lib|/opt)[^"[:space:]]*' "$target" || true; } |
|
|
sed -E 's/^[[:space:]]*(local[[:space:]]+)?[A-Za-z_][A-Za-z0-9_]*="?//' |
|
|
while IFS= read -r path; do
|
|
# Emit the path itself, never its parent. Taking dirname of a
|
|
# config *directory* is what turned MODPROBE_DIR="/etc/modprobe.d"
|
|
# into a bare `/etc`, and binding /etc wholesale shadows
|
|
# /etc/passwd and /etc/os-release -- the traced script then fails
|
|
# on an unrelated read and it looks like a broken split. A
|
|
# variable holding a file path yields a directory that does not
|
|
# exist inside the prefix, which bwrap creates on demand; that
|
|
# is harmless, whereas an over-broad bind is not.
|
|
printf '%s\n' "$path"
|
|
done
|
|
} | sort -u
|
|
}
|
|
|
|
# Build the argv that runs "$@" with the absolute destinations bound into the
|
|
# prefix. Echoes the command; empty output means "no bwrap needed".
|
|
#
|
|
# Each bound path is created inside the prefix first: bwrap requires the source
|
|
# of a --bind to exist.
|
|
trace_prefix_bwrap_argv() {
|
|
local prefix="$1"
|
|
shift
|
|
local -a abs_dirs=()
|
|
while [[ $# -gt 0 && $1 != "--" ]]; do
|
|
abs_dirs+=("$1")
|
|
shift
|
|
done
|
|
shift # drop the --
|
|
|
|
if [[ ${#abs_dirs[@]} -eq 0 ]]; then
|
|
return 0
|
|
fi
|
|
|
|
if ! command -v bwrap >/dev/null 2>&1; then
|
|
echo "Error: --bind-abs needs bubblewrap (pacman -S bubblewrap)" >&2
|
|
return 1
|
|
fi
|
|
|
|
local -a argv=(bwrap --dev-bind / / --unshare-user --uid 0 --gid 0)
|
|
local dir
|
|
for dir in "${abs_dirs[@]}"; do
|
|
mkdir -p "$prefix/abs$dir"
|
|
argv+=(--bind "$prefix/abs$dir" "$dir")
|
|
done
|
|
printf '%s\n' "${argv[@]}"
|
|
}
|
|
|
|
# The write manifest: what the run actually placed, with content hashes.
|
|
#
|
|
# This is the half of the change that satisfies "a silent stub is a trap".
|
|
# Redirecting a write without reporting it would recreate exactly the failure
|
|
# the doc warns about -- a dropped `cat > $HOME/.local/bin/foo.sh` would show
|
|
# up as nothing at all, and two traces would match while one lost a file.
|
|
#
|
|
# Content is normalized before hashing: the prefix path is replaced with a
|
|
# literal @PREFIX@. install_usage_monitoring.sh writes an UNQUOTED heredoc, so
|
|
# $HOME interpolates into the generated file's contents at write time; without
|
|
# normalization a mktemp-d prefix bakes a fresh random path into every artifact
|
|
# and no two traces ever match.
|
|
#
|
|
# What normalization does NOT remove is a timestamp the target itself embeds:
|
|
# nvidia_troubleshoot.sh writes `# Created by ... on $(date)`, so its hash
|
|
# changes every run and only its size is stable. That is a property of the
|
|
# traced script, not of the harness -- when diffing such a target, expect the
|
|
# sha to differ and compare the file LIST and sizes instead. Do not "fix" it by
|
|
# freezing the clock; that would hide a real content change too.
|
|
trace_prefix_manifest() {
|
|
local prefix="$1" rel abs hash size
|
|
if [[ ! -d $prefix ]]; then
|
|
return 0
|
|
fi
|
|
while IFS= read -r rel; do
|
|
abs="$prefix/$rel"
|
|
size="$(stat -c '%s' "$abs")"
|
|
hash="$(sed "s#${prefix}#@PREFIX@#g" "$abs" | sha256sum | cut -c1-16)"
|
|
printf '%s size=%s sha=%s\n' "$rel" "$size" "$hash"
|
|
done < <(find "$prefix" -type f -printf '%P\n' | sort)
|
|
}
|