mirror of
https://github.com/NVIDIA/OpenShell.git
synced 2026-10-03 16:11:17 +08:00
The supervisor binary runs inside sandbox images whose libc and glibc version are unknown at build time, so it must be statically linked. Add SUPERVISOR_LIBC to select between the default musl variant and a new glibc-static variant that builds the GNU target with +crt-static. glibc-static has no cross-compile path: zig cc accepts -static for *-linux-gnu targets and emits a dynamically linked binary anyway. The staging script therefore refuses a cross-arch request for that variant rather than silently degrading linkage, and requires a native per-architecture build. Add verify-static-binary.sh, run after every supervisor build in both the staging script and CI so linkage cannot regress unnoticed for either variant. It inspects via readelf (or greadelf/llvm-readelf) and fails closed rather than trusting the tool's exit status: every inspection must produce no diagnostics, the input must be an executable ELF (ET_EXEC, or ET_DYN with DF_1_PIE) whose PT_LOAD segments all lie within the file, whose dynamic table agrees with PT_DYNAMIC, and which carries no PT_INTERP and no DT_NEEDED. That rejects a dynamically linked, truncated, corrupt, non-ELF, or shared-object input that naive parsing would misread as static. Hosts without any inspector (e.g. macOS, which ships no binutils) skip with a warning; Linux, including CI, requires one and fails closed. No image or release workflow builds the glibc-static variant, so add a dedicated supervisor-static-validate workflow that builds it on both architectures and runs the verifier. rust-native-build.yml uses self-hosted runners, which reject pull_request-triggered jobs, so it validates in the merge queue and on pushes to main that touch the build inputs, plus a nightly schedule, so the GNU + crt-static build branch cannot regress unnoticed. The default is unchanged, so image, release, and CI behavior is identical. Selecting glibc-static statically links LGPL glibc into a redistributed binary, which is why it is opt-in. Signed-off-by: Mrunal Patel <mrunalp@gmail.com> Co-authored-by: Mrunal Patel <mrunalp@gmail.com>
214 lines
8.8 KiB
Bash
Executable File
214 lines
8.8 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.
|
|
#
|
|
# The supervisor is executed from inside arbitrary sandbox 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. Both supported supervisor libc variants (musl
|
|
# and glibc-static) must therefore produce a static binary.
|
|
#
|
|
# 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 the Linux supervisor 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"
|