mirror of
https://github.com/kuhyx/testsAndMisc.git
synced 2026-11-10 22:27:01 +01:00
All three were found by review and reproduced before fixing. Each let a run
report success while proving nothing -- the shape this harness exists to
prevent.
A reused prefix defeated dropped-write detection entirely. Run 1's files were
still there when run 2's manifest was taken, so the manifest reported writes
that never happened and the fixture pair diffed CLEAN. The doc's own examples
demonstrated that workflow. A non-empty prefix is now refused -- not deleted,
since the path is user-supplied -- and the examples use distinct prefixes.
A missing bwrap was swallowed. trace_prefix_bwrap_argv returned 1, but the
caller reads it through `mapfile < <(...)` and a process substitution's status
is discarded, so runner ended up empty and the run continued with no sandbox
and no uid 0. Same swallow as the grep-status one fixed earlier, one layer up.
bwrap is now probed in validate_requirements, before anything runs.
The absolute-path scan only fired when no --bind-abs was given at all, so
binding one path and missing three produced no warning. It now runs whenever
--prefix is given and reports only what is not already covered. Fixing that
surfaced a second defect: --bind-abs on a FILE aborted with "Can't mkdir
/etc/profile: Not a directory", and binding its parent reintroduced the
wholesale /etc shadowing this change had already measured as harmful ($SUDO_USER
collapsing to /home//, the real /etc/profile unreadable). Files are now bound
file-over-file, seeded with a copy so a script that greps before appending sees
real content.
Also measured rather than argued: the XDG redirect. A new fixture run with a
real XDG_DATA_HOME pointing at a decoy shows all four writes landing in the
prefix and the decoy empty -- the rsync --delete data-loss path, proven closed
without running install_leechblock.sh. And no-prefix output was diffed against
cdf2a55 from a detached worktree: byte-identical, so existing baselines hold.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011w2Pj4Ek3ZNMQVYcFHgXDY
193 lines
8.7 KiB
Bash
Executable File
193 lines
8.7 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
|
|
|
|
# Kill the whole process group rather than `return 1`. The caller reads this
|
|
# function through `mapfile < <(...)`, and a process substitution's exit
|
|
# status is discarded -- returning 1 left the caller with an empty argv,
|
|
# which is indistinguishable from "no bind needed", so the run proceeded
|
|
# unsandboxed as uid 1000. validate_requirements probes bwrap up front so
|
|
# this path should be unreachable; it is a backstop, and it must not be a
|
|
# silent one.
|
|
if ! command -v bwrap >/dev/null 2>&1; then
|
|
echo "Error: --bind-abs needs bubblewrap (pacman -S bubblewrap)" >&2
|
|
kill -TERM $$
|
|
exit 1
|
|
fi
|
|
|
|
local -a argv=(bwrap --dev-bind / / --unshare-user --uid 0 --gid 0)
|
|
local dir target
|
|
for dir in "${abs_dirs[@]}"; do
|
|
# A bind target that exists as a FILE is bound as a file: bwrap refuses
|
|
# to mount a directory over one ("Can't mkdir /etc/profile: Not a
|
|
# directory") and aborts before the run writes anything. Binding its
|
|
# PARENT instead is worse than the crash -- that is a wholesale /etc
|
|
# bind, which was measured to shadow /etc/passwd (collapsing $SUDO_USER
|
|
# to /home//) and hide the real /etc/profile from the script's own
|
|
# grep. So seed a file in the prefix and bind file-over-file.
|
|
#
|
|
# The seed is a COPY of the original, because a script that reads a
|
|
# config before rewriting it (nvidia_troubleshoot.sh greps /etc/profile,
|
|
# then appends) would otherwise see an empty file and take a branch it
|
|
# would not take live.
|
|
target="$prefix/abs$dir"
|
|
if [[ -f $dir && ! -d $dir ]]; then
|
|
mkdir -p "$(dirname "$target")"
|
|
cp -p "$dir" "$target"
|
|
else
|
|
mkdir -p "$target"
|
|
fi
|
|
argv+=(--bind "$target" "$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)
|
|
}
|