Files
OpenShell/tasks/scripts/verify-static-binary.sh
Emilien Macchi 99ed6a9df0 chore(build): remove stale static-supervisor leftovers (#3520)
The opt-in glibc-static supervisor variant from #2682 is gone: the Nix
release builds (#2977) removed it from CI and the supervisor Dockerfile,
and the RFC 0012 sandbox split (#2942) removed it from the staging script.
The split also moved the binary that runs inside workload images to
openshell-sandbox; the supervisor now runs from its own image and is
dynamically linked. A few places still describe the old model:

- skills/debug-openshell-cluster told operators to check that
  supervisor_image contains a static /openshell-supervisor. Ask instead for
  a supervisor from the matching release whose loader and shared libraries
  are available inside that image, and give a `--version` check that shows
  both. The static requirement for /openshell-sandbox is unchanged.
- verify-static-binary.sh justified its check with the supervisor and the
  glibc-static variant. Describe the property it enforces instead, with
  the musl sandbox runtime as the main example.
- stage-prebuilt-binaries.sh kept an unreachable gnu-static case in
  target_triple.

No behavior change: resolve_component only ever selects gnu or musl.

Signed-off-by: Emilien Macchi <emacchi@redhat.com>
2026-09-22 00:19:37 +00:00

215 lines
8.9 KiB
Bash
Executable File

#!/usr/bin/env bash
# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0
set -euo pipefail
# Verify a binary is a genuine, complete, fully static executable.
#
# Callers pass binaries that must run without a dynamic loader, such as the
# musl sandbox runtime (openshell-sandbox). It is executed from inside
# arbitrary workload images (Docker extraction, Podman image volumes, the
# Kubernetes copy-self path), so any dynamic linkage breaks it on musl-based
# images and on images whose glibc is older than the build host's. Other
# callers include the release prover and e2e fixtures.
#
# This check exists because the failure is silent: `zig cc` accepts `-static`
# for `*-linux-gnu` targets and emits a dynamically linked binary anyway, so a
# toolchain change can quietly downgrade linkage without failing the build.
#
# The verifier must also fail closed on malformed input; readelf can exit 0 on a
# damaged ELF, which naive parsing would read as "no interpreter, no
# dependencies". The checks below therefore require, for each binary:
# * an executable ELF: ET_EXEC (classic static) or ET_DYN (static-PIE);
# * a fully readable ELF header, program header table, and dynamic section
# (readelf must exit 0 AND emit no diagnostics — see readelf_strict);
# * at least one PT_LOAD segment, every PT_LOAD contained within the file
# (catches truncation that readelf does not otherwise report);
# * no PT_INTERP and no DT_NEEDED (the actual static-linkage properties);
# * for ET_DYN, the DF_1_PIE flag, which a static-PIE executable sets and a
# shared library does not.
#
# Accepts both classic static and static-PIE binaries. static-PIE keeps a
# PT_DYNAMIC segment for self-relocation, so linkage is judged by the absence of
# PT_INTERP and DT_NEEDED, not by the absence of a dynamic section.
usage() {
echo "Usage: verify-static-binary.sh <binary> [binary ...]" >&2
}
if [[ $# -lt 1 ]]; then
usage
exit 2
fi
# Resolve a readelf-compatible inspector. macOS ships none of these; Homebrew
# binutils provides `greadelf` and LLVM provides `llvm-readelf`, both of which
# emit GNU-style output this script parses. Prefer GNU readelf, then greadelf,
# then llvm-readelf.
READELF=""
for candidate in readelf greadelf llvm-readelf; do
if command -v "$candidate" >/dev/null 2>&1; then
READELF=$candidate
break
fi
done
if [[ -z $READELF ]]; then
host_os=""
command -v uname >/dev/null 2>&1 && host_os=$(uname -s 2>/dev/null || true)
# Skip only on a host positively identified as non-Linux — e.g. a macOS dev
# cross-building a Linux musl binary via cargo-zigbuild, where mise installs
# no binutils. Linux (including CI), or any host whose OS cannot be determined,
# fails closed so a missing inspector never silently passes. Static linkage is
# still enforced in CI, which runs on Linux.
if [[ "$host_os" == "Linux" || -z $host_os ]]; then
echo "error: readelf (or greadelf/llvm-readelf) is required to inspect binary linkage" >&2
exit 2
fi
echo "warning: no readelf/greadelf/llvm-readelf found on ${host_os}; skipping static-linkage verification." >&2
echo " install GNU binutils (greadelf) or LLVM (llvm-readelf) to verify locally; CI enforces it on Linux." >&2
exit 0
fi
# Explicit template: BSD/macOS mktemp requires one, GNU mktemp accepts it.
readelf_err=$(mktemp "${TMPDIR:-/tmp}/verify-static-binary.XXXXXXXX")
trap 'rm -f "$readelf_err"' EXIT
# Run readelf and print its stdout. Fails (returns non-zero) if readelf exits
# non-zero OR writes anything to stderr, leaving the diagnostics in
# $readelf_err. readelf reports a truncated or corrupt ELF on stderr while still
# exiting 0, so the stderr check — not the exit code — is what makes a damaged
# file fail closed instead of reading as "no PT_INTERP, no DT_NEEDED".
readelf_strict() {
local out
out=$("$READELF" "$@" 2>"$readelf_err") || return 1
[[ -s "$readelf_err" ]] && return 1
printf '%s\n' "$out"
return 0
}
failed=0
for binary in "$@"; do
if [[ ! -f $binary ]]; then
echo "error: binary not found: $binary" >&2
failed=1
continue
fi
echo "==> Inspecting $binary"
# llvm-readelf rejects the `--` end-of-options marker that GNU readelf accepts,
# so make a leading-dash path safe for either tool by prefixing "./" instead.
case "$binary" in
-*) scan_path="./$binary" ;;
*) scan_path="$binary" ;;
esac
if command -v file >/dev/null 2>&1; then
file "$scan_path" || true
fi
# The ELF header and the full program header table must be readable. A
# truncated or non-ELF file makes readelf emit a diagnostic, which fails here
# instead of being misread as a static binary.
if ! headers=$(readelf_strict --wide --file-header --program-headers "$scan_path"); then
echo "error: $binary: unable to read a complete ELF (truncated, malformed, or not an ELF)" >&2
sed 's/^/ /' "$readelf_err" >&2 || true
failed=1
continue
fi
if grep -Eq '^[[:space:]]*Type:[[:space:]]+EXEC' <<<"$headers"; then
elf_type=EXEC
elif grep -Eq '^[[:space:]]*Type:[[:space:]]+DYN' <<<"$headers"; then
elf_type=DYN
else
echo "error: $binary is not an executable ELF (expected ET_EXEC or ET_DYN)" >&2
failed=1
continue
fi
# Every runnable ELF has at least one PT_LOAD segment. Its absence means the
# program header table was truncated or the input is not a program image.
if ! grep -qw 'LOAD' <<<"$headers"; then
echo "error: $binary has no PT_LOAD segment; it is truncated or not an executable" >&2
failed=1
continue
fi
# Every PT_LOAD must lie within the file. readelf can exit 0 with empty stderr
# on a file whose section headers were stripped even though a LOAD segment runs
# past EOF, so validate p_offset + p_filesz <= file size explicitly rather than
# trusting readelf to notice the truncation.
# wc -c is portable (GNU stat -c / BSD stat -f differ); arithmetic strips any
# leading whitespace BSD wc prints. The redirect also tolerates a '-' path.
file_size=$(( $(wc -c < "$binary") ))
load_past_eof=0
while read -r ph_type ph_off _ph_va _ph_pa ph_fsize _ph_rest; do
[[ "$ph_type" == "LOAD" ]] || continue
# ph_off and ph_fsize are hex (e.g. 0x6a8440); bash arithmetic parses 0x.
if (( ph_off + ph_fsize > file_size )); then
echo "error: $binary: PT_LOAD at ${ph_off} (filesz ${ph_fsize}) extends past end of file (${file_size} bytes); it is truncated" >&2
load_past_eof=1
fi
done <<<"$headers"
if (( load_past_eof )); then
failed=1
continue
fi
if grep -qw 'INTERP' <<<"$headers"; then
echo "error: $binary has a program interpreter (PT_INTERP); it is dynamically linked" >&2
grep -w -A1 'INTERP' <<<"$headers" >&2 || true
failed=1
continue
fi
# The dynamic section must also be fully readable. A classic static binary has
# none (readelf says so on stdout and exits cleanly, with no stderr); a
# static-PIE has one without any DT_NEEDED entries.
if ! dynamic=$(readelf_strict --wide --dynamic "$scan_path"); then
echo "error: $binary: unable to read the ELF dynamic section (truncated or malformed)" >&2
sed 's/^/ /' "$readelf_err" >&2 || true
failed=1
continue
fi
# Anchor the dynamic table to PT_DYNAMIC. GNU readelf --dynamic reads the
# SHT_DYNAMIC *section*, whose file offset can be pointed away from the real
# PT_DYNAMIC *segment* to hide DT_NEEDED entries (llvm-readelf warns on this;
# GNU does not). Require the section offset readelf used to match the
# PT_DYNAMIC segment offset from the program headers; fail closed on any
# disagreement. Compare numerically so 0x0b1da8 and 0xb1da8 are equal.
dyn_seg_off=$(awk '$1 == "DYNAMIC" { print $2; exit }' <<<"$headers")
dyn_sec_off=$(grep -oE 'Dynamic section at offset 0x[0-9a-fA-F]+' <<<"$dynamic" | grep -oE '0x[0-9a-fA-F]+' | head -1 || true)
if [[ -n $dyn_seg_off || -n $dyn_sec_off ]]; then
if [[ -z $dyn_seg_off || -z $dyn_sec_off ]] || (( dyn_seg_off != dyn_sec_off )); then
echo "error: $binary: dynamic table location mismatch (PT_DYNAMIC ${dyn_seg_off:-none}, section ${dyn_sec_off:-none}); malformed or tampered" >&2
failed=1
continue
fi
fi
if grep -qw 'NEEDED' <<<"$dynamic"; then
echo "error: $binary depends on shared libraries (DT_NEEDED); it is dynamically linked" >&2
grep -w 'NEEDED' <<<"$dynamic" >&2 || true
failed=1
continue
fi
# An ET_DYN static-PIE executable sets DT_FLAGS_1 DF_1_PIE; a shared library
# (also ET_DYN, and possibly without PT_INTERP/DT_NEEDED) does not. Require the
# flag so a .so cannot pass as a static executable.
if [[ "$elf_type" == "DYN" ]] && ! grep -E '\(FLAGS_1\)' <<<"$dynamic" | grep -qw 'PIE'; then
echo "error: $binary is an ET_DYN object without DF_1_PIE; it looks like a shared library, not a static-PIE executable" >&2
failed=1
continue
fi
echo "statically linked: no PT_INTERP, no DT_NEEDED"
done
exit "$failed"