fix(build): drop compile-time x86 target-features — runtime AES dispatch

Users on CPUs without AES-NI (Nehalem i7-860, pre-2010 Intel Macs) or
without SSSE3 (AMD Phenom II) crashed at launch with SIGILL. The
global `-C target-feature=+aes,+ssse3` was kept after the #353/#343
x86-64-v3 incident on the belief that the aes crate's cpufeatures
runtime check would protect CPUs without AES-NI — but a compile-time
target feature makes cpufeatures compile its check to a constant, so
the AES-NI intrinsics ran unconditionally (credentials AES-GCM decrypt
at startup), and +ssse3 let LLVM autovectorize anywhere, breaking
Phenom II. Measured on the qbz-cmaf hot path (Ctr128BE<Aes128>
apply_keystream, 256 MB, release): 2145 MB/s compiled-in vs 2162 MB/s
runtime-dispatched, identical output — the flags bought nothing on
capable CPUs. Removed from .cargo/config.toml, all dev/build scripts
and the release/build workflows; old CPUs now get the software AES
fallback instead of a crash.

Fixes #549

cc
This commit is contained in:
vicrodh committed 2026-07-05 14:17:46 -06:00
1 parent 444b21f0c1
commit 395e537b59
9 files changed
+104 -68

No files matched your search

