WIP: Paused over-hardening fix wave (boot checkout verification, double-read package manifest)
This commit is contained in:
@@ -15,6 +15,16 @@ note() { findings+=("$1"); }
|
||||
|
||||
[[ -x "$boot" ]] || { printf 'boot contract: %s is not executable\n' "$boot" >&2; exit 1; }
|
||||
|
||||
# Git is the only package boot can install before the verified checkout exists.
|
||||
# Both root-server and ordinary-user paths must exclude ambient third-party
|
||||
# repositories while still allowing Fedora dependencies.
|
||||
for git_install in \
|
||||
'dnf install -y --repo=fedora --repo=updates --from-repo=fedora,updates git' \
|
||||
'sudo dnf install -y --repo=fedora --repo=updates --from-repo=fedora,updates git'; do
|
||||
grep -qF "$git_install" "$boot" \
|
||||
|| note "boot omits reviewed Fedora source binding: $git_install"
|
||||
done
|
||||
|
||||
work="$(mktemp -d)"
|
||||
trap 'rm -rf "$work"' EXIT
|
||||
|
||||
@@ -103,6 +113,16 @@ case "\${1:-}" in
|
||||
[[ "\$#" -eq 4 && "\$4" == 'HEAD^{commit}' ]] || exit 97
|
||||
cat "$state/head-revision"
|
||||
;;
|
||||
ls-tree)
|
||||
[[ "\$#" -eq 6 && "\$4" == -rz && "\$5" == --full-tree \
|
||||
&& "\$6" == "$revision" ]] || exit 97
|
||||
printf '100755 blob aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\tinstall\0'
|
||||
;;
|
||||
hash-object)
|
||||
[[ "\$#" -eq 6 && "\$4" == --no-filters && "\$5" == -- \
|
||||
&& "\$6" == install ]] || exit 97
|
||||
printf '%s\n' aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
;;
|
||||
*) exit 97 ;;
|
||||
esac
|
||||
;;
|
||||
@@ -268,6 +288,132 @@ run_boot "$revision" "$boot_sha"
|
||||
(( run_status != 0 )) || note 'existing HEAD mismatch returned success'
|
||||
assert_no_install_or_rewrite 'existing HEAD mismatch'
|
||||
|
||||
# Git's porcelain status deliberately trusts index hints. The bootstrap cannot:
|
||||
# these two flags can hide changed executable bytes while HEAD still names the
|
||||
# reviewed commit. Exercise real Git so the contract cannot accidentally teach
|
||||
# its adapter to expose state that Git itself hides.
|
||||
real_git="$(command -v git)"
|
||||
hidden_root="$work/hidden-index"
|
||||
mkdir -p "$hidden_root/home"
|
||||
"$real_git" init -q "$hidden_root/source"
|
||||
"$real_git" -C "$hidden_root/source" config user.email contract@panama
|
||||
"$real_git" -C "$hidden_root/source" config user.name contract
|
||||
printf '#!/usr/bin/env bash\nexit 0\n' >"$hidden_root/source/install"
|
||||
chmod +x "$hidden_root/source/install"
|
||||
printf 'trusted target bytes\n' >"$hidden_root/source/target"
|
||||
ln -s target "$hidden_root/source/trusted-link"
|
||||
"$real_git" -C "$hidden_root/source" add install target trusted-link
|
||||
"$real_git" -C "$hidden_root/source" commit -qm trusted
|
||||
hidden_revision="$("$real_git" -C "$hidden_root/source" rev-parse HEAD)"
|
||||
"$real_git" clone -q --bare "$hidden_root/source" "$hidden_root/origin.git"
|
||||
|
||||
# Exercise the exact boundary between checkout preparation and handoff. This
|
||||
# test-only copy inserts a same-UID replacement after prepare returns; the
|
||||
# production handoff must perform its complete comparison after that point.
|
||||
post_prepare_checkout="$hidden_root/post-prepare-swap"
|
||||
post_prepare_marker="$hidden_root/post-prepare-executed"
|
||||
post_prepare_hook_marker="$hidden_root/post-prepare-hook-fired"
|
||||
"$real_git" clone -q "$hidden_root/origin.git" "$post_prepare_checkout"
|
||||
post_prepare_hook="$hidden_root/swap-install"
|
||||
cat >"$post_prepare_hook" <<'HOOK'
|
||||
#!/usr/bin/env bash
|
||||
: >"$PANAMA_BOOT_POST_PREPARE_HOOK_MARKER"
|
||||
printf '#!/usr/bin/env bash\nprintf "executed\\n" >%q\n' \
|
||||
"$PANAMA_BOOT_POST_PREPARE_MARKER" >"$PANAMA_PATH/install"
|
||||
chmod +x "$PANAMA_PATH/install"
|
||||
HOOK
|
||||
chmod +x "$post_prepare_hook"
|
||||
hooked_boot="$hidden_root/boot-post-prepare-hook"
|
||||
awk '
|
||||
{
|
||||
print
|
||||
if ($0 == "prepare_panama_checkout \"$PANAMA_PATH\"") {
|
||||
prepare_count++
|
||||
if (prepare_count == 1) print "\"$PANAMA_BOOT_POST_PREPARE_FIXTURE\""
|
||||
}
|
||||
}
|
||||
' "$boot" >"$hooked_boot"
|
||||
hooked_boot_sha="$(sha256sum "$hooked_boot" | cut -d' ' -f1)"
|
||||
post_prepare_status=0
|
||||
HOME="$hidden_root/home" PANAMA_PATH="$post_prepare_checkout" \
|
||||
PANAMA_BOOT_REVISION="$hidden_revision" PANAMA_BOOT_SHA256="$hooked_boot_sha" \
|
||||
PANAMA_BOOT_POST_PREPARE_FIXTURE="$post_prepare_hook" \
|
||||
PANAMA_BOOT_POST_PREPARE_MARKER="$post_prepare_marker" \
|
||||
PANAMA_BOOT_POST_PREPARE_HOOK_MARKER="$post_prepare_hook_marker" \
|
||||
bash "$hooked_boot" </dev/null >"$hidden_root/post-prepare.out" 2>&1 \
|
||||
|| post_prepare_status=$?
|
||||
[[ -e "$post_prepare_hook_marker" ]] \
|
||||
|| note 'post-prepare replacement hook did not exercise the boundary'
|
||||
(( post_prepare_status != 0 )) \
|
||||
|| note 'post-prepare worktree replacement returned success'
|
||||
[[ ! -e "$post_prepare_marker" ]] \
|
||||
|| note 'post-prepare worktree replacement executed unreviewed install bytes'
|
||||
|
||||
# A valid tracked symlink must compare its link text with Git's 120000 blob;
|
||||
# hashing the pathname would follow it and hash the target file instead.
|
||||
symlink_checkout="$hidden_root/tracked-symlink"
|
||||
"$real_git" clone -q "$hidden_root/origin.git" "$symlink_checkout"
|
||||
symlink_status=0
|
||||
HOME="$hidden_root/home" PANAMA_PATH="$symlink_checkout" \
|
||||
PANAMA_BOOT_REVISION="$hidden_revision" PANAMA_BOOT_SHA256="$boot_sha" \
|
||||
bash "$boot" </dev/null >"$hidden_root/tracked-symlink.out" 2>&1 \
|
||||
|| symlink_status=$?
|
||||
(( symlink_status == 0 )) \
|
||||
|| note 'a checkout with a valid tracked symlink was rejected'
|
||||
|
||||
for hidden_flag in assume-unchanged skip-worktree; do
|
||||
hidden_checkout="$hidden_root/$hidden_flag"
|
||||
hidden_marker="$hidden_root/$hidden_flag-executed"
|
||||
"$real_git" clone -q "$hidden_root/origin.git" "$hidden_checkout"
|
||||
printf '#!/usr/bin/env bash\nprintf "executed\\n" >%q\n' "$hidden_marker" \
|
||||
>"$hidden_checkout/install"
|
||||
chmod +x "$hidden_checkout/install"
|
||||
"$real_git" -C "$hidden_checkout" update-index "--$hidden_flag" install
|
||||
[[ -z "$("$real_git" -C "$hidden_checkout" status --porcelain)" ]] \
|
||||
|| note "$hidden_flag fixture was not hidden from porcelain status"
|
||||
|
||||
hidden_status=0
|
||||
HOME="$hidden_root/home" PANAMA_PATH="$hidden_checkout" \
|
||||
PANAMA_BOOT_REVISION="$hidden_revision" PANAMA_BOOT_SHA256="$boot_sha" \
|
||||
bash "$boot" </dev/null >"$hidden_root/$hidden_flag.out" 2>&1 \
|
||||
|| hidden_status=$?
|
||||
(( hidden_status != 0 )) \
|
||||
|| note "$hidden_flag modified checkout returned success"
|
||||
[[ ! -e "$hidden_marker" ]] \
|
||||
|| note "$hidden_flag modified checkout executed unreviewed install bytes"
|
||||
done
|
||||
|
||||
# The same hidden-index state must not conceal a mode change or a different
|
||||
# symlink target; both are part of the reviewed Git tree, not metadata hints.
|
||||
for hidden_flag in assume-unchanged skip-worktree; do
|
||||
for hidden_change in mode symlink-target; do
|
||||
hidden_checkout="$hidden_root/$hidden_flag-$hidden_change"
|
||||
"$real_git" clone -q "$hidden_root/origin.git" "$hidden_checkout"
|
||||
case "$hidden_change" in
|
||||
mode)
|
||||
chmod -x "$hidden_checkout/install"
|
||||
hidden_path=install
|
||||
;;
|
||||
symlink-target)
|
||||
rm -- "$hidden_checkout/trusted-link"
|
||||
ln -s untrusted-target "$hidden_checkout/trusted-link"
|
||||
hidden_path=trusted-link
|
||||
;;
|
||||
esac
|
||||
"$real_git" -C "$hidden_checkout" update-index "--$hidden_flag" "$hidden_path"
|
||||
[[ -z "$("$real_git" -C "$hidden_checkout" status --porcelain)" ]] \
|
||||
|| note "$hidden_flag $hidden_change fixture was not hidden from porcelain status"
|
||||
|
||||
hidden_status=0
|
||||
HOME="$hidden_root/home" PANAMA_PATH="$hidden_checkout" \
|
||||
PANAMA_BOOT_REVISION="$hidden_revision" PANAMA_BOOT_SHA256="$boot_sha" \
|
||||
bash "$boot" </dev/null >"$hidden_root/$hidden_flag-$hidden_change.out" 2>&1 \
|
||||
|| hidden_status=$?
|
||||
(( hidden_status != 0 )) \
|
||||
|| note "$hidden_flag concealed a tracked $hidden_change change"
|
||||
done
|
||||
done
|
||||
|
||||
if (( ${#findings[@]} > 0 )); then
|
||||
printf 'boot contract: %d finding(s)\n' "${#findings[@]}" >&2
|
||||
printf ' - %s\n' "${findings[@]}" >&2
|
||||
|
||||
@@ -99,9 +99,10 @@ sed -n '/^if \[\[ "\$ROLE" == server \]\]; then/,/^fi/p' "$installer" | grep -q
|
||||
# Comments dropped and backslash continuations joined, so a `soft` invocation
|
||||
# wrapped across three lines reads as the one command it is.
|
||||
uncommented() { grep -vE '^\s*#' "$installer" | sed -e :a -e '/\\$/N; s/\\\n\s*/ /; ta'; }
|
||||
uncommented_installer="$(uncommented)"
|
||||
|
||||
while read -r command; do
|
||||
uncommented | grep -q "soft .*$command" \
|
||||
grep -q "soft .*$command" <<<"$uncommented_installer" \
|
||||
|| note "'$command' runs without soft, so its failure still ends the stage"
|
||||
done <<'FRAGILE'
|
||||
dnf swap -y 'ffmpeg-free'
|
||||
@@ -129,7 +130,7 @@ if "rpm -q hyprland" not in after or "exit 1" not in after:
|
||||
raise SystemExit(1)
|
||||
PY
|
||||
|
||||
uncommented | grep -q 'soft .*HYPR_PACKAGES' \
|
||||
grep -q 'soft .*HYPR_PACKAGES' <<<"$uncommented_installer" \
|
||||
&& note 'the Hyprland install is tolerated, so a machine with no desktop reports success'
|
||||
|
||||
# ── Soft failures are reported ──────────────────────────────────────────────
|
||||
|
||||
@@ -103,10 +103,13 @@ fi
|
||||
|
||||
nvidia="$(run_stage PANAMA_NVIDIA=yes)"
|
||||
|
||||
called "$nvidia" 'dnf install -y akmod-nvidia' \
|
||||
called "$nvidia" 'akmod-nvidia' \
|
||||
|| note 'answering yes to NVIDIA does not install akmod-nvidia'
|
||||
called "$nvidia" 'xorg-x11-drv-nvidia-cuda' \
|
||||
|| note 'the CUDA driver is not installed alongside the kernel module'
|
||||
expected_nvidia='sudo dnf install -y --repo=fedora --repo=updates --repo=rpmfusion-free --repo=rpmfusion-free-updates --repo=rpmfusion-nonfree --repo=rpmfusion-nonfree-updates --from-repo=rpmfusion-nonfree,rpmfusion-nonfree-updates akmod-nvidia xorg-x11-drv-nvidia-cuda'
|
||||
grep -Fxq -- "$expected_nvidia" <<<"$nvidia" \
|
||||
|| note 'the NVIDIA transaction is not limited to reviewed Fedora and RPM Fusion repositories'
|
||||
called "$nvidia" 'grubby --update-kernel=ALL' \
|
||||
|| note 'the kernel arguments are never set'
|
||||
called "$nvidia" 'modprobe.blacklist=nouveau' \
|
||||
|
||||
@@ -123,7 +123,7 @@ grep -q '/etc/profile.d/nvm.sh' "$stage" \
|
||||
|| note 'the extension build never sources nvm, so npm is missing on any machine without a system node'
|
||||
|
||||
# node_modules is a dependency tree, not configuration.
|
||||
git -C "$repo_dir" check-ignore -q "$extension/node_modules" 2>/dev/null \
|
||||
git -C "$repo_dir" check-ignore -q "$extension/node_modules/" 2>/dev/null \
|
||||
|| note 'the extension node_modules is not gitignored'
|
||||
|
||||
# npm must honour the committed dependency graph. This disposable fixture
|
||||
@@ -167,6 +167,131 @@ stage_output="$(PATH="$fixture_root/bin:$PATH" PANAMA_PATH="$fixture_root" \
|
||||
cmp -s -- "$lock_before" "$lockfile" \
|
||||
|| note 'a rejected Vicinae lockfile mismatch changed package-lock.json'
|
||||
|
||||
# Successful builds carry a digest receipt over both manifests and every
|
||||
# source file. Directory mtimes do not change when an existing file is edited,
|
||||
# so each byte class must independently invalidate the build.
|
||||
digest_root="$fixture_root/digest"
|
||||
digest_extension="$digest_root/config/local/share/vicinae/extensions/panama-search"
|
||||
digest_data="$digest_root/vicinae-data"
|
||||
mkdir -p "$digest_root/config/local/share/vicinae/scripts" \
|
||||
"$digest_extension/src" "$digest_extension/assets" "$digest_root/bin"
|
||||
cp -- "$manifest" "$digest_extension/package.json"
|
||||
cp -- "$repo_dir/config/local/share/vicinae/extensions/panama-search/package-lock.json" \
|
||||
"$digest_extension/package-lock.json"
|
||||
cp -- "$repo_dir/config/local/share/vicinae/extensions/panama-search/src/search.tsx" \
|
||||
"$digest_extension/src/search.tsx"
|
||||
cp -- "$repo_dir/config/local/share/vicinae/extensions/panama-search/tsconfig.json" \
|
||||
"$digest_extension/tsconfig.json"
|
||||
cp -- "$repo_dir/config/local/share/vicinae/extensions/panama-search/assets/extension_icon.svg" \
|
||||
"$digest_extension/assets/extension_icon.svg"
|
||||
cat >"$digest_root/bin/npm" <<'EOF'
|
||||
#!/usr/bin/env bash
|
||||
printf '%s\n' "$*" >>"${NPM_LOG:?}"
|
||||
case "${1:-}:${2:-}" in
|
||||
ci:--silent) exit 0 ;;
|
||||
run:build)
|
||||
mkdir -p "$VICINAE_DATA_DIR/extensions/$(basename "$PWD")"
|
||||
printf 'built\n' >"$VICINAE_DATA_DIR/extensions/$(basename "$PWD")/bundle"
|
||||
;;
|
||||
*) exit 64 ;;
|
||||
esac
|
||||
EOF
|
||||
cat >"$digest_root/bin/find" <<'EOF'
|
||||
#!/usr/bin/env bash
|
||||
set -uo pipefail
|
||||
status=0
|
||||
/usr/bin/find "$@" || status=$?
|
||||
[[ "${STUB_FIND_FAIL:-0}" != 1 ]] || exit 74
|
||||
exit "$status"
|
||||
EOF
|
||||
chmod +x "$digest_root/bin/npm" "$digest_root/bin/find"
|
||||
|
||||
run_digest_stage() {
|
||||
: >"$digest_root/npm.log"
|
||||
PATH="$digest_root/bin:$PATH" PANAMA_PATH="$digest_root" \
|
||||
VICINAE_DATA_DIR="$digest_data" NPM_LOG="$digest_root/npm.log" \
|
||||
STUB_FIND_FAIL="${STUB_FIND_FAIL:-0}" \
|
||||
bash "$stage" >"$digest_root/stage.out" 2>&1
|
||||
}
|
||||
|
||||
run_digest_stage || note 'the Vicinae digest fixture initial build failed'
|
||||
cmp -s <(printf 'ci --silent\nrun build\n') "$digest_root/npm.log" \
|
||||
|| note 'the Vicinae digest fixture did not perform its initial locked build'
|
||||
run_digest_stage || note 'the unchanged Vicinae digest fixture failed'
|
||||
[[ ! -s "$digest_root/npm.log" ]] \
|
||||
|| note 'an unchanged Vicinae extension rebuilt despite its matching receipt'
|
||||
|
||||
for digest_input in src/search.tsx package.json package-lock.json tsconfig.json \
|
||||
assets/extension_icon.svg; do
|
||||
printf '\n// digest mutation: %s\n' "$digest_input" >>"$digest_extension/$digest_input"
|
||||
run_digest_stage || note "the Vicinae digest fixture failed after changing $digest_input"
|
||||
cmp -s <(printf 'ci --silent\nrun build\n') "$digest_root/npm.log" \
|
||||
|| note "changing existing $digest_input bytes did not rebuild the Vicinae extension"
|
||||
done
|
||||
|
||||
# A traversal can emit valid-looking partial output and still fail. Sorting
|
||||
# that output must not hide find's producer status or replace the successful
|
||||
# build receipt with a digest over an incomplete source tree.
|
||||
digest_receipt="$digest_data/extensions/panama-search/.panama-source-sha256"
|
||||
cp -- "$digest_receipt" "$digest_root/receipt.before-find-failure"
|
||||
STUB_FIND_FAIL=1 run_digest_stage \
|
||||
|| note 'the Vicinae stage made a digest traversal failure fatal'
|
||||
[[ ! -s "$digest_root/npm.log" ]] \
|
||||
|| note 'a failed Vicinae digest traversal still rebuilt the extension'
|
||||
grep -q 'inputs could not be verified; skipping' "$digest_root/stage.out" \
|
||||
|| note 'a failed Vicinae digest traversal was accepted as verified input'
|
||||
cmp -s -- "$digest_root/receipt.before-find-failure" "$digest_receipt" \
|
||||
|| note 'a failed Vicinae digest traversal replaced the successful receipt'
|
||||
|
||||
# Helper writes run in conditional contexts in production, where Bash disables
|
||||
# implicit errexit inside the whole function. Each producer therefore has to
|
||||
# return its own write/publication failure and remove its temporary receipt.
|
||||
vicinae_helpers="$digest_root/vicinae-helpers"
|
||||
sed '/^panama_path=/,$d' "$stage" >"$vicinae_helpers"
|
||||
: >"$digest_root/empty-inputs"
|
||||
mkdir "$digest_root/manifest-output-directory"
|
||||
manifest_status=0
|
||||
bash -c 'source "$1"; set +e; _write_vicinae_manifest "$2" "$3" "$4"' bash \
|
||||
"$vicinae_helpers" "$digest_extension" "$digest_root/empty-inputs" \
|
||||
"$digest_root/manifest-output-directory" >/dev/null 2>&1 \
|
||||
|| manifest_status=$?
|
||||
[[ "$manifest_status" -ne 0 ]] \
|
||||
|| note 'a failed Vicinae manifest initialization returned success'
|
||||
|
||||
receipt_failure_root="$digest_root/receipt-publication-failure"
|
||||
mkdir -p "$receipt_failure_root/built" "$receipt_failure_root/bin"
|
||||
printf 'prior receipt\n' >"$receipt_failure_root/built/.panama-source-sha256"
|
||||
cat >"$receipt_failure_root/bin/mv" <<'EOF'
|
||||
#!/usr/bin/env bash
|
||||
destination="${!#}"
|
||||
[[ "$destination" != */.panama-source-sha256 ]] || exit 75
|
||||
exec /usr/bin/mv "$@"
|
||||
EOF
|
||||
chmod +x "$receipt_failure_root/bin/mv"
|
||||
receipt_status=0
|
||||
PATH="$receipt_failure_root/bin:$PATH" bash -c \
|
||||
'source "$1"; set +e; _record_vicinae_digest "$2" "$3"' bash \
|
||||
"$vicinae_helpers" "$receipt_failure_root/built" "$(printf 'a%.0s' {1..64})" \
|
||||
>/dev/null 2>&1 || receipt_status=$?
|
||||
[[ "$receipt_status" -ne 0 ]] \
|
||||
|| note 'a failed Vicinae receipt publication returned success'
|
||||
cmp -s <(printf 'prior receipt\n') \
|
||||
"$receipt_failure_root/built/.panama-source-sha256" \
|
||||
|| note 'a failed Vicinae receipt publication replaced the prior receipt'
|
||||
[[ -z "$(find "$receipt_failure_root/built" \
|
||||
-name '.panama-source-sha256.*' -print -quit)" ]] \
|
||||
|| note 'a failed Vicinae receipt publication left a temporary receipt'
|
||||
|
||||
# Prove the directory-only ignore rule in a repository where node_modules does
|
||||
# not already exist. The trailing slash is part of the query contract.
|
||||
ignore_root="$fixture_root/ignore-repository"
|
||||
mkdir -p "$ignore_root/config/local/share/vicinae/extensions/panama-search"
|
||||
cp -- "$repo_dir/.gitignore" "$ignore_root/.gitignore"
|
||||
git -C "$ignore_root" init -q
|
||||
git -C "$ignore_root" check-ignore -q \
|
||||
'config/local/share/vicinae/extensions/panama-search/node_modules/' \
|
||||
|| note 'a fresh clone with no node_modules directory does not match the ignore rule'
|
||||
|
||||
# ── Report ───────────────────────────────────────────────────────────────────
|
||||
|
||||
if (( ${#findings[@]} > 0 )); then
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -187,6 +187,18 @@ case "${1:-}" in
|
||||
*) exit 97 ;;
|
||||
esac
|
||||
;;
|
||||
ls-tree)
|
||||
[[ "$#" -eq 6 && "$4" == -rz && "$5" == --full-tree \
|
||||
&& "$6" == "$PANAMA_BOOT_REVISION" ]] || exit 97
|
||||
object_id="$(/usr/bin/git hash-object --no-filters -- \
|
||||
"$PANAMA_BOOT_FIXTURE_ROOT/stub-install")" || exit 97
|
||||
printf '100755 blob %s\tinstall\0' "$object_id"
|
||||
;;
|
||||
hash-object)
|
||||
[[ "$#" -eq 6 && "$4" == --no-filters && "$5" == -- \
|
||||
&& "$6" == install ]] || exit 97
|
||||
/usr/bin/git hash-object --no-filters -- "$2/$6"
|
||||
;;
|
||||
*) exit 97 ;;
|
||||
esac
|
||||
;;
|
||||
|
||||
@@ -51,7 +51,9 @@ copy_hash_inputs() {
|
||||
find "$repo_dir/setup/provenance" -type f -print0
|
||||
)
|
||||
mkdir -p "$root/setup/lib"
|
||||
cp -- "$repo_dir/setup/lib/artifact-provenance" "$root/setup/lib/artifact-provenance"
|
||||
cp -- "$repo_dir/setup/lib/artifact-provenance" \
|
||||
"$repo_dir/setup/lib/extras-catalog" \
|
||||
"$repo_dir/setup/lib/machine-role" "$root/setup/lib/"
|
||||
}
|
||||
|
||||
# A PANAMA_PATH that looks enough like the real one for install to run, and
|
||||
@@ -61,7 +63,7 @@ build_fixture() {
|
||||
rm -rf "$root"
|
||||
mkdir -p "$root/bin" "$root/setup/scripts" "$root/setup/packages" \
|
||||
"$root/setup/lib" "$root/setup/provenance/keys" \
|
||||
"$root/config/dot/quickshell/scripts"
|
||||
"$root/config/dot/quickshell/scripts" "$root/tmp"
|
||||
|
||||
cp "$installer" "$root/install"
|
||||
: >"$root/bin/ascii"
|
||||
@@ -126,6 +128,46 @@ EOF
|
||||
#!/usr/bin/env bash
|
||||
printf 'dnf-transaction\n' >>"$PANAMA_RAN"
|
||||
exit 0
|
||||
EOF
|
||||
cat >"$root/shim/mv" <<'EOF'
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
destination="${!#}"
|
||||
if [[ "${STUB_SIGNAL_PACKAGES_HASH:-0}" == 1 \
|
||||
&& "$destination" == */state/panama/packages-hash ]]; then
|
||||
printf 'signal:packages-receipt\n' >>"$PANAMA_RAN"
|
||||
pgid="$(ps -o pgid= -p $$ | tr -d ' ')"
|
||||
kill -TERM -- "-$pgid"
|
||||
sleep 2
|
||||
fi
|
||||
exec /usr/bin/mv "$@"
|
||||
EOF
|
||||
cat >"$root/shim/mktemp" <<'EOF'
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
if [[ "${STUB_SIGNAL_HASH_WORK:-0}" == 1 && "${1:-}" == -d ]]; then
|
||||
directory="$(/usr/bin/mktemp "$@")"
|
||||
printf '%s\n' "$directory"
|
||||
printf 'signal:packages-hash-work\n' >>"$PANAMA_RAN"
|
||||
pgid="$(ps -o pgid= -p $$ | tr -d ' ')"
|
||||
kill -TERM -- "-$pgid"
|
||||
sleep 2
|
||||
fi
|
||||
exec /usr/bin/mktemp "$@"
|
||||
EOF
|
||||
cat >"$root/shim/mkdir" <<'EOF'
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
target="${!#}"
|
||||
if [[ "${STUB_SIGNAL_HASH_WORK:-0}" == 1 \
|
||||
&& "$(basename -- "$target")" == panama-packages-hash.* ]]; then
|
||||
/usr/bin/mkdir "$@"
|
||||
printf 'signal:packages-hash-work\n' >>"$PANAMA_RAN"
|
||||
pgid="$(ps -o pgid= -p $$ | tr -d ' ')"
|
||||
kill -TERM -- "-$pgid"
|
||||
sleep 2
|
||||
fi
|
||||
exec /usr/bin/mkdir "$@"
|
||||
EOF
|
||||
for prerequisite in gum lspci mokutil fwupdmgr; do
|
||||
ln -s gsettings "$root/shim/$prerequisite"
|
||||
@@ -139,7 +181,8 @@ run_install() {
|
||||
local status=0
|
||||
: >"$root/ran"
|
||||
PATH="$root/shim:$PATH" PANAMA_PATH="$root" PANAMA_RAN="$root/ran" \
|
||||
XDG_STATE_HOME="$root/state" bash "$root/install" "$@" \
|
||||
XDG_STATE_HOME="$root/state" TMPDIR="$root/tmp" \
|
||||
/usr/bin/setsid bash "$root/install" "$@" \
|
||||
>"$root/out" 2>&1 || status=$?
|
||||
cat "$root/ran"
|
||||
return "$status"
|
||||
@@ -147,7 +190,8 @@ run_install() {
|
||||
|
||||
run_hash() {
|
||||
local root="$1"
|
||||
sed -n '/^hash_packages() {/,/^}$/p' "$root/install" >"$root/hash-only"
|
||||
sed -n '/^_collect_package_inputs() {/,/^PACKAGE_START_HASH=/p' \
|
||||
"$root/install" >"$root/hash-only"
|
||||
printf 'set -uo pipefail\nhash_packages\n' >>"$root/hash-only"
|
||||
PANAMA_PATH="$root" bash "$root/hash-only" 2>"$root/hash-only.err"
|
||||
}
|
||||
@@ -255,7 +299,8 @@ grep -qx 'install-packages' <<<"$ran_forced" \
|
||||
# Dynamically discovering them makes this fail when a new reviewed input is
|
||||
# added but omitted from hash_packages.
|
||||
for relative in "${package_inputs[@]}" "${provenance_inputs[@]}" \
|
||||
'setup/scripts/install-packages' 'setup/lib/artifact-provenance'; do
|
||||
'setup/scripts/install-packages' 'setup/lib/artifact-provenance' \
|
||||
'setup/lib/extras-catalog' 'setup/lib/machine-role'; do
|
||||
printf 'changed %s\n' "$relative" >>"$tmp/a/$relative"
|
||||
install_status=0
|
||||
ran_input_changed="$(run_install "$tmp/a" --upgrade)" || install_status=$?
|
||||
@@ -282,7 +327,8 @@ done
|
||||
# Fixed hash inputs must not silently disappear or degrade into a directory or
|
||||
# link. An unreadable package input also proves a failed content read cannot be
|
||||
# hidden by the final digest command.
|
||||
for fixed_input in setup/scripts/install-packages setup/lib/artifact-provenance; do
|
||||
for fixed_input in setup/scripts/install-packages setup/lib/artifact-provenance \
|
||||
setup/lib/extras-catalog setup/lib/machine-role; do
|
||||
for case_name in missing directory symlink unreadable; do
|
||||
case_root="$tmp/hash-${fixed_input//\//-}-$case_name"
|
||||
build_fixture "$case_root"
|
||||
@@ -305,6 +351,27 @@ build_fixture "$read_failure_root"
|
||||
chmod 000 "$read_failure_root/${package_inputs[0]}"
|
||||
assert_hash_failure "$read_failure_root" "${package_inputs[0]} unreadable"
|
||||
|
||||
# Discovery must reject a symlink instead of silently dropping it from the
|
||||
# receipt while a later consumer follows it.
|
||||
for discovered_root in setup/packages setup/provenance; do
|
||||
case_root="$tmp/hash-${discovered_root//\//-}-symlink"
|
||||
build_fixture "$case_root"
|
||||
printf 'linked installer input\n' >"$case_root/symlink-target"
|
||||
ln -s "$case_root/symlink-target" "$case_root/$discovered_root/symlink-input"
|
||||
assert_hash_failure "$case_root" "$discovered_root symlink input"
|
||||
done
|
||||
|
||||
# Discovery roots are behavior inputs too. GNU find -P treats a symlink passed
|
||||
# as its starting path as an empty traversal, so checking only descendants can
|
||||
# silently erase a whole package or provenance tree from the receipt.
|
||||
for discovered_root in setup/packages setup/provenance; do
|
||||
case_root="$tmp/hash-${discovered_root//\//-}-root-symlink"
|
||||
build_fixture "$case_root"
|
||||
mv -- "$case_root/$discovered_root" "$case_root/$discovered_root.real"
|
||||
ln -s "$case_root/$discovered_root.real" "$case_root/$discovered_root"
|
||||
assert_hash_failure "$case_root" "$discovered_root discovery-root symlink"
|
||||
done
|
||||
|
||||
# A hash failure is an installer failure, not a reason to skip the package
|
||||
# stage and retain a stale stamp.
|
||||
build_fixture "$tmp/hash-failure"
|
||||
@@ -320,6 +387,61 @@ grep -qx 'install-packages' <<<"$ran_hash_failure" \
|
||||
cmp -s -- "$tmp/hash-failure/stamp-before" "$tmp/hash-failure/state/panama/packages-hash" \
|
||||
|| note 'a failed package-state hash wrote a new packages-hash stamp'
|
||||
|
||||
# The stage may race its own input receipt. A successful stage that changes a
|
||||
# sourced behavior file must not stamp the new digest as though it were the
|
||||
# bytes used to decide this run.
|
||||
build_fixture "$tmp/hash-drift"
|
||||
cat >"$tmp/hash-drift/setup/scripts/install-packages" <<'EOF'
|
||||
#!/usr/bin/env bash
|
||||
if [[ "${1:-}" == --trust-preflight ]]; then
|
||||
printf 'trust-preflight\n' >>"$PANAMA_RAN"
|
||||
exit 0
|
||||
fi
|
||||
printf 'install-packages\n' >>"$PANAMA_RAN"
|
||||
printf '# changed during package stage\n' >>"$PANAMA_PATH/setup/lib/machine-role"
|
||||
EOF
|
||||
chmod +x "$tmp/hash-drift/setup/scripts/install-packages"
|
||||
install_status=0
|
||||
run_install "$tmp/hash-drift" --upgrade >/dev/null || install_status=$?
|
||||
[[ "$install_status" -ne 0 ]] \
|
||||
|| note 'mid-stage package input drift returned success'
|
||||
[[ ! -e "$tmp/hash-drift/state/panama/packages-hash" ]] \
|
||||
|| note 'mid-stage package input drift stamped bytes the stage did not start with'
|
||||
|
||||
# The hash workspace exists before command substitution publishes its pathname.
|
||||
# A process-group signal in that window must still remove the private tree.
|
||||
build_fixture "$tmp/hash-work-signal"
|
||||
install_status=0
|
||||
signal_run="$(STUB_SIGNAL_HASH_WORK=1 \
|
||||
run_install "$tmp/hash-work-signal" --upgrade)" || install_status=$?
|
||||
[[ "$install_status" -eq 143 ]] \
|
||||
|| note "package hash workspace signal returned $install_status instead of 143"
|
||||
grep -qx 'signal:packages-hash-work' <<<"$signal_run" \
|
||||
|| note 'package hash workspace adapter did not deliver a real process-group signal'
|
||||
[[ -z "$(find "$tmp/hash-work-signal/tmp" -mindepth 1 -print -quit)" ]] \
|
||||
|| note 'package hash workspace signal left a private temporary directory'
|
||||
|
||||
# A real process-group signal at the final receipt rename must preserve the
|
||||
# prior stamp and remove the private temporary receipt.
|
||||
build_fixture "$tmp/hash-receipt-signal"
|
||||
run_install "$tmp/hash-receipt-signal" --upgrade >/dev/null
|
||||
cp -- "$tmp/hash-receipt-signal/state/panama/packages-hash" \
|
||||
"$tmp/hash-receipt-signal/stamp-before"
|
||||
install_status=0
|
||||
signal_run="$(STUB_SIGNAL_PACKAGES_HASH=1 \
|
||||
run_install "$tmp/hash-receipt-signal" --upgrade --packages)" \
|
||||
|| install_status=$?
|
||||
[[ "$install_status" -eq 143 ]] \
|
||||
|| note "package receipt signal returned $install_status instead of 143"
|
||||
grep -qx 'signal:packages-receipt' <<<"$signal_run" \
|
||||
|| note 'package receipt signal adapter did not deliver a real process-group signal'
|
||||
cmp -s -- "$tmp/hash-receipt-signal/stamp-before" \
|
||||
"$tmp/hash-receipt-signal/state/panama/packages-hash" \
|
||||
|| note 'package receipt signal replaced the prior hash stamp'
|
||||
[[ -z "$(find "$tmp/hash-receipt-signal/state/panama" \
|
||||
-name '.packages-hash.*' -print -quit)" ]] \
|
||||
|| note 'package receipt signal left a temporary hash stamp'
|
||||
|
||||
# A failing stage must not record the hash, or the failure is hidden forever.
|
||||
build_fixture "$tmp/c" 1
|
||||
install_status=0
|
||||
@@ -368,6 +490,64 @@ for suppressed in link-dotfiles link-skills link-user change-settings install-ha
|
||||
&& note "stage-time Terra trust failure still ran $suppressed"
|
||||
done
|
||||
|
||||
# Exercise the complete real package entrypoint at the second boundary. The
|
||||
# outer preflight sees no Terra repository; the same DNF adapter exposes an
|
||||
# unsafe enabled Terra identity to the package stage's own preflight. Removing
|
||||
# that production call would reach the transaction marker below.
|
||||
real_preflight_root="$tmp/real-second-preflight"
|
||||
build_fixture "$real_preflight_root"
|
||||
cp -- "$repo_dir/setup/scripts/install-packages" \
|
||||
"$real_preflight_root/setup/scripts/install-packages"
|
||||
chmod +x "$real_preflight_root/setup/scripts/install-packages"
|
||||
mkdir -p "$real_preflight_root/state/panama"
|
||||
printf 'server\n' >"$real_preflight_root/state/panama/role"
|
||||
cat >"$real_preflight_root/shim/dnf" <<'EOF'
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
if [[ "$*" == '--quiet --no-plugins --dump-repo-config=*' ]]; then
|
||||
count=0
|
||||
[[ ! -f "$PANAMA_DNF_DUMP_COUNT" ]] || read -r count <"$PANAMA_DNF_DUMP_COUNT"
|
||||
count=$((count + 1))
|
||||
printf '%s\n' "$count" >"$PANAMA_DNF_DUMP_COUNT"
|
||||
printf 'dnf-dump\n' >>"$PANAMA_RAN"
|
||||
printf '======== "fedora" repository configuration: ========\n'
|
||||
printf 'baseurl = \nenabled = 1\ngpgcheck = 1\n'
|
||||
printf 'gpgkey = file:///etc/pki/rpm-gpg/RPM-GPG-KEY-fedora-44-primary\n'
|
||||
printf 'metalink = https://mirrors.fedoraproject.org/metalink\nmirrorlist\n'
|
||||
printf 'pkg_gpgcheck = 0\nrepo_gpgcheck = 0\n'
|
||||
if (( count == 2 )); then
|
||||
printf '======== "terra" repository configuration: ========\n'
|
||||
printf 'baseurl = https://evil.invalid/terra44\nenabled = 1\ngpgcheck = 0\n'
|
||||
printf 'gpgkey = https://evil.invalid/key\n'
|
||||
printf 'metalink = \nmirrorlist = \npkg_gpgcheck = 0\nrepo_gpgcheck = 0\n'
|
||||
fi
|
||||
exit 0
|
||||
fi
|
||||
printf 'dnf-transaction\n' >>"$PANAMA_RAN"
|
||||
exit 0
|
||||
EOF
|
||||
chmod +x "$real_preflight_root/shim/dnf"
|
||||
printf '0\n' >"$real_preflight_root/dnf-dump-count"
|
||||
: >"$real_preflight_root/ran"
|
||||
real_preflight_status=0
|
||||
PATH="$real_preflight_root/shim:$PATH" \
|
||||
PANAMA_PATH="$real_preflight_root" PANAMA_RAN="$real_preflight_root/ran" \
|
||||
PANAMA_DNF_DUMP_COUNT="$real_preflight_root/dnf-dump-count" \
|
||||
XDG_STATE_HOME="$real_preflight_root/state" \
|
||||
bash "$real_preflight_root/install" --upgrade --packages \
|
||||
>"$real_preflight_root/out" 2>&1 || real_preflight_status=$?
|
||||
[[ "$real_preflight_status" -eq 78 ]] \
|
||||
|| note "real second repository preflight returned $real_preflight_status instead of 78"
|
||||
[[ "$(<"$real_preflight_root/dnf-dump-count")" == 2 ]] \
|
||||
|| note "real package entrypoint executed $(<"$real_preflight_root/dnf-dump-count") repository preflights instead of two: $(tr '\n' ' ' <"$real_preflight_root/out")"
|
||||
[[ "$(grep -c '^dnf-dump$' "$real_preflight_root/ran")" -eq 2 ]] \
|
||||
|| note "real second preflight fixture log was: $(tr '\n' ',' <"$real_preflight_root/ran")"
|
||||
for suppressed in dnf-transaction link-dotfiles link-skills link-user change-settings \
|
||||
install-hardware; do
|
||||
grep -qx "$suppressed" "$real_preflight_root/ran" \
|
||||
&& note "real second repository preflight still ran $suppressed"
|
||||
done
|
||||
|
||||
# A full install always runs the stage, whatever any recorded hash says.
|
||||
build_fixture "$tmp/d"
|
||||
install_status=0
|
||||
|
||||
Reference in New Issue
Block a user