#!/bin/sh # iolite bootstrap - acquire the storage capability probe, then run it. # # curl -sSL https://iolite.st | sh # curl -sSL https://iolite.st | sh -s -- extended # a longer profile # curl -sSL https://iolite.st | sh -s -- --install-only # acquire, don't run # # Acquisition order, and why: # # 1. An existing binary that works is used as it stands. Re-running the # one-liner on a host that already has iolite must not silently replace # a binary someone deliberately put there. # 2. Otherwise, if a working C compiler is present, the source is fetched # and built here. A local build matches the local libc and needs no # trust in someone else's toolchain. # 3. Otherwise the prebuilt static binary is fetched. This is the fallback, # not the default: it exists so that a host with no toolchain at all can # still be measured, which is a common shape among small guests. # # Everything is verified against a published SHA-256 before it is used. HTTPS # already provides integrity; the checksum is what lets someone who fetched # over a proxy, or saved the file for later, confirm they have the same bytes. # # POSIX sh throughout - no bashisms, no external tools beyond a downloader and # a hasher, both of which are probed for rather than assumed. set -eu BASE="${IOLITE_URL:-https://iolite.st}" # The whole program lives inside main() so that `sh` reads this file to EOF # before executing any of it. Piped into a shell, a script that executes as it # is read can be truncated mid-run by a dropped connection, and anything it # spawns can eat the unread remainder from stdin. main() { INSTALL_ONLY=0 ARGS="" for a in "$@"; do case "$a" in --install-only) INSTALL_ONLY=1 ;; *) ARGS="$ARGS $a" ;; esac done say "iolite bootstrap" [ "$(uname -s)" = "Linux" ] || warn "not Linux: the probe reads /proc and cgroup state and will report less" need_downloader BIN=$(find_working_binary || true) if [ -n "$BIN" ]; then say "using existing $BIN ($("$BIN" --version 2>/dev/null | head -1))" else CC=$(find_working_compiler || true) if [ -n "$CC" ]; then say "compiler: $CC ($("$CC" --version 2>/dev/null | head -1))" BIN=$(build_from_source "$CC" || true) [ -n "$BIN" ] || warn "build failed, falling back to the prebuilt binary" else say "no C compiler found" fi [ -n "$BIN" ] || BIN=$(fetch_prebuilt) [ -n "$BIN" ] || die "no usable binary could be acquired" fi "$BIN" --selftest >/dev/null 2>&1 || die "instrument self-test failed - do not trust results from this binary" say "self-test passed" if [ "$INSTALL_ONLY" -eq 1 ]; then say "ready at $BIN - run it with: $BIN" return 0 fi # Say what is about to happen before it happens. The probe holds the volume # busy for the whole profile and writes a working set larger than RAM; that # is not something to discover halfway through. avail=$(df -Pk . | awk 'NR==2{print int($4/1048576)}') say "working directory $(pwd) - ${avail} GiB free" [ "$avail" -ge 6 ] || die "need ~6 GiB free to size a working set above RAM" say "running: $BIN$ARGS" echo >&2 # stdin is closed: the probe never reads it, and leaving it attached to a # pipe that is still delivering this script invites the shell and the # child to race for the same bytes. # shellcheck disable=SC2086 exec "$BIN" $ARGS &2; } warn() { echo "iolite: warning: $*" >&2; } die() { echo "iolite: $*" >&2; exit 2; } DL="" need_downloader() { if command -v curl >/dev/null 2>&1; then DL=curl elif command -v wget >/dev/null 2>&1; then DL=wget else die "neither curl nor wget is available" fi } fetch() { # url dest case "$DL" in curl) curl -fsSL --retry 3 --retry-delay 2 -o "$2" "$1" ;; wget) wget -q --tries=3 -O "$2" "$1" ;; esac } sha256_of() { # path -> hex on stdout, empty if no hasher exists if command -v sha256sum >/dev/null 2>&1; then sha256sum "$1" | cut -d' ' -f1 elif command -v shasum >/dev/null 2>&1; then shasum -a 256 "$1" | cut -d' ' -f1 elif command -v openssl >/dev/null 2>&1; then openssl dgst -sha256 "$1" | sed 's/.*= *//' fi } # Fetch $1 to $2 and check it against $1.sha256. A missing hasher is a warning # rather than a failure: the transport is already authenticated, and refusing # to run on a minimal image that lacks coreutils would trade a real capability # for a redundant check. fetch_verified() { # url dest label fetch "$1" "$2" || die "could not download $3 from $1" sums=$(mktemp) || die "cannot create a temporary file" if fetch "$1.sha256" "$sums" 2>/dev/null; then want=$(cut -d' ' -f1 "$sums"); rm -f "$sums" got=$(sha256_of "$2") if [ -z "$got" ]; then warn "no sha256 tool available - $3 not verified" elif [ "$want" != "$got" ]; then rm -f "$2" die "checksum mismatch on $3 - expected $want, got $got" else say "$3 verified ($(echo "$got" | cut -c1-16)...)" fi else rm -f "$sums" warn "no published checksum for $3" fi } # A binary counts as available only if it actually runs here. A file left by a # different architecture, a partial download, or a build against a libc this # host does not have all present as an executable that is present and unusable. find_working_binary() { for cand in ${IOLITE_BIN:-} ./iolite "$(command -v iolite 2>/dev/null || true)"; do [ -n "$cand" ] && [ -x "$cand" ] || continue "$cand" --version 2>/dev/null | head -1 | grep -q '^iolite ' || continue echo "$cand"; return 0 done return 1 } # Likewise a compiler counts only if it can link this program's actual # requirements. `cc` exists on images that ship no headers and no pthread, and # discovering that after downloading the source is a worse failure than # discovering it now. find_working_compiler() { probe=$(mktemp -d) || return 1 cat > "$probe/t.c" <<'EOF' #include #include static void *f(void *p) { return p; } int main(void) { pthread_t t; pthread_create(&t, 0, f, 0); pthread_join(t, 0); return 0; } EOF for c in ${CC:-} cc gcc clang tcc; do [ -n "$c" ] && command -v "$c" >/dev/null 2>&1 || continue if "$c" -O2 -pthread -o "$probe/t" "$probe/t.c" >/dev/null 2>&1; then rm -rf "$probe"; echo "$c"; return 0 fi done rm -rf "$probe"; return 1 } # -D_FORTIFY_SOURCE=2 is not decoration and belongs in every recipe: this # program once resolved a path into a fixed buffer that glibc may write past. # An unfortified build ran straight through it; a hardened one aborted before # its first line of output, which is the failure you want. -U first, because a # distro that already defines it would otherwise turn the redefinition into a # warning on every build. # # Static is tried first so the result can be copied to a host with no # toolchain, and dropped where static libc is unavailable. build_from_source() { # cc -> path on stdout cc="$1" fetch_verified "$BASE/iolite.c" ./iolite.c "source" >&2 say "building" if "$cc" -O2 -U_FORTIFY_SOURCE -D_FORTIFY_SOURCE=2 -static -pthread -o ./iolite ./iolite.c 2>/dev/null; then say "built ./iolite (static)" elif "$cc" -O2 -U_FORTIFY_SOURCE -D_FORTIFY_SOURCE=2 -pthread -o ./iolite ./iolite.c 2>/dev/null; then say "built ./iolite (dynamic - static libc unavailable)" else return 1 fi echo ./iolite } # The published binary is x86-64 Linux, statically linked. On any other # architecture the source path is the only one that works, so say that plainly # rather than downloading something that cannot execute. fetch_prebuilt() { case "$(uname -m)" in x86_64|amd64) ;; *) die "no prebuilt binary for $(uname -m); install a C compiler and re-run, and the source will be built here" ;; esac fetch_verified "$BASE/iolite" ./iolite "binary" >&2 chmod +x ./iolite echo ./iolite } main "$@"