+22 -39
View File
@@ -1,42 +1,25 @@
# Enable CPU intrinsics for AES-NI + SSSE3 on x86_64 targets only. # NO global x86_64 target-feature flags — baseline x86-64 on purpose.
# #
# The qbz-cmaf crate does AES-128-CTR decrypt on every offline-cached # History (#353 -> #343 -> #549): the binary once shipped with
# audio frame. Without AES-NI the decrypt falls back to a software # `target-cpu=x86-64-v3`, which SIGILLed every pre-Haswell CPU at launch.
# implementation that hits ~10 MB/s on a modern CPU; with AES-NI it # That was reduced to `target-feature=+aes,+ssse3` on the belief that the
# clears 400+ MB/s. For HiRes (200 MB+ tracks) this is the difference # `aes` crate's cpufeatures runtime check would keep CPUs without AES-NI on
# between a 20-second gap and a sub-second one. # the software path. That belief was WRONG: enabling a target feature at
# COMPILE time makes cpufeatures compile its check to a constant `true`, so
# the AES-NI intrinsics run unconditionally — every CPU without AES-NI
# (Nehalem/Lynnfield i7-860, pre-2010 Intel Macs, AMD pre-Bulldozer) died
# with SIGILL the moment AES ran (at LAUNCH: qbz-credentials decrypts with
# AES-GCM during startup). `+ssse3` additionally let LLVM autovectorize
# SSSE3 anywhere in the binary, killing AMD Phenom II (no SSSE3 until
# Bulldozer, 2011). That is issue #549.
# #
# What we need is ONLY the AES-NI instruction set compiled in. The # The flags buy NOTHING on capable CPUs: with no compile-time forcing, the
# `aes` crate uses `cpufeatures::new!` to do a runtime CPUID check # `aes` crate does per-function `#[target_feature]` dispatch after a runtime
# before dispatching to the hardware path, so as long as the AES-NI # CPUID check. Measured on the qbz-cmaf hot path (ctr::Ctr128BE<Aes128>
# code is present in the binary, CPUs without AES-NI fall back to the # apply_keystream, 256 MB, release): 2145 MB/s compiled-in vs 2162 MB/s
# software implementation gracefully — no crash, just slower decrypt. # runtime-dispatched — identical. Old CPUs get the software fallback
# (slower offline-cache decrypt) instead of a crash.
# #
# `target-feature=+aes,+ssse3` is what enables that compile-in. # If a future hot loop ever warrants AVX2/BMI2/etc., use per-function
# SSSE3 has been in every x86_64 CPU since 2006 (Core 2). # `#[target_feature]` + `is_x86_feature_detected!` dispatch — never a
# # global feature/microarch bump.
# We deliberately do NOT set `target-cpu=x86-64-v3` (Haswell baseline,
# 2013). v3 includes AVX2/BMI2/FMA/LZCNT/MOVBE and LLVM will freely
# emit those instructions anywhere in the binary — including the
# startup path, long before CMAF runs. On pre-Haswell CPUs (Sandy
# Bridge 2011, Ivy Bridge 2012, AMD Bulldozer/Piledriver) the process
# dies with SIGILL at launch. See issue #353 (Linux, Sandy Bridge) and
# discussion #343 (macOS, 2012 MacBookPro9,1 / Ivy Bridge). Raising
# the baseline gave no CMAF speedup — the perf win came entirely from
# `+aes` — so we keep the AES-NI path and drop the incompatible
# baseline.
#
# If a future hot loop ever warrants AVX2/BMI2, the correct pattern is
# per-function `#[target_feature]` with runtime dispatch (as the `aes`
# crate already does via `cpufeatures`), NOT a global microarch bump
# that silently breaks older hardware.
#
# IMPORTANT: these flags MUST be scoped to x86_64 targets. Using
# `[build] rustflags = ...` applies them to every target, which on
# aarch64-apple-darwin (and any other ARM target) corrupts the CPU
# feature inference inside the `ring` crate and fails the build with
# `assertion failed: CAPS_STATIC == MIN_STATIC_FEATURES`. ARM targets
# get their own AES/crypto extensions automatically via `aes` crate's
# armv8 path — we don't need to force anything there.
[target.'cfg(target_arch = "x86_64")']
rustflags = ["-C", "target-feature=+aes,+ssse3"]
+1 -1
View File
@@ -101,7 +101,7 @@ jobs:
CARGO_BUILD_JOBS: '1' CARGO_BUILD_JOBS: '1'
CARGO_PROFILE_RELEASE_CODEGEN_UNITS: '256' CARGO_PROFILE_RELEASE_CODEGEN_UNITS: '256'
CARGO_PROFILE_RELEASE_OPT_LEVEL: '2' CARGO_PROFILE_RELEASE_OPT_LEVEL: '2'
RUSTFLAGS: '-C target-feature=+aes,+ssse3 -C link-arg=-fuse-ld=mold -Z threads=1' RUSTFLAGS: '-C link-arg=-fuse-ld=mold -Z threads=1'
run: | run: |
cargo build --release --manifest-path crates/Cargo.toml -p qbz cargo build --release --manifest-path crates/Cargo.toml -p qbz
echo "::group::result"; ls -lh crates/target/release/qbz; df -h /; free -h; echo "::endgroup::" echo "::group::result"; ls -lh crates/target/release/qbz; df -h /; free -h; echo "::endgroup::"
+1 -1
View File
@@ -150,7 +150,7 @@ jobs:
# Keep symbols in this build: the unstripped binary is archived for # Keep symbols in this build: the unstripped binary is archived for
# crash symbolication, then stripped for every shipped artifact. # crash symbolication, then stripped for every shipped artifact.
CARGO_PROFILE_RELEASE_STRIP: "none" CARGO_PROFILE_RELEASE_STRIP: "none"
RUSTFLAGS: "-C target-feature=+aes,+ssse3 -C link-arg=-fuse-ld=mold -Z threads=1" RUSTFLAGS: "-C link-arg=-fuse-ld=mold -Z threads=1"
run: | run: |
cargo build --release --manifest-path crates/Cargo.toml -p qbz cargo build --release --manifest-path crates/Cargo.toml -p qbz
cp crates/target/release/qbz qbz-unstripped cp crates/target/release/qbz qbz-unstripped
+1 -1
View File
@@ -91,7 +91,7 @@ jobs:
CARGO_BUILD_JOBS: '1' CARGO_BUILD_JOBS: '1'
CARGO_PROFILE_RELEASE_CODEGEN_UNITS: '256' CARGO_PROFILE_RELEASE_CODEGEN_UNITS: '256'
CARGO_PROFILE_RELEASE_OPT_LEVEL: '2' CARGO_PROFILE_RELEASE_OPT_LEVEL: '2'
RUSTFLAGS: '-C target-feature=+aes,+ssse3 -C link-arg=-fuse-ld=mold -Z threads=1' RUSTFLAGS: '-C link-arg=-fuse-ld=mold -Z threads=1'
run: | run: |
cargo build --release --manifest-path crates/Cargo.toml -p qbz cargo build --release --manifest-path crates/Cargo.toml -p qbz
strip crates/target/release/qbz || true strip crates/target/release/qbz || true
+63
View File
@@ -0,0 +1,63 @@
#!/usr/bin/env bash
# QBZ — prune stale incremental-compilation caches under crates/target.
#
# WHY: `cargo check` writes an incremental cache dir per crate, keyed by a hash
# of the build config (RUSTFLAGS / codegen-units / opt-level). Because
# slint-run.sh flips those knobs per RAM tier, EVERY distinct config spawns a
# NEW `<crate>-<hash>/` dir and Cargo NEVER deletes the old ones. Over months
# they pile up (1000+ dirs, hundreds of GB of pure garbage).
#
# This keeps only the MOST-RECENTLY-MODIFIED incremental dir per crate (that's
# the one that actually speeds up your next check) and deletes the rest.
#
# NOTE: release builds (slint-run.sh / slint-dev.sh) do NOT use incremental at
# all, so this never slows them down. It only affects `cargo check` speed, and
# only prunes stale snapshots you're not using.
#
# Usage:
# ./scripts/prune-incremental.sh # prune, keep newest per crate
# DRY=1 ./scripts/prune-incremental.sh # show what WOULD be deleted
# KEEP=2 ./scripts/prune-incremental.sh # keep the 2 newest per crate
set -euo pipefail
cd "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/.."
TARGET="crates/target"
KEEP="${KEEP:-1}"
DRY="${DRY:-0}"
freed=0
for incdir in "$TARGET"/*/incremental; do
[[ -d "$incdir" ]] || continue
declare -A seen=() # reset per profile (debug/, release/, etc.)
# Feed "<mtime>\t<crate>\t<path>" sorted by crate asc, then mtime desc, so the
# newest dir of each crate arrives first.
while IFS=$'\t' read -r _mtime crate path; do
[[ -n "$path" ]] || continue
seen["$crate"]=$(( ${seen["$crate"]:-0} + 1 ))
(( seen["$crate"] <= KEEP )) && continue # keep the newest KEEP per crate
sz=$(du -sm "$path" 2>/dev/null | cut -f1)
freed=$(( freed + ${sz:-0} ))
if [[ "$DRY" == 1 ]]; then
echo "would delete: $path (${sz}M)"
else
rm -rf "$path"
echo "deleted: $path (${sz}M)"
fi
done < <(
for d in "$incdir"/*/; do
[[ -d "$d" ]] || continue
name=$(basename "$d")
printf '%s\t%s\t%s\n' "$(stat -c %Y "$d")" "${name%-*}" "${d%/}"
done | sort -t$'\t' -k2,2 -k1,1rn
)
unset seen
done
if [[ "$DRY" == 1 ]]; then
echo "--- DRY run: would free ~${freed} MB (KEEP=$KEEP) ---"
else
echo "--- freed ~${freed} MB (KEEP=$KEEP) ---"
fi
+1 -4
View File
@@ -15,12 +15,9 @@
# DO NOT use this to judge performance — the binary is unoptimised. Always do a # DO NOT use this to judge performance — the binary is unoptimised. Always do a
# final pass with ./slint-dev.sh (release) before trusting runtime behaviour. # final pass with ./slint-dev.sh (release) before trusting runtime behaviour.
# #
# RUSTFLAGS overrides .cargo/config.toml, so the AES-NI/SSSE3 features are
# re-listed here (keep in sync with slint-dev.sh / .cargo/config.toml).
#
# Usage: ./scripts/slint-dev-fast.sh [extra cargo args] # Usage: ./scripts/slint-dev-fast.sh [extra cargo args]
# THREADS=22 ./scripts/slint-dev-fast.sh # override frontend threads # THREADS=22 ./scripts/slint-dev-fast.sh # override frontend threads
set -euo pipefail set -euo pipefail
cd "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/.." cd "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/.."
export RUSTFLAGS="-C target-feature=+aes,+ssse3 -C link-arg=-fuse-ld=mold -C debuginfo=0 -Z threads=${THREADS:-16}" export RUSTFLAGS="-C link-arg=-fuse-ld=mold -C debuginfo=0 -Z threads=${THREADS:-16}"
exec cargo +nightly run --manifest-path crates/Cargo.toml -p qbz "$@" exec cargo +nightly run --manifest-path crates/Cargo.toml -p qbz "$@"
+7 -14
View File
@@ -11,11 +11,10 @@
# link (libc's build script) fails. Apple's default linker (ld-prime) is # link (libc's build script) fails. Apple's default linker (ld-prime) is
# already fast, so we simply don't override the linker. # already fast, so we simply don't override the linker.
# #
# 2. AES-NI/SSSE3 features are applied ONLY on x86_64. On Intel Macs they keep # 2. NO x86 target-features anywhere (#549): the aes crate runtime-dispatches
# the offline CMAF decrypt fast (matches .cargo/config.toml); on aarch64 # to AES-NI at identical speed, and compile-time features SIGILL CPUs
# they are meaningless (rustc warns "not a recognized feature" and ignores # without them (pre-2010 Intel Macs). aarch64 gets its AES from the armv8
# them — AES there comes from the armv8 crypto extensions automatically), # crypto extensions automatically.
# so we omit them to keep the build output clean.
# #
# 3. The nightly parallel frontend (`-Z threads`) is kept — it works on macOS # 3. The nightly parallel frontend (`-Z threads`) is kept — it works on macOS
# and only cuts COMPILE time; the produced binary is identical to a plain # and only cuts COMPILE time; the produced binary is identical to a plain
@@ -41,19 +40,13 @@ if [[ "${1:-}" == "--fast" ]]; then
shift shift
fi fi
# Re-list AES-NI/SSSE3 ONLY on x86_64. Setting RUSTFLAGS *overrides* (does not
# merge) .cargo/config.toml's [target.'cfg(target_arch = "x86_64")'] rustflags,
# so on an Intel Mac we must restate them here to keep the AES-NI decrypt path.
AES_FLAGS=""
if [[ "$(uname -m)" == "x86_64" ]]; then
AES_FLAGS="-C target-feature=+aes,+ssse3"
fi
# Parallel rustc frontend across cores. Defaults to the machine's logical CPU # Parallel rustc frontend across cores. Defaults to the machine's logical CPU
# count; override with THREADS=N. This only speeds compilation. # count; override with THREADS=N. This only speeds compilation.
THREADS="${THREADS:-$(sysctl -n hw.ncpu)}" THREADS="${THREADS:-$(sysctl -n hw.ncpu)}"
export RUSTFLAGS="${AES_FLAGS} -Z threads=${THREADS}" # No x86 target-features (#549): the aes crate runtime-dispatches to AES-NI
# at identical speed, and compile-time features SIGILL pre-2010 Intel Macs.
export RUSTFLAGS="-Z threads=${THREADS}"
if [[ "$FAST" == true ]]; then if [[ "$FAST" == true ]]; then
# DEBUG: opt-level 0 skips the heavy LLVM passes; -C debuginfo=0 means less to # DEBUG: opt-level 0 skips the heavy LLVM passes; -C debuginfo=0 means less to
+4 -5
View File
@@ -10,10 +10,9 @@
# only compile time improves. We deliberately do NOT use cranelift — it # only compile time improves. We deliberately do NOT use cranelift — it
# would lower runtime performance. # would lower runtime performance.
# #
# RUSTFLAGS must re-list the AES-NI/SSSE3 features from .cargo/config.toml, # No x86 target-features here or in .cargo/config.toml (#549): the aes crate
# because setting RUSTFLAGS *overrides* (does not merge) the config's # runtime-dispatches to AES-NI at identical speed, and compile-time features
# [target.'cfg(target_arch = "x86_64")'] rustflags. Keep these in sync with # SIGILL older CPUs.
# .cargo/config.toml (the AES-NI path keeps offline CMAF decrypt fast).
# #
# Usage: ./scripts/slint-dev.sh [extra cargo args] # Usage: ./scripts/slint-dev.sh [extra cargo args]
# THREADS=16 ./scripts/slint-dev.sh # override frontend threads # THREADS=16 ./scripts/slint-dev.sh # override frontend threads
@@ -21,5 +20,5 @@ set -euo pipefail
cd "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/.." cd "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/.."
# `mold` (faster linker) + more frontend threads only cut COMPILE time — they # `mold` (faster linker) + more frontend threads only cut COMPILE time — they
# don't change the optimised binary, so release perf measurements stay valid. # don't change the optimised binary, so release perf measurements stay valid.
export RUSTFLAGS="-C target-feature=+aes,+ssse3 -C link-arg=-fuse-ld=mold -Z threads=${THREADS:-16}" export RUSTFLAGS="-C link-arg=-fuse-ld=mold -Z threads=${THREADS:-16}"
exec cargo +nightly run --release --manifest-path crates/Cargo.toml -p qbz "$@" exec cargo +nightly run --release --manifest-path crates/Cargo.toml -p qbz "$@"
+4 -3
View File
@@ -51,9 +51,10 @@ else
echo "[slint-run] WARNING: only ${avail_mb} MB free — lowest-memory tier (slow). Close apps / drop to a TTY for a faster build." >&2 echo "[slint-run] WARNING: only ${avail_mb} MB free — lowest-memory tier (slow). Close apps / drop to a TTY for a faster build." >&2
fi fi
# RUSTFLAGS OVERRIDES (does not merge) the .cargo/config.toml rustflags, so the # No x86 target-features here or in .cargo/config.toml (#549): the aes crate
# AES-NI/SSSE3 features must be re-listed (keep in sync with slint-dev.sh). # runtime-dispatches to AES-NI at identical speed, and compile-time features
export RUSTFLAGS="-C target-feature=+aes,+ssse3 -C link-arg=-fuse-ld=mold -Z threads=${THREADS}" # SIGILL older CPUs.
export RUSTFLAGS="-C link-arg=-fuse-ld=mold -Z threads=${THREADS}"
export CARGO_PROFILE_RELEASE_CODEGEN_UNITS="${CODEGEN_UNITS}" export CARGO_PROFILE_RELEASE_CODEGEN_UNITS="${CODEGEN_UNITS}"
export CARGO_PROFILE_RELEASE_OPT_LEVEL="${OPT}" export CARGO_PROFILE_RELEASE_OPT_LEVEL="${OPT}"
export CARGO_BUILD_JOBS="${CARGO_BUILD_JOBS:-1}" # one rustc at a time (memory) export CARGO_BUILD_JOBS="${CARGO_BUILD_JOBS:-1}" # one rustc at a time (memory)