Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
67 changes: 60 additions & 7 deletions .github/workflows/cross-arch-build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -147,35 +147,88 @@ jobs:
# gcc is GNU ld + musl, so it links cleanly. Same script the goreleaser release
# build runs (scripts/build-static.sh); goreleaser then packages it via the tool shim.
- name: Build statically (Alpine / musl)
run: bash scripts/build-static.sh
run: bash scripts/build-static.sh amd64

- name: Assert binary is statically linked
run: |
info=$(file ./build/seid)
info=$(file ./build/seid-amd64)
echo "$info"
echo "$info" | grep -q "statically linked" \
|| { echo "ERROR: seid is not statically linked"; exit 1; }

- name: Assert no dynamic dependencies
run: |
# On a true static binary, ldd reports "not a dynamic executable".
ldd ./build/seid 2>&1 | grep -qE "not a dynamic|statically linked" \
|| { echo "ERROR: seid has dynamic dependencies:"; ldd ./build/seid; exit 1; }
ldd ./build/seid-amd64 2>&1 | grep -qE "not a dynamic|statically linked" \
|| { echo "ERROR: seid has dynamic dependencies:"; ldd ./build/seid-amd64; exit 1; }

- name: Smoke test
run: ./build/seid version --long
run: ./build/seid-amd64 version --long

# Repeated boots: the gcc>=12 unwind b-tree crash killed ~70% of boots at the
# genesis wasm store, so a single boot (let alone `seid version`, which never
# touches the wasm VM) can pass on luck. See scripts/boot-smoke.sh.
- name: Boot smoke (repeated)
run: bash scripts/boot-smoke.sh ./build/seid 8
run: bash scripts/boot-smoke.sh ./build/seid-amd64 8

- name: Upload artifact for inspection
uses: actions/upload-artifact@v5
with:
name: seid-linux-amd64-static
path: ./build/seid
path: ./build/seid-amd64
retention-days: 14

linux-arm64-static:
name: "Linux ARM64 (static)"
runs-on: ubuntu-24.04-arm
needs: [static-paths]
# Same gating as the amd64 static job. This is the only place the arm64 release
# binary is booted: the release runner is amd64 and cannot execute it.
if: >-
!cancelled() && (
startsWith(github.base_ref, 'release/') ||
startsWith(github.ref, 'refs/heads/release/') ||
startsWith(github.event.merge_group.base_ref, 'refs/heads/release/') ||
needs.static-paths.outputs.changed == 'true' )
timeout-minutes: 45
steps:
- name: Checkout code
# See: https://github.com/actions/checkout/releases/tag/v7.0.0
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0

# Native arm64, so no binfmt or emulation is involved and the boots below run on
# real hardware. The release job builds the same commit under emulation; the build
# is reproducible, so the binary gated here is the one that ships.
- name: Build statically (Alpine / musl)
run: bash scripts/build-static.sh arm64

- name: Assert binary is statically linked
run: |
info=$(file ./build/seid-arm64)
echo "$info"
echo "$info" | grep -q "statically linked" \
|| { echo "ERROR: seid is not statically linked"; exit 1; }
echo "$info" | grep -q "aarch64" \
|| { echo "ERROR: seid is not an aarch64 binary"; exit 1; }

- name: Assert no dynamic dependencies
run: |
ldd ./build/seid-arm64 2>&1 | grep -qE "not a dynamic|statically linked" \
|| { echo "ERROR: seid has dynamic dependencies:"; ldd ./build/seid-arm64; exit 1; }

- name: Smoke test
run: ./build/seid-arm64 version --long

# The gcc>=12 unwind b-tree crash reproduces on arm64 too: an unpinned build
# SIGSEGVs on the first boot under RAYON_NUM_THREADS=1, which boot-smoke pins.
- name: Boot smoke (repeated)
run: bash scripts/boot-smoke.sh ./build/seid-arm64 8

