diff --git a/qemu/lib.sh b/qemu/lib.sh index 6454160..0bd6e9a 100644 --- a/qemu/lib.sh +++ b/qemu/lib.sh @@ -22,15 +22,34 @@ qemu_get_busybox() { echo "FATAL: no pinned sha256 for busybox (expected $sha_file): refusing to build from an unverified binary" >&2 exit 1 } - if [ ! -f "$BB" ]; then - qemu_log "downloading $BB_URL" - curl --retry 3 --retry-delay 5 --retry-connrefused -fSL "$BB_URL" -o "$BB" - fi local want got want="$(cat "$sha_file")" - got="$(sha256sum "$BB" | awk '{print $1}')" - [ "$want" = "$got" ] || { echo "busybox sha256 mismatch: want $want got $got" >&2; exit 1; } - qemu_log "busybox sha256 verified" + if [ -f "$BB" ]; then + got="$(sha256sum "$BB" | awk '{print $1}')" + if [ "$want" = "$got" ]; then + qemu_log "busybox sha256 verified" + return + fi + qemu_log "cached busybox failed verification; fetching a clean copy" + fi + + local attempt tmp="${BB}.download.$$" + rm -f "$tmp" + for attempt in 1 2 3; do + qemu_log "downloading $BB_URL (attempt $attempt/3)" + if curl --retry 3 --retry-delay 5 --retry-connrefused -fSL "$BB_URL" -o "$tmp"; then + got="$(sha256sum "$tmp" | awk '{print $1}')" + if [ "$want" = "$got" ]; then + mv -f "$tmp" "$BB" + qemu_log "busybox sha256 verified" + return + fi + echo "busybox sha256 mismatch on attempt $attempt: want $want got $got" >&2 + fi + rm -f "$tmp" + done + echo "FATAL: could not fetch pinned busybox after 3 verified attempts" >&2 + exit 1 } # Stage the shared rootfs skeleton (qemu/rootfs/ + busybox) into $1. diff --git a/qemu/tests/busybox-fetch.sh b/qemu/tests/busybox-fetch.sh new file mode 100755 index 0000000..a20feca --- /dev/null +++ b/qemu/tests/busybox-fetch.sh @@ -0,0 +1,72 @@ +#!/usr/bin/env bash +set -euo pipefail + +TEST_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +QEMU_DIR="$(cd "$TEST_DIR/.." && pwd)" +SCRATCH="$(mktemp -d "${TMPDIR:-/tmp}/busybox-fetch.XXXXXX")" +trap 'rm -rf "$SCRATCH"' EXIT + +FAKE_BIN="$SCRATCH/bin" +FAKE_CURL_COUNT="$SCRATCH/curl-count" +mkdir -p "$FAKE_BIN" "$SCRATCH/qemu" +printf 'good payload' > "$SCRATCH/good" +sha256sum "$SCRATCH/good" | awk '{print $1}' > "$SCRATCH/qemu/busybox.sha256" + +FAKE_CURL="$FAKE_BIN/curl" +apply_fake_curl() { + chmod 0755 "$FAKE_CURL" + export PATH="$FAKE_BIN:$PATH" + export FAKE_CURL_COUNT FAKE_CURL_MODE +} + +# The path is resolved at runtime from this file's location. +# shellcheck disable=SC1091 +source "$QEMU_DIR/lib.sh" +QEMU_DIR="$SCRATCH/qemu" +# Read by qemu_get_busybox from the sourced helper. +# shellcheck disable=SC2034 +BB_URL="https://invalid.example/busybox" + +# These single-quoted lines are the literal source of the fake curl program. +# shellcheck disable=SC2016 +printf '%s\n' '#!/bin/sh' \ + 'count=0' \ + '[ ! -f "$FAKE_CURL_COUNT" ] || count=$(cat "$FAKE_CURL_COUNT")' \ + 'count=$((count + 1))' \ + 'printf "%s\n" "$count" > "$FAKE_CURL_COUNT"' \ + 'out=' \ + 'while [ "$#" -gt 0 ]; do' \ + ' if [ "$1" = "-o" ]; then shift; out="$1"; fi' \ + ' shift' \ + 'done' \ + '[ -n "$out" ] || exit 2' \ + 'if [ "$FAKE_CURL_MODE" = "flaky" ] && [ "$count" -gt 1 ]; then' \ + ' printf "good payload" > "$out"' \ + 'else' \ + ' printf "bad payload" > "$out"' \ + 'fi' > "$FAKE_CURL" + +FAKE_CURL_MODE=flaky +apply_fake_curl +OUT="$SCRATCH/flaky" qemu_get_busybox +test "$(cat "$FAKE_CURL_COUNT")" = 2 +test "$(sha256sum "$BB" | awk '{print $1}')" = "$(cat "$QEMU_DIR/busybox.sha256")" + +printf '0\n' > "$FAKE_CURL_COUNT" +FAKE_CURL_MODE=bad +apply_fake_curl +if (OUT="$SCRATCH/always-bad" qemu_get_busybox) >"$SCRATCH/failure.log" 2>&1; then + echo "FAIL: an unverified download was accepted" >&2 + exit 1 +fi +test "$(cat "$FAKE_CURL_COUNT")" = 3 +test ! -e "$SCRATCH/always-bad/busybox-armv7l" +grep -q 'after 3 verified attempts' "$SCRATCH/failure.log" + +printf '0\n' > "$FAKE_CURL_COUNT" +mkdir -p "$SCRATCH/cached" +cp "$SCRATCH/good" "$SCRATCH/cached/busybox-armv7l" +OUT="$SCRATCH/cached" qemu_get_busybox +test "$(cat "$FAKE_CURL_COUNT")" = 0 + +echo "busybox fetch tests passed"