mirror of
https://github.com/saymrwulf/betrusted-ed25519-verified.git
synced 2026-09-04 20:24:08 +00:00
Round-9 swept all ten instruments per fork instead of only the two buttons,
and found selftest-axgate.sh red in all four. Root cause, identical to the
KERNLOG defect fixed the day before: a self-test lifts one phase out of
check.sh and supplies its variables from a HAND-WRITTEN preamble, and the
phase later grew a dependency nobody told the preamble about.
Phase 2b stopped globbing Proofs/*.olean and started reading $PROOFS by
membership. Bash does not error on an unset array under `set -u` — it
expands to nothing — so the gate silently received
expected := [".olean"]
one entry with an empty name, and rejected the baseline via its own
fail-closed absence check. Exit code right, reason wrong.
· lift-guard.sh (new, pinned): derives every variable the lifted payload
reads, subtracts what the driver defines, and fails AT LIFT TIME naming
the remainder. Wired into all five lifting self-tests; selftest-shapes.sh
drops its inline copy. One implementation, not five, since drifting out
of sync is the entire failure mode.
· selftest-axgate.sh: lifts PROOFS verbatim, and its case 2 now poisons a
MANIFESTED leaf module rather than adding a stray file. The stray-file
attack was a no-op against membership semantics — that is the dead-file
gate's job, proven by selftest-harness case 8 — and it is also the
weaker attack, since adding files to Proofs/ must clear a gate that
editing an already-manifested module does not.
· Phase 2c's accounting identity moves behind its own marker with its own
ACCTFAIL verdict, so the phase is liftable by construction rather than
by a self-test knowing where to stop. Truncating the lift range alone
would have dropped Phase 2c's own verdict and left the test unable to fail.
· The other four lifting self-tests assert on the PAYLOAD rather than the
assembled driver, so a marker in the preamble cannot satisfy a check
meant to prove the lift landed.
Certified: 8/8 self-tests green in all four forks (~212 assertions, none
failing); check.sh bytes unchanged and still matching the pin written before
the 3h08m round-9 sweep, whose 36 GREEN button rows therefore stand.
--audit-only re-run against the new HARNESS.sha256 in every fork.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
254 lines
12 KiB
Bash
Executable file
254 lines
12 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() {
|
|
# THE LIFT RANGE STOPS AT THE ACCOUNTING IDENTITY, and that boundary is the
|
|
# fix for round-7 finding F5 (Claude). Phase 2c grew an accounting block that
|
|
# reads $KERNLOG — a file created in Phase 2b, one phase ABOVE the lift. Under
|
|
# `set -u` the driver aborted on its first expansion, so this self-test could
|
|
# not pass on any fork from the moment that block was added. It failed loudly
|
|
# rather than passing vacuously, which is why it was a red test and not a
|
|
# false green; but it meant the four-shapes property went unverified.
|
|
#
|
|
# This test attacks the WALKER — can a declaration hide from the inventory —
|
|
# and the accounting identity is a separate property with its own coverage.
|
|
# Lifting it here would only drag in Phase 2b's state.
|
|
awk '/^# ── Phase 2c/{f=1} f&&/^# ── (Phase 2c-accounting|Phase 3|Phases end)/{exit} f{print}' \
|
|
"$HERE/check.sh" > "$STASH/payload.sh"
|
|
|
|
{ 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"
|
|
cat "$STASH/payload.sh"
|
|
} > "$STASH/p2c.sh"
|
|
|
|
# Guard on the PAYLOAD, not the concatenation. The previous version grepped
|
|
# the assembled file, so a marker appearing in the preamble or in a lifted
|
|
# definition would have satisfied it — the same shape as the line-count check
|
|
# that an empty driver once passed because the CERTS array padded it.
|
|
for want in 'Phase 2c' 'inventory_gate.sh'; do
|
|
grep -qF "$want" "$STASH/payload.sh" || {
|
|
echo "FATAL: the lifted PAYLOAD has no '$want' — check.sh's phase markers moved."; exit 1; }
|
|
done
|
|
for want in 'PROOFS=(' 'SCALAR_MANIFEST='; do
|
|
grep -qF "$want" "$STASH/p2c.sh" || {
|
|
echo "FATAL: the lift carries no '$want' — a definition the phase needs is missing."; exit 1; }
|
|
done
|
|
|
|
# AND THE DURABLE GUARD: every variable the payload READS must be one the
|
|
# driver DEFINES. Derived mechanically rather than from a hand-kept list,
|
|
# because a hand-kept list is exactly what failed — the phase grew a
|
|
# dependency nobody thought to add. Shared with the other four lifting
|
|
# self-tests: ONE implementation, pinned, rather than five copies of the
|
|
# thing whose whole failure mode is drifting out of sync.
|
|
"$HERE/lift-guard.sh" "$STASH/payload.sh" "$STASH/p2c.sh" "check.sh Phase 2c" || exit 1
|
|
}
|
|
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
|