- name: Upload artifact for inspection
uses: actions/upload-artifact@v5
with:
name: seid-linux-arm64-static
path: ./build/seid-arm64
retention-days: 14
macos-arm64:
name: "macOS ARM64"
Expand Down
25 changes: 20 additions & 5 deletions .goreleaser.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,24 @@ project_name: sei-chain
before:
hooks:
- go mod download
# build-static.sh self-verifies (libwasmvm archives present + output is static).
- bash scripts/build-static.sh
# The release runner is amd64, so building linux/arm64 needs binfmt registered.
# Pinned by digest: this runs privileged on the release runner, which holds the
# credentials that publish the binaries.
- docker run --privileged --rm tonistiigi/binfmt:qemu-v8.1.5@sha256:2d2918e86e5327d0661f7083d67a95280b0f7be8f77ed79a8418f81d7d90ce6f --install arm64
# build-static.sh self-verifies per architecture (libwasmvm archives present, pinned
# libgcc checksums match, output is static, no gcc>=12 b-tree unwinder).
- bash scripts/build-static.sh amd64
- bash scripts/build-static.sh arm64
# Boot the binary before packaging: catches crash-at-first-wasm-use defects that
# neither a successful link nor `seid version` can (see scripts/boot-smoke.sh).
- bash scripts/boot-smoke.sh build/seid 4
# neither a successful link nor `seid version` can (see scripts/boot-smoke.sh). Only
# the native architecture can be booted here; the arm64 binary is gated by the
# Linux ARM64 (static) CI job, which runs the same 8-boot gauntlet on arm64 hardware
# against a bit-identical build of the same commit.
- bash scripts/boot-smoke.sh build/seid-amd64 4
# The arm64 binary boots here under emulation, so the artefact that ships is the one
# booted. Emulated timing is not trusted to surface the gcc>=12 unwind b-tree, which
# is why the Linux ARM64 (static) job still runs the full gauntlet on real hardware.
- bash scripts/boot-smoke.sh build/seid-arm64 4

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Emulated boot hits native timeout

High Severity

The new goreleaser hook runs boot-smoke on build/seid-arm64 under qemu binfmt, but boot-smoke still uses a fixed 25s timeout sized for native boots. Emulated wasmer JIT at genesis is typically far slower than native, so a healthy arm64 binary can be killed before Completed ABCI Handshake and fail the entire release before: hook.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit b798983. Configure here.


builds:
# The static seid is built in Alpine (musl-native) by the `before:` hook above
Expand All @@ -29,6 +42,7 @@ builds:
- linux
goarch:
- amd64
- arm64

archives:
- id: seid
Expand Down Expand Up @@ -86,7 +100,8 @@ release:
header: |
## Experimental Pre-built Binaries

Pre-built **statically-linked** `seid` binary attached below for `linux/amd64`.
Pre-built **statically-linked** `seid` binaries attached below for `linux/amd64`
and `linux/arm64`.

Verify your download:

Expand Down
22 changes: 17 additions & 5 deletions scripts/boot-smoke.sh
Original file line number Diff line number Diff line change
Expand Up @@ -24,16 +24,28 @@ BIN=${1:?usage: boot-smoke.sh <path-to-seid> [boots]}
BOOTS=${2:-8}
CHAIN_ID=boot-smoke-1

# Linux-only: this gate runs the linux/amd64 binary natively and uses GNU timeout.
# Skip cleanly on other hosts so local `goreleaser release --snapshot` on macOS still
# works (build-static.sh cross-builds in Docker there, but the ELF can't run on the
# host). The real release path and the CI static-build job both run on Linux and always
# Linux-only: this gate runs the binary natively and uses GNU timeout. Skip cleanly on
# other hosts so local `goreleaser release --snapshot` on macOS still works
# (build-static.sh cross-builds in Docker there, but the ELF can't run on the host).
# The real release path and the CI static-build jobs both run on Linux and always
# execute the gate.
if [ "$(uname -s)" != "Linux" ]; then
echo "boot-smoke: host is $(uname -s), not Linux; skipping (cannot run the linux/amd64 binary natively here)"
echo "boot-smoke: host is $(uname -s), not Linux; skipping (cannot run a linux binary natively here)"
exit 0
fi

