mirror of
https://github.com/saymrwulf/betrusted-ed25519-verified.git
synced 2026-09-03 20:13:47 +00:00
Phase 2c exists because a source-regex enumerator proved evadable in ltl-accumulator-verified: attributed, private and `instance` declarations and a nested-namespace basename collision all slipped past it. The fix was to stop reading source text and ask the Lean environment, and that fix was ported here. But a fix ported is not a fix tested. selftest-inventory.sh proves the GATE reacts to a difference; it feeds synthetic observations and never runs the walker. Nothing here had ever asked whether the WALKER SEES a declaration written in an evasive shape. selftest-shapes.sh adds all four shapes to an audited module, recompiles it, runs the real Phase 2c, and requires each one to be NAMED in the UNCLASSIFIED list. Asserting that the gate merely failed would not do: one shape surfacing fails the run while the other three ride along unseen. All four forks report all four. Negative-tested by removing the injection — the run then reports the walker blind and fails. The victim module is derived from each repo's own manifest, not named: the forks do not share a corpus (dalek/anza attack Proofs.Basic, risc0/betrusted Proofs.DecompressMain), and a hard-coded name would have silently found nothing on half of them. It must be manifested, must not be an inventory or audit driver, and must be imported by no other manifest module. Two notes for whoever edits this next. When re-deriving a leaf module, the inventory drivers must be excluded from the set of IMPORTERS as well as from the candidates: they import the whole corpus, so leaving them in makes every module look imported, finds no leaf, and the test silently has no victim at all. And a lift of Phase 2c needs SCALAR_SH/SCALAR_MANIFEST alongside PROOFS, or the coverage check dies on an unbound variable. New executable pinned in HARNESS.sha256. Certified by a full sweep: both buttons, all four forks, purged trees, machine otherwise idle. 8/8 green.
223 lines
10 KiB
Bash
Executable file
223 lines
10 KiB
Bash
Executable file
#!/usr/bin/env bash
|
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
# selftest-shapes.sh — can a declaration HIDE from the inventory walker?
|
|
#
|
|
# check.sh Phase 2c exists because a source-regex enumerator turned out to be
|
|
# evadable: in ltl-accumulator-verified a nine-attack self-test defeated one
|
|
# with attributed, private, indented and `instance` declarations and with a
|
|
# nested-namespace basename collision. The fix was to stop reading source text
|
|
# and ask the Lean environment instead, and that fix was PORTED here — see the
|
|
# note at the head of Phase 2c.
|
|
#
|
|
# But a fix ported is not a fix tested. `selftest-inventory.sh` proves the GATE
|
|
# reacts correctly to a difference; it feeds the gate synthetic observations and
|
|
# never runs the walker. Nothing in these four forks has ever asked the question
|
|
# the accumulator's suite asks: does the WALKER SEE a declaration written in an
|
|
# evasive shape? If it silently skipped one, the observation would match the
|
|
# allowlist, the gate would be satisfied, and a hidden theorem would ride along
|
|
# inside an audited module with every button green.
|
|
#
|
|
# 0 control: the untouched tree's observation equals the allowlist
|
|
# 1 FOUR EVASIVE SHAPES added to an audited module at once:
|
|
# @[simp] theorem an attribute in front of the declaration
|
|
# private theorem not exported, name mangled by the elaborator
|
|
# instance a declaration with no `theorem`/`def` keyword
|
|
# nested namespace Ns.Hidden.X, reusing the basename of an audited
|
|
# declaration Ns.X — the spelling-vs-membership shape.
|
|
# The victim module, its namespace and X are all
|
|
# derived from this repo's own manifest below.
|
|
# EVERY ONE must be named in the gate's UNCLASSIFIED list. Asserting the
|
|
# gate merely failed is not enough: one shape surfacing would fail the run
|
|
# while the other three rode along unseen.
|
|
#
|
|
# One recompile of one leaf module, then the real Phase 2c. Run after a green
|
|
# check.sh.
|
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
set -uo pipefail
|
|
source ~/aeneas-toolchain/env.sh
|
|
HERE="$(cd "$(dirname "$0")" && pwd)"
|
|
AENEAS_LEAN="$AENEAS_HOME/backends/lean"
|
|
TIMEOUT="${LEAN_TIMEOUT:-900}"
|
|
export LEAN_MEM_MB="${LEAN_MEM_MB:-8192}"
|
|
FAILURES=0
|
|
SAFE_EXIT=0
|
|
STASH="$(mktemp -d)"
|
|
|
|
# Pick the victim from this repo's own manifest rather than naming a module:
|
|
# the four forks do not share a corpus (dalek and anza carry Proofs.Basic;
|
|
# risc0 and betrusted do not), and a hard-coded name would silently pick
|
|
# nothing on half of them.
|
|
#
|
|
# The victim must be (a) in check.sh's PROOFS — attacking an unmanifested module
|
|
# would test the dead-file gate instead of the walker; (b) NOT an inventory
|
|
# driver or the audit driver — those are the instruments, and mutating one would
|
|
# be attacking the measuring device; (c) imported by no other manifest module,
|
|
# so one recompile cannot invalidate a second module's artifact. Of those, take
|
|
# the smallest, because this test compiles it twice.
|
|
#
|
|
# Note for anyone re-deriving this: the inventory drivers import the whole
|
|
# corpus, so they must be excluded from the set of importers as well as from the
|
|
# candidates. Leave them in and every module looks imported, no leaf is found,
|
|
# and the test silently has no victim at all.
|
|
MAIN=$(sed -n '/^PROOFS=(/,/^)/p' "$HERE/check.sh" \
|
|
| sed 's/#.*//; s/PROOFS=(//; s/)//' | tr -s ' \t' '\n' | sed '/^$/d')
|
|
SEARCHERS=$(for m in $MAIN; do case $m in Inventory*|Audit) ;; *) echo "$HERE/Proofs/$m.lean";; esac; done)
|
|
VICTIM_MOD=""; _best=999999
|
|
for m in $MAIN; do
|
|
case $m in Inventory*|Audit) continue;; esac
|
|
grep -q "^import Proofs\.$m\$" $SEARCHERS 2>/dev/null && continue
|
|
n=$(wc -l < "$HERE/Proofs/$m.lean")
|
|
if [ "$n" -lt "$_best" ]; then _best=$n; VICTIM_MOD=$m; fi
|
|
done
|
|
if [ -z "$VICTIM_MOD" ]; then
|
|
echo "FATAL: no manifested leaf module to attack — the corpus shape changed."; exit 1
|
|
fi
|
|
VICTIM_NS=$(grep -m1 '^namespace ' "$HERE/Proofs/$VICTIM_MOD.lean" | awk '{print $2}')
|
|
COLLIDE=$(grep -m1 '^theorem ' "$HERE/Proofs/$VICTIM_MOD.lean" | awk '{print $2}')
|
|
if [ -z "$VICTIM_NS" ] || [ -z "$COLLIDE" ]; then
|
|
echo "FATAL: $VICTIM_MOD has no namespace or no theorem to collide with."; exit 1
|
|
fi
|
|
|
|
cleanup() {
|
|
[ -f "$STASH/victim" ] && cp "$STASH/victim" "$HERE/Proofs/$VICTIM_MOD.lean"
|
|
[ "$SAFE_EXIT" -eq 1 ] || rm -f "$HERE/Proofs/$VICTIM_MOD.olean" "$HERE/Proofs/$VICTIM_MOD.ilean"
|
|
rm -rf "$STASH"
|
|
}
|
|
trap cleanup EXIT INT TERM
|
|
cp "$HERE/Proofs/$VICTIM_MOD.lean" "$STASH/victim"
|
|
|
|
# Phase 2c lifted from the shipping button. `set -euo pipefail` is copied from
|
|
# check.sh:32 deliberately — the phase does its Lean work in subshells, and
|
|
# without -e a failing subshell is masked by the next echo. (That exact mistake
|
|
# produced a green report for a red condition while selftest-tiers.sh was being
|
|
# built; do not "simplify" it away.)
|
|
lift() {
|
|
{ echo 'set -euo pipefail'
|
|
echo 'source ~/aeneas-toolchain/env.sh'
|
|
echo "HERE=\"$HERE\""
|
|
echo 'AENEAS_LEAN="$AENEAS_HOME/backends/lean"'
|
|
echo "TIMEOUT=$TIMEOUT"
|
|
# PROOFS, and the scalar manifest the coverage check consults. Both are
|
|
# lifted VERBATIM rather than re-derived here: re-deriving would let this
|
|
# test's idea of the manifest drift away from the button's, and then the
|
|
# test would be checking its own opinion instead of the shipping one.
|
|
sed -n '/^PROOFS=(/,/^)/p;/^SCALAR_SH=/p;/^SCALAR_MANIFEST=/p' "$HERE/check.sh"
|
|
awk '/^# ── Phase 2c/{f=1} f&&/^# ── (Phase 3|Phases end)/{exit} f{print}' "$HERE/check.sh"
|
|
} > "$STASH/p2c.sh"
|
|
for want in 'Phase 2c' 'inventory_gate.sh' 'PROOFS=(' 'SCALAR_MANIFEST='; do
|
|
grep -qF "$want" "$STASH/p2c.sh" || {
|
|
echo "FATAL: the lifted driver has no '$want' — check.sh's phase markers moved."; exit 1; }
|
|
done
|
|
}
|
|
lift
|
|
|
|
recompile() {
|
|
( cd "$AENEAS_LEAN" && lake env bash -c "
|
|
set -uo pipefail
|
|
cd '$HERE/gen' && export LEAN_PATH=\"\$LEAN_PATH:\$PWD:$HERE\"
|
|
cd '$HERE'
|
|
LEAN_TIMEOUT=$TIMEOUT '$HERE/lean-guard' Proofs/$VICTIM_MOD.lean
|
|
" ) >/dev/null 2>&1
|
|
}
|
|
|
|
run_phase() { bash "$STASH/p2c.sh" 2>&1; }
|
|
|
|
echo "=== selftest-shapes: can a declaration hide from the walker? ==="
|
|
echo " victim: Proofs/$VICTIM_MOD.lean ($_best lines), namespace $VICTIM_NS"
|
|
echo " colliding basename: $VICTIM_NS.$COLLIDE"
|
|
|
|
if ! recompile; then
|
|
echo "FATAL: Proofs/$VICTIM_MOD.lean does not compile before any attack — fix the tree first."
|
|
exit 1
|
|
fi
|
|
|
|
# ── 0. control ─────────────────────────────────────────────────────────────
|
|
OUT=$(run_phase); RC=$?
|
|
if [ "$RC" -eq 0 ] && grep -qF "environment == allowlist" <<<"$OUT"; then
|
|
echo " ✓ control: the untouched tree's observation equals the allowlist"
|
|
else
|
|
echo " ✗ control: exit $RC — the tree is not green before the attack"
|
|
tail -8 <<<"$OUT" | sed 's/^/ /'
|
|
FAILURES=$((FAILURES+1))
|
|
fi
|
|
|
|
# ── 1. the four shapes ─────────────────────────────────────────────────────
|
|
python3 - "$HERE/Proofs/$VICTIM_MOD.lean" "$VICTIM_NS" "$COLLIDE" <<'PY'
|
|
import sys
|
|
f, ns, collide = sys.argv[1], sys.argv[2], sys.argv[3]
|
|
s = open(f).read()
|
|
end = f"\nend {ns}"
|
|
assert s.rstrip().endswith(end.strip()), "victim does not end by closing its namespace"
|
|
i = s.rstrip().rfind(end)
|
|
inject = f"""
|
|
-- selftest-shapes: four declaration shapes that defeated a source-regex
|
|
-- enumerator in ltl-accumulator-verified. Each must surface in the walker's
|
|
-- observation. Removed by this script's cleanup.
|
|
@[simp] theorem shape_attributed : True := trivial
|
|
|
|
private theorem shape_private : True := trivial
|
|
|
|
instance shape_instance : Nonempty Nat := ⟨0⟩
|
|
|
|
namespace Hidden
|
|
theorem {collide} : True := trivial
|
|
end Hidden
|
|
"""
|
|
open(f, "w").write(s[:i] + inject + s[i:])
|
|
PY
|
|
|
|
if ! recompile; then
|
|
echo " ✗ case 1: the module with the four shapes did not compile (case is vacuous)"
|
|
FAILURES=$((FAILURES+1))
|
|
else
|
|
OUT=$(run_phase); RC=$?
|
|
if [ "$RC" -eq 0 ]; then
|
|
echo " ✗ case 1: FOUR hidden declarations and the inventory passed — the walker is blind"
|
|
FAILURES=$((FAILURES+1))
|
|
else
|
|
# Not "did it fail" but "did it see each one". A gate that reports three of
|
|
# four still lets one declaration ride along inside an audited module.
|
|
MISSED=0
|
|
for n in "$VICTIM_NS.shape_attributed" "shape_private" \
|
|
"$VICTIM_NS.shape_instance" "$VICTIM_NS.Hidden.$COLLIDE"; do
|
|
if grep -qF "UNCLASSIFIED" <<<"$OUT" && grep -F "UNCLASSIFIED" <<<"$OUT" | grep -qF "$n"; then
|
|
echo " · seen: $n"
|
|
else
|
|
echo " · NOT SEEN: $n — this shape hides from the walker"
|
|
MISSED=1
|
|
fi
|
|
done
|
|
if [ "$MISSED" -eq 0 ]; then
|
|
echo " ✓ case 1: all four evasive shapes surfaced as UNCLASSIFIED"
|
|
else
|
|
echo " ✗ case 1: the gate failed, but not for every shape"
|
|
grep -F "UNCLASSIFIED" <<<"$OUT" | head -8 | sed 's/^/ /'
|
|
FAILURES=$((FAILURES+1))
|
|
fi
|
|
fi
|
|
fi
|
|
|
|
cp "$STASH/victim" "$HERE/Proofs/$VICTIM_MOD.lean"
|
|
if recompile; then
|
|
SAFE_EXIT=1
|
|
else
|
|
echo " ✗ restore: the ORIGINAL module no longer compiles — tree left for inspection"
|
|
FAILURES=$((FAILURES+1))
|
|
fi
|
|
|
|
OUT=$(run_phase); RC=$?
|
|
if [ "$RC" -eq 0 ] && grep -qF "environment == allowlist" <<<"$OUT"; then
|
|
echo " ✓ restored: the observation equals the allowlist again"
|
|
else
|
|
echo " ✗ restored: the tree did not come back green (exit $RC)"
|
|
FAILURES=$((FAILURES+1))
|
|
fi
|
|
|
|
echo ""
|
|
if [ "$FAILURES" -eq 0 ]; then
|
|
echo "SELFTEST PASSED — no declaration shape tested here can hide inside an"
|
|
echo "audited module: the walker reports each one by name."
|
|
exit 0
|
|
fi
|
|
echo "SELFTEST FAILED: $FAILURES case(s) did not behave as claimed."
|
|
exit 1
|