Files
zzet--gortex/scripts/verify-static-elf.sh
Andrey Kumanyaev 1f7bf6ea48 release: link linux binaries statically so they run on any distro
The linux release was dynamically linked despite the README promising a
single static binary with no dependency chain. goreleaser-cross:v1.26 is
Ubuntu 24.04 / GCC 13.3, so the artifacts carried that host's floor —
GLIBCXX_3.4.32 and GLIBC_2.34 — and refused to start anywhere older:

  gortex: /lib/x86_64-linux-gnu/libstdc++.so.6:
          version `GLIBCXX_3.4.32' not found (required by gortex)

Debian 12 caps at GLIBCXX_3.4.30 and Ubuntu 22.04 at 3.4.29, so in practice
the binary only ran on Ubuntu 24.04+/Debian 13+. libstdc++ is in the link at
all because some tree-sitter grammars ship C++ external scanners. Windows
already linked -static; only linux was left dynamic.

Link -static with netgo, osusergo and static_link, which between them remove
every dlopen the binary would otherwise perform at runtime.

verify-static-elf.sh asserts no DT_NEEDED, no PT_INTERP and no symbol-version
requirements. It runs as a goreleaser build post-hook rather than a workflow
step because `goreleaser release` builds and publishes in the same run — a
check after it would only ever be a post-mortem on an already-published
artifact. The release job re-asserts it on the packaged tarballs and then
proves it by execution on debian:11 and alpine.
2026-07-26 17:23:21 +02:00

67 lines
2.8 KiB
Bash
Executable File

#!/bin/sh
# Guard for the linux "not actually a static binary" regression.
#
# README promises a "single static binary ... no dependency chain", but the
# linux builds linked libstdc++/libgcc/libc dynamically. That stamped them with
# the goreleaser-cross image's symbol-version floor (Ubuntu 24.04, GCC 13.3 ->
# GLIBCXX_3.4.32 and GLIBC_2.34), so the binary refused to even start anywhere
# older:
#
# gortex: /lib/x86_64-linux-gnu/libstdc++.so.6:
# version `GLIBCXX_3.4.32' not found (required by gortex)
#
# Debian 12 caps at GLIBCXX_3.4.30 and Ubuntu 22.04 at 3.4.29, so in practice
# the release only ran on Ubuntu 24.04+/Debian 13+. libstdc++ is in the link at
# all because some tree-sitter grammars ship C++ external scanners.
#
# .goreleaser.yml now links -static (with the netgo/osusergo tags so nothing
# needs a runtime dlopen of libnss_*). This script is the gate that keeps it
# that way: it runs as a goreleaser build post-hook, before any artifact is
# archived or published, and in CI on every change.
#
# Usage: verify-static-elf.sh <elf-binary>
set -eu
bin="${1:?usage: verify-static-elf.sh <elf-binary>}"
[ -f "$bin" ] || { echo "FATAL: $bin does not exist" >&2; exit 1; }
# GNU readelf reads foreign-architecture ELF fine, so one host readelf covers
# both the amd64 and the cross-linked arm64 binary. Never skip the check when
# it is missing — silently skipping is exactly how the broken shape shipped.
readelf=""
for cand in readelf llvm-readelf eu-readelf; do
if command -v "$cand" >/dev/null 2>&1; then readelf="$cand"; break; fi
done
[ -n "$readelf" ] || { echo "FATAL: no readelf available to verify $bin" >&2; exit 1; }
fail=0
# 1. A statically linked binary has no DT_NEEDED entries at all.
needed="$("$readelf" -d "$bin" 2>/dev/null | grep NEEDED || true)"
if [ -n "$needed" ]; then
echo "FATAL: $bin links shared libraries — static link incomplete:" >&2
echo "$needed" >&2
fail=1
fi
# 2. ... and no PT_INTERP segment, i.e. no dynamic loader is invoked. A binary
# can lose its NEEDED entries and still be dynamically loaded.
if "$readelf" -l "$bin" 2>/dev/null | grep -q INTERP; then
echo "FATAL: $bin has a PT_INTERP segment — it is not statically linked" >&2
fail=1
fi
# 3. ... and requires no versioned symbols. This is the check that maps
# directly to the reported failure: a partially static link can drop the
# NEEDED entry while still versioning symbols against the build host.
vers="$("$readelf" -V "$bin" 2>/dev/null | grep -oE 'GLIBCXX_[0-9.]+|GLIBC_[0-9.]+|CXXABI_[0-9.]+' | sort -u || true)"
if [ -n "$vers" ]; then
echo "FATAL: $bin carries symbol-version requirements (a distro floor):" >&2
echo "$vers" | sed 's/^/ /' >&2
fail=1
fi
[ "$fail" -eq 0 ] || exit 1
echo "ok: $bin is statically linked (no NEEDED, no PT_INTERP, no glibc/libstdc++ floor)"