# Refuse a binary this host cannot execute. Natively that means a matching
# architecture; with binfmt registered a foreign one runs too, which is how the release
# hook boots the arm64 binary on an amd64 runner. Probing execution covers both, and
# without it the run reaches `seid init`, dies with an exec-format error, and reports
# "did not reach the ABCI handshake", which reads as a crashing binary rather than a
# binary this machine was never able to run.
if ! "$BIN" version >/dev/null 2>&1; then
echo "boot-smoke: ERROR: cannot execute $BIN on $(uname -m)." >&2
echo " For a foreign architecture, register binfmt before running this." >&2
exit 1
fi

for i in $(seq 1 "$BOOTS"); do
H=$(mktemp -d)
"$BIN" init smoke --chain-id "$CHAIN_ID" --home "$H" >/dev/null 2>&1
Expand Down
93 changes: 74 additions & 19 deletions scripts/build-static.sh
Original file line number Diff line number Diff line change
@@ -1,49 +1,104 @@
#!/usr/bin/env bash
# Build the statically-linked linux/amd64 `seid` in Alpine (musl-native).
# Single source of truth for the static build: used by the goreleaser `before:` hook,
# Build a statically-linked `seid` in Alpine (musl-native) for one target architecture.
# Single source of truth for the static build: used by the goreleaser `before:` hooks,
# the cross-arch CI guard, and local runs (`goreleaser release --snapshot`). It also
# self-verifies (required libwasmvm archives present, and the output is actually static)
# so every entry point fails fast rather than producing or shipping a broken binary.
# self-verifies (required libwasmvm archives present, the pinned libgcc matches its
# checksums, the output is actually static and carries no b-tree unwinder) so every
# entry point fails fast rather than producing or shipping a broken binary.
#
# Usage: build-static.sh [amd64|arm64] (default amd64)
#
# Output is build/seid-<arch>, so the two architectures do not overwrite each other.
#
# Ubuntu's musl-gcc can't fully static-link on 24.04 (glibc libgcc needs _dl_find_object,
# absent in musl) and zig cc rejects the -z muldefs flag needed for the libwasmvm
# v152/v155 archives; Alpine's GNU ld + musl links cleanly. --platform linux/amd64 is a
# no-op on amd64 CI and forces the right arch on Apple Silicon for local runs.
# v152/v155 archives; Alpine's GNU ld + musl links cleanly. The pinned golang image
# digest is a multi-arch index, so the same pin serves both targets. Building a
# non-native architecture needs binfmt registered on the host.
#
# The link takes libgcc from third_party/alpine-gcc10-libgcc instead of the build
# The link takes libgcc from third_party/alpine-gcc10-libgcc/<arch> instead of the build
# image's toolchain: gcc >= 12's unwind-frame registry (a lock-free b-tree) corrupts
# under wasmer's JIT frame registration and SIGSEGVs at the genesis wasm store on most
# boots, so the static binary must carry the pre-b-tree registry. See that directory's
# README.md for the full story and provenance. The nm assertion below keeps a toolchain
# upgrade from silently reintroducing the b-tree.
# boots, on both architectures. See that directory's README.md for the full story and
# provenance. The nm assertion below keeps a toolchain upgrade from silently
# reintroducing the b-tree.
set -euo pipefail

ARCH="${1:-amd64}"
case "$ARCH" in
amd64) LIBGCC_ARCH=x86_64 ;;
arm64) LIBGCC_ARCH=aarch64 ;;
*) echo "build-static: unsupported architecture '$ARCH' (want amd64 or arm64)" >&2; exit 1 ;;
esac

# Fail fast if the required static libwasmvm archives are missing.
bash "$(dirname "$0")/check-libwasmvm-static.sh"

LIBGCC_DIR="third_party/alpine-gcc10-libgcc"
LIBGCC_DIR="third_party/alpine-gcc10-libgcc/$LIBGCC_ARCH"
OUT="build/seid-$ARCH"

