fix(installer): harden greenfield detector contracts

This commit is contained in:
2026-08-05 17:46:58 -05:00
parent 99e28d4100
commit 3edde464b3
18 changed files with 924 additions and 190 deletions
+121 -59
View File
@@ -133,16 +133,26 @@ make_fake_npm "$good_bin"
cp "$COMPAT_BIN/realpath" "$good_bin/realpath"
cat > "$good_bin/id" <<'ID'
#!/bin/bash
uid="${MOSAIC_TEST_UID:-1001}"
gid="${MOSAIC_TEST_GID:-1001}"
user="${MOSAIC_TEST_USER:-fixture-user}"
case "${1:-}" in
-u) echo 1001 ;;
-g) echo 1001 ;;
-un) echo fixture-user ;;
-u) echo "$uid" ;;
-g) echo "$gid" ;;
-un) echo "$user" ;;
*) exec /bin/id "$@" ;;
esac
ID
cat > "$good_bin/stat" <<'STAT'
#!/bin/bash
if [[ "${1:-} ${2:-}" == '-c %u' ]]; then echo 1001; exit 0; fi
if [[ "${1:-} ${2:-}" == '-c %u' ]]; then
[[ "${3:-}" == "${MOSAIC_TEST_WRONG_OWNER_PATH:-__none__}" ]] && echo 9999 || echo "${MOSAIC_TEST_UID:-1001}"
exit 0
fi
if [[ "${1:-} ${2:-}" == '-c %g' ]]; then
[[ "${3:-}" == "${MOSAIC_TEST_WRONG_GROUP_PATH:-__none__}" ]] && echo 9999 || echo "${MOSAIC_TEST_GID:-1001}"
exit 0
fi
exec /bin/stat "$@"
STAT
cat > "$good_bin/curl" <<'CURL'
@@ -161,7 +171,7 @@ CLI
chmod 0755 "$good_prefix/bin/mosaic"
cat > "$good_bin/getent" <<GETENT
#!/bin/bash
printf '%s:x:%s:%s::%s:%s\\n' '$(id -un)' '$(id -u)' '$(id -g)' '$good_home' '$good_bin/bash'
printf '%s:x:%s:%s::%s:%s\\n' "\${MOSAIC_TEST_USER:-fixture-user}" "\${MOSAIC_TEST_UID:-1001}" "\${MOSAIC_TEST_GID:-1001}" "\${MOSAIC_TEST_PASSWD_HOME:-$good_home}" '$good_bin/bash'
GETENT
cat > "$good_bin/bash" <<SHELL
#!/bin/bash
@@ -199,6 +209,101 @@ good_rows="$(grep -Ec '^\[P[0-8]\] PASS:' "$TMP/good-check.log" || true)"
[[ "$good_rows" -eq 9 ]] && pass_case 'good-host --check emitted nine PASS rows' \
|| { cat "$TMP/good-check.log" >&2; fail_case "good-host --check emitted $good_rows PASS rows"; }
printf '[test] case: P0 binds uid, username, passwd HOME, shell, and privilege mode\n'
passwd_home="$TMP/passwd-authoritative-home"
mkdir -p "$passwd_home"
set +e
HOME="$good_home" MOSAIC_HOME="$good_mosaic" MOSAIC_PREFIX="$good_prefix" \
MOSAIC_TEST_PASSWD_HOME="$passwd_home" MOSAIC_NO_COLOR=1 \
PATH="$good_bin:/usr/local/bin:/usr/bin:/bin" \
bash "$ROOT/tools/install.sh" --check --next >"$TMP/wrong-home.log" 2>&1
wrong_home_status=$?
set -e
[[ "$wrong_home_status" -ne 0 ]] || fail_case 'P0 accepted ambient HOME that disagrees with passwd HOME'
grep -q '^\[P0\] FAIL:.*HOME mismatch' "$TMP/wrong-home.log" \
&& pass_case 'P0 rejects ambient HOME that disagrees with passwd HOME' \
|| fail_case 'P0 did not attribute the passwd HOME mismatch'
for privilege_case in root-with-home sudo-with-inherited-home; do
extra_env=()
[[ "$privilege_case" == sudo-with-inherited-home ]] && extra_env+=(SUDO_USER=fixture-user SUDO_UID=1001)
set +e
env HOME="$good_home" MOSAIC_HOME="$good_mosaic" MOSAIC_PREFIX="$good_prefix" \
MOSAIC_TEST_UID=0 MOSAIC_TEST_GID=0 MOSAIC_TEST_USER=root MOSAIC_TEST_PASSWD_HOME=/root \
MOSAIC_NO_COLOR=1 PATH="$good_bin:/usr/local/bin:/usr/bin:/bin" "${extra_env[@]}" \
bash "$ROOT/tools/install.sh" --check --next >"$TMP/$privilege_case.log" 2>&1
privilege_status=$?
set -e
[[ "$privilege_status" -ne 0 ]] || fail_case "P0 accepted unsafe $privilege_case context"
grep -q '^\[P0\] FAIL:.*privilege=' "$TMP/$privilege_case.log" \
&& pass_case "P0 states and rejects $privilege_case privilege context" \
|| fail_case "P0 did not state $privilege_case privilege mode"
done
printf '[test] case: P3/P5 reject unsafe owner, group, and mode\n'
chmod 0777 "$good_prefix/bin/mosaic"
set +e
HOME="$good_home" MOSAIC_HOME="$good_mosaic" MOSAIC_PREFIX="$good_prefix" MOSAIC_NO_COLOR=1 \
PATH="$good_bin:/usr/local/bin:/usr/bin:/bin" bash "$ROOT/tools/install.sh" --check --next >"$TMP/p3-mode.log" 2>&1
p3_mode_status=$?
set -e
[[ "$p3_mode_status" -ne 0 ]] || fail_case 'P3 accepted mode-0777 CLI'
grep -q '^\[P3\] FAIL:.*unsafe owner/group/mode' "$TMP/p3-mode.log" \
&& pass_case 'P3 rejects group/world-writable CLI' || fail_case 'P3 did not attribute unsafe CLI mode'
chmod 0755 "$good_prefix/bin/mosaic"
for ownership_case in owner group; do
wrong_env=()
[[ "$ownership_case" == owner ]] && wrong_env+=(MOSAIC_TEST_WRONG_OWNER_PATH="$good_prefix/bin/mosaic")
[[ "$ownership_case" == group ]] && wrong_env+=(MOSAIC_TEST_WRONG_GROUP_PATH="$good_prefix/bin/mosaic")
set +e
env HOME="$good_home" MOSAIC_HOME="$good_mosaic" MOSAIC_PREFIX="$good_prefix" MOSAIC_NO_COLOR=1 \
PATH="$good_bin:/usr/local/bin:/usr/bin:/bin" "${wrong_env[@]}" \
bash "$ROOT/tools/install.sh" --check --next >"$TMP/p3-$ownership_case.log" 2>&1
owner_status=$?
set -e
[[ "$owner_status" -ne 0 ]] || fail_case "P3 accepted wrong CLI $ownership_case"
grep -q '^\[P3\] FAIL:.*unsafe owner/group/mode' "$TMP/p3-$ownership_case.log" \
&& pass_case "P3 rejects wrong CLI $ownership_case" || fail_case "P3 did not attribute wrong CLI $ownership_case"
done
chmod 0644 "$good_mosaic/SOUL.md" "$good_mosaic/USER.md"
set +e
HOME="$good_home" MOSAIC_HOME="$good_mosaic" MOSAIC_PREFIX="$good_prefix" MOSAIC_NO_COLOR=1 \
PATH="$good_bin:/usr/local/bin:/usr/bin:/bin" bash "$ROOT/tools/install.sh" --check --next >"$TMP/p5-mode.log" 2>&1
p5_mode_status=$?
set -e
[[ "$p5_mode_status" -ne 0 ]] || fail_case 'P5 accepted world-readable identity files'
grep -q '^\[P5\] FAIL:' "$TMP/p5-mode.log" \
&& pass_case 'P5 rejects world-readable identity files' || fail_case 'P5 did not reject identity mode 0644'
chmod 0600 "$good_mosaic/SOUL.md" "$good_mosaic/USER.md"
mkdir -p "$good_mosaic/credentials"
chmod 0755 "$good_mosaic/credentials"
set +e
HOME="$good_home" MOSAIC_HOME="$good_mosaic" MOSAIC_PREFIX="$good_prefix" MOSAIC_NO_COLOR=1 \
PATH="$good_bin:/usr/local/bin:/usr/bin:/bin" bash "$ROOT/tools/install.sh" --check --next >"$TMP/p5-credentials.log" 2>&1
credential_status=$?
set -e
[[ "$credential_status" -ne 0 ]] || fail_case 'P5 accepted mode-0755 credentials directory'
grep -q '^\[P5\] FAIL:.*credentials' "$TMP/p5-credentials.log" \
&& pass_case 'P5 rejects group/world-readable credential storage' \
|| fail_case 'P5 did not attribute unsafe credential directory mode'
chmod 0700 "$good_mosaic/credentials"
printf '# framework\n' > "$good_mosaic/AGENTS.md"
chmod 0666 "$good_mosaic/AGENTS.md"
set +e
HOME="$good_home" MOSAIC_HOME="$good_mosaic" MOSAIC_PREFIX="$good_prefix" MOSAIC_NO_COLOR=1 \
PATH="$good_bin:/usr/local/bin:/usr/bin:/bin" bash "$ROOT/tools/install.sh" --check --next >"$TMP/p4-tree-mode.log" 2>&1
framework_mode_status=$?
set -e
[[ "$framework_mode_status" -ne 0 ]] || fail_case 'P4 accepted group/world-writable framework path'
grep -q '^\[P4\] FAIL:.*owner/mode policy' "$TMP/p4-tree-mode.log" \
&& pass_case 'P4 inventories and rejects unsafe created framework paths' \
|| fail_case 'P4 did not attribute unsafe created-path mode'
chmod 0644 "$good_mosaic/AGENTS.md"
printf '[test] case: persisted required-action failures remain blocking\n'
for blocked_phase in P4 P6; do
node -e '
@@ -219,36 +324,12 @@ for blocked_phase in P4 P6; do
done
printf '{\n "lane": "next",\n "cliVersion": "0.0.50-next.999",\n "phaseOutcomes": {"P4":"committed","P6":"committed"}\n}\n' > "$good_mosaic/.install-manifest.json"
printf '[test] case: per-phase P2-P8 fault injection restores representative host mutations\n'
for phase in P2 P3 P4 P5 P6 P7 P8; do
home="$TMP/fault-$phase/home"
state="$TMP/fault-$phase/state"
mkdir -p "$home/.config/mosaic" "$home/.npm-global/bin" "$home/.claude" "$state"
printf 'operator-framework-sentinel\n' > "$home/.config/mosaic/operator.txt"
printf '@scope:registry=https://pre.example.invalid/\n' > "$home/.npmrc"
printf 'old-cli\n' > "$home/.npm-global/bin/mosaic"
printf '{"hooks":{"safe":true}}\n' > "$home/.claude/settings.json"
before="$(fingerprint "$home")"
set +e
HOME="$home" MOSAIC_HOME="$home/.config/mosaic" MOSAIC_PREFIX="$home/.npm-global" \
MOSAIC_INSTALL_STATE_DIR="$state" MOSAIC_INSTALL_FAULT_AFTER="$phase" \
MOSAIC_NO_COLOR=1 PATH="$COMPAT_BIN:$PATH" bash "$ROOT/tools/install.sh" --state-machine-self-test \
>"$TMP/fault-$phase.log" 2>&1
status=$?
set -e
after="$(fingerprint "$home")"
[[ "$status" -ne 0 ]] || fail_case "$phase injected fault returned zero"
grep -q "phase=$phase" "$TMP/fault-$phase.log" \
|| fail_case "$phase fault transcript did not name the injected phase"
[[ "$before" == "$after" ]] \
&& pass_case "$phase rollback restored framework/npmrc/prefix/runtime representative state" \
|| fail_case "$phase rollback mismatch (before=$before after=$after)"
if find "$state" -type f -exec grep -l '"status"[[:space:]]*:[[:space:]]*"in-progress"' {} + 2>/dev/null | grep -q .; then
fail_case "$phase left a journal in-progress"
else
pass_case "$phase left no journal falsely in-progress"
fi
done
printf '[test] case: fault injection has no synthetic mutation implementation\n'
if grep -q '\.selftest-' "$ROOT/tools/install.sh"; then
fail_case 'synthetic .selftest mutation path remains in the production fault seam'
else
pass_case 'fault seam is attached only to real P2-P8 action flow (exercised by install-next-lane.test.sh)'
fi
printf '[test] case: unsafe and overlapping rollback roots fail before mutation\n'
unsafe_home="$TMP/unsafe-home"
@@ -262,7 +343,7 @@ for case_name in root-target home-target overlap-target; do
before="$(fingerprint "$unsafe_home")"
set +e
HOME="$unsafe_home" MOSAIC_HOME="$unsafe_mosaic" MOSAIC_PREFIX="$unsafe_prefix" \
MOSAIC_NO_COLOR=1 PATH="$good_bin:/usr/local/bin:/usr/bin:/bin" \
MOSAIC_TEST_PASSWD_HOME="$unsafe_home" MOSAIC_NO_COLOR=1 PATH="$good_bin:/usr/local/bin:/usr/bin:/bin" \
bash "$ROOT/tools/install.sh" --check --next >"$TMP/$case_name.log" 2>&1
status=$?
set -e
@@ -279,7 +360,7 @@ mkdir -p "$symlink_home" "$symlink_outside"
ln -s "$symlink_outside" "$symlink_home/.config"
set +e
HOME="$symlink_home" MOSAIC_HOME="$symlink_home/.config/mosaic" MOSAIC_PREFIX="$symlink_home/.npm-global" \
MOSAIC_NO_COLOR=1 PATH="$good_bin:/usr/local/bin:/usr/bin:/bin" \
MOSAIC_TEST_PASSWD_HOME="$symlink_home" MOSAIC_NO_COLOR=1 PATH="$good_bin:/usr/local/bin:/usr/bin:/bin" \
bash "$ROOT/tools/install.sh" --check --next >"$TMP/symlink-target.log" 2>&1
status=$?
set -e
@@ -289,26 +370,6 @@ grep -q '^\[P0\] FAIL:.*unsafe context' "$TMP/symlink-target.log" \
|| fail_case 'symlinked rollback parent lacked an attributable P0 failure'
[[ -z "$(find "$symlink_outside" -mindepth 1 -print -quit)" ]] || fail_case 'symlink target was mutated'
printf '[test] case: stale in-progress projection does not impersonate a live OS lock\n'
stale_home="$TMP/stale/home"
stale_state="$TMP/stale/state"
mkdir -p "$stale_home/.config/mosaic" "$stale_state"
printf '{"status":"in-progress","journal":"%s"}\n' "$stale_state/dead-run/journal.ndjson" > "$stale_state/active.json"
set +e
HOME="$stale_home" MOSAIC_HOME="$stale_home/.config/mosaic" MOSAIC_PREFIX="$stale_home/.npm-global" \
MOSAIC_INSTALL_STATE_DIR="$stale_state" MOSAIC_INSTALL_FAULT_AFTER=P2 MOSAIC_NO_COLOR=1 \
PATH="$COMPAT_BIN:$PATH" bash "$ROOT/tools/install.sh" --state-machine-self-test >"$TMP/stale.log" 2>&1
status=$?
set -e
[[ "$status" -eq 97 ]] || fail_case "stale projection recovery expected injected status 97, got $status"
if find "$stale_state" -name prior-active.json -type f -print -quit | grep -q .; then
pass_case 'stale projection was preserved and superseded after the free OS lock was acquired'
else
fail_case 'stale projection was not preserved for recovery evidence'
fi
[[ "$(node -p "require('$stale_state/active.json').status")" == "rolled-back" ]] \
|| fail_case 'stale retry did not reach an honest rolled-back terminal state'
printf '[test] case: journal initialization failure is fatal before mutation\n'
journal_home="$TMP/journal-failure/home"
mkdir -p "$journal_home/.config/mosaic"
@@ -316,8 +377,9 @@ printf 'journal-sentinel\n' > "$journal_home/.config/mosaic/operator.txt"
before="$(fingerprint "$journal_home")"
set +e
HOME="$journal_home" MOSAIC_HOME="$journal_home/.config/mosaic" MOSAIC_PREFIX="$journal_home/.npm-global" \
MOSAIC_INSTALL_STATE_DIR="/proc/mosaic-journal-denied-$$" MOSAIC_INSTALL_FAULT_AFTER=P2 \
MOSAIC_NO_COLOR=1 bash "$ROOT/tools/install.sh" --state-machine-self-test \
MOSAIC_TEST_PASSWD_HOME="$journal_home" MOSAIC_INSTALL_STATE_DIR="/proc/mosaic-journal-denied-$$" \
MOSAIC_NO_COLOR=1 PATH="$good_bin:/usr/local/bin:/usr/bin:/bin" \
bash "$ROOT/tools/install.sh" --cli --next --yes --no-auto-launch \
>"$TMP/journal-failure.log" 2>&1
status=$?
set -e