[BUG] Upgrade zips are built without BLST_PORTABLE: SIGILL in blst_cgo_init on x86-64 CPUs without ADX #1949
Summary
The amd64 upgrade binaries published for release/v0.2.16-post1 (inferenced-amd64.zip, decentralized-api-amd64.zip, edge-api-amd64.zip) are built with blst in its default, non-portable mode. On x86-64 CPUs without the ADX extension they exit at startup:
Our full node runs on an Intel Xeon E5-1650 (Sandy Bridge). Before the upgrade it ran the inferenced from the ghcr.io/product-science/inferenced:0.2.15 image, which is built with -D__BLST_PORTABLE__. At the v0.2.16 upgrade height (proposal #112, block 6,449,400) cosmovisor switched to the binary from the upgrade zip, and that binary crashed in a restart loop. The node was down for ~7 hours, until we rebuilt the same tag with BLST_PORTABLE=1.
Motivation
Every upgrade zip goes through the same build path, so affected nodes will go down the same way at every future upgrade. Nothing fails before the upgrade height, so operators find out only after the node is down. The fix is a few lines in the build files.
Impact
- Who is affected: node operators (hosts/validators as well as full, RPC and sentry nodes) that get upgrade binaries through cosmovisor auto-download (the image sets
DAEMON_ALLOW_DOWNLOAD_BINARIES=true) on x86-64 CPUs without ADX: - Intel before Broadwell, e.g. Xeon E3/E5 v1–v3, which are common on budget dedicated servers;
- AMD before Zen;
- VMs whose CPU model hides ADX, e.g.
qemu64orkvm64. - Is effect network-wide or limited: limited. Each affected participant loses availability at every upgrade.
- Likelihood: organic. The crash is deterministic on every affected machine at every chain upgrade.
- Severity: Medium (Medium impact × Medium likelihood).
- Affected components:
inference-chain/Makefile,decentralized-api/Makefile,.github/workflows/publish_upgrade_binaries.yml, and the amd64 upgrade zips of every release.
Detailed description
Root cause
- On amd64 the blst Go binding is compiled with
-D__ADX__(#cgo amd64 CFLAGS: -D__ADX__ -mno-avx). Its README says: "If the test or target application crashes with an "illegal instruction" exception [after copying to an older system], rebuild withCGO_CFLAGSenvironment variable set to-O2 -D__BLST_PORTABLE__." - The Dockerfiles add
-D__BLST_PORTABLE__only whenBLST_PORTABLE=1, and the default isARG BLST_PORTABLE=0.scripts/blst-portable.mksets it to1only on Apple Silicon hosts; this mechanism came from #697 for macOS builds. DOCKER_BUILD(images) forwards--build-arg BLST_PORTABLE, butDOCKER_BUILD_UPGRADEininference-chain/Makefileanddecentralized-api/Makefiledoes not. Somake build-for-upgrade BLST_PORTABLE=1printsBLST_PORTABLE: 1and still produces a non-portable binary.edge-api/Makefiledoes forward the flag.publish_upgrade_binaries.ymlrunsmake … build-for-upgradeonubuntu-latestwithoutBLST_PORTABLE, so every published upgrade binary is non-portable.
https://github.com/gonka-ai/gonka/blob/136041c81ea8ff38e7620d76af66a7c7fe7eec50/inference-chain/Makefile#L115-L141
https://github.com/gonka-ai/gonka/blob/136041c81ea8ff38e7620d76af66a7c7fe7eec50/decentralized-api/Makefile#L55-L70
https://github.com/gonka-ai/gonka/blob/136041c81ea8ff38e7620d76af66a7c7fe7eec50/.github/workflows/publish_upgrade_binaries.yml#L147-L158
Reproduction (any x86-64 machine with Docker)
mkdir v0216 && cd v0216
curl -fsSLO 'https://github.com/gonka-ai/gonka/releases/download/release%2Fv0.2.16-post1/inferenced-amd64.zip'
unzip -q inferenced-amd64.zip && chmod +x inferenced
docker run --rm -v "$PWD:/x:ro" alpine:3.21 sh -c \
'apk add -q qemu-x86_64 && qemu-x86_64 -cpu SandyBridge /x/inferenced version'
# Caught SIGILL in blst_cgo_init, consult <blst>/bindings/go/README.md. (exit code 132)
With -cpu Broadwell-noTSX (has ADX) the same binary prints v0.2.16-post1.
Evidence
CGO_CFLAGS recorded in the build info (go version -m):
| Binary | CGO_CFLAGS |
|---|---|
inferenced in inferenced-amd64.zip |
-I/lib -O2 |
decentralized-api and inferenced in decentralized-api-amd64.zip |
-I/lib -O2 |
edge-api in edge-api-amd64.zip |
-I/lib -O2 |
/usr/bin/inferenced in ghcr.io/product-science/inferenced:0.2.15 |
-I/lib -O2 -D__BLST_PORTABLE__ |
Startup under qemu-x86_64 -cpu <model>:
| CPU model | binaries from the three v0.2.16-post1 zips | inferenced rebuilt with BLST_PORTABLE=1 |
|---|---|---|
SandyBridge |
SIGILL | OK |
Haswell-noTSX |
SIGILL | OK |
qemu64 |
SIGILL | OK |
Broadwell-noTSX |
OK | OK |
The rebuilt inferenced comes from the same tag through the same Dockerfile and LDFLAGS. Its build info differs from the official binary only in CGO_CFLAGS (-D__BLST_PORTABLE__), and two independent rebuilds produced the same sha256. Our node has been running v0.2.16 on it since.
Proposed fix
- Add
--build-arg BLST_PORTABLE=$(BLST_PORTABLE)toDOCKER_BUILD_UPGRADEininference-chain/Makefileanddecentralized-api/Makefile, the same wayDOCKER_BUILDandedge-api/Makefilealready do. - Pass
BLST_PORTABLE=1to the threemake … build-for-upgradecalls inpublish_upgrade_binaries.yml, or make portable the default for linux/amd64 release builds. - Optionally, add a release check: fail if
go version -mlacks-D__BLST_PORTABLE__, or smoke-run the binary underqemu-x86_64 -cpu SandyBridge … version.
The cost on modern CPUs should be negligible. In portable mode blst detects ADX once at startup (__blst_platform_cap), and each generic routine starts with testl $1,__blst_platform_cap(%rip); jnz <mulx/adx variant>, so CPUs with ADX keep the fast path.
A PR with (1) and (2) follows.
Workaround for affected operators
Before the upgrade height, build the release tag with BLST_PORTABLE=1 and put the binary at $DAEMON_HOME/cosmovisor/upgrades/<plan name>/bin/inferenced. If an executable is already there, cosmovisor switches to it and downloads nothing.
🔄 Auto-synced from Issue #1949 every hour.