# The checksums and the -L directory are both derived from $LIBGCC_ARCH above, so a
# build cannot verify one architecture's archives while linking another's.
case "$LIBGCC_ARCH" in
x86_64)
LIBGCC_SHA=d3e066fafde74d53a89d48f2ceb9ed9934249a5d450e281edd22947a829469d8
LIBGCC_EH_SHA=d14c9973a735909e11a863b0c850300bfd3aa683ef4689cbe76a53139766ed79
;;
aarch64)
LIBGCC_SHA=119d1714e0a2b47e1d829d0e92dc3ab51a25e1778f67ed43d2e6c87469573cc2
LIBGCC_EH_SHA=2963a26e62a46ee283c5463ebaad176ddf74b6a049850df833cee5f333ca57a9
;;
esac

docker run --rm --platform linux/amd64 -v "$PWD":/src -w /src golang:1.25.6-alpine@sha256:98e6cffc31ccc44c7c15d83df1d69891efee8115a5bb7ede2bf30a38af3e3c92 sh -c '
echo "build-static: target linux/$ARCH, libgcc pin $LIBGCC_DIR"

docker run --rm --platform "linux/$ARCH" -v "$PWD":/src -w /src golang:1.25.6-alpine@sha256:98e6cffc31ccc44c7c15d83df1d69891efee8115a5bb7ede2bf30a38af3e3c92 sh -c '
set -e
apk add --no-cache build-base git
git config --global --add safe.directory /src
printf "%s %s\n%s %s\n" \
d3e066fafde74d53a89d48f2ceb9ed9934249a5d450e281edd22947a829469d8 '"$LIBGCC_DIR"'/libgcc.a \
d14c9973a735909e11a863b0c850300bfd3aa683ef4689cbe76a53139766ed79 '"$LIBGCC_DIR"'/libgcc_eh.a \
'"$LIBGCC_SHA"' '"$LIBGCC_DIR"'/libgcc.a \
'"$LIBGCC_EH_SHA"' '"$LIBGCC_DIR"'/libgcc_eh.a \
| sha256sum -c -
LINK_STATICALLY=true BUILD_TAGS=muslc LEDGER_ENABLED=false \
STATIC_EXTRA_LDFLAGS="-L/src/'"$LIBGCC_DIR"'" make build
if nm build/seid | grep -q version_lock_lock_exclusive; then
mv build/seid '"$OUT"'
# Materialise the symbol table as its own command so `set -e` aborts when nm fails.
# Reading nm through a pipe into grep reports the grep status, so a broken nm would
# otherwise read as "no b-tree symbols found" and pass.
nm '"$OUT"' > /tmp/seid.syms

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[suggestion] The nm > file split correctly closes the pipe-status hole, but the assertion is still absence-only: if nm succeeds while emitting a short or empty symbol table (stripped output, a future -s -w in STATIC_EXTRA_LDFLAGS, an nm that skips the C objects), grep -q finds nothing and the build reports "pre-b-tree unwinder confirmed" — the same class of failing-open this change set out to fix.

Add a positive control so an unusable symbol table can't pass, e.g. assert a symbol the pinned libgcc must contribute:

  nm '"$OUT"' > /tmp/seid.syms
  grep -q __register_frame_info /tmp/seid.syms || {
    echo "build-static: ERROR: no unwinder symbols in $OUT; the b-tree assertion below would pass vacuously" >&2
    exit 1
  }

# Positive control. The b-tree check below is absence-only, so an empty or truncated
# symbol table would satisfy it vacuously. __register_frame is the entry point wasmer
# calls and every libgcc provides it, pinned or not, so its absence means the symbol
# table is unusable rather than the pin being wrong.
if ! grep -q __register_frame /tmp/seid.syms; then
echo "build-static: ERROR: no unwinder symbols in '"$OUT"'; the b-tree assertion would pass vacuously" >&2
exit 1
fi
if grep -q version_lock_lock_exclusive /tmp/seid.syms; then
echo "build-static: ERROR: binary contains the gcc>=12 unwind b-tree (libgcc pin not applied)" >&2
exit 1
fi
echo "build-static: pre-b-tree unwinder confirmed (no version_lock symbols)"'

# Assert the output really is statically linked, so a regression fails here rather than
# shipping a dynamically-linked binary advertised as static.
info="$(file build/seid)"
# Assert the output really is statically linked and built for the requested architecture,
# so a regression fails here rather than shipping a mislabelled binary.
info="$(file "$OUT")"
echo "$info"
case "$info" in
*"statically linked"*) ;;
*) echo "build-static: ERROR: build/seid is not statically linked" >&2; exit 1 ;;
*) echo "build-static: ERROR: $OUT is not statically linked" >&2; exit 1 ;;
esac
case "$ARCH:$info" in
amd64:*x86-64*|arm64:*aarch64*) ;;
*) echo "build-static: ERROR: $OUT is not a linux/$ARCH binary" >&2; exit 1 ;;
esac

# Record the digest so a CI build and a later release build of the same commit can be
# compared. `apk add build-base` resolves against a live branch index, so the toolchain
# can move underneath a pinned image digest and silently change the output.
if command -v sha256sum >/dev/null 2>&1; then
echo "build-static: sha256 $(sha256sum "$OUT" | cut -d" " -f1) $OUT"
else
echo "build-static: sha256 $(shasum -a 256 "$OUT" | cut -d" " -f1) $OUT"
fi
27 changes: 18 additions & 9 deletions scripts/goreleaser-shim.sh
Original file line number Diff line number Diff line change
Expand Up @@ -10,18 +10,19 @@
# Pro's `prebuilt` builder would do, but on the OSS distribution. Wired via `builds[].tool`
# in .goreleaser.yaml.
#
# The prebuilt binary path can be overridden with $PREBUILT_SEID (defaults to build/seid).
# The prebuilt binary for each architecture is build/seid-<goarch>, as produced by
# scripts/build-static.sh. Override with $PREBUILT_SEID_AMD64 / $PREBUILT_SEID_ARM64.
set -euo pipefail

PREBUILT="${PREBUILT_SEID:-build/seid}"

if [ "${1:-}" = "build" ]; then
# The prebuilt binary is linux/amd64 only. If goreleaser ever requests another arch
# (e.g. once arm64 lands, PLT-757), fail loudly rather than mislabel the amd64 binary.
if [ -n "${GOARCH:-}" ] && [ "$GOARCH" != "amd64" ]; then
echo "goreleaser-shim: only linux/amd64 is prebuilt; refusing to package for GOARCH=$GOARCH." >&2
exit 1
fi
# Resolve the prebuilt binary for the architecture goreleaser asked for. Anything else
# fails loudly rather than mislabelling one architecture's binary as another's.
case "${GOARCH:-}" in
amd64) PREBUILT="${PREBUILT_SEID_AMD64:-build/seid-amd64}" ;;
arm64) PREBUILT="${PREBUILT_SEID_ARM64:-build/seid-arm64}" ;;
*) echo "goreleaser-shim: no prebuilt binary for GOARCH=${GOARCH:-unset}; refusing to package." >&2
exit 1 ;;
esac

# Extract the output path GoReleaser asked us to write the binary to. Handle both
# `-o <path>` (what goreleaser emits) and `-o=<path>`, so the shim is robust either way.
Expand All @@ -40,6 +41,14 @@ if [ "${1:-}" = "build" ]; then
exit 1
fi

# A mislabelled archive is worse than a failed release, so check the ELF machine of the
# resolved binary rather than trusting its filename.
info="$(file -b "$PREBUILT")"
case "$GOARCH:$info" in
amd64:*x86-64*|arm64:*aarch64*) ;;
*) echo "goreleaser-shim: $PREBUILT is not a $GOARCH binary ($info)." >&2; exit 1 ;;
esac

mkdir -p "$(dirname "$out")"
cp "$PREBUILT" "$out"
echo "goreleaser-shim: packaged prebuilt $PREBUILT into $out" >&2
Expand Down
Loading
Loading